From 7f25599b60ca132e466b02def1968683f4c62fa6 Mon Sep 17 00:00:00 2001 From: Mukhtar Akere Date: Tue, 27 May 2025 19:31:19 +0100 Subject: [PATCH] - Add support for per-file deletion - Per-file repair instead of per-torrent - Fix issues with LoadLocation - Fix other minor bug fixes woth torbox --- pkg/debrid/debrid/cache.go | 52 ++++++++++++++--- pkg/debrid/debrid/download_link.go | 22 ++++++-- pkg/debrid/debrid/misc.go | 2 +- pkg/debrid/debrid/repair.go | 29 ++++------ pkg/debrid/torbox/torbox.go | 2 +- pkg/debrid/types/torrent.go | 29 +++++++--- pkg/qbit/downloader.go | 74 ++++++++++++++++++++++++- pkg/qbit/torrent.go | 6 +- pkg/repair/repair.go | 89 ++++++++++++++++-------------- pkg/webdav/file.go | 2 + pkg/webdav/handler.go | 64 ++++++++++++++++----- 11 files changed, 268 insertions(+), 103 deletions(-) diff --git a/pkg/debrid/debrid/cache.go b/pkg/debrid/debrid/cache.go index 752bb37..0c2d1ae 100644 --- a/pkg/debrid/debrid/cache.go +++ b/pkg/debrid/debrid/cache.go @@ -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 } diff --git a/pkg/debrid/debrid/download_link.go b/pkg/debrid/debrid/download_link.go index 9499dcb..b7df841 100644 --- a/pkg/debrid/debrid/download_link.go +++ b/pkg/debrid/debrid/download_link.go @@ -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) } diff --git a/pkg/debrid/debrid/misc.go b/pkg/debrid/debrid/misc.go index f547ada..b004078 100644 --- a/pkg/debrid/debrid/misc.go +++ b/pkg/debrid/debrid/misc.go @@ -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 } } diff --git a/pkg/debrid/debrid/repair.go b/pkg/debrid/debrid/repair.go index 5a0df5c..fae4ecd 100644 --- a/pkg/debrid/debrid/repair.go +++ b/pkg/debrid/debrid/repair.go @@ -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") diff --git a/pkg/debrid/torbox/torbox.go b/pkg/debrid/torbox/torbox.go index aac1e48..9ce0397 100644 --- a/pkg/debrid/torbox/torbox.go +++ b/pkg/debrid/torbox/torbox.go @@ -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 } diff --git a/pkg/debrid/types/torrent.go b/pkg/debrid/types/torrent.go index def1183..b072efe 100644 --- a/pkg/debrid/types/torrent.go +++ b/pkg/debrid/types/torrent.go @@ -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"` diff --git a/pkg/qbit/downloader.go b/pkg/qbit/downloader.go index 0071865..3881f7b 100644 --- a/pkg/qbit/downloader.go +++ b/pkg/qbit/downloader.go @@ -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) { diff --git a/pkg/qbit/torrent.go b/pkg/qbit/torrent.go index e75855a..5b61e39 100644 --- a/pkg/qbit/torrent.go +++ b/pkg/qbit/torrent.go @@ -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, diff --git a/pkg/repair/repair.go b/pkg/repair/repair.go index f59267e..4cc96ec 100644 --- a/pkg/repair/repair.go +++ b/pkg/repair/repair.go @@ -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) + } + } } } diff --git a/pkg/webdav/file.go b/pkg/webdav/file.go index 95fb405..528ae7f 100644 --- a/pkg/webdav/file.go +++ b/pkg/webdav/file.go @@ -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 diff --git a/pkg/webdav/handler.go b/pkg/webdav/handler.go index df1089a..95b6df0 100644 --- a/pkg/webdav/handler.go +++ b/pkg/webdav/handler.go @@ -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)