// 加载环境变量(必须在最开始) require('dotenv').config(); const express = require('express'); const cors = require('cors'); const cookieParser = require('cookie-parser'); const SftpClient = require('ssh2-sftp-client'); const multer = require('multer'); const path = require('path'); const fs = require('fs'); const { body, validationResult } = require('express-validator'); const archiver = require('archiver'); const { exec, execSync } = require('child_process'); const util = require('util'); const execAsync = util.promisify(exec); const { db, UserDB, ShareDB, SettingsDB, PasswordResetDB } = require('./database'); const { generateToken, authMiddleware, adminMiddleware } = require('./auth'); const app = express(); const PORT = process.env.PORT || 40001; // 配置CORS const allowedOrigins = process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(',').map(origin => origin.trim()) : ['*']; const corsOptions = { credentials: true, origin: (origin, callback) => { // 允许所有来源(仅限开发环境) if (allowedOrigins.includes('*')) { if (process.env.NODE_ENV === 'production') { console.warn('⚠️ 警告: 生产环境建议配置具体的ALLOWED_ORIGINS,而不是使用 *'); } callback(null, true); return; } // 允许来自配置列表中的域名 if (!origin || allowedOrigins.includes(origin)) { callback(null, true); } else { console.warn(`[CORS] 拒绝来自未授权来源的请求: ${origin}`); callback(new Error('CORS策略不允许来自该来源的访问')); } } }; // 中间件 app.use(cors(corsOptions)); app.use(express.json()); app.use(cookieParser()); // 请求日志 app.use((req, res, next) => { console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`); next(); }); // 文件上传配置(临时存储) const upload = multer({ dest: path.join(__dirname, 'uploads'), limits: { fileSize: 5 * 1024 * 1024 * 1024 } // 5GB限制 }); // ===== TTL缓存类 ===== // 带过期时间的缓存类 class TTLCache { constructor(defaultTTL = 3600000) { // 默认1小时 this.cache = new Map(); this.defaultTTL = defaultTTL; // 每10分钟清理一次过期缓存 this.cleanupInterval = setInterval(() => { this.cleanup(); }, 10 * 60 * 1000); } set(key, value, ttl = this.defaultTTL) { const expiresAt = Date.now() + ttl; this.cache.set(key, { value, expiresAt }); } get(key) { const item = this.cache.get(key); if (!item) { return undefined; } // 检查是否过期 if (Date.now() > item.expiresAt) { this.cache.delete(key); return undefined; } return item.value; } has(key) { const item = this.cache.get(key); if (!item) { return false; } // 检查是否过期 if (Date.now() > item.expiresAt) { this.cache.delete(key); return false; } return true; } delete(key) { return this.cache.delete(key); } // 清理过期缓存 cleanup() { const now = Date.now(); let cleaned = 0; for (const [key, item] of this.cache.entries()) { if (now > item.expiresAt) { this.cache.delete(key); cleaned++; } } if (cleaned > 0) { console.log(`[缓存清理] 已清理 ${cleaned} 个过期的分享缓存`); } } // 获取缓存大小 size() { return this.cache.size; } // 停止清理定时器 destroy() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } } } // 分享文件信息缓存(内存缓存,1小时TTL) const shareFileCache = new TTLCache(60 * 60 * 1000); // ===== 工具函数 ===== // 安全删除文件(不抛出异常) function safeDeleteFile(filePath) { try { if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); console.log(`[清理] 已删除临时文件: ${filePath}`); return true; } } catch (error) { console.error(`[清理] 删除临时文件失败: ${filePath}`, error.message); return false; } } // 清理旧的临时文件(启动时执行一次) function cleanupOldTempFiles() { const uploadsDir = path.join(__dirname, 'uploads'); if (!fs.existsSync(uploadsDir)) { return; } try { const files = fs.readdirSync(uploadsDir); const now = Date.now(); const maxAge = 24 * 60 * 60 * 1000; // 24小时 let cleaned = 0; files.forEach(file => { const filePath = path.join(uploadsDir, file); try { const stats = fs.statSync(filePath); if (now - stats.mtimeMs > maxAge) { fs.unlinkSync(filePath); cleaned++; } } catch (err) { console.error(`[清理] 检查文件失败: ${filePath}`, err.message); } }); if (cleaned > 0) { console.log(`[清理] 已清理 ${cleaned} 个超过24小时的临时文件`); } } catch (error) { console.error('[清理] 清理临时文件目录失败:', error.message); } } // SFTP连接 async function connectToSFTP(config) { const sftp = new SftpClient(); await sftp.connect({ host: config.ftp_host, port: config.ftp_port || 22, username: config.ftp_user, password: config.ftp_password }); return sftp; } // 格式化文件大小 function formatFileSize(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; } // ===== 公开API ===== // 健康检查 app.get('/api/health', (req, res) => { res.json({ success: true, message: 'Server is running' }); }); // 用户注册(简化版) app.post('/api/register', [ body('username').isLength({ min: 3, max: 20 }).withMessage('用户名长度3-20个字符'), body('email').optional({ checkFalsy: true }).isEmail().withMessage('邮箱格式不正确'), body('password').isLength({ min: 6 }).withMessage('密码至少6个字符') ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ success: false, errors: errors.array() }); } try { const { username, email, password } = req.body; // 检查用户名是否存在 if (UserDB.findByUsername(username)) { return res.status(400).json({ success: false, message: '用户名已存在' }); } // 如果提供了邮箱,检查邮箱是否存在 if (email && UserDB.findByEmail(email)) { return res.status(400).json({ success: false, message: '邮箱已被使用' }); } // 创建用户(不需要FTP配置) const userId = UserDB.create({ username, email: email || `${username}@localhost`, // 如果没提供邮箱,使用默认值 password }); res.json({ success: true, message: '注册成功', user_id: userId }); } catch (error) { console.error('注册失败:', error); res.status(500).json({ success: false, message: '注册失败: ' + error.message }); } } ); // 用户登录 app.post('/api/login', [ body('username').notEmpty().withMessage('用户名不能为空'), body('password').notEmpty().withMessage('密码不能为空') ], (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ success: false, errors: errors.array() }); } const { username, password } = req.body; try { const user = UserDB.findByUsername(username); if (!user) { return res.status(401).json({ success: false, message: '用户名或密码错误' }); } if (user.is_banned) { return res.status(403).json({ success: false, message: '账号已被封禁' }); } if (!UserDB.verifyPassword(password, user.password)) { return res.status(401).json({ success: false, message: '用户名或密码错误' }); } const token = generateToken(user); res.cookie('token', token, { httpOnly: true, secure: process.env.COOKIE_SECURE === 'true', // HTTPS环境下启用 sameSite: 'lax', // 防止CSRF攻击 maxAge: 7 * 24 * 60 * 60 * 1000 // 7天 }); res.json({ success: true, message: '登录成功', token, user: { id: user.id, username: user.username, email: user.email, is_admin: user.is_admin, has_ftp_config: user.has_ftp_config, // 存储相关字段 storage_permission: user.storage_permission || 'sftp_only', current_storage_type: user.current_storage_type || 'sftp', local_storage_quota: user.local_storage_quota || 1073741824, local_storage_used: user.local_storage_used || 0 } }); } catch (error) { console.error('登录失败:', error); res.status(500).json({ success: false, message: '登录失败: ' + error.message }); } } ); // ===== 需要认证的API ===== // 获取当前用户信息 app.get('/api/user/profile', authMiddleware, (req, res) => { // 不返回密码明文 const { ftp_password, password, ...safeUser } = req.user; res.json({ success: true, user: safeUser }); }); // 更新FTP配置 app.post('/api/user/update-ftp', authMiddleware, [ body('ftp_host').notEmpty().withMessage('FTP主机不能为空'), body('ftp_port').isInt({ min: 1, max: 65535 }).withMessage('FTP端口范围1-65535'), body('ftp_user').notEmpty().withMessage('FTP用户名不能为空') ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ success: false, errors: errors.array() }); } try { const { ftp_host, ftp_port, ftp_user, ftp_password, http_download_base_url } = req.body; // 调试日志:查看接收到的配置 console.log("[DEBUG] 收到SFTP配置:", { ftp_host, ftp_port, ftp_user, ftp_password: ftp_password ? "***" : "(empty)", http_download_base_url }); // 如果用户已配置FTP且密码为空,使用现有密码 let actualPassword = ftp_password; if (!ftp_password && req.user.has_ftp_config && req.user.ftp_password) { actualPassword = req.user.ftp_password; } else if (!ftp_password) { return res.status(400).json({ success: false, message: 'FTP密码不能为空' }); } // 验证FTP连接 try { const sftp = await connectToSFTP({ ftp_host, ftp_port, ftp_user, ftp_password: actualPassword }); await sftp.end(); } catch (error) { return res.status(400).json({ success: false, message: 'SFTP连接失败,请检查配置: ' + error.message }); } // 更新用户配置 UserDB.update(req.user.id, { ftp_host, ftp_port, ftp_user, ftp_password: actualPassword, http_download_base_url: http_download_base_url || null, has_ftp_config: 1 }); res.json({ success: true, message: 'SFTP配置已更新' }); } catch (error) { console.error('更新配置失败:', error); res.status(500).json({ success: false, message: '更新配置失败: ' + error.message }); } } ); // 修改管理员账号信息(仅管理员可修改用户名) app.post('/api/admin/update-profile', authMiddleware, adminMiddleware, [ body('username').isLength({ min: 3, max: 20 }).withMessage('用户名长度3-20个字符') ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ success: false, errors: errors.array() }); } try { const { username } = req.body; // 检查用户名是否被占用(排除自己) if (username !== req.user.username) { const existingUser = UserDB.findByUsername(username); if (existingUser && existingUser.id !== req.user.id) { return res.status(400).json({ success: false, message: '用户名已被使用' }); } // 更新用户名 UserDB.update(req.user.id, { username }); // 获取更新后的用户信息 const updatedUser = UserDB.findById(req.user.id); // 生成新的token(因为用户名变了) const newToken = generateToken(updatedUser); res.json({ success: true, message: '用户名已更新', token: newToken, user: { id: updatedUser.id, username: updatedUser.username, email: updatedUser.email, is_admin: updatedUser.is_admin } }); } else { res.json({ success: true, message: '没有需要更新的信息' }); } } catch (error) { console.error('更新账号信息失败:', error); res.status(500).json({ success: false, message: '更新失败: ' + error.message }); } } ); // 修改当前用户密码(需要验证当前密码) app.post('/api/user/change-password', authMiddleware, [ body('current_password').notEmpty().withMessage('当前密码不能为空'), body('new_password').isLength({ min: 6 }).withMessage('新密码至少6个字符') ], (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ success: false, errors: errors.array() }); } try { const { current_password, new_password } = req.body; // 获取当前用户信息 const user = UserDB.findById(req.user.id); if (!user) { return res.status(404).json({ success: false, message: '用户不存在' }); } // 验证当前密码 if (!UserDB.verifyPassword(current_password, user.password)) { return res.status(401).json({ success: false, message: '当前密码错误' }); } // 更新密码 UserDB.update(req.user.id, { password: new_password }); res.json({ success: true, message: '密码修改成功' }); } catch (error) { console.error('修改密码失败:', error); res.status(500).json({ success: false, message: '修改密码失败: ' + error.message }); } } ); // 修改当前用户名 app.post('/api/user/update-username', authMiddleware, [ body('username').isLength({ min: 3 }).withMessage('用户名至少3个字符') ], (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ success: false, errors: errors.array() }); } try { const { username } = req.body; // 检查用户名是否已存在 const existingUser = UserDB.findByUsername(username); if (existingUser && existingUser.id !== req.user.id) { return res.status(400).json({ success: false, message: '用户名已存在' }); } // 更新用户名 UserDB.update(req.user.id, { username }); res.json({ success: true, message: '用户名修改成功' }); } catch (error) { console.error('修改用户名失败:', error); res.status(500).json({ success: false, message: '修改用户名失败: ' + error.message }); } } ); // 切换存储方式 app.post('/api/user/switch-storage', authMiddleware, [ body('storage_type').isIn(['local', 'sftp']).withMessage('无效的存储类型') ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ success: false, errors: errors.array() }); } try { const { storage_type } = req.body; // 检查权限 if (req.user.storage_permission === 'local_only' && storage_type !== 'local') { return res.status(403).json({ success: false, message: '您只能使用本地存储' }); } if (req.user.storage_permission === 'sftp_only' && storage_type !== 'sftp') { return res.status(403).json({ success: false, message: '您只能使用SFTP存储' }); } // 检查SFTP配置 if (storage_type === 'sftp' && !req.user.has_ftp_config) { return res.status(400).json({ success: false, message: '请先配置SFTP服务器' }); } // 更新存储类型 UserDB.update(req.user.id, { current_storage_type: storage_type }); res.json({ success: true, message: '存储方式已切换', storage_type }); } catch (error) { console.error('切换存储失败:', error); res.status(500).json({ success: false, message: '切换存储失败: ' + error.message }); } } ); // 获取文件列表 app.get('/api/files', authMiddleware, async (req, res) => { const dirPath = req.query.path || '/'; let storage; try { // 使用统一存储接口 const { StorageInterface } = require('./storage'); const storageInterface = new StorageInterface(req.user); storage = await storageInterface.connect(); const list = await storage.list(dirPath); const httpBaseUrl = req.user.http_download_base_url || ''; const storageType = req.user.current_storage_type || 'sftp'; const formattedList = list.map(item => { // 构建完整的文件路径用于下载 let httpDownloadUrl = null; // 只有SFTP存储且配置了HTTP下载地址时才提供HTTP下载URL if (storageType === 'sftp' && httpBaseUrl && item.type !== 'd') { // 移除基础URL末尾的斜杠(如果有) const baseUrl = httpBaseUrl.replace(/\/+$/, ''); // 构建完整路径:当前目录路径 + 文件名 const fullPath = dirPath === '/' ? `/${item.name}` : `${dirPath}/${item.name}`; // 拼接最终的下载URL httpDownloadUrl = `${baseUrl}${fullPath}`; } return { name: item.name, type: item.type === 'd' ? 'directory' : 'file', size: item.size, sizeFormatted: formatFileSize(item.size), modifiedAt: new Date(item.modifyTime), isDirectory: item.type === 'd', httpDownloadUrl: httpDownloadUrl }; }); formattedList.sort((a, b) => { if (a.isDirectory && !b.isDirectory) return -1; if (!a.isDirectory && b.isDirectory) return 1; return a.name.localeCompare(b.name); }); res.json({ success: true, path: dirPath, items: formattedList, storageType: storageType, storagePermission: req.user.storage_permission || 'sftp_only' }); } catch (error) { console.error('获取文件列表失败:', error); res.status(500).json({ success: false, message: '获取文件列表失败: ' + error.message }); } finally { if (storage) await storage.end(); } }); // 重命名文件 app.post('/api/files/rename', authMiddleware, async (req, res) => { const { oldName, newName, path } = req.body; let storage; if (!oldName || !newName) { return res.status(400).json({ success: false, message: '缺少文件名参数' }); } try { // 使用统一存储接口 const { StorageInterface } = require('./storage'); const storageInterface = new StorageInterface(req.user); storage = await storageInterface.connect(); const oldPath = path === '/' ? `/${oldName}` : `${path}/${oldName}`; const newPath = path === '/' ? `/${newName}` : `${path}/${newName}`; await storage.rename(oldPath, newPath); res.json({ success: true, message: '文件重命名成功' }); } catch (error) { console.error('重命名文件失败:', error); res.status(500).json({ success: false, message: '重命名文件失败: ' + error.message }); } finally { if (storage) await storage.end(); } }); // 删除文件 app.post('/api/files/delete', authMiddleware, async (req, res) => { const { fileName, path } = req.body; let storage; if (!fileName) { return res.status(400).json({ success: false, message: '缺少文件名参数' }); } try { // 使用统一存储接口 const { StorageInterface } = require('./storage'); const storageInterface = new StorageInterface(req.user); storage = await storageInterface.connect(); const filePath = path === '/' ? `/${fileName}` : `${path}/${fileName}`; await storage.delete(filePath); res.json({ success: true, message: '文件删除成功' }); } catch (error) { console.error('删除文件失败:', error); res.status(500).json({ success: false, message: '删除文件失败: ' + error.message }); } finally { if (storage) await storage.end(); } }); // 上传文件 app.post('/api/upload', authMiddleware, upload.single('file'), async (req, res) => { if (!req.file) { return res.status(400).json({ success: false, message: '没有上传文件' }); } // 检查文件大小限制 const maxUploadSize = parseInt(SettingsDB.get('max_upload_size') || '10737418240'); if (req.file.size > maxUploadSize) { // 删除已上传的临时文件 if (fs.existsSync(req.file.path)) { safeDeleteFile(req.file.path); } return res.status(413).json({ success: false, message: '文件超过上传限制', maxSize: maxUploadSize, fileSize: req.file.size }); } const remotePath = req.body.path || '/'; // 修复中文文件名:multer将UTF-8转为了Latin1,需要转回来 const originalFilename = Buffer.from(req.file.originalname, 'latin1').toString('utf8'); const remoteFilePath = remotePath === '/' ? `/${originalFilename}` : `${remotePath}/${originalFilename}`; let storage; try { // 使用统一存储接口 const { StorageInterface } = require('./storage'); const storageInterface = new StorageInterface(req.user); storage = await storageInterface.connect(); // storage.put() 内部已经实现了临时文件+重命名逻辑 await storage.put(req.file.path, remoteFilePath); console.log(`[上传] 文件上传成功: ${remoteFilePath}`); // 删除本地临时文件 safeDeleteFile(req.file.path); res.json({ success: true, message: '文件上传成功', filename: originalFilename, path: remoteFilePath }); } catch (error) { console.error('文件上传失败:', error); // 删除临时文件 if (fs.existsSync(req.file.path)) { safeDeleteFile(req.file.path); } res.status(500).json({ success: false, message: '文件上传失败: ' + error.message }); } finally { if (storage) await storage.end(); } }); // 下载文件 app.get('/api/files/download', authMiddleware, async (req, res) => { const filePath = req.query.path; let storage; if (!filePath) { return res.status(400).json({ success: false, message: '缺少文件路径参数' }); } try { // 使用统一存储接口 const { StorageInterface } = require('./storage'); const storageInterface = new StorageInterface(req.user); storage = await storageInterface.connect(); // 获取文件名 const fileName = filePath.split('/').pop(); // 先获取文件信息(获取文件大小) const fileStats = await storage.stat(filePath); const fileSize = fileStats.size; console.log('[下载] 文件: ' + fileName + ', 大小: ' + fileSize + ' 字节'); // 设置响应头(包含文件大小,浏览器可显示下载进度) res.setHeader('Content-Type', 'application/octet-stream'); res.setHeader('Content-Length', fileSize); res.setHeader('Content-Disposition', 'attachment; filename="' + encodeURIComponent(fileName) + '"; filename*=UTF-8\'\'' + encodeURIComponent(fileName)); // 创建文件流并传输(流式下载,服务器不保存临时文件) const stream = storage.createReadStream(filePath); stream.on('error', (error) => { console.error('文件流错误:', error); if (!res.headersSent) { res.status(500).json({ success: false, message: '文件下载失败: ' + error.message }); } // 发生错误时关闭存储连接 if (storage) { storage.end().catch(err => console.error('关闭存储连接失败:', err)); } }); // 在传输完成后关闭存储连接 stream.on('close', () => { console.log('[下载] 文件传输完成,关闭存储连接'); if (storage) { storage.end().catch(err => console.error('关闭存储连接失败:', err)); } }); stream.pipe(res); } catch (error) { console.error('下载文件失败:', error); // 如果stream还未创建或发生错误,关闭storage连接 if (storage) { storage.end().catch(err => console.error('关闭存储连接失败:', err)); } if (!res.headersSent) { res.status(500).json({ success: false, message: '下载文件失败: ' + error.message }); } } }); // 生成上传工具(生成新密钥并创建配置文件) app.post('/api/upload/generate-tool', authMiddleware, async (req, res) => { try { // 生成新的API密钥(32位随机字符串) const crypto = require('crypto'); const newApiKey = crypto.randomBytes(16).toString('hex'); // 更新用户的upload_api_key UserDB.update(req.user.id, { upload_api_key: newApiKey }); // 创建配置文件内容 const config = { username: req.user.username, api_key: newApiKey, api_base_url: `${req.get('x-forwarded-proto') || req.protocol}://${req.get('host')}` }; res.json({ success: true, message: '上传工具已生成', config: config }); } catch (error) { console.error('生成上传工具失败:', error); res.status(500).json({ success: false, message: '生成上传工具失败: ' + error.message }); } }); // 下载上传工具(zip包含exe+config.json+README.txt) app.get('/api/upload/download-tool', authMiddleware, async (req, res) => { let tempZipPath = null; try { console.log(`[上传工具] 用户 ${req.user.username} 请求下载上传工具`); // 生成新的API密钥 const crypto = require('crypto'); const newApiKey = crypto.randomBytes(16).toString('hex'); // 更新用户的upload_api_key UserDB.update(req.user.id, { upload_api_key: newApiKey }); // 创建配置文件内容 const config = { username: req.user.username, api_key: newApiKey, api_base_url: `${req.get('x-forwarded-proto') || req.protocol}://${req.get('host')}` }; console.log("[上传工具配置]", JSON.stringify(config, null, 2)); // 检查exe文件是否存在 const toolDir = path.join(__dirname, '..', 'upload-tool'); const exePath = path.join(toolDir, 'dist', '玩玩云上传工具.exe'); const readmePath = path.join(toolDir, 'README.txt'); if (!fs.existsSync(exePath)) { console.error('[上传工具] exe文件不存在:', exePath); return res.status(500).json({ success: false, message: '上传工具尚未打包,请联系管理员运行 upload-tool/build.bat' }); } // 创建临时zip文件路径 const uploadsDir = path.join(__dirname, 'uploads'); if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }); } tempZipPath = path.join(uploadsDir, `tool_${req.user.username}_${Date.now()}.zip`); console.log('[上传工具] 开始创建zip包到临时文件:', tempZipPath); // 创建文件写入流 const output = fs.createWriteStream(tempZipPath); const archive = archiver('zip', { store: true // 使用STORE模式,不压缩,速度最快 }); // 等待zip文件创建完成 await new Promise((resolve, reject) => { output.on('close', () => { console.log(`[上传工具] zip创建完成,大小: ${archive.pointer()} 字节`); resolve(); }); archive.on('error', (err) => { console.error('[上传工具] archiver错误:', err); reject(err); }); // 连接archive到文件流 archive.pipe(output); // 添加exe文件 console.log('[上传工具] 添加exe文件...'); archive.file(exePath, { name: '玩玩云上传工具.exe' }); // 添加config.json console.log('[上传工具] 添加config.json...'); archive.append(JSON.stringify(config, null, 2), { name: 'config.json' }); // 添加README.txt if (fs.existsSync(readmePath)) { console.log('[上传工具] 添加README.txt...'); archive.file(readmePath, { name: 'README.txt' }); } // 完成打包 console.log('[上传工具] 执行finalize...'); archive.finalize(); }); // 获取文件大小 const stats = fs.statSync(tempZipPath); const fileSize = stats.size; console.log(`[上传工具] 准备发送文件,大小: ${fileSize} 字节`); // 设置响应头(包含Content-Length) const filename = `玩玩云上传工具_${req.user.username}.zip`; res.setHeader('Content-Type', 'application/zip'); res.setHeader('Content-Length', fileSize); res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"; filename*=UTF-8''${encodeURIComponent(filename)}`); // 创建文件读取流并发送 const fileStream = fs.createReadStream(tempZipPath); fileStream.on('end', () => { console.log(`[上传工具] 用户 ${req.user.username} 下载完成`); // 删除临时文件 if (tempZipPath && fs.existsSync(tempZipPath)) { fs.unlinkSync(tempZipPath); console.log('[上传工具] 临时文件已删除'); } }); fileStream.on('error', (err) => { console.error('[上传工具] 文件流错误:', err); // 删除临时文件 if (tempZipPath && fs.existsSync(tempZipPath)) { fs.unlinkSync(tempZipPath); } }); fileStream.pipe(res); } catch (error) { console.error('[上传工具] 异常:', error); // 删除临时文件 if (tempZipPath && fs.existsSync(tempZipPath)) { fs.unlinkSync(tempZipPath); console.log('[上传工具] 临时文件已删除(异常)'); } if (!res.headersSent) { res.status(500).json({ success: false, message: '下载失败: ' + error.message }); } } }); // 通过API密钥获取SFTP配置(供Python工具调用) app.post('/api/upload/get-config', async (req, res) => { try { const { api_key } = req.body; if (!api_key) { return res.status(400).json({ success: false, message: 'API密钥不能为空' }); } // 查找拥有此API密钥的用户 const user = db.prepare('SELECT * FROM users WHERE upload_api_key = ?').get(api_key); if (!user) { return res.status(401).json({ success: false, message: 'API密钥无效或已过期' }); } if (user.is_banned) { return res.status(403).json({ success: false, message: '账号已被封禁' }); } if (!user.has_ftp_config) { return res.status(400).json({ success: false, message: '用户未配置SFTP服务器' }); } // 返回SFTP配置 res.json({ success: true, sftp_config: { host: user.ftp_host, port: user.ftp_port, username: user.ftp_user, password: user.ftp_password } }); } catch (error) { console.error('获取SFTP配置失败:', error); res.status(500).json({ success: false, message: '获取SFTP配置失败: ' + error.message }); } }); // 创建分享链接 app.post('/api/share/create', authMiddleware, (req, res) => { try { const { share_type, file_path, file_name, password, expiry_days } = req.body; console.log("[DEBUG] 创建分享请求:", { share_type, file_path, file_name, password: password ? "***" : null, expiry_days }); if (share_type === 'file' && !file_path) { return res.status(400).json({ success: false, message: '文件路径不能为空' }); } const result = ShareDB.create(req.user.id, { share_type: share_type || 'file', file_path: file_path || '', file_name: file_name || '', password: password || null, expiry_days: expiry_days || null }); // 更新分享的存储类型 const { db } = require('./database'); db.prepare('UPDATE shares SET storage_type = ? WHERE id = ?') .run(req.user.current_storage_type || 'sftp', result.id); const shareUrl = `${req.protocol}://${req.get('host')}/s/${result.share_code}`; res.json({ success: true, message: '分享链接创建成功', share_code: result.share_code, share_url: shareUrl, share_type: result.share_type }); } catch (error) { console.error('创建分享链接失败:', error); res.status(500).json({ success: false, message: '创建分享链接失败: ' + error.message }); } }); // 获取我的分享列表 app.get('/api/share/my', authMiddleware, (req, res) => { try { const shares = ShareDB.getUserShares(req.user.id); res.json({ success: true, shares: shares.map(share => ({ ...share, share_url: `${req.protocol}://${req.get('host')}/s/${share.share_code}` })) }); } catch (error) { console.error('获取分享列表失败:', error); res.status(500).json({ success: false, message: '获取分享列表失败: ' + error.message }); } }); // 删除分享 app.delete('/api/share/:id', authMiddleware, (req, res) => { try { // 先获取分享信息以获得share_code const share = ShareDB.findById(req.params.id); if (share && share.user_id === req.user.id) { // 删除缓存 if (shareFileCache.has(share.share_code)) { shareFileCache.delete(share.share_code); console.log(`[缓存清除] 分享码: ${share.share_code}`); } // 删除数据库记录 ShareDB.delete(req.params.id, req.user.id); res.json({ success: true, message: '分享已删除' }); } else { res.status(404).json({ success: false, message: '分享不存在或无权限' }); } } catch (error) { console.error('删除分享失败:', error); res.status(500).json({ success: false, message: '删除分享失败: ' + error.message }); } }); // ===== 分享链接访问(公开) ===== // 访问分享链接 - 验证密码(支持本地存储和SFTP) app.post('/api/share/:code/verify', async (req, res) => { const { code } = req.params; const { password } = req.body; let storage; try { const share = ShareDB.findByCode(code); if (!share) { return res.status(404).json({ success: false, message: '分享不存在' }); } // 如果设置了密码,验证密码 if (share.share_password) { if (!password) { return res.status(401).json({ success: false, message: '需要密码', needPassword: true }); } if (!ShareDB.verifyPassword(password, share.share_password)) { return res.status(401).json({ success: false, message: '密码错误' }); } } // 增加查看次数 ShareDB.incrementViewCount(code); // 构建返回数据 const responseData = { success: true, share: { share_path: share.share_path, share_type: share.share_type, username: share.username, created_at: share.created_at } }; // 如果是单文件分享,查询存储获取文件信息(带缓存) if (share.share_type === 'file') { const filePath = share.share_path; const lastSlashIndex = filePath.lastIndexOf('/'); const dirPath = lastSlashIndex > 0 ? filePath.substring(0, lastSlashIndex) : '/'; const fileName = lastSlashIndex >= 0 ? filePath.substring(lastSlashIndex + 1) : filePath; // 检查缓存 if (shareFileCache.has(code)) { console.log(`[缓存命中] 分享码: ${code}`); responseData.file = shareFileCache.get(code); } else { // 缓存未命中,查询存储 const storageType = share.storage_type || 'sftp'; console.log(`[缓存未命中] 分享码: ${code},存储类型: ${storageType}`); try { // 获取分享者的用户信息 const shareOwner = UserDB.findById(share.user_id); if (!shareOwner) { throw new Error('分享者不存在'); } // 使用统一存储接口 const { StorageInterface } = require('./storage'); const userForStorage = { ...shareOwner, current_storage_type: storageType }; const storageInterface = new StorageInterface(userForStorage); storage = await storageInterface.connect(); const list = await storage.list(dirPath); const fileInfo = list.find(item => item.name === fileName); // 检查文件是否存在 if (!fileInfo) { shareFileCache.delete(code); throw new Error("分享的文件已被删除或不存在"); } if (fileInfo) { // 移除基础URL末尾的斜杠 const httpBaseUrl = share.http_download_base_url || ''; const baseUrl = httpBaseUrl ? httpBaseUrl.replace(/\/+$/, '') : ''; const normalizedFilePath = filePath.startsWith('/') ? filePath : `/${filePath}`; // SFTP存储才提供HTTP下载URL,本地存储使用API下载 const httpDownloadUrl = (storageType === 'sftp' && baseUrl) ? `${baseUrl}${normalizedFilePath}` : null; const fileData = { name: fileName, type: 'file', isDirectory: false, httpDownloadUrl: httpDownloadUrl, size: fileInfo.size, sizeFormatted: formatFileSize(fileInfo.size), modifiedAt: new Date(fileInfo.modifyTime) }; // 存入缓存 shareFileCache.set(code, fileData); console.log(`[缓存存储] 分享码: ${code},文件: ${fileName}`); responseData.file = fileData; } } catch (storageError) { console.error('获取文件信息失败:', storageError); // 如果是文件不存在的错误,重新抛出 if (storageError.message && storageError.message.includes("分享的文件已被删除或不存在")) { throw storageError; } // 存储失败时仍返回基本信息,只是没有大小 const httpBaseUrl = share.http_download_base_url || ''; const baseUrl = httpBaseUrl ? httpBaseUrl.replace(/\/+$/, '') : ''; const normalizedFilePath = filePath.startsWith('/') ? filePath : `/${filePath}`; const storageType = share.storage_type || 'sftp'; const httpDownloadUrl = (storageType === 'sftp' && baseUrl) ? `${baseUrl}${normalizedFilePath}` : null; responseData.file = { name: fileName, type: 'file', isDirectory: false, httpDownloadUrl: httpDownloadUrl, size: 0, sizeFormatted: '-' }; } } } res.json(responseData); } catch (error) { console.error('验证分享失败:', error); res.status(500).json({ success: false, message: '验证失败: ' + error.message }); } finally { if (storage) await storage.end(); } }); // 获取分享的文件列表(支持本地存储和SFTP) app.post('/api/share/:code/list', async (req, res) => { const { code } = req.params; const { password, path: subPath } = req.body; let storage; try { const share = ShareDB.findByCode(code); if (!share) { return res.status(404).json({ success: false, message: '分享不存在' }); } // 验证密码 if (share.share_password && !ShareDB.verifyPassword(password, share.share_password)) { return res.status(401).json({ success: false, message: '密码错误' }); } // 获取分享者的用户信息 const shareOwner = UserDB.findById(share.user_id); if (!shareOwner) { return res.status(404).json({ success: false, message: '分享者不存在' }); } // 使用统一存储接口,根据分享的storage_type选择存储后端 const { StorageInterface } = require('./storage'); const storageType = share.storage_type || 'sftp'; console.log(`[分享列表] 存储类型: ${storageType}, 分享路径: ${share.share_path}`); // 临时构造用户对象以使用存储接口 const userForStorage = { ...shareOwner, current_storage_type: storageType }; const storageInterface = new StorageInterface(userForStorage); storage = await storageInterface.connect(); const httpBaseUrl = share.http_download_base_url || ''; let formattedList = []; // 如果是单文件分享 if (share.share_type === 'file') { // share_path 就是文件路径 const filePath = share.share_path; // 提取父目录和文件名 const lastSlashIndex = filePath.lastIndexOf('/'); const dirPath = lastSlashIndex > 0 ? filePath.substring(0, lastSlashIndex) : '/'; const fileName = lastSlashIndex >= 0 ? filePath.substring(lastSlashIndex + 1) : filePath; // 列出父目录 const list = await storage.list(dirPath); // 只返回这个文件 const fileInfo = list.find(item => item.name === fileName); if (fileInfo) { // 移除基础URL末尾的斜杠 const baseUrl = httpBaseUrl ? httpBaseUrl.replace(/\/+$/, '') : ''; // 确保文件路径以斜杠开头 const normalizedFilePath = filePath.startsWith('/') ? filePath : `/${filePath}`; // SFTP存储才提供HTTP下载URL,本地存储使用API下载 const httpDownloadUrl = (storageType === 'sftp' && baseUrl) ? `${baseUrl}${normalizedFilePath}` : null; formattedList = [{ name: fileInfo.name, type: 'file', size: fileInfo.size, sizeFormatted: formatFileSize(fileInfo.size), modifiedAt: new Date(fileInfo.modifyTime), isDirectory: false, httpDownloadUrl: httpDownloadUrl }]; } } // 如果是目录分享(分享所有文件) else { const fullPath = subPath ? `${share.share_path}/${subPath}`.replace('//', '/') : share.share_path; const list = await storage.list(fullPath); formattedList = list.map(item => { // 构建完整的文件路径用于下载 let httpDownloadUrl = null; // SFTP存储才提供HTTP下载URL,本地存储使用API下载 if (storageType === 'sftp' && httpBaseUrl && item.type !== 'd') { // 移除基础URL末尾的斜杠 const baseUrl = httpBaseUrl.replace(/\/+$/, ''); // 确保fullPath以斜杠开头 const normalizedPath = fullPath.startsWith('/') ? fullPath : `/${fullPath}`; // 构建完整路径:当前目录路径 + 文件名 const filePath = normalizedPath === '/' ? `/${item.name}` : `${normalizedPath}/${item.name}`; // 拼接最终的下载URL httpDownloadUrl = `${baseUrl}${filePath}`; } return { name: item.name, type: item.type === 'd' ? 'directory' : 'file', size: item.size, sizeFormatted: formatFileSize(item.size), modifiedAt: new Date(item.modifyTime), isDirectory: item.type === 'd', httpDownloadUrl: httpDownloadUrl }; }); formattedList.sort((a, b) => { if (a.isDirectory && !b.isDirectory) return -1; if (!a.isDirectory && b.isDirectory) return 1; return a.name.localeCompare(b.name); }); } res.json({ success: true, path: share.share_path, items: formattedList }); } catch (error) { console.error('获取分享文件列表失败:', error); res.status(500).json({ success: false, message: '获取文件列表失败: ' + error.message }); } finally { if (storage) await storage.end(); } }); // 记录下载次数 app.post('/api/share/:code/download', (req, res) => { const { code } = req.params; try { const share = ShareDB.findByCode(code); if (!share) { return res.status(404).json({ success: false, message: '分享不存在' }); } // 增加下载次数 ShareDB.incrementDownloadCount(code); res.json({ success: true, message: '下载统计已记录' }); } catch (error) { console.error('记录下载失败:', error); res.status(500).json({ success: false, message: '记录下载失败: ' + error.message }); } }); // 分享文件下载(支持本地存储和SFTP,公开API,需要分享码和密码验证) app.get('/api/share/:code/download-file', async (req, res) => { const { code } = req.params; const { path: filePath, password } = req.query; let storage; if (!filePath) { return res.status(400).json({ success: false, message: '缺少文件路径参数' }); } try { const share = ShareDB.findByCode(code); if (!share) { return res.status(404).json({ success: false, message: '分享不存在' }); } // 验证密码(如果需要) if (share.share_password) { if (!password || !ShareDB.verifyPassword(password, share.share_password)) { return res.status(401).json({ success: false, message: '密码错误或未提供密码' }); } } // 获取分享者的用户信息 const shareOwner = UserDB.findById(share.user_id); if (!shareOwner) { return res.status(404).json({ success: false, message: '分享者不存在' }); } // 使用统一存储接口,根据分享的storage_type选择存储后端 const { StorageInterface } = require('./storage'); const storageType = share.storage_type || 'sftp'; console.log(`[分享下载] 存储类型: ${storageType}, 文件路径: ${filePath}`); // 临时构造用户对象以使用存储接口 const userForStorage = { ...shareOwner, current_storage_type: storageType }; const storageInterface = new StorageInterface(userForStorage); storage = await storageInterface.connect(); // 获取文件名 const fileName = filePath.split('/').pop(); // 获取文件信息(获取文件大小) const fileStats = await storage.stat(filePath); const fileSize = fileStats.size; console.log(`[分享下载] 文件: ${fileName}, 大小: ${fileSize} 字节`); // 增加下载次数 ShareDB.incrementDownloadCount(code); // 设置响应头(包含文件大小,浏览器可显示下载进度) res.setHeader('Content-Type', 'application/octet-stream'); res.setHeader('Content-Length', fileSize); res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(fileName)}"; filename*=UTF-8''${encodeURIComponent(fileName)}`); // 创建文件流并传输(流式下载,服务器不保存临时文件) const stream = storage.createReadStream(filePath); stream.on('error', (error) => { console.error('文件流错误:', error); if (!res.headersSent) { res.status(500).json({ success: false, message: '文件下载失败: ' + error.message }); } // 发生错误时关闭存储连接 if (storage) { storage.end().catch(err => console.error('关闭存储连接失败:', err)); } }); // 在传输完成后关闭存储连接 stream.on('close', () => { console.log('[分享下载] 文件传输完成,关闭存储连接'); if (storage) { storage.end().catch(err => console.error('关闭存储连接失败:', err)); } }); stream.pipe(res); } catch (error) { console.error('分享下载文件失败:', error); if (!res.headersSent) { res.status(500).json({ success: false, message: '下载文件失败: ' + error.message }); } // 如果发生错误,关闭存储连接 if (storage) { storage.end().catch(err => console.error('关闭存储连接失败:', err)); } } }); // ===== 管理员API ===== // 获取系统设置 app.get('/api/admin/settings', authMiddleware, adminMiddleware, (req, res) => { try { const maxUploadSize = parseInt(SettingsDB.get('max_upload_size') || '10737418240'); res.json({ success: true, settings: { max_upload_size: maxUploadSize } }); } catch (error) { console.error('获取系统设置失败:', error); res.status(500).json({ success: false, message: '获取系统设置失败: ' + error.message }); } }); // 更新系统设置 app.post('/api/admin/settings', authMiddleware, adminMiddleware, (req, res) => { try { const { max_upload_size } = req.body; if (max_upload_size !== undefined) { const size = parseInt(max_upload_size); if (isNaN(size) || size < 0) { return res.status(400).json({ success: false, message: '无效的文件大小' }); } SettingsDB.set('max_upload_size', size.toString()); } res.json({ success: true, message: '系统设置已更新' }); } catch (error) { console.error('更新系统设置失败:', error); res.status(500).json({ success: false, message: '更新系统设置失败: ' + error.message }); } }); // 获取服务器存储统计信息 app.get('/api/admin/storage-stats', authMiddleware, adminMiddleware, async (req, res) => { try { // 获取本地存储目录 const localStorageDir = path.join(__dirname, 'local-storage'); // 获取磁盘信息(使用df命令) let totalDisk = 0; let usedDisk = 0; let availableDisk = 0; try { // 获取本地存储目录所在分区的磁盘信息 const { stdout: dfOutput } = await execAsync(`df -B 1 / | tail -1`, { encoding: 'utf8' }); const parts = dfOutput.trim().split(/\s+/); if (parts.length >= 4) { totalDisk = parseInt(parts[1]) || 0; // 总大小 usedDisk = parseInt(parts[2]) || 0; // 已使用 availableDisk = parseInt(parts[3]) || 0; // 可用 } } catch (dfError) { console.error('获取磁盘信息失败:', dfError.message); // 如果df命令失败,尝试使用Windows的wmic命令 try { // 获取本地存储目录所在的驱动器号 const driveLetter = localStorageDir.charAt(0); const { stdout: wmicOutput } = await execAsync(`wmic logicaldisk where "DeviceID='' + driveLetter + '':''" get Size,FreeSpace /value`, { encoding: 'utf8' }); const freeMatch = wmicOutput.match(/FreeSpace=(\d+)/); const sizeMatch = wmicOutput.match(/Size=(\d+)/); if (sizeMatch && freeMatch) { totalDisk = parseInt(sizeMatch[1]) || 0; availableDisk = parseInt(freeMatch[1]) || 0; usedDisk = totalDisk - availableDisk; } } catch (wmicError) { console.error('获取Windows磁盘信息失败:', wmicError.message); } } // 从数据库获取所有用户的本地存储配额和使用情况 const users = UserDB.getAll(); let totalUserQuotas = 0; let totalUserUsed = 0; users.forEach(user => { // 只统计使用本地存储的用户(local_only 或 user_choice) const storagePermission = user.storage_permission || 'sftp_only'; if (storagePermission === 'local_only' || storagePermission === 'user_choice') { totalUserQuotas += user.local_storage_quota || 0; totalUserUsed += user.local_storage_used || 0; } }); res.json({ success: true, stats: { totalDisk, // 磁盘总容量 usedDisk, // 磁盘已使用 availableDisk, // 磁盘可用空间 totalUserQuotas, // 用户配额总和 totalUserUsed, // 用户实际使用总和 totalUsers: users.length // 用户总数 } }); // 获取所有用户 } catch (error) { console.error('获取存储统计失败:', error); res.status(500).json({ success: false, message: '获取存储统计失败: ' + error.message }); }}); app.get('/api/admin/users', authMiddleware, adminMiddleware, (req, res) => { try { const users = UserDB.getAll(); res.json({ success: true, users: users.map(u => ({ id: u.id, username: u.username, email: u.email, is_admin: u.is_admin, is_active: u.is_active, is_banned: u.is_banned, has_ftp_config: u.has_ftp_config, created_at: u.created_at, // 新增:存储相关字段 storage_permission: u.storage_permission || 'sftp_only', current_storage_type: u.current_storage_type || 'sftp', local_storage_quota: u.local_storage_quota || 1073741824, local_storage_used: u.local_storage_used || 0 })) }); } catch (error) { console.error('获取用户列表失败:', error); res.status(500).json({ success: false, message: '获取用户列表失败: ' + error.message }); } }); // 封禁/解封用户 app.post('/api/admin/users/:id/ban', authMiddleware, adminMiddleware, (req, res) => { try { const { id } = req.params; const { banned } = req.body; UserDB.setBanStatus(id, banned); res.json({ success: true, message: banned ? '用户已封禁' : '用户已解封' }); } catch (error) { console.error('操作失败:', error); res.status(500).json({ success: false, message: '操作失败: ' + error.message }); } }); // 删除用户(级联删除文件和分享) app.delete('/api/admin/users/:id', authMiddleware, adminMiddleware, async (req, res) => { try { const { id } = req.params; if (parseInt(id) === req.user.id) { return res.status(400).json({ success: false, message: '不能删除自己的账号' }); } // 获取用户信息 const user = UserDB.findById(id); if (!user) { return res.status(404).json({ success: false, message: '用户不存在' }); } const deletionLog = { userId: id, username: user.username, deletedFiles: [], deletedShares: 0, warnings: [] }; // 1. 删除本地存储文件(如果用户使用了本地存储) const storagePermission = user.storage_permission || 'sftp_only'; if (storagePermission === 'local_only' || storagePermission === 'user_choice') { const storageRoot = process.env.STORAGE_ROOT || path.join(__dirname, 'storage'); const userStorageDir = path.join(storageRoot, `user_${id}`); if (fs.existsSync(userStorageDir)) { try { // 递归删除用户目录 const deletedSize = getUserDirectorySize(userStorageDir); fs.rmSync(userStorageDir, { recursive: true, force: true }); deletionLog.deletedFiles.push({ type: 'local', path: userStorageDir, size: deletedSize }); console.log(`[删除用户] 已删除本地存储目录: ${userStorageDir}`); } catch (error) { console.error(`[删除用户] 删除本地存储失败:`, error); deletionLog.warnings.push(`删除本地存储失败: ${error.message}`); } } } // 2. SFTP存储文件 - 只记录警告,不实际删除(安全考虑) if (user.has_ftp_config && (storagePermission === 'sftp_only' || storagePermission === 'user_choice')) { deletionLog.warnings.push( `用户配置了SFTP存储 (${user.ftp_host}:${user.ftp_port}),SFTP文件未自动删除,请手动处理` ); } // 3. 删除用户的所有分享记录 try { const userShares = ShareDB.getUserShares(id); deletionLog.deletedShares = userShares.length; userShares.forEach(share => { ShareDB.delete(share.id); // 清除分享缓存 if (shareFileCache.has(share.share_code)) { shareFileCache.delete(share.share_code); } }); console.log(`[删除用户] 已删除 ${deletionLog.deletedShares} 条分享记录`); } catch (error) { console.error(`[删除用户] 删除分享记录失败:`, error); deletionLog.warnings.push(`删除分享记录失败: ${error.message}`); } // 4. 删除用户记录 UserDB.delete(id); // 构建响应消息 let message = `用户 ${user.username} 已删除`; if (deletionLog.deletedFiles.length > 0) { const totalSize = deletionLog.deletedFiles.reduce((sum, f) => sum + f.size, 0); message += `,已清理本地文件 ${formatFileSize(totalSize)}`; } if (deletionLog.deletedShares > 0) { message += `,已删除 ${deletionLog.deletedShares} 条分享`; } res.json({ success: true, message, details: deletionLog }); } catch (error) { console.error('删除用户失败:', error); res.status(500).json({ success: false, message: '删除用户失败: ' + error.message }); } }); // 辅助函数:计算目录大小 function getUserDirectorySize(dirPath) { let totalSize = 0; function calculateSize(currentPath) { try { const stats = fs.statSync(currentPath); if (stats.isDirectory()) { const files = fs.readdirSync(currentPath); files.forEach(file => { calculateSize(path.join(currentPath, file)); }); } else { totalSize += stats.size; } } catch (error) { console.error(`计算大小失败: ${currentPath}`, error); } } calculateSize(dirPath); return totalSize; } // 设置用户存储权限(管理员) app.post('/api/admin/users/:id/storage-permission', authMiddleware, adminMiddleware, [ body('storage_permission').isIn(['local_only', 'sftp_only', 'user_choice']).withMessage('无效的存储权限') ], (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ success: false, errors: errors.array() }); } try { const { id } = req.params; const { storage_permission, local_storage_quota } = req.body; const updates = { storage_permission }; // 如果提供了配额,更新配额(单位:字节) if (local_storage_quota !== undefined) { updates.local_storage_quota = parseInt(local_storage_quota); } // 根据权限设置自动调整存储类型 const user = UserDB.findById(id); if (!user) { return res.status(404).json({ success: false, message: '用户不存在' }); } if (storage_permission === 'local_only') { updates.current_storage_type = 'local'; } else if (storage_permission === 'sftp_only') { // 只有配置了SFTP才切换到SFTP if (user.has_ftp_config) { updates.current_storage_type = 'sftp'; } } // user_choice 不自动切换,保持用户当前选择 UserDB.update(id, updates); res.json({ success: true, message: '存储权限已更新' }); } catch (error) { console.error('设置存储权限失败:', error); res.status(500).json({ success: false, message: '设置存储权限失败: ' + error.message }); } } ); // 重置用户密码 // ===== 密码重置请求系统 ===== // 用户提交密码重置请求(公开API) app.post('/api/password-reset/request', [ body('username').notEmpty().withMessage('用户名不能为空'), body('new_password').isLength({ min: 6 }).withMessage('新密码至少6个字符') ], (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ success: false, errors: errors.array() }); } try { const { username, new_password } = req.body; const user = UserDB.findByUsername(username); if (!user) { return res.status(404).json({ success: false, message: '用户不存在' }); } // 检查是否已有待审核的请求 if (PasswordResetDB.hasPendingRequest(user.id)) { return res.status(400).json({ success: false, message: '您已经提交过密码重置请求,请等待管理员审核' }); } // 创建密码重置请求 PasswordResetDB.create(user.id, new_password); res.json({ success: true, message: '密码重置请求已提交,请等待管理员审核' }); } catch (error) { console.error('提交密码重置请求失败:', error); res.status(500).json({ success: false, message: '提交失败: ' + error.message }); } } ); // 获取待审核的密码重置请求(管理员) app.get('/api/admin/password-reset/pending', authMiddleware, adminMiddleware, (req, res) => { try { const requests = PasswordResetDB.getPending(); res.json({ success: true, requests }); } catch (error) { console.error('获取密码重置请求失败:', error); res.status(500).json({ success: false, message: '获取请求失败: ' + error.message }); } }); // 审核密码重置请求(管理员) app.post('/api/admin/password-reset/:id/review', authMiddleware, adminMiddleware, (req, res) => { try { const { id } = req.params; const { approved } = req.body; PasswordResetDB.review(id, req.user.id, approved); res.json({ success: true, message: approved ? '密码重置已批准' : '密码重置已拒绝' }); } catch (error) { console.error('审核密码重置请求失败:', error); res.status(500).json({ success: false, message: error.message || '审核失败' }); } }); // ===== 管理员文件审查功能 ===== // 查看用户文件列表(管理员,只读) app.get('/api/admin/users/:id/files', authMiddleware, adminMiddleware, async (req, res) => { const { id } = req.params; const dirPath = req.query.path || '/'; let sftp; try { const user = UserDB.findById(id); if (!user) { return res.status(404).json({ success: false, message: '用户不存在' }); } if (!user.has_ftp_config) { return res.status(400).json({ success: false, message: '该用户未配置SFTP服务器' }); } sftp = await connectToSFTP(user); const list = await sftp.list(dirPath); const formattedList = list.map(item => ({ name: item.name, type: item.type === 'd' ? 'directory' : 'file', size: item.size, sizeFormatted: formatFileSize(item.size), modifiedAt: new Date(item.modifyTime), isDirectory: item.type === 'd' })); formattedList.sort((a, b) => { if (a.isDirectory && !b.isDirectory) return -1; if (!a.isDirectory && b.isDirectory) return 1; return a.name.localeCompare(b.name); }); res.json({ success: true, username: user.username, path: dirPath, items: formattedList }); } catch (error) { console.error('管理员查看用户文件失败:', error); res.status(500).json({ success: false, message: '获取文件列表失败: ' + error.message }); } finally { if (sftp) await sftp.end(); } }); // 获取所有分享(管理员) app.get('/api/admin/shares', authMiddleware, adminMiddleware, (req, res) => { try { const shares = ShareDB.getAll(); res.json({ success: true, shares }); } catch (error) { console.error('获取分享列表失败:', error); res.status(500).json({ success: false, message: '获取分享列表失败: ' + error.message }); } }); // 删除分享(管理员) app.delete('/api/admin/shares/:id', authMiddleware, adminMiddleware, (req, res) => { try { // 先获取分享信息以获得share_code const share = ShareDB.findById(req.params.id); if (share) { // 删除缓存 if (shareFileCache.has(share.share_code)) { shareFileCache.delete(share.share_code); console.log(`[缓存清除] 分享码: ${share.share_code} (管理员操作)`); } // 删除数据库记录 ShareDB.delete(req.params.id); res.json({ success: true, message: '分享已删除' }); } else { res.status(404).json({ success: false, message: '分享不存在' }); } } catch (error) { console.error('删除分享失败:', error); res.status(500).json({ success: false, message: '删除分享失败: ' + error.message }); } }); // 分享页面访问路由 app.get("/s/:code", (req, res) => { const shareCode = req.params.code; // 使用相对路径重定向,浏览器会自动使用当前的协议和host const frontendUrl = `/share.html?code=${shareCode}`; console.log(`[分享] 重定向到: ${frontendUrl}`); res.redirect(frontendUrl); }); // 启动时清理旧临时文件 cleanupOldTempFiles(); // 启动服务器 app.listen(PORT, '0.0.0.0', () => { console.log(`\n========================================`); console.log(`玩玩云已启动`); console.log(`服务器地址: http://localhost:${PORT}`); console.log(`外网访问地址: http://0.0.0.0:${PORT}`); console.log(`========================================\n`); });