Skip to content

Commit

Permalink
feat: add assembling status to UploadPicker
Browse files Browse the repository at this point in the history
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
  • Loading branch information
skjnldsv committed Dec 18, 2024
1 parent 6184c17 commit e29c6d3
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 22 deletions.
6 changes: 1 addition & 5 deletions cypress/components/UploadPicker/UploadPicker.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,6 @@ describe('UploadPicker valid uploads', () => {
afterEach(() => resetDocument())

it('Uploads a file with chunking', () => {
// Init and reset chunk request spy
const chunksRequestsSpy = cy.spy()

// Intercept tmp upload chunks folder creation
cy.intercept('MKCOL', '/remote.php/dav/uploads/*/web-file-upload*', {
statusCode: 201,
Expand All @@ -151,7 +148,6 @@ describe('UploadPicker valid uploads', () => {
method: 'PUT',
url: '/remote.php/dav/uploads/*/web-file-upload*/*',
}, (req) => {
chunksRequestsSpy()
req.reply({
statusCode: 201,
})
Expand Down Expand Up @@ -193,7 +189,7 @@ describe('UploadPicker valid uploads', () => {
cy.get('[data-cy-upload-picker] .upload-picker__progress')
.as('progress')
.should('not.be.visible')
expect(chunksRequestsSpy).to.have.always.been.callCount(26)
cy.get('@chunks.all').should('have.lengthOf', 26)
})
})

Expand Down
143 changes: 143 additions & 0 deletions cypress/components/UploadPicker/status.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/* eslint-disable no-unused-expressions */
/**
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
// dist file might not be built when running eslint only
// eslint-disable-next-line import/no-unresolved,n/no-missing-import
import { Folder, Permission } from '@nextcloud/files'
import { generateRemoteUrl } from '@nextcloud/router'
import { getUploader, UploadPicker } from '../../../lib/index.ts'

let state: string | undefined
before(() => {
cy.window().then((win) => {
state = win.document.body.innerHTML
})
})

const resetDocument = () => {
if (state) {
cy.window().then((win) => {
win.document.body.innerHTML = state!
})
}
}

describe('UploadPicker: status testing', () => {
beforeEach(() => {
// Make sure we reset the destination
// so other tests do not interfere
const propsData = {
destination: new Folder({
id: 56,
owner: 'user',
source: generateRemoteUrl('dav/files/user'),
permissions: Permission.ALL,
root: '/files/user',
}),
}

// Mount picker
const onPause = cy.spy().as('pausedListener')
const onResume = cy.spy().as('resumedListener')
cy.mount(UploadPicker, {
propsData,
listeners: {
paused: onPause,
resumed: onResume,
},
}).as('uploadPicker')

// Check and init aliases
cy.get('[data-cy-upload-picker] [data-cy-upload-picker-input]').as('input').should('exist')
cy.get('[data-cy-upload-picker] .upload-picker__progress').as('progress').should('exist')
})

afterEach(() => resetDocument())

it('shows paused status on pause', () => {
// Intercept tmp upload chunks folder creation
cy.intercept('MKCOL', '/remote.php/dav/uploads/*/web-file-upload*', {
statusCode: 201,
}).as('init')

// Intercept chunks upload
cy.intercept({
method: 'PUT',
url: '/remote.php/dav/uploads/*/web-file-upload*/*',
}, (req) => {
req.reply({
statusCode: 201,
})
}).as('chunks')

// Intercept final assembly request
const assemblyStartStub = cy.stub().as('assemblyStart')
cy.intercept('MOVE', '/remote.php/dav/uploads/*/web-file-upload*/.file', (req) => {
assemblyStartStub()
req.reply({
statusCode: 204,
// Fake assembling chunks
delay: 5000,
})
}).as('assemblyEnd')

// Start upload
cy.get('@input').attachFile({
// Fake file of 256MB
fileContent: new Blob([new ArrayBuffer(256 * 1024 * 1024)]),
fileName: 'photos.zip',
mimeType: 'application/zip',
encoding: 'utf8',
lastModified: new Date().getTime(),
})

cy.wait('@init').then(() => {
cy.get('[data-cy-upload-picker] .upload-picker__progress')
.as('progress')
.should('be.visible')
})

cy.wait('@chunks').then(() => {
cy.get('[data-cy-upload-picker] .upload-picker__progress')
.as('progress')
.should('be.visible')
cy.get('@progress')
.children('progress')
.should('not.have.value', '0')
cy.get('[data-cy-upload-picker-progress-label]').should('not.contain', 'estimating time left')
cy.get('[data-cy-upload-picker-progress-label]').should('not.contain', 'paused')

cy.wait(1000).then(() => {
getUploader().pause()
})

cy.get('[data-cy-upload-picker-progress-label]').should('contain', 'paused')
cy.get('@pausedListener').should('have.been.calledOnce')

cy.wait(1000).then(() => {
getUploader().start()
})

cy.get('[data-cy-upload-picker-progress-label]').should('not.contain', 'paused')
cy.get('@resumedListener').should('have.been.calledOnce')
})

// Should will retry until success or timeout
cy.get('@assemblyStart', { timeout: 30000 }).should('have.been.calledOnce').then(() => {
cy.get('[data-cy-upload-picker] .upload-picker__progress')
.as('progress')
.should('be.visible')

cy.get('[data-cy-upload-picker-progress-label]').should('not.contain', 'paused')
cy.get('[data-cy-upload-picker-progress-label]').should('contain', 'assembling')
})

cy.wait('@assemblyEnd', { timeout: 60000 }).then(() => {
cy.get('[data-cy-upload-picker] .upload-picker__progress')
.as('progress')
.should('not.be.visible')
})
})
})
2 changes: 2 additions & 0 deletions cypress/support/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import { mount } from '@cypress/vue2'

// @ts-expect-error Mock window so this is an internal property
window._oc_capabilities = { files: {} }
// @ts-expect-error Mock window so this is an internal property
window._oc_debug = true

// Example use:
// cy.mount(MyComponent)
Expand Down
3 changes: 3 additions & 0 deletions l10n/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ msgstr ""
msgid "a few seconds left"
msgstr ""

msgid "assembling"
msgstr ""

msgid "Cancel"
msgstr ""

Expand Down
55 changes: 38 additions & 17 deletions lib/components/UploadPicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,14 @@
</NcActions>

<!-- Progressbar and status -->
<div v-show="isUploading" class="upload-picker__progress">
<div v-show="isUploading" class="upload-picker__progress" data-cy-upload-picker-progress>
<NcProgressBar :aria-label="t('Upload progress')"
:aria-describedby="progressTimeId"
:error="hasFailure"
:value="progress"
size="medium" />
<p :id="progressTimeId">
{{ timeLeft }}
<p :id="progressTimeId" data-cy-upload-picker-progress-label>
{{ status }}
</p>
</div>

Expand Down Expand Up @@ -250,7 +250,7 @@ export default defineComponent({
return {
eta: null as null|ReturnType<typeof makeEta>,
openedMenu: false,
timeLeft: '',
status: '',

newFileMenuEntries: [] as Entry[],
uploadManager: getUploader(),
Expand Down Expand Up @@ -294,16 +294,27 @@ export default defineComponent({
},

hasFailure(): boolean {
return this.queue?.filter((upload: Upload) => upload.status === UploadStatus.FAILED).length !== 0
return this.queue?.some((upload: Upload) => upload.status === UploadStatus.FAILED)
},
isUploading(): boolean {
return this.queue?.length > 0
},
isAssembling(): boolean {
return this.queue?.filter((upload: Upload) => upload.status === UploadStatus.ASSEMBLING).length !== 0
isOnlyAssembling(): boolean {
return !this.queue?.some((upload: Upload) => {
// ignore empty uploads or meta uploads
if (upload.size === 0) {
return false
}
// If all the uploads are assembling or finished, the ongoing task is assembling
return upload.status !== UploadStatus.ASSEMBLING && upload.status !== UploadStatus.FINISHED
})
},
isPaused(): boolean {
return this.uploadManager.info?.status === Status.PAUSED
return this.uploaderStatus === Status.PAUSED
},

uploaderStatus(): Status {
return this.uploadManager.info?.status || Status.IDLE
},

buttonLabel(): string {
Expand Down Expand Up @@ -347,12 +358,17 @@ export default defineComponent({
this.updateStatus()
},

isPaused(isPaused) {
if (isPaused) {
uploaderStatus(status, oldStatus) {
if (status === Status.PAUSED) {
this.$emit('paused', this.queue)
} else {
} else if (oldStatus === Status.PAUSED) {
this.$emit('resumed', this.queue)
}
this.updateStatus()
},

isOnlyAssembling() {
this.updateStatus()
},
},

Expand Down Expand Up @@ -444,28 +460,33 @@ export default defineComponent({

updateStatus() {
if (this.isPaused) {
this.timeLeft = t('paused')
this.status = t('paused')
return
}

if (this.isOnlyAssembling) {
this.status = t('assembling')
return
}

const estimate = Math.round(this.eta!.estimate())
const estimate = Math.round(this.eta?.estimate?.() || 0)

if (estimate === Infinity) {
this.timeLeft = t('estimating time left')
this.status = t('estimating time left')
return
}
if (estimate < 10) {
this.timeLeft = t('a few seconds left')
this.status = t('a few seconds left')
return
}
if (estimate > 60) {
const date = new Date(0)
date.setSeconds(estimate)
const time = date.toISOString().slice(11, 11 + 8)
this.timeLeft = t('{time} left', { time }) // TRANSLATORS time has the format 00:00:00
this.status = t('{time} left', { time }) // TRANSLATORS time has the format 00:00:00
return
}
this.timeLeft = t('{seconds} seconds left', { seconds: estimate })
this.status = t('{seconds} seconds left', { seconds: estimate })
},

setDestination(destination: Folder) {
Expand Down
5 changes: 5 additions & 0 deletions lib/uploader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ export class Uploader {
public pause() {
this._jobQueue.pause()
this._queueStatus = Status.PAUSED
this.updateStats()
logger.debug('Upload paused')
}

/**
Expand All @@ -183,6 +185,7 @@ export class Uploader {
this._jobQueue.start()
this._queueStatus = Status.UPLOADING
this.updateStats()
logger.debug('Upload resumed')
}

/**
Expand Down Expand Up @@ -512,6 +515,8 @@ export class Uploader {
await Promise.all(chunksQueue)
this.updateStats()

// Assemble the chunks
upload.status = UploadStatus.ASSEMBLING
upload.response = await axios.request({
method: 'MOVE',
url: `${tempUrl}/.file`,
Expand Down

0 comments on commit e29c6d3

Please sign in to comment.