- Add support for per-file deletion

- Per-file repair instead of per-torrent
- Fix issues with LoadLocation
- Fix other minor bug fixes woth torbox
This commit is contained in:
Mukhtar Akere
2025-05-27 19:31:19 +01:00
parent 09202b88e9
commit 7f25599b60
11 changed files with 268 additions and 103 deletions

View File

@@ -23,6 +23,7 @@ import (
"github.com/sirrobot01/decypharr/internal/logger"
"github.com/sirrobot01/decypharr/internal/utils"
"github.com/sirrobot01/decypharr/pkg/debrid/types"
_ "time/tzdata"
)
type WebDavFolderNaming string
@@ -109,9 +110,16 @@ type Cache struct {
func New(dc config.Debrid, client types.Client) *Cache {
cfg := config.Get()
cet, _ := time.LoadLocation("CET")
cetSc, _ := gocron.NewScheduler(gocron.WithLocation(cet))
scheduler, _ := gocron.NewScheduler(gocron.WithLocation(time.Local))
cetSc, err := gocron.NewScheduler(gocron.WithLocation(time.UTC))
if err != nil {
// If we can't create a CET scheduler, fallback to local time
cetSc, _ = gocron.NewScheduler(gocron.WithLocation(time.Local))
}
scheduler, err := gocron.NewScheduler(gocron.WithLocation(time.Local))
if err != nil {
// If we can't create a local scheduler, fallback to CET
scheduler = cetSc
}
autoExpiresLinksAfter, err := time.ParseDuration(dc.AutoExpireLinksAfter)
if autoExpiresLinksAfter == 0 || err != nil {
@@ -307,10 +315,10 @@ func (c *Cache) load(ctx context.Context) (map[string]CachedTorrent, error) {
}
isComplete := true
if len(ct.Files) != 0 {
if len(ct.GetFiles()) != 0 {
// Check if all files are valid, if not, delete the file.json and remove from cache.
fs := make(map[string]types.File, len(ct.Files))
for _, f := range ct.Files {
fs := make(map[string]types.File, len(ct.GetFiles()))
for _, f := range ct.GetFiles() {
if f.Link == "" {
isComplete = false
break
@@ -756,7 +764,7 @@ func (c *Cache) deleteTorrent(id string, removeFromDebrid bool) bool {
newFiles := map[string]types.File{}
newId := ""
for _, file := range t.Files {
for _, file := range t.GetFiles() {
if file.TorrentId != "" && file.TorrentId != id {
if newId == "" && file.TorrentId != "" {
newId = file.TorrentId
@@ -815,6 +823,36 @@ func (c *Cache) OnRemove(torrentId string) {
}
}
// RemoveFile removes a file from the torrent cache
// TODO sends a re-insert that removes the file from debrid
func (c *Cache) RemoveFile(torrentId string, filename string) error {
c.logger.Debug().Str("torrent_id", torrentId).Msgf("Removing file %s", filename)
torrent, ok := c.torrents.getByID(torrentId)
if !ok {
return fmt.Errorf("torrent %s not found", torrentId)
}
file, ok := torrent.GetFile(filename)
if !ok {
return fmt.Errorf("file %s not found in torrent %s", filename, torrentId)
}
file.Deleted = true
torrent.Files[filename] = file
// If the torrent has no files left, delete it
if len(torrent.GetFiles()) == 0 {
c.logger.Debug().Msgf("Torrent %s has no files left, deleting it", torrentId)
if err := c.DeleteTorrent(torrentId); err != nil {
return fmt.Errorf("failed to delete torrent %s: %w", torrentId, err)
}
return nil
}
c.setTorrent(torrent, func(torrent CachedTorrent) {
c.listingDebouncer.Call(true)
}) // Update the torrent in the cache
return nil
}
func (c *Cache) GetLogger() zerolog.Logger {
return c.logger
}

View File

@@ -103,7 +103,10 @@ func (c *Cache) fetchDownloadLink(torrentName, filename, fileLink string) (strin
if ct == nil {
return "", fmt.Errorf("torrent not found")
}
file := ct.Files[filename]
file, ok := ct.GetFile(filename)
if !ok {
return "", fmt.Errorf("file %s not found in torrent %s", filename, torrentName)
}
if file.Link == "" {
// file link is empty, refresh the torrent to get restricted links
@@ -111,7 +114,10 @@ func (c *Cache) fetchDownloadLink(torrentName, filename, fileLink string) (strin
if ct == nil {
return "", fmt.Errorf("failed to refresh torrent")
} else {
file = ct.Files[filename]
file, ok = ct.GetFile(filename)
if !ok {
return "", fmt.Errorf("file %s not found in refreshed torrent %s", filename, torrentName)
}
}
}
@@ -123,7 +129,10 @@ func (c *Cache) fetchDownloadLink(torrentName, filename, fileLink string) (strin
return "", fmt.Errorf("failed to reinsert torrent. %w", err)
}
ct = newCt
file = ct.Files[filename]
file, ok = ct.GetFile(filename)
if !ok {
return "", fmt.Errorf("file %s not found in reinserted torrent %s", filename, torrentName)
}
}
c.logger.Trace().Msgf("Getting download link for %s(%s)", filename, file.Link)
@@ -135,7 +144,10 @@ func (c *Cache) fetchDownloadLink(torrentName, filename, fileLink string) (strin
return "", fmt.Errorf("failed to reinsert torrent: %w", err)
}
ct = newCt
file = ct.Files[filename]
file, ok = ct.GetFile(filename)
if !ok {
return "", fmt.Errorf("file %s not found in reinserted torrent %s", filename, torrentName)
}
// Retry getting the download link
downloadLink, err = c.client.GetDownloadLink(ct.Torrent, &file)
if err != nil {
@@ -165,7 +177,7 @@ func (c *Cache) GenerateDownloadLinks(t CachedTorrent) {
c.logger.Error().Err(err).Str("torrent", t.Name).Msg("Failed to generate download links")
return
}
for _, file := range t.Files {
for _, file := range t.GetFiles() {
if file.DownloadLink != nil {
c.updateDownloadLink(file.DownloadLink)
}

View File

@@ -19,7 +19,7 @@ func mergeFiles(torrents ...CachedTorrent) map[string]types.File {
})
for _, torrent := range torrents {
for _, file := range torrent.Files {
for _, file := range torrent.GetFiles() {
merged[file.Name] = file
}
}

View File

@@ -59,11 +59,9 @@ func (c *Cache) markAsSuccessfullyReinserted(torrentId string) {
}
}
func (c *Cache) IsTorrentBroken(t *CachedTorrent, filenames []string) bool {
// Check torrent files
isBroken := false
func (c *Cache) GetBrokenFiles(t *CachedTorrent, filenames []string) []string {
files := make(map[string]types.File)
brokenFiles := make([]string, 0)
if len(filenames) > 0 {
for name, f := range t.Files {
if utils.Contains(filenames, name) {
@@ -73,8 +71,6 @@ func (c *Cache) IsTorrentBroken(t *CachedTorrent, filenames []string) bool {
} else {
files = t.Files
}
// Check empty links
for _, f := range files {
// Check if file is missing
if f.Link == "" {
@@ -83,14 +79,14 @@ func (c *Cache) IsTorrentBroken(t *CachedTorrent, filenames []string) bool {
t = newT
} else {
c.logger.Error().Str("torrentId", t.Torrent.Id).Msg("Failed to refresh torrent")
return true
return filenames // Return original filenames if refresh fails(torrent is somehow botched)
}
}
}
if t.Torrent == nil {
c.logger.Error().Str("torrentId", t.Torrent.Id).Msg("Failed to refresh torrent")
return true
return filenames // Return original filenames if refresh fails(torrent is somehow botched)
}
files = t.Files
@@ -98,29 +94,28 @@ func (c *Cache) IsTorrentBroken(t *CachedTorrent, filenames []string) bool {
for _, f := range files {
// Check if file link is still missing
if f.Link == "" {
isBroken = true
break
brokenFiles = append(brokenFiles, f.Name)
} else {
// Check if file.Link not in the downloadLink Cache
if err := c.client.CheckLink(f.Link); err != nil {
if errors.Is(err, request.HosterUnavailableError) {
isBroken = true
break
brokenFiles = append(brokenFiles, f.Name)
}
}
}
}
// Try to reinsert the torrent if it's broken
if isBroken && t.Torrent != nil {
if len(brokenFiles) > 0 && t.Torrent != nil {
// Check if the torrent is already in progress
if _, err := c.reInsertTorrent(t); err != nil {
c.logger.Error().Err(err).Str("torrentId", t.Torrent.Id).Msg("Failed to reinsert torrent")
return true
return brokenFiles // Return broken files if reinsert fails
}
return false
return nil // Return nil if the torrent was successfully reinserted
}
return isBroken
return brokenFiles
}
func (c *Cache) repairWorker(ctx context.Context) {
@@ -223,7 +218,7 @@ func (c *Cache) reInsertTorrent(ct *CachedTorrent) (*CachedTorrent, error) {
if err != nil {
addedOn = time.Now()
}
for _, f := range newTorrent.Files {
for _, f := range newTorrent.GetFiles() {
if f.Link == "" {
c.markAsFailedToReinsert(oldID)
return ct, fmt.Errorf("failed to reinsert torrent: empty link")

View File

@@ -234,7 +234,7 @@ func (tb *Torbox) GetTorrent(torrentId string) (*types.Torrent, error) {
Id: strconv.Itoa(f.Id),
Name: fileName,
Size: f.Size,
Path: fileName,
Path: f.Name,
}
t.Files[fileName] = file
}

View File

@@ -29,6 +29,7 @@ type Torrent struct {
Seeders int `json:"seeders"`
Links []string `json:"links"`
MountPath string `json:"mount_path"`
DeletedFiles []string `json:"deleted_files"`
Debrid string `json:"debrid"`
@@ -75,6 +76,24 @@ func (t *Torrent) GetMountFolder(rClonePath string) (string, error) {
return "", fmt.Errorf("no path found")
}
func (t *Torrent) GetFile(filename string) (File, bool) {
f, ok := t.Files[filename]
if !ok {
return File{}, false
}
return f, !f.Deleted
}
func (t *Torrent) GetFiles() []File {
files := make([]File, 0, len(t.Files))
for _, f := range t.Files {
if !f.Deleted {
files = append(files, f)
}
}
return files
}
type File struct {
TorrentId string `json:"torrent_id"`
Id string `json:"id"`
@@ -85,6 +104,7 @@ type File struct {
DownloadLink *DownloadLink `json:"-"`
AccountId string `json:"account_id"`
Generated time.Time `json:"generated"`
Deleted bool `json:"deleted"`
}
func (t *Torrent) Cleanup(remove bool) {
@@ -96,15 +116,6 @@ func (t *Torrent) Cleanup(remove bool) {
}
}
func (t *Torrent) GetFile(id string) *File {
for _, f := range t.Files {
if f.Id == id {
return &f
}
}
return nil
}
type Account struct {
ID string `json:"id"`
Disabled bool `json:"disabled"`

View File

@@ -68,7 +68,7 @@ func (q *QBit) downloadFiles(torrent *Torrent, parent string) {
var wg sync.WaitGroup
totalSize := int64(0)
for _, file := range debridTorrent.Files {
for _, file := range debridTorrent.GetFiles() {
totalSize += file.Size
}
debridTorrent.Mu.Lock()
@@ -100,7 +100,7 @@ func (q *QBit) downloadFiles(torrent *Torrent, parent string) {
},
}
errChan := make(chan error, len(debridTorrent.Files))
for _, file := range debridTorrent.Files {
for _, file := range debridTorrent.GetFiles() {
if file.DownloadLink == nil {
q.logger.Info().Msgf("No download link found for %s", file.Name)
continue
@@ -164,7 +164,75 @@ func (q *QBit) ProcessSymlink(torrent *Torrent) (string, error) {
torrentFolder = utils.RemoveExtension(torrentFolder)
torrentRclonePath = rCloneBase // /mnt/rclone/magnets/ // Remove the filename since it's in the root folder
}
return q.createSymlinks(debridTorrent, torrentRclonePath, torrentFolder) // verify cos we're using external webdav
torrentSymlinkPath := filepath.Join(q.DownloadFolder, debridTorrent.Arr.Name, torrentFolder) // /mnt/symlinks/{category}/MyTVShow/
err = os.MkdirAll(torrentSymlinkPath, os.ModePerm)
if err != nil {
return "", fmt.Errorf("failed to create directory: %s: %v", torrentSymlinkPath, err)
}
realPaths := make(map[string]string)
err = filepath.WalkDir(torrentRclonePath, func(path string, d os.DirEntry, err error) error {
if err != nil {
return nil
}
if !d.IsDir() {
filename := d.Name()
rel, _ := filepath.Rel(torrentRclonePath, path)
realPaths[filename] = rel
}
return nil
})
if err != nil {
q.logger.Warn().Msgf("Error while scanning rclone path: %v", err)
}
pending := make(map[string]debridTypes.File)
for _, file := range files {
if realRelPath, ok := realPaths[file.Name]; ok {
file.Path = realRelPath
}
pending[file.Path] = file
}
ticker := time.NewTicker(200 * time.Millisecond)
defer ticker.Stop()
timeout := time.After(30 * time.Minute)
filePaths := make([]string, 0, len(pending))
for len(pending) > 0 {
select {
case <-ticker.C:
for path, file := range pending {
fullFilePath := filepath.Join(torrentRclonePath, file.Path)
if _, err := os.Stat(fullFilePath); !os.IsNotExist(err) {
fileSymlinkPath := filepath.Join(torrentSymlinkPath, file.Name)
if err := os.Symlink(fullFilePath, fileSymlinkPath); err != nil && !os.IsExist(err) {
q.logger.Debug().Msgf("Failed to create symlink: %s: %v", fileSymlinkPath, err)
} else {
filePaths = append(filePaths, fileSymlinkPath)
delete(pending, path)
q.logger.Info().Msgf("File is ready: %s", file.Name)
}
}
}
case <-timeout:
q.logger.Warn().Msgf("Timeout waiting for files, %d files still pending", len(pending))
return torrentSymlinkPath, fmt.Errorf("timeout waiting for files: %d files still pending", len(pending))
}
}
if q.SkipPreCache {
return torrentSymlinkPath, nil
}
go func() {
if err := q.preCacheFile(debridTorrent.Name, filePaths); err != nil {
q.logger.Error().Msgf("Failed to pre-cache file: %s", err)
} else {
q.logger.Trace().Msgf("Pre-cached %d files", len(filePaths))
}
}()
return torrentSymlinkPath, nil
}
func (q *QBit) createSymlinksWebdav(debridTorrent *debridTypes.Torrent, rclonePath, torrentFolder string) (string, error) {

View File

@@ -109,8 +109,8 @@ func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debridTypes.Torrent
debridTorrent.Arr = arr
// Check if debrid supports webdav by checking cache
timer := time.Now()
if isSymlink {
timer := time.Now()
cache, useWebdav := svc.Debrid.Caches[debridTorrent.Debrid]
if useWebdav {
q.logger.Info().Msgf("Using internal webdav for %s", debridTorrent.Debrid)
@@ -131,7 +131,6 @@ func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debridTypes.Torrent
// User is using either zurg or debrid webdav
torrentSymlinkPath, err = q.ProcessSymlink(torrent) // /mnt/symlinks/{category}/MyTVShow/
}
q.logger.Info().Msgf("Adding %s took %s", debridTorrent.Name, time.Since(timer))
} else {
torrentSymlinkPath, err = q.ProcessManualFile(torrent)
}
@@ -145,6 +144,7 @@ func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debridTypes.Torrent
}
torrent.TorrentPath = torrentSymlinkPath
q.UpdateTorrent(torrent, debridTorrent)
q.logger.Info().Msgf("Adding %s took %s", debridTorrent.Name, time.Since(timer))
go func() {
if err := request.SendDiscordMessage("download_complete", "success", torrent.discordContext()); err != nil {
q.logger.Error().Msgf("Error sending discord message: %v", err)
@@ -289,7 +289,7 @@ func (q *QBit) GetTorrentFiles(t *Torrent) []*TorrentFile {
if t.DebridTorrent == nil {
return files
}
for _, file := range t.DebridTorrent.Files {
for _, file := range t.DebridTorrent.GetFiles() {
files = append(files, &TorrentFile{
Name: file.Path,
Size: file.Size,

View File

@@ -492,15 +492,14 @@ func (r *Repair) getFileBrokenFiles(media arr.Content) []arr.ContentFile {
uniqueParents := collectFiles(media)
for parent, f := range uniqueParents {
for parent, files := range uniqueParents {
// Check stat
// Check file stat first
firstFile := f[0]
// Read a tiny bit of the file
if err := fileIsReadable(firstFile.Path); err != nil {
r.logger.Debug().Msgf("Broken file found at: %s", parent)
brokenFiles = append(brokenFiles, f...)
continue
for _, file := range files {
if err := fileIsReadable(file.Path); err != nil {
r.logger.Debug().Msgf("Broken file found at: %s", parent)
brokenFiles = append(brokenFiles, file)
}
}
}
if len(brokenFiles) == 0 {
@@ -526,41 +525,44 @@ func (r *Repair) getZurgBrokenFiles(media arr.Content) []arr.ContentFile {
}
client := request.New(request.WithTimeout(0), request.WithTransport(tr))
// Access zurg url + symlink folder + first file(encoded)
for parent, f := range uniqueParents {
for parent, files := range uniqueParents {
r.logger.Debug().Msgf("Checking %s", parent)
torrentName := url.PathEscape(filepath.Base(parent))
encodedFile := url.PathEscape(f[0].TargetPath)
fullURL := fmt.Sprintf("%s/http/__all__/%s/%s", r.ZurgURL, torrentName, encodedFile)
// Check file stat first
if _, err := os.Stat(f[0].Path); os.IsNotExist(err) {
r.logger.Debug().Msgf("Broken symlink found: %s", fullURL)
brokenFiles = append(brokenFiles, f...)
if len(files) == 0 {
r.logger.Debug().Msgf("No files found for %s. Skipping", torrentName)
continue
}
resp, err := client.Get(fullURL)
if err != nil {
r.logger.Error().Err(err).Msgf("Failed to reach %s", fullURL)
brokenFiles = append(brokenFiles, f...)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
r.logger.Debug().Msgf("Failed to get download url for %s", fullURL)
for _, file := range files {
encodedFile := url.PathEscape(file.TargetPath)
fullURL := fmt.Sprintf("%s/http/__all__/%s/%s", r.ZurgURL, torrentName, encodedFile)
if _, err := os.Stat(file.Path); os.IsNotExist(err) {
r.logger.Debug().Msgf("Broken symlink found: %s", fullURL)
brokenFiles = append(brokenFiles, file)
continue
}
resp, err := client.Get(fullURL)
if err != nil {
r.logger.Error().Err(err).Msgf("Failed to reach %s", fullURL)
brokenFiles = append(brokenFiles, file)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
r.logger.Debug().Msgf("Failed to get download url for %s", fullURL)
resp.Body.Close()
brokenFiles = append(brokenFiles, file)
continue
}
downloadUrl := resp.Request.URL.String()
resp.Body.Close()
brokenFiles = append(brokenFiles, f...)
continue
}
downloadUrl := resp.Request.URL.String()
resp.Body.Close()
if downloadUrl != "" {
r.logger.Trace().Msgf("Found download url: %s", downloadUrl)
} else {
r.logger.Debug().Msgf("Failed to get download url for %s", fullURL)
brokenFiles = append(brokenFiles, f...)
continue
if downloadUrl != "" {
r.logger.Trace().Msgf("Found download url: %s", downloadUrl)
} else {
r.logger.Debug().Msgf("Failed to get download url for %s", fullURL)
brokenFiles = append(brokenFiles, file)
continue
}
}
}
if len(brokenFiles) == 0 {
@@ -588,7 +590,6 @@ func (r *Repair) getWebdavBrokenFiles(media arr.Content) []arr.ContentFile {
brokenFiles := make([]arr.ContentFile, 0)
uniqueParents := collectFiles(media)
// Access zurg url + symlink folder + first file(encoded)
for torrentPath, f := range uniqueParents {
r.logger.Debug().Msgf("Checking %s", torrentPath)
// Get the debrid first
@@ -627,11 +628,15 @@ func (r *Repair) getWebdavBrokenFiles(media arr.Content) []arr.ContentFile {
files = append(files, file.TargetPath)
}
if cache.IsTorrentBroken(torrent, files) {
r.logger.Debug().Msgf("[webdav] Broken symlink found: %s", torrentPath)
// Delete the torrent?
brokenFiles = append(brokenFiles, f...)
continue
_brokenFiles := cache.GetBrokenFiles(torrent, files)
totalBrokenFiles := len(_brokenFiles)
if totalBrokenFiles > 0 {
r.logger.Debug().Msgf("%d broken files found in %s", totalBrokenFiles, torrentName)
for _, contentFile := range f {
if utils.Contains(_brokenFiles, contentFile.TargetPath) {
brokenFiles = append(brokenFiles, contentFile)
}
}
}
}

View File

@@ -30,6 +30,7 @@ type File struct {
cache *debrid.Cache
fileId string
torrentName string
torrentId string
modTime time.Time
@@ -45,6 +46,7 @@ type File struct {
downloadLink string
link string
canDelete bool
}
// File interface implementations for File

View File

@@ -61,25 +61,58 @@ func (h *Handler) readinessMiddleware(next http.Handler) http.Handler {
// RemoveAll implements webdav.FileSystem
func (h *Handler) RemoveAll(ctx context.Context, name string) error {
if name[0] != '/' {
if !strings.HasPrefix(name, "/") {
name = "/" + name
}
name = path.Clean(name)
name = utils.PathUnescape(path.Clean(name))
rootDir := path.Clean(h.RootPath)
if name == rootDir {
return os.ErrPermission
}
torrentName, _ := getName(rootDir, name)
cachedTorrent := h.cache.GetTorrentByName(torrentName)
if cachedTorrent == nil {
h.logger.Debug().Msgf("Torrent not found: %s", torrentName)
return nil // It's possible that the torrent was removed
// Skip if it's version.txt
if name == path.Join(rootDir, "version.txt") {
return os.ErrPermission
}
// Check if the name is a parent path
if _, ok := h.isParentPath(name); ok {
return os.ErrPermission
}
// Check if the name is a torrent folder
rel := strings.TrimPrefix(name, rootDir+"/")
parts := strings.Split(rel, "/")
if len(parts) == 2 && utils.Contains(h.getParentItems(), parts[0]) {
torrentName := parts[1]
torrent := h.cache.GetTorrentByName(torrentName)
if torrent == nil {
return os.ErrNotExist
}
// Remove the torrent from the cache and debrid
h.cache.OnRemove(torrent.Id)
return nil
}
// If we reach here, it means the path is a file
if len(parts) >= 2 {
if utils.Contains(h.getParentItems(), parts[0]) {
torrentName := parts[1]
cached := h.cache.GetTorrentByName(torrentName)
if cached != nil && len(parts) >= 3 {
filename := filepath.Clean(path.Join(parts[2:]...))
if file, ok := cached.GetFile(filename); ok {
if err := h.cache.RemoveFile(cached.Id, file.Name); err != nil {
h.logger.Error().Err(err).Msgf("Failed to remove file %s from torrent %s", file.Name, torrentName)
return err
}
// If the file was successfully removed, we can return nil
return nil
}
}
}
}
h.cache.OnRemove(cachedTorrent.Id)
return nil
}
@@ -202,7 +235,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
cached := h.cache.GetTorrentByName(torrentName)
if cached != nil && len(parts) >= 3 {
filename := filepath.Clean(path.Join(parts[2:]...))
if file, ok := cached.Files[filename]; ok {
if file, ok := cached.GetFile(filename); ok && !file.Deleted {
return &File{
cache: h.cache,
torrentName: torrentName,
@@ -233,12 +266,13 @@ func (h *Handler) Stat(ctx context.Context, name string) (os.FileInfo, error) {
}
func (h *Handler) getFileInfos(torrent *types.Torrent) []os.FileInfo {
files := make([]os.FileInfo, 0, len(torrent.Files))
torrentFiles := torrent.GetFiles()
files := make([]os.FileInfo, 0, len(torrentFiles))
now := time.Now()
// Sort by file name since the order is lost when using the map
sortedFiles := make([]*types.File, 0, len(torrent.Files))
for _, file := range torrent.Files {
sortedFiles := make([]*types.File, 0, len(torrentFiles))
for _, file := range torrentFiles {
sortedFiles = append(sortedFiles, &file)
}
slices.SortFunc(sortedFiles, func(a, b *types.File) int {
@@ -273,7 +307,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.handlePropfind(w, r)
return
case "DELETE":
if err := h.handleDelete(w, r); err == nil {
if err := h.handleIDDelete(w, r); err == nil {
return
}
// fallthrough to default
@@ -504,7 +538,7 @@ func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) {
}
// handleDelete deletes a torrent from using id
func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request) error {
func (h *Handler) handleIDDelete(w http.ResponseWriter, r *http.Request) error {
cleanPath := path.Clean(r.URL.Path) // Remove any leading slashes
_, torrentId := path.Split(cleanPath)