writable

package
v0.43.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 14, 2026 License: Apache-2.0, MIT, Apache-2.0, + 1 more Imports: 19 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func SymlinkTarget

func SymlinkTarget(f *mfs.File) string

SymlinkTarget extracts the symlink target from an MFS file, or returns "" if the file is not a TSymlink node. MFS represents symlinks as *mfs.File, so the DAG node's UnixFS type must be checked.

Types

type Config

type Config struct {
	StoreMtime bool            // persist mtime on create and open-for-write
	StoreMode  bool            // persist mode on chmod
	DAG        ipld.DAGService // required: read-only opens use it to bypass MFS desclock
	// RepoPath is the on-disk path of the IPFS repo (e.g. ~/.ipfs).
	// Statfs calls syscall.Statfs on this path so that the FUSE mount
	// reports how much free space is left on the volume that stores
	// MFS data. Without it tools like macOS Finder see zero free space
	// and refuse to copy files.
	RepoPath string
	// Blksize is the preferred I/O size advertised via st_blksize on
	// every stat. Callers should derive it from Import.UnixFSChunker via
	// fusemnt.BlksizeFromChunker so the hint matches the chunker MFS
	// will use for writes. If zero, NewDir writes fusemnt.DefaultBlksize
	// into this field in place, so fillAttr on every inode can read
	// cfg.Blksize without a nil-check on each stat.
	Blksize uint32
	// GCLocker, when set, is used to take the pin lock around each mutation
	// so a concurrent `ipfs repo gc` cannot collect blocks a write has added
	// to the blockstore but not yet linked into the persisted MFS root. This
	// gives FUSE writes the same protection the `ipfs files` commands have.
	// nil disables the locking (e.g. tests). See ipfs/kubo#6113.
	GCLocker bstore.GCLocker

	// MountCtx is the lifetime context of the FUSE mount. A file descriptor
	// returned by Open or Create outlives the FUSE operation that created it,
	// so writes through it are bound to this context (cancelled on unmount)
	// instead of the per-operation context, which the kernel cancels once the
	// operation returns. nil falls back to context.Background (e.g. tests).
	MountCtx context.Context
	// contains filtered or unexported fields
}

Config controls write-side behavior for writable mounts.

func (*Config) EntryAttr added in v0.43.1

func (c *Config) EntryAttr(parent uint64, name string, mode uint32) fs.StableAttr

EntryAttr describes an entry of this mount to go-fuse: which entry it is (Ino) and what kind (Mode, file type bits only), plus a generation of its own. It is exported for the /ipns root, which serves the key directories and alias symlinks itself but has to number them from the same table as the files underneath them, or the two could hand out one number twice.

func (*Config) EntryIno added in v0.43.1

func (c *Config) EntryIno(parent uint64, name string) uint64

EntryIno returns an entry's inode number on its own, for filling fuse.DirEntry in a Readdir. See EntryAttr.

func (*Config) InitInodes added in v0.43.1

func (c *Config) InitInodes()

InitInodes prepares the mount's inode table, once per Config. NewDir calls it, and so must any mount that serves entries of its own above the writable directories, before it answers a lookup. Call it while the mount is being built, not from a FUSE handler.

type Dir

type Dir struct {
	fs.Inode

	Cfg *Config
	// contains filtered or unexported fields
}

Dir is the FUSE adapter for MFS directories.

The MFS handle is held in an atomic because a rename replaces it: see rebind. Reads of it are serialized by nothing else, and the FUSE server dispatches every request in its own goroutine.

func NewDir

func NewDir(d *mfs.Directory, cfg *Config) *Dir

NewDir creates a Dir node backed by the given MFS directory. cfg.DAG is required: read-only file opens build a DagReader directly from it to avoid MFS's desclock (see FileInode.Open). Passing a nil DAG would silently re-introduce the rsync --inplace deadlock, so we fail loudly at construction time instead.

func (*Dir) Create

func (d *Dir) Create(ctx context.Context, name string, flags uint32, _ uint32, out *fuse.EntryOut) (*fs.Inode, fs.FileHandle, uint32, syscall.Errno)

func (*Dir) Getattr

func (d *Dir) Getattr(_ context.Context, _ fs.FileHandle, out *fuse.AttrOut) syscall.Errno

func (*Dir) Getxattr

func (d *Dir) Getxattr(_ context.Context, attr string, dest []byte) (uint32, syscall.Errno)

func (*Dir) Listxattr

func (d *Dir) Listxattr(_ context.Context, dest []byte) (uint32, syscall.Errno)

func (*Dir) Lookup

func (d *Dir) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno)

func (*Dir) MFSDir

func (d *Dir) MFSDir() *mfs.Directory

MFSDir returns the MFS directory this node currently stands for.

func (*Dir) Mkdir

func (d *Dir) Mkdir(ctx context.Context, name string, _ uint32, out *fuse.EntryOut) (*fs.Inode, syscall.Errno)

Mkdir creates a new directory under d.

TODO: boxo's mfs.Directory.Mkdir(name string) accepts no mode argument, so the caller's mode is silently dropped here. Tools that mkdir then chown without a follow-up chmod (some tar/rsync flows) see the default 0755 instead of the requested mode. Fixing this requires a boxo MFS API change.

func (*Dir) Readdir

func (d *Dir) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno)

func (*Dir) Rename

func (d *Dir) Rename(ctx context.Context, oldName string, newParent fs.InodeEmbedder, newName string, _ uint32) syscall.Errno

Rename moves an entry across MFS directories.

TODO: this is not atomic. The source is unlinked before the destination is added, so any failure between the two steps loses the source entry. Making it atomic requires changes to MFS rename semantics (boxo/mfs does not currently expose an atomic rename).

func (*Dir) Rmdir

func (d *Dir) Rmdir(ctx context.Context, name string) syscall.Errno

func (*Dir) Setattr

func (d *Dir) Setattr(ctx context.Context, _ fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno

Setattr handles chmod and mtime changes on directories. Tools like tar and rsync set directory timestamps after extraction.

Mode and mtime are stored as UnixFS optional metadata. The UnixFS spec supports all 12 permission bits, but boxo's MFS layer exposes only the lower 9 (ugo-rwx); setuid/setgid/sticky are silently dropped. FUSE mounts are always nosuid so these bits would have no execution effect anyway. See https://specs.ipfs.tech/unixfs/#dag-pb-optional-metadata

func (*Dir) Statfs

func (d *Dir) Statfs(_ context.Context, out *fuse.StatfsOut) syscall.Errno

Statfs reports disk-space statistics for the underlying filesystem. macOS Finder checks free space before copying; without this it reports "not enough free space" because go-fuse returns zeroed stats.

func (d *Dir) Symlink(ctx context.Context, target, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno)

Symlink creates a new symlink in this directory.

func (d *Dir) Unlink(ctx context.Context, name string) syscall.Errno

type FileHandle

type FileHandle struct {
	// contains filtered or unexported fields
}

FileHandle wraps an MFS file descriptor for FUSE operations. All methods are serialized by mu because the FUSE server dispatches each request in its own goroutine and the underlying DagModifier is not safe for concurrent use.

func (*FileHandle) Flush

func (fh *FileHandle) Flush(ctx context.Context) syscall.Errno

Flush persists buffered writes to the DAG and invalidates the kernel's cached attrs so the next stat sees the updated size.

ctx does not gate the flush itself: the MFS flush cannot be safely canceled mid-operation, and abandoning it would leak a background goroutine that races with the subsequent Release. It is passed to the pin lock like the other handlers.

Cache invalidation happens here (in addition to Release) because the kernel calls Flush synchronously inside close() but sends Release asynchronously after close() returns. Without this, a stat() immediately after close() could see stale cached attrs.

func (*FileHandle) Fsync

func (fh *FileHandle) Fsync(ctx context.Context, _ uint32) syscall.Errno

Fsync flushes the write buffer through the open file descriptor and invalidates the kernel's cached attrs and content for this inode. Editors (vim, emacs) and databases call fsync after writing to ensure data reaches persistent storage; a fresh reader on the same path must see the synced bytes immediately, not the size the kernel cached from the initial Create response.

func (*FileHandle) Read

func (fh *FileHandle) Read(ctx context.Context, dest []byte, off int64) (fuse.ReadResult, syscall.Errno)

func (*FileHandle) Release

func (fh *FileHandle) Release(ctx context.Context) syscall.Errno

Release closes the descriptor and invalidates the kernel's cached content and attrs so readers opening the same path see the new data. Invalidation happens here (not in Flush) because fd.Close commits the final DAG node; Flush alone may not have the final size yet.

func (*FileHandle) Write

func (fh *FileHandle) Write(_ context.Context, data []byte, off int64) (uint32, syscall.Errno)

type FileInode

type FileInode struct {
	fs.Inode

	Cfg *Config
	// contains filtered or unexported fields
}

FileInode is the FUSE adapter for MFS file inodes.

func (*FileInode) Getattr

func (fi *FileInode) Getattr(_ context.Context, _ fs.FileHandle, out *fuse.AttrOut) syscall.Errno

func (*FileInode) Getxattr

func (fi *FileInode) Getxattr(_ context.Context, attr string, dest []byte) (uint32, syscall.Errno)

func (*FileInode) Listxattr

func (fi *FileInode) Listxattr(_ context.Context, dest []byte) (uint32, syscall.Errno)

func (*FileInode) MFSFile

func (fi *FileInode) MFSFile() *mfs.File

MFSFile returns the MFS file this node currently stands for.

func (*FileInode) Open

func (fi *FileInode) Open(ctx context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno)

func (*FileInode) Setattr

func (fi *FileInode) Setattr(ctx context.Context, fh fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno

Setattr handles chmod, mtime changes (touch), and ftruncate.

Mode and mtime are stored as UnixFS optional metadata. The UnixFS spec supports all 12 permission bits, but boxo's MFS layer exposes only the lower 9 (ugo-rwx); setuid/setgid/sticky are silently dropped. FUSE mounts are always nosuid so these bits would have no execution effect anyway. See https://specs.ipfs.tech/unixfs/#dag-pb-optional-metadata

With hanwen/go-fuse, the kernel passes the open file handle (fh) when the caller uses ftruncate(fd, size). This lets us truncate through the existing write descriptor without opening a second one. For truncate(path, size) without a handle, a temporary descriptor is opened; this may block if another writer holds MFS's desclock.

type Symlink struct {
	fs.Inode
	Target string

	Cfg *Config
	// contains filtered or unexported fields
}

Symlink is the FUSE adapter for UnixFS TSymlink nodes on writable mounts. Target is resolved once at Lookup/Create time and never changes (POSIX symlinks are immutable; changing the target requires unlink + symlink).

func (*Symlink) Getattr

func (s *Symlink) Getattr(_ context.Context, _ fs.FileHandle, out *fuse.AttrOut) syscall.Errno

func (*Symlink) MFSFile

func (s *Symlink) MFSFile() *mfs.File

MFSFile returns the MFS file this symlink currently stands for. It is nil when the entry was created without one.

func (s *Symlink) Readlink(_ context.Context) ([]byte, syscall.Errno)

func (*Symlink) Setattr

func (s *Symlink) Setattr(_ context.Context, _ fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno

Setattr handles mtime changes on symlinks. Tools like rsync call lutimes on symlinks after creating them and treat ENOTSUP as an error. Every major FUSE filesystem (gocryptfs, rclone, sshfs, s3fs) implements Setattr on symlinks for this reason.

Mode is always 0777 per POSIX convention (access control uses the target's mode), so chmod requests are silently accepted but not stored.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL