feat: expose file byte read API

This commit is contained in:
2026-06-29 03:16:16 +08:00
parent 64168054ac
commit ef959d6018
13 changed files with 92 additions and 9 deletions
+7
View File
@@ -22,6 +22,7 @@ describe('VerstakPluginAPI contract', () => {
expect(typeof api.files.list).toBe('function');
expect(typeof api.files.metadata).toBe('function');
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.createFolder).toBe('function');
expect(typeof api.files.move).toBe('function');
@@ -250,6 +251,12 @@ describe('VerstakPluginAPI contract', () => {
await api.files.createFolder('PlatformTest');
await api.files.writeText('PlatformTest/one.txt', 'hello', { 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',
size: 5,
mimeHint: 'text/plain; charset=utf-8',
dataBase64: 'aGVsbG8=',
});
await expect(api.files.list('PlatformTest')).resolves.toEqual([
expect.objectContaining({ relativePath: 'PlatformTest/one.txt', type: 'file' }),
]);
+2
View File
@@ -7,6 +7,7 @@
import type {
CapabilityEntry,
FileBytes,
FileEntry,
FileMetadata,
MovePathOptions,
@@ -118,6 +119,7 @@ export interface VerstakPluginAPI {
list(relativeDir?: string): Promise<FileEntry[]>;
metadata(relativePath: string): Promise<FileMetadata>;
readText(relativePath: string): Promise<string>;
readBytes(relativePath: string): Promise<FileBytes>;
writeText(relativePath: string, content: string, options?: WriteTextOptions): Promise<void>;
createFolder(relativePath: string): Promise<void>;
move(fromRelativePath: string, toRelativePath: string, options?: MovePathOptions): Promise<void>;
+29
View File
@@ -82,6 +82,22 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options: MockPlugi
return idx === -1 ? path : path.slice(idx + 1);
}
function base64FromString(value: string): string {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
let result = '';
let i = 0;
while (i < value.length) {
const a = value.charCodeAt(i++) & 0xff;
const b = i < value.length ? value.charCodeAt(i++) & 0xff : NaN;
const c = i < value.length ? value.charCodeAt(i++) & 0xff : NaN;
result += alphabet[a >> 2];
result += alphabet[((a & 3) << 4) | (Number.isNaN(b) ? 0 : b >> 4)];
result += Number.isNaN(b) ? '=' : alphabet[((b & 15) << 2) | (Number.isNaN(c) ? 0 : c >> 6)];
result += Number.isNaN(c) ? '=' : alphabet[c & 63];
}
return result;
}
function entry(path: string, node: { type: 'file' | 'folder'; content?: string; modifiedAt: string }) {
const name = baseName(path);
const dot = name.lastIndexOf('.');
@@ -190,6 +206,19 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options: MockPlugi
if (node.type !== 'file') throw new Error(`not-regular-file: ${path}`);
return node.content || '';
}),
readBytes: vi.fn(async (relativePath: string) => {
const path = normalizePath(relativePath);
const node = files.get(path);
if (!node) throw new Error(`not-found: ${path}`);
if (node.type !== 'file') throw new Error(`not-regular-file: ${path}`);
const content = node.content || '';
return {
relativePath: path,
size: content.length,
mimeHint: path.toLowerCase().endsWith('.txt') ? 'text/plain; charset=utf-8' : '',
dataBase64: base64FromString(content),
};
}),
writeText: vi.fn(async (relativePath: string, content: string, options = {}) => {
const path = normalizePath(relativePath);
const node = files.get(path);
+7
View File
@@ -122,6 +122,13 @@ export interface FileMetadata {
canWrite: boolean;
}
export interface FileBytes {
relativePath: string;
size: number;
mimeHint: string;
dataBase64: string;
}
export interface WriteTextOptions {
/** Create the file when it is missing. Parent folder must already exist. */
createIfMissing?: boolean;