Initial release of bdrclone
This commit is contained in:
341
internal/mount/cmount.go
Normal file
341
internal/mount/cmount.go
Normal file
@@ -0,0 +1,341 @@
|
||||
//go:build darwin && cgo && cmount
|
||||
|
||||
package mount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"gitea.dddbg.com/youbin/bdrclone/internal/baidu"
|
||||
"github.com/winfsp/cgofuse/fuse"
|
||||
)
|
||||
|
||||
type cMountFS struct {
|
||||
fuse.FileSystemBase
|
||||
client *baidu.Client
|
||||
readOnly bool
|
||||
|
||||
mu sync.Mutex
|
||||
next uint64
|
||||
handles map[uint64]*mountHandle
|
||||
}
|
||||
|
||||
type mountHandle struct {
|
||||
mu sync.Mutex
|
||||
read baidu.File
|
||||
write *os.File
|
||||
name string
|
||||
dirty bool
|
||||
}
|
||||
|
||||
func newCMountFS(client *baidu.Client, readOnly bool) *cMountFS {
|
||||
return &cMountFS{client: client, readOnly: readOnly, next: 1, handles: make(map[uint64]*mountHandle)}
|
||||
}
|
||||
|
||||
func (f *cMountFS) Getattr(name string, stat *fuse.Stat_t, fh uint64) int {
|
||||
if fh != ^uint64(0) {
|
||||
f.mu.Lock()
|
||||
handle, ok := f.handles[fh]
|
||||
f.mu.Unlock()
|
||||
if ok {
|
||||
handle.mu.Lock()
|
||||
defer handle.mu.Unlock()
|
||||
if handle.write == nil {
|
||||
return -fuse.EBADF
|
||||
}
|
||||
info, err := handle.write.Stat()
|
||||
if err != nil {
|
||||
return errno(err)
|
||||
}
|
||||
stat.Mode, stat.Size = fuse.S_IFREG|0o644, info.Size()
|
||||
stat.Mtim = fuse.NewTimespec(info.ModTime())
|
||||
return 0
|
||||
}
|
||||
}
|
||||
if name == "/" {
|
||||
stat.Mode, stat.Nlink = fuse.S_IFDIR|0o755, 2
|
||||
return 0
|
||||
}
|
||||
entry, err := f.client.Stat(context.Background(), name)
|
||||
if err != nil {
|
||||
return errno(err)
|
||||
}
|
||||
stat.Size = entry.Size
|
||||
stat.Mtim = fuse.NewTimespec(entry.ModTime())
|
||||
if entry.IsDirectory() {
|
||||
stat.Mode, stat.Nlink = fuse.S_IFDIR|0o755, 2
|
||||
} else {
|
||||
stat.Mode, stat.Nlink = fuse.S_IFREG|0o644, 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (f *cMountFS) Readdir(name string, fill func(string, *fuse.Stat_t, int64) bool, _ int64, _ uint64) int {
|
||||
entries, err := f.client.List(context.Background(), name)
|
||||
if err != nil {
|
||||
return errno(err)
|
||||
}
|
||||
fill(".", nil, 0)
|
||||
fill("..", nil, 0)
|
||||
for _, entry := range entries {
|
||||
stat := &fuse.Stat_t{Size: entry.Size, Mtim: fuse.NewTimespec(entry.ModTime())}
|
||||
if entry.IsDirectory() {
|
||||
stat.Mode = fuse.S_IFDIR | 0o755
|
||||
} else {
|
||||
stat.Mode = fuse.S_IFREG | 0o644
|
||||
}
|
||||
if !fill(entry.Name(), stat, 0) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (f *cMountFS) Open(name string, flags int) (int, uint64) {
|
||||
write := flags&(os.O_WRONLY|os.O_RDWR) != 0
|
||||
entry, err := f.client.Stat(context.Background(), name)
|
||||
if err != nil {
|
||||
return errno(err), 0
|
||||
}
|
||||
if !write {
|
||||
return 0, f.addHandle(&mountHandle{read: entry, name: name})
|
||||
}
|
||||
if f.readOnly {
|
||||
return -fuse.EROFS, 0
|
||||
}
|
||||
tmp, err := os.CreateTemp("", "bdrclone-write-*")
|
||||
if err != nil {
|
||||
return errno(err), 0
|
||||
}
|
||||
if flags&os.O_TRUNC == 0 && entry.Size > 0 {
|
||||
body, openErr := f.client.Open(context.Background(), entry, 0, 0)
|
||||
if openErr == nil {
|
||||
_, openErr = io.Copy(tmp, body)
|
||||
openErr = errors.Join(openErr, body.Close())
|
||||
}
|
||||
if openErr != nil {
|
||||
tmp.Close()
|
||||
os.Remove(tmp.Name())
|
||||
return errno(openErr), 0
|
||||
}
|
||||
}
|
||||
return 0, f.addHandle(&mountHandle{write: tmp, read: entry, name: name, dirty: flags&os.O_TRUNC != 0})
|
||||
}
|
||||
|
||||
func (f *cMountFS) Create(name string, flags int, mode uint32) (int, uint64) {
|
||||
if f.readOnly {
|
||||
return -fuse.EROFS, 0
|
||||
}
|
||||
tmp, err := os.CreateTemp("", "bdrclone-write-*")
|
||||
if err != nil {
|
||||
return errno(err), 0
|
||||
}
|
||||
return 0, f.addHandle(&mountHandle{write: tmp, name: name, dirty: true})
|
||||
}
|
||||
|
||||
func (f *cMountFS) Read(_ string, dest []byte, offset int64, fh uint64) int {
|
||||
handle, ok := f.handle(fh)
|
||||
if !ok {
|
||||
return -fuse.EBADF
|
||||
}
|
||||
handle.mu.Lock()
|
||||
defer handle.mu.Unlock()
|
||||
if handle.write != nil {
|
||||
n, err := handle.write.ReadAt(dest, offset)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return errno(err)
|
||||
}
|
||||
return n
|
||||
}
|
||||
body, err := f.client.Open(context.Background(), handle.read, offset, int64(len(dest)))
|
||||
if err != nil {
|
||||
return errno(err)
|
||||
}
|
||||
n, readErr := io.ReadFull(body, dest)
|
||||
readErr = errors.Join(readErr, body.Close())
|
||||
if readErr != nil && !errors.Is(readErr, io.EOF) && !errors.Is(readErr, io.ErrUnexpectedEOF) {
|
||||
return errno(readErr)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (f *cMountFS) Write(_ string, data []byte, offset int64, fh uint64) int {
|
||||
handle, ok := f.handle(fh)
|
||||
if !ok {
|
||||
return -fuse.EBADF
|
||||
}
|
||||
handle.mu.Lock()
|
||||
defer handle.mu.Unlock()
|
||||
if handle.write == nil {
|
||||
return -fuse.EBADF
|
||||
}
|
||||
n, err := handle.write.WriteAt(data, offset)
|
||||
if n > 0 {
|
||||
handle.dirty = true
|
||||
}
|
||||
if err != nil {
|
||||
return errno(err)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (f *cMountFS) Flush(_ string, fh uint64) int { return f.flush(fh) }
|
||||
|
||||
func (f *cMountFS) Fsync(_ string, _ bool, fh uint64) int { return f.flush(fh) }
|
||||
|
||||
func (f *cMountFS) Truncate(_ string, size int64, fh uint64) int {
|
||||
handle, ok := f.handle(fh)
|
||||
if !ok {
|
||||
return -fuse.EBADF
|
||||
}
|
||||
handle.mu.Lock()
|
||||
defer handle.mu.Unlock()
|
||||
if handle.write == nil {
|
||||
return -fuse.EBADF
|
||||
}
|
||||
if err := handle.write.Truncate(size); err != nil {
|
||||
return errno(err)
|
||||
}
|
||||
handle.dirty = true
|
||||
return 0
|
||||
}
|
||||
|
||||
func (f *cMountFS) Release(_ string, fh uint64) int {
|
||||
status := f.flush(fh)
|
||||
f.mu.Lock()
|
||||
handle, ok := f.handles[fh]
|
||||
delete(f.handles, fh)
|
||||
f.mu.Unlock()
|
||||
if ok {
|
||||
handle.mu.Lock()
|
||||
defer handle.mu.Unlock()
|
||||
if handle.write == nil {
|
||||
return status
|
||||
}
|
||||
closeErr := handle.write.Close()
|
||||
if status != 0 {
|
||||
recoveryPath, recoveryErr := preserveFailedWrite(handle.write.Name(), handle.name)
|
||||
fmt.Fprintf(os.Stderr, "bdrclone: upload failed for %s; local recovery file: %s\n", handle.name, recoveryPath)
|
||||
if closeErr != nil || recoveryErr != nil {
|
||||
status = -fuse.EIO
|
||||
}
|
||||
} else if err := errors.Join(closeErr, os.Remove(handle.write.Name())); err != nil {
|
||||
status = errno(err)
|
||||
}
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
func (f *cMountFS) flush(fh uint64) int {
|
||||
handle, ok := f.handle(fh)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
handle.mu.Lock()
|
||||
defer handle.mu.Unlock()
|
||||
if handle.write == nil || !handle.dirty {
|
||||
return 0
|
||||
}
|
||||
info, err := handle.write.Stat()
|
||||
if err == nil && info.Size() == 0 {
|
||||
err = errors.New("Baidu Netdisk does not allow empty files")
|
||||
}
|
||||
if err == nil {
|
||||
_, err = f.client.Upload(context.Background(), handle.write, info.Size(), handle.name, info.ModTime(), nil)
|
||||
}
|
||||
if err == nil {
|
||||
handle.dirty = false
|
||||
}
|
||||
return errno(err)
|
||||
}
|
||||
|
||||
func (f *cMountFS) Mkdir(name string, _ uint32) int {
|
||||
if f.readOnly {
|
||||
return -fuse.EROFS
|
||||
}
|
||||
_, err := f.client.Mkdir(context.Background(), name)
|
||||
return errno(err)
|
||||
}
|
||||
|
||||
func (f *cMountFS) Unlink(name string) int {
|
||||
if f.readOnly {
|
||||
return -fuse.EROFS
|
||||
}
|
||||
return errno(f.client.Delete(context.Background(), name))
|
||||
}
|
||||
func (f *cMountFS) Rmdir(name string) int {
|
||||
if f.readOnly {
|
||||
return -fuse.EROFS
|
||||
}
|
||||
entries, err := f.client.List(context.Background(), name)
|
||||
if err != nil {
|
||||
return errno(err)
|
||||
}
|
||||
if len(entries) > 0 {
|
||||
return -fuse.ENOTEMPTY
|
||||
}
|
||||
return errno(f.client.Delete(context.Background(), name))
|
||||
}
|
||||
func (f *cMountFS) Rename(oldName, newName string) int {
|
||||
if f.readOnly {
|
||||
return -fuse.EROFS
|
||||
}
|
||||
return errno(f.client.Move(context.Background(), oldName, newName))
|
||||
}
|
||||
|
||||
func (f *cMountFS) addHandle(handle *mountHandle) uint64 {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
id := f.next
|
||||
f.next++
|
||||
f.handles[id] = handle
|
||||
return id
|
||||
}
|
||||
|
||||
func (f *cMountFS) handle(id uint64) (*mountHandle, bool) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
h, ok := f.handles[id]
|
||||
return h, ok
|
||||
}
|
||||
|
||||
func errno(err error) int {
|
||||
if err == nil {
|
||||
return 0
|
||||
}
|
||||
if errors.Is(err, baidu.ErrNotFound) || errors.Is(err, os.ErrNotExist) {
|
||||
return -fuse.ENOENT
|
||||
}
|
||||
if errors.Is(err, syscall.EACCES) {
|
||||
return -fuse.EACCES
|
||||
}
|
||||
return -fuse.EIO
|
||||
}
|
||||
|
||||
func Mount(ctx context.Context, client *baidu.Client, mountpoint string, options Options) error {
|
||||
host := fuse.NewFileSystemHost(newCMountFS(client, options.ReadOnly))
|
||||
args := []string{"-o", "fsname=bdrclone", "-o", "subtype=bdrclone", "-o", "volname=Baidu Netdisk", "-o", "noappledouble", "-o", "noapplexattr"}
|
||||
if options.ReadOnly {
|
||||
args = append(args, "-o", "ro")
|
||||
}
|
||||
done := make(chan bool, 1)
|
||||
go func() { done <- host.Mount(mountpoint, args) }()
|
||||
select {
|
||||
case ok := <-done:
|
||||
if !ok {
|
||||
return errors.New("macFUSE mount failed")
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
host.Unmount()
|
||||
<-done
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var _ fuse.FileSystemInterface = (*cMountFS)(nil)
|
||||
368
internal/mount/fs.go
Normal file
368
internal/mount/fs.go
Normal file
@@ -0,0 +1,368 @@
|
||||
//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)
|
||||
)
|
||||
37
internal/mount/mount.go
Normal file
37
internal/mount/mount.go
Normal file
@@ -0,0 +1,37 @@
|
||||
//go:build linux
|
||||
|
||||
package mount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"bazil.org/fuse"
|
||||
"bazil.org/fuse/fs"
|
||||
"gitea.dddbg.com/youbin/bdrclone/internal/baidu"
|
||||
)
|
||||
|
||||
func Mount(ctx context.Context, client *baidu.Client, mountpoint string, options Options) error {
|
||||
if options.Name == "" {
|
||||
options.Name = "bdrclone"
|
||||
}
|
||||
mountOptions := []fuse.MountOption{fuse.FSName(options.Name), fuse.Subtype("bdrclone")}
|
||||
if options.ReadOnly {
|
||||
mountOptions = append(mountOptions, fuse.ReadOnly())
|
||||
}
|
||||
conn, err := fuse.Mount(mountpoint, mountOptions...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("mount %s: %w", mountpoint, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
server := fs.New(conn, &fs.Config{})
|
||||
serveErr := make(chan error, 1)
|
||||
go func() { serveErr <- server.Serve(New(client, options.ReadOnly)) }()
|
||||
select {
|
||||
case err := <-serveErr:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
_ = fuse.Unmount(mountpoint)
|
||||
return <-serveErr
|
||||
}
|
||||
}
|
||||
14
internal/mount/mount_unsupported.go
Normal file
14
internal/mount/mount_unsupported.go
Normal file
@@ -0,0 +1,14 @@
|
||||
//go:build !linux && !(darwin && cgo && cmount)
|
||||
|
||||
package mount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"gitea.dddbg.com/youbin/bdrclone/internal/baidu"
|
||||
)
|
||||
|
||||
func Mount(context.Context, *baidu.Client, string, Options) error {
|
||||
return errors.New("this build has no mount backend; on macOS install macFUSE and rebuild with `go build -tags cmount ./cmd/bdrclone`")
|
||||
}
|
||||
6
internal/mount/options.go
Normal file
6
internal/mount/options.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package mount
|
||||
|
||||
type Options struct {
|
||||
ReadOnly bool
|
||||
Name string
|
||||
}
|
||||
29
internal/mount/recovery.go
Normal file
29
internal/mount/recovery.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package mount
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func preserveFailedWrite(tempPath, remotePath string) (string, error) {
|
||||
cacheDir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
return tempPath, err
|
||||
}
|
||||
recoveryDir := filepath.Join(cacheDir, "bdrclone", "failed-writes")
|
||||
if err := os.MkdirAll(recoveryDir, 0o700); err != nil {
|
||||
return tempPath, err
|
||||
}
|
||||
name := strings.ReplaceAll(filepath.Base(remotePath), string(filepath.Separator), "_")
|
||||
if name == "" || name == "." {
|
||||
name = "remote-file"
|
||||
}
|
||||
destination := filepath.Join(recoveryDir, fmt.Sprintf("%s-%s", time.Now().Format("20060102-150405.000000000"), name))
|
||||
if err := os.Rename(tempPath, destination); err != nil {
|
||||
return tempPath, err
|
||||
}
|
||||
return destination, nil
|
||||
}
|
||||
Reference in New Issue
Block a user