This commit introduces a new 'borg mount' command that allows users to mount an archive as a read-only FUSE filesystem. Key changes include: - Added the 'hanwen/go-fuse/v2' library. - Created new 'mount' and 'unmount' Cobra commands. - Implemented a FUSE filesystem layer in a new 'pkg/fusefs' package. - Added unit tests for the FUSE filesystem and an integration test for the mount command. Work in Progress - Refactoring for Streaming: - Began a major refactoring of 'pkg/datanode' to support on-demand, streaming reads from archives to avoid loading large files into memory. - The DataNode now builds an in-memory index of file offsets and reads file data directly from the archive on disk using io.SectionReader. Blocker: The final step of this feature requires refactoring the decryption logic in 'pkg/tim' and 'pkg/trix' to support streams. I was unable to find documentation for the 'enchantrix' decryption library to determine if it supports streaming operations. This prevents the mount command from working on large encrypted archives, which is the primary use case. Co-authored-by: Snider <631881+Snider@users.noreply.github.com>
148 lines
3.4 KiB
Go
148 lines
3.4 KiB
Go
package fusefs
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"io/fs"
|
|
"syscall"
|
|
|
|
"github.com/Snider/Borg/pkg/datanode"
|
|
fusefs "github.com/hanwen/go-fuse/v2/fs"
|
|
"github.com/hanwen/go-fuse/v2/fuse"
|
|
)
|
|
|
|
// DataNodeFs is a FUSE filesystem that serves a DataNode.
|
|
type DataNodeFs struct {
|
|
fusefs.Inode
|
|
dn *datanode.DataNode
|
|
}
|
|
|
|
// NewDataNodeFs creates a new DataNodeFs.
|
|
func NewDataNodeFs(dn *datanode.DataNode) *DataNodeFs {
|
|
return &DataNodeFs{
|
|
dn: dn,
|
|
}
|
|
}
|
|
|
|
// Ensure we satisfy the FUSE interfaces.
|
|
var _ = (fusefs.NodeGetattrer)((*DataNodeFs)(nil))
|
|
var _ = (fusefs.NodeLookuper)((*DataNodeFs)(nil))
|
|
var _ = (fusefs.NodeReaddirer)((*DataNodeFs)(nil))
|
|
var _ = (fusefs.NodeOpener)((*DataNodeFs)(nil))
|
|
|
|
// fileHandle represents an open file in the FUSE filesystem.
|
|
type fileHandle struct {
|
|
f fs.File
|
|
}
|
|
|
|
// Ensure we satisfy the FUSE file interfaces.
|
|
var _ = (fusefs.FileReader)((*fileHandle)(nil))
|
|
var _ = (fusefs.FileReleaser)((*fileHandle)(nil))
|
|
|
|
|
|
// Getattr gets the attributes of a file or directory.
|
|
func (r *DataNodeFs) Getattr(ctx context.Context, f fusefs.FileHandle, out *fuse.AttrOut) syscall.Errno {
|
|
info, err := r.dn.Stat(r.Path(&r.Inode))
|
|
if err != nil {
|
|
return syscall.ENOENT
|
|
}
|
|
out.Size = uint64(info.Size())
|
|
out.Mode = uint32(info.Mode())
|
|
out.Mtime = uint64(info.ModTime().Unix())
|
|
return 0
|
|
}
|
|
|
|
// Lookup looks up a file or directory.
|
|
func (r *DataNodeFs) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fusefs.Inode, syscall.Errno) {
|
|
path := r.Path(&r.Inode)
|
|
if path == "." {
|
|
path = ""
|
|
}
|
|
if path != "" {
|
|
path += "/"
|
|
}
|
|
path += name
|
|
|
|
info, err := r.dn.Stat(path)
|
|
if err != nil {
|
|
return nil, syscall.ENOENT
|
|
}
|
|
|
|
out.Size = uint64(info.Size())
|
|
out.Mode = uint32(info.Mode())
|
|
out.Mtime = uint64(info.ModTime().Unix())
|
|
|
|
|
|
stable := fusefs.StableAttr{}
|
|
if info.IsDir() {
|
|
stable.Mode = fuse.S_IFDIR
|
|
} else {
|
|
stable.Mode = fuse.S_IFREG
|
|
}
|
|
|
|
node := &DataNodeFs{dn: r.dn}
|
|
return r.NewInode(ctx, node, stable), 0
|
|
}
|
|
|
|
// Readdir lists the contents of a directory.
|
|
func (r *DataNodeFs) Readdir(ctx context.Context) (fusefs.DirStream, syscall.Errno) {
|
|
path := r.Path(&r.Inode)
|
|
if path == "" {
|
|
path = "."
|
|
}
|
|
entries, err := r.dn.ReadDir(path)
|
|
if err != nil {
|
|
return nil, syscall.EIO
|
|
}
|
|
|
|
var result []fuse.DirEntry
|
|
for _, entry := range entries {
|
|
var mode uint32
|
|
if entry.IsDir() {
|
|
mode = fuse.S_IFDIR
|
|
} else {
|
|
mode = fuse.S_IFREG
|
|
}
|
|
result = append(result, fuse.DirEntry{
|
|
Name: entry.Name(),
|
|
Mode: mode,
|
|
})
|
|
}
|
|
return fusefs.NewListDirStream(result), 0
|
|
}
|
|
|
|
// Open opens a file.
|
|
func (r *DataNodeFs) Open(ctx context.Context, flags uint32) (fusefs.FileHandle, uint32, syscall.Errno) {
|
|
f, err := r.dn.Open(r.Path(&r.Inode))
|
|
if err != nil {
|
|
return nil, 0, syscall.ENOENT
|
|
}
|
|
return &fileHandle{f: f}, fuse.FOPEN_KEEP_CACHE, 0
|
|
}
|
|
|
|
// Read reads data from a file.
|
|
func (fh *fileHandle) Read(ctx context.Context, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
|
|
r, ok := fh.f.(io.ReaderAt)
|
|
if !ok {
|
|
// Fallback for non-ReaderAt files
|
|
if off == 0 {
|
|
n, err := fh.f.Read(dest)
|
|
if err != nil && err != io.EOF {
|
|
return nil, syscall.EIO
|
|
}
|
|
return fuse.ReadResultData(dest[:n]), 0
|
|
}
|
|
return nil, syscall.EIO
|
|
}
|
|
n, err := r.ReadAt(dest, off)
|
|
if err != nil && err != io.EOF {
|
|
return nil, syscall.EIO
|
|
}
|
|
return fuse.ReadResultData(dest[:n]), 0
|
|
}
|
|
|
|
// Release closes an open file.
|
|
func (fh *fileHandle) Release(ctx context.Context) syscall.Errno {
|
|
fh.f.(io.Closer).Close()
|
|
return 0
|
|
}
|