1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
16
17
18
19
20
21 package schema
22
23 import (
24 "bytes"
25 "context"
26 "crypto/rand"
27 "encoding/base64"
28 "encoding/json"
29 "errors"
30 "fmt"
31 "hash"
32 "io"
33 "log"
34 "os"
35 "regexp"
36 "strconv"
37 "strings"
38 "sync"
39 "time"
40 "unicode/utf8"
41
42 "github.com/bradfitz/latlong"
43 "perkeep.org/internal/pools"
44 "perkeep.org/pkg/blob"
45
46 "github.com/rwcarlsen/goexif/exif"
47 "github.com/rwcarlsen/goexif/tiff"
48 "go4.org/strutil"
49 "go4.org/types"
50 )
51
52 func init() {
53
54
55 strutil.RegisterCommonString(
56 "bytes",
57 "claim",
58 "directory",
59 "file",
60 "permanode",
61 "share",
62 "static-set",
63 "symlink",
64 )
65 }
66
67
68
69 const MaxSchemaBlobSize = 1 << 20
70
71 var (
72 ErrNoCamliVersion = errors.New("schema: no camliVersion key in map")
73 )
74
75 var clockNow = time.Now
76
77 type StatHasher interface {
78 Lstat(fileName string) (os.FileInfo, error)
79 Hash(fileName string) (blob.Ref, error)
80 }
81
82
83
84 type File interface {
85 io.Closer
86 io.ReaderAt
87 io.Reader
88 Size() int64
89 }
90
91
92 type Directory interface {
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107 Readdir(ctx context.Context, n int) ([]DirectoryEntry, error)
108 }
109
110 type Symlink interface {
111
112 }
113
114
115 type FIFO interface {
116
117 }
118
119
120 type Socket interface {
121
122 }
123
124
125
126 type DirectoryEntry interface {
127
128
129
130 CamliType() CamliType
131
132 FileName() string
133 BlobRef() blob.Ref
134
135 File(ctx context.Context) (File, error)
136 Directory(ctx context.Context) (Directory, error)
137 Symlink() (Symlink, error)
138 FIFO() (FIFO, error)
139 Socket() (Socket, error)
140 }
141
142
143 type dirEntry struct {
144 ss superset
145 fetcher blob.Fetcher
146 fr *FileReader
147 dr *DirReader
148 }
149
150
151
152 type SearchQuery interface{}
153
154 func (de *dirEntry) CamliType() CamliType {
155 return de.ss.Type
156 }
157
158 func (de *dirEntry) FileName() string {
159 return de.ss.FileNameString()
160 }
161
162 func (de *dirEntry) BlobRef() blob.Ref {
163 return de.ss.BlobRef
164 }
165
166 func (de *dirEntry) File(ctx context.Context) (File, error) {
167 if de.fr == nil {
168 if de.ss.Type != TypeFile {
169 return nil, fmt.Errorf("DirectoryEntry is camliType %q, not %q", de.ss.Type, TypeFile)
170 }
171 fr, err := NewFileReader(ctx, de.fetcher, de.ss.BlobRef)
172 if err != nil {
173 return nil, err
174 }
175 de.fr = fr
176 }
177 return de.fr, nil
178 }
179
180 func (de *dirEntry) Directory(ctx context.Context) (Directory, error) {
181 if de.dr == nil {
182 if de.ss.Type != TypeDirectory {
183 return nil, fmt.Errorf("DirectoryEntry is camliType %q, not %q", de.ss.Type, TypeDirectory)
184 }
185 dr, err := NewDirReader(ctx, de.fetcher, de.ss.BlobRef)
186 if err != nil {
187 return nil, err
188 }
189 de.dr = dr
190 }
191 return de.dr, nil
192 }
193
194 func (de *dirEntry) Symlink() (Symlink, error) {
195 return 0, errors.New("TODO: Symlink not implemented")
196 }
197
198 func (de *dirEntry) FIFO() (FIFO, error) {
199 return 0, errors.New("TODO: FIFO not implemented")
200 }
201
202 func (de *dirEntry) Socket() (Socket, error) {
203 return 0, errors.New("TODO: Socket not implemented")
204 }
205
206
207
208
209
210 func newDirectoryEntry(fetcher blob.Fetcher, ss *superset) (DirectoryEntry, error) {
211 if ss == nil {
212 return nil, errors.New("ss was nil")
213 }
214 if !ss.BlobRef.Valid() {
215 return nil, errors.New("ss.BlobRef was invalid")
216 }
217 switch ss.Type {
218 case TypeFile, TypeDirectory, TypeSymlink, TypeFIFO, TypeSocket:
219
220 default:
221 return nil, fmt.Errorf("invalid DirectoryEntry camliType of %q", ss.Type)
222 }
223 de := &dirEntry{ss: *ss, fetcher: fetcher}
224 return de, nil
225 }
226
227
228
229
230
231
232
233 func NewDirectoryEntryFromBlobRef(ctx context.Context, fetcher blob.Fetcher, blobRef blob.Ref) (DirectoryEntry, error) {
234 ss := new(superset)
235 err := ss.setFromBlobRef(ctx, fetcher, blobRef)
236 if err != nil {
237 return nil, fmt.Errorf("schema/filereader: can't fill superset: %w", err)
238 }
239 return newDirectoryEntry(fetcher, ss)
240 }
241
242
243
244
245 type superset struct {
246
247
248 BlobRef blob.Ref
249
250 Version int `json:"camliVersion"`
251 Type CamliType `json:"camliType"`
252
253 Signer blob.Ref `json:"camliSigner"`
254 Sig string `json:"camliSig"`
255
256 ClaimType string `json:"claimType"`
257 ClaimDate types.Time3339 `json:"claimDate"`
258
259 Permanode blob.Ref `json:"permaNode"`
260 Attribute string `json:"attribute"`
261 Value string `json:"value"`
262
263
264
265
266
267 FileName string `json:"fileName"`
268 FileNameBytes []interface{} `json:"fileNameBytes"`
269
270 SymlinkTarget string `json:"symlinkTarget"`
271 SymlinkTargetBytes []interface{} `json:"symlinkTargetBytes"`
272
273 UnixPermission string `json:"unixPermission"`
274 UnixOwnerId int `json:"unixOwnerId"`
275 UnixOwner string `json:"unixOwner"`
276 UnixGroupId int `json:"unixGroupId"`
277 UnixGroup string `json:"unixGroup"`
278 UnixMtime string `json:"unixMtime"`
279 UnixCtime string `json:"unixCtime"`
280 UnixAtime string `json:"unixAtime"`
281
282
283
284 Parts []*BytesPart `json:"parts"`
285
286 Entries blob.Ref `json:"entries"`
287 Members []blob.Ref `json:"members"`
288 MergeSets []blob.Ref `json:"mergeSets"`
289
290
291 Search SearchQuery `json:"search"`
292
293
294 Target blob.Ref `json:"target"`
295
296 Transitive bool `json:"transitive"`
297
298
299
300 AuthType string `json:"authType"`
301 Expires types.Time3339 `json:"expires"`
302 }
303
304 var errSchemaBlobTooLarge = errors.New("schema blob too large")
305
306 func parseSuperset(r io.Reader) (*superset, error) {
307 buf := pools.BytesBuffer()
308 defer pools.PutBuffer(buf)
309
310 n, err := io.CopyN(buf, r, MaxSchemaBlobSize+1)
311 if err != nil && err != io.EOF {
312 return nil, err
313 }
314 if n > MaxSchemaBlobSize {
315 return nil, errSchemaBlobTooLarge
316 }
317
318 ss := new(superset)
319 if err := json.Unmarshal(buf.Bytes(), ss); err != nil {
320 return nil, err
321 }
322 return ss, nil
323 }
324
325
326
327
328 func BlobFromReader(ref blob.Ref, r io.Reader) (*Blob, error) {
329 if !ref.Valid() {
330 return nil, errors.New("schema.BlobFromReader: invalid blobref")
331 }
332 var buf bytes.Buffer
333 tee := io.TeeReader(r, &buf)
334 ss, err := parseSuperset(tee)
335 if err != nil {
336 return nil, fmt.Errorf("error parsing Blob %v: %w", ref, err)
337 }
338 return &Blob{ref, buf.String(), ss}, nil
339 }
340
341
342
343
344
345 type BytesPart struct {
346
347 Size uint64 `json:"size"`
348
349
350
351
352
353 BlobRef blob.Ref `json:"blobRef,omitempty"`
354 BytesRef blob.Ref `json:"bytesRef,omitempty"`
355
356
357
358 Offset uint64 `json:"offset,omitempty"`
359 }
360
361
362
363
364
365
366 func stringFromMixedArray(parts []interface{}) string {
367 var buf bytes.Buffer
368 for _, part := range parts {
369 if s, ok := part.(string); ok {
370 buf.WriteString(s)
371 continue
372 }
373 if num, ok := part.(float64); ok {
374 buf.WriteByte(byte(num))
375 continue
376 }
377 }
378 return buf.String()
379 }
380
381
382
383
384 func mixedArrayFromString(s string) (parts []interface{}) {
385 for len(s) > 0 {
386 if n := utf8StrLen(s); n > 0 {
387 parts = append(parts, s[:n])
388 s = s[n:]
389 } else {
390 parts = append(parts, s[0])
391 s = s[1:]
392 }
393 }
394 return parts
395 }
396
397
398 func utf8StrLen(s string) int {
399 for i, r := range s {
400 for r == utf8.RuneError {
401
402
403
404
405 _, size := utf8.DecodeRuneInString(s[i:])
406 if size == 1 {
407 return i
408 }
409 }
410 }
411 return len(s)
412 }
413
414 func (ss *superset) SumPartsSize() (size uint64) {
415 for _, part := range ss.Parts {
416 size += uint64(part.Size)
417 }
418 return size
419 }
420
421 func (ss *superset) SymlinkTargetString() string {
422 if ss.SymlinkTarget != "" {
423 return ss.SymlinkTarget
424 }
425 return stringFromMixedArray(ss.SymlinkTargetBytes)
426 }
427
428
429
430
431
432 func (ss *superset) FileNameString() string {
433 v := ss.FileName
434 if v == "" {
435 v = stringFromMixedArray(ss.FileNameBytes)
436 }
437 if v != "" {
438 if strings.Contains(v, "/") {
439
440 return ""
441 }
442 if strings.Contains(v, "\\") {
443
444 return ""
445 }
446 }
447 return v
448 }
449
450 func (ss *superset) HasFilename(name string) bool {
451 return ss.FileNameString() == name
452 }
453
454 func (b *Blob) FileMode() os.FileMode {
455
456 return b.ss.FileMode()
457 }
458
459 func (ss *superset) FileMode() os.FileMode {
460 var mode os.FileMode
461 hasPerm := ss.UnixPermission != ""
462 if hasPerm {
463 m64, err := strconv.ParseUint(ss.UnixPermission, 8, 64)
464 if err == nil {
465 mode = mode | os.FileMode(m64)
466 }
467 }
468
469
470 switch ss.Type {
471 case TypeDirectory:
472 mode = mode | os.ModeDir
473 case TypeFile:
474
475 case TypeSymlink:
476 mode = mode | os.ModeSymlink
477 case TypeFIFO:
478 mode = mode | os.ModeNamedPipe
479 case TypeSocket:
480 mode = mode | os.ModeSocket
481 }
482 if !hasPerm {
483 switch ss.Type {
484 case TypeDirectory:
485 mode |= 0755
486 default:
487 mode |= 0644
488 }
489 }
490 return mode
491 }
492
493
494
495
496 func (b *Blob) MapUid() int { return b.ss.MapUid() }
497
498
499
500
501 func (b *Blob) MapGid() int { return b.ss.MapGid() }
502
503 func (ss *superset) MapUid() int {
504 if ss.UnixOwner != "" {
505 uid, ok := getUidFromName(ss.UnixOwner)
506 if ok {
507 return uid
508 }
509 }
510 return ss.UnixOwnerId
511 }
512
513 func (ss *superset) MapGid() int {
514 if ss.UnixGroup != "" {
515 gid, ok := getGidFromName(ss.UnixGroup)
516 if ok {
517 return gid
518 }
519 }
520 return ss.UnixGroupId
521 }
522
523 func (ss *superset) ModTime() time.Time {
524 if ss.UnixMtime == "" {
525 return time.Time{}
526 }
527 t, err := time.Parse(time.RFC3339, ss.UnixMtime)
528 if err != nil {
529 return time.Time{}
530 }
531 return t
532 }
533
534 var DefaultStatHasher = &defaultStatHasher{}
535
536 type defaultStatHasher struct{}
537
538 func (d *defaultStatHasher) Lstat(fileName string) (os.FileInfo, error) {
539 return os.Lstat(fileName)
540 }
541
542 func (d *defaultStatHasher) Hash(fileName string) (blob.Ref, error) {
543 h := blob.NewHash()
544 file, err := os.Open(fileName)
545 if err != nil {
546 return blob.Ref{}, err
547 }
548 defer file.Close()
549 _, err = io.Copy(h, file)
550 if err != nil {
551 return blob.Ref{}, err
552 }
553 return blob.RefFromHash(h), nil
554 }
555
556
557
558
559
560
561 var maxStaticSetMembers = 10000
562
563
564
565 func NewStaticSet() *Builder {
566 return base(1, TypeStaticSet)
567 }
568
569
570
571
572
573
574
575
576 func (bb *Builder) SetStaticSetMembers(members []blob.Ref) []*Blob {
577 if bb.Type() != TypeStaticSet {
578 panic("called SetStaticSetMembers on non static-set")
579 }
580
581 if len(members) <= maxStaticSetMembers {
582 ms := make([]string, len(members))
583 for i := range members {
584 ms[i] = members[i].String()
585 }
586 bb.m["members"] = ms
587 return nil
588 }
589
590
591
592 subsetsNumber := len(members) / maxStaticSetMembers
593 var perSubset int
594 if subsetsNumber < maxStaticSetMembers {
595
596
597 perSubset = maxStaticSetMembers
598 } else {
599
600
601
602
603 subsetsNumber = maxStaticSetMembers - 1
604 perSubset = len(members) / subsetsNumber
605 }
606
607 subsets := make([]*Blob, 0, subsetsNumber)
608
609 allSubsets := make([]*Blob, 0, subsetsNumber)
610 for i := 0; i < subsetsNumber; i++ {
611 ss := NewStaticSet()
612 subss := ss.SetStaticSetMembers(members[i*perSubset : (i+1)*perSubset])
613 subsets = append(subsets, ss.Blob())
614 allSubsets = append(allSubsets, ss.Blob())
615 allSubsets = append(allSubsets, subss...)
616 }
617
618
619 if perSubset*subsetsNumber < len(members) {
620 ss := NewStaticSet()
621 ss.SetStaticSetMembers(members[perSubset*subsetsNumber:])
622 allSubsets = append(allSubsets, ss.Blob())
623 subsets = append(subsets, ss.Blob())
624 }
625
626 mss := make([]string, len(subsets))
627 for i := range subsets {
628 mss[i] = subsets[i].BlobRef().String()
629 }
630 bb.m["mergeSets"] = mss
631 return allSubsets
632 }
633
634 func base(version int, ctype CamliType) *Builder {
635 return &Builder{map[string]interface{}{
636 "camliVersion": version,
637 "camliType": string(ctype),
638 }}
639 }
640
641
642 func NewUnsignedPermanode() *Builder {
643 bb := base(1, TypePermanode)
644 chars := make([]byte, 20)
645 _, err := io.ReadFull(rand.Reader, chars)
646 if err != nil {
647 panic("error reading random bytes: " + err.Error())
648 }
649 bb.m["random"] = base64.StdEncoding.EncodeToString(chars)
650 return bb
651 }
652
653
654
655
656
657
658 func NewPlannedPermanode(key string) *Builder {
659 bb := base(1, TypePermanode)
660 bb.m["key"] = key
661 return bb
662 }
663
664
665
666 func NewHashPlannedPermanode(h hash.Hash) *Builder {
667 return NewPlannedPermanode(blob.RefFromHash(h).String())
668 }
669
670
671
672
673
674
675 func mapJSON(m map[string]interface{}) (string, error) {
676 version, hasVersion := m["camliVersion"]
677 if !hasVersion {
678 return "", ErrNoCamliVersion
679 }
680 delete(m, "camliVersion")
681 jsonBytes, err := json.MarshalIndent(m, "", " ")
682 if err != nil {
683 return "", err
684 }
685 m["camliVersion"] = version
686 var buf bytes.Buffer
687 fmt.Fprintf(&buf, "{\"camliVersion\": %v,\n", version)
688 buf.Write(jsonBytes[2:])
689 return buf.String(), nil
690 }
691
692
693
694 func NewFileMap(fileName string) *Builder {
695 return newCommonFilenameMap(fileName).SetType(TypeFile)
696 }
697
698
699 func NewDirMap(fileName string) *Builder {
700 return newCommonFilenameMap(fileName).SetType(TypeDirectory)
701 }
702
703 func newCommonFilenameMap(fileName string) *Builder {
704 bb := base(1, "" )
705 if fileName != "" {
706 bb.SetFileName(fileName)
707 }
708 return bb
709 }
710
711 var populateSchemaStat []func(schemaMap map[string]interface{}, fi os.FileInfo)
712
713 func NewCommonFileMap(fileName string, fi os.FileInfo) *Builder {
714 bb := newCommonFilenameMap(fileName)
715
716 if fi.Mode()&os.ModeSymlink == 0 {
717 bb.m["unixPermission"] = fmt.Sprintf("0%o", fi.Mode().Perm())
718 }
719
720
721 for _, f := range populateSchemaStat {
722 f(bb.m, fi)
723 }
724
725 if mtime := fi.ModTime(); !mtime.IsZero() {
726 bb.m["unixMtime"] = RFC3339FromTime(mtime)
727 }
728 return bb
729 }
730
731
732
733
734
735 func (bb *Builder) PopulateParts(size int64, parts []BytesPart) error {
736 return populateParts(bb.m, size, parts)
737 }
738
739 func populateParts(m map[string]interface{}, size int64, parts []BytesPart) error {
740 sumSize := int64(0)
741 mparts := make([]map[string]interface{}, len(parts))
742 for idx, part := range parts {
743 mpart := make(map[string]interface{})
744 mparts[idx] = mpart
745 switch {
746 case part.BlobRef.Valid() && part.BytesRef.Valid():
747 return errors.New("schema: part contains both BlobRef and BytesRef")
748 case part.BlobRef.Valid():
749 mpart["blobRef"] = part.BlobRef.String()
750 case part.BytesRef.Valid():
751 mpart["bytesRef"] = part.BytesRef.String()
752 default:
753 return errors.New("schema: part must contain either a BlobRef or BytesRef")
754 }
755 mpart["size"] = part.Size
756 sumSize += int64(part.Size)
757 if part.Offset != 0 {
758 mpart["offset"] = part.Offset
759 }
760 }
761 if sumSize != size {
762 return fmt.Errorf("schema: declared size %d doesn't match sum of parts size %d", size, sumSize)
763 }
764 m["parts"] = mparts
765 return nil
766 }
767
768 func newBytes() *Builder {
769 return base(1, TypeBytes)
770 }
771
772
773 type CamliType string
774
775 const (
776 TypeBytes CamliType = "bytes"
777 TypeClaim CamliType = "claim"
778 TypeDirectory CamliType = "directory"
779 TypeFIFO CamliType = "fifo"
780 TypeFile CamliType = "file"
781 TypeInode CamliType = "inode"
782 TypeKeep CamliType = "keep"
783 TypePermanode CamliType = "permanode"
784 TypeShare CamliType = "share"
785 TypeSocket CamliType = "socket"
786 TypeStaticSet CamliType = "static-set"
787 TypeSymlink CamliType = "symlink"
788 )
789
790
791 type ClaimType string
792
793 const (
794 SetAttributeClaim ClaimType = "set-attribute"
795 AddAttributeClaim ClaimType = "add-attribute"
796 DelAttributeClaim ClaimType = "del-attribute"
797 ShareClaim ClaimType = "share"
798
799
800 DeleteClaim ClaimType = "delete"
801 )
802
803
804 type claimParam struct {
805 claimType ClaimType
806
807
808 permanode blob.Ref
809 attribute string
810 value string
811
812
813 authType string
814 transitive bool
815
816
817 target blob.Ref
818 }
819
820 func newClaim(claims ...*claimParam) *Builder {
821 bb := base(1, TypeClaim)
822 bb.SetClaimDate(clockNow())
823 if len(claims) == 1 {
824 cp := claims[0]
825 populateClaimMap(bb.m, cp)
826 return bb
827 }
828 var claimList []interface{}
829 for _, cp := range claims {
830 m := map[string]interface{}{}
831 populateClaimMap(m, cp)
832 claimList = append(claimList, m)
833 }
834 bb.m["claimType"] = "multi"
835 bb.m["claims"] = claimList
836 return bb
837 }
838
839 func populateClaimMap(m map[string]interface{}, cp *claimParam) {
840 m["claimType"] = string(cp.claimType)
841 switch cp.claimType {
842 case ShareClaim:
843 m["authType"] = cp.authType
844 m["transitive"] = cp.transitive
845 case DeleteClaim:
846 m["target"] = cp.target.String()
847 default:
848 m["permaNode"] = cp.permanode.String()
849 m["attribute"] = cp.attribute
850 if !(cp.claimType == DelAttributeClaim && cp.value == "") {
851 m["value"] = cp.value
852 }
853 }
854 }
855
856
857 func NewShareRef(authType string, transitive bool) *Builder {
858 return newClaim(&claimParam{
859 claimType: ShareClaim,
860 authType: authType,
861 transitive: transitive,
862 })
863 }
864
865 func NewSetAttributeClaim(permaNode blob.Ref, attr, value string) *Builder {
866 return newClaim(&claimParam{
867 permanode: permaNode,
868 claimType: SetAttributeClaim,
869 attribute: attr,
870 value: value,
871 })
872 }
873
874 func NewAddAttributeClaim(permaNode blob.Ref, attr, value string) *Builder {
875 return newClaim(&claimParam{
876 permanode: permaNode,
877 claimType: AddAttributeClaim,
878 attribute: attr,
879 value: value,
880 })
881 }
882
883
884
885
886 func NewDelAttributeClaim(permaNode blob.Ref, attr, value string) *Builder {
887 return newClaim(&claimParam{
888 permanode: permaNode,
889 claimType: DelAttributeClaim,
890 attribute: attr,
891 value: value,
892 })
893 }
894
895
896 func NewDeleteClaim(target blob.Ref) *Builder {
897 return newClaim(&claimParam{
898 target: target,
899 claimType: DeleteClaim,
900 })
901 }
902
903
904
905
906
907 const ShareHaveRef = "haveref"
908
909
910
911
912 var UnknownLocation = time.FixedZone("Unknown", -60)
913
914
915
916
917 func IsZoneKnown(t time.Time) bool {
918 if t.Location() == UnknownLocation {
919 return false
920 }
921 if _, off := t.Zone(); off == -60 {
922 return false
923 }
924 return true
925 }
926
927
928
929
930
931
932
933
934
935 func RFC3339FromTime(t time.Time) string {
936 if IsZoneKnown(t) {
937 t = t.UTC()
938 }
939 if t.UnixNano()%1e9 == 0 {
940 return t.Format(time.RFC3339)
941 }
942 return t.Format(time.RFC3339Nano)
943 }
944
945 var bytesCamliVersion = []byte("camliVersion")
946
947
948
949 func LikelySchemaBlob(buf []byte) bool {
950 if len(buf) == 0 || buf[0] != '{' {
951 return false
952 }
953 return bytes.Contains(buf, bytesCamliVersion)
954 }
955
956
957
958
959 func findSize(v interface{}) (size int64, ok bool) {
960 if fi, ok := v.(*os.File); ok {
961 v, _ = fi.Stat()
962 }
963 if sz, ok := v.(interface {
964 Size() int64
965 }); ok {
966 return sz.Size(), true
967 }
968
969 if li, ok := v.(interface {
970 Len() int
971 }); ok {
972 ln := int64(li.Len())
973
974 if sk, ok := v.(io.Seeker); ok {
975 if cur, err := sk.Seek(0, 1); err == nil {
976 ln += cur
977 }
978 }
979 return ln, true
980 }
981 return 0, false
982 }
983
984
985
986
987
988
989 func FileTime(f io.ReaderAt) (time.Time, error) {
990 var ct time.Time
991 defaultTime := func() (time.Time, error) {
992 if osf, ok := f.(*os.File); ok {
993 fi, err := osf.Stat()
994 if err != nil {
995 return ct, fmt.Errorf("Failed to find a modtime: stat: %w", err)
996 }
997 return fi.ModTime(), nil
998 }
999 return ct, errors.New("all methods failed to find a creation time or modtime")
1000 }
1001
1002 size, ok := findSize(f)
1003 if !ok {
1004 size = 256 << 10
1005 }
1006 r := io.NewSectionReader(f, 0, size)
1007 var tiffErr error
1008 ex, err := exif.Decode(r)
1009 if err != nil {
1010 tiffErr = err
1011 if exif.IsShortReadTagValueError(err) {
1012 return ct, io.ErrUnexpectedEOF
1013 }
1014 if exif.IsCriticalError(err) || exif.IsExifError(err) {
1015 return defaultTime()
1016 }
1017 }
1018 ct, err = ex.DateTime()
1019 if err != nil {
1020 return defaultTime()
1021 }
1022
1023
1024 if ct.Location() == time.Local {
1025 if exif.IsGPSError(tiffErr) {
1026 log.Printf("Invalid EXIF GPS data: %v", tiffErr)
1027 return ct, nil
1028 }
1029 if lat, long, err := ex.LatLong(); err == nil {
1030 if loc := lookupLocation(latlong.LookupZoneName(lat, long)); loc != nil {
1031 if t, err := exifDateTimeInLocation(ex, loc); err == nil {
1032 return t, nil
1033 }
1034 }
1035 } else if !exif.IsTagNotPresentError(err) {
1036 log.Printf("Invalid EXIF GPS data: %v", err)
1037 }
1038 }
1039 return ct, nil
1040 }
1041
1042
1043
1044
1045
1046 func exifDateTimeInLocation(x *exif.Exif, loc *time.Location) (time.Time, error) {
1047 tag, err := x.Get(exif.DateTimeOriginal)
1048 if err != nil {
1049 tag, err = x.Get(exif.DateTime)
1050 if err != nil {
1051 return time.Time{}, err
1052 }
1053 }
1054 if tag.Format() != tiff.StringVal {
1055 return time.Time{}, errors.New("DateTime[Original] not in string format")
1056 }
1057 const exifTimeLayout = "2006:01:02 15:04:05"
1058 dateStr := strings.TrimRight(string(tag.Val), "\x00")
1059 return time.ParseInLocation(exifTimeLayout, dateStr, loc)
1060 }
1061
1062 var zoneCache struct {
1063 sync.RWMutex
1064 m map[string]*time.Location
1065 }
1066
1067 func lookupLocation(zone string) *time.Location {
1068 if zone == "" {
1069 return nil
1070 }
1071 zoneCache.RLock()
1072 l, ok := zoneCache.m[zone]
1073 zoneCache.RUnlock()
1074 if ok {
1075 return l
1076 }
1077
1078
1079 loc, err := time.LoadLocation(zone)
1080
1081 zoneCache.Lock()
1082 if zoneCache.m == nil {
1083 zoneCache.m = make(map[string]*time.Location)
1084 }
1085 zoneCache.m[zone] = loc
1086 zoneCache.Unlock()
1087
1088 if err != nil {
1089 log.Printf("failed to lookup timezone %q: %v", zone, err)
1090 return nil
1091 }
1092 return loc
1093 }
1094
1095 var boringTitlePattern = regexp.MustCompile(`^(?:IMG_|DSC|PANO_|ESR_).*$`)
1096
1097
1098
1099
1100 func IsInterestingTitle(title string) bool {
1101 return !boringTitlePattern.MatchString(title)
1102 }