feat: expose file trash metadata
This commit is contained in:
parent
ed4b117a94
commit
e5bdaec0aa
|
|
@ -266,6 +266,12 @@ export function createPluginAPI(pluginId) {
|
||||||
return App.TrashVaultPath(pluginId, relativePath);
|
return App.TrashVaultPath(pluginId, relativePath);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
listTrash: function() {
|
||||||
|
assertActive('files.listTrash');
|
||||||
|
return callBackend(pluginId, 'files.listTrash', function() {
|
||||||
|
return App.ListVaultTrash(pluginId);
|
||||||
|
});
|
||||||
|
},
|
||||||
openExternal: function(relativePath) {
|
openExternal: function(relativePath) {
|
||||||
assertActive('files.openExternal(' + relativePath + ')');
|
assertActive('files.openExternal(' + relativePath + ')');
|
||||||
return callBackendErrorString(pluginId, 'files.openExternal(' + relativePath + ')', function() {
|
return callBackendErrorString(pluginId, 'files.openExternal(' + relativePath + ')', function() {
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,7 @@
|
||||||
};
|
};
|
||||||
var vaultFiles = makeDefaultVaultFiles();
|
var vaultFiles = makeDefaultVaultFiles();
|
||||||
var externalOpens = [];
|
var externalOpens = [];
|
||||||
|
var trashEntries = [];
|
||||||
window.__wailsMockExternalOpens = [];
|
window.__wailsMockExternalOpens = [];
|
||||||
var workspaceTree = makeDefaultWorkspaceTree();
|
var workspaceTree = makeDefaultWorkspaceTree();
|
||||||
var reloadResponseMode = 'tuple';
|
var reloadResponseMode = 'tuple';
|
||||||
|
|
@ -1342,9 +1343,17 @@
|
||||||
if (!vaultFiles[norm.path]) return Promise.resolve([{}, 'not-found: ' + norm.path]);
|
if (!vaultFiles[norm.path]) return Promise.resolve([{}, 'not-found: ' + norm.path]);
|
||||||
var trashId = 'mock-' + Date.now() + '-' + Math.random().toString(16).slice(2);
|
var trashId = 'mock-' + Date.now() + '-' + Math.random().toString(16).slice(2);
|
||||||
var trashPath = '.verstak/trash/files/' + trashId + '/' + baseName(norm.path);
|
var trashPath = '.verstak/trash/files/' + trashId + '/' + baseName(norm.path);
|
||||||
|
var originalType = vaultFiles[norm.path].type || 'file';
|
||||||
var moving = Object.keys(vaultFiles).filter(function (path) { return path === norm.path || path.indexOf(norm.path + '/') === 0; });
|
var moving = Object.keys(vaultFiles).filter(function (path) { return path === norm.path || path.indexOf(norm.path + '/') === 0; });
|
||||||
moving.forEach(function (path) { delete vaultFiles[path]; });
|
moving.forEach(function (path) { delete vaultFiles[path]; });
|
||||||
return Promise.resolve([{ originalPath: norm.path, trashPath: trashPath, trashId: trashId, deletedAt: new Date().toISOString() }, '']);
|
var entry = { originalPath: norm.path, trashPath: trashPath, trashId: trashId, deletedAt: new Date().toISOString(), originalType: originalType, basename: baseName(norm.path) };
|
||||||
|
trashEntries.unshift(entry);
|
||||||
|
return Promise.resolve([entry, '']);
|
||||||
|
},
|
||||||
|
ListVaultTrash: function (pluginId) {
|
||||||
|
var err = requirePluginPermission(pluginId, 'files.delete');
|
||||||
|
if (err) return Promise.resolve([[], err]);
|
||||||
|
return Promise.resolve([trashEntries.slice(), '']);
|
||||||
},
|
},
|
||||||
OpenVaultPathExternal: function (pluginId, relativePath) {
|
OpenVaultPathExternal: function (pluginId, relativePath) {
|
||||||
var err = requirePluginPermission(pluginId, 'files.openExternal');
|
var err = requirePluginPermission(pluginId, 'files.openExternal');
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,8 @@ export function SubscribePluginEvent(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|
||||||
export function TrashVaultPath(arg1:string,arg2:string):Promise<files.TrashResult|string>;
|
export function TrashVaultPath(arg1:string,arg2:string):Promise<files.TrashResult|string>;
|
||||||
|
|
||||||
|
export function ListVaultTrash(arg1:string):Promise<any[]|string>;
|
||||||
|
|
||||||
export function TrashWorkspace(arg1:string):Promise<workspace.TrashResult|string>;
|
export function TrashWorkspace(arg1:string):Promise<workspace.TrashResult|string>;
|
||||||
|
|
||||||
export function UpdateAppSettings(arg1:Record<string, any>):Promise<string>;
|
export function UpdateAppSettings(arg1:Record<string, any>):Promise<string>;
|
||||||
|
|
|
||||||
|
|
@ -238,6 +238,10 @@ export function TrashVaultPath(arg1, arg2) {
|
||||||
return window['go']['api']['App']['TrashVaultPath'](arg1, arg2);
|
return window['go']['api']['App']['TrashVaultPath'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ListVaultTrash(arg1) {
|
||||||
|
return window['go']['api']['App']['ListVaultTrash'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function TrashWorkspace(arg1) {
|
export function TrashWorkspace(arg1) {
|
||||||
return window['go']['api']['App']['TrashWorkspace'](arg1);
|
return window['go']['api']['App']['TrashWorkspace'](arg1);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -972,6 +972,21 @@ func (a *App) TrashVaultPath(pluginID, relativePath string) (corefiles.TrashResu
|
||||||
return result, ""
|
return result, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListVaultTrash returns trash metadata entries for a plugin with files.delete.
|
||||||
|
func (a *App) ListVaultTrash(pluginID string) ([]corefiles.TrashEntry, string) {
|
||||||
|
if _, err := a.requirePluginAccess(pluginID, "files.delete"); err != nil {
|
||||||
|
return nil, err.Error()
|
||||||
|
}
|
||||||
|
if a.files == nil {
|
||||||
|
return nil, "files service not initialized"
|
||||||
|
}
|
||||||
|
entries, err := a.files.ListTrashEntries()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err.Error()
|
||||||
|
}
|
||||||
|
return entries, ""
|
||||||
|
}
|
||||||
|
|
||||||
// OpenVaultPathExternal opens a vault-relative file or folder in the OS default app.
|
// OpenVaultPathExternal opens a vault-relative file or folder in the OS default app.
|
||||||
func (a *App) OpenVaultPathExternal(pluginID, relativePath string) string {
|
func (a *App) OpenVaultPathExternal(pluginID, relativePath string) string {
|
||||||
if _, err := a.requirePluginAccess(pluginID, "files.openExternal"); err != nil {
|
if _, err := a.requirePluginAccess(pluginID, "files.openExternal"); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -388,6 +388,13 @@ func TestFilesBridgeReadWriteListMoveTrash(t *testing.T) {
|
||||||
if _, err := os.Stat(filepath.Join(root, trash.TrashPath)); err != nil {
|
if _, err := os.Stat(filepath.Join(root, trash.TrashPath)); err != nil {
|
||||||
t.Fatalf("trash path missing: %v", err)
|
t.Fatalf("trash path missing: %v", err)
|
||||||
}
|
}
|
||||||
|
trashEntries, errStr := app.ListVaultTrash("files.plugin")
|
||||||
|
if errStr != "" {
|
||||||
|
t.Fatalf("ListVaultTrash: %s", errStr)
|
||||||
|
}
|
||||||
|
if len(trashEntries) != 1 || trashEntries[0].OriginalPath != "Docs/two.txt" || trashEntries[0].TrashID != trash.TrashID {
|
||||||
|
t.Fatalf("trash entries = %+v, want Docs/two.txt", trashEntries)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilesBridgeWritePublishesFileChangedActivityEvent(t *testing.T) {
|
func TestFilesBridgeWritePublishesFileChangedActivityEvent(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"mime"
|
"mime"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
@ -337,6 +338,64 @@ func (s *Service) TrashVaultPath(relativePath string) (TrashResult, error) {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListTrashEntries() ([]TrashEntry, error) {
|
||||||
|
root, err := s.vaultRoot()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
trashRoot := filepath.Join(root, ".verstak", "trash", "files")
|
||||||
|
dirs, err := os.ReadDir(trashRoot)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return []TrashEntry{}, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := make([]TrashEntry, 0, len(dirs))
|
||||||
|
for _, dir := range dirs {
|
||||||
|
if !dir.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(filepath.Join(trashRoot, dir.Name(), "metadata.json"))
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var raw struct {
|
||||||
|
OriginalPath string `json:"originalPath"`
|
||||||
|
TrashPath string `json:"trashPath"`
|
||||||
|
TrashID string `json:"trashId"`
|
||||||
|
DeletedAt string `json:"deletedAt"`
|
||||||
|
OriginalType string `json:"originalType"`
|
||||||
|
Basename string `json:"basename"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if raw.OriginalPath == "" || raw.TrashPath == "" || raw.TrashID == "" || raw.DeletedAt == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entries = append(entries, TrashEntry{
|
||||||
|
OriginalPath: raw.OriginalPath,
|
||||||
|
TrashPath: raw.TrashPath,
|
||||||
|
TrashID: raw.TrashID,
|
||||||
|
DeletedAt: raw.DeletedAt,
|
||||||
|
OriginalType: FileType(raw.OriginalType),
|
||||||
|
Basename: raw.Basename,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sort.Slice(entries, func(i, j int) bool {
|
||||||
|
if entries[i].DeletedAt != entries[j].DeletedAt {
|
||||||
|
return entries[i].DeletedAt > entries[j].DeletedAt
|
||||||
|
}
|
||||||
|
return entries[i].TrashID > entries[j].TrashID
|
||||||
|
})
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) vaultRoot() (string, error) {
|
func (s *Service) vaultRoot() (string, error) {
|
||||||
if s == nil || s.vault == nil {
|
if s == nil || s.vault == nil {
|
||||||
return "", fmt.Errorf("vault-not-initialized")
|
return "", fmt.Errorf("vault-not-initialized")
|
||||||
|
|
|
||||||
|
|
@ -301,6 +301,39 @@ func TestTrashVaultPathMovesToReservedTrashAndHidesFromList(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListTrashEntriesReturnsMetadata(t *testing.T) {
|
||||||
|
s, root := newTestService(t)
|
||||||
|
if err := os.WriteFile(filepath.Join(root, "old.txt"), []byte("old"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(root, "new.txt"), []byte("new"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oldResult, err := s.TrashVaultPath("old.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("trash old: %v", err)
|
||||||
|
}
|
||||||
|
newResult, err := s.TrashVaultPath("new.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("trash new: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := s.ListTrashEntries()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListTrashEntries: %v", err)
|
||||||
|
}
|
||||||
|
if len(entries) != 2 {
|
||||||
|
t.Fatalf("trash entries count = %d, want 2: %+v", len(entries), entries)
|
||||||
|
}
|
||||||
|
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" {
|
||||||
|
t.Fatalf("new trash entry = %+v", entries[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSymlinkEscapeRejected(t *testing.T) {
|
func TestSymlinkEscapeRejected(t *testing.T) {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
t.Skip("symlink creation requires privileges on many Windows test environments")
|
t.Skip("symlink creation requires privileges on many Windows test environments")
|
||||||
|
|
|
||||||
|
|
@ -60,3 +60,12 @@ type TrashResult struct {
|
||||||
TrashID string `json:"trashId"`
|
TrashID string `json:"trashId"`
|
||||||
DeletedAt string `json:"deletedAt"`
|
DeletedAt string `json:"deletedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TrashEntry struct {
|
||||||
|
OriginalPath string `json:"originalPath"`
|
||||||
|
TrashPath string `json:"trashPath"`
|
||||||
|
TrashID string `json:"trashId"`
|
||||||
|
DeletedAt string `json:"deletedAt"`
|
||||||
|
OriginalType FileType `json:"originalType"`
|
||||||
|
Basename string `json:"basename"`
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue