mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-12 12:21:34 +00:00
Add chunked, resumable uploads
Split each uploaded file into 5 MB chunks posted sequentially to a new /api/upload/chunk endpoint, then assemble them on the server via /api/upload/finalize. The client queries /api/upload/chunk/:uploadId/:fileIndex to skip chunks already received, so an interrupted upload resumes instead of restarting. Each chunk is retried with backoff. Path components are sanitized and the same canUpload/library-access checks as /api/upload are enforced. The legacy /api/upload endpoint is left intact. The chunk staging directory defaults to <metadata>/uploads and can be relocated with UPLOAD_TEMP_DIR so large in-progress uploads can be staged off the metadata volume.
This commit is contained in:
parent
cbda0360aa
commit
669e0255a9
4 changed files with 410 additions and 22 deletions
|
|
@ -307,37 +307,87 @@ export default {
|
|||
}
|
||||
},
|
||||
async uploadItem(item) {
|
||||
var form = new FormData()
|
||||
form.set('title', item.title)
|
||||
if (!this.selectedLibraryIsPodcast) {
|
||||
form.set('author', item.author || '')
|
||||
form.set('series', item.series || '')
|
||||
}
|
||||
form.set('library', this.selectedLibraryId)
|
||||
form.set('folder', this.selectedFolderId)
|
||||
const chunkSize = 5 * 1024 * 1024
|
||||
const uploadId = `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||
const totalBytes = item.files.reduce((sum, file) => sum + file.size, 0)
|
||||
let sentBytes = 0
|
||||
|
||||
var index = 0
|
||||
item.files.forEach((file) => {
|
||||
form.set(`${index++}`, file)
|
||||
})
|
||||
for (let fileIndex = 0; fileIndex < item.files.length; fileIndex++) {
|
||||
const file = item.files[fileIndex]
|
||||
const numChunks = Math.max(1, Math.ceil(file.size / chunkSize))
|
||||
|
||||
const config = {
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.lengthComputable) {
|
||||
const progress = {
|
||||
loaded: progressEvent.loaded,
|
||||
total: progressEvent.total
|
||||
}
|
||||
this.updateItemCardProgress(item.index, progress)
|
||||
let uploadedChunks = []
|
||||
try {
|
||||
const existing = await this.$axios.$get(`/api/upload/chunk/${uploadId}/${fileIndex}`)
|
||||
uploadedChunks = existing.chunks || []
|
||||
} catch (error) {
|
||||
uploadedChunks = []
|
||||
}
|
||||
|
||||
for (let chunkIndex = 0; chunkIndex < numChunks; chunkIndex++) {
|
||||
const start = chunkIndex * chunkSize
|
||||
const end = Math.min(file.size, start + chunkSize)
|
||||
if (uploadedChunks.includes(chunkIndex)) {
|
||||
sentBytes += end - start
|
||||
this.updateItemCardProgress(item.index, { loaded: sentBytes, total: totalBytes })
|
||||
continue
|
||||
}
|
||||
|
||||
const form = new FormData()
|
||||
form.set('uploadId', uploadId)
|
||||
form.set('fileIndex', `${fileIndex}`)
|
||||
form.set('chunkIndex', `${chunkIndex}`)
|
||||
form.set('numChunks', `${numChunks}`)
|
||||
form.set('chunk', file.slice(start, end), file.name)
|
||||
|
||||
let attempt = 0
|
||||
while (true) {
|
||||
try {
|
||||
await this.$axios.$post('/api/upload/chunk', form, {
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.lengthComputable) {
|
||||
this.updateItemCardProgress(item.index, { loaded: sentBytes + progressEvent.loaded, total: totalBytes })
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
} catch (error) {
|
||||
attempt++
|
||||
if (attempt >= 5) {
|
||||
console.error('Failed to upload chunk', error)
|
||||
this.$toast.error(error.response?.data || 'Oops, something went wrong...')
|
||||
return false
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt))
|
||||
}
|
||||
}
|
||||
|
||||
sentBytes += end - start
|
||||
this.updateItemCardProgress(item.index, { loaded: sentBytes, total: totalBytes })
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
uploadId,
|
||||
title: item.title,
|
||||
library: this.selectedLibraryId,
|
||||
folder: this.selectedFolderId,
|
||||
files: item.files.map((file, index) => ({
|
||||
index,
|
||||
name: file.name,
|
||||
numChunks: Math.max(1, Math.ceil(file.size / chunkSize))
|
||||
}))
|
||||
}
|
||||
if (!this.selectedLibraryIsPodcast) {
|
||||
payload.author = item.author || ''
|
||||
payload.series = item.series || ''
|
||||
}
|
||||
|
||||
return this.$axios
|
||||
.$post('/api/upload', form, config)
|
||||
.$post('/api/upload/finalize', payload)
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
console.error('Failed to upload item', error)
|
||||
console.error('Failed to finalize upload', error)
|
||||
this.$toast.error(error.response?.data || 'Oops, something went wrong...')
|
||||
return false
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue