// 附件: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 }; }