From 2835d8a6f4e39359bb00c57272a0621f5da9b889 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Mon, 6 Jul 2026 18:30:07 +0200 Subject: [PATCH] fix(upload): check if we need to create a folder before creating it There are some cases when we need to create the folder: - sub folders - root folder if `upload` path was set (e.g. `batchUpload('upload', ...)` So the HEAD request is more resilient. Signed-off-by: Ferdinand Thiessen --- .github/workflows/node-test.yml | 1 + __tests__/start-nextcloud-server.js | 2 +- __tests__/uploader/upload.e2e.spec.ts | 55 ++++++++++++++++++++++++++- lib/upload/uploader/UploadFileTree.ts | 21 ++++++++-- 4 files changed, 73 insertions(+), 6 deletions(-) diff --git a/.github/workflows/node-test.yml b/.github/workflows/node-test.yml index 8fe6b01f2..7776d16d4 100644 --- a/.github/workflows/node-test.yml +++ b/.github/workflows/node-test.yml @@ -51,6 +51,7 @@ jobs: CYPRESS_INSTALL_BINARY: 0 run: | npm ci + npx playwright install --with-deps npm run build --if-present - name: Test diff --git a/__tests__/start-nextcloud-server.js b/__tests__/start-nextcloud-server.js index 135972726..152b9af0b 100644 --- a/__tests__/start-nextcloud-server.js +++ b/__tests__/start-nextcloud-server.js @@ -9,7 +9,7 @@ export async function setup(project) { // Start the Nextcloud docker container const ip = await start() await waitOnNextcloud(ip) - await configureNextcloud(['end_to_end_encryption']) + await configureNextcloud([]) project.provide('ip', ip) diff --git a/__tests__/uploader/upload.e2e.spec.ts b/__tests__/uploader/upload.e2e.spec.ts index bccadc093..a28270eba 100644 --- a/__tests__/uploader/upload.e2e.spec.ts +++ b/__tests__/uploader/upload.e2e.spec.ts @@ -63,7 +63,7 @@ describe('Uploader (current API)', () => { await expect(client.getFileContents('/files/admin/test-multi/c.txt', { format: 'text' })).resolves.toBe('content-c') }) - it('should upload a folder structure', async () => { + it('should upload a new folder structure', async () => { const client = getClient() await client.deleteFile('/files/admin/test-folder').catch(() => {}) await client.createDirectory('/files/admin/test-folder') @@ -75,6 +75,59 @@ describe('Uploader (current API)', () => { }) const uploader = new Uploader(false, folder) + const finishedPromise = new Promise((resolve) => uploader.addEventListener('finished', () => resolve())) + await uploader.batchUpload('', [ + fileWithPath('root file', 'root.txt'), + fileWithPath('nested file', 'subdir/nested.txt'), + fileWithPath('deep file', 'subdir/deep/deep.txt'), + ]) + await finishedPromise + + await expect(client.stat('/files/admin/test-folder')).resolves.toEqual(expect.objectContaining({ type: 'directory' })) + await expect(client.getFileContents('/files/admin/test-folder/root.txt', { format: 'text' })).resolves.toBe('root file') + await expect(client.getFileContents('/files/admin/test-folder/subdir/nested.txt', { format: 'text' })).resolves.toBe('nested file') + await expect(client.getFileContents('/files/admin/test-folder/subdir/deep/deep.txt', { format: 'text' })).resolves.toBe('deep file') + }) + + it('should upload a folder structure into a subfolder', async () => { + const client = getClient() + await client.deleteFile('/files/admin/test-folder').catch(() => {}) + await client.createDirectory('/files/admin/test-folder') + + const folder = new Folder({ + owner: 'admin', + root: '/files/admin', + source: `${defaultRemoteURL}/files/admin/test-folder`, + }) + const uploader = new Uploader(false, folder) + + const finishedPromise = new Promise((resolve) => uploader.addEventListener('finished', () => resolve())) + await uploader.batchUpload('upload', [ + fileWithPath('root file', 'root.txt'), + fileWithPath('nested file', 'subdir/nested.txt'), + fileWithPath('deep file', 'subdir/deep/deep.txt'), + ]) + await finishedPromise + + await expect(client.stat('/files/admin/test-folder/upload')).resolves.toEqual(expect.objectContaining({ type: 'directory' })) + await expect(client.getFileContents('/files/admin/test-folder/upload/root.txt', { format: 'text' })).resolves.toBe('root file') + await expect(client.getFileContents('/files/admin/test-folder/upload/subdir/nested.txt', { format: 'text' })).resolves.toBe('nested file') + await expect(client.getFileContents('/files/admin/test-folder/upload/subdir/deep/deep.txt', { format: 'text' })).resolves.toBe('deep file') + }) + + it('should upload a folder structure into an existing subfolder', async () => { + const client = getClient() + await client.deleteFile('/files/admin/test-folder').catch(() => {}) + await client.createDirectory('/files/admin/test-folder') + await client.createDirectory('/files/admin/test-folder/upload') + + const folder = new Folder({ + owner: 'admin', + root: '/files/admin', + source: `${defaultRemoteURL}/files/admin/test-folder`, + }) + const uploader = new Uploader(false, folder) + const finishedPromise = new Promise((resolve) => uploader.addEventListener('finished', () => resolve())) await uploader.batchUpload('upload', [ fileWithPath('root file', 'root.txt'), diff --git a/lib/upload/uploader/UploadFileTree.ts b/lib/upload/uploader/UploadFileTree.ts index 48d8be39a..5f55bd7f8 100644 --- a/lib/upload/uploader/UploadFileTree.ts +++ b/lib/upload/uploader/UploadFileTree.ts @@ -134,10 +134,7 @@ export class UploadFileTree extends Upload implements IUpload { this.uploadedBytes = 0 this.status = UploadStatus.UPLOADING - // if this is not the root of a tree, we need to create the directory first before uploading the children - if (this.#directory.webkitRelativePath) { - await this.#createDirectory(queue) - } + await this.#createDirectory(queue) if (this.needConflictResolution && this.#conflictsCallback) { const nodes = await this.#conflictsCallback( this.#directory.children.map((node) => basename(node.name)), @@ -193,6 +190,22 @@ export class UploadFileTree extends Upload implements IUpload { */ async #createDirectory(queue: PQueue): Promise { await queue.add(async () => { + try { + await axios.head(this.source, { + signal: this.signal, + headers: { + ...this.#customHeaders, + }, + }) + return // directory already exists, no need to create it + } catch (error) { + if (isRequestAborted(error)) { + this.cancel() + return + } + } + + // directory does not exist, we need to create it try { await axios.request({ method: 'MKCOL',