From c2d5ea7f505e1d4b66740842004a5d1336b4cd39 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Tue, 4 Jun 2024 02:03:38 +0200 Subject: [PATCH 1/2] refactor: Only import from nextcloud-axios not axios directly Signed-off-by: Ferdinand Thiessen --- lib/uploader.ts | 14 +++++++------- package-lock.json | 17 ++++++++++------- package.json | 6 +++--- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/lib/uploader.ts b/lib/uploader.ts index 4a7ecc41..2d1eddb0 100644 --- a/lib/uploader.ts +++ b/lib/uploader.ts @@ -1,13 +1,13 @@ import type { AxiosError, AxiosResponse } from 'axios' import type { WebDAVClient } from 'webdav' -import { CanceledError } from 'axios' -import { encodePath } from '@nextcloud/paths' +import { getCurrentUser } from '@nextcloud/auth' import { Folder, Permission, davGetClient } from '@nextcloud/files' +import { encodePath } from '@nextcloud/paths' import { generateRemoteUrl } from '@nextcloud/router' -import { getCurrentUser } from '@nextcloud/auth' import { normalize } from 'path' -import axios from '@nextcloud/axios' + +import axios, { isCancel } from '@nextcloud/axios' import PCancelable from 'p-cancelable' import PQueue from 'p-queue' @@ -406,7 +406,7 @@ export class Uploader { throw error } - if (!(error instanceof CanceledError)) { + if (!isCancel(error)) { logger.error(`Chunk ${chunk + 1} ${bufferStart} - ${bufferEnd} uploading failed`, { error, upload }) // TODO: support retrying ? // https://github.com/nextcloud-libraries/nextcloud-upload/issues/5 @@ -439,7 +439,7 @@ export class Uploader { logger.debug(`Successfully uploaded ${file.name}`, { file, upload }) resolve(upload) } catch (error) { - if (!(error instanceof CanceledError)) { + if (!isCancel(error)) { upload.status = UploadStatus.FAILED reject('Failed assembling the chunks together') } else { @@ -486,7 +486,7 @@ export class Uploader { logger.debug(`Successfully uploaded ${file.name}`, { file, upload }) resolve(upload) } catch (error) { - if (error instanceof CanceledError) { + if (isCancel(error)) { upload.status = UploadStatus.FAILED reject(t('Upload has been cancelled')) return diff --git a/package-lock.json b/package-lock.json index e203c052..d0e78272 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,15 +10,15 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@nextcloud/auth": "^2.2.1", - "@nextcloud/axios": "^2.4.0", + "@nextcloud/axios": "^2.5.0", "@nextcloud/dialogs": "^5.2.0", "@nextcloud/files": "^3.3.1", + "@nextcloud/initial-state": "^2.2.0", "@nextcloud/l10n": "^3.0.1", "@nextcloud/logger": "^3.0.1", "@nextcloud/paths": "^2.1.0", "@nextcloud/router": "^3.0.0", - "axios": "^1.6.8", - "buffer": "^6.0.3", + "axios": "^1.7.2", "crypto-browserify": "^3.12.0", "p-cancelable": "^4.0.1", "p-queue": "^8.0.0", @@ -1573,12 +1573,12 @@ } }, "node_modules/@nextcloud/initial-state": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-2.1.0.tgz", - "integrity": "sha512-b92X/GvUPGQJpUQwauyG3D3dHsWowViVLnTtFPSMUc0rXtvYR5CvhkqJRfPC7O7W4VC7+V3q+FWeA+mQWMxN2Q==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-2.2.0.tgz", + "integrity": "sha512-cDW98L5KGGgpS8pzd+05304/p80cyu8U2xSDQGa+kGPTpUFmCbv2qnO5WrwwGTauyjYijCal2bmw82VddSH+Pg==", "engines": { "node": "^20.0.0", - "npm": "^9.0.0" + "npm": "^10.0.0" } }, "node_modules/@nextcloud/l10n": { @@ -3545,6 +3545,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, "funding": [ { "type": "github", @@ -3771,6 +3772,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, "funding": [ { "type": "github", @@ -7070,6 +7072,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, "funding": [ { "type": "github", diff --git a/package.json b/package.json index 8e8ba672..33a98da8 100644 --- a/package.json +++ b/package.json @@ -73,15 +73,15 @@ }, "dependencies": { "@nextcloud/auth": "^2.2.1", - "@nextcloud/axios": "^2.4.0", + "@nextcloud/axios": "^2.5.0", "@nextcloud/dialogs": "^5.2.0", "@nextcloud/files": "^3.3.1", + "@nextcloud/initial-state": "^2.2.0", "@nextcloud/l10n": "^3.0.1", "@nextcloud/logger": "^3.0.1", "@nextcloud/paths": "^2.1.0", "@nextcloud/router": "^3.0.0", - "axios": "^1.6.8", - "buffer": "^6.0.3", + "axios": "^1.7.2", "crypto-browserify": "^3.12.0", "p-cancelable": "^4.0.1", "p-queue": "^8.0.0", From c72f7839c8cd1cac2af1bfc4306d5e7ca7cbc706 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Tue, 4 Jun 2024 03:11:22 +0200 Subject: [PATCH 2/2] feat: Implement upload on public shares using dav endpoint v2 Signed-off-by: Ferdinand Thiessen --- __tests__/utils/uploader.spec.ts | 49 ++++++++++++++++++++++++++++++++ lib/uploader.ts | 24 +++++++++++++--- vitest.config.ts | 5 ++++ 3 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 __tests__/utils/uploader.spec.ts diff --git a/__tests__/utils/uploader.spec.ts b/__tests__/utils/uploader.spec.ts new file mode 100644 index 00000000..75ceff37 --- /dev/null +++ b/__tests__/utils/uploader.spec.ts @@ -0,0 +1,49 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest' +import { Uploader } from '../../lib/uploader.js' + +import type { NextcloudUser } from '@nextcloud/auth' + +const initialState = vi.hoisted(() => ({ loadState: vi.fn() })) +const auth = vi.hoisted(() => ({ getCurrentUser: vi.fn(() => null) })) +vi.mock('@nextcloud/initial-state', () => initialState) +vi.mock('@nextcloud/auth', () => auth) + +describe('uploader', () => { + beforeEach(() => { + vi.resetAllMocks() + const node = document.getElementById('sharingToken') + if (node) { + document.body.removeChild(node) + } + }) + + test('constructor sets default target folder for user', async () => { + auth.getCurrentUser.mockImplementationOnce(() => ({ uid: 'my-user', displayName: 'User', isAdmin: false })) + const uploader = new Uploader() + expect(uploader.destination.source).match(/\/remote\.php\/dav\/files\/my-user\/?$/) + }) + + test('constructor sets default target folder for public share', async () => { + initialState.loadState.mockImplementationOnce((app, key) => app === 'files_sharing' && key === 'sharingToken' ? 'token-123' : null) + const uploader = new Uploader(true) + expect(uploader.destination.source).match(/\/public\.php\/dav\/files\/token-123\/?$/) + }) + + test('constructor sets default target folder for legacy public share', async () => { + const input = document.createElement('input') + input.id = 'sharingToken' + input.value = 'legacy-token' + document.body.appendChild(input) + const uploader = new Uploader(true) + expect(uploader.destination.source).match(/\/public\.php\/dav\/files\/legacy-token\/?$/) + }) + + test('fails if no sharingToken on public share', async () => { + expect(() => new Uploader(true)).toThrow(/No sharing token found/) + }) + + test('fails if not logged in and not on public share', async () => { + expect(() => new Uploader()).toThrow(/User is not logged in/) + expect(initialState.loadState).not.toBeCalled() + }) +}) diff --git a/lib/uploader.ts b/lib/uploader.ts index 2d1eddb0..9bef99cd 100644 --- a/lib/uploader.ts +++ b/lib/uploader.ts @@ -3,6 +3,7 @@ import type { WebDAVClient } from 'webdav' import { getCurrentUser } from '@nextcloud/auth' import { Folder, Permission, davGetClient } from '@nextcloud/files' +import { loadState } from '@nextcloud/initial-state' import { encodePath } from '@nextcloud/paths' import { generateRemoteUrl } from '@nextcloud/router' import { normalize } from 'path' @@ -53,11 +54,26 @@ export class Uploader { this._isPublic = isPublic if (!destinationFolder) { - const owner = getCurrentUser()?.uid - const source = generateRemoteUrl(`dav/files/${owner}`) - if (!owner) { - throw new Error('User is not logged in') + let owner: string + let source: string + + if (isPublic) { + const sharingToken = loadState('files_sharing', 'sharingToken', null) ?? document.querySelector('input#sharingToken')?.value + if (!sharingToken) { + logger.error('No sharing token found for public shares, please specify the destination folder manually.') + throw new Error('No sharing token found.') + } + owner = sharingToken + source = generateRemoteUrl(`dav/files/${sharingToken}`).replace('remote.php', 'public.php') + } else { + const user = getCurrentUser()?.uid + if (!user) { + throw new Error('User is not logged in') + } + owner = user + source = generateRemoteUrl(`dav/files/${owner}`) } + destinationFolder = new Folder({ id: 0, owner, diff --git a/vitest.config.ts b/vitest.config.ts index 672d0138..78fbc934 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -15,6 +15,11 @@ export default async (env) => { exclude: ['lib/utils/l10n.ts'], reporter: ['lcov', 'text'], }, + server: { + deps: { + inline: ['@nextcloud/files'], + }, + }, } as UserConfig return cfg }