larc r10

376 lines ยท 7.9 KB Raw
1 package mount
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "strings"
8 "sync"
9 "syscall"
10 "time"
11
12 "github.com/jacobsa/fuse"
13 "github.com/jacobsa/fuse/fuseops"
14 "github.com/jacobsa/fuse/fuseutil"
15
16 "larc.wejust.rest/larc/internal/core"
17 "larc.wejust.rest/larc/internal/protocol"
18 )
19
20 /* LarcFS implements a read-only FUSE filesystem for larc repositories.
21 * Files are lazily fetched from the server and cached locally. */
22
23 const (
24 rootInodeID fuseops.InodeID = fuseops.RootInodeID
25 )
26
27 type inodeInfo struct {
28 id fuseops.InodeID
29 name string
30 entry *core.TreeEntry // nil for synthetic directories
31 children map[string]fuseops.InodeID // name -> inode (only for dirs)
32 parent fuseops.InodeID
33 isDir bool
34 }
35
36 // LarcFS is the FUSE filesystem implementation
37 type LarcFS struct {
38 fuseutil.NotImplementedFileSystem
39
40 fetcher *BlobFetcher
41 repoName string
42 revision int64
43
44 inodes map[fuseops.InodeID]*inodeInfo
45 nextInode fuseops.InodeID
46 mu sync.RWMutex
47
48 /* file handle tracking */
49 handles map[fuseops.HandleID]fuseops.InodeID
50 nextHandle fuseops.HandleID
51 handleMu sync.Mutex
52 }
53
54 // NewLarcFS creates a new filesystem from a tree
55 func NewLarcFS(client *protocol.Client, repoName string, revision int64, tree *core.Tree, cache *BlobCache) *LarcFS {
56 fs := &LarcFS{
57 fetcher: NewBlobFetcher(client, repoName, cache),
58 repoName: repoName,
59 revision: revision,
60 inodes: make(map[fuseops.InodeID]*inodeInfo),
61 nextInode: rootInodeID + 1,
62 handles: make(map[fuseops.HandleID]fuseops.InodeID),
63 nextHandle: 1,
64 }
65
66 fs.buildInodeTree(tree)
67 return fs
68 }
69
70 func (fs *LarcFS) buildInodeTree(tree *core.Tree) {
71 /* create root inode */
72 root := &inodeInfo{
73 id: rootInodeID,
74 name: "",
75 isDir: true,
76 children: make(map[string]fuseops.InodeID),
77 parent: rootInodeID,
78 }
79 fs.inodes[rootInodeID] = root
80
81 /* process all entries and build directory structure */
82 for i := range tree.Entries {
83 entry := &tree.Entries[i]
84 fs.addEntry(entry)
85 }
86 }
87
88 func (fs *LarcFS) addEntry(entry *core.TreeEntry) {
89 parts := strings.Split(entry.Path, string(filepath.Separator))
90 if len(parts) == 0 {
91 return
92 }
93
94 /* ensure all parent directories exist */
95 currentInode := rootInodeID
96 for i, part := range parts[:len(parts)-1] {
97 current := fs.inodes[currentInode]
98 if childID, ok := current.children[part]; ok {
99 currentInode = childID
100 } else {
101 /* create synthetic directory */
102 newID := fs.nextInode
103 fs.nextInode++
104
105 dirPath := strings.Join(parts[:i+1], string(filepath.Separator))
106 newDir := &inodeInfo{
107 id: newID,
108 name: part,
109 isDir: true,
110 children: make(map[string]fuseops.InodeID),
111 parent: currentInode,
112 entry: &core.TreeEntry{
113 Path: dirPath,
114 Kind: core.EntryKindDir,
115 Mode: 0755,
116 },
117 }
118 fs.inodes[newID] = newDir
119 current.children[part] = newID
120 currentInode = newID
121 }
122 }
123
124 /* add the file/final entry */
125 name := parts[len(parts)-1]
126 parent := fs.inodes[currentInode]
127
128 newID := fs.nextInode
129 fs.nextInode++
130
131 isDir := entry.Kind == core.EntryKindDir
132 info := &inodeInfo{
133 id: newID,
134 name: name,
135 entry: entry,
136 parent: currentInode,
137 isDir: isDir,
138 }
139 if isDir {
140 info.children = make(map[string]fuseops.InodeID)
141 }
142
143 fs.inodes[newID] = info
144 parent.children[name] = newID
145 }
146
147 func (fs *LarcFS) allocHandle(inode fuseops.InodeID) fuseops.HandleID {
148 fs.handleMu.Lock()
149 defer fs.handleMu.Unlock()
150
151 h := fs.nextHandle
152 fs.nextHandle++
153 fs.handles[h] = inode
154 return h
155 }
156
157 func (fs *LarcFS) freeHandle(h fuseops.HandleID) {
158 fs.handleMu.Lock()
159 defer fs.handleMu.Unlock()
160 delete(fs.handles, h)
161 }
162
163 /* FUSE operations */
164
165 func (fs *LarcFS) StatFS(ctx context.Context, op *fuseops.StatFSOp) error {
166 op.BlockSize = 4096
167 op.Blocks = 1000000
168 op.BlocksFree = 0
169 op.BlocksAvailable = 0
170 op.IoSize = 65536
171 op.Inodes = uint64(len(fs.inodes))
172 op.InodesFree = 0
173 return nil
174 }
175
176 func (fs *LarcFS) LookUpInode(ctx context.Context, op *fuseops.LookUpInodeOp) error {
177 fs.mu.RLock()
178 defer fs.mu.RUnlock()
179
180 parent, ok := fs.inodes[op.Parent]
181 if !ok {
182 return fuse.ENOENT
183 }
184
185 childID, ok := parent.children[op.Name]
186 if !ok {
187 return fuse.ENOENT
188 }
189
190 child := fs.inodes[childID]
191 op.Entry.Child = childID
192 op.Entry.Attributes = fs.getAttrs(child)
193 op.Entry.AttributesExpiration = time.Now().Add(time.Hour)
194 op.Entry.EntryExpiration = time.Now().Add(time.Hour)
195
196 return nil
197 }
198
199 func (fs *LarcFS) GetInodeAttributes(ctx context.Context, op *fuseops.GetInodeAttributesOp) error {
200 fs.mu.RLock()
201 defer fs.mu.RUnlock()
202
203 info, ok := fs.inodes[op.Inode]
204 if !ok {
205 return fuse.ENOENT
206 }
207
208 op.Attributes = fs.getAttrs(info)
209 op.AttributesExpiration = time.Now().Add(time.Hour)
210 return nil
211 }
212
213 func (fs *LarcFS) getAttrs(info *inodeInfo) fuseops.InodeAttributes {
214 attrs := fuseops.InodeAttributes{
215 Nlink: 1,
216 Uid: uint32(os.Getuid()),
217 Gid: uint32(os.Getgid()),
218 }
219
220 if info.isDir {
221 attrs.Mode = os.ModeDir | 0755
222 attrs.Size = 4096
223 } else if info.entry != nil {
224 attrs.Mode = os.FileMode(info.entry.Mode) & 0777
225 attrs.Size = uint64(info.entry.Size)
226 } else {
227 attrs.Mode = 0644
228 }
229
230 return attrs
231 }
232
233 func (fs *LarcFS) OpenDir(ctx context.Context, op *fuseops.OpenDirOp) error {
234 fs.mu.RLock()
235 defer fs.mu.RUnlock()
236
237 info, ok := fs.inodes[op.Inode]
238 if !ok {
239 return fuse.ENOENT
240 }
241 if !info.isDir {
242 return syscall.ENOTDIR
243 }
244
245 op.Handle = fs.allocHandle(op.Inode)
246 return nil
247 }
248
249 func (fs *LarcFS) ReadDir(ctx context.Context, op *fuseops.ReadDirOp) error {
250 fs.mu.RLock()
251 defer fs.mu.RUnlock()
252
253 info, ok := fs.inodes[op.Inode]
254 if !ok {
255 return fuse.ENOENT
256 }
257
258 /* collect children in stable order */
259 type dirEntry struct {
260 name string
261 inode fuseops.InodeID
262 }
263 var entries []dirEntry
264 for name, id := range info.children {
265 entries = append(entries, dirEntry{name: name, inode: id})
266 }
267
268 /* write entries starting from offset */
269 var bytesWritten int
270 for i := int(op.Offset); i < len(entries); i++ {
271 e := entries[i]
272 child := fs.inodes[e.inode]
273
274 var dtype fuseutil.DirentType
275 if child.isDir {
276 dtype = fuseutil.DT_Directory
277 } else {
278 dtype = fuseutil.DT_File
279 }
280
281 dirent := fuseutil.Dirent{
282 Offset: fuseops.DirOffset(i + 1),
283 Inode: e.inode,
284 Name: e.name,
285 Type: dtype,
286 }
287
288 n := fuseutil.WriteDirent(op.Dst[bytesWritten:], dirent)
289 if n == 0 {
290 break
291 }
292 bytesWritten += n
293 }
294
295 op.BytesRead = bytesWritten
296 return nil
297 }
298
299 func (fs *LarcFS) ReleaseDirHandle(ctx context.Context, op *fuseops.ReleaseDirHandleOp) error {
300 fs.freeHandle(op.Handle)
301 return nil
302 }
303
304 func (fs *LarcFS) OpenFile(ctx context.Context, op *fuseops.OpenFileOp) error {
305 fs.mu.RLock()
306 defer fs.mu.RUnlock()
307
308 info, ok := fs.inodes[op.Inode]
309 if !ok {
310 return fuse.ENOENT
311 }
312 if info.isDir {
313 return syscall.EISDIR
314 }
315
316 op.Handle = fs.allocHandle(op.Inode)
317 op.KeepPageCache = true
318 return nil
319 }
320
321 func (fs *LarcFS) ReadFile(ctx context.Context, op *fuseops.ReadFileOp) error {
322 fs.mu.RLock()
323 info, ok := fs.inodes[op.Inode]
324 fs.mu.RUnlock()
325
326 if !ok {
327 return fuse.ENOENT
328 }
329 if info.entry == nil || info.entry.BlobHash == "" {
330 return fuse.EIO
331 }
332
333 /* fetch blob (from cache or server) */
334 data, err := fs.fetcher.Fetch(info.entry.BlobHash)
335 if err != nil {
336 return fuse.EIO
337 }
338
339 /* handle offset and size */
340 start := int(op.Offset)
341 if start >= len(data) {
342 op.BytesRead = 0
343 return nil
344 }
345
346 end := start + len(op.Dst)
347 if end > len(data) {
348 end = len(data)
349 }
350
351 op.BytesRead = copy(op.Dst, data[start:end])
352 return nil
353 }
354
355 func (fs *LarcFS) ReleaseFileHandle(ctx context.Context, op *fuseops.ReleaseFileHandleOp) error {
356 fs.freeHandle(op.Handle)
357 return nil
358 }
359
360 // Mount mounts the filesystem at the given path
361 func Mount(mountPoint string, fs *LarcFS) (*fuse.MountedFileSystem, error) {
362 server := fuseutil.NewFileSystemServer(fs)
363
364 cfg := &fuse.MountConfig{
365 FSName: "larc:" + fs.repoName,
366 ReadOnly: true,
367 }
368
369 return fuse.Mount(mountPoint, server, cfg)
370 }
371
372 // Unmount unmounts the filesystem at the given path
373 func Unmount(mountPoint string) error {
374 return fuse.Unmount(mountPoint)
375 }
376