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.read", "description": "Read vault files and metadata", "dangerous": false },
|
||||||
{ "name": "vault.write", "description": "Write vault files and metadata", "dangerous": true },
|
{ "name": "vault.write", "description": "Write vault files and metadata", "dangerous": true },
|
||||||
{ "name": "vault.watch", "description": "Watch vault file changes", "dangerous": false },
|
{ "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.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 text files, and move paths through the vault Files API", "dangerous": true },
|
{ "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.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": "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 },
|
{ "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.readText).toBe('function');
|
||||||
expect(typeof api.files.readBytes).toBe('function');
|
expect(typeof api.files.readBytes).toBe('function');
|
||||||
expect(typeof api.files.writeText).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.createFolder).toBe('function');
|
||||||
expect(typeof api.files.move).toBe('function');
|
expect(typeof api.files.move).toBe('function');
|
||||||
expect(typeof api.files.trash).toBe('function');
|
expect(typeof api.files.trash).toBe('function');
|
||||||
|
|
@ -286,6 +287,7 @@ describe('VerstakPluginAPI contract', () => {
|
||||||
|
|
||||||
await api.files.createFolder('PlatformTest');
|
await api.files.createFolder('PlatformTest');
|
||||||
await api.files.writeText('PlatformTest/one.txt', 'hello', { createIfMissing: true });
|
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.readText('PlatformTest/one.txt')).resolves.toBe('hello');
|
||||||
await expect(api.files.readBytes('PlatformTest/one.txt')).resolves.toEqual({
|
await expect(api.files.readBytes('PlatformTest/one.txt')).resolves.toEqual({
|
||||||
relativePath: 'PlatformTest/one.txt',
|
relativePath: 'PlatformTest/one.txt',
|
||||||
|
|
@ -293,9 +295,16 @@ describe('VerstakPluginAPI contract', () => {
|
||||||
mimeHint: 'text/plain; charset=utf-8',
|
mimeHint: 'text/plain; charset=utf-8',
|
||||||
dataBase64: 'aGVsbG8=',
|
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/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.openExternal('PlatformTest/one.txt')).resolves.toBeUndefined();
|
||||||
await expect(api.files.showInFolder('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');
|
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 }),
|
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.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/two.txt', type: 'file' }),
|
||||||
]);
|
expect.objectContaining({ relativePath: 'PlatformTest/image.bin', type: 'file' }),
|
||||||
|
]));
|
||||||
await expect(api.files.listTrash()).resolves.toEqual([]);
|
await expect(api.files.listTrash()).resolves.toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,7 @@ export interface VerstakPluginAPI {
|
||||||
readText(relativePath: string): Promise<string>;
|
readText(relativePath: string): Promise<string>;
|
||||||
readBytes(relativePath: string): Promise<FileBytes>;
|
readBytes(relativePath: string): Promise<FileBytes>;
|
||||||
writeText(relativePath: string, content: string, options?: WriteTextOptions): Promise<void>;
|
writeText(relativePath: string, content: string, options?: WriteTextOptions): Promise<void>;
|
||||||
|
writeBytes(relativePath: string, dataBase64: string, options?: WriteTextOptions): Promise<void>;
|
||||||
createFolder(relativePath: string): Promise<void>;
|
createFolder(relativePath: string): Promise<void>;
|
||||||
move(fromRelativePath: string, toRelativePath: string, options?: MovePathOptions): Promise<void>;
|
move(fromRelativePath: string, toRelativePath: string, options?: MovePathOptions): Promise<void>;
|
||||||
trash(relativePath: string): Promise<TrashResult>;
|
trash(relativePath: string): Promise<TrashResult>;
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,26 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options: MockPlugi
|
||||||
return result;
|
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 }) {
|
function entry(path: string, node: { type: 'file' | 'folder'; content?: string; modifiedAt: string }) {
|
||||||
const name = baseName(path);
|
const name = baseName(path);
|
||||||
const dot = name.lastIndexOf('.');
|
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}`);
|
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() });
|
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) => {
|
createFolder: vi.fn(async (relativePath: string) => {
|
||||||
const path = normalizePath(relativePath);
|
const path = normalizePath(relativePath);
|
||||||
if (files.has(path)) throw new Error(`conflict: ${path}`);
|
if (files.has(path)) throw new Error(`conflict: ${path}`);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue