Skip to content

Commit

Permalink
fix(uploader): Ensure jobQueue is not reset (empty) during chunk asse…
Browse files Browse the repository at this point in the history
…mbling

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
  • Loading branch information
susnux committed Feb 25, 2025
1 parent 8292965 commit 2c9fd0d
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 37 deletions.
74 changes: 72 additions & 2 deletions cypress/components/UploadPicker/progress.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ describe('UploadPicker: progress handling', () => {
}),
}

// Start paused
getUploader(false, true).pause()

// Mount picker
cy.mount(UploadPicker, {
propsData,
Expand All @@ -74,7 +77,6 @@ describe('UploadPicker: progress handling', () => {
it('has increasing progress bar during non-chunked upload', () => {
// Start in paused mode
const uploader = getUploader()
uploader.pause()

cy.get('@input').attachFile({
// file of 5 MiB
Expand Down Expand Up @@ -150,7 +152,6 @@ describe('UploadPicker: progress handling', () => {

// Start in paused mode
const uploader = getUploader()
uploader.pause()

// 3 MiB/s meaning upload will take 5 seconds
throttleUpload(3 * 1024 * 1024)
Expand Down Expand Up @@ -205,6 +206,74 @@ describe('UploadPicker: progress handling', () => {
cy.get('@progress')
.should('not.be.visible')
})

it('shows the progress bar while assembling', () => {
// Maximum the responses can take
Cypress.config({ defaultCommandTimeout: 7000 })

const { promise, resolve } = Promise.withResolvers<void>()

cy.intercept('PUT', '/remote.php/dav/files/user/file.txt', { statusCode: 201 }).as('upload')
cy.intercept('MKCOL', '/remote.php/dav/uploads/user/*', { statusCode: 201 }).as('mkdir')
cy.intercept('PUT', '/remote.php/dav/uploads/user/*/*', (rq) => {
rq.reply({ statusCode: 201 })
if (rq.url.endsWith('/2')) {
rq.on('response', async () => await promise)
}
}).as('uploadBig')
cy.intercept('MOVE', '/remote.php/dav/uploads/user/*/.file', { statusCode: 201, delay: 1000 }).as('move')

// Start in paused mode
const uploader = getUploader()

cy.get('@input').attachFile([
{
// file of 5 MiB so it is not chunked
fileContent: new Blob([new ArrayBuffer(5 * 1024 * 1024)]),
fileName: 'file.txt',
mimeType: 'text/plain',
encoding: 'utf8',
lastModified: new Date().getTime(),
},
{
// file of 15 MiB so it is chunked in 10MiB and 5 MiB
fileContent: new Blob([new ArrayBuffer(15 * 1024 * 1024)]),
fileName: 'big-file.txt',
mimeType: 'text/plain',
encoding: 'utf8',
lastModified: new Date().getTime(),
},
])

// See there is no progress yet
cy.get('@progress')
.should('be.visible')
.should('have.value', 0)
cy.get('@progressLabel')
.should('contain.text', 'paused')
// start the uploader
.then(() => uploader.start())

// MKCOL was successfully so the upload can begin
cy.wait('@mkdir')

cy.get('@progress', { timeout: 2000 })
.should((el) => expect(el.val()).to.be.greaterThan(10))
.and((el) => expect(el.val()).to.be.lessThan(95))

cy.wait('@upload')
cy.wait('@uploadBig')
.then(() => resolve())

cy.get('@progressLabel')
.should('be.visible')
.and('contain.text', 'assembling')

cy.wait('@move')

cy.get('@progress')
.should('not.be.visible')
})
})

describe('UploadPicker: reset progress on retry', () => {
Expand All @@ -217,6 +286,7 @@ describe('UploadPicker: reset progress on retry', () => {
cy.window()
.then((win) => {
// Internal global variable
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(win as any)._oc_capabilities = { files: { chunked_upload: { max_parallel_count: 1 } } }
})

Expand Down
75 changes: 40 additions & 35 deletions lib/uploader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,46 +545,51 @@ export class Uploader {
chunksQueue.push(this._jobQueue.add(request))
}

try {
// Once all chunks are sent, assemble the final file
await Promise.all(chunksQueue)
this.updateStats()
const request = async () => {
try {
// Once all chunks are sent, assemble the final file
await Promise.all(chunksQueue)

// Assemble the chunks
upload.status = UploadStatus.ASSEMBLING
upload.response = await axios.request({
method: 'MOVE',
url: `${tempUrl}/.file`,
headers: {
...this._customHeaders,
'X-OC-Mtime': Math.floor(file.lastModified / 1000),
'OC-Total-Length': file.size,
Destination: encodedDestinationFile,
},
})
// Assemble the chunks
upload.status = UploadStatus.ASSEMBLING
this.updateStats()

this.updateStats()
upload.status = UploadStatus.FINISHED
logger.debug(`Successfully uploaded ${file.name}`, { file, upload })
resolve(upload)
} catch (error) {
if (isCancel(error) || error instanceof UploadCancelledError) {
upload.status = UploadStatus.CANCELLED
reject(new UploadCancelledError(error))
} else {
upload.status = UploadStatus.FAILED
reject(t('Failed assembling the chunks together'))
}
// Send the assemble request
upload.response = await axios.request({
method: 'MOVE',
url: `${tempUrl}/.file`,
headers: {
...this._customHeaders,
'X-OC-Mtime': Math.floor(file.lastModified / 1000),
'OC-Total-Length': file.size,
Destination: encodedDestinationFile,
},
})
upload.status = UploadStatus.FINISHED
this.updateStats()

// Cleaning up temp directory
axios.request({
method: 'DELETE',
url: `${tempUrl}`,
})
logger.debug(`Successfully uploaded ${file.name}`, { file, upload })
resolve(upload)
} catch (error) {
if (isCancel(error) || error instanceof UploadCancelledError) {
upload.status = UploadStatus.CANCELLED
reject(new UploadCancelledError(error))
} else {
upload.status = UploadStatus.FAILED
reject(t('Failed assembling the chunks together'))
}
// Cleaning up temp directory
axios.request({
method: 'DELETE',
url: `${tempUrl}`,
})
} finally {
// Notify listeners of the upload completion
this._notifyAll(upload)
}
}

// Notify listeners of the upload completion
this._notifyAll(upload)
this._jobQueue.add(request)
} else {
logger.debug('Initializing regular upload', { file, upload })

Expand Down

0 comments on commit 2c9fd0d

Please sign in to comment.