feat: improve upload resilience and release 0.1.23

This commit is contained in:
2026-02-20 20:21:42 +08:00
parent 6618de1aed
commit 01384a2215
9 changed files with 435 additions and 115 deletions

View File

@@ -112,7 +112,7 @@ const DOWNLOAD_SIGNED_URL_EXPIRES_SECONDS = Math.max(
10,
Math.min(3600, Number(process.env.DOWNLOAD_SIGNED_URL_EXPIRES_SECONDS || 30))
);
const DEFAULT_DESKTOP_VERSION = process.env.DESKTOP_LATEST_VERSION || '0.1.22';
const DEFAULT_DESKTOP_VERSION = process.env.DESKTOP_LATEST_VERSION || '0.1.23';
const DEFAULT_DESKTOP_INSTALLER_URL = process.env.DESKTOP_INSTALLER_URL || '';
const DEFAULT_DESKTOP_RELEASE_NOTES = process.env.DESKTOP_RELEASE_NOTES || '';
const DESKTOP_INSTALLERS_DIR = path.resolve(__dirname, '../frontend/downloads');
@@ -2332,9 +2332,13 @@ function logSecurity(req, action, message, details = null, level = 'warn') {
}
// 文件上传配置(临时存储)
const MULTER_UPLOAD_MAX_BYTES = Math.max(
1 * 1024 * 1024,
Number(process.env.MULTER_UPLOAD_MAX_BYTES || (50 * 1024 * 1024 * 1024))
);
const upload = multer({
dest: path.join(__dirname, 'uploads'),
limits: { fileSize: 5 * 1024 * 1024 * 1024 } // 5GB限制
limits: { fileSize: MULTER_UPLOAD_MAX_BYTES }
});
// ===== TTL缓存类 =====
@@ -5949,11 +5953,15 @@ app.post('/api/files/instant-upload/check', authMiddleware, async (req, res) =>
// 本地分片上传初始化(断点续传)
app.post('/api/upload/resumable/init', authMiddleware, async (req, res) => {
try {
if ((req.user.current_storage_type || 'oss') !== 'local') {
return res.status(400).json({
success: false,
message: '当前存储模式不支持分片上传'
});
const uploadStorageType = (req.user.current_storage_type || 'oss') === 'local' ? 'local' : 'oss';
if (uploadStorageType === 'oss') {
const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig();
if (!req.user.has_oss_config && !hasUnifiedConfig) {
return res.status(400).json({
success: false,
message: '未配置 OSS 服务,无法上传文件'
});
}
}
const filename = typeof req.body?.filename === 'string' ? req.body.filename.trim() : '';
@@ -6013,6 +6021,7 @@ app.post('/api/upload/resumable/init', authMiddleware, async (req, res) => {
const totalChunks = Math.ceil(fileSize / chunkSize);
const existingSession = UploadSessionDB.findActiveForResume(
req.user.id,
uploadStorageType,
targetPath,
fileSize,
fileHash || null
@@ -6038,6 +6047,7 @@ app.post('/api/upload/resumable/init', authMiddleware, async (req, res) => {
return res.json({
success: true,
resumed: true,
storage_type: existingSession.storage_type || uploadStorageType,
session_id: existingSession.session_id,
target_path: existingSession.target_path,
file_name: existingSession.file_name,
@@ -6061,7 +6071,7 @@ app.post('/api/upload/resumable/init', authMiddleware, async (req, res) => {
const createdSession = UploadSessionDB.create({
sessionId,
userId: req.user.id,
storageType: 'local',
storageType: uploadStorageType,
targetPath,
fileName: filename,
fileSize,
@@ -6085,6 +6095,7 @@ app.post('/api/upload/resumable/init', authMiddleware, async (req, res) => {
res.json({
success: true,
resumed: false,
storage_type: createdSession.storage_type || uploadStorageType,
session_id: createdSession.session_id,
target_path: createdSession.target_path,
file_name: createdSession.file_name,
@@ -6154,14 +6165,6 @@ app.get('/api/upload/resumable/status', authMiddleware, (req, res) => {
app.post('/api/upload/resumable/chunk', authMiddleware, upload.single('chunk'), async (req, res) => {
const tempChunkPath = req.file?.path;
try {
if ((req.user.current_storage_type || 'oss') !== 'local') {
if (tempChunkPath) safeDeleteFile(tempChunkPath);
return res.status(400).json({
success: false,
message: '当前存储模式不支持分片上传'
});
}
const sessionId = typeof req.body?.session_id === 'string' ? req.body.session_id.trim() : '';
const chunkIndex = Math.floor(Number(req.body?.chunk_index));
if (!sessionId) {
@@ -6287,16 +6290,10 @@ app.post('/api/upload/resumable/chunk', authMiddleware, upload.single('chunk'),
}
});
// 完成分片上传(写入本地存储
// 完成分片上传(写入当前存储:本地或 OSS
app.post('/api/upload/resumable/complete', authMiddleware, async (req, res) => {
let storage = null;
try {
if ((req.user.current_storage_type || 'oss') !== 'local') {
return res.status(400).json({
success: false,
message: '当前存储模式不支持分片上传'
});
}
const sessionId = typeof req.body?.session_id === 'string' ? req.body.session_id.trim() : '';
if (!sessionId) {
return res.status(400).json({
@@ -6345,6 +6342,8 @@ app.post('/api/upload/resumable/complete', authMiddleware, async (req, res) => {
});
}
const sessionStorageType = session.storage_type === 'local' ? 'local' : 'oss';
const latestUser = UserDB.findById(req.user.id);
if (!latestUser) {
return res.status(404).json({
@@ -6353,12 +6352,25 @@ app.post('/api/upload/resumable/complete', authMiddleware, async (req, res) => {
});
}
const localUserContext = buildStorageUserContext(latestUser, {
current_storage_type: 'local'
if (sessionStorageType === 'oss') {
const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig();
if (!latestUser.has_oss_config && !hasUnifiedConfig) {
return res.status(400).json({
success: false,
message: '未配置 OSS 服务,无法完成分片上传'
});
}
}
const storageUserContext = buildStorageUserContext(latestUser, {
current_storage_type: sessionStorageType
});
const localStorage = new LocalStorageClient(localUserContext);
await localStorage.init();
await localStorage.put(session.temp_file_path, session.target_path);
const storageInterface = new StorageInterface(storageUserContext);
storage = await storageInterface.connect();
await storage.put(session.temp_file_path, session.target_path);
if (sessionStorageType === 'oss') {
clearOssUsageCache(req.user.id);
}
UploadSessionDB.setStatus(session.session_id, 'completed', {
completed: true,
@@ -6368,7 +6380,7 @@ app.post('/api/upload/resumable/complete', authMiddleware, async (req, res) => {
await trackFileHashIndexForUpload({
userId: req.user.id,
storageType: 'local',
storageType: sessionStorageType,
fileHash: session.file_hash,
fileSize,
filePath: session.target_path
@@ -6387,6 +6399,10 @@ app.post('/api/upload/resumable/complete', authMiddleware, async (req, res) => {
success: false,
message: getSafeErrorMessage(error, '完成分片上传失败,请稍后重试', '完成分片上传')
});
} finally {
if (storage) {
await storage.end().catch(() => {});
}
}
});