1// +build linux darwin
2
3/*
4Copyright 2013 The Perkeep Authors
5
6Licensed under the Apache License, Version 2.0 (the "License");
7you may not use this file except in compliance with the License.
8You may obtain a copy of the License at
9
10     http://www.apache.org/licenses/LICENSE-2.0
11
12Unless required by applicable law or agreed to in writing, software
13distributed under the License is distributed on an "AS IS" BASIS,
14WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15See the License for the specific language governing permissions and
16limitations under the License.
17*/
18
19package fs
20
21import (
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// recentDir implements fuse.Node and is a directory of recent
37// permanodes' files, for permanodes with a camliContent pointing to a
38// "file".
39type recentDir struct {
40	fs *CamliFileSystem
41
42	mu          sync.Mutex
43	ents        map[string]*search.DescribedBlob // filename to blob meta
44	modTime     map[string]time.Time             // filename to permanode modtime
45	lastReaddir time.Time
46	lastNames   []string
47}
48
49var (
50	_ fs.Node               = (*recentDir)(nil)
51	_ fs.HandleReadDirAller = (*recentDir)(nil)
52	_ fs.NodeStringLookuper = (*recentDir)(nil)
53)
54
55func (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
62const recentSearchInterval = 10 * time.Second
63
64func (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, fuse.EIO
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
139func (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		// Odd case: a Lookup before a Readdir. Force a readdir to
144		// seed our map. Mostly hit just during development.
145		n.mu.Unlock() // release, since ReadDirAll will acquire
146		n.ReadDirAll(ctx)
147		n.mu.Lock()
148	}
149	db := n.ents[name]
150	Logger.Printf("fs.recent: Lookup(%q) = %v", name, db)
151	if db == nil {
152		return nil, fuse.ENOENT
153	}
154	nod := &node{
155		fs:           n.fs,
156		blobref:      db.BlobRef,
157		pnodeModTime: n.modTime[name],
158	}
159	return nod, nil
160}
161