Compare commits

...

2 Commits

Author SHA1 Message Date
Claude Opus
e8d053f28d fix: 完善存储客户端实现并删除重复导入
- LocalStorageClient 和 OssStorageClient 添加 formatSize() 方法
- 删除 mkdir() 中重复的 PutObjectCommand 导入
- 统一使用共享的 formatFileSize() 函数

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 21:49:44 +08:00
Claude Opus
7aa8a862a4 chore: 优化代码质量和安全性\n\n- 删除未使用的 @aws-sdk/lib-storage 依赖,简化依赖\n- 修复重复导入 database 模块\n- 消除 formatSize 重复代码,提取为共享函数\n- 修复 verify.html XSS 漏洞,添加 HTML 转义\n- 更新 index.html 过时文案(断点续传→直连上传) 2026-01-18 20:23:39 +08:00
5 changed files with 49 additions and 57 deletions

View File

@@ -31,7 +31,6 @@
"multer": "^2.0.2", "multer": "^2.0.2",
"nodemailer": "^6.9.14", "nodemailer": "^6.9.14",
"@aws-sdk/client-s3": "^3.600.0", "@aws-sdk/client-s3": "^3.600.0",
"@aws-sdk/lib-storage": "^3.600.0",
"svg-captcha": "^1.4.0" "svg-captcha": "^1.4.0"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -3195,7 +3195,6 @@ app.post('/api/share/create', authMiddleware, (req, res) => {
}); });
// 更新分享的存储类型 // 更新分享的存储类型
const { db } = require('./database');
db.prepare('UPDATE shares SET storage_type = ? WHERE id = ?') db.prepare('UPDATE shares SET storage_type = ? WHERE id = ?')
.run(req.user.current_storage_type || 'oss', result.id); .run(req.user.current_storage_type || 'oss', result.id);

View File

@@ -1,9 +1,21 @@
const { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectsCommand, ListObjectsV2Command, HeadObjectCommand } = require('@aws-sdk/client-s3'); const { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectsCommand, ListObjectsV2Command, HeadObjectCommand } = require('@aws-sdk/client-s3');
const { Upload } = require('@aws-sdk/lib-storage');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { UserDB } = require('./database'); const { UserDB } = require('./database');
// ===== 工具函数 =====
/**
* 格式化文件大小
*/
function formatFileSize(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];
}
// ===== 统一存储接口 ===== // ===== 统一存储接口 =====
/** /**
@@ -314,11 +326,7 @@ class LocalStorageClient {
* 格式化文件大小 * 格式化文件大小
*/ */
formatSize(bytes) { formatSize(bytes) {
if (bytes === 0) return '0 B'; return formatFileSize(bytes);
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];
} }
} }
@@ -516,55 +524,34 @@ class OssStorageClient {
} }
/** /**
* 上传文件(支持分片上传 * 上传文件(直接上传,简单高效
* @param {string} localPath - 本地文件路径
* @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.user.oss_bucket; const bucket = this.user.oss_bucket;
const fileSize = fs.statSync(localPath).size; const fileSize = fs.statSync(localPath).size;
// 创建文件读取流 // 创建文件读取流
fileStream = fs.createReadStream(localPath); const fileStream = fs.createReadStream(localPath);
// 使用 AWS SDK 的 Upload 类处理分片上传 // 直接上传AWS S3 支持最大 5GB 的单文件上传
const upload = new Upload({ const command = new PutObjectCommand({
client: this.s3Client,
params: {
Bucket: bucket, Bucket: bucket,
Key: key, Key: key,
Body: fileStream Body: fileStream
},
queueSize: 3, // 并发分片数
partSize: 5 * 1024 * 1024 // 5MB 分片
}); });
// 监听上传进度(可选) await this.s3Client.send(command);
upload.on('httpUploadProgress', (progress) => { console.log(`[OSS存储] 上传成功: ${key} (${formatFileSize(fileSize)})`);
if (progress && progress.loaded && progress.total) {
const percent = Math.round((progress.loaded / progress.total) * 100);
// 只在较大文件时打印进度(避免日志过多)
if (progress.total > 10 * 1024 * 1024 || percent % 20 === 0) {
console.log(`[OSS存储] 上传进度: ${percent}% (${key})`);
}
}
});
await upload.done(); // 关闭流
console.log(`[OSS存储] 上传成功: ${key} (${this.formatSize(fileSize)})`); if (!fileStream.destroyed) {
// 上传成功后,手动关闭流
if (fileStream && !fileStream.destroyed) {
fileStream.destroy(); fileStream.destroy();
} }
} catch (error) { } catch (error) {
// 确保流被关闭,防止泄漏
if (fileStream && !fileStream.destroyed) {
fileStream.destroy();
}
console.error(`[OSS存储] 上传失败: ${remotePath}`, error.message); console.error(`[OSS存储] 上传失败: ${remotePath}`, error.message);
// 判断错误类型并给出友好的错误信息 // 判断错误类型并给出友好的错误信息
@@ -764,7 +751,6 @@ class OssStorageClient {
// OSS 中文件夹通过以斜杠结尾的空对象模拟 // OSS 中文件夹通过以斜杠结尾的空对象模拟
const folderKey = key.endsWith('/') ? key : `${key}/`; const folderKey = key.endsWith('/') ? key : `${key}/`;
const { PutObjectCommand } = require('@aws-sdk/client-s3');
const command = new PutObjectCommand({ const command = new PutObjectCommand({
Bucket: bucket, Bucket: bucket,
Key: folderKey, Key: folderKey,
@@ -808,27 +794,24 @@ class OssStorageClient {
return `${baseUrl}/${key}`; return `${baseUrl}/${key}`;
} }
/**
* 格式化文件大小
*/
formatSize(bytes) {
return formatFileSize(bytes);
}
/** /**
* 关闭连接S3Client 无需显式关闭) * 关闭连接S3Client 无需显式关闭)
*/ */
async end() { async end() {
this.s3Client = null; this.s3Client = null;
} }
/**
* 格式化文件大小
*/
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];
}
} }
module.exports = { module.exports = {
StorageInterface, StorageInterface,
LocalStorageClient, LocalStorageClient,
OssStorageClient OssStorageClient,
formatFileSize // 导出共享的工具函数
}; };

View File

@@ -606,7 +606,7 @@
<i class="fas fa-cloud-arrow-up"></i> <i class="fas fa-cloud-arrow-up"></i>
</div> </div>
<h3 class="feature-title">极速上传</h3> <h3 class="feature-title">极速上传</h3>
<p class="feature-desc">拖拽上传,实时进度,支持大文件断点续</p> <p class="feature-desc">拖拽上传,实时进度,支持大文件直连上</p>
</div> </div>
<div class="feature-card"> <div class="feature-card">
<div class="feature-icon"> <div class="feature-icon">

View File

@@ -221,17 +221,28 @@
return url.searchParams.get(name); return url.searchParams.get(name);
} }
// HTML 转义函数(防御 XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 显示结果 // 显示结果
function showResult(success, message, showButton = true) { function showResult(success, message, showButton = true) {
const content = document.getElementById('content'); const content = document.getElementById('content');
const iconClass = success ? 'success' : 'error'; const iconClass = success ? 'success' : 'error';
const iconName = success ? 'fa-check-circle' : 'fa-times-circle'; const iconName = success ? 'fa-check-circle' : 'fa-times-circle';
// 转义用户消息(但允许安全的 HTML 标签如 <br>
const safeMessage = message.replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/&lt;br&gt;/g, '<br>'); // 允许 <br> 标签
let html = ` let html = `
<div class="status-icon ${iconClass}"> <div class="status-icon ${iconClass}">
<i class="fas ${iconName}"></i> <i class="fas ${iconName}"></i>
</div> </div>
<p class="message">${message}</p> <p class="message">${safeMessage}</p>
`; `;
if (showButton) { if (showButton) {