master
go 816 lines 25.2 KB
Raw
1 // Package writable implements FUSE filesystem types shared by the
2 // mutable /mfs and /ipns mounts. Both mounts expose MFS directories
3 // as writable POSIX filesystems; the only differences are how the
4 // root is created and how xattr names are published.
5 //
6 //go:build (linux || darwin || freebsd) && !nofuse
7
8 package writable
9
10 import (
11 "context"
12 "io"
13 "os"
14 "sync"
15 "syscall"
16 "time"
17
18 "github.com/hanwen/go-fuse/v2/fs"
19 "github.com/hanwen/go-fuse/v2/fuse"
20
21 "github.com/ipfs/boxo/files"
22 dag "github.com/ipfs/boxo/ipld/merkledag"
23 ft "github.com/ipfs/boxo/ipld/unixfs"
24 uio "github.com/ipfs/boxo/ipld/unixfs/io"
25 "github.com/ipfs/boxo/mfs"
26 ipld "github.com/ipfs/go-ipld-format"
27 logging "github.com/ipfs/go-log/v2"
28 fusemnt "github.com/ipfs/kubo/fuse/mount"
29 )
30
31 var log = logging.Logger("fuse/writable")
32
33 // Config controls write-side behavior for writable mounts.
34 type Config struct {
35 StoreMtime bool // persist mtime on create and open-for-write
36 StoreMode bool // persist mode on chmod
37 DAG ipld.DAGService // required: read-only opens use it to bypass MFS desclock
38 // RepoPath is the on-disk path of the IPFS repo (e.g. ~/.ipfs).
39 // Statfs calls syscall.Statfs on this path so that the FUSE mount
40 // reports how much free space is left on the volume that stores
41 // MFS data. Without it tools like macOS Finder see zero free space
42 // and refuse to copy files.
43 RepoPath string
44 // Blksize is the preferred I/O size advertised via st_blksize on
45 // every stat. Callers should derive it from Import.UnixFSChunker via
46 // fusemnt.BlksizeFromChunker so the hint matches the chunker MFS
47 // will use for writes. If zero, NewDir writes fusemnt.DefaultBlksize
48 // into this field in place, so fillAttr on every inode can read
49 // cfg.Blksize without a nil-check on each stat.
50 Blksize uint32
51 }
52
53 // NewDir creates a Dir node backed by the given MFS directory.
54 // cfg.DAG is required: read-only file opens build a DagReader directly
55 // from it to avoid MFS's desclock (see FileInode.Open). Passing a nil
56 // DAG would silently re-introduce the rsync --inplace deadlock, so we
57 // fail loudly at construction time instead.
58 func NewDir(d *mfs.Directory, cfg *Config) *Dir {
59 if cfg == nil || cfg.DAG == nil {
60 panic("fuse/writable: Config.DAG is required")
61 }
62 // Tests and callers that don't plumb Import.UnixFSChunker leave
63 // Blksize zero; fall back to the FUSE default so stat advertises a
64 // usable st_blksize. See Config.Blksize for why we mutate in place.
65 if cfg.Blksize == 0 {
66 cfg.Blksize = fusemnt.DefaultBlksize
67 }
68 return &Dir{MFSDir: d, Cfg: cfg}
69 }
70
71 // Dir is the FUSE adapter for MFS directories.
72 type Dir struct {
73 fs.Inode
74 MFSDir *mfs.Directory
75 Cfg *Config
76 }
77
78 // fillAttr fills stat attributes for a directory. Blocks and Blksize
79 // are set explicitly because go-fuse's setBlocks otherwise auto-fills
80 // them from Size with a 4 KiB page-based fallback. For directories
81 // Size is 0, so the fallback yields st_blocks=0, which some tools
82 // (dedup scanners, file managers) treat as "unsupported".
83 func (d *Dir) fillAttr(a *fuse.Attr) {
84 a.Mode = uint32(fusemnt.DefaultDirModeRW.Perm())
85 a.Blocks = 1
86 a.Blksize = d.Cfg.Blksize
87 if m, err := d.MFSDir.Mode(); err == nil && m != 0 {
88 a.Mode = files.ModePermsToUnixPerms(m)
89 }
90 if t, err := d.MFSDir.ModTime(); err == nil && !t.IsZero() {
91 a.SetTimes(nil, &t, nil)
92 }
93 }
94
95 func (d *Dir) Getattr(_ context.Context, _ fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
96 d.fillAttr(&out.Attr)
97 return 0
98 }
99
100 // Statfs reports disk-space statistics for the underlying filesystem.
101 // macOS Finder checks free space before copying; without this it
102 // reports "not enough free space" because go-fuse returns zeroed stats.
103 func (d *Dir) Statfs(_ context.Context, out *fuse.StatfsOut) syscall.Errno {
104 if d.Cfg.RepoPath == "" {
105 return 0
106 }
107 var s syscall.Statfs_t
108 if err := syscall.Statfs(d.Cfg.RepoPath, &s); err != nil {
109 return fs.ToErrno(err)
110 }
111 out.FromStatfsT(&s)
112 return 0
113 }
114
115 // Setattr handles chmod and mtime changes on directories.
116 // Tools like tar and rsync set directory timestamps after extraction.
117 //
118 // Mode and mtime are stored as UnixFS optional metadata.
119 // The UnixFS spec supports all 12 permission bits, but boxo's MFS
120 // layer exposes only the lower 9 (ugo-rwx); setuid/setgid/sticky
121 // are silently dropped. FUSE mounts are always nosuid so these
122 // bits would have no execution effect anyway.
123 // See https://specs.ipfs.tech/unixfs/#dag-pb-optional-metadata
124 func (d *Dir) Setattr(_ context.Context, _ fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno {
125 if mode, ok := in.GetMode(); ok && d.Cfg.StoreMode {
126 if err := d.MFSDir.SetMode(files.UnixPermsToModePerms(mode)); err != nil {
127 return fs.ToErrno(err)
128 }
129 }
130 if mtime, ok := in.GetMTime(); ok && d.Cfg.StoreMtime {
131 if err := d.MFSDir.SetModTime(mtime); err != nil {
132 return fs.ToErrno(err)
133 }
134 }
135 d.fillAttr(&out.Attr)
136 return 0
137 }
138
139 func (d *Dir) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
140 mfsNode, err := d.MFSDir.Child(name)
141 if err != nil {
142 return nil, syscall.ENOENT
143 }
144
145 switch mfsNode.Type() {
146 case mfs.TDir:
147 child := &Dir{MFSDir: mfsNode.(*mfs.Directory), Cfg: d.Cfg}
148 child.fillAttr(&out.Attr)
149 return d.NewInode(ctx, child, fs.StableAttr{Mode: syscall.S_IFDIR}), 0
150 case mfs.TFile:
151 mfsFile := mfsNode.(*mfs.File)
152 if target := SymlinkTarget(mfsFile); target != "" {
153 child := &Symlink{Target: target, MFSFile: mfsFile, Cfg: d.Cfg}
154 child.fillAttr(&out.Attr)
155 return d.NewInode(ctx, child, fs.StableAttr{Mode: syscall.S_IFLNK}), 0
156 }
157 child := &FileInode{MFSFile: mfsFile, Cfg: d.Cfg}
158 child.fillAttr(&out.Attr)
159 return d.NewInode(ctx, child, fs.StableAttr{}), 0
160 default:
161 log.Errorf("unexpected MFS node type %d under directory", mfsNode.Type())
162 return nil, syscall.EIO
163 }
164 }
165
166 func (d *Dir) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) {
167 nodes, err := d.MFSDir.List(ctx)
168 if err != nil {
169 return nil, fs.ToErrno(err)
170 }
171
172 entries := make([]fuse.DirEntry, len(nodes))
173 for i, node := range nodes {
174 var mode uint32
175 switch {
176 case node.Type == int(mfs.TDir):
177 mode = syscall.S_IFDIR
178 case node.Type == int(mfs.TFile):
179 // MFS represents symlinks as TFile; check the DAG node.
180 if child, err := d.MFSDir.Child(node.Name); err == nil {
181 if f, ok := child.(*mfs.File); ok && SymlinkTarget(f) != "" {
182 mode = syscall.S_IFLNK
183 }
184 }
185 }
186 entries[i] = fuse.DirEntry{Name: node.Name, Mode: mode}
187 }
188 return fs.NewListDirStream(entries), 0
189 }
190
191 // Mkdir creates a new directory under d.
192 //
193 // TODO: boxo's mfs.Directory.Mkdir(name string) accepts no mode
194 // argument, so the caller's mode is silently dropped here. Tools
195 // that mkdir then chown without a follow-up chmod (some tar/rsync
196 // flows) see the default 0755 instead of the requested mode.
197 // Fixing this requires a boxo MFS API change.
198 func (d *Dir) Mkdir(ctx context.Context, name string, _ uint32, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
199 mfsDir, err := d.MFSDir.Mkdir(name)
200 if err != nil {
201 return nil, fs.ToErrno(err)
202 }
203 child := &Dir{MFSDir: mfsDir, Cfg: d.Cfg}
204 // Fill the response attrs so the kernel doesn't cache zero values
205 // until AttrTimeout expires. Matches Dir.Create and FileInode.Setattr.
206 child.fillAttr(&out.Attr)
207 return d.NewInode(ctx, child, fs.StableAttr{Mode: syscall.S_IFDIR}), 0
208 }
209
210 func (d *Dir) Unlink(_ context.Context, name string) syscall.Errno {
211 if err := d.MFSDir.Unlink(name); err != nil {
212 return fs.ToErrno(err)
213 }
214 return fs.ToErrno(d.MFSDir.Flush())
215 }
216
217 func (d *Dir) Rmdir(ctx context.Context, name string) syscall.Errno {
218 child, err := d.MFSDir.Child(name)
219 if err != nil {
220 return fs.ToErrno(err)
221 }
222 target, ok := child.(*mfs.Directory)
223 if !ok {
224 return syscall.ENOTDIR
225 }
226
227 children, err := target.ListNames(ctx)
228 if err != nil {
229 return fs.ToErrno(err)
230 }
231 if len(children) > 0 {
232 return syscall.ENOTEMPTY
233 }
234
235 if err := d.MFSDir.Unlink(name); err != nil {
236 return fs.ToErrno(err)
237 }
238 return fs.ToErrno(d.MFSDir.Flush())
239 }
240
241 // Rename moves an entry across MFS directories.
242 //
243 // TODO: this is not atomic. The source is unlinked before the
244 // destination is added, so any failure between the two steps loses
245 // the source entry. Making it atomic requires changes to MFS rename
246 // semantics (boxo/mfs does not currently expose an atomic rename).
247 func (d *Dir) Rename(_ context.Context, oldName string, newParent fs.InodeEmbedder, newName string, _ uint32) syscall.Errno {
248 child, err := d.MFSDir.Child(oldName)
249 if err != nil {
250 return fs.ToErrno(err)
251 }
252
253 nd, err := child.GetNode()
254 if err != nil {
255 return fs.ToErrno(err)
256 }
257
258 // Unlink the source first. For same-directory renames, this clears
259 // the old name from the directory's entry cache before AddChild
260 // repopulates it with the new name. Without this ordering, Flush
261 // would sync the stale cache entry back into the DAG.
262 if err := d.MFSDir.Unlink(oldName); err != nil {
263 return fs.ToErrno(err)
264 }
265
266 targetDir, ok := newParent.EmbeddedInode().Operations().(*Dir)
267 if !ok {
268 return syscall.EINVAL
269 }
270 if err := targetDir.MFSDir.Unlink(newName); err != nil && err != os.ErrNotExist {
271 return fs.ToErrno(err)
272 }
273 if err := targetDir.MFSDir.AddChild(newName, nd); err != nil {
274 return fs.ToErrno(err)
275 }
276
277 return fs.ToErrno(d.MFSDir.Flush())
278 }
279
280 func (d *Dir) Create(ctx context.Context, name string, flags uint32, _ uint32, out *fuse.EntryOut) (*fs.Inode, fs.FileHandle, uint32, syscall.Errno) {
281 node := dag.NodeWithData(ft.FilePBData(nil, 0))
282 if err := node.SetCidBuilder(d.MFSDir.GetCidBuilder()); err != nil {
283 return nil, nil, 0, fs.ToErrno(err)
284 }
285
286 if err := d.MFSDir.AddChild(name, node); err != nil {
287 return nil, nil, 0, fs.ToErrno(err)
288 }
289
290 if err := d.MFSDir.Flush(); err != nil {
291 return nil, nil, 0, fs.ToErrno(err)
292 }
293
294 mfsNode, err := d.MFSDir.Child(name)
295 if err != nil {
296 return nil, nil, 0, fs.ToErrno(err)
297 }
298 if d.Cfg.StoreMtime {
299 if err := mfsNode.SetModTime(time.Now()); err != nil {
300 return nil, nil, 0, fs.ToErrno(err)
301 }
302 }
303
304 mfsFile, ok := mfsNode.(*mfs.File)
305 if !ok {
306 return nil, nil, 0, syscall.EIO
307 }
308 fileInode := &FileInode{MFSFile: mfsFile, Cfg: d.Cfg}
309
310 accessMode := flags & syscall.O_ACCMODE
311 fd, err := mfsFile.Open(mfs.Flags{
312 Read: accessMode == syscall.O_RDONLY || accessMode == syscall.O_RDWR,
313 Write: accessMode == syscall.O_WRONLY || accessMode == syscall.O_RDWR,
314 Sync: true,
315 })
316 if err != nil {
317 return nil, nil, 0, fs.ToErrno(err)
318 }
319
320 // Fill the response attrs so the kernel doesn't cache zero values
321 // (mode 0, size 0) for the new inode until AttrTimeout expires.
322 // fstat on the open file handle returned to the caller hits this
323 // cache, so leaving it empty makes f.Stat() report mode 0 right
324 // after open. Matches FileInode.Setattr and Dir.Mkdir.
325 fileInode.fillAttr(&out.Attr)
326
327 inode := d.NewInode(ctx, fileInode, fs.StableAttr{})
328 return inode, &FileHandle{inode: inode, fd: fd}, 0, 0
329 }
330
331 func (d *Dir) Listxattr(_ context.Context, dest []byte) (uint32, syscall.Errno) {
332 data := []byte(fusemnt.XattrCID + "\x00")
333 if len(dest) == 0 {
334 return uint32(len(data)), 0
335 }
336 if len(dest) < len(data) {
337 return 0, syscall.ERANGE
338 }
339 return uint32(copy(dest, data)), 0
340 }
341
342 func (d *Dir) Getxattr(_ context.Context, attr string, dest []byte) (uint32, syscall.Errno) {
343 if attr == fusemnt.XattrCIDDeprecated {
344 log.Errorf("xattr %q is deprecated, use %q instead", fusemnt.XattrCIDDeprecated, fusemnt.XattrCID)
345 attr = fusemnt.XattrCID
346 }
347 if attr != fusemnt.XattrCID {
348 return 0, fs.ENOATTR
349 }
350 nd, err := d.MFSDir.GetNode()
351 if err != nil {
352 return 0, fs.ToErrno(err)
353 }
354 data := []byte(nd.Cid().String())
355 if len(dest) == 0 {
356 return uint32(len(data)), 0
357 }
358 if len(dest) < len(data) {
359 return 0, syscall.ERANGE
360 }
361 return uint32(copy(dest, data)), 0
362 }
363
364 // Symlink creates a new symlink in this directory.
365 func (d *Dir) Symlink(ctx context.Context, target, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
366 data, err := ft.SymlinkData(target)
367 if err != nil {
368 return nil, fs.ToErrno(err)
369 }
370 nd := dag.NodeWithData(data)
371 if err := nd.SetCidBuilder(d.MFSDir.GetCidBuilder()); err != nil {
372 return nil, fs.ToErrno(err)
373 }
374 if err := d.MFSDir.AddChild(name, nd); err != nil {
375 return nil, fs.ToErrno(err)
376 }
377 if err := d.MFSDir.Flush(); err != nil {
378 return nil, fs.ToErrno(err)
379 }
380
381 // Retrieve the mfs.File so Setattr can persist mtime.
382 mfsNode, err := d.MFSDir.Child(name)
383 if err != nil {
384 return nil, fs.ToErrno(err)
385 }
386 mfsFile, _ := mfsNode.(*mfs.File)
387
388 sym := &Symlink{Target: target, MFSFile: mfsFile, Cfg: d.Cfg}
389 sym.fillAttr(&out.Attr)
390 return d.NewInode(ctx, sym, fs.StableAttr{Mode: syscall.S_IFLNK}), 0
391 }
392
393 // FileInode is the FUSE adapter for MFS file inodes.
394 type FileInode struct {
395 fs.Inode
396 MFSFile *mfs.File
397 Cfg *Config
398 }
399
400 func (fi *FileInode) fillAttr(a *fuse.Attr) {
401 size, _ := fi.MFSFile.Size()
402 a.Size = uint64(size)
403 a.Blocks = fusemnt.SizeToStatBlocks(a.Size)
404 a.Blksize = fi.Cfg.Blksize
405 a.Mode = uint32(fusemnt.DefaultFileModeRW.Perm())
406 if m, err := fi.MFSFile.Mode(); err == nil && m != 0 {
407 a.Mode = files.ModePermsToUnixPerms(m)
408 }
409 if t, _ := fi.MFSFile.ModTime(); !t.IsZero() {
410 a.SetTimes(nil, &t, nil)
411 }
412 }
413
414 func (fi *FileInode) Getattr(_ context.Context, _ fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
415 fi.fillAttr(&out.Attr)
416 return 0
417 }
418
419 func (fi *FileInode) Open(ctx context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) {
420 accessMode := flags & syscall.O_ACCMODE
421
422 // Read-only opens bypass MFS's desclock by creating a DagReader
423 // directly from the current DAG node. MFS holds desclock.RLock
424 // for the lifetime of a read descriptor, which blocks any
425 // concurrent write open on the same file (desclock.Lock). Tools
426 // like rsync --inplace open the destination for reading and
427 // writing simultaneously, deadlocking on MFS's lock. Creating
428 // a DagReader here avoids the lock entirely: the reader gets a
429 // snapshot of the file at open time, and writers proceed through
430 // MFS independently. Cfg.DAG is required by NewDir.
431 if accessMode == syscall.O_RDONLY {
432 nd, err := fi.MFSFile.GetNode()
433 if err != nil {
434 return nil, 0, fs.ToErrno(err)
435 }
436 r, err := uio.NewDagReader(ctx, nd, fi.Cfg.DAG)
437 if err != nil {
438 return nil, 0, fusemnt.ReadErrno(err)
439 }
440 return &roFileHandle{r: r}, fuse.FOPEN_KEEP_CACHE, 0
441 }
442
443 mfsFlags := mfs.Flags{
444 Read: accessMode == syscall.O_RDONLY || accessMode == syscall.O_RDWR,
445 Write: accessMode == syscall.O_WRONLY || accessMode == syscall.O_RDWR,
446 Sync: true,
447 }
448 fd, err := fi.MFSFile.Open(mfsFlags)
449 if err != nil {
450 return nil, 0, fs.ToErrno(err)
451 }
452
453 if flags&syscall.O_TRUNC != 0 {
454 if !mfsFlags.Write {
455 fd.Close()
456 log.Error("tried to open a readonly file with truncate")
457 return nil, 0, syscall.ENOTSUP
458 }
459 if err := fd.Truncate(0); err != nil {
460 fd.Close()
461 return nil, 0, fs.ToErrno(err)
462 }
463 }
464 // O_APPEND is handled in FileHandle.Write by seeking to end.
465
466 if mfsFlags.Write && fi.Cfg.StoreMtime {
467 if err := fi.MFSFile.SetModTime(time.Now()); err != nil {
468 fd.Close()
469 return nil, 0, fs.ToErrno(err)
470 }
471 }
472
473 return &FileHandle{inode: fi.EmbeddedInode(), fd: fd, appendMode: flags&syscall.O_APPEND != 0}, 0, 0
474 }
475
476 // Setattr handles chmod, mtime changes (touch), and ftruncate.
477 //
478 // Mode and mtime are stored as UnixFS optional metadata.
479 // The UnixFS spec supports all 12 permission bits, but boxo's MFS
480 // layer exposes only the lower 9 (ugo-rwx); setuid/setgid/sticky
481 // are silently dropped. FUSE mounts are always nosuid so these
482 // bits would have no execution effect anyway.
483 // See https://specs.ipfs.tech/unixfs/#dag-pb-optional-metadata
484 //
485 // With hanwen/go-fuse, the kernel passes the open file handle (fh) when
486 // the caller uses ftruncate(fd, size). This lets us truncate through
487 // the existing write descriptor without opening a second one. For
488 // truncate(path, size) without a handle, a temporary descriptor is
489 // opened; this may block if another writer holds MFS's desclock.
490 func (fi *FileInode) Setattr(_ context.Context, fh fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno {
491 if sz, ok := in.GetSize(); ok {
492 if f, ok := fh.(*FileHandle); ok {
493 // ftruncate(fd, size): use the existing write descriptor.
494 f.mu.Lock()
495 err := f.fd.Truncate(int64(sz))
496 f.mu.Unlock()
497 if err != nil {
498 return fs.ToErrno(err)
499 }
500 } else {
501 // truncate(path, size) without an open file descriptor.
502 // Open a temporary write descriptor, truncate, flush, and
503 // close. This may block if another writer holds MFS's
504 // desclock; the FUSE kernel timeout (30s) bounds the wait.
505 fd, err := fi.MFSFile.Open(mfs.Flags{Write: true, Sync: true})
506 if err != nil {
507 return fs.ToErrno(err)
508 }
509 if err := fd.Truncate(int64(sz)); err != nil {
510 fd.Close()
511 return fs.ToErrno(err)
512 }
513 if err := fd.Flush(); err != nil {
514 fd.Close()
515 return fs.ToErrno(err)
516 }
517 if err := fd.Close(); err != nil {
518 return fs.ToErrno(err)
519 }
520 }
521 }
522 if mode, ok := in.GetMode(); ok && fi.Cfg.StoreMode {
523 if err := fi.MFSFile.SetMode(files.UnixPermsToModePerms(mode)); err != nil {
524 return fs.ToErrno(err)
525 }
526 }
527 if mtime, ok := in.GetMTime(); ok && fi.Cfg.StoreMtime {
528 if err := fi.MFSFile.SetModTime(mtime); err != nil {
529 return fs.ToErrno(err)
530 }
531 }
532 // Fill the response attrs so the kernel doesn't cache stale zero
533 // values until AttrTimeout expires. Matches Dir.Setattr behavior.
534 fi.fillAttr(&out.Attr)
535 return 0
536 }
537
538 func (fi *FileInode) Listxattr(_ context.Context, dest []byte) (uint32, syscall.Errno) {
539 data := []byte(fusemnt.XattrCID + "\x00")
540 if len(dest) == 0 {
541 return uint32(len(data)), 0
542 }
543 if len(dest) < len(data) {
544 return 0, syscall.ERANGE
545 }
546 return uint32(copy(dest, data)), 0
547 }
548
549 func (fi *FileInode) Getxattr(_ context.Context, attr string, dest []byte) (uint32, syscall.Errno) {
550 if attr == fusemnt.XattrCIDDeprecated {
551 log.Errorf("xattr %q is deprecated, use %q instead", fusemnt.XattrCIDDeprecated, fusemnt.XattrCID)
552 attr = fusemnt.XattrCID
553 }
554 if attr != fusemnt.XattrCID {
555 return 0, fs.ENOATTR
556 }
557 nd, err := fi.MFSFile.GetNode()
558 if err != nil {
559 return 0, fs.ToErrno(err)
560 }
561 data := []byte(nd.Cid().String())
562 if len(dest) == 0 {
563 return uint32(len(data)), 0
564 }
565 if len(dest) < len(data) {
566 return 0, syscall.ERANGE
567 }
568 return uint32(copy(dest, data)), 0
569 }
570
571 // FileHandle wraps an MFS file descriptor for FUSE operations.
572 // All methods are serialized by mu because the FUSE server dispatches
573 // each request in its own goroutine and the underlying DagModifier
574 // is not safe for concurrent use.
575 type FileHandle struct {
576 inode *fs.Inode // back-pointer for kernel cache invalidation
577 fd mfs.FileDescriptor
578 mu sync.Mutex
579 appendMode bool // O_APPEND: writes always go to end of file
580 }
581
582 func (fh *FileHandle) Read(ctx context.Context, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
583 fh.mu.Lock()
584 defer fh.mu.Unlock()
585
586 if _, err := fh.fd.Seek(off, io.SeekStart); err != nil {
587 return nil, fs.ToErrno(err)
588 }
589
590 size, err := fh.fd.Size()
591 if err != nil {
592 return nil, fs.ToErrno(err)
593 }
594
595 n := min(len(dest), int(size-off))
596 if n <= 0 {
597 return fuse.ReadResultData(nil), 0
598 }
599 got, err := fh.fd.CtxReadFull(ctx, dest[:n])
600 if err != nil {
601 return nil, fusemnt.ReadErrno(err)
602 }
603 return fuse.ReadResultData(dest[:got]), 0
604 }
605
606 func (fh *FileHandle) Write(_ context.Context, data []byte, off int64) (uint32, syscall.Errno) {
607 fh.mu.Lock()
608 defer fh.mu.Unlock()
609
610 if fh.appendMode {
611 // O_APPEND: the kernel may send offset 0, but POSIX says
612 // writes must go to the end of the file.
613 if _, err := fh.fd.Seek(0, io.SeekEnd); err != nil {
614 return 0, fs.ToErrno(err)
615 }
616 n, err := fh.fd.Write(data)
617 if err != nil {
618 return 0, fs.ToErrno(err)
619 }
620 return uint32(n), 0
621 }
622
623 n, err := fh.fd.WriteAt(data, off)
624 if err != nil {
625 return 0, fs.ToErrno(err)
626 }
627 return uint32(n), 0
628 }
629
630 // Flush persists buffered writes to the DAG and invalidates the
631 // kernel's cached attrs so the next stat sees the updated size.
632 //
633 // We intentionally ignore ctx: the underlying MFS flush cannot be
634 // safely canceled mid-operation, and abandoning it would leak a
635 // background goroutine that races with the subsequent Release.
636 //
637 // Cache invalidation happens here (in addition to Release) because
638 // the kernel calls Flush synchronously inside close() but sends
639 // Release asynchronously after close() returns. Without this, a
640 // stat() immediately after close() could see stale cached attrs.
641 func (fh *FileHandle) Flush(_ context.Context) syscall.Errno {
642 fh.mu.Lock()
643 defer fh.mu.Unlock()
644
645 err := fh.fd.Flush()
646 if fh.inode != nil {
647 _ = fh.inode.NotifyContent(0, 0)
648 }
649 return fs.ToErrno(err)
650 }
651
652 // Release closes the descriptor and invalidates the kernel's cached
653 // content and attrs so readers opening the same path see the new data.
654 // Invalidation happens here (not in Flush) because fd.Close commits
655 // the final DAG node; Flush alone may not have the final size yet.
656 func (fh *FileHandle) Release(_ context.Context) syscall.Errno {
657 fh.mu.Lock()
658 defer fh.mu.Unlock()
659
660 err := fh.fd.Close()
661 if fh.inode != nil {
662 _ = fh.inode.NotifyContent(0, 0)
663 }
664 return fs.ToErrno(err)
665 }
666
667 // Fsync flushes the write buffer through the open file descriptor and
668 // invalidates the kernel's cached attrs and content for this inode.
669 // Editors (vim, emacs) and databases call fsync after writing to
670 // ensure data reaches persistent storage; a fresh reader on the same
671 // path must see the synced bytes immediately, not the size the kernel
672 // cached from the initial Create response.
673 func (fh *FileHandle) Fsync(_ context.Context, _ uint32) syscall.Errno {
674 fh.mu.Lock()
675 defer fh.mu.Unlock()
676
677 err := fh.fd.Flush()
678 if fh.inode != nil {
679 _ = fh.inode.NotifyContent(0, 0)
680 }
681 return fs.ToErrno(err)
682 }
683
684 // Symlink is the FUSE adapter for UnixFS TSymlink nodes on writable mounts.
685 // Target is resolved once at Lookup/Create time and never changes
686 // (POSIX symlinks are immutable; changing the target requires unlink + symlink).
687 type Symlink struct {
688 fs.Inode
689 Target string
690 MFSFile *mfs.File // backing MFS node for mtime persistence
691 Cfg *Config
692 }
693
694 func (s *Symlink) Readlink(_ context.Context) ([]byte, syscall.Errno) {
695 return []byte(s.Target), 0
696 }
697
698 func (s *Symlink) fillAttr(a *fuse.Attr) {
699 a.Mode = uint32(fusemnt.SymlinkMode.Perm())
700 a.Size = uint64(len(s.Target))
701 a.Blocks = fusemnt.SizeToStatBlocks(a.Size)
702 a.Blksize = s.Cfg.Blksize
703 if s.MFSFile != nil {
704 if t, err := s.MFSFile.ModTime(); err == nil && !t.IsZero() {
705 a.SetTimes(nil, &t, nil)
706 }
707 }
708 }
709
710 func (s *Symlink) Getattr(_ context.Context, _ fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
711 s.fillAttr(&out.Attr)
712 return 0
713 }
714
715 // Setattr handles mtime changes on symlinks.
716 // Tools like rsync call lutimes on symlinks after creating them and
717 // treat ENOTSUP as an error. Every major FUSE filesystem (gocryptfs,
718 // rclone, sshfs, s3fs) implements Setattr on symlinks for this reason.
719 //
720 // Mode is always 0777 per POSIX convention (access control uses the
721 // target's mode), so chmod requests are silently accepted but not stored.
722 func (s *Symlink) Setattr(_ context.Context, _ fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno {
723 if s.MFSFile != nil {
724 if mtime, ok := in.GetMTime(); ok && s.Cfg.StoreMtime {
725 if err := s.MFSFile.SetModTime(mtime); err != nil {
726 return fs.ToErrno(err)
727 }
728 }
729 }
730 s.fillAttr(&out.Attr)
731 return 0
732 }
733
734 // roFileHandle is a read-only file handle backed by a DagReader.
735 // Used for O_RDONLY opens to bypass MFS's desclock (see FileInode.Open).
736 type roFileHandle struct {
737 r uio.DagReader
738 mu sync.Mutex
739 }
740
741 func (fh *roFileHandle) Read(ctx context.Context, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
742 fh.mu.Lock()
743 defer fh.mu.Unlock()
744
745 if _, err := fh.r.Seek(off, io.SeekStart); err != nil {
746 return nil, fs.ToErrno(err)
747 }
748 n, err := fh.r.CtxReadFull(ctx, dest)
749 switch err {
750 case nil, io.EOF, io.ErrUnexpectedEOF:
751 default:
752 return nil, fusemnt.ReadErrno(err)
753 }
754 return fuse.ReadResultData(dest[:n]), 0
755 }
756
757 func (fh *roFileHandle) Release(_ context.Context) syscall.Errno {
758 fh.mu.Lock()
759 defer fh.mu.Unlock()
760
761 return fs.ToErrno(fh.r.Close())
762 }
763
764 // SymlinkTarget extracts the symlink target from an MFS file, or
765 // returns "" if the file is not a TSymlink node. MFS represents
766 // symlinks as *mfs.File, so the DAG node's UnixFS type must be checked.
767 func SymlinkTarget(f *mfs.File) string {
768 nd, err := f.GetNode()
769 if err != nil {
770 return ""
771 }
772 fsn, err := ft.ExtractFSNode(nd)
773 if err != nil {
774 return ""
775 }
776 if fsn.Type() != ft.TSymlink {
777 return ""
778 }
779 return string(fsn.Data())
780 }
781
782 // Interface compliance checks.
783 var (
784 _ fs.NodeGetattrer = (*Dir)(nil)
785 _ fs.NodeStatfser = (*Dir)(nil)
786 _ fs.NodeSetattrer = (*Dir)(nil)
787 _ fs.NodeLookuper = (*Dir)(nil)
788 _ fs.NodeReaddirer = (*Dir)(nil)
789 _ fs.NodeMkdirer = (*Dir)(nil)
790 _ fs.NodeUnlinker = (*Dir)(nil)
791 _ fs.NodeRmdirer = (*Dir)(nil)
792 _ fs.NodeRenamer = (*Dir)(nil)
793 _ fs.NodeCreater = (*Dir)(nil)
794 _ fs.NodeSymlinker = (*Dir)(nil)
795 _ fs.NodeGetxattrer = (*Dir)(nil)
796 _ fs.NodeListxattrer = (*Dir)(nil)
797
798 _ fs.NodeGetattrer = (*FileInode)(nil)
799 _ fs.NodeOpener = (*FileInode)(nil)
800 _ fs.NodeSetattrer = (*FileInode)(nil)
801 _ fs.NodeGetxattrer = (*FileInode)(nil)
802 _ fs.NodeListxattrer = (*FileInode)(nil)
803
804 _ fs.NodeGetattrer = (*Symlink)(nil)
805 _ fs.NodeSetattrer = (*Symlink)(nil)
806 _ fs.NodeReadlinker = (*Symlink)(nil)
807
808 _ fs.FileReader = (*FileHandle)(nil)
809 _ fs.FileWriter = (*FileHandle)(nil)
810 _ fs.FileFlusher = (*FileHandle)(nil)
811 _ fs.FileReleaser = (*FileHandle)(nil)
812 _ fs.FileFsyncer = (*FileHandle)(nil)
813
814 _ fs.FileReader = (*roFileHandle)(nil)
815 _ fs.FileReleaser = (*roFileHandle)(nil)
816 )