chore: 批量代码优化与功能迭代更新
本次提交包含大量代码优化、功能新增与服务端配置更新: 1. 修复分析报告统计数据,调整CMake策略设置 2. 优化APP权限配置、编辑器与聊天界面组件 3. 更新依赖库版本与pubspec配置 4. 新增文件传输服务端、信令服务器相关配置与脚本 5. 完善用户注销功能与数据库迁移脚本 6. 优化多处动画效果、代码风格与日志输出 7. 新增多种调试与部署脚本,修复已知BUG
This commit is contained in:
472
server/cloud_cache_routes.js
Normal file
472
server/cloud_cache_routes.js
Normal file
@@ -0,0 +1,472 @@
|
||||
// ============================================================
|
||||
// 闲言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
|
||||
};
|
||||
Reference in New Issue
Block a user