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
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:
@@ -0,0 +1,24 @@
|
||||
package torrfs
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ioFSAdapter struct {
|
||||
root *RootDir
|
||||
}
|
||||
|
||||
func AsFS(root *RootDir) fs.FS {
|
||||
return &ioFSAdapter{root: root}
|
||||
}
|
||||
|
||||
func (a *ioFSAdapter) Open(name string) (fs.File, error) {
|
||||
name = path.Clean(name)
|
||||
if name == "." || name == "/" || name == "" {
|
||||
return a.root.Open(".")
|
||||
}
|
||||
name = strings.TrimPrefix(name, "/")
|
||||
return a.root.Open(name)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package torrfs
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"time"
|
||||
|
||||
"server/settings"
|
||||
|
||||
"server/torr"
|
||||
)
|
||||
|
||||
type CategoryDir struct {
|
||||
info fs.FileInfo
|
||||
}
|
||||
|
||||
func NewCategoryDir(category string) *CategoryDir {
|
||||
if category == "" {
|
||||
category = "other"
|
||||
}
|
||||
d := &CategoryDir{
|
||||
info: info{
|
||||
name: category,
|
||||
size: 4096,
|
||||
mode: 0o555,
|
||||
mtime: time.Unix(477033666, 0),
|
||||
isDir: true,
|
||||
},
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *CategoryDir) Stat() (fs.FileInfo, error) {
|
||||
return d.info, nil
|
||||
}
|
||||
|
||||
func (d *CategoryDir) ReadDir(n int) ([]fs.DirEntry, error) {
|
||||
nodes := []fs.DirEntry{}
|
||||
torrs := torr.ListTorrent()
|
||||
for _, t := range torrs {
|
||||
if t.Category == "" {
|
||||
t.Category = "other"
|
||||
}
|
||||
if t.Category == d.Name() {
|
||||
if settings.BTsets.ShowFSActiveTorr && !t.GotInfo() {
|
||||
continue
|
||||
}
|
||||
td := NewTorrDir(nil, t.Title, t)
|
||||
nodes = append(nodes, td)
|
||||
}
|
||||
}
|
||||
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
// INode
|
||||
func (d *CategoryDir) Open(name string) (fs.File, error) { return Open(d, name) }
|
||||
func (d *CategoryDir) Parent() INode { return nil }
|
||||
func (d *CategoryDir) Torrent() *torr.Torrent { return nil }
|
||||
func (d *CategoryDir) SetTorrent(torr *torr.Torrent) {}
|
||||
|
||||
// DirEntry
|
||||
func (d *CategoryDir) Name() string { return d.info.Name() }
|
||||
func (d *CategoryDir) IsDir() bool { return true }
|
||||
func (d *CategoryDir) Type() fs.FileMode {
|
||||
s, _ := d.Stat()
|
||||
return s.Mode()
|
||||
}
|
||||
func (d *CategoryDir) Info() (fs.FileInfo, error) { return d.info, nil }
|
||||
|
||||
// File
|
||||
func (d *CategoryDir) Read(bytes []byte) (int, error) { return 0, fs.ErrInvalid }
|
||||
func (d *CategoryDir) Close() error { return nil }
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package fuse
|
||||
|
||||
import (
|
||||
"server/log"
|
||||
"server/settings"
|
||||
)
|
||||
|
||||
func FuseAutoMount() {
|
||||
if settings.Args.FusePath != "" {
|
||||
log.TLogln("Windows not support FUSE")
|
||||
}
|
||||
}
|
||||
|
||||
func FuseCleanup() {
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package torrfs
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"time"
|
||||
)
|
||||
|
||||
type info struct {
|
||||
name string
|
||||
size int64
|
||||
mode fs.FileMode
|
||||
mtime time.Time
|
||||
isDir bool
|
||||
}
|
||||
|
||||
func (i info) Name() string { return i.name }
|
||||
func (i info) Size() int64 { return i.size }
|
||||
func (i info) Mode() fs.FileMode { return i.mode }
|
||||
func (i info) ModTime() time.Time { return i.mtime }
|
||||
func (i info) IsDir() bool { return i.isDir }
|
||||
func (i info) Sys() any { return nil }
|
||||
@@ -0,0 +1,43 @@
|
||||
package torrfs
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"strings"
|
||||
|
||||
"server/torr"
|
||||
)
|
||||
|
||||
type INode interface {
|
||||
fs.ReadDirFile
|
||||
fs.DirEntry
|
||||
|
||||
Open(name string) (fs.File, error)
|
||||
|
||||
Parent() INode
|
||||
|
||||
Torrent() *torr.Torrent
|
||||
SetTorrent(torr *torr.Torrent)
|
||||
}
|
||||
|
||||
func Open(d INode, name string) (fs.File, error) {
|
||||
trimPath := strings.TrimPrefix(name, d.Name())
|
||||
trimPath = strings.TrimSuffix(trimPath, "/")
|
||||
trimPath = strings.TrimPrefix(trimPath, "/")
|
||||
if trimPath == "" {
|
||||
return d, nil
|
||||
}
|
||||
arr := strings.Split(trimPath, "/")
|
||||
if len(arr) == 0 {
|
||||
return nil, fs.ErrNotExist
|
||||
}
|
||||
|
||||
dirs, _ := d.ReadDir(-1)
|
||||
|
||||
for _, dir := range dirs {
|
||||
if dir.Name() == arr[0] {
|
||||
return dir.(INode).Open(trimPath)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fs.ErrNotExist
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package torrfs
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/torr"
|
||||
)
|
||||
|
||||
type RootDir struct {
|
||||
info fs.FileInfo
|
||||
}
|
||||
|
||||
func NewRootDir() *RootDir {
|
||||
return &RootDir{
|
||||
info: info{
|
||||
name: "/",
|
||||
size: 4096,
|
||||
mode: 0o555,
|
||||
mtime: time.Unix(477033600, 0),
|
||||
isDir: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *RootDir) Open(name string) (fs.File, error) {
|
||||
name = path.Clean(name)
|
||||
if !fs.ValidPath(name) {
|
||||
return nil, &fs.PathError{Path: name, Err: fs.ErrInvalid}
|
||||
}
|
||||
|
||||
if name == "." || name == "/" {
|
||||
return d, nil
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(name, "/") {
|
||||
name = "/" + name
|
||||
}
|
||||
|
||||
return Open(d, name)
|
||||
}
|
||||
|
||||
func (d *RootDir) Stat() (fs.FileInfo, error) {
|
||||
return d.info, nil
|
||||
}
|
||||
|
||||
func (d *RootDir) ReadDir(n int) ([]fs.DirEntry, error) {
|
||||
torrs := torr.ListTorrent()
|
||||
cats := map[string]struct{}{}
|
||||
nodes := map[string]INode{}
|
||||
|
||||
for _, torrent := range torrs {
|
||||
cats[torrent.Category] = struct{}{}
|
||||
}
|
||||
|
||||
for cat := range cats {
|
||||
if cat == "" {
|
||||
cat = "other"
|
||||
}
|
||||
nodes[cat] = NewCategoryDir(cat)
|
||||
}
|
||||
|
||||
var entries []fs.DirEntry
|
||||
for _, c := range nodes {
|
||||
entries = append(entries, c)
|
||||
}
|
||||
if n > 0 && len(entries) > n {
|
||||
entries = entries[:n]
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// INode
|
||||
func (d *RootDir) Parent() INode { return nil }
|
||||
func (d *RootDir) Torrent() *torr.Torrent { return nil }
|
||||
func (d *RootDir) SetTorrent(torr *torr.Torrent) {}
|
||||
|
||||
// DirEntry
|
||||
func (d *RootDir) Name() string { return d.info.Name() }
|
||||
func (d *RootDir) IsDir() bool { return true }
|
||||
func (d *RootDir) Type() fs.FileMode {
|
||||
s, _ := d.Stat()
|
||||
return s.Mode()
|
||||
}
|
||||
func (d *RootDir) Info() (fs.FileInfo, error) { return d.info, nil }
|
||||
|
||||
// File
|
||||
func (d *RootDir) Read(bytes []byte) (int, error) { return 0, fs.ErrInvalid }
|
||||
func (d *RootDir) Close() error { return nil }
|
||||
@@ -0,0 +1,142 @@
|
||||
package torrfs
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/settings"
|
||||
"server/torr"
|
||||
)
|
||||
|
||||
type TorrDir struct {
|
||||
parent INode
|
||||
children map[string]INode
|
||||
|
||||
info fs.FileInfo
|
||||
|
||||
torr *torr.Torrent
|
||||
}
|
||||
|
||||
func NewTorrDir(parent INode, name string, torrent *torr.Torrent) *TorrDir {
|
||||
d := &TorrDir{
|
||||
parent: parent,
|
||||
torr: torrent,
|
||||
info: info{
|
||||
name: name,
|
||||
size: 4096,
|
||||
mode: 0o555,
|
||||
mtime: time.Unix(torrent.Timestamp, 0),
|
||||
isDir: true,
|
||||
},
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *TorrDir) ReadDir(n int) ([]fs.DirEntry, error) {
|
||||
if d.Torrent() == nil {
|
||||
return nil, fs.ErrInvalid
|
||||
}
|
||||
// соединяемся с торрентом при чтении директории торрента
|
||||
if !d.Torrent().GotInfo() {
|
||||
hash := d.Torrent().Hash().String()
|
||||
for i := 0; i < settings.BTsets.TorrentDisconnectTimeout*2; i++ {
|
||||
tor := torr.GetTorrent(hash)
|
||||
if tor.GotInfo() {
|
||||
d.SetTorrent(tor)
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
}
|
||||
if d.Torrent() == nil {
|
||||
return nil, fs.ErrNotExist
|
||||
}
|
||||
}
|
||||
|
||||
files := d.Torrent().Files()
|
||||
nodes := map[string]fs.DirEntry{}
|
||||
|
||||
currTorrPath := d.getTorrPath()
|
||||
|
||||
for _, file := range files {
|
||||
dp := file.DisplayPath()
|
||||
|
||||
var rel string
|
||||
if currTorrPath == "" {
|
||||
rel = dp
|
||||
} else {
|
||||
prefix := currTorrPath + "/"
|
||||
if !strings.HasPrefix(dp, prefix) {
|
||||
continue
|
||||
}
|
||||
rel = strings.TrimPrefix(dp, prefix)
|
||||
}
|
||||
|
||||
if rel == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
arr := strings.SplitN(rel, "/", 2)
|
||||
name := arr[0]
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(arr) == 1 {
|
||||
nodes[name] = NewTorrFile(d, name, file)
|
||||
} else if _, ok := nodes[name]; !ok {
|
||||
nodes[name] = NewTorrDir(d, name, d.Torrent())
|
||||
}
|
||||
}
|
||||
|
||||
var entries []fs.DirEntry
|
||||
for _, c := range nodes {
|
||||
entries = append(entries, c)
|
||||
}
|
||||
if n > 0 && len(entries) > n {
|
||||
entries = entries[:n]
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func (d *TorrDir) getTorrPath() string {
|
||||
parts := []string{}
|
||||
|
||||
for n := INode(d); n != nil && n.Torrent() != nil; n = n.Parent() {
|
||||
if n.Parent() != nil && n.Parent().Torrent() == nil {
|
||||
continue
|
||||
}
|
||||
parts = append([]string{n.Name()}, parts...)
|
||||
}
|
||||
|
||||
// отдаем без самого названия торрента
|
||||
if len(parts) > 0 {
|
||||
return path.Join(parts[1:]...)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (d *TorrDir) Open(name string) (fs.File, error) {
|
||||
return Open(d, name)
|
||||
}
|
||||
|
||||
// INode
|
||||
func (d *TorrDir) Parent() INode { return d.parent }
|
||||
func (d *TorrDir) Torrent() *torr.Torrent { return d.torr }
|
||||
func (d *TorrDir) SetTorrent(torr *torr.Torrent) { d.torr = torr }
|
||||
|
||||
// DirEntry
|
||||
func (d *TorrDir) Name() string { return d.info.Name() }
|
||||
func (d *TorrDir) IsDir() bool { return true }
|
||||
func (d *TorrDir) Type() fs.FileMode {
|
||||
s, _ := d.Stat()
|
||||
return s.Mode()
|
||||
}
|
||||
func (d *TorrDir) Info() (fs.FileInfo, error) { return d.info, nil }
|
||||
func (d *TorrDir) Stat() (fs.FileInfo, error) { return d.info, nil }
|
||||
|
||||
// File
|
||||
func (d *TorrDir) Read(bytes []byte) (int, error) { return 0, fs.ErrInvalid }
|
||||
func (d *TorrDir) Close() error { return nil }
|
||||
@@ -0,0 +1,85 @@
|
||||
package torrfs
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"time"
|
||||
|
||||
sets "server/settings"
|
||||
"server/torr"
|
||||
"server/torr/storage/torrstor"
|
||||
|
||||
"github.com/anacrolix/torrent"
|
||||
)
|
||||
|
||||
type TorrFile struct {
|
||||
parent INode
|
||||
|
||||
info fs.FileInfo
|
||||
|
||||
torr *torr.Torrent
|
||||
file *torrent.File
|
||||
reader *torrstor.Reader
|
||||
}
|
||||
|
||||
type TorrFileHandle struct {
|
||||
*TorrFile
|
||||
r *torrstor.Reader
|
||||
}
|
||||
|
||||
func NewTorrFile(parent INode, name string, file *torrent.File) *TorrFile {
|
||||
f := &TorrFile{
|
||||
file: file,
|
||||
parent: parent,
|
||||
torr: parent.Torrent(),
|
||||
info: info{
|
||||
name: name,
|
||||
size: file.Length(),
|
||||
mode: 0o444,
|
||||
mtime: time.Unix(parent.Torrent().Timestamp, 0),
|
||||
isDir: false,
|
||||
},
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *TorrFile) Open(name string) (fs.File, error) {
|
||||
r := f.Torrent().NewReader(f.file)
|
||||
if r == nil {
|
||||
return nil, fs.ErrInvalid
|
||||
}
|
||||
if sets.BTsets.ResponsiveMode {
|
||||
r.SetResponsive()
|
||||
}
|
||||
return &TorrFileHandle{TorrFile: f, r: r}, nil
|
||||
}
|
||||
|
||||
// INode
|
||||
func (f *TorrFile) Parent() INode { return f.parent }
|
||||
func (f *TorrFile) Torrent() *torr.Torrent { return f.torr }
|
||||
func (f *TorrFile) SetTorrent(torr *torr.Torrent) { f.torr = torr }
|
||||
|
||||
// DirEntry
|
||||
func (f *TorrFile) Name() string { return f.info.Name() }
|
||||
func (f *TorrFile) IsDir() bool { return false }
|
||||
func (f *TorrFile) Type() fs.FileMode {
|
||||
s, _ := f.Stat()
|
||||
return s.Mode()
|
||||
}
|
||||
func (f *TorrFile) Info() (fs.FileInfo, error) { return f.info, nil }
|
||||
func (f *TorrFile) Stat() (fs.FileInfo, error) { return f.info, nil }
|
||||
func (f *TorrFile) Read(p []byte) (int, error) { return 0, fs.ErrInvalid }
|
||||
func (f *TorrFile) Close() error { return nil }
|
||||
func (f *TorrFile) ReadDir(n int) ([]fs.DirEntry, error) { return nil, fs.ErrInvalid }
|
||||
|
||||
func (h *TorrFileHandle) Read(p []byte) (int, error) {
|
||||
return h.r.Read(p)
|
||||
}
|
||||
|
||||
func (h *TorrFileHandle) Seek(off int64, whence int) (int64, error) {
|
||||
return h.r.Seek(off, whence)
|
||||
}
|
||||
|
||||
func (h *TorrFileHandle) Close() error {
|
||||
h.torr.CloseReader(h.r)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package torrfs
|
||||
|
||||
func New() *RootDir {
|
||||
r := NewRootDir()
|
||||
return r
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"server/log"
|
||||
|
||||
"server/torrfs"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/net/webdav"
|
||||
)
|
||||
|
||||
var missingMethods = []string{
|
||||
"PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK",
|
||||
}
|
||||
|
||||
func MountWebDAV(r *gin.Engine) {
|
||||
log.TLogln("Starting WebDAV")
|
||||
tfs := torrfs.AsFS(torrfs.New())
|
||||
|
||||
h := &webdav.Handler{
|
||||
Prefix: "/dav",
|
||||
FileSystem: &ReadOnlyFS{FS: tfs},
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
}
|
||||
|
||||
grp := r.Group("/dav")
|
||||
|
||||
handler := func(c *gin.Context) {
|
||||
h.ServeHTTP(c.Writer, c.Request)
|
||||
}
|
||||
|
||||
grp.Any("/*webdav", handler)
|
||||
for _, m := range missingMethods {
|
||||
grp.Handle(m, "/*webdav", handler)
|
||||
}
|
||||
|
||||
grp.Any("", handler)
|
||||
for _, m := range missingMethods {
|
||||
grp.Handle(m, "", handler)
|
||||
}
|
||||
}
|
||||
|
||||
type ReadOnlyFS struct {
|
||||
FS fs.FS
|
||||
}
|
||||
|
||||
var _ webdav.FileSystem = (*ReadOnlyFS)(nil)
|
||||
|
||||
func (ro *ReadOnlyFS) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
|
||||
return os.ErrPermission
|
||||
}
|
||||
|
||||
func (ro *ReadOnlyFS) RemoveAll(ctx context.Context, name string) error {
|
||||
return os.ErrPermission
|
||||
}
|
||||
|
||||
func (ro *ReadOnlyFS) Rename(ctx context.Context, oldName, newName string) error {
|
||||
return os.ErrPermission
|
||||
}
|
||||
|
||||
func (ro *ReadOnlyFS) Stat(ctx context.Context, name string) (os.FileInfo, error) {
|
||||
name = cleanWebDAVPath(name)
|
||||
return fs.Stat(ro.FS, name)
|
||||
}
|
||||
|
||||
func (ro *ReadOnlyFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
|
||||
if flag&(os.O_WRONLY|os.O_RDWR|os.O_APPEND|os.O_CREATE|os.O_TRUNC) != 0 {
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
|
||||
name = cleanWebDAVPath(name)
|
||||
|
||||
f, err := ro.FS.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newROFile(ro.FS, name, f), nil
|
||||
}
|
||||
|
||||
// --- file wrapper ---
|
||||
|
||||
type roFile struct {
|
||||
fsys fs.FS
|
||||
name string
|
||||
|
||||
mu sync.Mutex
|
||||
f fs.File
|
||||
|
||||
dirPos int
|
||||
dirList []fs.DirEntry
|
||||
}
|
||||
|
||||
func newROFile(fsys fs.FS, name string, f fs.File) *roFile {
|
||||
return &roFile{fsys: fsys, name: name, f: f}
|
||||
}
|
||||
|
||||
var _ webdav.File = (*roFile)(nil)
|
||||
|
||||
func (f *roFile) Write(p []byte) (n int, err error) {
|
||||
return 0, fs.ErrPermission
|
||||
}
|
||||
|
||||
func (f *roFile) Close() error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if f.f == nil {
|
||||
return nil
|
||||
}
|
||||
err := f.f.Close()
|
||||
f.f = nil
|
||||
f.dirList = nil
|
||||
f.dirPos = 0
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *roFile) Read(p []byte) (int, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
if f.f == nil {
|
||||
return 0, fs.ErrClosed
|
||||
}
|
||||
r, ok := f.f.(io.Reader)
|
||||
if !ok {
|
||||
return 0, fs.ErrInvalid
|
||||
}
|
||||
return r.Read(p)
|
||||
}
|
||||
|
||||
func (f *roFile) Seek(offset int64, whence int) (int64, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
if f.f == nil {
|
||||
return 0, fs.ErrClosed
|
||||
}
|
||||
rs, ok := f.f.(io.Seeker)
|
||||
if !ok {
|
||||
return 0, errors.New("seek not supported")
|
||||
}
|
||||
return rs.Seek(offset, whence)
|
||||
}
|
||||
|
||||
func (f *roFile) Stat() (os.FileInfo, error) {
|
||||
return fs.Stat(f.fsys, f.name)
|
||||
}
|
||||
|
||||
func (f *roFile) Readdir(count int) ([]os.FileInfo, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
if f.f == nil {
|
||||
return nil, fs.ErrClosed
|
||||
}
|
||||
|
||||
fi, err := fs.Stat(f.fsys, f.name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return nil, fs.ErrInvalid
|
||||
}
|
||||
|
||||
if f.dirList == nil {
|
||||
des, err := fs.ReadDir(f.fsys, f.name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.dirList = des
|
||||
f.dirPos = 0
|
||||
}
|
||||
|
||||
if count <= 0 {
|
||||
out := make([]os.FileInfo, 0, len(f.dirList)-f.dirPos)
|
||||
for f.dirPos < len(f.dirList) {
|
||||
de := f.dirList[f.dirPos]
|
||||
f.dirPos++
|
||||
info, err := de.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, info)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
out := make([]os.FileInfo, 0, count)
|
||||
for f.dirPos < len(f.dirList) && len(out) < count {
|
||||
de := f.dirList[f.dirPos]
|
||||
f.dirPos++
|
||||
info, err := de.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, info)
|
||||
}
|
||||
|
||||
if len(out) == 0 {
|
||||
return nil, io.EOF
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// --- path helpers ---
|
||||
func cleanWebDAVPath(name string) string {
|
||||
if name == "" || name == "/" {
|
||||
return "."
|
||||
}
|
||||
name = path.Clean("/" + name)
|
||||
name = name[1:]
|
||||
if name == "" {
|
||||
return "."
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func nonZeroTime(t time.Time) time.Time {
|
||||
if t.IsZero() {
|
||||
return time.Unix(0, 0)
|
||||
}
|
||||
return t
|
||||
}
|
||||
Reference in New Issue
Block a user