diff --git a/pkg/filemanager/driver/local/local.go b/pkg/filemanager/driver/local/local.go index 699c5d91..127eaa6a 100644 --- a/pkg/filemanager/driver/local/local.go +++ b/pkg/filemanager/driver/local/local.go @@ -7,6 +7,7 @@ import ( "io" "os" "path/filepath" + "strings" "time" "github.com/cloudreve/Cloudreve/v4/ent" @@ -184,6 +185,8 @@ func (handler *Driver) Delete(ctx context.Context, files ...string) ([]string, e handler.l.Warning("Failed to delete file: %s", err) retErr = err deleteFailed = append(deleteFailed, value) + } else { + handler.pruneEmptyAncestors(filePath) } } @@ -194,6 +197,23 @@ func (handler *Driver) Delete(ctx context.Context, files ...string) ([]string, e return deleteFailed, retErr } +// pruneEmptyAncestors removes empty parent directories left behind by a +// deleted blob (#3290). os.Remove fails on non-empty directories, so the +// walk self-terminates; the climb is also bounded to the application +// root so it can never escape the storage tree. +func (handler *Driver) pruneEmptyAncestors(filePath string) { + root := filepath.Dir(util.RelativePath("x")) + for dir := filepath.Dir(filePath); ; dir = filepath.Dir(dir) { + rel, err := filepath.Rel(root, dir) + if err != nil || rel == "." || strings.HasPrefix(rel, "..") { + return + } + if err := os.Remove(dir); err != nil { + return + } + } +} + // Thumb 获取文件缩略图 func (handler *Driver) Thumb(ctx context.Context, expire *time.Time, ext string, e fs.Entity) (string, error) { return "", errors.New("not implemented") diff --git a/pkg/filemanager/driver/local/local_test.go b/pkg/filemanager/driver/local/local_test.go new file mode 100644 index 00000000..165be0e0 --- /dev/null +++ b/pkg/filemanager/driver/local/local_test.go @@ -0,0 +1,39 @@ +package local + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/cloudreve/Cloudreve/v4/pkg/logging" + "github.com/cloudreve/Cloudreve/v4/pkg/util" + "github.com/stretchr/testify/require" +) + +func TestDeletePrunesEmptyAncestors(t *testing.T) { + old := util.UseWorkingDir + util.UseWorkingDir = true + t.Cleanup(func() { util.UseWorkingDir = old }) + t.Chdir(t.TempDir()) + + nested := filepath.Join("uploads", "3", "deep", "nested") + require.NoError(t, os.MkdirAll(nested, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(nested, "a.txt"), []byte("a"), 0644)) + require.NoError(t, os.WriteFile(filepath.Join(nested, "b.txt"), []byte("b"), 0644)) + require.NoError(t, os.WriteFile(filepath.Join("uploads", "keep.txt"), []byte("k"), 0644)) + + d := New(nil, logging.NewConsoleLogger(logging.LevelDebug), nil) + + // Deleting both files should remove deep/nested/3 but stop at uploads + // (still holds keep.txt). + failed, err := d.Delete(context.Background(), + filepath.Join("uploads", "3", "deep", "nested", "a.txt"), + filepath.Join("uploads", "3", "deep", "nested", "b.txt"), + ) + require.NoError(t, err) + require.Empty(t, failed) + require.NoDirExists(t, filepath.Join("uploads", "3")) + require.DirExists(t, "uploads") + require.FileExists(t, filepath.Join("uploads", "keep.txt")) +}