Files
vue-driven-cloud-storage/backend/server.js
WanWanYun f097dfd179 修复: 将默认上传限制从100MB提升到10GB
- database.js: 修改数据库初始化默认值为10GB (10737418240字节)
- server.js: 修改两处fallback默认值为10GB
- 确保新部署的系统默认支持10GB单文件上传
- 解决用户报告的100MB上传限制问题

版本: v1.2.5
2025-11-11 01:57:06 +08:00

2117 lines
61 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 { execSync } = require('child_process');
const { db, UserDB, ShareDB, SettingsDB, PasswordResetDB } = require('./database');
const { generateToken, authMiddleware, adminMiddleware } = require('./auth');
const app = express();
const PORT = process.env.PORT || 40001;
// 中间件
app.use(cors({ credentials: true, origin: true }));
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限制
});
// 分享文件信息缓存(内存缓存)
// 格式: Map<share_code, { name, size, sizeFormatted, modifiedAt, httpDownloadUrl }>
const shareFileCache = new Map();
// ===== 工具函数 =====
// 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,
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('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 { new_password } = req.body;
// 直接更新密码,不需要验证当前密码
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)) {
fs.unlinkSync(req.file.path);
}
return res.status(413).json({
success: false,
message: '文件超过上传限制',
maxSize: maxUploadSize,
fileSize: req.file.size
});
}
const remotePath = req.body.path || '/';
const remoteFilePath = remotePath === '/'
? `/${req.file.originalname}`
: `${remotePath}/${req.file.originalname}`;
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}`);
// 删除本地临时文件
fs.unlinkSync(req.file.path);
res.json({
success: true,
message: '文件上传成功',
filename: req.file.originalname,
path: remoteFilePath
});
} catch (error) {
console.error('文件上传失败:', error);
// 删除临时文件
if (fs.existsSync(req.file.path)) {
fs.unlinkSync(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
});
}
});
stream.pipe(res);
} catch (error) {
console.error('下载文件失败:', error);
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, (req, res) => {
try {
// 获取本地存储目录
const localStorageDir = path.join(__dirname, 'local-storage');
// 获取磁盘信息使用df命令
let totalDisk = 0;
let usedDisk = 0;
let availableDisk = 0;
try {
// 获取本地存储目录所在分区的磁盘信息
const dfOutput = execSync(`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 wmicOutput = execSync(`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);
});
// 启动服务器
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`);
});