1
2
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
18
19 package fs
20
21 import (
22 "context"
23 "os"
24 "path/filepath"
25 "strings"
26 "sync"
27 "time"
28
29 "perkeep.org/pkg/blob"
30 "perkeep.org/pkg/search"
31
32 "bazil.org/fuse"
33 "bazil.org/fuse/fs"
34 )
35
36
37
38
39 type recentDir struct {
40 fs *CamliFileSystem
41
42 mu sync.Mutex
43 ents map[string]*search.DescribedBlob
44 modTime map[string]time.Time
45 lastReaddir time.Time
46 lastNames []string
47 }
48
49 var (
50 _ fs.Node = (*recentDir)(nil)
51 _ fs.HandleReadDirAller = (*recentDir)(nil)
52 _ fs.NodeStringLookuper = (*recentDir)(nil)
53 )
54
55 func (n *recentDir) Attr(ctx context.Context, a *fuse.Attr) error {
56 a.Mode = os.ModeDir | 0500
57 a.Uid = uint32(os.Getuid())
58 a.Gid = uint32(os.Getgid())
59 return nil
60 }
61
62 const recentSearchInterval = 10 * time.Second
63
64 func (n *recentDir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
65 var ents []fuse.Dirent
66
67 n.mu.Lock()
68 defer n.mu.Unlock()
69 if n.lastReaddir.After(time.Now().Add(-recentSearchInterval)) {
70 Logger.Printf("fs.recent: ReadDirAll from cache")
71 for _, name := range n.lastNames {
72 ents = append(ents, fuse.Dirent{Name: name})
73 }
74 return ents, nil
75 }
76
77 Logger.Printf("fs.recent: ReadDirAll, doing search")
78
79 n.ents = make(map[string]*search.DescribedBlob)
80 n.modTime = make(map[string]time.Time)
81
82 req := &search.RecentRequest{N: 100}
83 res, err := n.fs.client.GetRecentPermanodes(ctx, req)
84 if err != nil {
85 Logger.Printf("fs.recent: GetRecentPermanodes error in ReadDirAll: %v", err)
86 return nil, handleEIOorEINTR(err)
87 }
88
89 n.lastNames = nil
90 for _, ri := range res.Recent {
91 modTime := ri.ModTime.Time()
92 meta := res.Meta.Get(ri.BlobRef)
93 if meta == nil || meta.Permanode == nil {
94 continue
95 }
96 cc, ok := blob.Parse(meta.Permanode.Attr.Get("camliContent"))
97 if !ok {
98 continue
99 }
100 ccMeta := res.Meta.Get(cc)
101 if ccMeta == nil {
102 continue
103 }
104 var name string
105 switch {
106 case ccMeta.File != nil:
107 name = ccMeta.File.FileName
108 if mt := ccMeta.File.Time; !mt.IsAnyZero() {
109 modTime = mt.Time()
110 }
111 case ccMeta.Dir != nil:
112 name = ccMeta.Dir.FileName
113 default:
114 continue
115 }
116 if name == "" || n.ents[name] != nil {
117 ext := filepath.Ext(name)
118 if ext == "" && ccMeta.File != nil && strings.HasSuffix(ccMeta.File.MIMEType, "image/jpeg") {
119 ext = ".jpg"
120 }
121 name = strings.TrimPrefix(ccMeta.BlobRef.String(), ccMeta.BlobRef.HashName()+"-")[:10] + ext
122 if n.ents[name] != nil {
123 continue
124 }
125 }
126 n.ents[name] = ccMeta
127 n.modTime[name] = modTime
128 Logger.Printf("fs.recent: name %q = %v (at %v -> %v)", name, ccMeta.BlobRef, ri.ModTime.Time(), modTime)
129 n.lastNames = append(n.lastNames, name)
130 ents = append(ents, fuse.Dirent{
131 Name: name,
132 })
133 }
134 Logger.Printf("fs.recent returning %d entries", len(ents))
135 n.lastReaddir = time.Now()
136 return ents, nil
137 }
138
139 func (n *recentDir) Lookup(ctx context.Context, name string) (fs.Node, error) {
140 n.mu.Lock()
141 defer n.mu.Unlock()
142 if n.ents == nil {
143
144
145 refresh := func() error {
146 n.mu.Unlock()
147 defer n.mu.Lock()
148
149 _, err := n.ReadDirAll(ctx)
150 return err
151 }
152 if err := refresh(); err != nil {
153 return nil, err
154 }
155 }
156 db := n.ents[name]
157 Logger.Printf("fs.recent: Lookup(%q) = %v", name, db)
158 if db == nil {
159 return nil, fuse.ENOENT
160 }
161 nod := &node{
162 fs: n.fs,
163 blobref: db.BlobRef,
164 pnodeModTime: n.modTime[name],
165 }
166 return nod, nil
167 }