fix: upload didn't resume after disconnection (unless it was renamed)
Massimo Melina committed
Jun 9, 2025 at 09:38 UTC
4d030d6b22b962583c02e54dc661cbf6c0851313
4 files changed
+38
-34
frontend/src/BrowseFiles.ts
+2
-2
@@ -14,7 +14,7 @@ import { alertDialog } from './dialog'
14
import useFetchList, { usePath } from './useFetchList'
15
import { useAuthorized } from './login'
16
import { acceptDropFiles } from './upload'
17
-import { enqueueUpload } from './uploadQueue'
17
+import { enqueueUpload, getFilePath } from './uploadQueue'
18
import _ from 'lodash'
19
import { makeOnClickOpen, openFileMenu } from './fileMenu'
20
import { ClipBar } from './clip'
@@ -41,7 +41,7 @@ export function BrowseFiles() {
41
const propsDropFiles = useMemo(() => ({
42
id: 'files-dropper',
43
...acceptDropFiles((files, to) =>
44
- props?.can_upload ? enqueueUpload(files.map(file => ({ file })), location.pathname + to)
44
+ props?.can_upload ? enqueueUpload(files.map(file => ({ file, path: getFilePath(file) })), location.pathname + to)
45
: alertDialog(t("Upload not available"), 'warning')
46
),
47
}), [props])
frontend/src/upload.ts
+18
-15
@@ -54,7 +54,11 @@ export function showUpload() {
54
const size = formatBytes(adding.reduce((a, x) => a + x.file.size, 0))
55
const isMobile = useIsMobile()
56
57
- return h(FlexV, { gap: '.5em', props: acceptDropFiles((files, to) => uploadState.adding.push(...files.map(f => ({ file: ref(f), to })))) },
57
+ return h(FlexV, {
58
+ gap: '.5em',
59
+ props: acceptDropFiles( (files, to) =>
60
+ uploadState.adding.push(...files.map(f => ({ file: ref(f), path: getFilePath(f), to }))) )
61
+ },
62
h(FlexV, { className: 'upload-toolbar' },
63
!props?.can_upload ? t('no_upload_here', "No upload permission for the current folder")
64
: h(FlexV, {},
@@ -104,13 +108,13 @@ export function showUpload() {
108
rec.comment = s || undefined
109
},
110
async edit(rec) {
107
- const value = rec.path || getFilePath(rec.file)
111
+ const was = rec.path
112
const s = await promptDialog(t('upload_name', "Upload with new name"), {
109
- value,
113
+ value: was,
114
onField: el => {
111
- const ofs = value.lastIndexOf('/') + 1 // browsers picking a folder use / as separator even on Windows
112
- const end = value.slice(ofs).lastIndexOf('.')
113
- el.setSelectionRange(ofs, end < 0 ? value.length : ofs + end)
115
+ const ofs = was.lastIndexOf('/') + 1 // browsers picking a folder use / as separator even on Windows
116
+ const end = was.slice(ofs).lastIndexOf('.')
117
+ el.setSelectionRange(ofs, end < 0 ? was.length : ofs + end)
118
},
119
})
120
if (!s) return
@@ -124,7 +128,7 @@ export function showUpload() {
128
[etaStr, formatSpeed(speed), queueStr].filter(Boolean).join(', '),
129
inQ > 0 && iconBtn('delete', ()=> {
130
uploadState.qs = []
127
- abortCurrentUpload()
131
+ abortCurrentUpload(true)
132
}, { title: t`Clear` }),
133
inQ > 0 && iconBtn(paused ? 'play' : 'pause', () => {
134
uploadState.paused = !uploadState.paused
@@ -142,7 +146,7 @@ export function showUpload() {
146
actions: {
147
cancel: f => {
148
if (f === uploadState.uploading)
145
- return abortCurrentUpload()
149
+ return abortCurrentUpload(true)
150
const q = uploadState.qs[idx]
151
_.pull(q.entries, f)
152
if (!q.entries.length)
@@ -156,7 +160,8 @@ export function showUpload() {
160
161
function pickFiles(options: Parameters<typeof selectFiles>[1]) {
162
selectFiles(list => {
159
- uploadState.adding.push( ...Array.from(list || []).filter(simulateBrowserAccept).map(f => ({ file: ref(f) })) )
163
+ uploadState.adding.push( ...Array.from(list || []).filter(simulateBrowserAccept)
164
+ .map(f => ({ file: ref(f), path: getFilePath(f) })) )
165
}, options)
166
}
167
}
@@ -181,7 +186,7 @@ function FileList({ entries, actions }: { entries: ToUpload[], actions: { [icon:
186
cb && iconBtn(icon, () => cb(entries[i]), { className: `action-${icon}` })) ),
187
h('td', {}, formatBytes(e.file.size)),
188
h('td', {},
184
- h('span', {}, e.path || getFilePath(entries[i].file)),
189
+ h('span', {}, e.path),
190
working && h('span', { className: 'upload-progress', title }, formatBytes(partial)),
191
working && hashing && h('span', { className: 'upload-hashing' }, t`Considering resume`, ' (', formatPerc(hashing), ')'),
192
working && h('progress', { className: 'upload-progress-bar', title, max: 1, value: _.round(progress, 3) }), // round for fewer dom updates
@@ -239,11 +244,9 @@ export function UploadStatus({ snapshot, ...props }: { snapshot?: INTERNAL_Snaps
244
[msgErrors, errors]
245
] as const).map(([msg, list], i) =>
246
msg && h('div', { key: i }, msg, h('ul', {},
242
- list.map((x, i) => h('li', { key: i },
243
- x.path || getFilePath(x.file),
244
- prefix(' (', x.error, ')')
245
- ))
246
- ))
247
+ list.map((x, i) =>
248
+ h('li', { key: i }, x.path, prefix(' (', x.error, ')'))
249
+ )))
250
)
251
))
252
}
frontend/src/uploadQueue.ts
+17
-16
@@ -15,7 +15,7 @@ import { hfsEvent, onHfsEvent } from './misc'
15
import i18n from './i18n'
16
const { t } = i18n
17
18
-export interface ToUpload { file: File, comment?: string, path?: string, to?: string, error?: string }
18
+export interface ToUpload { file: File, comment?: string, path: string, to?: string, error?: string }
19
export const uploadState = proxy<{
20
done: (ToUpload & { response?: any })[] // res will contain the response from the server,
21
doneByte: number
@@ -88,7 +88,8 @@ export function resetReloadOnClose() {
88
}
89
90
export async function startUpload(toUpload: ToUpload, to: string, startingResume=0) {
91
- console.debug('start upload', getFilePath(toUpload.file), startingResume)
91
+ const uploadPath = toUpload.path
92
+ console.debug('start upload', uploadPath, startingResume)
93
let resuming = false
94
let preserveTempFile = undefined
95
uploadState.uploading = toUpload
@@ -99,15 +100,16 @@ export async function startUpload(toUpload: ToUpload, to: string, startingResume
100
const splitSize = getHFS().splitUploads
101
const fullSize = toUpload.file.size
102
let offset = startingResume
102
- let splitResume = startingResume // keep track of "split" advancements
103
+ let resume = startingResume // this will advance with splitSize
104
let lastWrittenReceived = 0
105
let stopLooping = false
106
do { // at least one iteration, even for empty files
106
- offset = Math.max(splitResume, lastWrittenReceived)
107
+ offset = Math.max(resume, lastWrittenReceived)
108
const req = currentReq = new XMLHttpRequest()
109
const requestIsOver = pendingPromise()
110
overrideStatus = 0
111
stuckSince = Date.now()
112
+ // beware of 'abort' event: it isn't triggered if connection isn't established yet
113
req.onloadend = async () => {
114
try {
115
currentReq = undefined
@@ -132,8 +134,8 @@ export async function startUpload(toUpload: ToUpload, to: string, startingResume
134
return await wait(2000) // wait before resolving `finished`
135
else {
136
if (splitSize) {
135
- splitResume += splitSize
136
- if (splitResume < fullSize) return // go on with the next chunk
137
+ resume += splitSize
138
+ if (resume < fullSize) return // go on with the next chunk
139
}
140
stopLooping = true
141
waitSecondChunk.resolve() // we finished, no need to wait
@@ -156,7 +158,6 @@ export async function startUpload(toUpload: ToUpload, to: string, startingResume
158
stuckSince = Date.now()
159
lastProgress = e.loaded
160
}
159
- const uploadPath = toUpload.path || getFilePath(toUpload.file)
161
const partial = splitSize && offset + splitSize < fullSize
162
req.open('PUT', to + pathEncode(uploadPath) + buildUrlQueryString({
163
notifications: notificationChannel,
@@ -185,12 +186,11 @@ export async function startUpload(toUpload: ToUpload, to: string, startingResume
186
async function notificationHandler(name: string, data: any) {
187
const {uploading} = uploadState
188
if (!uploading) return
188
- const PREFIX = 'resume hash/'
189
if (name === UPLOAD_RESUMABLE_HASH)
190
- return hfsEvent(PREFIX + data.path, data.hash)
190
+ return hfsEvent(UPLOAD_RESUMABLE_HASH + data.path, data.hash)
191
if (name === UPLOAD_RESUMABLE) {
192
waitSecondChunk.resolve()
193
- if (uploading.path !== data.path) return // is it about current file?
193
+ if (uploadPath !== data.path) return // is it about current file?
194
if (data.written)
195
return lastWrittenReceived = data.written
196
const {size} = data //TODO use toUpload?
@@ -205,7 +205,7 @@ export async function startUpload(toUpload: ToUpload, to: string, startingResume
205
console.debug('upload unchanged')
206
}
207
else { // timestamp may miss if the file is left by old version, or HFS was killed
208
- const hashFromServer = new Promise<any>(res => onHfsEvent(PREFIX + getFilePath(uploading.file), res, { once: true }))
208
+ const hashFromServer = new Promise<any>(res => onHfsEvent(UPLOAD_RESUMABLE_HASH + uploadPath, res, { once: true }))
209
const hashed = await calcHash(uploading.file, size) // therefore, we attempt a check using the hash
210
if (!hashed) return // too late, we are working on another file
211
if (hashed !== await hashFromServer) return console.debug('upload hash mismatch')
@@ -219,7 +219,8 @@ export async function startUpload(toUpload: ToUpload, to: string, startingResume
219
return startUpload(toUpload, to, size)
220
}
221
if (name === UPLOAD_REQUEST_STATUS) {
222
- overrideStatus = data?.[getFilePath(uploading.file)]
222
+ if (uploadPath !== data.path) return // is it about current file?
223
+ overrideStatus = data.status
224
if (overrideStatus >= 400)
225
abortCurrentUpload()
226
return
@@ -264,8 +265,8 @@ export async function startUpload(toUpload: ToUpload, to: string, startingResume
265
}
266
}
267
267
-export function abortCurrentUpload() {
268
- userAborted = true // must track because abort event isn't trigger if connection isn't established yet
268
+export function abortCurrentUpload(userAskedForIt=false) {
269
+ userAborted = userAskedForIt
270
currentReq?.abort()
271
}
272
subscribe(uploadState, () => {
@@ -283,13 +284,13 @@ export async function enqueueUpload(entries: ToUpload[], to=location.pathname) {
284
if (_.remove(entries, x => !simulateBrowserAccept(x.file)).length)
285
await alertDialog(t('upload_file_rejected', "Some files were not accepted"), 'warning')
286
286
- entries = _.uniqBy(entries, x => getFilePath(x.file))
287
+ entries = _.uniqBy(entries, x => x.path)
288
if (!entries.length) return
289
entries = entries.map(x => ({ ...x, file: ref(x.file) })) // avoid valtio to mess with File object
290
const q = _.find(uploadState.qs, { to })
291
if (!q)
292
return uploadState.qs.push({ to, entries })
292
- const missing = _.differenceBy(entries, q.entries, x => getFilePath(x.file))
293
+ const missing = _.differenceBy(entries, q.entries, x => x.path)
294
q.entries.push(...missing.map(ref))
295
}
296
src/upload.ts
+1
-1
@@ -326,7 +326,7 @@ export function uploadWriter(base: VfsNode, baseUri: string, path: string, ctx:
326
ctx.status = status
327
if (msg)
328
ctx.body = msg
329
- notifyClient(ctx, UPLOAD_REQUEST_STATUS, { [path]: status }) // allow browsers to detect failure while still sending body
329
+ notifyClient(ctx, UPLOAD_REQUEST_STATUS, { path, status }) // allow browsers to detect failure while still sending body
330
}
331
}
332