feat: support permanent vault trash deletion
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
@@ -365,20 +366,24 @@ func (s *Service) TrashVaultPath(relativePath string) (TrashResult, error) {
|
||||
if err := os.Rename(full, trashFull); err != nil {
|
||||
return TrashResult{}, err
|
||||
}
|
||||
originalType := fileTypeFromInfo(info)
|
||||
size := sizeForType(originalType, info)
|
||||
result := TrashResult{
|
||||
OriginalPath: rel,
|
||||
TrashPath: trashRel,
|
||||
TrashID: trashID,
|
||||
DeletedAt: deletedAt,
|
||||
Size: size,
|
||||
}
|
||||
meta := map[string]string{
|
||||
"originalPath": rel,
|
||||
"trashPath": trashRel,
|
||||
"trashId": trashID,
|
||||
"deletedAt": deletedAt,
|
||||
"originalType": string(fileTypeFromInfo(info)),
|
||||
"originalType": string(originalType),
|
||||
"basename": filepath.Base(rel),
|
||||
"type": string(fileTypeFromInfo(info)),
|
||||
"type": string(originalType),
|
||||
"size": strconv.FormatInt(size, 10),
|
||||
}
|
||||
data, err := json.MarshalIndent(meta, "", " ")
|
||||
if err != nil {
|
||||
@@ -423,6 +428,7 @@ func (s *Service) ListTrashEntries() ([]TrashEntry, error) {
|
||||
DeletedAt string `json:"deletedAt"`
|
||||
OriginalType string `json:"originalType"`
|
||||
Basename string `json:"basename"`
|
||||
Size string `json:"size"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
continue
|
||||
@@ -437,6 +443,7 @@ func (s *Service) ListTrashEntries() ([]TrashEntry, error) {
|
||||
DeletedAt: raw.DeletedAt,
|
||||
OriginalType: FileType(raw.OriginalType),
|
||||
Basename: raw.Basename,
|
||||
Size: parseTrashSize(raw.Size),
|
||||
})
|
||||
}
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
@@ -524,6 +531,21 @@ func (s *Service) RestoreTrashEntry(trashID string, options RestoreOptions) (str
|
||||
return targetRel, nil
|
||||
}
|
||||
|
||||
// DeleteTrashEntry permanently removes an internal trash entry.
|
||||
func (s *Service) DeleteTrashEntry(trashID string) error {
|
||||
root, err := s.vaultRoot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateTrashID(trashID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := readTrashEntry(root, trashID); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.RemoveAll(filepath.Join(root, ".verstak", "trash", "files", trashID))
|
||||
}
|
||||
|
||||
func readTrashEntry(root, trashID string) (TrashEntry, error) {
|
||||
if err := validateTrashID(trashID); err != nil {
|
||||
return TrashEntry{}, err
|
||||
@@ -542,6 +564,7 @@ func readTrashEntry(root, trashID string) (TrashEntry, error) {
|
||||
DeletedAt string `json:"deletedAt"`
|
||||
OriginalType string `json:"originalType"`
|
||||
Basename string `json:"basename"`
|
||||
Size string `json:"size"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return TrashEntry{}, err
|
||||
@@ -559,9 +582,21 @@ func readTrashEntry(root, trashID string) (TrashEntry, error) {
|
||||
DeletedAt: raw.DeletedAt,
|
||||
OriginalType: FileType(raw.OriginalType),
|
||||
Basename: raw.Basename,
|
||||
Size: parseTrashSize(raw.Size),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseTrashSize(value string) int64 {
|
||||
if value == "" {
|
||||
return 0
|
||||
}
|
||||
size, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil || size < 0 {
|
||||
return 0
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
func validateTrashID(trashID string) error {
|
||||
if trashID == "" || trashID == "." || trashID == ".." || strings.ContainsAny(trashID, "/\\\x00") {
|
||||
return fmt.Errorf("invalid-trash-id")
|
||||
|
||||
@@ -423,7 +423,7 @@ func TestListTrashEntriesReturnsMetadata(t *testing.T) {
|
||||
if entries[0].TrashID != newResult.TrashID || entries[1].TrashID != oldResult.TrashID {
|
||||
t.Fatalf("trash entries order = %+v, want newest first", entries)
|
||||
}
|
||||
if entries[0].OriginalPath != "new.txt" || entries[0].TrashPath != newResult.TrashPath || entries[0].OriginalType != FileTypeFile || entries[0].Basename != "new.txt" {
|
||||
if entries[0].OriginalPath != "new.txt" || entries[0].TrashPath != newResult.TrashPath || entries[0].OriginalType != FileTypeFile || entries[0].Basename != "new.txt" || entries[0].Size != 3 {
|
||||
t.Fatalf("new trash entry = %+v", entries[0])
|
||||
}
|
||||
}
|
||||
@@ -467,6 +467,34 @@ func TestRestoreTrashEntryRestoresOriginalPathAndRemovesTrashMetadata(t *testing
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteTrashEntryRemovesPayloadAndMetadata(t *testing.T) {
|
||||
s, root := newTestService(t)
|
||||
if err := os.WriteFile(filepath.Join(root, "delete-permanently.txt"), []byte("remove me"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
trash, err := s.TrashVaultPath("delete-permanently.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("TrashVaultPath: %v", err)
|
||||
}
|
||||
|
||||
if err := s.DeleteTrashEntry(trash.TrashID); err != nil {
|
||||
t.Fatalf("DeleteTrashEntry: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(root, ".verstak", "trash", "files", trash.TrashID)); !os.IsNotExist(err) {
|
||||
t.Fatalf("trash directory should be removed, stat err = %v", err)
|
||||
}
|
||||
entries, err := s.ListTrashEntries()
|
||||
if err != nil {
|
||||
t.Fatalf("ListTrashEntries: %v", err)
|
||||
}
|
||||
if len(entries) != 0 {
|
||||
t.Fatalf("trash entries after permanent delete = %+v, want none", entries)
|
||||
}
|
||||
if err := s.DeleteTrashEntry(trash.TrashID); err == nil || !strings.Contains(err.Error(), "not-found: trash entry") {
|
||||
t.Fatalf("second DeleteTrashEntry error = %v, want not found", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreTrashEntryConflictAndOverwrite(t *testing.T) {
|
||||
s, root := newTestService(t)
|
||||
if err := os.WriteFile(filepath.Join(root, "conflict.txt"), []byte("trashed"), 0o644); err != nil {
|
||||
|
||||
@@ -72,6 +72,7 @@ type TrashResult struct {
|
||||
TrashPath string `json:"trashPath"`
|
||||
TrashID string `json:"trashId"`
|
||||
DeletedAt string `json:"deletedAt"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
type TrashEntry struct {
|
||||
@@ -81,4 +82,5 @@ type TrashEntry struct {
|
||||
DeletedAt string `json:"deletedAt"`
|
||||
OriginalType FileType `json:"originalType"`
|
||||
Basename string `json:"basename"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user