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 newFileSize = fs.statSync(localPath).size; // 如果目标文件存在,计算实际需要的额外空间 let oldFileSize = 0; if (fs.existsSync(destPath)) { try { oldFileSize = fs.statSync(destPath).size; } catch (err) { // 文件可能已被删除,忽略错误 } } // 检查配额:只检查净增量(新文件大小 - 旧文件大小) const netIncrease = newFileSize - oldFileSize; if (netIncrease > 0) { this.checkQuota(netIncrease); } // 确保目标目录存在 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); // 更新已使用空间(使用净增量) if (netIncrease !== 0) { this.updateUsedSpace(netIncrease); } } 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 };