Files
2026-07-27 19:31:43 +08:00

305 lines
6.8 KiB
Go

//go:build fuse
package fuse
import (
"context"
"io"
"os"
"path/filepath"
"strings"
"sync"
"syscall"
"time"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
"litepan/internal/domain"
"litepan/internal/upload"
)
type stagingFile struct {
fs.Inode
b *backend
name string
tempPath string
size int64
modTime time.Time
mu sync.RWMutex
}
var (
_ fs.NodeGetattrer = (*stagingFile)(nil)
_ fs.NodeOpener = (*stagingFile)(nil)
_ fs.FileWriter = (*stagingUploadHandle)(nil)
_ fs.FileFlusher = (*stagingUploadHandle)(nil)
_ fs.FileFsyncer = (*stagingUploadHandle)(nil)
_ fs.FileReleaser = (*stagingUploadHandle)(nil)
_ fs.FileReader = (*localStagingReadHandle)(nil)
_ fs.FileReleaser = (*localStagingReadHandle)(nil)
)
func (d *cloudDir) Create(ctx context.Context, name string, flags uint32, _ uint32, out *fuse.EntryOut) (*fs.Inode, fs.FileHandle, uint32, syscall.Errno) {
name, errno := normalizeChildName(name)
if errno != 0 {
return nil, nil, 0, errno
}
if d.b.deps.Uploads == nil {
return nil, nil, 0, syscall.EROFS
}
if _, found, errno := d.lookupChildItemIfExists(ctx, name); errno != 0 {
return nil, nil, 0, errno
} else if found {
return nil, nil, 0, syscall.EEXIST
}
tmp, tmpPath, cleanup, untrack, err := createFuseTempFile(d.b.deps.Uploads, name)
if err != nil {
return nil, nil, 0, syscall.EIO
}
node := &stagingFile{
b: d.b,
name: name,
tempPath: tmpPath,
modTime: time.Now(),
}
child := d.NewPersistentInode(ctx, node, fs.StableAttr{
Ino: fileIno(d.b.accountID(), "pending:"+tmpPath),
Mode: fuse.S_IFREG,
})
if child == nil {
cleanup()
_ = tmp.Close()
return nil, nil, 0, syscall.EIO
}
fillEntryAttr(out, d.b, domain.FileItem{Name: name, Size: 0, ModTime: node.modTime})
return child, &stagingUploadHandle{
node: node,
uploads: d.b.deps.Uploads,
accountID: d.b.accountID(),
parentID: d.item.ID,
targetDisplayPath: d.targetDisplayPath(),
fileName: name,
tempPath: tmpPath,
file: tmp,
cleanup: cleanup,
untrack: untrack,
flags: flags,
}, 0, 0
}
func (d *cloudDir) targetDisplayPath() string {
return strings.Trim(d.Path(nil), "/")
}
func (f *stagingFile) Getattr(_ context.Context, _ fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
f.mu.RLock()
item := domain.FileItem{
Name: f.name,
Size: f.size,
ModTime: f.modTime,
}
f.mu.RUnlock()
fillFileAttr(&out.Attr, f.b, item)
return 0
}
func (f *stagingFile) Open(_ context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) {
if flags&syscall.O_ACCMODE != syscall.O_RDONLY {
return nil, 0, syscall.EROFS
}
f.mu.RLock()
tempPath := f.tempPath
f.mu.RUnlock()
if tempPath == "" {
return nil, 0, syscall.ENOENT
}
file, err := os.Open(tempPath)
if err != nil {
if os.IsNotExist(err) {
return nil, 0, syscall.ENOENT
}
return nil, 0, syscall.EIO
}
return &localStagingReadHandle{file: file}, 0, 0
}
func (f *stagingFile) updateSize(size int64) {
f.mu.Lock()
if size > f.size {
f.size = size
}
f.modTime = time.Now()
f.mu.Unlock()
}
func (f *stagingFile) updateFromStat(st os.FileInfo) {
if st == nil {
return
}
f.mu.Lock()
f.size = st.Size()
f.modTime = st.ModTime()
f.mu.Unlock()
}
type stagingUploadHandle struct {
node *stagingFile
uploads UploadManager
accountID int64
parentID string
targetDisplayPath string
fileName string
tempPath string
file *os.File
cleanup func()
untrack func()
flags uint32
queued bool
released bool
mu sync.Mutex
}
func (h *stagingUploadHandle) Write(_ context.Context, data []byte, off int64) (uint32, syscall.Errno) {
h.mu.Lock()
file := h.file
h.mu.Unlock()
if file == nil {
return 0, syscall.EBADF
}
n, err := file.WriteAt(data, off)
if n > 0 && h.node != nil {
h.node.updateSize(off + int64(n))
}
if err != nil {
return uint32(n), syscall.EIO
}
return uint32(n), 0
}
func (h *stagingUploadHandle) Flush(_ context.Context) syscall.Errno {
h.mu.Lock()
defer h.mu.Unlock()
if h.queued {
return 0
}
if h.file == nil {
return syscall.EBADF
}
if h.flags&syscall.O_ACCMODE == syscall.O_RDONLY {
return syscall.EBADF
}
if err := h.file.Sync(); err != nil {
return syscall.EIO
}
st, err := h.file.Stat()
if err != nil {
return syscall.EIO
}
if h.node != nil {
h.node.updateFromStat(st)
}
if _, err := h.uploads.Create(context.Background(), upload.CreateParams{
AccountID: h.accountID,
FileName: h.fileName,
DisplayName: h.fileName,
TargetPath: h.parentID,
TargetDisplayPath: h.targetDisplayPath,
LocalPath: h.tempPath,
TotalBytes: st.Size(),
ConflictPolicy: "overwrite",
}); err != nil {
return errnoFor(err)
}
h.queued = true
if h.untrack != nil {
h.untrack()
h.untrack = nil
}
return 0
}
func (h *stagingUploadHandle) Fsync(_ context.Context, _ uint32) syscall.Errno {
h.mu.Lock()
defer h.mu.Unlock()
if h.file == nil {
return syscall.EBADF
}
if err := h.file.Sync(); err != nil {
return syscall.EIO
}
return 0
}
func (h *stagingUploadHandle) Release(_ context.Context) syscall.Errno {
h.mu.Lock()
defer h.mu.Unlock()
if h.released {
return 0
}
h.released = true
if h.file != nil {
_ = h.file.Close()
h.file = nil
}
if !h.queued && h.cleanup != nil {
h.cleanup()
h.cleanup = nil
}
return 0
}
type localStagingReadHandle struct {
file *os.File
}
func (h *localStagingReadHandle) Read(_ context.Context, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
if h.file == nil {
return nil, syscall.EBADF
}
n, err := h.file.ReadAt(dest, off)
if n > 0 {
return fuse.ReadResultData(dest[:n]), 0
}
if err == nil || err == io.EOF {
return fuse.ReadResultData(nil), 0
}
if os.IsNotExist(err) {
return nil, syscall.ENOENT
}
return nil, syscall.EIO
}
func (h *localStagingReadHandle) Release(_ context.Context) syscall.Errno {
if h.file != nil {
_ = h.file.Close()
h.file = nil
}
return 0
}
func createFuseTempFile(uploads UploadManager, fileName string) (*os.File, string, func(), func(), error) {
dir := uploads.TempDir()
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, "", nil, nil, err
}
safeName := filepath.Base(fileName)
if safeName == "" || safeName == "." {
safeName = "upload.bin"
}
f, err := os.CreateTemp(dir, "fuse_*_"+safeName)
if err != nil {
return nil, "", nil, nil, err
}
path := filepath.Clean(f.Name())
untrack := func() {}
if registry := uploads.TempRegistry(); registry != nil {
untrack = registry.Track(path)
}
cleanup := func() {
untrack()
_ = os.Remove(path)
}
return f, path, cleanup, untrack, nil
}