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

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
};