fix(local): prune empty parent dirs after blob delete (#3290)

Deleting a folder removed file blobs but left the empty directory
skeleton on the local filesystem. After a successful remove, walk
upward deleting empty ancestors — os.Remove self-terminates on the
first non-empty dir, and the climb is bounded to the application root.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
pull/3582/head
Tomas Dvorak 2 weeks ago
parent 3e1d5ed56d
commit 01e65f4e69

@ -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")

@ -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"))
}
Loading…
Cancel
Save