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
+24
View File
@@ -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)
}
+72
View File
@@ -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 }
+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
}
}
+18
View File
@@ -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() {
}
+21
View File
@@ -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 }
+43
View File
@@ -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
}
+91
View File
@@ -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 }
+142
View File
@@ -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 }
+85
View File
@@ -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
}
+6
View File
@@ -0,0 +1,6 @@
package torrfs
func New() *RootDir {
r := NewRootDir()
return r
}
+232
View File
@@ -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
}