Files
vue-driven-cloud-storage/backend/server.js
喻勇祥 04e9ff5b7e fix: 修复两个安全漏洞
1. 修复分享下载接口越权访问漏洞(高危)
   - 添加isPathWithinShare函数验证请求路径是否在分享范围内
   - 单文件分享只允许下载该文件
   - 目录分享只允许下载该目录及子目录的文件
   - 防止攻击者通过构造path参数访问分享者的任意文件
   - 相关文件:backend/server.js

2. 修复本地存储路径处理问题(中高危)
   - 优化getFullPath方法处理绝对路径的逻辑
   - 修复Linux环境下path.join处理'/'导致的路径错误
   - 将绝对路径转换为相对路径,确保正确拼接用户目录
   - 相关文件:backend/storage.js

🔐 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-13 18:00:09 +08:00

2352 lines
67 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 加载环境变量(必须在最开始)
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 isPathWithinShare(requestPath, share) {
if (!requestPath || !share) {
return false;
}
// 规范化路径(移除 ../ 等危险路径,统一分隔符)
const normalizedRequest = path.normalize(requestPath).replace(/^(\.\.[\/\\])+/, '').replace(/\\/g, '/');
const normalizedShare = path.normalize(share.share_path).replace(/\\/g, '/');
if (share.share_type === 'file') {
// 单文件分享:只允许下载该文件
return normalizedRequest === normalizedShare;
} else {
// 目录分享:只允许下载该目录及其子目录下的文件
// 确保分享路径以斜杠结尾用于前缀匹配
const sharePrefix = normalizedShare.endsWith('/') ? normalizedShare : normalizedShare + '/';
return normalizedRequest === normalizedShare || normalizedRequest.startsWith(sharePrefix);
}
}
// 清理旧的临时文件(启动时执行一次)
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 = shareOwner.current_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: '密码错误或未提供密码'
});
}
}
// ✅ 安全验证:检查请求路径是否在分享范围内(防止越权访问)
if (!isPathWithinShare(filePath, share)) {
console.warn(`[安全] 检测到越权访问尝试 - 分享码: ${code}, 请求路径: ${filePath}, 分享路径: ${share.share_path}`);
return res.status(403).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 = shareOwner.current_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`);
});