Files
Verso/services/web/frontend/js/infrastructure/batch-file-uploader.ts
T
claudeandClaude Sonnet 4.6 41d38a70ed
Build and Deploy Verso / deploy (push) Successful in 15m39s
Increase upload timeout to 15 min for slow connections
Frontend fetch gets AbortSignal.timeout(15 min) so hung connections
fail cleanly. Server requestTimeout raised from Node default (5 min)
to match, preventing large-file uploads from being cut off server-side.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 16:58:10 +00:00

114 lines
2.6 KiB
TypeScript

import pLimit from 'p-limit'
import getMeta from '@/utils/meta'
import { getErrorMessageForStatusCode } from './http-status-messages'
export type BatchUploadItem = {
file: Blob
name: string
relativePath?: string
}
export type BatchUploadOptions = {
projectId: string
folderId: string
/**
* Maximum number of uploads to run in parallel.
* Must be greater than 0; non-positive values fall back to the default of 3.
*/
concurrency?: number
}
export type UploadResult =
| {
status: 'success'
name: string
relativePath?: string
data: unknown
}
| {
status: 'error'
name: string
relativePath?: string
error: string
}
const DEFAULT_CONCURRENCY = 3
export async function uploadBatch(
items: BatchUploadItem[],
options: BatchUploadOptions
): Promise<UploadResult[]> {
if (items.length === 0) {
return []
}
const concurrency =
options.concurrency && options.concurrency > 0
? options.concurrency
: DEFAULT_CONCURRENCY
const limit = pLimit(concurrency)
return Promise.all(items.map(item => limit(() => uploadOne(item, options))))
}
async function uploadOne(
item: BatchUploadItem,
options: BatchUploadOptions
): Promise<UploadResult> {
const formData = new FormData()
formData.append('qqfile', item.file, item.name)
formData.append('name', item.name)
if (item.relativePath) {
formData.append('relativePath', item.relativePath)
}
const url = `/project/${options.projectId}/upload?folder_id=${options.folderId}`
try {
const response = await fetch(url, {
method: 'POST',
body: formData,
headers: {
'X-CSRF-TOKEN': getMeta('ol-csrfToken'),
},
signal: AbortSignal.timeout(15 * 60 * 1000),
})
if (!response.ok) {
const error = await extractErrorMessage(response)
return {
status: 'error',
name: item.name,
relativePath: item.relativePath,
error,
}
}
const data = await response.json()
return {
status: 'success',
name: item.name,
relativePath: item.relativePath,
data,
}
} catch (err) {
return {
status: 'error',
name: item.name,
relativePath: item.relativePath,
error: err instanceof Error ? err.message : String(err),
}
}
}
async function extractErrorMessage(response: Response): Promise<string> {
try {
const body = await response.json()
if (typeof body?.error === 'string') {
return body.error
}
} catch {
// JSON body not available — fall back to status-code message
}
return getErrorMessageForStatusCode(response.status)
}