feat: convert browser binary captures
This commit is contained in:
parent
95d16092d3
commit
5f2926545a
|
|
@ -216,6 +216,7 @@
|
||||||
fileMime: text(payload.fileMime).trim(),
|
fileMime: text(payload.fileMime).trim(),
|
||||||
fileSize: Number(payload.fileSize) || 0,
|
fileSize: Number(payload.fileSize) || 0,
|
||||||
fileText: text(payload.fileText),
|
fileText: text(payload.fileText),
|
||||||
|
fileDataBase64: text(payload.fileDataBase64).trim(),
|
||||||
source: text(payload.source).trim(),
|
source: text(payload.source).trim(),
|
||||||
browserName: text(payload.browserName).trim(),
|
browserName: text(payload.browserName).trim(),
|
||||||
workspaceRootPath: workspaceFromPayload(payload) || (scope && scope.workspaceRoot) || ''
|
workspaceRootPath: workspaceFromPayload(payload) || (scope && scope.workspaceRoot) || ''
|
||||||
|
|
@ -240,6 +241,7 @@
|
||||||
fileMime: text(item.fileMime),
|
fileMime: text(item.fileMime),
|
||||||
fileSize: Number(item.fileSize) || 0,
|
fileSize: Number(item.fileSize) || 0,
|
||||||
fileText: text(item.fileText),
|
fileText: text(item.fileText),
|
||||||
|
fileDataBase64: text(item.fileDataBase64).trim(),
|
||||||
source: text(item.source),
|
source: text(item.source),
|
||||||
browserName: text(item.browserName),
|
browserName: text(item.browserName),
|
||||||
workspaceRootPath: cleanWorkspace(item.workspaceRootPath),
|
workspaceRootPath: cleanWorkspace(item.workspaceRootPath),
|
||||||
|
|
@ -264,6 +266,7 @@
|
||||||
fileMime: item.fileMime,
|
fileMime: item.fileMime,
|
||||||
fileSize: item.fileSize,
|
fileSize: item.fileSize,
|
||||||
fileText: item.fileText,
|
fileText: item.fileText,
|
||||||
|
fileDataBase64: item.fileDataBase64,
|
||||||
source: item.source,
|
source: item.source,
|
||||||
browserName: item.browserName,
|
browserName: item.browserName,
|
||||||
workspaceRootPath: item.workspaceRootPath,
|
workspaceRootPath: item.workspaceRootPath,
|
||||||
|
|
@ -531,8 +534,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFileFromCapture(capture) {
|
function createFileFromCapture(capture) {
|
||||||
if (!capture || !capture.workspaceRootPath || capture.kind !== 'file' || !capture.fileName || !capture.fileText) return Promise.resolve();
|
if (!capture || !capture.workspaceRootPath || capture.kind !== 'file' || !capture.fileName || (!capture.fileText && !capture.fileDataBase64)) return Promise.resolve();
|
||||||
if (!api || !api.files || typeof api.files.writeText !== 'function') {
|
if (!api || !api.files || (capture.fileDataBase64 ? typeof api.files.writeBytes !== 'function' : typeof api.files.writeText !== 'function')) {
|
||||||
statusText = 'Could not create file: files API unavailable';
|
statusText = 'Could not create file: files API unavailable';
|
||||||
statusClass = 'error';
|
statusClass = 'error';
|
||||||
render();
|
render();
|
||||||
|
|
@ -543,10 +546,14 @@
|
||||||
statusText = 'Creating file...';
|
statusText = 'Creating file...';
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
render();
|
render();
|
||||||
return api.files.writeText(filePath, capture.fileText, {
|
var writeOptions = {
|
||||||
createIfMissing: true,
|
createIfMissing: true,
|
||||||
overwrite: false
|
overwrite: false
|
||||||
}).then(function () {
|
};
|
||||||
|
var writePromise = capture.fileDataBase64
|
||||||
|
? api.files.writeBytes(filePath, capture.fileDataBase64, writeOptions)
|
||||||
|
: api.files.writeText(filePath, capture.fileText, writeOptions);
|
||||||
|
return writePromise.then(function () {
|
||||||
if (api.events && typeof api.events.publish === 'function') {
|
if (api.events && typeof api.events.publish === 'function') {
|
||||||
return api.events.publish('browser.capture.converted', {
|
return api.events.publish('browser.capture.converted', {
|
||||||
captureId: capture.captureId,
|
captureId: capture.captureId,
|
||||||
|
|
@ -648,7 +655,7 @@
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
if (capture.kind === 'file' && capture.fileName && capture.fileText) {
|
if (capture.kind === 'file' && capture.fileName && (capture.fileText || capture.fileDataBase64)) {
|
||||||
actionButtons.push(el('button', {
|
actionButtons.push(el('button', {
|
||||||
className: 'browser-inbox-btn',
|
className: 'browser-inbox-btn',
|
||||||
'data-browser-inbox-action': 'create-file',
|
'data-browser-inbox-action': 'create-file',
|
||||||
|
|
|
||||||
|
|
@ -436,7 +436,9 @@
|
||||||
{ label: 'capabilities.has', ok: typeof api.capabilities.has === 'function' },
|
{ label: 'capabilities.has', ok: typeof api.capabilities.has === 'function' },
|
||||||
{ label: 'files.list', ok: typeof api.files.list === 'function' },
|
{ label: 'files.list', ok: typeof api.files.list === 'function' },
|
||||||
{ label: 'files.readText', ok: typeof api.files.readText === 'function' },
|
{ label: 'files.readText', ok: typeof api.files.readText === 'function' },
|
||||||
|
{ label: 'files.readBytes', ok: typeof api.files.readBytes === 'function' },
|
||||||
{ label: 'files.writeText', ok: typeof api.files.writeText === 'function' },
|
{ label: 'files.writeText', ok: typeof api.files.writeText === 'function' },
|
||||||
|
{ label: 'files.writeBytes', ok: typeof api.files.writeBytes === 'function' },
|
||||||
{ label: 'files.trash', ok: typeof api.files.trash === 'function' },
|
{ label: 'files.trash', ok: typeof api.files.trash === 'function' },
|
||||||
{ label: 'workbench.openResource', ok: typeof api.workbench.openResource === 'function' },
|
{ label: 'workbench.openResource', ok: typeof api.workbench.openResource === 'function' },
|
||||||
{ label: 'workbench.editResource', ok: typeof api.workbench.editResource === 'function' },
|
{ label: 'workbench.editResource', ok: typeof api.workbench.editResource === 'function' },
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,7 @@ function makeApi(initialSettings = {}) {
|
||||||
const handlers = {};
|
const handlers = {};
|
||||||
const unsubscribed = [];
|
const unsubscribed = [];
|
||||||
const fileWrites = [];
|
const fileWrites = [];
|
||||||
|
const fileByteWrites = [];
|
||||||
const publishedEvents = [];
|
const publishedEvents = [];
|
||||||
let nextWriteError = null;
|
let nextWriteError = null;
|
||||||
return {
|
return {
|
||||||
|
|
@ -142,6 +143,7 @@ function makeApi(initialSettings = {}) {
|
||||||
handlers,
|
handlers,
|
||||||
unsubscribed,
|
unsubscribed,
|
||||||
fileWrites,
|
fileWrites,
|
||||||
|
fileByteWrites,
|
||||||
publishedEvents,
|
publishedEvents,
|
||||||
failNextWrite(message) {
|
failNextWrite(message) {
|
||||||
nextWriteError = new Error(message || 'write failed');
|
nextWriteError = new Error(message || 'write failed');
|
||||||
|
|
@ -174,6 +176,14 @@ function makeApi(initialSettings = {}) {
|
||||||
}
|
}
|
||||||
fileWrites.push({ relativePath, content, options });
|
fileWrites.push({ relativePath, content, options });
|
||||||
},
|
},
|
||||||
|
writeBytes: async (relativePath, dataBase64, options = {}) => {
|
||||||
|
if (nextWriteError) {
|
||||||
|
const err = nextWriteError;
|
||||||
|
nextWriteError = null;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
fileByteWrites.push({ relativePath, dataBase64, options });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
getStoredCaptures(key = 'captures') {
|
getStoredCaptures(key = 'captures') {
|
||||||
return settings[key] || [];
|
return settings[key] || [];
|
||||||
|
|
@ -585,6 +595,41 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w
|
||||||
if (convertedFileEvent.payload.filePath !== 'Project/Files/notes.txt') throw new Error('converted file event filePath mismatch');
|
if (convertedFileEvent.payload.filePath !== 'Project/Files/notes.txt') throw new Error('converted file event filePath mismatch');
|
||||||
component.unmount && component.unmount(fileConversionView.container);
|
component.unmount && component.unmount(fileConversionView.container);
|
||||||
|
|
||||||
|
const binaryFileApi = makeApi({
|
||||||
|
'captures:workspace:Project': [{
|
||||||
|
captureId: 'convert-binary-file',
|
||||||
|
capturedAt: '2026-06-29T02:25:00.000Z',
|
||||||
|
kind: 'file',
|
||||||
|
url: 'https://example.com/files',
|
||||||
|
title: 'Example Files',
|
||||||
|
domain: 'example.com',
|
||||||
|
fileName: 'logo.png',
|
||||||
|
fileMime: 'image/png',
|
||||||
|
fileSize: 4,
|
||||||
|
fileDataBase64: 'iVBORw==',
|
||||||
|
workspaceRootPath: 'Project',
|
||||||
|
workspaceName: 'Project',
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
const binaryFileView = await mountWithApi(binaryFileApi);
|
||||||
|
const createBinaryFileButton = walk(binaryFileView.container, (node) => node.getAttribute && node.getAttribute('data-browser-inbox-action') === 'create-file');
|
||||||
|
if (!createBinaryFileButton) throw new Error('create file button for binary capture was not rendered');
|
||||||
|
createBinaryFileButton.click();
|
||||||
|
await flush();
|
||||||
|
if (binaryFileApi.fileWrites.length !== 0) throw new Error('binary file conversion used writeText');
|
||||||
|
if (binaryFileApi.fileByteWrites.length !== 1) throw new Error(`expected one byte write, got ${binaryFileApi.fileByteWrites.length}`);
|
||||||
|
const byteWrite = binaryFileApi.fileByteWrites[0];
|
||||||
|
if (byteWrite.relativePath !== 'Project/Files/logo.png') throw new Error(`byte write path mismatch: ${byteWrite.relativePath}`);
|
||||||
|
if (byteWrite.dataBase64 !== 'iVBORw==') throw new Error(`byte write data mismatch: ${byteWrite.dataBase64}`);
|
||||||
|
if (byteWrite.options.createIfMissing !== true || byteWrite.options.overwrite !== false) {
|
||||||
|
throw new Error(`byte write options mismatch: ${JSON.stringify(byteWrite.options)}`);
|
||||||
|
}
|
||||||
|
const convertedBinaryFileEvent = binaryFileApi.publishedEvents.find((event) => event.name === 'browser.capture.converted');
|
||||||
|
if (!convertedBinaryFileEvent || convertedBinaryFileEvent.payload.filePath !== 'Project/Files/logo.png') {
|
||||||
|
throw new Error('binary file conversion event mismatch');
|
||||||
|
}
|
||||||
|
component.unmount && component.unmount(binaryFileView.container);
|
||||||
|
|
||||||
const failedFileApi = makeApi({
|
const failedFileApi = makeApi({
|
||||||
'captures:workspace:Project': [{
|
'captures:workspace:Project': [{
|
||||||
captureId: 'convert-file-conflict',
|
captureId: 'convert-file-conflict',
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,15 @@ const api = {
|
||||||
writeText: async (relativePath, content) => {
|
writeText: async (relativePath, content) => {
|
||||||
api.files._entries.set(relativePath, { type: 'file', content });
|
api.files._entries.set(relativePath, { type: 'file', content });
|
||||||
},
|
},
|
||||||
|
readBytes: async (relativePath) => {
|
||||||
|
const entry = api.files._entries.get(relativePath);
|
||||||
|
if (!entry) throw new Error(`not-found: ${relativePath}`);
|
||||||
|
const content = entry.content || '';
|
||||||
|
return { relativePath, size: content.length, mimeHint: '', dataBase64: Buffer.from(content, 'binary').toString('base64') };
|
||||||
|
},
|
||||||
|
writeBytes: async (relativePath, dataBase64) => {
|
||||||
|
api.files._entries.set(relativePath, { type: 'file', content: Buffer.from(dataBase64, 'base64').toString('binary') });
|
||||||
|
},
|
||||||
readText: async (relativePath) => {
|
readText: async (relativePath) => {
|
||||||
if (String(relativePath).split('/')[0].toLowerCase() === '.verstak') {
|
if (String(relativePath).split('/')[0].toLowerCase() === '.verstak') {
|
||||||
throw new Error('reserved-path: .verstak is internal');
|
throw new Error('reserved-path: .verstak is internal');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue