feat(electron): main process, vault, search index, links, import, capture
Vault on plain .md files with FTS5 index, link rewriting, canvas files, history/trash/backup, link previews, image fetch, tasks and unlinked mentions, Notion/Obsidian importer, quick-capture window and shortcut. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0b5f8bfa8b
commit
313c41f867
16 files changed
+2706
-15
No files matched your search
@@ -0,0 +1,79 @@
|
||||
'use strict';
|
||||
// Download a picture from a web address (for a page's cover). Runs in the main process, and has the same rule as
|
||||
// link previews: only public http(s) addresses, every redirect checked again (see linkpreview.cjs).
|
||||
const path = require('node:path');
|
||||
const { allowed, hooks } = require('./linkpreview.cjs');
|
||||
|
||||
const MAX_BYTES = 50 * 1024 * 1024;
|
||||
const TIMEOUT = 25000;
|
||||
const MAX_REDIRECTS = 5;
|
||||
const EXT = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/jpg': 'jpg', 'image/gif': 'gif', 'image/webp': 'webp', 'image/avif': 'avif', 'image/svg+xml': 'svg', 'image/bmp': 'bmp' };
|
||||
const KNOWN = /\.(png|jpe?g|gif|webp|avif|svg|bmp)$/i;
|
||||
|
||||
// A file name for it: the address's own (when it looks like one), else "cover" with the right extension.
|
||||
function nameFor(u, type) {
|
||||
let base = '';
|
||||
try { base = decodeURIComponent(path.posix.basename(u.pathname)); } catch { base = path.posix.basename(u.pathname); }
|
||||
base = base.replace(/[\\/:*?"<>|\u0000-\u001f]/g, '-').trim().slice(0, 80);
|
||||
if (KNOWN.test(base)) return base;
|
||||
const ext = EXT[type] || 'jpg';
|
||||
return `${base && !base.startsWith('.') ? base.replace(/\.[a-z0-9]{1,5}$/i, '') : 'cover'}.${ext}`;
|
||||
}
|
||||
|
||||
async function readLimited(res, max) {
|
||||
const chunks = [];
|
||||
let size = 0;
|
||||
const reader = res.body.getReader();
|
||||
try {
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
size += value.length;
|
||||
if (size > max) throw new Error('That image is too big (50 MB at most).');
|
||||
chunks.push(Buffer.from(value));
|
||||
}
|
||||
} finally {
|
||||
reader.cancel().catch(() => {});
|
||||
}
|
||||
return Buffer.concat(chunks);
|
||||
}
|
||||
|
||||
// -> { bytes: Buffer, name, type }. Throws an Error whose message is meant for the person.
|
||||
async function fetchImage(url, { fetch: doFetch, lookup } = {}) {
|
||||
const get = doFetch || hooks.fetch || globalThis.fetch;
|
||||
const look = lookup || hooks.lookup;
|
||||
const signal = AbortSignal.timeout(TIMEOUT);
|
||||
let u = await allowed(String(url || '').trim(), look);
|
||||
if (!u) throw new Error('That is not a public web address (http or https).');
|
||||
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
||||
let res;
|
||||
try {
|
||||
res = await get(u.href, {
|
||||
redirect: 'manual',
|
||||
signal,
|
||||
headers: { 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36', accept: 'image/*,*/*;q=0.1' }
|
||||
});
|
||||
} catch {
|
||||
throw new Error('Could not reach that address.');
|
||||
}
|
||||
if (res.status >= 300 && res.status < 400) {
|
||||
const loc = res.headers.get('location');
|
||||
const next = loc ? await allowed(new URL(loc, u).href, look) : null;
|
||||
if (!next) throw new Error('That address redirects somewhere that is not allowed.');
|
||||
u = next;
|
||||
continue;
|
||||
}
|
||||
if (!res.ok) throw new Error(`The server answered ${res.status}.`);
|
||||
const type = String(res.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
|
||||
if (!type.startsWith('image/')) throw new Error('That address is not a picture.');
|
||||
const len = Number(res.headers.get('content-length'));
|
||||
if (len > MAX_BYTES) throw new Error('That image is too big (50 MB at most).');
|
||||
if (!res.body) throw new Error('That address sent nothing.');
|
||||
const bytes = await readLimited(res, MAX_BYTES);
|
||||
if (!bytes.length) throw new Error('That address sent nothing.');
|
||||
return { bytes, name: nameFor(u, type), type };
|
||||
}
|
||||
throw new Error('Too many redirects.');
|
||||
}
|
||||
|
||||
module.exports = { fetchImage, nameFor };
|
||||
Reference in new issue
Block a user