Merge 673dfde919 into a77024aad4
This commit is contained in:
commit
00ad1600fc
8 changed files with 494 additions and 72 deletions
97
cmd/mount.go
Normal file
97
cmd/mount.go
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/Snider/Borg/pkg/datanode"
|
||||
"github.com/Snider/Borg/pkg/fusefs"
|
||||
"github.com/Snider/Borg/pkg/tim"
|
||||
"github.com/Snider/Borg/pkg/trix"
|
||||
"github.com/hanwen/go-fuse/v2/fs"
|
||||
"github.com/hanwen/go-fuse/v2/fuse"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var mountCmd = NewMountCmd()
|
||||
|
||||
func NewMountCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "mount [archive] [mountpoint]",
|
||||
Short: "Mount an archive as a read-only filesystem",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
archiveFile := args[0]
|
||||
mountpoint := args[1]
|
||||
password, _ := cmd.Flags().GetString("password")
|
||||
|
||||
data, err := os.ReadFile(archiveFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var dn *datanode.DataNode
|
||||
if strings.HasSuffix(archiveFile, ".stim") || (len(data) >= 4 && string(data[:4]) == "STIM") {
|
||||
if password == "" {
|
||||
return fmt.Errorf("password required for .stim files")
|
||||
}
|
||||
m, err := tim.FromSigil(data, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tarball, err := m.ToTar()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dn, err = datanode.FromTar(tarball)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// This handles .dat, .tar, .trix, and .tim files
|
||||
dn, err = trix.FromTrix(data, password)
|
||||
if err != nil {
|
||||
// If FromTrix fails, try FromTar as a fallback for plain tarballs
|
||||
if dn, err = datanode.FromTar(data); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root := fusefs.NewDataNodeFs(dn)
|
||||
server, err := fs.Mount(mountpoint, root, &fs.Options{
|
||||
MountOptions: fuse.MountOptions{
|
||||
Debug: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-c
|
||||
server.Unmount()
|
||||
}()
|
||||
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Archive mounted at %s. Press Ctrl+C to unmount.\n", mountpoint)
|
||||
server.Wait()
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringP("password", "p", "", "Password for encrypted archives")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func GetMountCmd() *cobra.Command {
|
||||
return mountCmd
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(GetMountCmd())
|
||||
}
|
||||
108
cmd/mount_test.go
Normal file
108
cmd/mount_test.go
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Snider/Borg/pkg/datanode"
|
||||
"github.com/hanwen/go-fuse/v2/fuse"
|
||||
)
|
||||
|
||||
func TestMountCommand(t *testing.T) {
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skip("skipping mount test on non-linux systems")
|
||||
}
|
||||
if _, err := exec.LookPath("fusermount"); err != nil {
|
||||
t.Skip("fusermount not found, skipping mount test")
|
||||
}
|
||||
|
||||
// Create a temporary directory for the mount point
|
||||
mountDir := t.TempDir()
|
||||
|
||||
// Create a dummy archive
|
||||
dn := datanode.New()
|
||||
dn.AddData("test.txt", []byte("hello"))
|
||||
tarball, err := dn.ToTar()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
archiveFile := filepath.Join(t.TempDir(), "test.dat")
|
||||
if err := os.WriteFile(archiveFile, tarball, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Run the mount command in the background
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cmd := NewMountCmd()
|
||||
cmd.SetArgs([]string{archiveFile, mountDir})
|
||||
|
||||
cmdErrCh := make(chan error, 1)
|
||||
go func() {
|
||||
cmdErrCh <- cmd.ExecuteContext(ctx)
|
||||
}()
|
||||
|
||||
// Give the FUSE server a moment to start up and stabilize
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
// Check that the mount is active.
|
||||
mounted, err := isMounted(mountDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check if mounted: %v", err)
|
||||
}
|
||||
if !mounted {
|
||||
t.Errorf("Mount directory does not appear to be mounted.")
|
||||
}
|
||||
|
||||
|
||||
// Cancel the context to stop the command
|
||||
cancel()
|
||||
|
||||
// Wait for the command to exit
|
||||
select {
|
||||
case err := <-cmdErrCh:
|
||||
if err != nil && err != context.Canceled {
|
||||
t.Errorf("Mount command returned an unexpected error: %v", err)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Error("Mount command did not exit after context was canceled.")
|
||||
}
|
||||
|
||||
// Give the filesystem a moment to unmount.
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
|
||||
// Verify that the mount point is now unmounted.
|
||||
mounted, err = isMounted(mountDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check if unmounted: %v", err)
|
||||
}
|
||||
if mounted {
|
||||
// As a fallback, try to unmount it manually.
|
||||
server, _ := fuse.NewServer(nil, mountDir, nil)
|
||||
server.Unmount()
|
||||
t.Errorf("Mount directory was not unmounted cleanly.")
|
||||
}
|
||||
}
|
||||
|
||||
// isMounted checks if a directory is a FUSE mount point.
|
||||
// This is a simple heuristic and might not be 100% reliable.
|
||||
func isMounted(path string) (bool, error) {
|
||||
// On Linux, we can check the mountinfo.
|
||||
if runtime.GOOS == "linux" {
|
||||
data, err := os.ReadFile("/proc/self/mountinfo")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return strings.Contains(string(data), path) && strings.Contains(string(data), "fuse"), nil
|
||||
}
|
||||
return false, nil // Not implemented for other OSes.
|
||||
}
|
||||
50
cmd/unmount.go
Normal file
50
cmd/unmount.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
||||
"github.com/hanwen/go-fuse/v2/fuse"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var unmountCmd = NewUnmountCmd()
|
||||
|
||||
func NewUnmountCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "unmount [mountpoint]",
|
||||
Short: "Unmount a filesystem",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
mountpoint := args[0]
|
||||
server, _ := fuse.NewServer(nil, mountpoint, nil)
|
||||
err := server.Unmount()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fallback to system commands
|
||||
var unmountCmd *exec.Cmd
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
unmountCmd = exec.Command("fusermount", "-u", mountpoint)
|
||||
case "darwin":
|
||||
unmountCmd = exec.Command("umount", mountpoint)
|
||||
default:
|
||||
return fmt.Errorf("unmount not supported on %s: %v", runtime.GOOS, err)
|
||||
}
|
||||
|
||||
return unmountCmd.Run()
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func GetUnmountCmd() *cobra.Command {
|
||||
return unmountCmd
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(GetUnmountCmd())
|
||||
}
|
||||
1
go.mod
1
go.mod
|
|
@ -34,6 +34,7 @@ require (
|
|||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/hanwen/go-fuse/v2 v2.9.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -59,6 +59,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hanwen/go-fuse/v2 v2.9.0 h1:0AOGUkHtbOVeyGLr0tXupiid1Vg7QB7M6YUcdmVdC58=
|
||||
github.com/hanwen/go-fuse/v2 v2.9.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
|
|
|
|||
|
|
@ -18,23 +18,39 @@ var (
|
|||
ErrPasswordRequired = errors.New("password required")
|
||||
)
|
||||
|
||||
// DataNode is an in-memory filesystem that is compatible with fs.FS.
|
||||
// DataNode is a filesystem that reads from a tar archive on demand.
|
||||
type DataNode struct {
|
||||
files map[string]*dataFile
|
||||
archive io.ReaderAt
|
||||
files map[string]*fileIndex
|
||||
}
|
||||
|
||||
// fileIndex stores the metadata for a file in the archive.
|
||||
type fileIndex struct {
|
||||
name string
|
||||
offset int64
|
||||
size int64
|
||||
modTime time.Time
|
||||
}
|
||||
|
||||
// New creates a new, empty DataNode.
|
||||
func New() *DataNode {
|
||||
return &DataNode{files: make(map[string]*dataFile)}
|
||||
func New(archive io.ReaderAt) *DataNode {
|
||||
return &DataNode{
|
||||
archive: archive,
|
||||
files: make(map[string]*fileIndex),
|
||||
}
|
||||
}
|
||||
|
||||
// FromTar creates a new DataNode from a tarball.
|
||||
func FromTar(tarball []byte) (*DataNode, error) {
|
||||
dn := New()
|
||||
tarReader := tar.NewReader(bytes.NewReader(tarball))
|
||||
func FromTar(archive io.ReaderAt) (*DataNode, error) {
|
||||
dn := New(archive)
|
||||
if seeker, ok := archive.(io.Seeker); ok {
|
||||
seeker.Seek(0, io.SeekStart)
|
||||
}
|
||||
|
||||
offset := int64(0)
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
headerData := make([]byte, 512)
|
||||
_, err := archive.ReadAt(headerData, offset)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
|
@ -42,68 +58,54 @@ func FromTar(tarball []byte) (*DataNode, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if header.Typeflag == tar.TypeReg {
|
||||
data, err := io.ReadAll(tarReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dn.AddData(header.Name, data)
|
||||
header, err := tar.NewReader(bytes.NewReader(headerData)).Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
offset += 512
|
||||
if header.Typeflag == tar.TypeReg {
|
||||
dn.files[header.Name] = &fileIndex{
|
||||
name: header.Name,
|
||||
offset: offset,
|
||||
size: header.Size,
|
||||
modTime: header.ModTime,
|
||||
}
|
||||
offset += header.Size
|
||||
if remainder := header.Size % 512; remainder != 0 {
|
||||
offset += 512 - remainder
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return dn, nil
|
||||
}
|
||||
|
||||
// ToTar serializes the DataNode to a tarball.
|
||||
// This function will need to be re-implemented to read from the archive.
|
||||
// For now, it will return an error.
|
||||
func (d *DataNode) ToTar() ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
tw := tar.NewWriter(buf)
|
||||
|
||||
for _, file := range d.files {
|
||||
hdr := &tar.Header{
|
||||
Name: file.name,
|
||||
Mode: 0600,
|
||||
Size: int64(len(file.content)),
|
||||
ModTime: file.modTime,
|
||||
}
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := tw.Write(file.content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tw.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
return nil, errors.New("ToTar is not implemented for streaming DataNodes")
|
||||
}
|
||||
|
||||
// AddData adds a file to the DataNode.
|
||||
// AddData is not supported for streaming DataNodes.
|
||||
func (d *DataNode) AddData(name string, content []byte) {
|
||||
name = strings.TrimPrefix(name, "/")
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
// Directories are implicit, so we don't store them.
|
||||
// A name ending in "/" is treated as a directory.
|
||||
if strings.HasSuffix(name, "/") {
|
||||
return
|
||||
}
|
||||
d.files[name] = &dataFile{
|
||||
name: name,
|
||||
content: content,
|
||||
modTime: time.Now(),
|
||||
}
|
||||
// This is a no-op for now.
|
||||
}
|
||||
|
||||
// Open opens a file from the DataNode.
|
||||
func (d *DataNode) Open(name string) (fs.File, error) {
|
||||
name = strings.TrimPrefix(name, "/")
|
||||
if file, ok := d.files[name]; ok {
|
||||
return &dataFileReader{file: file}, nil
|
||||
sectionReader := io.NewSectionReader(d.archive, file.offset, file.size)
|
||||
return &dataFileReader{
|
||||
file: file,
|
||||
reader: sectionReader,
|
||||
}, nil
|
||||
}
|
||||
// Check if it's a directory
|
||||
prefix := name + "/"
|
||||
|
|
@ -231,7 +233,7 @@ func (d *DataNode) Walk(root string, fn fs.WalkDirFunc, opts ...WalkOptions) err
|
|||
if len(opts) > 0 {
|
||||
maxDepth = opts[0].MaxDepth
|
||||
filter = opts[0].Filter
|
||||
skipErrors = opts[0].SkipErrors
|
||||
_ skipErrors = opts[0].SkipErrors
|
||||
}
|
||||
|
||||
return fs.WalkDir(d, root, func(path string, de fs.DirEntry, err error) error {
|
||||
|
|
@ -294,22 +296,13 @@ func (d *DataNode) CopyFile(sourcePath string, target string, perm os.FileMode)
|
|||
return err
|
||||
}
|
||||
|
||||
// dataFile represents a file in the DataNode.
|
||||
type dataFile struct {
|
||||
name string
|
||||
content []byte
|
||||
modTime time.Time
|
||||
}
|
||||
|
||||
func (d *dataFile) Stat() (fs.FileInfo, error) { return &dataFileInfo{file: d}, nil }
|
||||
func (d *dataFile) Read(p []byte) (int, error) { return 0, io.EOF }
|
||||
func (d *dataFile) Close() error { return nil }
|
||||
func (d *fileIndex) Stat() (fs.FileInfo, error) { return &dataFileInfo{file: d}, nil }
|
||||
|
||||
// dataFileInfo implements fs.FileInfo for a dataFile.
|
||||
type dataFileInfo struct{ file *dataFile }
|
||||
type dataFileInfo struct{ file *fileIndex }
|
||||
|
||||
func (d *dataFileInfo) Name() string { return path.Base(d.file.name) }
|
||||
func (d *dataFileInfo) Size() int64 { return int64(len(d.file.content)) }
|
||||
func (d *dataFileInfo) Size() int64 { return d.file.size }
|
||||
func (d *dataFileInfo) Mode() fs.FileMode { return 0444 }
|
||||
func (d *dataFileInfo) ModTime() time.Time { return d.file.modTime }
|
||||
func (d *dataFileInfo) IsDir() bool { return false }
|
||||
|
|
@ -317,16 +310,16 @@ func (d *dataFileInfo) Sys() interface{} { return nil }
|
|||
|
||||
// dataFileReader implements fs.File for a dataFile.
|
||||
type dataFileReader struct {
|
||||
file *dataFile
|
||||
reader *bytes.Reader
|
||||
file *fileIndex
|
||||
reader io.ReaderAt
|
||||
}
|
||||
|
||||
func (d *dataFileReader) Stat() (fs.FileInfo, error) { return d.file.Stat() }
|
||||
func (d *dataFileReader) Read(p []byte) (int, error) {
|
||||
if d.reader == nil {
|
||||
d.reader = bytes.NewReader(d.file.content)
|
||||
}
|
||||
return d.reader.Read(p)
|
||||
return 0, &fs.PathError{Op: "read", Path: d.file.name, Err: fs.ErrInvalid}
|
||||
}
|
||||
func (d *dataFileReader) ReadAt(p []byte, off int64) (n int, err error) {
|
||||
return d.reader.ReadAt(p, off)
|
||||
}
|
||||
func (d *dataFileReader) Close() error { return nil }
|
||||
|
||||
|
|
|
|||
148
pkg/fusefs/fs.go
Normal file
148
pkg/fusefs/fs.go
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
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
|
||||
}
|
||||
23
pkg/fusefs/fs_test.go
Normal file
23
pkg/fusefs/fs_test.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package fusefs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/Snider/Borg/pkg/datanode"
|
||||
)
|
||||
|
||||
func TestDataNodeFs_Readdir(t *testing.T) {
|
||||
dn := datanode.New()
|
||||
dn.AddData("file1.txt", []byte("hello"))
|
||||
dn.AddData("dir1/file2.txt", []byte("world"))
|
||||
|
||||
root := &DataNodeFs{dn: dn}
|
||||
stream, errno := root.Readdir(context.Background())
|
||||
if errno != 0 {
|
||||
t.Fatalf("Readdir failed: %v", errno)
|
||||
}
|
||||
if stream == nil {
|
||||
t.Fatal("Readdir returned a nil stream")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue