//go:build linux package mount import ( "context" "errors" "fmt" "hash/fnv" "io" "os" "path" "sync" "syscall" "time" "bazil.org/fuse" "bazil.org/fuse/fs" "gitea.dddbg.com/youbin/bdrclone/internal/baidu" ) type FileSystem struct { client *baidu.Client readOnly bool } func New(client *baidu.Client, readOnly bool) *FileSystem { return &FileSystem{client: client, readOnly: readOnly} } func (f *FileSystem) Root() (fs.Node, error) { return &Dir{fs: f, name: "/"}, nil } type Dir struct { fs *FileSystem name string } func (d *Dir) Attr(_ context.Context, attr *fuse.Attr) error { attr.Inode = inode(d.name) attr.Mode = os.ModeDir | 0o755 attr.Mtime = time.Now() return nil } func (d *Dir) Lookup(ctx context.Context, req *fuse.LookupRequest, _ *fuse.LookupResponse) (fs.Node, error) { remote := path.Join(d.name, req.Name) entry, err := d.fs.client.Stat(ctx, remote) if err != nil { if errors.Is(err, baidu.ErrNotFound) { return nil, syscall.ENOENT } return nil, err } return d.fs.node(d.fs.client.APIPath(entry.Path), entry), nil } func (d *Dir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { entries, err := d.fs.client.List(ctx, d.name) if err != nil { return nil, err } result := make([]fuse.Dirent, 0, len(entries)) for _, entry := range entries { typeID := fuse.DT_File if entry.IsDirectory() { typeID = fuse.DT_Dir } result = append(result, fuse.Dirent{Inode: inode(d.fs.client.APIPath(entry.Path)), Name: entry.Name(), Type: typeID}) } return result, nil } func (d *Dir) Mkdir(ctx context.Context, req *fuse.MkdirRequest) (fs.Node, error) { if d.fs.readOnly { return nil, syscall.EROFS } name := path.Join(d.name, req.Name) entry, err := d.fs.client.Mkdir(ctx, name) if err != nil { return nil, err } return d.fs.node(name, entry), nil } func (d *Dir) Create(ctx context.Context, req *fuse.CreateRequest, resp *fuse.CreateResponse) (fs.Node, fs.Handle, error) { if d.fs.readOnly { return nil, nil, syscall.EROFS } name := path.Join(d.name, req.Name) tmp, err := os.CreateTemp("", "bdrclone-write-*") if err != nil { return nil, nil, err } node := &File{fs: d.fs, name: name, info: baidu.File{Path: d.fs.client.RemotePath(name), ServerFilename: req.Name}} handle := &writeHandle{node: node, file: tmp, dirty: true} node.active = handle resp.Flags |= fuse.OpenDirectIO return node, handle, nil } func (d *Dir) Remove(ctx context.Context, req *fuse.RemoveRequest) error { if d.fs.readOnly { return syscall.EROFS } name := path.Join(d.name, req.Name) if req.Dir { entries, err := d.fs.client.List(ctx, name) if err != nil { return err } if len(entries) > 0 { return syscall.ENOTEMPTY } } return d.fs.client.Delete(ctx, name) } func (d *Dir) Rename(ctx context.Context, req *fuse.RenameRequest, newDir fs.Node) error { if d.fs.readOnly { return syscall.EROFS } destinationDir, ok := newDir.(*Dir) if !ok { return syscall.ENOTDIR } return d.fs.client.Move(ctx, path.Join(d.name, req.OldName), path.Join(destinationDir.name, req.NewName)) } func (f *FileSystem) node(name string, entry baidu.File) fs.Node { if entry.IsDirectory() { return &Dir{fs: f, name: name} } return &File{fs: f, name: name, info: entry} } type File struct { fs *FileSystem name string mu sync.Mutex info baidu.File active *writeHandle } func (f *File) Attr(_ context.Context, attr *fuse.Attr) error { f.mu.Lock() defer f.mu.Unlock() attr.Inode = inode(f.name) attr.Mode = 0o644 attr.Size = uint64(f.info.Size) attr.Mtime = f.info.ModTime() return nil } func (f *File) Open(ctx context.Context, req *fuse.OpenRequest, resp *fuse.OpenResponse) (fs.Handle, error) { if !req.Flags.IsWriteOnly() && !req.Flags.IsReadWrite() { resp.Flags |= fuse.OpenDirectIO return &readHandle{file: f}, nil } if f.fs.readOnly { return nil, syscall.EROFS } f.mu.Lock() busy := f.active != nil f.mu.Unlock() if busy { return nil, syscall.EBUSY } tmp, err := os.CreateTemp("", "bdrclone-write-*") if err != nil { return nil, err } if req.Flags&fuse.OpenTruncate == 0 && f.info.Size > 0 { body, err := f.fs.client.Open(ctx, f.info, 0, 0) if err != nil { tmp.Close() os.Remove(tmp.Name()) return nil, err } _, copyErr := io.Copy(tmp, body) closeErr := body.Close() if copyErr != nil || closeErr != nil { tmp.Close() os.Remove(tmp.Name()) return nil, errors.Join(copyErr, closeErr) } } resp.Flags |= fuse.OpenDirectIO handle := &writeHandle{node: f, file: tmp, dirty: req.Flags&fuse.OpenTruncate != 0} f.mu.Lock() f.active = handle f.mu.Unlock() return handle, nil } func (f *File) Setattr(_ context.Context, req *fuse.SetattrRequest, _ *fuse.SetattrResponse) error { if f.fs.readOnly && req.Valid.Size() { return syscall.EROFS } if req.Valid.Size() { f.mu.Lock() handle := f.active f.mu.Unlock() if handle == nil { return syscall.EBADF } return handle.truncate(int64(req.Size)) } return nil } func (f *File) Fsync(ctx context.Context, _ *fuse.FsyncRequest) error { f.mu.Lock() handle := f.active f.mu.Unlock() if handle == nil { return nil } return handle.sync(ctx) } type readHandle struct{ file *File } func (h *readHandle) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error { h.file.mu.Lock() info := h.file.info h.file.mu.Unlock() if req.Offset >= info.Size { resp.Data = nil return nil } size := min(int64(req.Size), info.Size-req.Offset) body, err := h.file.fs.client.Open(ctx, info, req.Offset, size) if err != nil { return err } defer body.Close() resp.Data, err = io.ReadAll(io.LimitReader(body, size)) return err } type writeHandle struct { node *File file *os.File mu sync.Mutex dirty bool done bool } func (h *writeHandle) Read(_ context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error { h.mu.Lock() defer h.mu.Unlock() buf := make([]byte, req.Size) n, err := h.file.ReadAt(buf, req.Offset) if err != nil && !errors.Is(err, io.EOF) { return err } resp.Data = buf[:n] return nil } func (h *writeHandle) Write(_ context.Context, req *fuse.WriteRequest, resp *fuse.WriteResponse) error { h.mu.Lock() defer h.mu.Unlock() n, err := h.file.WriteAt(req.Data, req.Offset) resp.Size = n if n > 0 { h.dirty = true } return err } func (h *writeHandle) Setattr(_ context.Context, req *fuse.SetattrRequest, _ *fuse.SetattrResponse) error { if !req.Valid.Size() { return nil } return h.truncate(int64(req.Size)) } func (h *writeHandle) truncate(size int64) error { h.mu.Lock() defer h.mu.Unlock() if err := h.file.Truncate(size); err != nil { return err } h.dirty = true return nil } func (h *writeHandle) Fsync(ctx context.Context, _ *fuse.FsyncRequest) error { return h.sync(ctx) } func (h *writeHandle) Flush(ctx context.Context, _ *fuse.FlushRequest) error { return h.sync(ctx) } func (h *writeHandle) Release(ctx context.Context, _ *fuse.ReleaseRequest) error { err := h.sync(ctx) h.mu.Lock() if !h.done { h.done = true closeErr := h.file.Close() if err != nil { recoveryPath, recoveryErr := preserveFailedWrite(h.file.Name(), h.node.name) fmt.Fprintf(os.Stderr, "bdrclone: upload failed for %s; local recovery file: %s\n", h.node.name, recoveryPath) err = errors.Join(err, closeErr, recoveryErr) } else { err = errors.Join(closeErr, os.Remove(h.file.Name())) } } h.mu.Unlock() h.node.mu.Lock() if h.node.active == h { h.node.active = nil } h.node.mu.Unlock() return err } func (h *writeHandle) sync(ctx context.Context) error { h.mu.Lock() defer h.mu.Unlock() if !h.dirty || h.done { return nil } stat, err := h.file.Stat() if err != nil { return err } if stat.Size() == 0 { return errors.New("Baidu Netdisk does not support empty files") } entry, err := h.node.fs.client.Upload(ctx, h.file, stat.Size(), h.node.name, stat.ModTime(), nil) if err != nil { return fmt.Errorf("upload %s: %w", h.node.name, err) } h.node.mu.Lock() h.node.info = entry h.node.mu.Unlock() h.dirty = false return nil } func inode(name string) uint64 { h := fnv.New64a() _, _ = h.Write([]byte(name)) return h.Sum64() } var ( _ fs.FS = (*FileSystem)(nil) _ fs.Node = (*Dir)(nil) _ fs.NodeRequestLookuper = (*Dir)(nil) _ fs.HandleReadDirAller = (*Dir)(nil) _ fs.NodeMkdirer = (*Dir)(nil) _ fs.NodeCreater = (*Dir)(nil) _ fs.NodeRemover = (*Dir)(nil) _ fs.NodeRenamer = (*Dir)(nil) _ fs.Node = (*File)(nil) _ fs.NodeOpener = (*File)(nil) _ fs.NodeSetattrer = (*File)(nil) _ fs.NodeFsyncer = (*File)(nil) _ fs.HandleReader = (*readHandle)(nil) _ fs.HandleReader = (*writeHandle)(nil) _ fs.HandleWriter = (*writeHandle)(nil) _ fs.HandleFlusher = (*writeHandle)(nil) _ fs.HandleReleaser = (*writeHandle)(nil) )