Initial commit: docker compose config
Release Docker multi arch / docker (push) Has been cancelled
Test Install Script / Test Script Syntax (push) Has been cancelled
Test Install Script / Test on almalinux-10 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-10 (root) (push) Has been cancelled
Test Install Script / Test on almalinux-8 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-8 (root) (push) Has been cancelled
Test Install Script / Test on almalinux-9 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-9 (root) (push) Has been cancelled
Test Install Script / Test on amazonlinux-2 (default) (push) Has been cancelled
Test Install Script / Test on amazonlinux-2 (root) (push) Has been cancelled
Test Install Script / Test on debian-11 (default) (push) Has been cancelled
Test Install Script / Test on debian-11 (root) (push) Has been cancelled
Test Install Script / Test on debian-12 (default) (push) Has been cancelled
Test Install Script / Test on debian-12 (root) (push) Has been cancelled
Test Install Script / Test on debian-13 (default) (push) Has been cancelled
Test Install Script / Test on debian-13 (root) (push) Has been cancelled
Test Install Script / Test on fedora-latest (default) (push) Has been cancelled
Test Install Script / Test on fedora-latest (root) (push) Has been cancelled
Test Install Script / Test on rocky-10 (default) (push) Has been cancelled
Test Install Script / Test on rocky-10 (root) (push) Has been cancelled
Test Install Script / Test on rocky-8 (default) (push) Has been cancelled
Test Install Script / Test on rocky-8 (root) (push) Has been cancelled
Test Install Script / Test on rocky-9 (default) (push) Has been cancelled
Test Install Script / Test on rocky-9 (root) (push) Has been cancelled
Test Install Script / Test on ubuntu-22.04 (default) (push) Has been cancelled
Test Install Script / Test on ubuntu-22.04 (root) (push) Has been cancelled
Test Install Script / Test on ubuntu-24.04 (default) (push) Has been cancelled
Test Install Script / Test on ubuntu-24.04 (root) (push) Has been cancelled

This commit is contained in:
2026-05-30 12:07:11 +00:00
commit 616c6b1c62
381 changed files with 55145 additions and 0 deletions
+383
View File
@@ -0,0 +1,383 @@
//go:build !windows
// +build !windows
package fuse
import (
"context"
"errors"
"io"
"io/fs"
"os"
"path"
"sync"
"syscall"
"time"
"server/log"
"server/settings"
torrfs "server/torrfs"
gofusefs "github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
)
type FuseStatus struct {
Enabled bool `json:"enabled"`
MountPath string `json:"mount_path"`
}
type FuseFS struct {
gofusefs.Inode
mountPath string
server *fuse.Server
mu sync.RWMutex
enabled bool
tfs fs.FS
p string // "."
}
var (
globalFuseFS *FuseFS
fuseMutex sync.Mutex
)
func NewFuseFS() *FuseFS { return &FuseFS{enabled: false} }
func FuseAutoMount() {
if settings.Args.FusePath != "" {
ffs := GetFuseFS()
if !ffs.enabled {
log.TLogln("FUSE mount")
err := ffs.Mount(settings.Args.FusePath)
if err != nil {
log.TLogln("Failed to auto-mount FUSE filesystem:", err)
os.Exit(1)
}
}
}
}
func FuseCleanup() {
ffs := GetFuseFS()
if ffs.enabled {
_ = ffs.Unmount()
}
}
func GetFuseFS() *FuseFS {
fuseMutex.Lock()
defer fuseMutex.Unlock()
if globalFuseFS == nil {
globalFuseFS = NewFuseFS()
}
return globalFuseFS
}
func (ffs *FuseFS) GetMountPath() string {
ffs.mu.RLock()
defer ffs.mu.RUnlock()
return ffs.mountPath
}
func (ffs *FuseFS) Mount(mountPath string) error {
ffs.mu.Lock()
defer ffs.mu.Unlock()
if ffs.enabled {
return errors.New("FUSE filesystem is already mounted")
}
if err := os.MkdirAll(mountPath, 0o755); err != nil {
log.TLogln("Error create FUSE mount point dir:", err)
return err
}
ffs.mountPath = mountPath
ffs.tfs = torrfs.AsFS(torrfs.New())
ffs.p = "."
entryTimeout := time.Second
attrTimeout := time.Second
opts := &gofusefs.Options{
MountOptions: fuse.MountOptions{
AllowOther: true,
Name: "torrserver",
FsName: "torrserver-fuse",
Debug: settings.BTsets.EnableDebug,
},
EntryTimeout: &entryTimeout,
AttrTimeout: &attrTimeout,
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
}
srv, err := gofusefs.Mount(mountPath, ffs, opts)
if err != nil {
log.TLogln("Error mount FUSE filesystem:", err)
return err
}
ffs.server = srv
ffs.enabled = true
log.TLogln("FUSE filesystem mounted at", mountPath)
go ffs.server.Wait()
return nil
}
func (ffs *FuseFS) Unmount() error {
ffs.mu.Lock()
defer ffs.mu.Unlock()
if !ffs.enabled {
return errors.New("FUSE filesystem is not mounted")
}
if err := ffs.server.Unmount(); err != nil {
return err
}
ffs.enabled = false
ffs.server = nil
ffs.mountPath = ""
ffs.tfs = nil
ffs.p = ""
log.TLogln("FUSE filesystem unmounted")
return nil
}
// ----- go-fuse integration -----
var (
_ = (gofusefs.InodeEmbedder)((*FuseFS)(nil))
_ = (gofusefs.NodeOnAdder)((*FuseFS)(nil))
_ = (gofusefs.NodeGetattrer)((*FuseFS)(nil))
_ = (gofusefs.NodeReaddirer)((*FuseFS)(nil))
_ = (gofusefs.NodeLookuper)((*FuseFS)(nil))
)
func (ffs *FuseFS) EmbeddedInode() *gofusefs.Inode { return &ffs.Inode }
func (ffs *FuseFS) OnAdd(ctx context.Context) {
if ffs.p == "" {
ffs.p = "."
}
}
// ----- Root ops -----
func (ffs *FuseFS) Getattr(ctx context.Context, fh gofusefs.FileHandle, out *fuse.AttrOut) syscall.Errno {
fi, err := fs.Stat(ffs.tfs, ".")
if err != nil {
return errno(err)
}
fillAttr(&out.Attr, fi)
return 0
}
func (ffs *FuseFS) Readdir(ctx context.Context) (gofusefs.DirStream, syscall.Errno) {
des, err := fs.ReadDir(ffs.tfs, ".")
if err != nil {
log.TLogln("FUSE root Readdir error:", err)
return nil, errno(err)
}
out := make([]fuse.DirEntry, 0, len(des))
for _, de := range des {
fi, err := de.Info()
if err != nil {
continue
}
out = append(out, fuse.DirEntry{
Name: de.Name(),
Mode: fuseModeFromInfo(fi),
})
}
return gofusefs.NewListDirStream(out), 0
}
func (ffs *FuseFS) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*gofusefs.Inode, syscall.Errno) {
childPath := path.Join(".", name)
fi, err := fs.Stat(ffs.tfs, childPath)
if err != nil {
return nil, errno(err)
}
fillAttr(&out.Attr, fi)
out.AttrValid = 1
out.AttrValidNsec = 0
out.EntryValid = 1
out.EntryValidNsec = 0
mode := fuseModeFromInfo(fi)
ch := ffs.NewInode(ctx, &tfsNode{tfs: ffs.tfs, p: childPath}, gofusefs.StableAttr{Mode: mode})
return ch, 0
}
// ----- Regular nodes -----
type tfsNode struct {
gofusefs.Inode
tfs fs.FS
p string
}
var (
_ = (gofusefs.NodeGetattrer)((*tfsNode)(nil))
_ = (gofusefs.NodeReaddirer)((*tfsNode)(nil))
_ = (gofusefs.NodeLookuper)((*tfsNode)(nil))
_ = (gofusefs.NodeOpener)((*tfsNode)(nil))
)
func (n *tfsNode) full(name string) string { return path.Join(n.p, name) }
func (n *tfsNode) Getattr(ctx context.Context, fh gofusefs.FileHandle, out *fuse.AttrOut) syscall.Errno {
fi, err := fs.Stat(n.tfs, n.p)
if err != nil {
return errno(err)
}
fillAttr(&out.Attr, fi)
return 0
}
func (n *tfsNode) Readdir(ctx context.Context) (gofusefs.DirStream, syscall.Errno) {
des, err := fs.ReadDir(n.tfs, n.p)
if err != nil {
return nil, errno(err)
}
out := make([]fuse.DirEntry, 0, len(des))
for _, de := range des {
fi, err := de.Info()
if err != nil {
continue
}
out = append(out, fuse.DirEntry{
Name: de.Name(),
Mode: fuseModeFromInfo(fi),
})
}
return gofusefs.NewListDirStream(out), 0
}
func (n *tfsNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*gofusefs.Inode, syscall.Errno) {
childPath := n.full(name)
fi, err := fs.Stat(n.tfs, childPath)
if err != nil {
return nil, errno(err)
}
fillAttr(&out.Attr, fi)
out.AttrValid = 1
out.AttrValidNsec = 0
out.EntryValid = 1
out.EntryValidNsec = 0
mode := fuseModeFromInfo(fi)
ch := n.NewInode(ctx, &tfsNode{tfs: n.tfs, p: childPath}, gofusefs.StableAttr{Mode: mode})
return ch, 0
}
func (n *tfsNode) Open(ctx context.Context, flags uint32) (gofusefs.FileHandle, uint32, syscall.Errno) {
if flags&(fuse.O_ANYWRITE) != 0 {
return nil, 0, syscall.EROFS
}
f, err := n.tfs.Open(n.p)
if err != nil {
return nil, 0, errno(err)
}
if _, ok := f.(io.ReadSeeker); !ok {
_ = f.Close()
return nil, 0, syscall.ENOSYS
}
return &tfsHandle{f: f}, fuse.FOPEN_DIRECT_IO, 0
}
// ----- File handle -----
type tfsHandle struct {
f fs.File // must implement io.ReadSeeker
}
var (
_ = (gofusefs.FileReader)((*tfsHandle)(nil))
_ = (gofusefs.FileReleaser)((*tfsHandle)(nil))
)
func (h *tfsHandle) Read(ctx context.Context, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
rs := h.f.(io.ReadSeeker)
if _, err := rs.Seek(off, io.SeekStart); err != nil {
return nil, syscall.EIO
}
n, err := rs.Read(dest)
if err != nil && err != io.EOF {
return nil, syscall.EIO
}
return fuse.ReadResultData(dest[:n]), 0
}
func (h *tfsHandle) Release(ctx context.Context) syscall.Errno {
_ = h.f.Close()
return 0
}
// ----- Attribute helpers -----
func fuseModeFromInfo(fi fs.FileInfo) uint32 {
if fi.IsDir() {
return fuse.S_IFDIR | uint32(fi.Mode().Perm())
}
return fuse.S_IFREG | uint32(fi.Mode().Perm())
}
func fillAttr(a *fuse.Attr, fi fs.FileInfo) {
a.Mode = fuseModeFromInfo(fi)
if fi.IsDir() {
a.Size = 4096
} else {
a.Size = uint64(fi.Size())
}
mt := fi.ModTime()
if mt.IsZero() {
mt = time.Now()
}
a.Mtime = uint64(mt.Unix())
a.Mtimensec = uint32(mt.Nanosecond())
a.Ctime = a.Mtime
a.Ctimensec = a.Mtimensec
a.Atime = a.Mtime
a.Atimensec = a.Mtimensec
}
// ----- errno mapping -----
func errno(err error) syscall.Errno {
if err == nil {
return 0
}
if pe, ok := err.(*fs.PathError); ok {
return errno(pe.Err)
}
switch err {
case fs.ErrNotExist:
return syscall.ENOENT
case fs.ErrPermission:
return syscall.EPERM
case fs.ErrInvalid:
return syscall.EINVAL
default:
return syscall.EIO
}
}