//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)