feat: bridge permanent trash deletion to plugins

This commit is contained in:
mirivlad 2026-07-10 08:35:46 +08:00
parent 8445ea11db
commit 3f27ba79a4
7 changed files with 65 additions and 16 deletions

View File

@ -85,7 +85,7 @@ Frontend bundles are mounted with a plugin-scoped API created by
- `capabilities.list/get/has` - `capabilities.list/get/has`
- `commands.register/execute` for handlers declared in `contributes.commands` - `commands.register/execute` for handlers declared in `contributes.commands`
- `events.publish/subscribe` using the bundled frontend event bus - `events.publish/subscribe` using the bundled frontend event bus
- `files.list/metadata/readText/readBytes/writeText/createFolder/move/trash/openExternal/showInFolder` - `files.list/metadata/readText/readBytes/writeText/createFolder/move/trash/listTrash/restoreTrash/deleteTrash/openExternal/showInFolder`
for canonical vault-relative slash paths guarded by `files.read`, for canonical vault-relative slash paths guarded by `files.read`,
`files.write`, `files.delete`, and `files.openExternal`. Backslashes, `files.write`, `files.delete`, and `files.openExternal`. Backslashes,
Windows absolute paths, UNC paths, traversal, `.verstak` variants, and Windows absolute paths, UNC paths, traversal, `.verstak` variants, and

View File

@ -125,6 +125,29 @@ test.describe('D: Plugin API bridge', () => {
expect(result.disconnected.statusLabel).toBe('disabled'); expect(result.disconnected.statusLabel).toBe('disabled');
}); });
test('files API permanently deletes an existing trash entry through the bridge', async ({ page }) => {
const result = await page.evaluate(async () => {
const api = window.createPluginAPI('verstak.files');
await api.files.writeText('Project/bridge-trash.txt', 'remove me', { createIfMissing: true });
const trash = await api.files.trash('Project/bridge-trash.txt');
const before = await api.files.listTrash();
await api.files.deleteTrash(trash.trashId);
const after = await api.files.listTrash();
let readError = '';
try {
await api.files.readText('Project/bridge-trash.txt');
} catch (error) {
readError = String(error && error.message || error);
}
api.dispose();
return { before, after, readError };
});
expect(result.before).toHaveLength(1);
expect(result.after).toEqual([]);
expect(result.readError).toContain('not-found: Project/bridge-trash.txt');
});
test('backend plugin events are dispatched to subscribed frontend handlers', async ({ page }) => { test('backend plugin events are dispatched to subscribed frontend handlers', async ({ page }) => {
const result = await page.evaluate(async () => { const result = await page.evaluate(async () => {
const api = window.createPluginAPI('verstak.platform-test'); const api = window.createPluginAPI('verstak.platform-test');

View File

@ -380,6 +380,12 @@ export function createPluginAPI(pluginId) {
return App.RestoreVaultTrash(pluginId, trashId, options || {}); return App.RestoreVaultTrash(pluginId, trashId, options || {});
}); });
}, },
deleteTrash: function(trashId) {
assertActive('files.deleteTrash(' + trashId + ')');
return callBackendErrorString(pluginId, 'files.deleteTrash(' + trashId + ')', function() {
return App.DeleteVaultTrash(pluginId, trashId);
});
},
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() {

View File

@ -2608,13 +2608,15 @@
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 originalNode = vaultFiles[norm.path];
var originalType = originalNode.type || 'file';
var originalSize = originalType === 'file' ? String(originalNode.content || '').length : 0;
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; });
trashPayloads[trashId] = moving.map(function (path) { trashPayloads[trashId] = moving.map(function (path) {
return { suffix: path.slice(norm.path.length), entry: Object.assign({}, vaultFiles[path]) }; return { suffix: path.slice(norm.path.length), entry: Object.assign({}, vaultFiles[path]) };
}); });
moving.forEach(function (path) { delete vaultFiles[path]; }); moving.forEach(function (path) { delete vaultFiles[path]; });
var entry = { originalPath: norm.path, trashPath: trashPath, trashId: trashId, deletedAt: new Date().toISOString(), originalType: originalType, basename: baseName(norm.path) }; var entry = { originalPath: norm.path, trashPath: trashPath, trashId: trashId, deletedAt: new Date().toISOString(), originalType: originalType, basename: baseName(norm.path), size: originalSize };
trashEntries.unshift(entry); trashEntries.unshift(entry);
return Promise.resolve([entry, '']); return Promise.resolve([entry, '']);
}, },
@ -2646,6 +2648,15 @@
trashEntries = trashEntries.filter(function (item) { return item.trashId !== trashId; }); trashEntries = trashEntries.filter(function (item) { return item.trashId !== trashId; });
return Promise.resolve([target.path, '']); return Promise.resolve([target.path, '']);
}, },
DeleteVaultTrash: function (pluginId, trashId) {
var err = requirePluginPermission(pluginId, 'files.delete');
if (err) return Promise.resolve(err);
var entry = trashEntries.find(function (item) { return item.trashId === trashId; });
if (!entry) return Promise.resolve('not-found: trash entry ' + trashId);
delete trashPayloads[trashId];
trashEntries = trashEntries.filter(function (item) { return item.trashId !== trashId; });
return Promise.resolve('');
},
OpenVaultPathExternal: function (pluginId, relativePath) { OpenVaultPathExternal: function (pluginId, relativePath) {
var err = requirePluginPermission(pluginId, 'files.openExternal'); var err = requirePluginPermission(pluginId, 'files.openExternal');
if (err) return Promise.resolve(err); if (err) return Promise.resolve(err);

View File

@ -20,6 +20,8 @@ export function CreateWorkspace(arg1:string,arg2:string):Promise<workspace.Works
export function CreateWorkspaceNode(arg1:string,arg2:string,arg3:string):Promise<Record<string, any>>; export function CreateWorkspaceNode(arg1:string,arg2:string,arg3:string):Promise<Record<string, any>>;
export function DeleteVaultTrash(arg1:string,arg2:string):Promise<string>;
export function DisablePlugin(arg1:string):Promise<string>; export function DisablePlugin(arg1:string):Promise<string>;
export function EditWorkbenchResource(arg1:string,arg2:Record<string, any>):Promise<workbench.OpenResourceResult|string>; export function EditWorkbenchResource(arg1:string,arg2:Record<string, any>):Promise<workbench.OpenResourceResult|string>;
@ -80,6 +82,10 @@ export function OpenVaultPathExternal(arg1:string,arg2:string):Promise<string>;
export function OpenWorkbenchResource(arg1:string,arg2:Record<string, any>):Promise<workbench.OpenResourceResult|string>; export function OpenWorkbenchResource(arg1:string,arg2:Record<string, any>):Promise<workbench.OpenResourceResult|string>;
export function PluginBrowserReceiverPairing(arg1:string):Promise<Record<string, string>|string>;
export function PluginRotateBrowserReceiverToken(arg1:string):Promise<Record<string, string>|string>;
export function PluginSecretsCopyLink(arg1:string,arg2:string):Promise<string|string>; export function PluginSecretsCopyLink(arg1:string,arg2:string):Promise<string|string>;
export function PluginSecretsDelete(arg1:string,arg2:string):Promise<string>; export function PluginSecretsDelete(arg1:string,arg2:string):Promise<string>;
@ -94,10 +100,6 @@ export function PluginSecretsUnlock(arg1:string,arg2:string):Promise<string>;
export function PluginSecretsWrite(arg1:string,arg2:Record<string, any>):Promise<Record<string, any>|string>; export function PluginSecretsWrite(arg1:string,arg2:Record<string, any>):Promise<Record<string, any>|string>;
export function PluginBrowserReceiverPairing(arg1:string):Promise<Record<string, string>|string>;
export function PluginRotateBrowserReceiverToken(arg1:string):Promise<Record<string, string>|string>;
export function PluginSyncConfigure(arg1:string,arg2:string,arg3:string,arg4:string):Promise<string>; export function PluginSyncConfigure(arg1:string,arg2:string,arg3:string,arg4:string):Promise<string>;
export function PluginSyncDisconnect(arg1:string):Promise<string>; export function PluginSyncDisconnect(arg1:string):Promise<string>;

View File

@ -26,6 +26,10 @@ export function CreateWorkspaceNode(arg1, arg2, arg3) {
return window['go']['api']['App']['CreateWorkspaceNode'](arg1, arg2, arg3); return window['go']['api']['App']['CreateWorkspaceNode'](arg1, arg2, arg3);
} }
export function DeleteVaultTrash(arg1, arg2) {
return window['go']['api']['App']['DeleteVaultTrash'](arg1, arg2);
}
export function DisablePlugin(arg1) { export function DisablePlugin(arg1) {
return window['go']['api']['App']['DisablePlugin'](arg1); return window['go']['api']['App']['DisablePlugin'](arg1);
} }
@ -146,6 +150,14 @@ export function OpenWorkbenchResource(arg1, arg2) {
return window['go']['api']['App']['OpenWorkbenchResource'](arg1, arg2); return window['go']['api']['App']['OpenWorkbenchResource'](arg1, arg2);
} }
export function PluginBrowserReceiverPairing(arg1) {
return window['go']['api']['App']['PluginBrowserReceiverPairing'](arg1);
}
export function PluginRotateBrowserReceiverToken(arg1) {
return window['go']['api']['App']['PluginRotateBrowserReceiverToken'](arg1);
}
export function PluginSecretsCopyLink(arg1, arg2) { export function PluginSecretsCopyLink(arg1, arg2) {
return window['go']['api']['App']['PluginSecretsCopyLink'](arg1, arg2); return window['go']['api']['App']['PluginSecretsCopyLink'](arg1, arg2);
} }
@ -174,14 +186,6 @@ export function PluginSecretsWrite(arg1, arg2) {
return window['go']['api']['App']['PluginSecretsWrite'](arg1, arg2); return window['go']['api']['App']['PluginSecretsWrite'](arg1, arg2);
} }
export function PluginBrowserReceiverPairing(arg1) {
return window['go']['api']['App']['PluginBrowserReceiverPairing'](arg1);
}
export function PluginRotateBrowserReceiverToken(arg1) {
return window['go']['api']['App']['PluginRotateBrowserReceiverToken'](arg1);
}
export function PluginSyncConfigure(arg1, arg2, arg3, arg4) { export function PluginSyncConfigure(arg1, arg2, arg3, arg4) {
return window['go']['api']['App']['PluginSyncConfigure'](arg1, arg2, arg3, arg4); return window['go']['api']['App']['PluginSyncConfigure'](arg1, arg2, arg3, arg4);
} }

View File

@ -484,6 +484,7 @@ export namespace files {
deletedAt: string; deletedAt: string;
originalType: string; originalType: string;
basename: string; basename: string;
size: number;
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
return new TrashEntry(source); return new TrashEntry(source);
@ -497,6 +498,7 @@ export namespace files {
this.deletedAt = source["deletedAt"]; this.deletedAt = source["deletedAt"];
this.originalType = source["originalType"]; this.originalType = source["originalType"];
this.basename = source["basename"]; this.basename = source["basename"];
this.size = source["size"];
} }
} }
export class TrashResult { export class TrashResult {
@ -504,6 +506,7 @@ export namespace files {
trashPath: string; trashPath: string;
trashId: string; trashId: string;
deletedAt: string; deletedAt: string;
size: number;
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
return new TrashResult(source); return new TrashResult(source);
@ -515,6 +518,7 @@ export namespace files {
this.trashPath = source["trashPath"]; this.trashPath = source["trashPath"];
this.trashId = source["trashId"]; this.trashId = source["trashId"];
this.deletedAt = source["deletedAt"]; this.deletedAt = source["deletedAt"];
this.size = source["size"];
} }
} }
export class WriteOptions { export class WriteOptions {
@ -1315,4 +1319,3 @@ export namespace workspace {
} }
} }