diff --git a/backend/server.js b/backend/server.js index b9ef94b..0600653 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1896,7 +1896,9 @@ app.post('/api/login', storage_permission: user.storage_permission || 'oss_only', current_storage_type: user.current_storage_type || 'oss', local_storage_quota: user.local_storage_quota || 1073741824, - local_storage_used: user.local_storage_used || 0 + local_storage_used: user.local_storage_used || 0, + // OSS配置来源(重要:用于前端判断是否使用OSS直连上传) + oss_config_source: SettingsDB.hasUnifiedOssConfig() ? 'unified' : (user.has_oss_config ? 'personal' : 'none') } }); } catch (error) { @@ -2201,8 +2203,9 @@ app.post('/api/user/test-oss', // ===== P0 性能优化:优先使用数据库缓存,避免全量统计 ===== app.get('/api/user/oss-usage', authMiddleware, async (req, res) => { try { - // 检查用户是否配置了 OSS - if (!req.user.has_oss_config) { + // 检查是否有可用的OSS配置(个人配置或系统级统一配置) + const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig(); + if (!req.user.has_oss_config && !hasUnifiedConfig) { return res.status(400).json({ success: false, message: '未配置OSS服务' @@ -2238,8 +2241,9 @@ app.get('/api/user/oss-usage', authMiddleware, async (req, res) => { // ===== P0 性能优化:此接口较慢,建议只在必要时调用 ===== app.get('/api/user/oss-usage-full', authMiddleware, async (req, res) => { try { - // 检查用户是否配置了 OSS - if (!req.user.has_oss_config) { + // 检查是否有可用的OSS配置(个人配置或系统级统一配置) + const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig(); + if (!req.user.has_oss_config && !hasUnifiedConfig) { return res.status(400).json({ success: false, message: '未配置OSS服务' @@ -2517,12 +2521,15 @@ app.post('/api/user/switch-storage', }); } - // 检查OSS配置 - if (storage_type === 'oss' && !req.user.has_oss_config) { - return res.status(400).json({ - success: false, - message: '请先配置OSS服务' - }); + // 检查OSS配置(包括个人配置和系统级统一配置) + if (storage_type === 'oss') { + const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig(); + if (!req.user.has_oss_config && !hasUnifiedConfig) { + return res.status(400).json({ + success: false, + message: 'OSS服务未配置,请联系管理员配置系统级OSS服务' + }); + } } // 更新存储类型 @@ -3020,8 +3027,9 @@ app.get('/api/files/upload-signature', authMiddleware, async (req, res) => { }); } - // 检查用户是否配置了 OSS - if (!req.user.has_oss_config) { + // 检查用户是否配置了 OSS(包括个人配置和系统级统一配置) + const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig(); + if (!req.user.has_oss_config && !hasUnifiedConfig) { return res.status(400).json({ success: false, message: '未配置 OSS 服务' @@ -3032,6 +3040,10 @@ app.get('/api/files/upload-signature', authMiddleware, async (req, res) => { const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3'); const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); + // 获取有效的 OSS 配置(系统配置优先) + const unifiedConfig = SettingsDB.getUnifiedOssConfig(); + const effectiveBucket = unifiedConfig ? unifiedConfig.bucket : req.user.oss_bucket; + // 构建 S3 客户端 const client = new S3Client(buildS3Config(req.user)); @@ -3056,7 +3068,7 @@ app.get('/api/files/upload-signature', authMiddleware, async (req, res) => { // 创建 PutObject 命令 const command = new PutObjectCommand({ - Bucket: req.user.oss_bucket, + Bucket: effectiveBucket, Key: objectKey, ContentType: contentType }); @@ -3098,6 +3110,15 @@ app.post('/api/files/upload-complete', authMiddleware, async (req, res) => { }); } + // 安全检查:验证用户是否配置了OSS(个人配置或系统级统一配置) + const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig(); + if (!req.user.has_oss_config && !hasUnifiedConfig) { + return res.status(400).json({ + success: false, + message: '未配置OSS服务,无法完成上传' + }); + } + try { // 更新存储使用量缓存(增量更新) await StorageUsageCache.updateUsage(req.user.id, size); @@ -3139,8 +3160,9 @@ app.get('/api/files/download-url', authMiddleware, async (req, res) => { }); } - // 检查用户是否配置了 OSS - if (!req.user.has_oss_config) { + // 检查用户是否配置了 OSS(包括个人配置和系统级统一配置) + const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig(); + if (!req.user.has_oss_config && !hasUnifiedConfig) { return res.status(400).json({ success: false, message: '未配置 OSS 服务' @@ -3151,15 +3173,20 @@ app.get('/api/files/download-url', authMiddleware, async (req, res) => { const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3'); const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); + // 获取有效的 OSS 配置(系统配置优先) + const unifiedConfig = SettingsDB.getUnifiedOssConfig(); + const effectiveBucket = unifiedConfig ? unifiedConfig.bucket : req.user.oss_bucket; + // 构建 S3 客户端 const client = new S3Client(buildS3Config(req.user)); - // 构建对象 Key(使用安全的规范化路径) - const objectKey = `user_${req.user.id}${normalizedPath}`; + // 构建对象 Key(复用 OssStorageClient 的 getObjectKey 方法,确保路径格式正确) + const tempClient = new OssStorageClient(req.user); + const objectKey = tempClient.getObjectKey(normalizedPath); // 创建 GetObject 命令 const command = new GetObjectCommand({ - Bucket: req.user.oss_bucket, + Bucket: effectiveBucket, Key: objectKey, ResponseContentDisposition: `attachment; filename="${encodeURIComponent(filePath.split('/').pop())}"` }); @@ -3186,7 +3213,8 @@ function buildS3Config(user) { // 创建临时 OssStorageClient 实例并复用其 buildConfig 方法 // OssStorageClient 已在文件顶部导入 const tempClient = new OssStorageClient(user); - return tempClient.buildConfig(); + const config = tempClient.getEffectiveConfig(); // 先获取有效配置(系统配置优先) + return tempClient.buildConfig(config); // 然后传递给buildConfig } // 辅助函数:清理文件名(增强版安全处理) @@ -4286,8 +4314,9 @@ app.get('/api/share/:code/download-url', shareRateLimitMiddleware, async (req, r }); } - // 检查是否使用 OSS - if (!shareOwner.has_oss_config || share.storage_type !== 'oss') { + // 检查是否使用 OSS(包括个人配置和系统级统一配置) + const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig(); + if (!shareOwner.has_oss_config && !hasUnifiedConfig) { // 本地存储模式:返回后端下载 URL return res.json({ success: true, @@ -4300,15 +4329,20 @@ app.get('/api/share/:code/download-url', shareRateLimitMiddleware, async (req, r const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3'); const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); + // 获取有效的 OSS 配置(系统配置优先) + const unifiedConfig = SettingsDB.getUnifiedOssConfig(); + const effectiveBucket = unifiedConfig ? unifiedConfig.bucket : shareOwner.oss_bucket; + // 构建 S3 客户端 const client = new S3Client(buildS3Config(shareOwner)); - // 构建对象 Key - const objectKey = `user_${shareOwner.id}${filePath}`; + // 构建对象 Key(复用 OssStorageClient 的 getObjectKey 方法,确保路径格式正确) + const tempClient = new OssStorageClient(shareOwner); + const objectKey = tempClient.getObjectKey(filePath); // 创建 GetObject 命令 const command = new GetObjectCommand({ - Bucket: shareOwner.oss_bucket, + Bucket: effectiveBucket, Key: objectKey, ResponseContentDisposition: `attachment; filename="${encodeURIComponent(filePath.split('/').pop())}"` }); @@ -5342,7 +5376,9 @@ app.get('/api/admin/storage-cache/check/:userId', }); } - if (!user.has_oss_config || !user.oss_bucket) { + // 检查是否有可用的OSS配置(个人配置或系统级统一配置) + const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig(); + if (!user.has_oss_config && !hasUnifiedConfig) { return res.json({ success: true, message: '用户未配置 OSS,无需检查', @@ -5405,7 +5441,9 @@ app.post('/api/admin/storage-cache/rebuild/:userId', }); } - if (!user.has_oss_config || !user.oss_bucket) { + // 检查是否有可用的OSS配置(个人配置或系统级统一配置) + const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig(); + if (!user.has_oss_config && !hasUnifiedConfig) { return res.status(400).json({ success: false, message: '用户未配置 OSS' @@ -5508,9 +5546,12 @@ app.post('/api/admin/storage-cache/auto-fix', const users = UserDB.getAll(); const fixResults = []; + // 检查是否有系统级统一OSS配置 + const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig(); + for (const user of users) { - // 跳过没有配置 OSS 的用户 - if (!user.has_oss_config || !user.oss_bucket) { + // 跳过没有配置 OSS 的用户(个人配置或系统级配置都没有) + if (!user.has_oss_config && !hasUnifiedConfig) { continue; } @@ -5845,8 +5886,9 @@ app.post('/api/admin/users/:id/storage-permission', if (storage_permission === 'local_only') { updates.current_storage_type = 'local'; } else if (storage_permission === 'oss_only') { - // 只有配置了OSS才切换到OSS - if (user.has_oss_config) { + // 只有配置了OSS才切换到OSS(个人配置或系统级统一配置) + const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig(); + if (user.has_oss_config || hasUnifiedConfig) { updates.current_storage_type = 'oss'; } } @@ -5895,7 +5937,9 @@ app.get('/api/admin/users/:id/files', authMiddleware, adminMiddleware, async (re }); } - if (!user.has_oss_config) { + // 检查是否有可用的OSS配置(个人配置或系统级统一配置) + const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig(); + if (!user.has_oss_config && !hasUnifiedConfig) { return res.status(400).json({ success: false, message: '该用户未配置OSS服务' diff --git a/backend/storage.js b/backend/storage.js index bea2779..9091d98 100644 --- a/backend/storage.js +++ b/backend/storage.js @@ -820,7 +820,11 @@ class OssStorageClient { httpsAgent: { timeout: 30000 } }, // 重试配置 - maxAttempts: 3 + maxAttempts: 3, + // 禁用AWS特定的计算功能,提升阿里云OSS兼容性 + disableNormalizeBucketName: true, + // 禁用MD5校验和计算(阿里云OSS不完全支持AWS的checksum特性) + checksumValidation: false }; // 阿里云 OSS @@ -841,6 +845,9 @@ class OssStorageClient { } // 阿里云 OSS 使用 virtual-hosted-style,但需要设置 forcePathStyle 为 false s3Config.forcePathStyle = false; + // 阿里云OSS特定配置:禁用AWS特定的计算功能 + s3Config.disableNormalizeBucketName = true; + s3Config.checksumValidation = false; } // 腾讯云 COS else if (oss_provider === 'tencent') { @@ -1083,8 +1090,6 @@ class OssStorageClient { * @param {string} remotePath - 远程文件路径 */ async put(localPath, remotePath) { - let fileStream = null; - try { const key = this.getObjectKey(remotePath); const bucket = this.getBucket(); @@ -1103,20 +1108,18 @@ class OssStorageClient { throw new Error(`文件过大 (${formatFileSize(fileSize)}),单次上传最大支持 5GB,请使用分片上传`); } - // 创建文件读取流 - fileStream = fs.createReadStream(localPath); - - // 处理流错误 - fileStream.on('error', (err) => { - console.error(`[OSS存储] 文件流读取错误: ${localPath}`, err.message); - }); + // 使用Buffer上传而非流式上传,避免AWS SDK使用aws-chunked编码 + // 这样可以确保与阿里云OSS的兼容性 + const fileContent = fs.readFileSync(localPath); // 直接上传 const command = new PutObjectCommand({ Bucket: bucket, Key: key, - Body: fileStream, - ContentLength: fileSize // 明确指定内容长度,避免某些服务端问题 + Body: fileContent, + ContentLength: fileSize, // 明确指定内容长度,避免某些服务端问题 + // 禁用checksum算法(阿里云OSS不完全支持AWS的x-amz-content-sha256头) + ChecksumAlgorithm: undefined }); await this.s3Client.send(command); @@ -1136,11 +1139,6 @@ class OssStorageClient { throw new Error(`本地文件不存在: ${localPath}`); } throw new Error(`文件上传失败: ${error.message}`); - } finally { - // 确保流被正确关闭(无论成功还是失败) - if (fileStream && !fileStream.destroyed) { - fileStream.destroy(); - } } } @@ -1258,7 +1256,7 @@ class OssStorageClient { async rename(oldPath, newPath) { const oldKey = this.getObjectKey(oldPath); const newKey = this.getObjectKey(newPath); - const bucket = this.user.oss_bucket; + const bucket = this.getBucket(); // 使用getBucket()方法以支持系统级统一OSS配置 // 验证源和目标不同 if (oldKey === newKey) { @@ -1293,13 +1291,11 @@ class OssStorageClient { await this.s3Client.send(copyCommand); copySuccess = true; - // 复制成功后删除原文件 - const deleteCommand = new DeleteObjectsCommand({ + // 复制成功后删除原文件(使用DeleteObjectCommand删除单个文件) + const { DeleteObjectCommand } = require('@aws-sdk/client-s3'); + const deleteCommand = new DeleteObjectCommand({ Bucket: bucket, - Delete: { - Objects: [{ Key: oldKey }], - Quiet: true - } + Key: oldKey }); await this.s3Client.send(deleteCommand); @@ -1311,12 +1307,10 @@ class OssStorageClient { if (copySuccess) { try { console.log(`[OSS存储] 尝试回滚:删除已复制的文件 ${newKey}`); - const deleteCommand = new DeleteObjectsCommand({ + const { DeleteObjectCommand } = require('@aws-sdk/client-s3'); + const deleteCommand = new DeleteObjectCommand({ Bucket: bucket, - Delete: { - Objects: [{ Key: newKey }], - Quiet: true - } + Key: newKey }); await this.s3Client.send(deleteCommand); console.log(`[OSS存储] 回滚成功:已删除 ${newKey}`); @@ -1500,7 +1494,7 @@ class OssStorageClient { */ async stat(filePath) { const key = this.getObjectKey(filePath); - const bucket = this.user.oss_bucket; + const bucket = this.getBucket(); // 使用getBucket()方法以支持系统级统一OSS配置 try { const command = new HeadObjectCommand({ diff --git a/backend/utils/storage-cache.js b/backend/utils/storage-cache.js index bf45dd4..df9702b 100644 --- a/backend/utils/storage-cache.js +++ b/backend/utils/storage-cache.js @@ -83,7 +83,14 @@ class StorageUsageCache { */ static async resetUsage(userId, totalSize) { try { - UserDB.update(userId, { storage_used: totalSize }); + // 使用直接SQL更新,绕过UserDB.update()的字段白名单限制 + const { db } = require('../database'); + db.prepare(` + UPDATE users + SET storage_used = ? + WHERE id = ? + `).run(totalSize, userId); + console.log(`[存储缓存] 用户 ${userId} 存储重置: ${totalSize} 字节`); return true; } catch (error) { @@ -114,7 +121,7 @@ class StorageUsageCache { do { const command = new ListObjectsV2Command({ - Bucket: user.oss_bucket, + Bucket: ossClient.getBucket(), // 使用ossClient的getBucket()方法以支持系统级统一OSS配置 Prefix: `user_${userId}/`, ContinuationToken: continuationToken }); @@ -172,7 +179,7 @@ class StorageUsageCache { do { const command = new ListObjectsV2Command({ - Bucket: user.oss_bucket, + Bucket: ossClient.getBucket(), // 使用ossClient的getBucket()方法以支持系统级统一OSS配置 Prefix: `user_${userId}/`, ContinuationToken: continuationToken }); @@ -233,7 +240,7 @@ class StorageUsageCache { do { const command = new ListObjectsV2Command({ - Bucket: user.oss_bucket, + Bucket: ossClient.getBucket(), // 使用ossClient的getBucket()方法以支持系统级统一OSS配置 Prefix: `user_${userId}/`, ContinuationToken: continuationToken }); @@ -278,8 +285,10 @@ class StorageUsageCache { const results = []; for (const user of users) { - // 跳过没有配置 OSS 的用户 - if (!user.has_oss_config || !user.oss_bucket) { + // 跳过没有配置 OSS 的用户(需要检查系统级统一配置) + const { SettingsDB } = require('../database'); + const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig(); + if (!user.has_oss_config && !hasUnifiedConfig) { continue; } diff --git a/frontend/app.html b/frontend/app.html index 791a921..d6dd197 100644 --- a/frontend/app.html +++ b/frontend/app.html @@ -1782,14 +1782,16 @@ 当前