From d789f6813538a3571c5b0c5eb7e0ec1e03b7a16e Mon Sep 17 00:00:00 2001 From: Lan Date: Mon, 24 Nov 2025 19:25:08 +0800 Subject: [PATCH 1/3] fix: https://github.com/vastsa/FileCodeBox/issues/420 --- src/services/index.ts | 2 +- src/views/manage/FileManageView.vue | 45 ----------------------------- 2 files changed, 1 insertion(+), 46 deletions(-) diff --git a/src/services/index.ts b/src/services/index.ts index 4320647..536296c 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -78,7 +78,7 @@ export class FileService { } static async updateFile(data: FileEditForm): Promise { - return api.post('/admin/file/update', data) + return api.patch('/admin/file/update', data) } static async deleteAdminFile(id: number): Promise { diff --git a/src/views/manage/FileManageView.vue b/src/views/manage/FileManageView.vue index 3d86489..635df10 100644 --- a/src/views/manage/FileManageView.vue +++ b/src/views/manage/FileManageView.vue @@ -405,29 +405,6 @@ const handleUpdate = async () => { } } -// 下载文件处理 - 暂时移除未使用的函数 -// const downloadFile = async (id: number) => { -// try { -// const response = await FileService.downloadAdminFile(id) - -// const contentDisposition = response.headers['content-disposition'] -// let filename = 'file' -// const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/) -// if (filenameMatch != null && filenameMatch[1]) { -// filename = filenameMatch[1].replace(/['"]/g, '') -// } - -// // @ts-expect-error - showSaveFilePicker is not in standard Window interface -// if (window.showSaveFilePicker) { -// await saveFileByWebApi(response.data, filename) -// } else { -// await saveFileByElementA(response.data, filename) -// } -// } catch (error) { -// console.error('下载失败:', error) -// } -// } - // 删除文件处理 const deleteFile = async (id: number) => { try { @@ -438,28 +415,6 @@ const deleteFile = async (id: number) => { } } -// 文件保存辅助函数 - 暂时移除未使用的函数 -// async function saveFileByElementA(fileBlob: Blob, filename: string) { -// const downloadUrl = window.URL.createObjectURL(fileBlob) -// const link = document.createElement('a') -// link.href = downloadUrl -// link.download = filename -// document.body.appendChild(link) -// link.click() -// window.URL.revokeObjectURL(downloadUrl) -// document.body.removeChild(link) -// } - -// async function saveFileByWebApi(fileBlob: Blob, filename: string) { -// // @ts-expect-error - showSaveFilePicker is not in standard Window interface -// const newHandle = await window.showSaveFilePicker({ -// suggestedName: filename -// }) -// const writableStream = await newHandle.createWritable() -// await writableStream.write(fileBlob) -// await writableStream.close() -// } - // 加载文件列表 const loadFiles = async () => { try { From f8a123ab9437a11ad65b533fbefaf97522623a5a Mon Sep 17 00:00:00 2001 From: Lan Date: Mon, 24 Nov 2025 19:26:46 +0800 Subject: [PATCH 2/3] fix: https://github.com/vastsa/FileCodeBox/issues/408 --- src/constants/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants/index.ts b/src/constants/index.ts index 9887e55..9585dc7 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -56,7 +56,7 @@ export const FILE_SIZE_LIMITS = { // 时间相关常量 export const TIME_CONSTANTS = { ALERT_DURATION: 5000, // 5秒 - REQUEST_TIMEOUT: 30000, // 30秒 + REQUEST_TIMEOUT: 300000000, PROGRESS_UPDATE_INTERVAL: 100 // 100毫秒 } as const From 6b8ba04e6d366534cb9075e003e8370e393eeda3 Mon Sep 17 00:00:00 2001 From: Lan Date: Mon, 1 Dec 2025 15:29:12 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E9=80=82=E9=85=8D=E5=88=87?= =?UTF-8?q?=E7=89=87=EF=BC=8C=E5=A4=9A=E8=AF=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/i18n/locales/en-US.ts | 6 +- src/i18n/locales/zh-CN.ts | 21 ++++- src/views/SendFileView.vue | 107 ++++++++++++++---------- src/views/manage/SystemSettingsView.vue | 33 ++++++-- 4 files changed, 111 insertions(+), 56 deletions(-) diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index 60e4de9..8aeec94 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -92,7 +92,7 @@ export default { localStorage: 'Local Storage', s3Storage: 'S3 Storage', webdavStorage: 'Webdav Storage', - chunkUploadNote: 'Enable chunk upload (experimental feature, still in development, currently only available for local storage, may have unknown issues)', + chunkUploadNote: 'Enable chunk upload (experimental feature, may have unknown issues)', s3AccessKeyId: 'S3 AccessKeyId', s3SecretAccessKey: 'S3 SecretAccessKey', s3BucketName: 'S3 BucketName', @@ -334,7 +334,7 @@ export default { localStorage: 'Local Storage', s3Storage: 'S3 Storage', webdavStorage: 'Webdav Storage', - chunkUploadNote: 'Enable chunk upload (experimental feature, still in development, currently only available for local storage, may have unknown issues)', + chunkUploadNote: 'Enable chunk upload (experimental feature, may have unknown issues)', enabled: 'Enabled', disabled: 'Disabled', webdavUrl: 'Please enter Webdav URL', @@ -401,7 +401,7 @@ export default { localStorage: 'Local Storage', s3Storage: 'S3 Storage', webdavStorage: 'Webdav Storage', - chunkUploadNote: 'Enable chunk upload (experimental feature, still in development, currently only available for local storage, may have unknown issues)', + chunkUploadNote: 'Enable chunk upload (experimental feature, may have unknown issues)', enabled: 'Enabled', disabled: 'Disabled', webdavUrl: 'Please enter Webdav URL', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index e8bed31..5fca9d5 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -91,7 +91,7 @@ export default { localStorage: '本地存储', s3Storage: 'S3 存储', webdavStorage: 'Webdav 存储', - chunkUploadNote: '开启切片上传(实验性功能,还在开发中,目前仅本地存储可用,可能会出现未知问题)', + chunkUploadNote: '开启切片上传(实验性功能,可能会出现未知问题)', s3AccessKeyId: 'S3 AccessKeyId', s3SecretAccessKey: 'S3 SecretAccessKey', s3BucketName: 'S3 BucketName', @@ -369,7 +369,9 @@ export default { storagePathPlaceholder: '留空则使用默认路径,可不填写', storageMethod: '存储方式', localStorage: '本地存储', - chunkUploadNote: '开启切片上传(实验性功能,还在开发中,目前仅本地存储可用,可能会出现未知问题)', + s3Storage: 'S3 存储', + webdavStorage: 'Webdav 存储', + chunkUploadNote: '开启切片上传(实验性功能,可能会出现未知问题)', uploadLimits: '上传限制', uploadPerMinute: '每分钟上传限制', uploadCountLimit: '上传数量限制', @@ -382,7 +384,20 @@ export default { count: '按次数' }, maxSaveTime: '最长保存时间', + s3AccessKeyId: 'S3 AccessKeyId', s3SecretAccessKey: 'S3 SecretAccessKey', + s3BucketName: 'S3 BucketName', + s3EndpointUrl: 'S3 EndpointUrl', + s3RegionName: 'S3 Region Name', + s3SignatureVersion: 'S3 Signature Version', + s3Hostname: 'S3 Hostname', + s3v2: 'S3v2', + s3v4: 'S3v4', + autoPlaceholder: 'auto', + enableProxy: '启用代理', + webdavUrlPlaceholder: '请输入 Webdav URL', + webdavUsernamePlaceholder: '请输入 Webdav Username', + webdavPasswordPlaceholder: '请输入 Webdav Password', fileSizeUnits: { kb: 'KB', mb: 'MB', @@ -457,7 +472,7 @@ export default { localStorage: '本地存储', s3Storage: 'S3 存储', webdavStorage: 'Webdav 存储', - chunkUploadNote: '开启切片上传(实验性功能,还在开发中,目前仅本地存储可用,可能会出现未知问题)', + chunkUploadNote: '开启切片上传(实验性功能,可能会出现未知问题)', enabled: '已开启', disabled: '已关闭', webdavUrl: '请输入 Webdav URL', diff --git a/src/views/SendFileView.vue b/src/views/SendFileView.vue index 230d30a..b4e00af 100644 --- a/src/views/SendFileView.vue +++ b/src/views/SendFileView.vue @@ -556,8 +556,11 @@ const fileHash = ref('') -const handleFileSelected = (file: File) => { +const handleFileSelected = async (file: File) => { selectedFile.value = file + if (!checkOpenUpload()) return + if (!checkFileSize(file)) return + fileHash.value = await calculateFileHash(file) } @@ -648,49 +651,46 @@ const handlePaste = async (event: ClipboardEvent) => { } const calculateFileHash = async (file: File): Promise => { - return new Promise((resolve) => { - const chunkSize = 2097152 // 保持 2MB 的切片大小用于计算哈希 - const fileReader = new FileReader() - let currentChunk = 0 - const chunks = Math.ceil(file.size / chunkSize) - - fileReader.onload = async (e) => { - const chunk = new Uint8Array(e.target!.result as ArrayBuffer) - - try { - // 尝试使用 crypto.subtle.digest - if (window.isSecureContext) { - const hashBuffer = await crypto.subtle.digest('SHA-256', chunk) - const hashArray = Array.from(new Uint8Array(hashBuffer)) - const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('') - - currentChunk++ - if (currentChunk < chunks) { - loadNext() - } else { - resolve(hashHex) - } - } else { - // 如果不是安全上下文(HTTP),则返回一个基于文件信息的替代哈希 - const fallbackHash = generateFallbackHash(file) - resolve(fallbackHash) - } - } catch (err) { - // 如果 crypto.subtle.digest 失败,使用替代方案 - const fallbackHash = generateFallbackHash(file) - console.error('File hash calculation failed:', err) - resolve(fallbackHash) + try { + // 对于小文件,直接计算整个文件的哈希 + if (file.size <= 10 * 1024 * 1024) { + const buffer = await file.arrayBuffer() + if (window.isSecureContext) { + const hashBuffer = await crypto.subtle.digest('SHA-256', buffer) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('') } + return generateFallbackHash(file) } - const loadNext = () => { - const start = currentChunk * chunkSize - const end = start + chunkSize >= file.size ? file.size : start + chunkSize - fileReader.readAsArrayBuffer(file.slice(start, end)) - } + // 对于大文件,取首尾各5MB计算哈希 + const chunkSize = 5 * 1024 * 1024 + const firstChunk = file.slice(0, chunkSize) + const lastChunk = file.slice(-chunkSize) - loadNext() - }) + const [firstBuffer, lastBuffer] = await Promise.all([ + firstChunk.arrayBuffer(), + lastChunk.arrayBuffer() + ]) + + // 合并首尾数据 + const combined = new Uint8Array(firstBuffer.byteLength + lastBuffer.byteLength + 16) + combined.set(new Uint8Array(firstBuffer), 0) + combined.set(new Uint8Array(lastBuffer), firstBuffer.byteLength) + // 添加文件大小信息 + const sizeBytes = new TextEncoder().encode(file.size.toString()) + combined.set(sizeBytes, firstBuffer.byteLength + lastBuffer.byteLength) + + if (window.isSecureContext) { + const hashBuffer = await crypto.subtle.digest('SHA-256', combined) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('') + } + return generateFallbackHash(file) + } catch (err) { + console.error('File hash calculation failed:', err) + return generateFallbackHash(file) + } } // 生成替代哈希的函数 @@ -777,6 +777,11 @@ const getExpirationTime = (method: string, value: string) => { const handleChunkUpload = async (file: File) => { try { + // 确保文件哈希已计算 + // 每次上传都重新计算哈希,确保哈希值正确 + const hash = await calculateFileHash(file) + fileHash.value = hash + console.log('Calculated file hash:', hash) // 默认切片大小为5MB const chunkSize = 5 * 1024 * 1024 const chunks = Math.ceil(file.size / chunkSize) @@ -786,6 +791,7 @@ const handleChunkUpload = async (file: File) => { name?: string upload_id?: string existed?: boolean + uploaded_chunks?: number[] }> = await api.post('chunk/upload/init/', { file_name: file.name, file_size: file.size, @@ -800,17 +806,22 @@ const handleChunkUpload = async (file: File) => { return initResponse } const uploadId = initResponse.detail?.upload_id + const uploadedChunks = new Set(initResponse.detail?.uploaded_chunks || []) - // 2. 上传切片 + // 2. 上传切片(跳过已上传的) for (let i = 0; i < chunks; i++) { + // 跳过已上传的块 + if (uploadedChunks.has(i)) { + continue + } + const start = i * chunkSize const end = Math.min(start + chunkSize, file.size) const chunk = file.slice(start, end) const chunkFormData = new FormData() - chunkFormData.append('chunk', new Blob([chunk], { type: file.type })) // 确保以Blob形式添加 + chunkFormData.append('chunk', new Blob([chunk], { type: file.type })) - // 使用 application/x-www-form-urlencoded 格式 const chunkResponse: ApiResponse = await api.post( `chunk/upload/chunk/${uploadId}/${i}`, chunkFormData, @@ -819,10 +830,16 @@ const handleChunkUpload = async (file: File) => { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (progressEvent: { loaded: number; total?: number }) => { + // 计算已上传块的大小 + const uploadedSize = Array.from(uploadedChunks).reduce((acc, idx) => { + const chunkEnd = Math.min((idx + 1) * chunkSize, file.size) + const chunkStart = idx * chunkSize + return acc + (chunkEnd - chunkStart) + }, 0) const percentCompleted = Math.round( - ((i * chunkSize + progressEvent.loaded) * 100) / file.size + ((uploadedSize + i * chunkSize + progressEvent.loaded) * 100) / file.size ) - uploadProgress.value = percentCompleted + uploadProgress.value = Math.min(percentCompleted, 99) } } ) diff --git a/src/views/manage/SystemSettingsView.vue b/src/views/manage/SystemSettingsView.vue index 4d43e00..1e52068 100644 --- a/src/views/manage/SystemSettingsView.vue +++ b/src/views/manage/SystemSettingsView.vue @@ -108,9 +108,9 @@ const refreshData = async () => { } } } catch (error) { - const errorMessage = error instanceof Error ? error.message : '获取配置失败' + const errorMessage = error instanceof Error ? error.message : t('manage.systemSettings.getConfigFailed') alertStore.showAlert(errorMessage, 'error') - console.error('获取系统配置失败:', error) + console.error('Failed to get system config:', error) } } // 转换文件大小为字节 @@ -136,12 +136,12 @@ const submitSave = () => { ConfigService.updateConfig(formData).then((res) => { if (res.code == 200) { - alertStore.showAlert('保存成功', 'success') + alertStore.showAlert(t('manage.systemSettings.saveSuccess'), 'success') } else { - alertStore.showAlert(res.message || '保存失败', 'error') + alertStore.showAlert(res.message || t('manage.systemSettings.saveFailed'), 'error') } }).catch((error) => { - const errorMessage = error instanceof Error ? error.message : '保存失败' + const errorMessage = error instanceof Error ? error.message : t('manage.systemSettings.saveFailed') alertStore.showAlert(errorMessage, 'error') }) } @@ -504,6 +504,29 @@ refreshData() + +
+ +
+ + + {{ config.enableChunk === 1 ? t('common.enabled') : t('common.disabled') }} + +
+