Files
FootPrint/sync-server/src/attachments.mjs
T
root 42ca57abfd feat: 足迹 Footprints 个人留痕系统
桌面应用(Tauri+React+SQLite):时间线/记录/学年学科成绩梯度/统计/类型与字段模板/附件内容寻址/详情视图/PWA

同步服务端(sync-server):多用户+邀请码/LWW推拉/seq游标/附件去重/Docker部署

多端同步地基:uuid v7/HLC混合逻辑时钟/软删除墓碑/sync_oplog,Node与Rust双端一致并通过自动化测试
2026-10-16 13:53:15 +08:00

38 lines
1.7 KiB
JavaScript

// 附件:SHA-256 内容寻址去重存储
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { getDb, BLOB_DIR } from './db.mjs';
export function upload(user_id, { sha256, size, mime, file_name, data_b64 }) {
const d = getDb();
if (!/^[a-f0-9]{64}$/.test(sha256 || '')) throw new Error('sha256 格式不合法');
const buf = Buffer.from(data_b64 || '', 'base64');
const actual = crypto.createHash('sha256').update(buf).digest('hex');
if (actual !== sha256) throw new Error('内容哈希与声明不符');
if (size != null && buf.length !== Number(size)) throw new Error('大小与声明不符');
if (buf.length > 100 * 1024 * 1024) throw new Error('单文件不得超过 100MB');
const dir = path.join(BLOB_DIR, user_id);
fs.mkdirSync(dir, { recursive: true });
const file = path.join(dir, sha256);
if (!fs.existsSync(file)) fs.writeFileSync(file, buf);
const existing = d.prepare('SELECT 1 FROM attachments WHERE user_id=? AND sha256=?').get(user_id, sha256);
if (!existing) {
d.prepare('INSERT INTO attachments (user_id, sha256, size, mime, file_name) VALUES (?,?,?,?,?)')
.run(user_id, sha256, buf.length, mime ?? null, (file_name || '').slice(0, 255));
return { dedup: false, size: buf.length };
}
return { dedup: true, size: buf.length };
}
export function download(user_id, sha256) {
const d = getDb();
const row = d.prepare('SELECT * FROM attachments WHERE user_id=? AND sha256=?').get(user_id, sha256);
if (!row) return null;
const file = path.join(BLOB_DIR, user_id, sha256);
if (!fs.existsSync(file)) return null;
return { buf: fs.readFileSync(file), mime: row.mime, file_name: row.file_name, size: row.size };
}