Files
decypharr/pkg/webdav/handler.go
2025-02-12 22:11:58 +01:00

264 lines
6.2 KiB
Go

package webdav
import (
"context"
"fmt"
"github.com/rs/zerolog"
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/cache"
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent"
"golang.org/x/net/webdav"
"html/template"
"net/http"
"os"
"path"
"strings"
"sync"
"sync/atomic"
"time"
)
type Handler struct {
Name string
logger zerolog.Logger
cache *cache.Cache
rootListing atomic.Value
lastRefresh time.Time
refreshMutex sync.Mutex
RootPath string
}
func NewHandler(name string, cache *cache.Cache, logger zerolog.Logger) *Handler {
h := &Handler{
Name: name,
cache: cache,
logger: logger,
RootPath: fmt.Sprintf("/%s", name),
}
h.refreshRootListing()
// Start background refresh
go h.backgroundRefresh()
return h
}
func (h *Handler) backgroundRefresh() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
h.refreshRootListing()
}
}
func (h *Handler) refreshRootListing() {
h.refreshMutex.Lock()
defer h.refreshMutex.Unlock()
if time.Since(h.lastRefresh) < time.Minute {
return
}
var files []os.FileInfo
h.cache.GetTorrents().Range(func(key, value interface{}) bool {
cachedTorrent := value.(*cache.CachedTorrent)
if cachedTorrent != nil && cachedTorrent.Torrent != nil {
files = append(files, &FileInfo{
name: cachedTorrent.Torrent.Name,
size: 0,
mode: 0755 | os.ModeDir,
modTime: time.Now(),
isDir: true,
})
}
return true
})
h.rootListing.Store(files)
h.lastRefresh = time.Now()
}
func (h *Handler) getParentRootPath() string {
return fmt.Sprintf("/webdav/%s", h.Name)
}
func (h *Handler) getRootFileInfos() []os.FileInfo {
if listing := h.rootListing.Load(); listing != nil {
return listing.([]os.FileInfo)
}
return []os.FileInfo{}
}
// Mkdir implements webdav.FileSystem
func (h *Handler) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
return os.ErrPermission // Read-only filesystem
}
// RemoveAll implements webdav.FileSystem
func (h *Handler) RemoveAll(ctx context.Context, name string) error {
return os.ErrPermission // Read-only filesystem
}
// Rename implements webdav.FileSystem
func (h *Handler) Rename(ctx context.Context, oldName, newName string) error {
return os.ErrPermission // Read-only filesystem
}
func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
name = path.Clean("/" + name)
// Fast path for root directory
if name == h.getParentRootPath() {
return &File{
cache: h.cache,
isDir: true,
children: h.getRootFileInfos(),
}, nil
}
// Remove root directory from path
name = strings.TrimPrefix(name, h.getParentRootPath())
name = strings.TrimPrefix(name, "/")
parts := strings.SplitN(name, "/", 2)
// Get torrent from cache using sync.Map
cachedTorrent := h.cache.GetTorrentByName(parts[0])
if cachedTorrent == nil {
h.logger.Debug().Msgf("Torrent not found: %s", parts[0])
return nil, os.ErrNotExist
}
if len(parts) == 1 {
return &File{
cache: h.cache,
cachedTorrent: cachedTorrent,
isDir: true,
children: h.getTorrentFileInfos(cachedTorrent.Torrent),
}, nil
}
// Use a map for faster file lookup
fileMap := make(map[string]*torrent.File, len(cachedTorrent.Torrent.Files))
for i := range cachedTorrent.Torrent.Files {
fileMap[cachedTorrent.Torrent.Files[i].Name] = &cachedTorrent.Torrent.Files[i]
}
if file, ok := fileMap[parts[1]]; ok {
return &File{
cache: h.cache,
cachedTorrent: cachedTorrent,
file: file,
isDir: false,
}, nil
}
h.logger.Debug().Msgf("File not found: %s", name)
return nil, os.ErrNotExist
}
// Stat implements webdav.FileSystem
func (h *Handler) Stat(ctx context.Context, name string) (os.FileInfo, error) {
f, err := h.OpenFile(ctx, name, os.O_RDONLY, 0)
if err != nil {
return nil, err
}
return f.Stat()
}
func (h *Handler) getTorrentFileInfos(torrent *torrent.Torrent) []os.FileInfo {
files := make([]os.FileInfo, 0, len(torrent.Files))
for _, file := range torrent.Files {
files = append(files, &FileInfo{
name: file.Name,
size: file.Size,
mode: 0644,
modTime: time.Now(),
isDir: false,
})
}
return files
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Handle OPTIONS
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
// Create WebDAV handler
handler := &webdav.Handler{
FileSystem: h,
LockSystem: webdav.NewMemLS(),
Logger: func(r *http.Request, err error) {
if err != nil {
h.logger.Error().
Err(err).
Str("method", r.Method).
Str("path", r.URL.Path).
Msg("WebDAV error")
}
},
}
// Special handling for GET requests on directories
if r.Method == "GET" {
if f, err := h.OpenFile(r.Context(), r.URL.Path, os.O_RDONLY, 0); err == nil {
if fi, err := f.Stat(); err == nil && fi.IsDir() {
h.serveDirectory(w, r, f)
return
}
f.Close()
}
}
handler.ServeHTTP(w, r)
}
func (h *Handler) serveDirectory(w http.ResponseWriter, r *http.Request, file webdav.File) {
var children []os.FileInfo
if f, ok := file.(*File); ok {
children = f.children
} else {
var err error
children, err = file.Readdir(-1)
if err != nil {
http.Error(w, "Failed to list directory", http.StatusInternalServerError)
return
}
}
// Clean and prepare the path
cleanPath := path.Clean(r.URL.Path)
parentPath := path.Dir(cleanPath)
showParent := cleanPath != "/" && parentPath != "." && parentPath != cleanPath
// Prepare template data
data := struct {
Path string
ParentPath string
ShowParent bool
Children []os.FileInfo
}{
Path: cleanPath,
ParentPath: parentPath,
ShowParent: showParent,
Children: children,
}
// Parse and execute template
tmpl, err := template.New("directory").Parse(directoryTemplate)
if err != nil {
h.logger.Error().Err(err).Msg("Failed to parse directory template")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := tmpl.Execute(w, data); err != nil {
h.logger.Error().Err(err).Msg("Failed to execute directory template")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
}