342 lines
7.8 KiB
Go
342 lines
7.8 KiB
Go
//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)
|