- 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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user