- 添加 generateShareUrl 辅助函数,智能处理端口号 - 支持通过 PUBLIC_PORT 环境变量配置nginx监听端口 - 更新 POST /api/share/create 使用新的URL生成方法 - 更新 GET /api/share/my 使用新的URL生成方法 - 修复IP模式部署时分享链接无法正常访问的问题
2348 lines
67 KiB
JavaScript
2348 lines
67 KiB
JavaScript
// 加载环境变量(必须在最开始)
|
||
require('dotenv').config();
|
||
|
||
const express = require('express');
|
||
const cors = require('cors');
|
||
const cookieParser = require('cookie-parser');
|
||
const SftpClient = require('ssh2-sftp-client');
|
||
const multer = require('multer');
|
||
const path = require('path');
|
||
const fs = require('fs');
|
||
const { body, validationResult } = require('express-validator');
|
||
const archiver = require('archiver');
|
||
const { exec, execSync } = require('child_process');
|
||
const util = require('util');
|
||
const execAsync = util.promisify(exec);
|
||
|
||
const { db, UserDB, ShareDB, SettingsDB, PasswordResetDB } = require('./database');
|
||
const { generateToken, authMiddleware, adminMiddleware } = require('./auth');
|
||
|
||
const app = express();
|
||
const PORT = process.env.PORT || 40001;
|
||
|
||
|
||
// 配置CORS
|
||
const allowedOrigins = process.env.ALLOWED_ORIGINS
|
||
? process.env.ALLOWED_ORIGINS.split(',').map(origin => origin.trim())
|
||
: ['*'];
|
||
|
||
const corsOptions = {
|
||
credentials: true,
|
||
origin: (origin, callback) => {
|
||
// 允许所有来源(仅限开发环境)
|
||
if (allowedOrigins.includes('*')) {
|
||
if (process.env.NODE_ENV === 'production') {
|
||
console.warn('⚠️ 警告: 生产环境建议配置具体的ALLOWED_ORIGINS,而不是使用 *');
|
||
}
|
||
callback(null, true);
|
||
return;
|
||
}
|
||
|
||
// 允许来自配置列表中的域名
|
||
if (!origin || allowedOrigins.includes(origin)) {
|
||
callback(null, true);
|
||
} else {
|
||
console.warn(`[CORS] 拒绝来自未授权来源的请求: ${origin}`);
|
||
callback(new Error('CORS策略不允许来自该来源的访问'));
|
||
}
|
||
}
|
||
};
|
||
|
||
// 中间件
|
||
app.use(cors(corsOptions));
|
||
app.use(express.json());
|
||
app.use(cookieParser());
|
||
|
||
// 请求日志
|
||
app.use((req, res, next) => {
|
||
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
|
||
next();
|
||
});
|
||
|
||
// 文件上传配置(临时存储)
|
||
const upload = multer({
|
||
dest: path.join(__dirname, 'uploads'),
|
||
limits: { fileSize: 5 * 1024 * 1024 * 1024 } // 5GB限制
|
||
});
|
||
|
||
// ===== TTL缓存类 =====
|
||
|
||
// 带过期时间的缓存类
|
||
class TTLCache {
|
||
constructor(defaultTTL = 3600000) { // 默认1小时
|
||
this.cache = new Map();
|
||
this.defaultTTL = defaultTTL;
|
||
|
||
// 每10分钟清理一次过期缓存
|
||
this.cleanupInterval = setInterval(() => {
|
||
this.cleanup();
|
||
}, 10 * 60 * 1000);
|
||
}
|
||
|
||
set(key, value, ttl = this.defaultTTL) {
|
||
const expiresAt = Date.now() + ttl;
|
||
this.cache.set(key, { value, expiresAt });
|
||
}
|
||
|
||
get(key) {
|
||
const item = this.cache.get(key);
|
||
if (!item) {
|
||
return undefined;
|
||
}
|
||
|
||
// 检查是否过期
|
||
if (Date.now() > item.expiresAt) {
|
||
this.cache.delete(key);
|
||
return undefined;
|
||
}
|
||
|
||
return item.value;
|
||
}
|
||
|
||
has(key) {
|
||
const item = this.cache.get(key);
|
||
if (!item) {
|
||
return false;
|
||
}
|
||
|
||
// 检查是否过期
|
||
if (Date.now() > item.expiresAt) {
|
||
this.cache.delete(key);
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
delete(key) {
|
||
return this.cache.delete(key);
|
||
}
|
||
|
||
// 清理过期缓存
|
||
cleanup() {
|
||
const now = Date.now();
|
||
let cleaned = 0;
|
||
|
||
for (const [key, item] of this.cache.entries()) {
|
||
if (now > item.expiresAt) {
|
||
this.cache.delete(key);
|
||
cleaned++;
|
||
}
|
||
}
|
||
|
||
if (cleaned > 0) {
|
||
console.log(`[缓存清理] 已清理 ${cleaned} 个过期的分享缓存`);
|
||
}
|
||
}
|
||
|
||
// 获取缓存大小
|
||
size() {
|
||
return this.cache.size;
|
||
}
|
||
|
||
// 停止清理定时器
|
||
destroy() {
|
||
if (this.cleanupInterval) {
|
||
clearInterval(this.cleanupInterval);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 分享文件信息缓存(内存缓存,1小时TTL)
|
||
const shareFileCache = new TTLCache(60 * 60 * 1000);
|
||
|
||
// ===== 工具函数 =====
|
||
|
||
|
||
// 安全删除文件(不抛出异常)
|
||
function safeDeleteFile(filePath) {
|
||
try {
|
||
if (fs.existsSync(filePath)) {
|
||
fs.unlinkSync(filePath);
|
||
console.log(`[清理] 已删除临时文件: ${filePath}`);
|
||
return true;
|
||
}
|
||
} catch (error) {
|
||
console.error(`[清理] 删除临时文件失败: ${filePath}`, error.message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 清理旧的临时文件(启动时执行一次)
|
||
function cleanupOldTempFiles() {
|
||
const uploadsDir = path.join(__dirname, 'uploads');
|
||
if (!fs.existsSync(uploadsDir)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const files = fs.readdirSync(uploadsDir);
|
||
const now = Date.now();
|
||
const maxAge = 24 * 60 * 60 * 1000; // 24小时
|
||
|
||
let cleaned = 0;
|
||
files.forEach(file => {
|
||
const filePath = path.join(uploadsDir, file);
|
||
try {
|
||
const stats = fs.statSync(filePath);
|
||
if (now - stats.mtimeMs > maxAge) {
|
||
fs.unlinkSync(filePath);
|
||
cleaned++;
|
||
}
|
||
} catch (err) {
|
||
console.error(`[清理] 检查文件失败: ${filePath}`, err.message);
|
||
}
|
||
});
|
||
|
||
if (cleaned > 0) {
|
||
console.log(`[清理] 已清理 ${cleaned} 个超过24小时的临时文件`);
|
||
}
|
||
} catch (error) {
|
||
console.error('[清理] 清理临时文件目录失败:', error.message);
|
||
}
|
||
}
|
||
// SFTP连接
|
||
async function connectToSFTP(config) {
|
||
const sftp = new SftpClient();
|
||
await sftp.connect({
|
||
host: config.ftp_host,
|
||
port: config.ftp_port || 22,
|
||
username: config.ftp_user,
|
||
password: config.ftp_password
|
||
});
|
||
return sftp;
|
||
}
|
||
|
||
// 格式化文件大小
|
||
function formatFileSize(bytes) {
|
||
if (bytes === 0) return '0 B';
|
||
const k = 1024;
|
||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||
}
|
||
|
||
|
||
// 生成分享URL(处理非标准端口)
|
||
function generateShareUrl(req, shareCode) {
|
||
const protocol = req.protocol;
|
||
const host = req.get('host'); // 可能包含或不包含端口
|
||
|
||
// 如果 host 已经包含端口号,直接使用
|
||
if (host.includes(':')) {
|
||
return `${protocol}://${host}/s/${shareCode}`;
|
||
}
|
||
|
||
// 如果没有端口号,检查是否需要添加
|
||
// 从环境变量读取公开端口(nginx监听的端口)
|
||
const publicPort = process.env.PUBLIC_PORT || null;
|
||
|
||
// 如果配置了公开端口,且不是标准端口(80/443),则添加端口号
|
||
if (publicPort && publicPort !== '80' && publicPort !== '443') {
|
||
return `${protocol}://${host}:${publicPort}/s/${shareCode}`;
|
||
}
|
||
|
||
// 标准端口或未配置,不添加端口号
|
||
return `${protocol}://${host}/s/${shareCode}`;
|
||
}
|
||
|
||
// ===== 公开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 = generateShareUrl(req, 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: generateShareUrl(req, 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 {
|
||
// 缓存未命中,查询存储
|
||
try {
|
||
// 获取分享者的用户信息
|
||
const shareOwner = UserDB.findById(share.user_id);
|
||
if (!shareOwner) {
|
||
throw new Error('分享者不存在');
|
||
}
|
||
|
||
// 使用分享者当前的存储类型(而不是分享创建时的存储类型)
|
||
const storageType = shareOwner.current_storage_type || 'sftp';
|
||
console.log(`[缓存未命中] 分享码: ${code},存储类型: ${storageType} (分享者当前)`);
|
||
|
||
// 使用统一存储接口
|
||
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 = shareOwner.current_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 = shareOwner.current_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 = 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`);
|
||
});
|