Move completed uploads exclusion logic (#6058)

into uploader plugins

fixes #6051

also [fix deprecated usage of done
callback](2511142276)
This commit is contained in:
Mikael Finstad 2025-11-24 19:47:45 +07:00 committed by GitHub
parent c3c16ae069
commit ac12f35f5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 52 additions and 33 deletions

View file

@ -0,0 +1,9 @@
---
"@uppy/aws-s3": patch
"@uppy/core": patch
"@uppy/tus": patch
"@uppy/utils": patch
"@uppy/xhr-upload": patch
---
Fix: Move completed uploads exclusion logic into uploaders. This fixes the problem where postprocessors would not run for already uploaded files.

View file

@ -16,7 +16,7 @@ import type {
import {
createAbortError,
filterFilesToEmitUploadStarted,
filterNonFailedFiles,
filterFilesToUpload,
getAllowedMetaFields,
RateLimitedQueue,
} from '@uppy/utils'
@ -938,7 +938,7 @@ export default class AwsS3Multipart<
if (fileIDs.length === 0) return undefined
const files = this.uppy.getFilesByIds(fileIDs)
const filesFiltered = filterNonFailedFiles(files)
const filesFiltered = filterFilesToUpload(files)
const filesToEmit = filterFilesToEmitUploadStarted(filesFiltered)
this.uppy.emit('upload-start', filesToEmit)

View file

@ -2609,7 +2609,7 @@ describe('src/Core', () => {
expect(infoVisibleEvent.mock.calls.length).toEqual(1)
})
it('should set an info message to be displayed for a period of time before hiding', (done) => {
it('should set an info message to be displayed for a period of time before hiding', async () => {
const infoVisibleEvent = vi.fn()
const infoHiddenEvent = vi.fn()
const core = new Core()
@ -2618,12 +2618,10 @@ describe('src/Core', () => {
core.info('This is the message', 'info', 100)
expect(infoHiddenEvent.mock.calls.length).toEqual(0)
setTimeout(() => {
expect(infoHiddenEvent.mock.calls.length).toEqual(1)
expect(core.getState().info).toEqual([])
// @ts-ignore
done()
}, 110)
await new Promise((resolve) => setTimeout(resolve, 110))
expect(infoHiddenEvent.mock.calls.length).toEqual(1)
expect(core.getState().info).toEqual([])
})
it('should hide an info message', () => {

View file

@ -2233,9 +2233,6 @@ export class Uppy<
]
try {
for (let step = currentUpload.step || 0; step < steps.length; step++) {
if (!currentUpload) {
break
}
const fn = steps[step]
this.setState({
@ -2248,13 +2245,7 @@ export class Uppy<
},
})
// when restoring (e.g. using golden retriever), we don't need to re-upload already successfully uploaded files
// so let's exclude them here:
// https://github.com/transloadit/uppy/issues/5930
const fileIDs = currentUpload.fileIDs.filter((fileID) => {
const file = this.getFile(fileID)
return !file.progress.uploadComplete
})
const { fileIDs } = currentUpload
// TODO give this the `updatedUpload` object as its only parameter maybe?
// Otherwise when more metadata may be added to the upload this would keep getting more parameters
@ -2262,6 +2253,9 @@ export class Uppy<
// Update currentUpload value in case it was modified asynchronously.
currentUpload = getCurrentUpload()
if (!currentUpload) {
break
}
}
} catch (err) {
this.#removeUpload(uploadID)

View file

@ -20,7 +20,9 @@ function createUppy(
const root = document.createElement('div')
document.body.appendChild(root)
return new Uppy().use(Dashboard, {
return new Uppy({
debug: true,
}).use(Dashboard, {
target: root,
inline: true,
...opts,
@ -36,9 +38,11 @@ beforeEach(async () => {
document.body.innerHTML = ''
})
let fileIndex = 0
const createMockFile = ({
size,
name = `${Date.now()}.txt`,
name = `${fileIndex++}.txt`,
type = 'text/plain',
}: {
name?: string
@ -99,7 +103,9 @@ describe('Golden retriever', () => {
requestAt += 1
return HttpResponse.json({})
}
// never reply to subsequent requests (leave them hanging)
// fail subsequent requests
requestAt += 1
return HttpResponse.json({}, { status: 400 })
}),
)
@ -117,10 +123,15 @@ describe('Golden retriever', () => {
uppy.once('upload-success', () => resolve()),
)
const uploadCompletePromise = new Promise<void>((resolve) =>
uppy.once('complete', () => resolve()),
)
// Start the upload
await page.getByRole('button', { name: 'Upload 2 files' }).click()
await uploadFirstFileCompletePromise
await uploadCompletePromise
// reload page and recreate Uppy instance
uppy = createUppy({ withPageReload: true })
@ -140,7 +151,8 @@ describe('Golden retriever', () => {
return HttpResponse.json({})
}
// don't allow more than 1 request
return new HttpResponse({ status: 400 })
requestAt += 1
return HttpResponse.json({}, { status: 400 })
}),
)
@ -152,6 +164,7 @@ describe('Golden retriever', () => {
.toBeVisible()
expect(uppy.getFiles().length).toBe(2)
expect(requestAt).toBe(1) // only the first file upload should have happened so far
})
test('Should not clean up files upon completion if there were failed uploads and it should only make the failed file a ghost', async ({

View file

@ -10,7 +10,7 @@ import type {
import { BasePlugin, EventManager } from '@uppy/core'
import {
filterFilesToEmitUploadStarted,
filterNonFailedFiles,
filterFilesToUpload,
getAllowedMetaFields,
hasProperty,
isNetworkError,
@ -563,7 +563,7 @@ export default class Tus<M extends Meta, B extends Body> extends BasePlugin<
}
async #uploadFiles(files: UppyFile<M, B>[]) {
const filesFiltered = filterNonFailedFiles(files)
const filesFiltered = filterFilesToUpload(files)
const filesToEmit = filterFilesToEmitUploadStarted(filesFiltered)
this.uppy.emit('upload-start', filesToEmit)

View file

@ -1,12 +1,17 @@
import type { UppyFile } from './UppyFile.js'
export function filterNonFailedFiles(
const hasError = (file: UppyFile<any, any>) => 'error' in file && !!file.error
// We don't need to re-upload already successfully uploaded files
// so let's exclude them here:
// https://github.com/transloadit/uppy/issues/5930
// This happens for example when restoring a partially finished session (e.g. using golden retriever).
const isCompleted = (file: UppyFile<any, any>) => file.progress.uploadComplete
export function filterFilesToUpload(
files: UppyFile<any, any>[],
): UppyFile<any, any>[] {
const hasError = (file: UppyFile<any, any>): boolean =>
'error' in file && !!file.error
return files.filter((file) => !hasError(file))
return files.filter((file) => !hasError(file) && !isCompleted(file))
}
// Don't double-emit upload-started for Golden Retriever-restored files that were already started

View file

@ -37,7 +37,7 @@ export { default as fetchWithNetworkError } from './fetchWithNetworkError.js'
export {
filterFilesToEmitUploadStarted,
filterNonFailedFiles,
filterFilesToUpload,
} from './fileFilters.js'
export { default as findAllDOMElements } from './findAllDOMElements.js'

View file

@ -13,7 +13,7 @@ import {
type FetcherOptions,
fetcher,
filterFilesToEmitUploadStarted,
filterNonFailedFiles,
filterFilesToUpload,
getAllowedMetaFields,
internalRateLimitedQueue,
isNetworkError,
@ -535,7 +535,7 @@ export default class XHRUpload<
this.uppy.log('[XHRUpload] Uploading...')
const files = this.uppy.getFilesByIds(fileIDs)
const filesFiltered = filterNonFailedFiles(files)
const filesFiltered = filterFilesToUpload(files)
const filesToEmit = filterFilesToEmitUploadStarted(filesFiltered)
this.uppy.emit('upload-start', filesToEmit)