feat: server-side attachment thumbnails (480px webp at upload) + session caches - detail grids no longer download full originals
This commit is contained in:
+25
-3
@@ -125,7 +125,7 @@ function preMigrateColumns(d) {
|
||||
}
|
||||
};
|
||||
for (const t of SYNC_TABLES) addCols(t, SYNC_COLS);
|
||||
addCols('attachment', [['sha256', 'TEXT']]);
|
||||
addCols('attachment', [['sha256', 'TEXT'], ['thumb_rel_path', 'TEXT']]);
|
||||
}
|
||||
|
||||
export function migrateSync(d) {
|
||||
@@ -1011,13 +1011,25 @@ function listFootprints(d, query) {
|
||||
const abs = path.join(DATA_DIR, rel);
|
||||
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
||||
fs.writeFileSync(abs, buf);
|
||||
// 服务端缩略图(上传时由客户端 canvas 生成 480px WebP;无则 NULL,读取时回退原图)
|
||||
let thumbRel = null;
|
||||
if (p.thumb_b64) {
|
||||
const tb = Buffer.from(p.thumb_b64, 'base64');
|
||||
if (tb.length) {
|
||||
thumbRel = path.join('attachments', String(p.owner_id), crypto.randomUUID() + '_thumb.webp');
|
||||
const tAbs = path.join(DATA_DIR, thumbRel);
|
||||
fs.mkdirSync(path.dirname(tAbs), { recursive: true });
|
||||
fs.writeFileSync(tAbs, tb);
|
||||
thumbRel = thumbRel.split(path.sep).join('/');
|
||||
}
|
||||
}
|
||||
const uuid = uuidv7();
|
||||
let item = null;
|
||||
withTx(d, () => {
|
||||
const h = hlcNext(d);
|
||||
const r = d.prepare(
|
||||
"INSERT INTO attachment (owner_type, owner_id, kind, file_name, mime, size, rel_path, sha256, uuid, hlc, origin) VALUES ('footprint',?,?,?,?,?,?,?,?,?,?)"
|
||||
).run(p.owner_id, null, p.file_name, p.mime ?? null, buf.length, rel.split(path.sep).join('/'), sha, uuid, h, getDeviceId(d));
|
||||
"INSERT INTO attachment (owner_type, owner_id, kind, file_name, mime, size, rel_path, thumb_rel_path, sha256, uuid, hlc, origin) VALUES ('footprint',?,?,?,?,?,?,?,?,?,?,?)"
|
||||
).run(p.owner_id, null, p.file_name, p.mime ?? null, buf.length, rel.split(path.sep).join('/'), thumbRel, sha, uuid, h, getDeviceId(d));
|
||||
item = d.prepare('SELECT * FROM attachment WHERE id=?').get(Number(r.lastInsertRowid));
|
||||
touchOplog(d, 'attachment', uuid, 'upsert', h);
|
||||
});
|
||||
@@ -1037,6 +1049,7 @@ function listFootprints(d, query) {
|
||||
withTx(d, () => softDelete(d, 'attachment', payload.id));
|
||||
const abs = path.join(DATA_DIR, row.rel_path);
|
||||
try { fs.unlinkSync(abs); } catch {}
|
||||
if (row.thumb_rel_path) { try { fs.unlinkSync(path.join(DATA_DIR, row.thumb_rel_path)); } catch {} }
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
@@ -1048,6 +1061,15 @@ function listFootprints(d, query) {
|
||||
return { data_b64: data.toString('base64'), mime: row.mime, file_name: row.file_name };
|
||||
}
|
||||
|
||||
case 'attachment_thumb': {
|
||||
// 缩略图读取:有服务端缩略图用之(20KB 级 WebP),老附件回退原图
|
||||
const row = d.prepare('SELECT * FROM attachment WHERE id=?').get(payload.id);
|
||||
if (!row) throw new Error('附件不存在');
|
||||
const rel = row.thumb_rel_path || row.rel_path;
|
||||
const data = fs.readFileSync(path.join(DATA_DIR, rel));
|
||||
return { data_b64: data.toString('base64'), mime: row.thumb_rel_path ? 'image/webp' : row.mime, file_name: row.file_name };
|
||||
}
|
||||
|
||||
case 'view_list':
|
||||
return { items: d.prepare('SELECT * FROM saved_view WHERE deleted_at IS NULL ORDER BY id').all().map(v => ({
|
||||
...v, filter: JSON.parse(v.filter || '{}'), sort: JSON.parse(v.sort || '{}'), is_default: !!v.is_default
|
||||
|
||||
Reference in New Issue
Block a user