From 0442d3419211649f53f360c233a2e97f219fb7bc Mon Sep 17 00:00:00 2001 From: cballevre Date: Thu, 16 May 2024 17:44:40 +0200 Subject: [PATCH 1/3] feat: Add a collection to access files into a Nextcloud --- .../cozy-stack-client/src/CozyStackClient.js | 6 +++ .../src/NextcloudFilesCollection.js | 46 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 packages/cozy-stack-client/src/NextcloudFilesCollection.js diff --git a/packages/cozy-stack-client/src/CozyStackClient.js b/packages/cozy-stack-client/src/CozyStackClient.js index 5ffe076548..605bf436f9 100644 --- a/packages/cozy-stack-client/src/CozyStackClient.js +++ b/packages/cozy-stack-client/src/CozyStackClient.js @@ -26,6 +26,10 @@ import MicroEE from 'microee' import errors, { FetchError } from './errors' import logger from './logger' import PromiseCache from './promise-cache' +import { + NEXTCLOUD_FILES_DOCTYPE, + NextcloudFilesCollection +} from './NextcloudFilesCollection' const normalizeUri = uriArg => { let uri = uriArg @@ -100,6 +104,8 @@ class CozyStackClient { return new ShortcutsCollection(this) case APPS_REGISTRY_DOCTYPE: return new AppsRegistryCollection(this) + case NEXTCLOUD_FILES_DOCTYPE: + return new NextcloudFilesCollection(this) default: return new DocumentCollection(doctype, this) } diff --git a/packages/cozy-stack-client/src/NextcloudFilesCollection.js b/packages/cozy-stack-client/src/NextcloudFilesCollection.js new file mode 100644 index 0000000000..34278823e4 --- /dev/null +++ b/packages/cozy-stack-client/src/NextcloudFilesCollection.js @@ -0,0 +1,46 @@ +import DocumentCollection from './DocumentCollection' + +const NEXTCLOUD_FILES_DOCTYPE = 'io.cozy.remote.nextcloud.files' + +const normalizeDoc = DocumentCollection.normalizeDoctypeJsonApi( + NEXTCLOUD_FILES_DOCTYPE +) +const normalizeNextcloudFile = (sourceAccount, path) => file => { + const extendedAttributes = { + ...file.attributes, + path: `${path}${path.endsWith('/') ? '' : '/'}${file.attributes.name}`, + cozyMetadata: { + ...file.attributes.cozyMetadata, + sourceAccount + } + } + + return { + ...normalizeDoc(file, NEXTCLOUD_FILES_DOCTYPE), + ...extendedAttributes + } +} + +class NextcloudFilesCollection extends DocumentCollection { + constructor(stackClient) { + super(NEXTCLOUD_FILES_DOCTYPE, stackClient) + } + + async find(selector) { + if (selector.sourceAccount && selector.path) { + const resp = await this.stackClient.fetchJSON( + 'GET', + `/remote/nextcloud/${selector.sourceAccount}/${selector.path}` + ) + + return { + data: resp.data.map( + normalizeNextcloudFile(selector.sourceAccount, selector.path) + ) + } + } + throw new Error('Not implemented') + } +} + +export { NextcloudFilesCollection, NEXTCLOUD_FILES_DOCTYPE } From 98a9e8e77c242e927acdb7ebfa120aa7439dcb3a Mon Sep 17 00:00:00 2001 From: cballevre Date: Thu, 16 May 2024 17:44:07 +0200 Subject: [PATCH 2/3] feat(nextcloud): Download a file --- docs/api/cozy-stack-client.md | 15 +++++++++++++++ packages/cozy-stack-client/src/FileCollection.js | 10 ++-------- .../src/NextcloudFilesCollection.js | 16 ++++++++++++++++ packages/cozy-stack-client/src/utils.js | 16 ++++++++++++++++ 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/docs/api/cozy-stack-client.md b/docs/api/cozy-stack-client.md index dd81f3acd1..6d74476151 100644 --- a/docs/api/cozy-stack-client.md +++ b/docs/api/cozy-stack-client.md @@ -108,6 +108,9 @@ query to work

Rules determine the behavior of the sharing when changes are made to the shared document See https://docs.cozy.io/en/cozy-stack/sharing-design/#description-of-a-sharing

+
forceDownload
+

Force a download from the given href

+
## Functions @@ -2200,6 +2203,18 @@ See https://docs.cozy.io/en/cozy-stack/sharing-design/#description-of-a-sharing | document | [Sharing](#Sharing) | The document to share. Should have and _id and a name | | sharingType | [SharingType](#SharingType) | The type of the sharing | + + +## forceDownload +Force a download from the given href + +**Kind**: global constant + +| Param | Type | Description | +| --- | --- | --- | +| href | string | The link to download | +| filename | string | The file name to download | + ## getAccessToken() ⇒ string diff --git a/packages/cozy-stack-client/src/FileCollection.js b/packages/cozy-stack-client/src/FileCollection.js index 91a507b618..e7f84beb3d 100644 --- a/packages/cozy-stack-client/src/FileCollection.js +++ b/packages/cozy-stack-client/src/FileCollection.js @@ -6,7 +6,7 @@ import pick from 'lodash/pick' import { MangoQueryOptions } from './mangoIndex' import DocumentCollection, { normalizeDoc } from './DocumentCollection' -import { uri, slugify, formatBytes } from './utils' +import { uri, slugify, formatBytes, forceDownload } from './utils' import { FetchError } from './errors' import { dontThrowNotFoundError } from './Collection' import { getIllegalCharacters } from './getIllegalCharacter' @@ -659,13 +659,7 @@ class FileCollection extends DocumentCollection { * @param {string} filename - The file name to download */ forceFileDownload = (href, filename) => { - const element = document.createElement('a') - element.setAttribute('href', href) - element.setAttribute('download', filename) - element.style.display = 'none' - document.body.appendChild(element) - element.click() - document.body.removeChild(element) + forceDownload(href, filename) } /** diff --git a/packages/cozy-stack-client/src/NextcloudFilesCollection.js b/packages/cozy-stack-client/src/NextcloudFilesCollection.js index 34278823e4..503c6a5ef9 100644 --- a/packages/cozy-stack-client/src/NextcloudFilesCollection.js +++ b/packages/cozy-stack-client/src/NextcloudFilesCollection.js @@ -1,4 +1,5 @@ import DocumentCollection from './DocumentCollection' +import { forceDownload } from './utils' const NEXTCLOUD_FILES_DOCTYPE = 'io.cozy.remote.nextcloud.files' @@ -41,6 +42,21 @@ class NextcloudFilesCollection extends DocumentCollection { } throw new Error('Not implemented') } + + /** + * Download a file from a Nextcloud server + * + */ + async download(file) { + const res = await this.stackClient.fetch( + 'GET', + `/remote/nextcloud/${file.cozyMetadata.sourceAccount}/${file.path}?Dl=1` + ) + const blob = await res.blob() + const href = URL.createObjectURL(blob) + const filename = file.path.split('/').pop() + forceDownload(href, filename) + } } export { NextcloudFilesCollection, NEXTCLOUD_FILES_DOCTYPE } diff --git a/packages/cozy-stack-client/src/utils.js b/packages/cozy-stack-client/src/utils.js index 954dc58b00..336c4aa5db 100644 --- a/packages/cozy-stack-client/src/utils.js +++ b/packages/cozy-stack-client/src/utils.js @@ -69,3 +69,19 @@ export const formatBytes = (bytes, decimals = 2) => { return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] } + +/** + * Force a download from the given href + * + * @param {string} href - The link to download + * @param {string} filename - The file name to download + */ +export const forceDownload = (href, filename) => { + const element = document.createElement('a') + element.setAttribute('href', href) + element.setAttribute('download', filename) + element.style.display = 'none' + document.body.appendChild(element) + element.click() + document.body.removeChild(element) +} From d0cda89ccba153efc35741fff5e0a8f1833d96c1 Mon Sep 17 00:00:00 2001 From: cballevre Date: Wed, 22 May 2024 16:47:49 +0200 Subject: [PATCH 3/3] fix(useQuery): Add function type for definition prop To use the enabled option, the definition is passed in the form of a general function. I've adapted the typing to this practice. This allows me to display the warning also for definition passed with a function --- docs/api/cozy-client/README.md | 2 +- packages/cozy-client/src/hooks/useQuery.js | 4 ++-- packages/cozy-client/types/hooks/useQuery.d.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/api/cozy-client/README.md b/docs/api/cozy-client/README.md index 7e2d1f8c00..39ae3f65a9 100644 --- a/docs/api/cozy-client/README.md +++ b/docs/api/cozy-client/README.md @@ -894,7 +894,7 @@ Fetches a queryDefinition and returns the queryState | Name | Type | Description | | :------ | :------ | :------ | -| `queryDefinition` | [`QueryDefinition`](classes/QueryDefinition.md) | Definition created with Q() | +| `queryDefinition` | [`QueryDefinition`](classes/QueryDefinition.md) | () => [`QueryDefinition`](classes/QueryDefinition.md) | Definition created with Q() | | `options` | `QueryOptions` | Options created with Q() | *Returns* diff --git a/packages/cozy-client/src/hooks/useQuery.js b/packages/cozy-client/src/hooks/useQuery.js index db6f5c3b9e..7bff34c242 100644 --- a/packages/cozy-client/src/hooks/useQuery.js +++ b/packages/cozy-client/src/hooks/useQuery.js @@ -21,7 +21,7 @@ const generateFetchMoreQueryDefinition = queryResult => { /** * Fetches a queryDefinition and returns the queryState * - * @param {QueryDefinition} queryDefinition - Definition created with Q() + * @param {QueryDefinition|(() => QueryDefinition)} queryDefinition - Definition created with Q() * @param {import("../types").QueryOptions} options - Options created with Q() * @returns {import("../types").UseQueryReturnValue} */ @@ -51,7 +51,7 @@ const useQuery = (queryDefinition, options) => { const client = useClient() const queryState = useSelector(() => { - if (options.singleDocData === undefined && queryDefinition.id) { + if (options.singleDocData === undefined && definition?.id) { logger.warn( 'useQuery options.singleDocData will pass to true in a next version of cozy-client, please add it now to prevent any problem in the future.' ) diff --git a/packages/cozy-client/types/hooks/useQuery.d.ts b/packages/cozy-client/types/hooks/useQuery.d.ts index 0465270e1c..d05eae2c1b 100644 --- a/packages/cozy-client/types/hooks/useQuery.d.ts +++ b/packages/cozy-client/types/hooks/useQuery.d.ts @@ -3,9 +3,9 @@ export default useQuery; /** * Fetches a queryDefinition and returns the queryState * - * @param {QueryDefinition} queryDefinition - Definition created with Q() + * @param {QueryDefinition|(() => QueryDefinition)} queryDefinition - Definition created with Q() * @param {import("../types").QueryOptions} options - Options created with Q() * @returns {import("../types").UseQueryReturnValue} */ -declare function useQuery(queryDefinition: QueryDefinition, options: import("../types").QueryOptions): import("../types").UseQueryReturnValue; +declare function useQuery(queryDefinition: QueryDefinition | (() => QueryDefinition), options: import("../types").QueryOptions): import("../types").UseQueryReturnValue; import { QueryDefinition } from "../queries/dsl";