Initial commit - 玩玩云文件管理系统 v1.0.0

- 完整的前后端代码
- 支持本地存储和SFTP存储
- 文件分享功能
- 上传工具源代码
- 完整的部署文档
- Nginx配置模板

技术栈:
- 后端: Node.js + Express + SQLite
- 前端: Vue.js 3 + Axios
- 存储: 本地存储 / SFTP远程存储
This commit is contained in:
WanWanYun
2025-11-10 21:50:16 +08:00
commit 0f133962dc
36 changed files with 32178 additions and 0 deletions

8
backend/.env.example Normal file
View File

@@ -0,0 +1,8 @@
# FTP服务器配置
FTP_HOST=your-ftp-host.com
FTP_PORT=21
FTP_USER=your-username
FTP_PASSWORD=your-password
# 服务器配置
PORT=3000

21
backend/Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM node:20-alpine
WORKDIR /app
# 安装编译工具
RUN apk add --no-cache python3 make g++
# 复制package文件
COPY package*.json ./
# 安装依赖
RUN npm install --production
# 复制应用代码
COPY . .
# 暴露端口
EXPOSE 40001
# 启动应用
CMD ["node", "server.js"]

108
backend/auth.js Normal file
View File

@@ -0,0 +1,108 @@
const jwt = require('jsonwebtoken');
const { UserDB } = require('./database');
// JWT密钥生产环境应该放在环境变量中
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
// 生成JWT Token
function generateToken(user) {
return jwt.sign(
{
id: user.id,
username: user.username,
is_admin: user.is_admin
},
JWT_SECRET,
{ expiresIn: '7d' }
);
}
// 验证Token中间件
function authMiddleware(req, res, next) {
// 从请求头、cookie或URL参数中获取token
const token = req.headers.authorization?.replace('Bearer ', '') || req.cookies?.token || req.query?.token;
if (!token) {
return res.status(401).json({
success: false,
message: '未提供认证令牌'
});
}
try {
const decoded = jwt.verify(token, JWT_SECRET);
const user = UserDB.findById(decoded.id);
if (!user) {
return res.status(401).json({
success: false,
message: '用户不存在'
});
}
if (user.is_banned) {
return res.status(403).json({
success: false,
message: '账号已被封禁'
});
}
if (!user.is_active) {
return res.status(403).json({
success: false,
message: '账号未激活'
});
}
// 将用户信息附加到请求对象(包含所有存储相关字段)
req.user = {
id: user.id,
username: user.username,
email: user.email,
is_admin: user.is_admin,
has_ftp_config: user.has_ftp_config,
ftp_host: user.ftp_host,
ftp_port: user.ftp_port,
ftp_user: user.ftp_user,
ftp_password: user.ftp_password,
http_download_base_url: user.http_download_base_url,
// 存储相关字段v2.0新增)
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
};
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: '令牌已过期'
});
}
return res.status(401).json({
success: false,
message: '无效的令牌'
});
}
}
// 管理员权限中间件
function adminMiddleware(req, res, next) {
if (!req.user || !req.user.is_admin) {
return res.status(403).json({
success: false,
message: '需要管理员权限'
});
}
next();
}
module.exports = {
JWT_SECRET,
generateToken,
authMiddleware,
adminMiddleware
};

52
backend/backup.bat Normal file
View File

@@ -0,0 +1,52 @@
@echo off
chcp 65001 >nul
echo ========================================
echo 数据库备份工具
echo ========================================
echo.
cd /d %~dp0
REM 创建备份目录
if not exist backup mkdir backup
REM 生成时间戳
set YEAR=%date:~0,4%
set MONTH=%date:~5,2%
set DAY=%date:~8,2%
set HOUR=%time:~0,2%
set MINUTE=%time:~3,2%
set SECOND=%time:~6,2%
REM 去掉小时前面的空格
if "%HOUR:~0,1%" == " " set HOUR=0%HOUR:~1,1%
set TIMESTAMP=%YEAR%%MONTH%%DAY%_%HOUR%%MINUTE%%SECOND%
REM 备份数据库
copy ftp-manager.db backup\ftp-manager-%TIMESTAMP%.db >nul
if %errorlevel% == 0 (
echo [成功] 备份完成!
echo 文件: backup\ftp-manager-%TIMESTAMP%.db
REM 获取文件大小
for %%A in (backup\ftp-manager-%TIMESTAMP%.db) do echo 大小: %%~zA 字节
) else (
echo [错误] 备份失败!
)
echo.
REM 清理30天前的备份
echo 清理30天前的旧备份...
forfiles /P backup /M ftp-manager-*.db /D -30 /C "cmd /c del @path" 2>nul
if %errorlevel% == 0 (
echo [成功] 旧备份已清理
) else (
echo [提示] 没有需要清理的旧备份
)
echo.
echo ========================================
pause

554
backend/database.js Normal file
View File

@@ -0,0 +1,554 @@
const Database = require('better-sqlite3');
const bcrypt = require('bcryptjs');
const path = require('path');
// 创建或连接数据库
const db = new Database(path.join(__dirname, 'ftp-manager.db'));
// 启用外键约束
db.pragma('foreign_keys = ON');
// 初始化数据库表
function initDatabase() {
// 用户表
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
-- FTP配置可选
ftp_host TEXT,
ftp_port INTEGER DEFAULT 22,
ftp_user TEXT,
ftp_password TEXT,
http_download_base_url TEXT,
-- 上传工具API密钥
upload_api_key TEXT,
-- 用户状态
is_admin INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1,
is_banned INTEGER DEFAULT 0,
has_ftp_config INTEGER DEFAULT 0,
-- 时间戳
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// 分享链接表
db.exec(`
CREATE TABLE IF NOT EXISTS shares (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
share_code TEXT UNIQUE NOT NULL,
share_path TEXT NOT NULL,
share_type TEXT DEFAULT 'file',
share_password TEXT,
-- 分享统计
view_count INTEGER DEFAULT 0,
download_count INTEGER DEFAULT 0,
-- 时间戳
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
`);
// 系统设置表
db.exec(`
CREATE TABLE IF NOT EXISTS system_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// 密码重置请求表
db.exec(`
CREATE TABLE IF NOT EXISTS password_reset_requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
new_password TEXT NOT NULL,
status TEXT DEFAULT 'pending', -- pending, approved, rejected
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
reviewed_at DATETIME,
reviewed_by INTEGER,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (reviewed_by) REFERENCES users (id)
)
`);
// 创建索引
db.exec(`
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_shares_code ON shares(share_code);
CREATE INDEX IF NOT EXISTS idx_shares_user ON shares(user_id);
CREATE INDEX IF NOT EXISTS idx_reset_requests_user ON password_reset_requests(user_id);
CREATE INDEX IF NOT EXISTS idx_reset_requests_status ON password_reset_requests(status);
`);
// 数据库迁移添加upload_api_key字段如果不存在
try {
const columns = db.prepare("PRAGMA table_info(users)").all();
const hasUploadApiKey = columns.some(col => col.name === 'upload_api_key');
if (!hasUploadApiKey) {
db.exec(`ALTER TABLE users ADD COLUMN upload_api_key TEXT`);
console.log('数据库迁移添加upload_api_key字段完成');
}
} catch (error) {
console.error('数据库迁移失败:', error);
}
// 数据库迁移添加share_type字段如果不存在
try {
const shareColumns = db.prepare("PRAGMA table_info(shares)").all();
const hasShareType = shareColumns.some(col => col.name === 'share_type');
if (!hasShareType) {
db.exec(`ALTER TABLE shares ADD COLUMN share_type TEXT DEFAULT 'file'`);
console.log('数据库迁移添加share_type字段完成');
}
} catch (error) {
console.error('数据库迁移share_type失败:', error);
}
console.log('数据库初始化完成');
}
// 创建默认管理员账号
function createDefaultAdmin() {
const adminExists = db.prepare('SELECT id FROM users WHERE is_admin = 1').get();
if (!adminExists) {
const hashedPassword = bcrypt.hashSync('admin123', 10);
db.prepare(`
INSERT INTO users (
username, email, password,
is_admin, is_active, has_ftp_config
) VALUES (?, ?, ?, ?, ?, ?)
`).run(
'admin',
'admin@example.com',
hashedPassword,
1,
1,
0 // 管理员不需要FTP配置
);
console.log('默认管理员账号已创建');
console.log('用户名: admin');
console.log('密码: admin123');
console.log('⚠️ 请登录后立即修改密码!');
}
}
// 用户相关操作
const UserDB = {
// 创建用户
create(userData) {
const hashedPassword = bcrypt.hashSync(userData.password, 10);
const hasFtpConfig = userData.ftp_host && userData.ftp_user && userData.ftp_password ? 1 : 0;
const stmt = db.prepare(`
INSERT INTO users (
username, email, password,
ftp_host, ftp_port, ftp_user, ftp_password, http_download_base_url,
has_ftp_config
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
userData.username,
userData.email,
hashedPassword,
userData.ftp_host || null,
userData.ftp_port || 22,
userData.ftp_user || null,
userData.ftp_password || null,
userData.http_download_base_url || null,
hasFtpConfig
);
return result.lastInsertRowid;
},
// 根据用户名查找
findByUsername(username) {
return db.prepare('SELECT * FROM users WHERE username = ?').get(username);
},
// 根据邮箱查找
findByEmail(email) {
return db.prepare('SELECT * FROM users WHERE email = ?').get(email);
},
// 根据ID查找
findById(id) {
return db.prepare('SELECT * FROM users WHERE id = ?').get(id);
},
// 验证密码
verifyPassword(plainPassword, hashedPassword) {
return bcrypt.compareSync(plainPassword, hashedPassword);
},
// 更新用户
update(id, updates) {
const fields = [];
const values = [];
for (const [key, value] of Object.entries(updates)) {
if (key === 'password') {
fields.push(`${key} = ?`);
values.push(bcrypt.hashSync(value, 10));
} else {
fields.push(`${key} = ?`);
values.push(value);
}
}
fields.push('updated_at = CURRENT_TIMESTAMP');
values.push(id);
const stmt = db.prepare(`UPDATE users SET ${fields.join(', ')} WHERE id = ?`);
return stmt.run(...values);
},
// 获取所有用户
getAll(filters = {}) {
let query = 'SELECT * FROM users WHERE 1=1';
const params = [];
if (filters.is_admin !== undefined) {
query += ' AND is_admin = ?';
params.push(filters.is_admin);
}
if (filters.is_banned !== undefined) {
query += ' AND is_banned = ?';
params.push(filters.is_banned);
}
query += ' ORDER BY created_at DESC';
return db.prepare(query).all(...params);
},
// 删除用户
delete(id) {
return db.prepare('DELETE FROM users WHERE id = ?').run(id);
},
// 封禁/解封用户
setBanStatus(id, isBanned) {
return db.prepare('UPDATE users SET is_banned = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
.run(isBanned ? 1 : 0, id);
}
};
// 分享链接相关操作
const ShareDB = {
// 生成随机分享码
generateShareCode(length = 8) {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let code = '';
for (let i = 0; i < length; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length));
}
return code;
},
// 创建分享链接
// 创建分享链接
create(userId, options = {}) {
const {
share_type = 'file',
file_path = '',
file_name = '',
password = null,
expiry_days = null
} = options;
let shareCode;
let attempts = 0;
// 尝试生成唯一的分享码
do {
shareCode = this.generateShareCode();
attempts++;
if (attempts > 10) {
shareCode = this.generateShareCode(10); // 增加长度
}
} while (this.findByCode(shareCode) && attempts < 20);
// 计算过期时间
let expiresAt = null;
if (expiry_days) {
const expireDate = new Date();
expireDate.setDate(expireDate.getDate() + parseInt(expiry_days));
expiresAt = expireDate.toISOString();
}
const stmt = db.prepare(`
INSERT INTO shares (user_id, share_code, share_path, share_type, share_password, expires_at)
VALUES (?, ?, ?, ?, ?, ?)
`);
const hashedPassword = password ? bcrypt.hashSync(password, 10) : null;
const sharePath = share_type === 'file' ? file_path : '/';
const result = stmt.run(
userId,
shareCode,
sharePath,
share_type,
hashedPassword,
expiresAt
);
return {
id: result.lastInsertRowid,
share_code: shareCode,
share_type: share_type
};
},
// 根据分享码查找
findByCode(shareCode) {
return db.prepare(`
SELECT s.*, u.username, u.ftp_host, u.ftp_port, u.ftp_user, u.ftp_password, u.http_download_base_url
FROM shares s
JOIN users u ON s.user_id = u.id
WHERE s.share_code = ?
`).get(shareCode);
},
// 根据ID查找
findById(id) {
return db.prepare('SELECT * FROM shares WHERE id = ?').get(id);
},
// 验证分享密码
verifyPassword(plainPassword, hashedPassword) {
return bcrypt.compareSync(plainPassword, hashedPassword);
},
// 获取用户的所有分享
getUserShares(userId) {
return db.prepare(`
SELECT * FROM shares
WHERE user_id = ?
ORDER BY created_at DESC
`).all(userId);
},
// 增加查看次数
incrementViewCount(shareCode) {
return db.prepare(`
UPDATE shares
SET view_count = view_count + 1
WHERE share_code = ?
`).run(shareCode);
},
// 增加下载次数
incrementDownloadCount(shareCode) {
return db.prepare(`
UPDATE shares
SET download_count = download_count + 1
WHERE share_code = ?
`).run(shareCode);
},
// 删除分享
delete(id, userId = null) {
if (userId) {
return db.prepare('DELETE FROM shares WHERE id = ? AND user_id = ?').run(id, userId);
}
return db.prepare('DELETE FROM shares WHERE id = ?').run(id);
},
// 获取所有分享(管理员)
getAll() {
return db.prepare(`
SELECT s.*, u.username
FROM shares s
JOIN users u ON s.user_id = u.id
ORDER BY s.created_at DESC
`).all();
}
};
// 系统设置管理
const SettingsDB = {
// 获取设置
get(key) {
const row = db.prepare('SELECT value FROM system_settings WHERE key = ?').get(key);
return row ? row.value : null;
},
// 设置值
set(key, value) {
db.prepare(`
INSERT INTO system_settings (key, value, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(key) DO UPDATE SET
value = excluded.value,
updated_at = CURRENT_TIMESTAMP
`).run(key, value);
},
// 获取所有设置
getAll() {
return db.prepare('SELECT key, value FROM system_settings').all();
}
};
// 密码重置请求管理
const PasswordResetDB = {
// 创建密码重置请求
create(userId, newPassword) {
const hashedPassword = bcrypt.hashSync(newPassword, 10);
// 删除该用户之前的pending请求
db.prepare('DELETE FROM password_reset_requests WHERE user_id = ? AND status = ?')
.run(userId, 'pending');
const stmt = db.prepare(`
INSERT INTO password_reset_requests (user_id, new_password, status)
VALUES (?, ?, 'pending')
`);
const result = stmt.run(userId, hashedPassword);
return result.lastInsertRowid;
},
// 获取待审核的请求
getPending() {
return db.prepare(`
SELECT r.*, u.username, u.email
FROM password_reset_requests r
JOIN users u ON r.user_id = u.id
WHERE r.status = 'pending'
ORDER BY r.created_at DESC
`).all();
},
// 审核请求(批准或拒绝)
review(requestId, adminId, approved) {
const request = db.prepare('SELECT * FROM password_reset_requests WHERE id = ?').get(requestId);
if (!request || request.status !== 'pending') {
throw new Error('请求不存在或已被处理');
}
const newStatus = approved ? 'approved' : 'rejected';
db.prepare(`
UPDATE password_reset_requests
SET status = ?, reviewed_at = CURRENT_TIMESTAMP, reviewed_by = ?
WHERE id = ?
`).run(newStatus, adminId, requestId);
// 如果批准,更新用户密码
if (approved) {
db.prepare('UPDATE users SET password = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
.run(request.new_password, request.user_id);
}
return true;
},
// 获取用户的所有请求
getUserRequests(userId) {
return db.prepare(`
SELECT * FROM password_reset_requests
WHERE user_id = ?
ORDER BY created_at DESC
`).all(userId);
},
// 检查用户是否有待处理的请求
hasPendingRequest(userId) {
const request = db.prepare(`
SELECT id FROM password_reset_requests
WHERE user_id = ? AND status = 'pending'
`).get(userId);
return !!request;
}
};
// 初始化默认设置
function initDefaultSettings() {
// 默认上传限制为100MB
if (!SettingsDB.get('max_upload_size')) {
SettingsDB.set('max_upload_size', '104857600'); // 100MB in bytes
}
}
// 数据库版本迁移 - v2.0 本地存储功能
function migrateToV2() {
try {
const columns = db.prepare("PRAGMA table_info(users)").all();
const hasStoragePermission = columns.some(col => col.name === 'storage_permission');
if (!hasStoragePermission) {
console.log('[数据库迁移] 检测到旧版本数据库,开始升级到 v2.0...');
// 添加本地存储相关字段
db.exec(`
ALTER TABLE users ADD COLUMN storage_permission TEXT DEFAULT 'sftp_only';
ALTER TABLE users ADD COLUMN current_storage_type TEXT DEFAULT 'sftp';
ALTER TABLE users ADD COLUMN local_storage_quota INTEGER DEFAULT 1073741824;
ALTER TABLE users ADD COLUMN local_storage_used INTEGER DEFAULT 0;
`);
// 更新现有用户为SFTP模式保持兼容
const updateStmt = db.prepare("UPDATE users SET current_storage_type = 'sftp' WHERE has_ftp_config = 1");
updateStmt.run();
console.log('[数据库迁移] ✓ 用户表已升级');
// 为分享表添加存储类型字段
const shareColumns = db.prepare("PRAGMA table_info(shares)").all();
const hasShareStorageType = shareColumns.some(col => col.name === 'storage_type');
if (!hasShareStorageType) {
db.exec(`ALTER TABLE shares ADD COLUMN storage_type TEXT DEFAULT 'sftp';`);
console.log('[数据库迁移] ✓ 分享表已升级');
}
console.log('[数据库迁移] ✅ 数据库升级到 v2.0 完成!本地存储功能已启用');
}
} catch (error) {
console.error('[数据库迁移] 迁移失败:', error);
throw error;
}
}
// 初始化数据库
initDatabase();
createDefaultAdmin();
initDefaultSettings();
migrateToV2(); // 执行数据库迁移
module.exports = {
db,
UserDB,
ShareDB,
SettingsDB,
PasswordResetDB
};

3010
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
backend/package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "ftp-web-manager-backend",
"version": "1.0.0",
"description": "FTP Web Manager Backend",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"keywords": [
"ftp",
"web",
"file-manager"
],
"author": "",
"license": "MIT",
"dependencies": {
"archiver": "^7.0.1",
"basic-ftp": "^5.0.4",
"bcryptjs": "^3.0.3",
"better-sqlite3": "^12.4.1",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-validator": "^7.3.0",
"jsonwebtoken": "^9.0.2",
"multer": "^2.0.2",
"ssh2-sftp-client": "^12.0.1"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}

2113
backend/server.js Normal file

File diff suppressed because it is too large Load Diff

10
backend/start.bat Normal file
View File

@@ -0,0 +1,10 @@
@echo off
echo ========================================
echo FTP 网盘管理平台 - 启动脚本
echo ========================================
echo.
cd /d %~dp0
node server.js
pause

321
backend/storage.js Normal file
View File

@@ -0,0 +1,321 @@
const SftpClient = require('ssh2-sftp-client');
const fs = require('fs');
const path = require('path');
const { UserDB } = require('./database');
// ===== 统一存储接口 =====
/**
* 存储接口工厂
* 根据用户的存储类型返回对应的存储客户端
*/
class StorageInterface {
constructor(user) {
this.user = user;
this.type = user.current_storage_type || 'sftp';
}
/**
* 创建并返回存储客户端
*/
async connect() {
if (this.type === 'local') {
const client = new LocalStorageClient(this.user);
await client.init();
return client;
} else {
const client = new SftpStorageClient(this.user);
await client.connect();
return client;
}
}
}
// ===== 本地存储客户端 =====
class LocalStorageClient {
constructor(user) {
this.user = user;
// 使用环境变量或默认路径(不硬编码)
const storageRoot = process.env.STORAGE_ROOT || path.join(__dirname, 'storage');
this.basePath = path.join(storageRoot, `user_${user.id}`);
}
/**
* 初始化用户存储目录
*/
async init() {
if (!fs.existsSync(this.basePath)) {
fs.mkdirSync(this.basePath, { recursive: true, mode: 0o755 });
console.log(`[本地存储] 创建用户目录: ${this.basePath}`);
}
}
/**
* 列出目录内容
*/
async list(dirPath) {
const fullPath = this.getFullPath(dirPath);
// 确保目录存在
if (!fs.existsSync(fullPath)) {
fs.mkdirSync(fullPath, { recursive: true });
return [];
}
const items = fs.readdirSync(fullPath, { withFileTypes: true });
return items.map(item => {
const itemPath = path.join(fullPath, item.name);
const stats = fs.statSync(itemPath);
return {
name: item.name,
type: item.isDirectory() ? 'd' : '-',
size: stats.size,
modifyTime: stats.mtimeMs
};
});
}
/**
* 上传文件
*/
async put(localPath, remotePath) {
const destPath = this.getFullPath(remotePath);
// 检查配额
const fileSize = fs.statSync(localPath).size;
this.checkQuota(fileSize);
// 确保目标目录存在
const destDir = path.dirname(destPath);
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
}
// 使用临时文件+重命名模式,避免文件被占用问题
const tempPath = `${destPath}.uploading_${Date.now()}`;
try {
// 复制到临时文件
fs.copyFileSync(localPath, tempPath);
// 如果目标文件存在,先删除
if (fs.existsSync(destPath)) {
fs.unlinkSync(destPath);
}
// 重命名临时文件为目标文件
fs.renameSync(tempPath, destPath);
// 更新已使用空间
this.updateUsedSpace(fileSize);
} catch (error) {
// 清理临时文件
if (fs.existsSync(tempPath)) {
fs.unlinkSync(tempPath);
}
throw error;
}
}
/**
* 删除文件
*/
async delete(filePath) {
const fullPath = this.getFullPath(filePath);
const stats = fs.statSync(fullPath);
fs.unlinkSync(fullPath);
// 更新已使用空间
this.updateUsedSpace(-stats.size);
}
/**
* 重命名文件
*/
async rename(oldPath, newPath) {
const oldFullPath = this.getFullPath(oldPath);
const newFullPath = this.getFullPath(newPath);
// 确保新路径的目录存在
const newDir = path.dirname(newFullPath);
if (!fs.existsSync(newDir)) {
fs.mkdirSync(newDir, { recursive: true });
}
fs.renameSync(oldFullPath, newFullPath);
}
/**
* 获取文件信息
*/
async stat(filePath) {
const fullPath = this.getFullPath(filePath);
return fs.statSync(fullPath);
}
/**
* 创建文件读取流
*/
createReadStream(filePath) {
const fullPath = this.getFullPath(filePath);
return fs.createReadStream(fullPath);
}
/**
* 关闭连接(本地存储无需关闭)
*/
async end() {
// 本地存储无需关闭连接
}
// ===== 辅助方法 =====
/**
* 获取完整路径(带安全检查)
*/
getFullPath(relativePath) {
// 1. 规范化路径,移除 ../ 等危险路径
const normalized = path.normalize(relativePath).replace(/^(\.\.[\/\\])+/, '');
// 2. 拼接完整路径
const fullPath = path.join(this.basePath, normalized);
// 3. 安全检查:确保路径在用户目录内(防止目录遍历攻击)
if (!fullPath.startsWith(this.basePath)) {
throw new Error('非法路径访问');
}
return fullPath;
}
/**
* 检查配额
*/
checkQuota(additionalSize) {
const newUsed = (this.user.local_storage_used || 0) + additionalSize;
if (newUsed > this.user.local_storage_quota) {
const used = this.formatSize(this.user.local_storage_used);
const quota = this.formatSize(this.user.local_storage_quota);
const need = this.formatSize(additionalSize);
throw new Error(`存储配额不足。已使用: ${used}, 配额: ${quota}, 需要: ${need}`);
}
}
/**
* 更新已使用空间
*/
updateUsedSpace(delta) {
const newUsed = Math.max(0, (this.user.local_storage_used || 0) + delta);
UserDB.update(this.user.id, { local_storage_used: newUsed });
// 更新内存中的值
this.user.local_storage_used = newUsed;
}
/**
* 格式化文件大小
*/
formatSize(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];
}
}
// ===== SFTP存储客户端 =====
class SftpStorageClient {
constructor(user) {
this.user = user;
this.sftp = new SftpClient();
}
/**
* 连接SFTP服务器
*/
async connect() {
await this.sftp.connect({
host: this.user.ftp_host,
port: this.user.ftp_port || 22,
username: this.user.ftp_user,
password: this.user.ftp_password
});
return this;
}
/**
* 列出目录内容
*/
async list(dirPath) {
return await this.sftp.list(dirPath);
}
/**
* 上传文件
*/
async put(localPath, remotePath) {
// 使用临时文件+重命名模式与upload_tool保持一致
const tempRemotePath = `${remotePath}.uploading_${Date.now()}`;
// 第一步:上传到临时文件
await this.sftp.put(localPath, tempRemotePath);
// 第二步:检查目标文件是否存在,如果存在先删除
try {
await this.sftp.stat(remotePath);
await this.sftp.delete(remotePath);
} catch (err) {
// 文件不存在,无需删除
}
// 第三步:重命名临时文件为目标文件
await this.sftp.rename(tempRemotePath, remotePath);
}
/**
* 删除文件
*/
async delete(filePath) {
return await this.sftp.delete(filePath);
}
/**
* 重命名文件
*/
async rename(oldPath, newPath) {
return await this.sftp.rename(oldPath, newPath);
}
/**
* 获取文件信息
*/
async stat(filePath) {
return await this.sftp.stat(filePath);
}
/**
* 创建文件读取流
*/
createReadStream(filePath) {
return this.sftp.createReadStream(filePath);
}
/**
* 关闭连接
*/
async end() {
if (this.sftp) {
await this.sftp.end();
}
}
}
module.exports = {
StorageInterface,
LocalStorageClient,
SftpStorageClient
};