369 lines
8.7 KiB
Go
369 lines
8.7 KiB
Go
//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)
|
|
)
|