feat: expose binary file writes in sdk
This commit is contained in:
parent
380f90c5ec
commit
b11178d8fd
|
|
@ -30,8 +30,8 @@
|
|||
{ "name": "vault.read", "description": "Read vault files and metadata", "dangerous": false },
|
||||
{ "name": "vault.write", "description": "Write vault files and metadata", "dangerous": true },
|
||||
{ "name": "vault.watch", "description": "Watch vault file changes", "dangerous": false },
|
||||
{ "name": "files.read", "description": "List files and read text files through the vault Files API", "dangerous": false },
|
||||
{ "name": "files.write", "description": "Create folders, write text files, and move paths through the vault Files API", "dangerous": true },
|
||||
{ "name": "files.read", "description": "List files and read bounded text or byte payloads through the vault Files API", "dangerous": false },
|
||||
{ "name": "files.write", "description": "Create folders, write bounded text or byte payloads, and move paths through the vault Files API", "dangerous": true },
|
||||
{ "name": "files.delete", "description": "Trash vault files and folders through the vault Files API", "dangerous": true },
|
||||
{ "name": "files.openExternal", "description": "Open vault files and folders in external OS applications", "dangerous": true },
|
||||
{ "name": "storage.namespace", "description": "Read/write plugin's own storage namespace", "dangerous": false },
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ describe('VerstakPluginAPI contract', () => {
|
|||
expect(typeof api.files.readText).toBe('function');
|
||||
expect(typeof api.files.readBytes).toBe('function');
|
||||
expect(typeof api.files.writeText).toBe('function');
|
||||
expect(typeof api.files.writeBytes).toBe('function');
|
||||
expect(typeof api.files.createFolder).toBe('function');
|
||||
expect(typeof api.files.move).toBe('function');
|
||||
expect(typeof api.files.trash).toBe('function');
|
||||
|
|
@ -286,6 +287,7 @@ describe('VerstakPluginAPI contract', () => {
|
|||
|
||||
await api.files.createFolder('PlatformTest');
|
||||
await api.files.writeText('PlatformTest/one.txt', 'hello', { createIfMissing: true });
|
||||
await api.files.writeBytes('PlatformTest/image.bin', 'AQID', { createIfMissing: true });
|
||||
await expect(api.files.readText('PlatformTest/one.txt')).resolves.toBe('hello');
|
||||
await expect(api.files.readBytes('PlatformTest/one.txt')).resolves.toEqual({
|
||||
relativePath: 'PlatformTest/one.txt',
|
||||
|
|
@ -293,9 +295,16 @@ describe('VerstakPluginAPI contract', () => {
|
|||
mimeHint: 'text/plain; charset=utf-8',
|
||||
dataBase64: 'aGVsbG8=',
|
||||
});
|
||||
await expect(api.files.list('PlatformTest')).resolves.toEqual([
|
||||
await expect(api.files.readBytes('PlatformTest/image.bin')).resolves.toEqual({
|
||||
relativePath: 'PlatformTest/image.bin',
|
||||
size: 3,
|
||||
mimeHint: '',
|
||||
dataBase64: 'AQID',
|
||||
});
|
||||
await expect(api.files.list('PlatformTest')).resolves.toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ relativePath: 'PlatformTest/one.txt', type: 'file' }),
|
||||
]);
|
||||
expect.objectContaining({ relativePath: 'PlatformTest/image.bin', type: 'file' }),
|
||||
]));
|
||||
await expect(api.files.openExternal('PlatformTest/one.txt')).resolves.toBeUndefined();
|
||||
await expect(api.files.showInFolder('PlatformTest/one.txt')).resolves.toBeUndefined();
|
||||
await api.files.move('PlatformTest/one.txt', 'PlatformTest/two.txt');
|
||||
|
|
@ -308,9 +317,10 @@ describe('VerstakPluginAPI contract', () => {
|
|||
expect.objectContaining({ originalPath: 'PlatformTest/two.txt', trashId: trash.trashId }),
|
||||
]);
|
||||
await expect(api.files.restoreTrash(trash.trashId)).resolves.toBe('PlatformTest/two.txt');
|
||||
await expect(api.files.list('PlatformTest')).resolves.toEqual([
|
||||
await expect(api.files.list('PlatformTest')).resolves.toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ relativePath: 'PlatformTest/two.txt', type: 'file' }),
|
||||
]);
|
||||
expect.objectContaining({ relativePath: 'PlatformTest/image.bin', type: 'file' }),
|
||||
]));
|
||||
await expect(api.files.listTrash()).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ export interface VerstakPluginAPI {
|
|||
readText(relativePath: string): Promise<string>;
|
||||
readBytes(relativePath: string): Promise<FileBytes>;
|
||||
writeText(relativePath: string, content: string, options?: WriteTextOptions): Promise<void>;
|
||||
writeBytes(relativePath: string, dataBase64: string, options?: WriteTextOptions): Promise<void>;
|
||||
createFolder(relativePath: string): Promise<void>;
|
||||
move(fromRelativePath: string, toRelativePath: string, options?: MovePathOptions): Promise<void>;
|
||||
trash(relativePath: string): Promise<TrashResult>;
|
||||
|
|
|
|||
|
|
@ -99,6 +99,26 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options: MockPlugi
|
|||
return result;
|
||||
}
|
||||
|
||||
function stringFromBase64(value: string): string {
|
||||
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
const clean = String(value || '').replace(/\s+/g, '');
|
||||
if (clean.length % 4 === 1) throw new Error('invalid-base64');
|
||||
let result = '';
|
||||
for (let i = 0; i < clean.length; i += 4) {
|
||||
const a = alphabet.indexOf(clean[i]);
|
||||
const b = alphabet.indexOf(clean[i + 1]);
|
||||
const c = clean[i + 2] === '=' ? -1 : alphabet.indexOf(clean[i + 2]);
|
||||
const d = clean[i + 3] === '=' ? -1 : alphabet.indexOf(clean[i + 3]);
|
||||
if (a < 0 || b < 0 || (clean[i + 2] !== '=' && c < 0) || (clean[i + 3] !== '=' && d < 0)) {
|
||||
throw new Error('invalid-base64');
|
||||
}
|
||||
result += String.fromCharCode((a << 2) | (b >> 4));
|
||||
if (c >= 0) result += String.fromCharCode(((b & 15) << 4) | (c >> 2));
|
||||
if (d >= 0) result += String.fromCharCode(((c & 3) << 6) | d);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function entry(path: string, node: { type: 'file' | 'folder'; content?: string; modifiedAt: string }) {
|
||||
const name = baseName(path);
|
||||
const dot = name.lastIndexOf('.');
|
||||
|
|
@ -249,6 +269,16 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options: MockPlugi
|
|||
if (!files.get(parent) || files.get(parent)?.type !== 'folder') throw new Error(`parent-not-found: ${parent}`);
|
||||
files.set(path, { type: 'file', content, modifiedAt: new Date().toISOString() });
|
||||
}),
|
||||
writeBytes: vi.fn(async (relativePath: string, dataBase64: string, options = {}) => {
|
||||
const path = normalizePath(relativePath);
|
||||
const node = files.get(path);
|
||||
if (node && node.type !== 'file') throw new Error(`not-regular-file: ${path}`);
|
||||
if (node && !options.overwrite) throw new Error(`conflict: ${path}`);
|
||||
if (!node && !options.createIfMissing) throw new Error(`not-found: ${path}`);
|
||||
const parent = parentPath(path);
|
||||
if (!files.get(parent) || files.get(parent)?.type !== 'folder') throw new Error(`parent-not-found: ${parent}`);
|
||||
files.set(path, { type: 'file', content: stringFromBase64(dataBase64), modifiedAt: new Date().toISOString() });
|
||||
}),
|
||||
createFolder: vi.fn(async (relativePath: string) => {
|
||||
const path = normalizePath(relativePath);
|
||||
if (files.has(path)) throw new Error(`conflict: ${path}`);
|
||||
|
|
|
|||
Loading…
Reference in New Issue