feat: capture binary files for browser inbox
This commit is contained in:
parent
8970333d13
commit
7b4db61a9c
|
|
@ -1,7 +1,7 @@
|
|||
# verstak-browser-extension
|
||||
|
||||
Verstak Browser Extension captures pages, selected text, links, and selected
|
||||
text files and sends them to a local Verstak browser inbox receiver.
|
||||
files and sends them to a local Verstak browser inbox receiver.
|
||||
|
||||
The extension does not know Notes, Files, Activity, or Journal internals. It
|
||||
only sends capture events through the public local receiver protocol. If the
|
||||
|
|
@ -84,7 +84,8 @@ Supported `kind` values:
|
|||
- `page`
|
||||
- `selection`, with `selection.text`
|
||||
- `link`, with `link.url` and optional `link.text`
|
||||
- `file`, with `file.name` and `file.text` for selected text files up to 2 MB
|
||||
- `file`, with `file.name` and either `file.dataBase64` for selected files up to
|
||||
8 MB or `file.text` for text-compatible captures
|
||||
|
||||
Expected success response:
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"manifest_version": 3,
|
||||
"name": "Verstak Bridge",
|
||||
"version": "2.0.1",
|
||||
"description": "Send pages, selections, links, and text files to the local Verstak browser inbox.",
|
||||
"description": "Send pages, selections, links, and files to the local Verstak browser inbox.",
|
||||
"author": "Verstak",
|
||||
"homepage_url": "https://git.mirv.top/verstak/verstak-browser-extension",
|
||||
"permissions": ["contextMenus", "storage", "tabs"],
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"manifest_version": 2,
|
||||
"name": "Verstak Bridge",
|
||||
"version": "2.0.1",
|
||||
"description": "Send pages, selections, links, and text files to the local Verstak browser inbox.",
|
||||
"description": "Send pages, selections, links, and files to the local Verstak browser inbox.",
|
||||
"author": "Verstak",
|
||||
"homepage_url": "https://git.mirv.top/verstak/verstak-browser-extension",
|
||||
"browser_specific_settings": {
|
||||
|
|
|
|||
|
|
@ -32,16 +32,18 @@ const file = protocol.buildCapture({
|
|||
fileName: 'notes.txt',
|
||||
fileMime: 'text/plain',
|
||||
fileSize: 12,
|
||||
fileText: 'hello file'
|
||||
fileText: 'hello file',
|
||||
fileDataBase64: 'aGVsbG8gZmlsZQ=='
|
||||
});
|
||||
assert.equal(file.file.name, 'notes.txt');
|
||||
assert.equal(file.file.mime, 'text/plain');
|
||||
assert.equal(file.file.size, 12);
|
||||
assert.equal(file.file.text, 'hello file');
|
||||
assert.equal(file.file.dataBase64, 'aGVsbG8gZmlsZQ==');
|
||||
assert.equal(protocol.validateCapture(file), true);
|
||||
|
||||
assert.throws(() => protocol.validateCapture({ schemaVersion: 1, kind: 'link', captureId: 'x', capturedAt: 'now', page: { url: 'https://example.com' } }), /link.url/);
|
||||
assert.throws(() => protocol.validateCapture({ schemaVersion: 1, kind: 'file', captureId: 'x', capturedAt: 'now', page: { url: 'https://example.com' }, file: { name: 'notes.txt' } }), /file.text/);
|
||||
assert.throws(() => protocol.validateCapture({ schemaVersion: 1, kind: 'file', captureId: 'x', capturedAt: 'now', page: { url: 'https://example.com' }, file: { name: 'notes.txt' } }), /file.text or file.dataBase64/);
|
||||
|
||||
let request;
|
||||
const fetchOk = (url, options) => {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@
|
|||
fileName: info.fileName || '',
|
||||
fileMime: info.fileMime || '',
|
||||
fileSize: info.fileSize || 0,
|
||||
fileText: info.fileText || ''
|
||||
fileText: info.fileText || '',
|
||||
fileDataBase64: info.fileDataBase64 || ''
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
</section>
|
||||
|
||||
<section class="file-picker">
|
||||
<label for="file-input">Text file</label>
|
||||
<label for="file-input">File</label>
|
||||
<input id="file-input" type="file">
|
||||
</section>
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,24 @@
|
|||
var pendingCountEl = document.getElementById('pending-count');
|
||||
var statusDotEl = document.getElementById('status-dot');
|
||||
var MAX_FILE_TEXT_LENGTH = 2 * 1024 * 1024;
|
||||
var MAX_FILE_BYTES = 8 * 1024 * 1024;
|
||||
|
||||
function arrayBufferToBase64(buffer) {
|
||||
var bytes = new Uint8Array(buffer);
|
||||
var chunkSize = 0x8000;
|
||||
var binary = '';
|
||||
for (var i = 0; i < bytes.length; i += chunkSize) {
|
||||
var chunk = bytes.subarray(i, i + chunkSize);
|
||||
binary += String.fromCharCode.apply(null, chunk);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
function readOptionalText(file) {
|
||||
if (file.size > MAX_FILE_TEXT_LENGTH) return Promise.resolve('');
|
||||
if (file.type && file.type.indexOf('text/') !== 0 && file.type !== 'application/json') return Promise.resolve('');
|
||||
return file.text().catch(function () { return ''; });
|
||||
}
|
||||
|
||||
function setStatus(text) {
|
||||
statusEl.textContent = text;
|
||||
|
|
@ -75,19 +93,21 @@
|
|||
setStatus('Choose a text file first');
|
||||
return;
|
||||
}
|
||||
if (file.size > MAX_FILE_TEXT_LENGTH) {
|
||||
setStatus('File is too large for text capture');
|
||||
if (file.size > MAX_FILE_BYTES) {
|
||||
setStatus('File is too large for browser capture');
|
||||
return;
|
||||
}
|
||||
setStatus('Reading file...');
|
||||
file.text().then(function (content) {
|
||||
Promise.all([file.arrayBuffer(), readOptionalText(file)]).then(function (results) {
|
||||
var content = results[1] || '';
|
||||
send({
|
||||
type: 'verstak.capture',
|
||||
kind: 'file',
|
||||
fileName: file.name,
|
||||
fileMime: file.type || 'text/plain',
|
||||
fileMime: file.type || '',
|
||||
fileSize: file.size,
|
||||
fileText: content
|
||||
fileText: content,
|
||||
fileDataBase64: arrayBufferToBase64(results[0])
|
||||
});
|
||||
}).catch(function (err) {
|
||||
setStatus(err && err.message ? err.message : String(err));
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
var CAPTURE_SCHEMA_VERSION = 1;
|
||||
var DEFAULT_RECEIVER_URL = 'http://127.0.0.1:47731/api/browser-inbox/v1/captures';
|
||||
var MAX_FILE_TEXT_LENGTH = 2 * 1024 * 1024;
|
||||
var MAX_FILE_BYTES = 8 * 1024 * 1024;
|
||||
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
|
|
@ -73,7 +74,8 @@
|
|||
name: cleanString(input.fileName || '', 255),
|
||||
mime: cleanString(input.fileMime || '', 128),
|
||||
size: Number.isFinite(fileSize) && fileSize >= 0 ? fileSize : fileText.length,
|
||||
text: fileText
|
||||
text: fileText,
|
||||
dataBase64: cleanString(input.fileDataBase64 || '', Math.ceil(MAX_FILE_BYTES * 4 / 3) + 4)
|
||||
};
|
||||
}
|
||||
if (input.context) payload.context = input.context;
|
||||
|
|
@ -90,7 +92,8 @@
|
|||
if (payload.kind === 'selection' && (!payload.selection || !payload.selection.text)) throw new Error('selection.text is required');
|
||||
if (payload.kind === 'link' && (!payload.link || !payload.link.url)) throw new Error('link.url is required');
|
||||
if (payload.kind === 'file' && (!payload.file || !payload.file.name)) throw new Error('file.name is required');
|
||||
if (payload.kind === 'file' && (!payload.file || !payload.file.text)) throw new Error('file.text is required');
|
||||
if (payload.kind === 'file' && Number(payload.file.size || 0) > MAX_FILE_BYTES) throw new Error('file.size exceeds limit');
|
||||
if (payload.kind === 'file' && (!payload.file || (!payload.file.text && !payload.file.dataBase64))) throw new Error('file.text or file.dataBase64 is required');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -98,6 +101,7 @@
|
|||
CAPTURE_SCHEMA_VERSION: CAPTURE_SCHEMA_VERSION,
|
||||
DEFAULT_RECEIVER_URL: DEFAULT_RECEIVER_URL,
|
||||
MAX_FILE_TEXT_LENGTH: MAX_FILE_TEXT_LENGTH,
|
||||
MAX_FILE_BYTES: MAX_FILE_BYTES,
|
||||
buildCapture: buildCapture,
|
||||
validateCapture: validateCapture
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue