Files
xianyan/server/cloud_cache_routes.js
Developer 283950ea07 chore: 批量代码优化与功能迭代更新
本次提交包含大量代码优化、功能新增与服务端配置更新:
1. 修复分析报告统计数据,调整CMake策略设置
2. 优化APP权限配置、编辑器与聊天界面组件
3. 更新依赖库版本与pubspec配置
4. 新增文件传输服务端、信令服务器相关配置与脚本
5. 完善用户注销功能与数据库迁移脚本
6. 优化多处动画效果、代码风格与日志输出
7. 新增多种调试与部署脚本,修复已知BUG
2026-05-12 06:28:04 +08:00

473 lines
14 KiB
JavaScript

// ============================================================
// 闲言APP — 云端暂存API路由
// 创建时间: 2026-05-12
// 更新时间: 2026-05-12
// 作用: 云端暂存文件上传/下载/列表/删除/定时清理
// 上次更新: v11.3.0 初始版本 — Express+multer+node-cron
// ============================================================
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const router = express.Router();
// ============================================================
// 配置
// ============================================================
const DATA_DIR = path.join(__dirname, 'data', 'cloud_cache');
const META_FILE = path.join(DATA_DIR, 'meta.json');
const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB
const DEFAULT_EXPIRE_HOURS = 24;
const CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 1小时
// ============================================================
// 初始化
// ============================================================
function ensureDataDir() {
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
}
}
function loadMeta() {
ensureDataDir();
try {
if (fs.existsSync(META_FILE)) {
return JSON.parse(fs.readFileSync(META_FILE, 'utf8'));
}
} catch (e) {
console.error('CloudCache: Failed to load meta:', e.message);
}
return {};
}
function saveMeta(meta) {
ensureDataDir();
try {
fs.writeFileSync(META_FILE, JSON.stringify(meta, null, 2));
} catch (e) {
console.error('CloudCache: Failed to save meta:', e.message);
}
}
// ============================================================
// Multer配置
// ============================================================
const storage = multer.diskStorage({
destination: function (req, file, cb) {
ensureDataDir();
cb(null, DATA_DIR);
},
filename: function (req, file, cb) {
const cacheId = req.body.cacheId || crypto.randomUUID();
const ext = path.extname(file.originalname) || '.enc';
cb(null, cacheId + ext);
}
});
const upload = multer({
storage: storage,
limits: { fileSize: MAX_FILE_SIZE }
});
// ============================================================
// 中间件: 请求日志
// ============================================================
router.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`CloudCache: ${req.method} ${req.path} ${res.statusCode} ${duration}ms`);
});
next();
});
// ============================================================
// POST /upload — 上传暂存文件
// ============================================================
router.post('/upload', upload.single('file'), (req, res) => {
try {
if (!req.file) {
return res.json({ code: 0, msg: 'No file provided', data: null });
}
const fromId = req.body.fromId || '';
const toId = req.body.toId || '';
const encryptKeyHash = req.body.encryptKeyHash || '';
const expireHours = parseInt(req.body.expireHours) || DEFAULT_EXPIRE_HOURS;
const fileName = req.body.fileName || req.file.originalname;
const fileSize = parseInt(req.body.fileSize) || req.file.size;
const mimeType = req.body.mimeType || 'application/octet-stream';
if (!fromId || !toId) {
fs.unlinkSync(req.file.path);
return res.json({ code: 0, msg: 'fromId and toId required', data: null });
}
const cacheId = path.basename(req.file.filename, path.extname(req.file.filename));
const now = Date.now();
const expiresAt = now + expireHours * 3600 * 1000;
const meta = loadMeta();
meta[cacheId] = {
id: cacheId,
fileName: fileName,
fileSize: fileSize,
mimeType: mimeType,
fromId: fromId,
toId: toId,
encryptKeyHash: encryptKeyHash,
storedFile: req.file.filename,
uploadedAt: now,
expiresAt: expiresAt,
downloadedBy: [],
status: 'active'
};
saveMeta(meta);
console.log(`CloudCache: Uploaded cacheId=${cacheId} fileName=${fileName} fromId=${fromId} toId=${toId} expiresAt=${expiresAt}`);
res.json({
code: 1,
msg: 'ok',
data: {
cacheId: cacheId,
expiresAt: expiresAt,
uploadedAt: now
}
});
} catch (e) {
console.error('CloudCache: Upload error:', e.message);
res.json({ code: 0, msg: 'Upload failed: ' + e.message, data: null });
}
});
// ============================================================
// GET /download — 下载暂存文件
// ============================================================
router.get('/download', (req, res) => {
try {
const cacheId = req.query.cacheId;
const deviceId = req.query.deviceId || req.headers['x-device-id'] || '';
if (!cacheId) {
return res.json({ code: 0, msg: 'cacheId required', data: null });
}
const meta = loadMeta();
const record = meta[cacheId];
if (!record || record.status !== 'active') {
return res.json({ code: 0, msg: 'Cache not found or expired', data: null });
}
if (Date.now() > record.expiresAt) {
_deleteCache(cacheId, meta);
return res.json({ code: 0, msg: 'Cache expired', data: null });
}
const filePath = path.join(DATA_DIR, record.storedFile);
if (!fs.existsSync(filePath)) {
delete meta[cacheId];
saveMeta(meta);
return res.json({ code: 0, msg: 'File not found on disk', data: null });
}
if (deviceId && !record.downloadedBy.includes(deviceId)) {
record.downloadedBy.push(deviceId);
saveMeta(meta);
}
const downloadName = record.fileName || cacheId + '.enc';
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(downloadName)}"`);
res.setHeader('X-Cache-Id', cacheId);
res.setHeader('X-From-Id', record.fromId);
const fileStream = fs.createReadStream(filePath);
fileStream.pipe(res);
console.log(`CloudCache: Downloaded cacheId=${cacheId} deviceId=${deviceId}`);
} catch (e) {
console.error('CloudCache: Download error:', e.message);
res.json({ code: 0, msg: 'Download failed: ' + e.message, data: null });
}
});
// ============================================================
// GET /list — 查询暂存列表
// ============================================================
router.get('/list', (req, res) => {
try {
const userId = req.query.userId || req.headers['x-device-id'] || '';
if (!userId) {
return res.json({ code: 0, msg: 'userId required', data: null });
}
const meta = loadMeta();
const now = Date.now();
const items = [];
for (const [cacheId, record] of Object.entries(meta)) {
if (record.status !== 'active') continue;
if (now > record.expiresAt) continue;
if (record.toId === userId || record.fromId === userId) {
items.push({
cacheId: record.id,
fileName: record.fileName,
fileSize: record.fileSize,
mimeType: record.mimeType,
fromId: record.fromId,
toId: record.toId,
encryptKeyHash: record.encryptKeyHash,
uploadedAt: record.uploadedAt,
expiresAt: record.expiresAt,
isDownloaded: record.downloadedBy.includes(userId),
direction: record.toId === userId ? 'incoming' : 'outgoing'
});
}
}
items.sort((a, b) => b.uploadedAt - a.uploadedAt);
res.json({
code: 1,
msg: 'ok',
data: items
});
} catch (e) {
console.error('CloudCache: List error:', e.message);
res.json({ code: 0, msg: 'List failed: ' + e.message, data: null });
}
});
// ============================================================
// DELETE /delete — 删除暂存
// ============================================================
router.delete('/delete', (req, res) => {
try {
const { cacheId, deviceId } = req.body;
if (!cacheId) {
return res.json({ code: 0, msg: 'cacheId required', data: null });
}
const meta = loadMeta();
const record = meta[cacheId];
if (!record) {
return res.json({ code: 0, msg: 'Cache not found', data: null });
}
if (deviceId && record.fromId !== deviceId && record.toId !== deviceId) {
return res.json({ code: 0, msg: 'Not authorized', data: null });
}
_deleteCache(cacheId, meta);
res.json({
code: 1,
msg: 'ok',
data: { cacheId: cacheId }
});
} catch (e) {
console.error('CloudCache: Delete error:', e.message);
res.json({ code: 0, msg: 'Delete failed: ' + e.message, data: null });
}
});
// ============================================================
// POST /notify — 通知接收方有暂存文件
// ============================================================
router.post('/notify', (req, res) => {
try {
const { cacheId, toId, fromId, fileName } = req.body;
if (!cacheId || !toId) {
return res.json({ code: 0, msg: 'cacheId and toId required', data: null });
}
if (typeof notifyCallback === 'function') {
notifyCallback({
type: 'cloud-cache-notify',
from: fromId || 'server',
to: toId,
payload: {
cacheId: cacheId,
fileName: fileName || '',
fromId: fromId || ''
}
});
}
res.json({
code: 1,
msg: 'ok',
data: { notified: true, toId: toId }
});
} catch (e) {
console.error('CloudCache: Notify error:', e.message);
res.json({ code: 0, msg: 'Notify failed: ' + e.message, data: null });
}
});
// ============================================================
// GET /info — 查询单个暂存信息
// ============================================================
router.get('/info', (req, res) => {
try {
const cacheId = req.query.cacheId;
if (!cacheId) {
return res.json({ code: 0, msg: 'cacheId required', data: null });
}
const meta = loadMeta();
const record = meta[cacheId];
if (!record || record.status !== 'active') {
return res.json({ code: 0, msg: 'Cache not found', data: null });
}
res.json({
code: 1,
msg: 'ok',
data: {
cacheId: record.id,
fileName: record.fileName,
fileSize: record.fileSize,
mimeType: record.mimeType,
fromId: record.fromId,
toId: record.toId,
uploadedAt: record.uploadedAt,
expiresAt: record.expiresAt,
downloadedBy: record.downloadedBy,
isExpired: Date.now() > record.expiresAt
}
});
} catch (e) {
console.error('CloudCache: Info error:', e.message);
res.json({ code: 0, msg: 'Info failed: ' + e.message, data: null });
}
});
// ============================================================
// 内部方法
// ============================================================
function _deleteCache(cacheId, meta) {
const record = meta[cacheId];
if (record && record.storedFile) {
const filePath = path.join(DATA_DIR, record.storedFile);
try {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
} catch (e) {
console.error('CloudCache: Failed to delete file:', e.message);
}
}
delete meta[cacheId];
saveMeta(meta);
console.log(`CloudCache: Deleted cacheId=${cacheId}`);
}
function _cleanupExpired() {
const meta = loadMeta();
const now = Date.now();
let cleaned = 0;
for (const [cacheId, record] of Object.entries(meta)) {
if (now > record.expiresAt || record.status === 'deleted') {
_deleteCache(cacheId, meta);
cleaned++;
}
}
if (cleaned > 0) {
console.log(`CloudCache: Cleaned ${cleaned} expired caches`);
}
return cleaned;
}
// ============================================================
// 定时清理
// ============================================================
let _cleanupTimer = null;
function startCleanupSchedule() {
if (_cleanupTimer) return;
_cleanupTimer = setInterval(_cleanupExpired, CLEANUP_INTERVAL_MS);
console.log('CloudCache: Cleanup schedule started (every 1 hour)');
}
function stopCleanupSchedule() {
if (_cleanupTimer) {
clearInterval(_cleanupTimer);
_cleanupTimer = null;
console.log('CloudCache: Cleanup schedule stopped');
}
}
// ============================================================
// 通知回调(由外部设置)
// ============================================================
let notifyCallback = null;
function setNotifyCallback(callback) {
notifyCallback = callback;
}
// ============================================================
// 统计
// ============================================================
function getStats() {
const meta = loadMeta();
const now = Date.now();
let total = 0, active = 0, expired = 0, totalSize = 0;
for (const [cacheId, record] of Object.entries(meta)) {
total++;
if (now > record.expiresAt) {
expired++;
} else if (record.status === 'active') {
active++;
totalSize += record.fileSize || 0;
}
}
return { total, active, expired, totalSize };
}
// ============================================================
// 导出
// ============================================================
module.exports = {
router,
startCleanupSchedule,
stopCleanupSchedule,
setNotifyCallback,
getStats,
_cleanupExpired
};