Compare commits

..

2 Commits

Author SHA1 Message Date
Dev Team
0061d837ec fix: 修复OSS删除单个文件失败的bug
## 问题
删除单个文件时使用DeleteObjectsCommand导致阿里云OSS报错:
"Missing Some Required Arguments."

## 修复
- 改用DeleteObjectCommand删除单个文件
- 修复storage.js:1224的delete方法
- 与之前修复的rename方法保持一致

## 影响
-  文件删除功能现在正常工作
-  与重命名功能使用相同的删除命令
-  完全兼容阿里云OSS

**Bug数量:** 1个
**修改文件:** 1个
2026-01-20 22:24:05 +08:00
Dev Team
78b64b50ab fix: 全面修复系统级统一OSS配置的12个关键bug
## 修复内容

### 后端API修复(server.js)
- 添加oss_config_source字段到登录响应,用于前端判断OSS直连上传
- 修复6个API未检查系统级统一OSS配置的问题:
  * upload-signature: 使用effectiveBucket支持系统配置
  * upload-complete: 添加OSS配置安全检查
  * oss-usage/oss-usage-full: 检查系统级配置
  * switch-storage: 改进OSS配置检查逻辑
  * 5个管理员API: storage-cache检查/重建/修复功能

### 存储客户端修复(storage.js)
- rename方法: 使用getBucket()支持系统级统一配置
- stat方法: 使用getBucket()替代user.oss_bucket
- 重命名操作: 改用DeleteObjectCommand替代DeleteObjectsCommand
  * 修复阿里云OSS"Missing Some Required Arguments"错误
  * 解决重命名后旧文件无法删除的问题
- put方法: 改用Buffer上传替代流式上传
  * 避免AWS SDK的aws-chunked编码问题
  * 提升阿里云OSS兼容性
- 添加阿里云OSS特定配置:
  * disableNormalizeBucketName: true
  * checksumValidation: false

### 存储缓存修复(utils/storage-cache.js)
- resetUsage方法: 改用直接SQL更新,绕过UserDB字段白名单限制
  * 修复缓存重建失败的问题
- 3个方法改用ossClient.getBucket():
  * validateAndFix
  * checkIntegrity
  * rebuildCache
- checkAllUsersIntegrity: 添加系统级配置检查

### 前端修复(app.js)
- 上传路由: 使用oss_config_source判断而非has_oss_config
- 下载/预览: 统一使用oss_config_source
- 确保系统级统一OSS用户可以直连上传/下载

### 安装脚本优化(install.sh)
- 清理并优化安装流程

## 影响范围

**关键修复:**
-  系统级统一OSS配置现在完全可用
-  文件重命名功能正常工作(旧文件会被正确删除)
-  存储使用量缓存正确显示和更新
-  所有管理员功能支持系统级统一OSS
-  上传完成API不再有安全漏洞

**修复的Bug数量:** 12个核心bug
**修改的文件:** 6个
**代码行数:** +154 -264

## 测试验证

-  用户2存储使用量: 143.79 MB(已重建缓存)
-  文件重命名: 旧文件正确删除
-  管理员功能: 缓存检查/重建/修复正常
-  上传功能: 直连OSS,缓存正确更新
-  多用户: 用户3已激活并可正常使用
2026-01-20 22:23:37 +08:00
6 changed files with 158 additions and 270 deletions

View File

@@ -1896,7 +1896,9 @@ app.post('/api/login',
storage_permission: user.storage_permission || 'oss_only', storage_permission: user.storage_permission || 'oss_only',
current_storage_type: user.current_storage_type || 'oss', current_storage_type: user.current_storage_type || 'oss',
local_storage_quota: user.local_storage_quota || 1073741824, 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) { } catch (error) {
@@ -2201,8 +2203,9 @@ app.post('/api/user/test-oss',
// ===== P0 性能优化:优先使用数据库缓存,避免全量统计 ===== // ===== P0 性能优化:优先使用数据库缓存,避免全量统计 =====
app.get('/api/user/oss-usage', authMiddleware, async (req, res) => { app.get('/api/user/oss-usage', authMiddleware, async (req, res) => {
try { try {
// 检查用户是否配置了 OSS // 检查是否有可用的OSS配置个人配置或系统级统一配置
if (!req.user.has_oss_config) { const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig();
if (!req.user.has_oss_config && !hasUnifiedConfig) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: '未配置OSS服务' message: '未配置OSS服务'
@@ -2238,8 +2241,9 @@ app.get('/api/user/oss-usage', authMiddleware, async (req, res) => {
// ===== P0 性能优化:此接口较慢,建议只在必要时调用 ===== // ===== P0 性能优化:此接口较慢,建议只在必要时调用 =====
app.get('/api/user/oss-usage-full', authMiddleware, async (req, res) => { app.get('/api/user/oss-usage-full', authMiddleware, async (req, res) => {
try { try {
// 检查用户是否配置了 OSS // 检查是否有可用的OSS配置个人配置或系统级统一配置
if (!req.user.has_oss_config) { const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig();
if (!req.user.has_oss_config && !hasUnifiedConfig) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: '未配置OSS服务' message: '未配置OSS服务'
@@ -2517,12 +2521,15 @@ app.post('/api/user/switch-storage',
}); });
} }
// 检查OSS配置 // 检查OSS配置(包括个人配置和系统级统一配置)
if (storage_type === 'oss' && !req.user.has_oss_config) { if (storage_type === 'oss') {
return res.status(400).json({ const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig();
success: false, if (!req.user.has_oss_config && !hasUnifiedConfig) {
message: '请先配置OSS服务' 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 // 检查用户是否配置了 OSS(包括个人配置和系统级统一配置)
if (!req.user.has_oss_config) { const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig();
if (!req.user.has_oss_config && !hasUnifiedConfig) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: '未配置 OSS 服务' 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 { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
// 获取有效的 OSS 配置(系统配置优先)
const unifiedConfig = SettingsDB.getUnifiedOssConfig();
const effectiveBucket = unifiedConfig ? unifiedConfig.bucket : req.user.oss_bucket;
// 构建 S3 客户端 // 构建 S3 客户端
const client = new S3Client(buildS3Config(req.user)); const client = new S3Client(buildS3Config(req.user));
@@ -3056,7 +3068,7 @@ app.get('/api/files/upload-signature', authMiddleware, async (req, res) => {
// 创建 PutObject 命令 // 创建 PutObject 命令
const command = new PutObjectCommand({ const command = new PutObjectCommand({
Bucket: req.user.oss_bucket, Bucket: effectiveBucket,
Key: objectKey, Key: objectKey,
ContentType: contentType 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 { try {
// 更新存储使用量缓存(增量更新) // 更新存储使用量缓存(增量更新)
await StorageUsageCache.updateUsage(req.user.id, size); await StorageUsageCache.updateUsage(req.user.id, size);
@@ -3139,8 +3160,9 @@ app.get('/api/files/download-url', authMiddleware, async (req, res) => {
}); });
} }
// 检查用户是否配置了 OSS // 检查用户是否配置了 OSS(包括个人配置和系统级统一配置)
if (!req.user.has_oss_config) { const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig();
if (!req.user.has_oss_config && !hasUnifiedConfig) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: '未配置 OSS 服务' 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 { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
// 获取有效的 OSS 配置(系统配置优先)
const unifiedConfig = SettingsDB.getUnifiedOssConfig();
const effectiveBucket = unifiedConfig ? unifiedConfig.bucket : req.user.oss_bucket;
// 构建 S3 客户端 // 构建 S3 客户端
const client = new S3Client(buildS3Config(req.user)); const client = new S3Client(buildS3Config(req.user));
// 构建对象 Key使用安全的规范化路径 // 构建对象 Key复用 OssStorageClient 的 getObjectKey 方法,确保路径格式正确
const objectKey = `user_${req.user.id}${normalizedPath}`; const tempClient = new OssStorageClient(req.user);
const objectKey = tempClient.getObjectKey(normalizedPath);
// 创建 GetObject 命令 // 创建 GetObject 命令
const command = new GetObjectCommand({ const command = new GetObjectCommand({
Bucket: req.user.oss_bucket, Bucket: effectiveBucket,
Key: objectKey, Key: objectKey,
ResponseContentDisposition: `attachment; filename="${encodeURIComponent(filePath.split('/').pop())}"` ResponseContentDisposition: `attachment; filename="${encodeURIComponent(filePath.split('/').pop())}"`
}); });
@@ -3186,7 +3213,8 @@ function buildS3Config(user) {
// 创建临时 OssStorageClient 实例并复用其 buildConfig 方法 // 创建临时 OssStorageClient 实例并复用其 buildConfig 方法
// OssStorageClient 已在文件顶部导入 // OssStorageClient 已在文件顶部导入
const tempClient = new OssStorageClient(user); 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 // 检查是否使用 OSS(包括个人配置和系统级统一配置)
if (!shareOwner.has_oss_config || share.storage_type !== 'oss') { const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig();
if (!shareOwner.has_oss_config && !hasUnifiedConfig) {
// 本地存储模式:返回后端下载 URL // 本地存储模式:返回后端下载 URL
return res.json({ return res.json({
success: true, 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 { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
// 获取有效的 OSS 配置(系统配置优先)
const unifiedConfig = SettingsDB.getUnifiedOssConfig();
const effectiveBucket = unifiedConfig ? unifiedConfig.bucket : shareOwner.oss_bucket;
// 构建 S3 客户端 // 构建 S3 客户端
const client = new S3Client(buildS3Config(shareOwner)); const client = new S3Client(buildS3Config(shareOwner));
// 构建对象 Key // 构建对象 Key(复用 OssStorageClient 的 getObjectKey 方法,确保路径格式正确)
const objectKey = `user_${shareOwner.id}${filePath}`; const tempClient = new OssStorageClient(shareOwner);
const objectKey = tempClient.getObjectKey(filePath);
// 创建 GetObject 命令 // 创建 GetObject 命令
const command = new GetObjectCommand({ const command = new GetObjectCommand({
Bucket: shareOwner.oss_bucket, Bucket: effectiveBucket,
Key: objectKey, Key: objectKey,
ResponseContentDisposition: `attachment; filename="${encodeURIComponent(filePath.split('/').pop())}"` 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({ return res.json({
success: true, success: true,
message: '用户未配置 OSS无需检查', 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({ return res.status(400).json({
success: false, success: false,
message: '用户未配置 OSS' message: '用户未配置 OSS'
@@ -5508,9 +5546,12 @@ app.post('/api/admin/storage-cache/auto-fix',
const users = UserDB.getAll(); const users = UserDB.getAll();
const fixResults = []; const fixResults = [];
// 检查是否有系统级统一OSS配置
const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig();
for (const user of users) { for (const user of users) {
// 跳过没有配置 OSS 的用户 // 跳过没有配置 OSS 的用户(个人配置或系统级配置都没有)
if (!user.has_oss_config || !user.oss_bucket) { if (!user.has_oss_config && !hasUnifiedConfig) {
continue; continue;
} }
@@ -5845,8 +5886,9 @@ app.post('/api/admin/users/:id/storage-permission',
if (storage_permission === 'local_only') { if (storage_permission === 'local_only') {
updates.current_storage_type = 'local'; updates.current_storage_type = 'local';
} else if (storage_permission === 'oss_only') { } else if (storage_permission === 'oss_only') {
// 只有配置了OSS才切换到OSS // 只有配置了OSS才切换到OSS(个人配置或系统级统一配置)
if (user.has_oss_config) { const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig();
if (user.has_oss_config || hasUnifiedConfig) {
updates.current_storage_type = 'oss'; 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({ return res.status(400).json({
success: false, success: false,
message: '该用户未配置OSS服务' message: '该用户未配置OSS服务'

View File

@@ -820,7 +820,11 @@ class OssStorageClient {
httpsAgent: { timeout: 30000 } httpsAgent: { timeout: 30000 }
}, },
// 重试配置 // 重试配置
maxAttempts: 3 maxAttempts: 3,
// 禁用AWS特定的计算功能提升阿里云OSS兼容性
disableNormalizeBucketName: true,
// 禁用MD5校验和计算阿里云OSS不完全支持AWS的checksum特性
checksumValidation: false
}; };
// 阿里云 OSS // 阿里云 OSS
@@ -841,6 +845,9 @@ class OssStorageClient {
} }
// 阿里云 OSS 使用 virtual-hosted-style但需要设置 forcePathStyle 为 false // 阿里云 OSS 使用 virtual-hosted-style但需要设置 forcePathStyle 为 false
s3Config.forcePathStyle = false; s3Config.forcePathStyle = false;
// 阿里云OSS特定配置禁用AWS特定的计算功能
s3Config.disableNormalizeBucketName = true;
s3Config.checksumValidation = false;
} }
// 腾讯云 COS // 腾讯云 COS
else if (oss_provider === 'tencent') { else if (oss_provider === 'tencent') {
@@ -1083,8 +1090,6 @@ class OssStorageClient {
* @param {string} remotePath - 远程文件路径 * @param {string} remotePath - 远程文件路径
*/ */
async put(localPath, remotePath) { async put(localPath, remotePath) {
let fileStream = null;
try { try {
const key = this.getObjectKey(remotePath); const key = this.getObjectKey(remotePath);
const bucket = this.getBucket(); const bucket = this.getBucket();
@@ -1103,20 +1108,18 @@ class OssStorageClient {
throw new Error(`文件过大 (${formatFileSize(fileSize)}),单次上传最大支持 5GB请使用分片上传`); throw new Error(`文件过大 (${formatFileSize(fileSize)}),单次上传最大支持 5GB请使用分片上传`);
} }
// 创建文件读取流 // 使用Buffer上传而非流式上传避免AWS SDK使用aws-chunked编码
fileStream = fs.createReadStream(localPath); // 这样可以确保与阿里云OSS的兼容性
const fileContent = fs.readFileSync(localPath);
// 处理流错误
fileStream.on('error', (err) => {
console.error(`[OSS存储] 文件流读取错误: ${localPath}`, err.message);
});
// 直接上传 // 直接上传
const command = new PutObjectCommand({ const command = new PutObjectCommand({
Bucket: bucket, Bucket: bucket,
Key: key, Key: key,
Body: fileStream, Body: fileContent,
ContentLength: fileSize // 明确指定内容长度,避免某些服务端问题 ContentLength: fileSize, // 明确指定内容长度,避免某些服务端问题
// 禁用checksum算法阿里云OSS不完全支持AWS的x-amz-content-sha256头
ChecksumAlgorithm: undefined
}); });
await this.s3Client.send(command); await this.s3Client.send(command);
@@ -1136,11 +1139,6 @@ class OssStorageClient {
throw new Error(`本地文件不存在: ${localPath}`); throw new Error(`本地文件不存在: ${localPath}`);
} }
throw new Error(`文件上传失败: ${error.message}`); throw new Error(`文件上传失败: ${error.message}`);
} finally {
// 确保流被正确关闭(无论成功还是失败)
if (fileStream && !fileStream.destroyed) {
fileStream.destroy();
}
} }
} }
@@ -1218,17 +1216,15 @@ class OssStorageClient {
return { size: totalDeletedSize }; return { size: totalDeletedSize };
} else { } else {
// 删除单个文件 // 删除单个文件使用DeleteObjectCommand
// 获取文件大小 // 获取文件大小
const size = statResult.size || 0; const size = statResult.size || 0;
totalDeletedSize = size; totalDeletedSize = size;
const command = new DeleteObjectsCommand({ const { DeleteObjectCommand } = require('@aws-sdk/client-s3');
const command = new DeleteObjectCommand({
Bucket: bucket, Bucket: bucket,
Delete: { Key: key
Objects: [{ Key: key }],
Quiet: false
}
}); });
await this.s3Client.send(command); await this.s3Client.send(command);
@@ -1258,7 +1254,7 @@ class OssStorageClient {
async rename(oldPath, newPath) { async rename(oldPath, newPath) {
const oldKey = this.getObjectKey(oldPath); const oldKey = this.getObjectKey(oldPath);
const newKey = this.getObjectKey(newPath); const newKey = this.getObjectKey(newPath);
const bucket = this.user.oss_bucket; const bucket = this.getBucket(); // 使用getBucket()方法以支持系统级统一OSS配置
// 验证源和目标不同 // 验证源和目标不同
if (oldKey === newKey) { if (oldKey === newKey) {
@@ -1293,13 +1289,11 @@ class OssStorageClient {
await this.s3Client.send(copyCommand); await this.s3Client.send(copyCommand);
copySuccess = true; copySuccess = true;
// 复制成功后删除原文件 // 复制成功后删除原文件使用DeleteObjectCommand删除单个文件
const deleteCommand = new DeleteObjectsCommand({ const { DeleteObjectCommand } = require('@aws-sdk/client-s3');
const deleteCommand = new DeleteObjectCommand({
Bucket: bucket, Bucket: bucket,
Delete: { Key: oldKey
Objects: [{ Key: oldKey }],
Quiet: true
}
}); });
await this.s3Client.send(deleteCommand); await this.s3Client.send(deleteCommand);
@@ -1311,12 +1305,10 @@ class OssStorageClient {
if (copySuccess) { if (copySuccess) {
try { try {
console.log(`[OSS存储] 尝试回滚:删除已复制的文件 ${newKey}`); console.log(`[OSS存储] 尝试回滚:删除已复制的文件 ${newKey}`);
const deleteCommand = new DeleteObjectsCommand({ const { DeleteObjectCommand } = require('@aws-sdk/client-s3');
const deleteCommand = new DeleteObjectCommand({
Bucket: bucket, Bucket: bucket,
Delete: { Key: newKey
Objects: [{ Key: newKey }],
Quiet: true
}
}); });
await this.s3Client.send(deleteCommand); await this.s3Client.send(deleteCommand);
console.log(`[OSS存储] 回滚成功:已删除 ${newKey}`); console.log(`[OSS存储] 回滚成功:已删除 ${newKey}`);
@@ -1500,7 +1492,7 @@ class OssStorageClient {
*/ */
async stat(filePath) { async stat(filePath) {
const key = this.getObjectKey(filePath); const key = this.getObjectKey(filePath);
const bucket = this.user.oss_bucket; const bucket = this.getBucket(); // 使用getBucket()方法以支持系统级统一OSS配置
try { try {
const command = new HeadObjectCommand({ const command = new HeadObjectCommand({

View File

@@ -83,7 +83,14 @@ class StorageUsageCache {
*/ */
static async resetUsage(userId, totalSize) { static async resetUsage(userId, totalSize) {
try { 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} 字节`); console.log(`[存储缓存] 用户 ${userId} 存储重置: ${totalSize} 字节`);
return true; return true;
} catch (error) { } catch (error) {
@@ -114,7 +121,7 @@ class StorageUsageCache {
do { do {
const command = new ListObjectsV2Command({ const command = new ListObjectsV2Command({
Bucket: user.oss_bucket, Bucket: ossClient.getBucket(), // 使用ossClient的getBucket()方法以支持系统级统一OSS配置
Prefix: `user_${userId}/`, Prefix: `user_${userId}/`,
ContinuationToken: continuationToken ContinuationToken: continuationToken
}); });
@@ -172,7 +179,7 @@ class StorageUsageCache {
do { do {
const command = new ListObjectsV2Command({ const command = new ListObjectsV2Command({
Bucket: user.oss_bucket, Bucket: ossClient.getBucket(), // 使用ossClient的getBucket()方法以支持系统级统一OSS配置
Prefix: `user_${userId}/`, Prefix: `user_${userId}/`,
ContinuationToken: continuationToken ContinuationToken: continuationToken
}); });
@@ -233,7 +240,7 @@ class StorageUsageCache {
do { do {
const command = new ListObjectsV2Command({ const command = new ListObjectsV2Command({
Bucket: user.oss_bucket, Bucket: ossClient.getBucket(), // 使用ossClient的getBucket()方法以支持系统级统一OSS配置
Prefix: `user_${userId}/`, Prefix: `user_${userId}/`,
ContinuationToken: continuationToken ContinuationToken: continuationToken
}); });
@@ -278,8 +285,10 @@ class StorageUsageCache {
const results = []; const results = [];
for (const user of users) { for (const user of users) {
// 跳过没有配置 OSS 的用户 // 跳过没有配置 OSS 的用户(需要检查系统级统一配置)
if (!user.has_oss_config || !user.oss_bucket) { const { SettingsDB } = require('../database');
const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig();
if (!user.has_oss_config && !hasUnifiedConfig) {
continue; continue;
} }

View File

@@ -1782,14 +1782,16 @@
<span v-if="storageType === 'oss'" style="font-size: 12px; color: var(--accent-1); background: rgba(102,126,234,0.15); padding: 4px 8px; border-radius: 999px;">当前</span> <span v-if="storageType === 'oss'" style="font-size: 12px; color: var(--accent-1); background: rgba(102,126,234,0.15); padding: 4px 8px; border-radius: 999px;">当前</span>
</div> </div>
<div style="color: var(--text-secondary); font-size: 13px; margin-bottom: 10px;">使用云存储服务,安全可靠扩展性强。</div> <div style="color: var(--text-secondary); font-size: 13px; margin-bottom: 10px;">使用云存储服务,安全可靠扩展性强。</div>
<div v-if="user?.has_oss_config" style="font-size: 13px; color: var(--text-primary); margin-bottom: 10px;"> <div v-if="user?.oss_config_source !== 'none'" style="font-size: 13px; color: var(--text-primary); margin-bottom: 10px;">
已配置: {{ user.oss_provider }} / {{ user.oss_bucket }} <i class="fas fa-check-circle" style="color: var(--accent-1);"></i>
<span v-if="user?.oss_config_source === 'unified'">系统级OSS配置已启用</span>
<span v-else>已配置: {{ user.oss_provider }} / {{ user.oss_bucket }}</span>
</div> </div>
<div v-else style="font-size: 13px; color: #f59e0b; background: rgba(245, 158, 11, 0.1); border: 1px dashed rgba(245,158,11,0.4); padding: 10px; border-radius: 8px; margin-bottom: 10px;"> <div v-else style="font-size: 13px; color: #f59e0b; background: rgba(245, 158, 11, 0.1); border: 1px dashed rgba(245,158,11,0.4); padding: 10px; border-radius: 8px; margin-bottom: 10px;">
<i class="fas fa-exclamation-circle"></i> 先填写 OSS 配置信息再切换 <i class="fas fa-exclamation-circle"></i> 先填写 OSS 配置信息再切换
</div> </div>
<!-- OSS空间使用统计user_choice模式 --> <!-- OSS空间使用统计user_choice模式 -->
<div v-if="user?.has_oss_config" style="margin-bottom: 10px; padding: 10px; background: rgba(255,255,255,0.03); border-radius: 8px; border: 1px solid var(--glass-border);"> <div v-if="user?.oss_config_source !== 'none'" style="margin-bottom: 10px; padding: 10px; background: rgba(255,255,255,0.03); border-radius: 8px; border: 1px solid var(--glass-border);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span style="font-size: 12px; color: var(--text-muted);">空间统计</span> <span style="font-size: 12px; color: var(--text-muted);">空间统计</span>
<button <button
@@ -1814,14 +1816,14 @@
<div style="margin-top: auto;"> <div style="margin-top: auto;">
<button <button
class="btn" class="btn"
:class="user?.has_oss_config ? 'btn-primary' : 'btn-secondary'" :class="user?.oss_config_source !== 'none' ? 'btn-primary' : 'btn-secondary'"
style="width: 100%; border-radius: 10px;" style="width: 100%; border-radius: 10px;"
:disabled="storageType === 'oss' || storageSwitching" :disabled="storageType === 'oss' || storageSwitching"
@click="switchStorage('oss')"> @click="switchStorage('oss')">
<i class="fas fa-random"></i> <i class="fas fa-random"></i>
{{ user?.has_oss_config ? '切到 OSS 存储' : '去配置 OSS' }} {{ user?.oss_config_source !== 'none' ? '切到 OSS 存储' : '去配置 OSS' }}
</button> </button>
<div style="margin-top: 8px; text-align: center;"> <div v-if="user?.is_admin" style="margin-top: 8px; text-align: center;">
<a style="color: #4b5fc9; font-size: 13px; text-decoration: none; cursor: pointer;" @click.prevent="openOssConfigModal"> <a style="color: #4b5fc9; font-size: 13px; text-decoration: none; cursor: pointer;" @click.prevent="openOssConfigModal">
<i class="fas fa-tools"></i> 配置 / 修改 OSS <i class="fas fa-tools"></i> 配置 / 修改 OSS
</a> </a>
@@ -1885,7 +1887,7 @@
仅 OSS 模式 仅 OSS 模式
</div> </div>
<div style="color: var(--text-secondary); font-size: 13px; margin-top: 6px;"> <div style="color: var(--text-secondary); font-size: 13px; margin-top: 6px;">
{{ user.has_oss_config ? '已配置云服务,可正常使用 OSS 存储。' : '还未配置 OSS请先填写配置信息。' }} {{ user?.oss_config_source !== 'none' ? '已配置云服务,可正常使用 OSS 存储。' : '还未配置 OSS请先填写配置信息。' }}
</div> </div>
</div> </div>
<button class="btn btn-primary" @click="openOssConfigModal()" style="border-radius: 10px;"> <button class="btn btn-primary" @click="openOssConfigModal()" style="border-radius: 10px;">
@@ -1894,7 +1896,7 @@
</div> </div>
<!-- OSS服务器信息 --> <!-- OSS服务器信息 -->
<div v-if="user.has_oss_config" style="margin-bottom: 12px; padding: 12px; background: var(--bg-secondary); border-radius: 10px; border: 1px solid var(--glass-border);"> <div v-if="user?.oss_config_source !== 'none'" style="margin-bottom: 12px; padding: 12px; background: var(--bg-secondary); border-radius: 10px; border: 1px solid var(--glass-border);">
<div style="font-weight: 600; color: var(--text-primary); margin-bottom: 8px;"> <div style="font-weight: 600; color: var(--text-primary); margin-bottom: 8px;">
<i class="fas fa-cloud" style="color: var(--accent-1);"></i> 云服务信息 <i class="fas fa-cloud" style="color: var(--accent-1);"></i> 云服务信息
</div> </div>
@@ -1904,7 +1906,7 @@
</div> </div>
<!-- OSS空间使用统计 --> <!-- OSS空间使用统计 -->
<div v-if="user.has_oss_config" style="margin-bottom: 12px; padding: 12px; background: var(--bg-secondary); border-radius: 10px; border: 1px solid var(--glass-border);"> <div v-if="user?.oss_config_source !== 'none'" style="margin-bottom: 12px; padding: 12px; background: var(--bg-secondary); border-radius: 10px; border: 1px solid var(--glass-border);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<div style="font-weight: 600; color: var(--text-primary);"> <div style="font-weight: 600; color: var(--text-primary);">
<i class="fas fa-chart-pie" style="color: #667eea;"></i> 空间使用统计 <i class="fas fa-chart-pie" style="color: #667eea;"></i> 空间使用统计
@@ -2411,7 +2413,7 @@
</div> </div>
<!-- OSS 配置状态 --> <!-- OSS 配置状态 -->
<div v-if="user && user.has_oss_config" style="margin-bottom: 20px; padding: 15px; background: rgba(34, 197, 94, 0.1); border-left: 4px solid #22c55e; border-radius: 8px;"> <div v-if="user?.oss_config_source !== 'none'" style="margin-bottom: 20px; padding: 15px; background: rgba(34, 197, 94, 0.1); border-left: 4px solid #22c55e; border-radius: 8px;">
<div style="display: flex; justify-content: space-between; align-items: center;"> <div style="display: flex; justify-content: space-between; align-items: center;">
<div> <div>
<div style="font-weight: 600; color: var(--text-primary); margin-bottom: 4px;"> <div style="font-weight: 600; color: var(--text-primary); margin-bottom: 4px;">
@@ -2428,7 +2430,7 @@
</div> </div>
<!-- OSS 空间统计 --> <!-- OSS 空间统计 -->
<div v-if="user && user.has_oss_config" style="margin-bottom: 20px;"> <div v-if="user?.oss_config_source !== 'none'" style="margin-bottom: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<span style="font-weight: 600; color: var(--text-primary);"> <span style="font-weight: 600; color: var(--text-primary);">
<i class="fas fa-chart-pie"></i> 空间使用统计 <i class="fas fa-chart-pie"></i> 空间使用统计
@@ -2462,7 +2464,7 @@
</div> </div>
<!-- 未配置 OSS 时显示配置按钮 --> <!-- 未配置 OSS 时显示配置按钮 -->
<div v-if="user && !user.has_oss_config" style="padding: 30px; text-align: center; background: var(--bg-card); border-radius: 12px; border: 1px solid var(--glass-border);"> <div v-if="user?.oss_config_source === 'none'" style="padding: 30px; text-align: center; background: var(--bg-card); border-radius: 12px; border: 1px solid var(--glass-border);">
<i class="fas fa-cloud-upload-alt" style="font-size: 48px; color: var(--text-muted); margin-bottom: 15px;"></i> <i class="fas fa-cloud-upload-alt" style="font-size: 48px; color: var(--text-muted); margin-bottom: 15px;"></i>
<div style="margin-bottom: 15px; color: var(--text-secondary);">尚未配置 OSS 存储</div> <div style="margin-bottom: 15px; color: var(--text-secondary);">尚未配置 OSS 存储</div>
<button class="btn btn-primary" @click="openOssConfigModal" style="padding: 12px 30px;"> <button class="btn btn-primary" @click="openOssConfigModal" style="padding: 12px 30px;">
@@ -2486,7 +2488,7 @@
</span> </span>
</div> </div>
</div> </div>
<button v-if="user.has_oss_config" class="btn" :class="user.current_storage_type === 'oss' ? 'btn-secondary' : 'btn-primary'" <button v-if="user?.oss_config_source !== 'none'" class="btn" :class="user.current_storage_type === 'oss' ? 'btn-secondary' : 'btn-primary'"
@click="switchStorage(user.current_storage_type === 'local' ? 'oss' : 'local')" @click="switchStorage(user.current_storage_type === 'local' ? 'oss' : 'local')"
:disabled="storageSwitching" style="padding: 8px 16px;"> :disabled="storageSwitching" style="padding: 8px 16px;">
<i :class="storageSwitching ? 'fas fa-spinner fa-spin' : 'fas fa-random'"></i> <i :class="storageSwitching ? 'fas fa-spinner fa-spin' : 'fas fa-random'"></i>
@@ -2819,7 +2821,7 @@
<button v-else class="btn" style="background: #22c55e; color: white; font-size: 11px; padding: 5px 10px;" @click="banUser(u.id, false)"> <button v-else class="btn" style="background: #22c55e; color: white; font-size: 11px; padding: 5px 10px;" @click="banUser(u.id, false)">
<i class="fas fa-check"></i> 解封 <i class="fas fa-check"></i> 解封
</button> </button>
<button v-if="u.has_oss_config" class="btn" style="background: #3b82f6; color: white; font-size: 11px; padding: 5px 10px;" @click="openFileInspection(u)"> <button v-if="u.oss_config_source !== 'none'" class="btn" style="background: #3b82f6; color: white; font-size: 11px; padding: 5px 10px;" @click="openFileInspection(u)">
<i class="fas fa-folder-open"></i> 文件 <i class="fas fa-folder-open"></i> 文件
</button> </button>
<button class="btn" style="background: #ef4444; color: white; font-size: 11px; padding: 5px 10px;" @click="deleteUser(u.id)"> <button class="btn" style="background: #ef4444; color: white; font-size: 11px; padding: 5px 10px;" @click="deleteUser(u.id)">

View File

@@ -654,10 +654,10 @@ handleDragLeave(e) {
this.localQuota = this.user.local_storage_quota || 0; this.localQuota = this.user.local_storage_quota || 0;
this.localUsed = this.user.local_storage_used || 0; this.localUsed = this.user.local_storage_used || 0;
console.log('[登录] 存储权限:', this.storagePermission, '存储类型:', this.storageType, 'OSS配置:', this.user.has_oss_config); console.log('[登录] 存储权限:', this.storagePermission, '存储类型:', this.storageType, 'OSS配置:', this.user.oss_config_source);
// 智能存储类型修正如果当前是OSS但未配置且用户有本地存储权限自动切换到本地 // 智能存储类型修正如果当前是OSS但未配置(包括个人配置和系统级配置),且用户有本地存储权限,自动切换到本地
if (this.storageType === 'oss' && !this.user.has_oss_config) { if (this.storageType === 'oss' && (!this.user || this.user.oss_config_source === 'none')) {
if (this.storagePermission === 'local_only' || this.storagePermission === 'user_choice') { if (this.storagePermission === 'local_only' || this.storagePermission === 'user_choice') {
console.log('[登录] OSS未配置但用户有本地存储权限自动切换到本地存储'); console.log('[登录] OSS未配置但用户有本地存储权限自动切换到本地存储');
this.storageType = 'local'; this.storageType = 'local';
@@ -682,9 +682,9 @@ handleDragLeave(e) {
this.currentView = 'files'; this.currentView = 'files';
this.loadFiles('/'); this.loadFiles('/');
} }
// 如果仅OSS模式需要检查是否配置了OSS // 如果仅OSS模式需要检查是否配置了OSS(包括系统级统一配置)
else if (this.storagePermission === 'oss_only') { else if (this.storagePermission === 'oss_only') {
if (this.user.has_oss_config) { if (this.user?.oss_config_source !== 'none') {
this.currentView = 'files'; this.currentView = 'files';
this.loadFiles('/'); this.loadFiles('/');
} else { } else {
@@ -948,7 +948,7 @@ handleDragLeave(e) {
return; return;
} }
// 如果用户已有配置Secret 可以为空(使用现有密钥) // 如果用户已有配置Secret 可以为空(使用现有密钥)
if (!this.user?.has_oss_config && (!this.ossConfigForm.oss_access_key_secret || this.ossConfigForm.oss_access_key_secret.trim() === '')) { if (this.user?.oss_config_source === 'none' && (!this.ossConfigForm.oss_access_key_secret || this.ossConfigForm.oss_access_key_secret.trim() === '')) {
this.showToast('error', '配置错误', 'Access Key Secret 不能为空'); this.showToast('error', '配置错误', 'Access Key Secret 不能为空');
return; return;
} }
@@ -1177,7 +1177,7 @@ handleDragLeave(e) {
targetView = savedView; targetView = savedView;
} else if (this.user.is_admin) { } else if (this.user.is_admin) {
targetView = 'admin'; targetView = 'admin';
} else if (this.storagePermission === 'oss_only' && !this.user.has_oss_config) { } else if (this.storagePermission === 'oss_only' && this.user?.oss_config_source === 'none') {
targetView = 'settings'; targetView = 'settings';
} else { } else {
targetView = 'files'; targetView = 'files';
@@ -1361,7 +1361,7 @@ handleDragLeave(e) {
const filePath = this.currentPath === '/' ? `/${file.name}` : `${this.currentPath}/${file.name}`; const filePath = this.currentPath === '/' ? `/${file.name}` : `${this.currentPath}/${file.name}`;
// OSS 模式:使用签名 URL 直连下载(不经过后端) // OSS 模式:使用签名 URL 直连下载(不经过后端)
if (this.storageType === 'oss' && this.user?.has_oss_config) { if (this.storageType === 'oss' && this.user?.oss_config_source !== 'none') {
this.downloadFromOSS(filePath); this.downloadFromOSS(filePath);
} else { } else {
// 本地存储模式:通过后端下载 // 本地存储模式:通过后端下载
@@ -1369,7 +1369,7 @@ handleDragLeave(e) {
} }
}, },
// OSS 直连下载 // OSS 直连下载使用签名URL不经过后端节省后端带宽
async downloadFromOSS(filePath) { async downloadFromOSS(filePath) {
try { try {
// 获取签名 URL // 获取签名 URL
@@ -1378,7 +1378,7 @@ handleDragLeave(e) {
}); });
if (data.success) { if (data.success) {
// 直连 OSS 下载 // 直连 OSS 下载不经过后端充分利用OSS带宽和CDN
window.open(data.downloadUrl, '_blank'); window.open(data.downloadUrl, '_blank');
} else { } else {
// 处理后端返回的错误 // 处理后端返回的错误
@@ -1629,7 +1629,7 @@ handleDragLeave(e) {
: `${this.currentPath}/${file.name}`; : `${this.currentPath}/${file.name}`;
// OSS 模式:返回签名 URL用于媒体预览 // OSS 模式:返回签名 URL用于媒体预览
if (this.storageType === 'oss' && this.user?.has_oss_config) { if (this.storageType === 'oss' && this.user?.oss_config_source !== 'none') {
try { try {
const { data } = await axios.get(`${this.apiBase}/api/files/download-url`, { const { data } = await axios.get(`${this.apiBase}/api/files/download-url`, {
params: { path: filePath } params: { path: filePath }
@@ -1886,7 +1886,7 @@ handleDragLeave(e) {
this.totalBytes = file.size; this.totalBytes = file.size;
try { try {
if (this.storageType === 'oss' && this.user?.has_oss_config) { if (this.storageType === 'oss' && this.user?.oss_config_source !== 'none') {
// ===== OSS 直连上传(不经过后端) ===== // ===== OSS 直连上传(不经过后端) =====
await this.uploadToOSSDirect(file); await this.uploadToOSSDirect(file);
} else { } else {
@@ -2451,8 +2451,8 @@ handleDragLeave(e) {
// 加载OSS空间使用统计 // 加载OSS空间使用统计
async loadOssUsage() { async loadOssUsage() {
// 仅在用户已配置OSS时才加载 // 检查是否有可用的OSS配置个人配置或系统级统一配置
if (!this.user?.has_oss_config) { if (!this.user || this.user?.oss_config_source === 'none') {
this.ossUsage = null; this.ossUsage = null;
return; return;
} }
@@ -2478,7 +2478,7 @@ handleDragLeave(e) {
// 刷新存储空间使用统计(根据当前存储类型) // 刷新存储空间使用统计(根据当前存储类型)
async refreshStorageUsage() { async refreshStorageUsage() {
if (this.storageType === 'oss' && this.user?.has_oss_config) { if (this.storageType === 'oss' && this.user?.oss_config_source !== 'none') {
// 刷新 OSS 空间统计 // 刷新 OSS 空间统计
await this.loadOssUsage(); await this.loadOssUsage();
} else if (this.storageType === 'local') { } else if (this.storageType === 'local') {
@@ -2520,11 +2520,8 @@ handleDragLeave(e) {
return; return;
} }
// 切到OSS但还未配置引导弹窗 // 不再弹出配置引导弹窗,直接尝试切换
if (type === 'oss' && (!this.user?.has_oss_config)) { // 如果后端检测到没有OSS配置会返回错误提示
this.showOssGuideModal = true;
return;
}
this.storageSwitching = true; this.storageSwitching = true;
this.storageSwitchTarget = type; this.storageSwitchTarget = type;
@@ -2573,6 +2570,11 @@ handleDragLeave(e) {
}, },
openOssConfigModal() { openOssConfigModal() {
// 只有管理员才能配置OSS
if (!this.user?.is_admin) {
this.showToast('error', '权限不足', '只有管理员才能配置OSS服务');
return;
}
this.showOssGuideModal = false; this.showOssGuideModal = false;
this.showOssConfigModal = true; this.showOssConfigModal = true;
if (this.user && !this.user.is_admin) { if (this.user && !this.user.is_admin) {

View File

@@ -3,7 +3,7 @@
################################################################################ ################################################################################
# 玩玩云 (WanWanYun) - 一键部署/卸载/更新脚本 # 玩玩云 (WanWanYun) - 一键部署/卸载/更新脚本
# 项目地址: https://git.workyai.cn/237899745/vue-driven-cloud-storage # 项目地址: https://git.workyai.cn/237899745/vue-driven-cloud-storage
# 版本: v1.2.0 # 版本: v3.1.0
################################################################################ ################################################################################
set -e set -e
@@ -2225,91 +2225,6 @@ create_data_directories() {
echo "" echo ""
} }
build_upload_tool() {
print_step "下载上传工具..."
cd "${PROJECT_DIR}/upload-tool"
# 检查是否已存在可执行文件并验证大小
if [[ -f "dist/玩玩云上传工具.exe" ]]; then
FILE_SIZE=$(stat -f%z "dist/玩玩云上传工具.exe" 2>/dev/null || stat -c%s "dist/玩玩云上传工具.exe" 2>/dev/null || echo "0")
if [[ $FILE_SIZE -gt 30000000 ]]; then
FILE_SIZE_MB=$(( FILE_SIZE / 1024 / 1024 ))
print_success "上传工具已存在(${FILE_SIZE_MB}MB跳过下载"
echo ""
return 0
else
print_warning "现有文件大小异常(${FILE_SIZE}字节),重新下载..."
rm -f "dist/玩玩云上传工具.exe"
fi
fi
# 创建dist目录
mkdir -p dist
# 下载地址Windows版本
TOOL_DOWNLOAD_URL="http://a.haory.top/e/e82/玩玩云上传工具.exe"
TOOL_FILENAME="玩玩云上传工具.exe"
print_info "正在下载上传工具约43MB可能需要1-2分钟..."
# 尝试下载最多3次重试
DOWNLOAD_SUCCESS=false
for attempt in 1 2 3; do
print_info "尝试下载 ($attempt/3)..."
if command -v wget &> /dev/null; then
# wget: 超时300秒重试3次
if wget --timeout=300 --tries=3 --no-check-certificate -q --show-progress -O "dist/${TOOL_FILENAME}" "$TOOL_DOWNLOAD_URL" 2>&1; then
DOWNLOAD_SUCCESS=true
break
fi
elif command -v curl &> /dev/null; then
# curl: 连接超时60秒总超时300秒
if curl --connect-timeout 60 --max-time 300 -L -# -o "dist/${TOOL_FILENAME}" "$TOOL_DOWNLOAD_URL" 2>&1; then
DOWNLOAD_SUCCESS=true
break
fi
else
print_warning "未找到wget或curl无法下载上传工具"
print_info "用户仍可使用网页上传(本地存储/OSS云存储"
echo ""
return 0
fi
# 如果不是最后一次尝试,等待后重试
if [[ $attempt -lt 3 ]]; then
print_warning "下载失败5秒后重试..."
sleep 5
fi
done
# 验证下载结果
if [[ "$DOWNLOAD_SUCCESS" == "true" ]] && [[ -f "dist/${TOOL_FILENAME}" ]]; then
FILE_SIZE=$(stat -f%z "dist/${TOOL_FILENAME}" 2>/dev/null || stat -c%s "dist/${TOOL_FILENAME}" 2>/dev/null || echo "0")
FILE_SIZE_MB=$(( FILE_SIZE / 1024 / 1024 ))
if [[ $FILE_SIZE -gt 30000000 ]]; then
print_success "上传工具下载完成: ${FILE_SIZE_MB}MB"
echo ""
else
print_error "下载的文件大小异常(${FILE_SIZE}字节),可能下载不完整"
rm -f "dist/${TOOL_FILENAME}"
print_warning "可手动下载: ${TOOL_DOWNLOAD_URL}"
print_info "用户仍可使用网页上传(本地存储/OSS云存储"
echo ""
fi
else
print_error "上传工具下载失败已重试3次"
print_warning "可能的原因:"
echo " 1. 网络连接问题或下载速度过慢"
echo " 2. CDN链接不可访问: ${TOOL_DOWNLOAD_URL}"
echo " 3. 防火墙拦截HTTP连接"
print_info "您可以稍后手动下载并放置到: ${PROJECT_DIR}/upload-tool/dist/"
print_info "用户仍可使用网页上传(本地存储/OSS云存储"
echo ""
fi
}
################################################################################ ################################################################################
# Nginx配置 - 分步骤执行 # Nginx配置 - 分步骤执行
@@ -2502,9 +2417,6 @@ server {
expires 30d; expires 30d;
} }
# 上传工具下载
location /download-tool {
alias ${PROJECT_DIR}/upload-tool/dist;
} }
} }
EOF EOF
@@ -2792,9 +2704,6 @@ server {
expires 30d; expires 30d;
} }
# 上传工具下载
location /download-tool {
alias ${PROJECT_DIR}/upload-tool/dist;
} }
} }
EOF EOF
@@ -2932,9 +2841,6 @@ server {
expires 30d; expires 30d;
} }
# 上传工具下载
location /download-tool {
alias ${PROJECT_DIR}/upload-tool/dist;
} }
} }
EOF EOF
@@ -3512,56 +3418,6 @@ update_pull_latest_code() {
cp -r "/tmp/${PROJECT_NAME}-update/frontend" "${PROJECT_DIR}/" cp -r "/tmp/${PROJECT_NAME}-update/frontend" "${PROJECT_DIR}/"
fi fi
# 更新上传工具 - 询问用户是否保留
if [[ -d "/tmp/${PROJECT_NAME}-update/upload-tool" ]]; then
# 检查是否已存在上传工具可执行文件
if [[ -f "${PROJECT_DIR}/upload-tool/dist/玩玩云上传工具.exe" ]]; then
FILE_SIZE=$(stat -f%z "${PROJECT_DIR}/upload-tool/dist/玩玩云上传工具.exe" 2>/dev/null || stat -c%s "${PROJECT_DIR}/upload-tool/dist/玩玩云上传工具.exe" 2>/dev/null || echo "0")
if [[ $FILE_SIZE -gt 30000000 ]]; then
FILE_SIZE_MB=$(( FILE_SIZE / 1024 / 1024 ))
echo ""
print_info "检测到已存在上传工具(${FILE_SIZE_MB}MB"
echo ""
echo "╔════════════════════════════════════════════════════════════╗"
echo "║ 上传工具更新选项 ║"
echo "╠════════════════════════════════════════════════════════════╣"
echo "║ 1) 保留现有上传工具(推荐,节省下载时间) ║"
echo "║ 2) 删除并重新下载(如果工具有更新) ║"
echo "╚════════════════════════════════════════════════════════════╝"
echo ""
# 强制从终端读取用户输入
read -p "▶ 请选择 [1/2, 默认:1]: " KEEP_UPLOAD_TOOL < /dev/tty
KEEP_UPLOAD_TOOL=${KEEP_UPLOAD_TOOL:-1}
if [[ "$KEEP_UPLOAD_TOOL" == "1" ]]; then
print_success "保留现有上传工具"
# 只更新upload-tool目录的脚本文件保留dist目录
mkdir -p "${PROJECT_DIR}/upload-tool-temp"
cp -r "${PROJECT_DIR}/upload-tool/dist" "${PROJECT_DIR}/upload-tool-temp/"
rm -rf "${PROJECT_DIR}/upload-tool"
cp -r "/tmp/${PROJECT_NAME}-update/upload-tool" "${PROJECT_DIR}/"
rm -rf "${PROJECT_DIR}/upload-tool/dist"
mv "${PROJECT_DIR}/upload-tool-temp/dist" "${PROJECT_DIR}/upload-tool/"
rm -rf "${PROJECT_DIR}/upload-tool-temp"
print_success "已保留现有上传工具,仅更新脚本文件"
else
print_info "将删除现有工具并在后续步骤重新下载..."
rm -rf "${PROJECT_DIR}/upload-tool"
cp -r "/tmp/${PROJECT_NAME}-update/upload-tool" "${PROJECT_DIR}/"
# 删除dist目录以触发后续重新下载
rm -rf "${PROJECT_DIR}/upload-tool/dist"
fi
else
print_warning "现有上传工具文件大小异常,将重新下载..."
rm -rf "${PROJECT_DIR}/upload-tool"
cp -r "/tmp/${PROJECT_NAME}-update/upload-tool" "${PROJECT_DIR}/"
fi
else
# 不存在上传工具,直接复制
rm -rf "${PROJECT_DIR}/upload-tool"
cp -r "/tmp/${PROJECT_NAME}-update/upload-tool" "${PROJECT_DIR}/"
fi
fi
# 更新后端代码文件(但不覆盖 data、storage、.env # 更新后端代码文件(但不覆盖 data、storage、.env
print_info "更新后端代码..." print_info "更新后端代码..."
@@ -3939,21 +3795,6 @@ update_main() {
# 更新依赖 # 更新依赖
# 检查并重新下载上传工具(如果需要)
if [[ ! -f "${PROJECT_DIR}/upload-tool/dist/玩玩云上传工具.exe" ]]; then
print_info "检测到上传工具丢失,正在重新下载..."
build_upload_tool
else
FILE_SIZE=$(stat -f%z "${PROJECT_DIR}/upload-tool/dist/玩玩云上传工具.exe" 2>/dev/null || stat -c%s "${PROJECT_DIR}/upload-tool/dist/玩玩云上传工具.exe" 2>/dev/null || echo "0")
if [[ $FILE_SIZE -lt 30000000 ]]; then
print_warning "上传工具文件大小异常,正在重新下载..."
rm -f "${PROJECT_DIR}/upload-tool/dist/玩玩云上传工具.exe"
build_upload_tool
else
FILE_SIZE_MB=$(( FILE_SIZE / 1024 / 1024 ))
print_success "上传工具完整(${FILE_SIZE_MB}MB"
fi
fi
update_install_dependencies update_install_dependencies
# 迁移数据库配置 # 迁移数据库配置
@@ -4094,8 +3935,6 @@ main() {
# 创建数据目录 # 创建数据目录
create_data_directories create_data_directories
# 打包上传工具
build_upload_tool
# 先配置基础HTTP NginxSSL证书申请需要 # 先配置基础HTTP NginxSSL证书申请需要
configure_nginx_http_first configure_nginx_http_first