fix(bt): clean partial files and empty parent directories

This commit is contained in:
Levi committed 2026-09-17 23:35:40 +08:00
1 parent 967ef25408
commit 7c33d978c4
2 files changed
+88 -8

No files matched your search

+30 -4
View File
@@ -399,10 +399,8 @@ func (f *Fetcher) Wait() (err error) {
}
}
if !selected {
// Path already includes the torrent's root directory.
name := filepath.Join(f.meta.Opts.Path, file.Path())
if err := util.SafeRemove(name); err != nil {
return fmt.Errorf("remove unselected torrent file %q: %w", name, err)
if err := removeUnselectedFile(f.meta.Opts.Path, file.Path()); err != nil {
return err
}
}
}
@@ -413,6 +411,34 @@ func (f *Fetcher) Wait() (err error) {
}
}
// Path already includes the torrent's root directory. Never prune the download
// directory itself, and only remove parent directories that are empty.
func removeUnselectedFile(downloadDir, torrentPath string) error {
root := filepath.Clean(downloadDir)
name := filepath.Join(root, torrentPath)
for _, candidate := range []string{name, name + ".part"} {
if err := util.SafeRemove(candidate); err != nil {
return fmt.Errorf("remove unselected torrent file %q: %w", candidate, err)
}
}
for dir := filepath.Dir(name); dir != root && dir != filepath.Dir(dir); dir = filepath.Dir(dir) {
entries, err := os.ReadDir(dir)
if os.IsNotExist(err) {
continue
}
if err != nil {
return fmt.Errorf("read torrent directory %q: %w", dir, err)
}
if len(entries) != 0 {
break
}
if err := util.SafeRemove(dir); err != nil {
return fmt.Errorf("remove empty torrent directory %q: %w", dir, err)
}
}
return nil
}
func (f *Fetcher) isDone() bool {
if f.meta.Opts == nil {
return false
+58 -4
View File
@@ -3,6 +3,7 @@ package bt
import (
"context"
"crypto/sha1"
"fmt"
"os"
"path/filepath"
"testing"
@@ -62,7 +63,7 @@ func TestStartUsesSelectionSubmittedAfterResolve(t *testing.T) {
}
func TestWaitCleansUnselectedFilesFromSharedPiece(t *testing.T) {
for _, mode := range []string{"partial", "all", "already removed", "remove error"} {
for _, mode := range []string{"partial", "part only", "both paths", "all", "already removed", "remove error"} {
t.Run(mode, func(t *testing.T) {
dir := t.TempDir()
keepPath := filepath.Join(dir, "bundle", "keep.txt")
@@ -118,7 +119,12 @@ func TestWaitCleansUnselectedFilesFromSharedPiece(t *testing.T) {
if !f.isDone() {
t.Fatal("selected file is not complete")
}
if mode == "already removed" || mode == "remove error" {
if mode == "part only" || mode == "both paths" || mode == "all" {
if err := os.WriteFile(skipPath+".part", []byte("skip"), 0644); err != nil {
t.Fatal(err)
}
}
if mode == "part only" || mode == "already removed" || mode == "remove error" {
if err := os.Remove(skipPath); err != nil {
t.Fatal(err)
}
@@ -147,15 +153,63 @@ func TestWaitCleansUnselectedFilesFromSharedPiece(t *testing.T) {
t.Fatalf("selected file changed: %q, %v", data, err)
}
if mode == "all" {
if _, err := os.Stat(skipPath + ".part"); err != nil {
t.Fatalf("selected part file was removed: %v", err)
}
data, err := os.ReadFile(skipPath)
if err != nil || string(data) != "skip" {
t.Fatalf("selected file changed: %q, %v", data, err)
}
} else if mode != "remove error" {
if _, err := os.Stat(skipPath); !os.IsNotExist(err) {
t.Fatalf("unselected file remains: %v", err)
for _, removed := range []string{skipPath, skipPath + ".part", filepath.Dir(skipPath)} {
if _, err := os.Stat(removed); !os.IsNotExist(err) {
t.Fatalf("unselected file or empty directory remains: %s: %v", removed, err)
}
}
}
})
}
}
func TestRemoveUnselectedFilePrunesOnlyEmptyParents(t *testing.T) {
for _, preserveSibling := range []bool{false, true} {
t.Run(fmt.Sprint(preserveSibling), func(t *testing.T) {
root := t.TempDir()
relative := filepath.Join("bundle", "nested", "deep", "skip.txt")
name := filepath.Join(root, relative)
if err := os.MkdirAll(filepath.Dir(name), 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(name+".part", []byte("partial"), 0644); err != nil {
t.Fatal(err)
}
sibling := filepath.Join(root, "bundle", "keep.txt")
if preserveSibling {
if err := os.WriteFile(sibling, []byte("keep"), 0644); err != nil {
t.Fatal(err)
}
}
if err := removeUnselectedFile(root, relative); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(root, "bundle", "nested")); !os.IsNotExist(err) {
t.Fatalf("empty ancestors remain: %v", err)
}
if preserveSibling {
data, err := os.ReadFile(sibling)
if err != nil || string(data) != "keep" {
t.Fatalf("sibling changed: %q, %v", data, err)
}
} else if _, err := os.Stat(filepath.Join(root, "bundle")); !os.IsNotExist(err) {
t.Fatalf("empty torrent directory remains: %v", err)
}
if _, err := os.Stat(root); err != nil {
t.Fatalf("download directory was removed: %v", err)
}
// Repeating cleanup after all files and directories are gone is harmless.
if err := removeUnselectedFile(root, relative); err != nil {
t.Fatal(err)
}
})
}
}