Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 65 additions & 4 deletions lib/upload/uploader/Uploader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,20 @@ vi.mock('./UploadFile.ts', () => ({
removeEventListener() {}
dispatchEvent = (() => true) as any
dispatchTypedEvent = (() => true) as any
cancel = vi.fn(() => {

// Mirrors the real `Upload.cancel`: a method that accesses private state,
// so calling it with a foreign `this` throws instead of silently working.
#cancelled = false
get cancelled(): boolean {
return this.#cancelled
}

cancel() {
this.#cancelled = true
if (this.status !== UploadStatus.FINISHED) {
this.status = UploadStatus.CANCELLED
}
})
}

start = async () => {
// simulate progress then finish
Expand Down Expand Up @@ -107,11 +116,20 @@ vi.mock('./UploadFileTree.ts', () => ({
removeEventListener = (() => {}) as any
dispatchEvent = (() => true) as any
dispatchTypedEvent = (() => true) as any
cancel = vi.fn(() => {

// Mirrors the real `Upload.cancel`: a method that accesses private state,
// so calling it with a foreign `this` throws instead of silently working.
#cancelled = false
get cancelled(): boolean {
return this.#cancelled
}

cancel() {
this.#cancelled = true
if (this.status !== UploadStatus.FINISHED) {
this.status = UploadStatus.CANCELLED as TUploadStatus
}
})
}

initialize = () => []
start = async () => {
Expand Down Expand Up @@ -250,6 +268,49 @@ describe('Uploader (current API)', () => {
expect(finished).toHaveBeenCalled()
})

describe('abort signal', () => {
it('cancels a single upload when the signal is aborted', async () => {
const uploader = new Uploader()
const controller = new AbortController()

const upload = await uploader.upload('/hello.txt', new File(['a'], 'hello.txt'), { signal: controller.signal })
expect((upload as unknown as { cancelled: boolean }).cancelled).toBe(false)

controller.abort()
expect((upload as unknown as { cancelled: boolean }).cancelled).toBe(true)
})

it('cancels a batch upload when the signal is aborted', async () => {
const uploader = new Uploader()
const controller = new AbortController()

const uploads = await uploader.batchUpload('/dir', [new File(['a'], 'a.txt')], { signal: controller.signal })
const root = uploads.at(-1) as unknown as { cancelled: boolean }
expect(root.cancelled).toBe(false)

controller.abort()
expect(root.cancelled).toBe(true)
})

it('cancels a single upload when the signal is already aborted', async () => {
const uploader = new Uploader()
const controller = new AbortController()
controller.abort()

const upload = await uploader.upload('/hello.txt', new File(['a'], 'hello.txt'), { signal: controller.signal })
expect((upload as unknown as { cancelled: boolean }).cancelled).toBe(true)
})

it('cancels a batch upload when the signal is already aborted', async () => {
const uploader = new Uploader()
const controller = new AbortController()
controller.abort()

const uploads = await uploader.batchUpload('/dir', [new File(['a'], 'a.txt')], { signal: controller.signal })
expect((uploads.at(-1) as unknown as { cancelled: boolean }).cancelled).toBe(true)
})
})

describe('upload target resolution', () => {
// destination folder source is mocked to https://localhost/remote.php/dav/files/test
const defaultRoot = 'https://localhost/remote.php/dav/files/test'
Expand Down
18 changes: 16 additions & 2 deletions lib/upload/uploader/Uploader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ export class Uploader extends TypedEventTarget<UploaderEventsMap> {
{ ...options, callback, headers },
)
if (options?.signal) {
options.signal.addEventListener('abort', upload.cancel)
this.#attachAbortSignal(options.signal, upload)
}

const uploads = [...upload.initialize(), upload]
Expand All @@ -352,7 +352,7 @@ export class Uploader extends TypedEventTarget<UploaderEventsMap> {
const headers = Object.fromEntries(this.#customHeaders.entries())
const upload = new UploadFile(target, fileHandle, { ...options, headers })
if (options?.signal) {
options.signal.addEventListener('abort', upload.cancel)
this.#attachAbortSignal(options.signal, upload)
}

this.#attachEventListeners(upload)
Expand All @@ -363,6 +363,20 @@ export class Uploader extends TypedEventTarget<UploaderEventsMap> {
return upload
}

/**
* Cancel the upload when the caller provided abort signal is aborted.
*
* @param signal - The abort signal provided by the caller
* @param upload - The upload to cancel when the signal is aborted
*/
#attachAbortSignal(signal: AbortSignal, upload: IUpload): void {
if (signal.aborted) {
upload.cancel()
return
}
signal.addEventListener('abort', () => upload.cancel(), { once: true })
}

/**
* Resolve the absolute upload target for a destination relative to the root folder.
*
Expand Down
Loading