From 4444fe76e0f6f5281a794419c6633912baa50c2a Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Mon, 2 Sep 2024 22:17:56 +0200 Subject: [PATCH 01/19] initial poc --- .env.example | 4 + docs/companion.md | 4 +- .../companion-plugins/google-picker.mdx | 5 + docs/user-interfaces/dashboard.mdx | 2 + e2e/package.json | 1 + packages/@uppy/google-picker/.npmignore | 1 + packages/@uppy/google-picker/CHANGELOG.md | 1 + packages/@uppy/google-picker/LICENSE | 21 +++ packages/@uppy/google-picker/README.md | 51 ++++++ packages/@uppy/google-picker/package.json | 33 ++++ .../@uppy/google-picker/src/GooglePicker.tsx | 98 +++++++++++ packages/@uppy/google-picker/src/index.ts | 1 + packages/@uppy/google-picker/src/locale.ts | 5 + .../@uppy/google-picker/tsconfig.build.json | 35 ++++ packages/@uppy/google-picker/tsconfig.json | 31 ++++ packages/@uppy/provider-views/package.json | 3 + .../provider-views/src/GooglePicker/index.css | 7 + .../provider-views/src/GooglePicker/index.tsx | 155 ++++++++++++++++++ packages/@uppy/provider-views/src/index.ts | 2 + packages/@uppy/provider-views/tsconfig.json | 1 + .../@uppy/remote-sources/tsconfig.build.json | 5 + packages/@uppy/transloadit/src/index.ts | 1 + packages/uppy/package.json | 1 + packages/uppy/src/bundle.ts | 1 + private/dev/Dashboard.js | 12 ++ tsconfig.json | 3 + yarn.lock | 48 ++++++ 27 files changed, 531 insertions(+), 1 deletion(-) create mode 100644 docs/sources/companion-plugins/google-picker.mdx create mode 100644 packages/@uppy/google-picker/.npmignore create mode 100644 packages/@uppy/google-picker/CHANGELOG.md create mode 100644 packages/@uppy/google-picker/LICENSE create mode 100644 packages/@uppy/google-picker/README.md create mode 100644 packages/@uppy/google-picker/package.json create mode 100644 packages/@uppy/google-picker/src/GooglePicker.tsx create mode 100644 packages/@uppy/google-picker/src/index.ts create mode 100644 packages/@uppy/google-picker/src/locale.ts create mode 100644 packages/@uppy/google-picker/tsconfig.build.json create mode 100644 packages/@uppy/google-picker/tsconfig.json create mode 100644 packages/@uppy/provider-views/src/GooglePicker/index.css create mode 100644 packages/@uppy/provider-views/src/GooglePicker/index.tsx diff --git a/.env.example b/.env.example index 7df30462fc..e3a3a14e54 100644 --- a/.env.example +++ b/.env.example @@ -89,3 +89,7 @@ VITE_TRANSLOADIT_TEMPLATE=*** VITE_TRANSLOADIT_SERVICE_URL=https://api2.transloadit.com # Fill in if you want requests sent to Transloadit to be signed: # VITE_TRANSLOADIT_SECRET=*** + +VITE_GOOGLE_PICKER_CLIENT_ID=*** +VITE_GOOGLE_PICKER_API_KEY=*** +VITE_GOOGLE_PICKER_APP_ID=*** diff --git a/docs/companion.md b/docs/companion.md index e58c00a6ee..9d874601ff 100644 --- a/docs/companion.md +++ b/docs/companion.md @@ -22,7 +22,7 @@ OAuth. ## When should I use it? If you want to let users download files from [Box][], [Dropbox][], [Facebook][], -[Google Drive][googledrive], [Google Photos][googlephotos], [Instagram][], +[Google Drive][googledrive], [Google Photos][googlephotos], [Google Picker][googlepicker], [Instagram][], [OneDrive][], [Unsplash][], [Import from URL][url], or [Zoom][] — you need Companion. @@ -476,6 +476,7 @@ the secret, nothing else. | Facebook | `facebook` | `COMPANION_FACEBOOK_KEY`, `COMPANION_FACEBOOK_SECRET`, `COMPANION_FACEBOOK_SECRET_FILE` | | Google Drive | `drive` | `COMPANION_GOOGLE_KEY`, `COMPANION_GOOGLE_SECRET`, `COMPANION_GOOGLE_SECRET_FILE` | | Google Photos | `googlephotos` | `COMPANION_GOOGLE_KEY`, `COMPANION_GOOGLE_SECRET`, `COMPANION_GOOGLE_SECRET_FILE` | +| Google Picker | `googlepicker` | `COMPANION_GOOGLE_PICKER_CLIENT_ID`, `COMPANION_GOOGLE_PICKER_API_KEY`, `COMPANION_GOOGLE_PICKER_APP_ID` | | Instagram | `instagram` | `COMPANION_INSTAGRAM_KEY`, `COMPANION_INSTAGRAM_SECRET`, `COMPANION_INSTAGRAM_SECRET_FILE` | | OneDrive | `onedrive` | `COMPANION_ONEDRIVE_KEY`, `COMPANION_ONEDRIVE_SECRET`, `COMPANION_ONEDRIVE_SECRET_FILE`, `COMPANION_ONEDRIVE_DOMAIN_VALIDATION` (Settings this variable to `true` enables a route that can be used to validate your app with OneDrive) | | Zoom | `zoom` | `COMPANION_ZOOM_KEY`, `COMPANION_ZOOM_SECRET`, `COMPANION_ZOOM_SECRET_FILE`, `COMPANION_ZOOM_VERIFICATION_TOKEN` | @@ -979,6 +980,7 @@ automatically restart when files are changed. [facebook]: /docs/facebook [googledrive]: /docs/google-drive [googlephotos]: /docs/google-photos +[googlepicker]: /docs/google-picker [instagram]: /docs/instagram [onedrive]: /docs/onedrive [unsplash]: /docs/unsplash diff --git a/docs/sources/companion-plugins/google-picker.mdx b/docs/sources/companion-plugins/google-picker.mdx new file mode 100644 index 0000000000..dd4c89a986 --- /dev/null +++ b/docs/sources/companion-plugins/google-picker.mdx @@ -0,0 +1,5 @@ +--- +slug: /google-picker +--- + +**TODO** diff --git a/docs/user-interfaces/dashboard.mdx b/docs/user-interfaces/dashboard.mdx index 340efbd4e4..717d84b13e 100644 --- a/docs/user-interfaces/dashboard.mdx +++ b/docs/user-interfaces/dashboard.mdx @@ -716,6 +716,8 @@ all Uppy plugins. [Google Drive](https://drive.google.com). - [`@uppy/google-photos`](/docs/google-photos) — import from [Google Photos](https://photos.google.com). +- [`@uppy/google-picker`](/docs/google-picker) — import from + [Google Drive](https://drive.google.com). - [`@uppy/instagram`](/docs/instagram) — import from [Instagram](https://instagram.com). - [`@uppy/onedrive`](/docs/onedrive) — import from diff --git a/e2e/package.json b/e2e/package.json index e923ebf3bd..39bf019103 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -26,6 +26,7 @@ "@uppy/golden-retriever": "workspace:^", "@uppy/google-drive": "workspace:^", "@uppy/google-photos": "workspace:^", + "@uppy/google-picker": "workspace:^", "@uppy/image-editor": "workspace:^", "@uppy/informer": "workspace:^", "@uppy/instagram": "workspace:^", diff --git a/packages/@uppy/google-picker/.npmignore b/packages/@uppy/google-picker/.npmignore new file mode 100644 index 0000000000..6c816673f0 --- /dev/null +++ b/packages/@uppy/google-picker/.npmignore @@ -0,0 +1 @@ +tsconfig.* diff --git a/packages/@uppy/google-picker/CHANGELOG.md b/packages/@uppy/google-picker/CHANGELOG.md new file mode 100644 index 0000000000..b96e5b9273 --- /dev/null +++ b/packages/@uppy/google-picker/CHANGELOG.md @@ -0,0 +1 @@ +# @uppy/google-picker diff --git a/packages/@uppy/google-picker/LICENSE b/packages/@uppy/google-picker/LICENSE new file mode 100644 index 0000000000..6f25c43720 --- /dev/null +++ b/packages/@uppy/google-picker/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024 Transloadit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/@uppy/google-picker/README.md b/packages/@uppy/google-picker/README.md new file mode 100644 index 0000000000..20c4ced0b9 --- /dev/null +++ b/packages/@uppy/google-picker/README.md @@ -0,0 +1,51 @@ +# @uppy/google-picker + +Uppy logo: a smiling puppy above a pink upwards arrow + +[![npm version](https://img.shields.io/npm/v/@uppy/google-photos.svg?style=flat-square)](https://www.npmjs.com/package/@uppy/google-photos) +![CI status for Uppy tests](https://github.com/transloadit/uppy/workflows/Tests/badge.svg) +![CI status for Companion tests](https://github.com/transloadit/uppy/workflows/Companion/badge.svg) +![CI status for browser tests](https://github.com/transloadit/uppy/workflows/End-to-end%20tests/badge.svg) + +The Google Photos plugin for Uppy lets users import photos from their Google +Photos account. + +A Companion instance is required for the GooglePhotos plugin to work. Companion +handles authentication with Google, downloads photos from Google Photos and +uploads them to the destination. This saves the user bandwidth, especially +helpful if they are on a mobile connection. + +Uppy is being developed by the folks at [Transloadit](https://transloadit.com), +a versatile file encoding service. + +## Example + +```js +import Uppy from '@uppy/core' +import GooglePhotos from '@uppy/google-photos' + +const uppy = new Uppy() +uppy.use(GooglePhotos, { + // Options +}) +``` + +## Installation + +```bash +$ npm install @uppy/google-photos +``` + +Alternatively, you can also use this plugin in a pre-built bundle from +Transloadit’s CDN: Edgly. In that case `Uppy` will attach itself to the global +`window.Uppy` object. See the +[main Uppy documentation](https://uppy.io/docs/#Installation) for instructions. + +## Documentation + +Documentation for this plugin can be found on the +[Uppy website](https://uppy.io/docs/google-photos). + +## License + +The [MIT License](./LICENSE). diff --git a/packages/@uppy/google-picker/package.json b/packages/@uppy/google-picker/package.json new file mode 100644 index 0000000000..3feb796012 --- /dev/null +++ b/packages/@uppy/google-picker/package.json @@ -0,0 +1,33 @@ +{ + "name": "@uppy/google-picker", + "description": "The Google Picker plugin for Uppy lets users import files from their Google Drive account", + "version": "0.1.0", + "license": "MIT", + "main": "lib/index.js", + "type": "module", + "keywords": [ + "file uploader", + "google drive", + "google picker", + "cloud storage", + "uppy", + "uppy-plugin" + ], + "homepage": "https://uppy.io", + "bugs": { + "url": "https://github.com/transloadit/uppy/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/transloadit/uppy.git" + }, + "dependencies": { + "@uppy/companion-client": "workspace:^", + "@uppy/provider-views": "workspace:^", + "@uppy/utils": "workspace:^", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "workspace:^" + } +} diff --git a/packages/@uppy/google-picker/src/GooglePicker.tsx b/packages/@uppy/google-picker/src/GooglePicker.tsx new file mode 100644 index 0000000000..9bc13e54e0 --- /dev/null +++ b/packages/@uppy/google-picker/src/GooglePicker.tsx @@ -0,0 +1,98 @@ +import { UIPlugin, Uppy } from '@uppy/core' +import { GooglePickerView } from '@uppy/provider-views' +import { + Provider, + getAllowedHosts, + type CompanionPluginOptions, +} from '@uppy/companion-client' +import { h, type ComponentChild } from 'preact' + +import type { UppyFile, Body, Meta } from '@uppy/utils/lib/UppyFile' +import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy.js' + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore We don't want TS to generate types for the package.json +import packageJson from '../package.json' +import locale from './locale.ts' + +export type GooglePickerOptions = CompanionPluginOptions & { + clientId: string, + apiKey: string, + appId: string, +} + +export default class GooglePicker< + M extends Meta, + B extends Body, +> extends UIPlugin { + static VERSION = packageJson.version + + icon: () => h.JSX.Element + + provider: Provider + + files: UppyFile[] + + constructor(uppy: Uppy, opts: GooglePickerOptions) { + super(uppy, opts) + this.type = 'acquirer' + this.files = [] + this.id = this.opts.id || 'GooglePicker' + this.icon = () => ( + + ) + + this.opts.companionAllowedHosts = getAllowedHosts( + this.opts.companionAllowedHosts, + this.opts.companionUrl, + ) + this.provider = new Provider(uppy, { + companionUrl: this.opts.companionUrl, + companionHeaders: this.opts.companionHeaders, + companionKeysParams: this.opts.companionKeysParams, + companionCookiesRule: this.opts.companionCookiesRule, + + provider: 'googlepicker', + pluginId: this.id, + }) + + this.defaultLocale = locale + + this.i18nInit() + this.title = this.i18n('pluginNameGooglePicker') + + this.render = this.render.bind(this) + } + + install(): void { + const { target } = this.opts + if (target) { + this.mount(target, this) + } + } + + uninstall(): void { + this.unmount() + } + + render(): ComponentChild { + return GooglePickerView({ + provider: this.provider, uppy: this.uppy, + clientId: this.opts.clientId, + apiKey: this.opts.apiKey, + appId: this.opts.appId, + }) + } +} diff --git a/packages/@uppy/google-picker/src/index.ts b/packages/@uppy/google-picker/src/index.ts new file mode 100644 index 0000000000..7c93edd7f2 --- /dev/null +++ b/packages/@uppy/google-picker/src/index.ts @@ -0,0 +1 @@ +export { default } from './GooglePicker.tsx' diff --git a/packages/@uppy/google-picker/src/locale.ts b/packages/@uppy/google-picker/src/locale.ts new file mode 100644 index 0000000000..525b41188b --- /dev/null +++ b/packages/@uppy/google-picker/src/locale.ts @@ -0,0 +1,5 @@ +export default { + strings: { + pluginNameGooglePicker: 'Google Picker', + }, +} diff --git a/packages/@uppy/google-picker/tsconfig.build.json b/packages/@uppy/google-picker/tsconfig.build.json new file mode 100644 index 0000000000..99aaf378de --- /dev/null +++ b/packages/@uppy/google-picker/tsconfig.build.json @@ -0,0 +1,35 @@ +{ + "extends": "../../../tsconfig.shared", + "compilerOptions": { + "noImplicitAny": false, + "outDir": "./lib", + "paths": { + "@uppy/companion-client": ["../companion-client/src/index.js"], + "@uppy/companion-client/lib/*": ["../companion-client/src/*"], + "@uppy/provider-views": ["../provider-views/src/index.js"], + "@uppy/provider-views/lib/*": ["../provider-views/src/*"], + "@uppy/utils/lib/*": ["../utils/src/*"], + "@uppy/core": ["../core/src/index.js"], + "@uppy/core/lib/*": ["../core/src/*"] + }, + "resolveJsonModule": false, + "rootDir": "./src", + "skipLibCheck": true + }, + "include": ["./src/**/*.*"], + "exclude": ["./src/**/*.test.ts"], + "references": [ + { + "path": "../companion-client/tsconfig.build.json" + }, + { + "path": "../provider-views/tsconfig.build.json" + }, + { + "path": "../utils/tsconfig.build.json" + }, + { + "path": "../core/tsconfig.build.json" + } + ] +} diff --git a/packages/@uppy/google-picker/tsconfig.json b/packages/@uppy/google-picker/tsconfig.json new file mode 100644 index 0000000000..e5220fb5ab --- /dev/null +++ b/packages/@uppy/google-picker/tsconfig.json @@ -0,0 +1,31 @@ +{ + "extends": "../../../tsconfig.shared", + "compilerOptions": { + "emitDeclarationOnly": false, + "noEmit": true, + "paths": { + "@uppy/companion-client": ["../companion-client/src/index.js"], + "@uppy/companion-client/lib/*": ["../companion-client/src/*"], + "@uppy/provider-views": ["../provider-views/src/index.js"], + "@uppy/provider-views/lib/*": ["../provider-views/src/*"], + "@uppy/utils/lib/*": ["../utils/src/*"], + "@uppy/core": ["../core/src/index.js"], + "@uppy/core/lib/*": ["../core/src/*"], + }, + }, + "include": ["./package.json", "./src/**/*.*"], + "references": [ + { + "path": "../companion-client/tsconfig.build.json", + }, + { + "path": "../provider-views/tsconfig.build.json", + }, + { + "path": "../utils/tsconfig.build.json", + }, + { + "path": "../core/tsconfig.build.json", + }, + ], +} diff --git a/packages/@uppy/provider-views/package.json b/packages/@uppy/provider-views/package.json index 52dd934a4b..dcbd3628b7 100644 --- a/packages/@uppy/provider-views/package.json +++ b/packages/@uppy/provider-views/package.json @@ -26,6 +26,9 @@ "preact": "^10.5.13" }, "devDependencies": { + "@types/gapi": "^0.0.47", + "@types/google.accounts": "^0.0.14", + "@types/google.picker": "^0.0.42", "vitest": "^1.6.0" }, "peerDependencies": { diff --git a/packages/@uppy/provider-views/src/GooglePicker/index.css b/packages/@uppy/provider-views/src/GooglePicker/index.css new file mode 100644 index 0000000000..126a640918 --- /dev/null +++ b/packages/@uppy/provider-views/src/GooglePicker/index.css @@ -0,0 +1,7 @@ +/* https://stackoverflow.com/a/33082658/6519037 */ +.picker-dialog-bg { + z-index: 20000 !important; +} +.picker-dialog { + z-index: 20001 !important; +} diff --git a/packages/@uppy/provider-views/src/GooglePicker/index.tsx b/packages/@uppy/provider-views/src/GooglePicker/index.tsx new file mode 100644 index 0000000000..c8840fd1be --- /dev/null +++ b/packages/@uppy/provider-views/src/GooglePicker/index.tsx @@ -0,0 +1,155 @@ +import { h } from 'preact' +import { Fragment } from 'preact/compat' +import type { Uppy } from '@uppy/core'; +import { useCallback, useEffect, useState } from 'preact/hooks'; + +import './index.css'; + + +const injectedScripts = new Set(); + +// https://stackoverflow.com/a/39008859/6519037 +async function injectScript(src: string) { + if (injectedScripts.has(src)) return; + + await new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = src; + script.addEventListener('load', () => resolve()); + script.addEventListener('error', e => reject(e.error)); + document.head.appendChild(script); + }); + injectedScripts.add(src); +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export default function GooglePickerView({ provider, uppy, clientId, apiKey, appId }: { + provider: unknown, // todo + uppy: Uppy, + clientId: string, + apiKey: string, + appId: string, +}) { + // Authorization scopes required by the API; multiple scopes can be included, separated by spaces. + const scopes = 'https://www.googleapis.com/auth/drive.file'; + // 'https://www.googleapis.com/auth/photoslibrary.readonly' would be for google photos, + // but it doesn't seem to work (see comment below) + + const [accessToken, setAccessToken] = useState(); + const [loading, setLoading] = useState(false); // todo + + useEffect(() => { + (async () => { + try { + await Promise.all([ + injectScript('https://accounts.google.com/gsi/client'), // Google Identity Services + (async () => { + await injectScript('https://apis.google.com/js/api.js'); + await new Promise((resolve) => gapi.load('client:picker', () => resolve())); + await gapi.client.load('https://www.googleapis.com/discovery/v1/apis/drive/v3/rest'); + })(), + ]); + + + // setTokenClient(newTokenClient); + + } catch (err) { + uppy.log(err) + } + })() + }, [uppy]); + + const onPicked = useCallback(async (picked: google.picker.ResponseObject) => { + if (picked.action === google.picker.Action.PICKED) { + // eslint-disable-next-line no-console + console.log('Picker response', JSON.stringify(picked, null, 2)); + // todo add these files along with any metadata needed by companion to download the files, + // like accessToken, clientId etc. + // companion needs to present an endpoint that will call gapi.client.drive.files.get, + // and stream (download/upload) the file to the destination (e.g. tus/s3 etc) + // something like this: + /* const document = picked[google.picker.Response.DOCUMENTS][0]; + const fileId = document[google.picker.Document.ID]; + console.log(fileId); + const res = await gapi.client.drive.files.get({ + fileId, + 'fields': '*', + }); + console.log('Drive API response for first document', JSON.stringify(res.result, null, 2)); + */ + } + }, []); + + const showPicker = useCallback((token: string) => { + const picker = new google.picker.PickerBuilder() + .enableFeature(google.picker.Feature.NAV_HIDDEN) + .enableFeature(google.picker.Feature.MULTISELECT_ENABLED) + .setDeveloperKey(apiKey) + .setAppId(appId) + .setOAuthToken(token) + .addView( + new google.picker.DocsView(google.picker.ViewId.DOCS) + .setIncludeFolders(true) + // Note: setEnableDrives doesn't seem to work + // .setEnableDrives(true) + .setSelectFolderEnabled(true) + ) + // NOTE: photos is broken and results in an error being returned from Google + // .addView(google.picker.ViewId.PHOTOS) + .setCallback(onPicked) + .build(); + + picker.setVisible(true); + }, [apiKey, appId, onPicked]); + + const handleAuthOrRefreshClick = useCallback(async () => { + setLoading(true); + try { + const response = await new Promise((resolve) => { + const tokenClient = google.accounts.oauth2.initTokenClient({ + client_id: clientId, + scope: scopes, + callback: resolve, + }); + + if (accessToken === null) { + // Prompt the user to select a Google Account and ask for consent to share their data + // when establishing a new session. + tokenClient.requestAccessToken({ prompt: 'consent' }); + } else { + // Skip display of account chooser and consent dialog for an existing session. + tokenClient.requestAccessToken({ prompt: '' }); + } + }) + if (response.error !== undefined) { + throw (response); + } + const { access_token: newAccessToken } = response; + setAccessToken(newAccessToken); + + showPicker(newAccessToken); + } finally { + setLoading(false); + } + }, [accessToken, clientId, showPicker]); + + const handleSignoutClick = useCallback(async () => { + if (accessToken == null) return; + if (accessToken) { + await new Promise((resolve) => google.accounts.oauth2.revoke(accessToken, resolve)); + setAccessToken(undefined); + } + }, [accessToken]); + + return ( + <> + {accessToken != null && ( + <> + + + + )} + + + ) +} diff --git a/packages/@uppy/provider-views/src/index.ts b/packages/@uppy/provider-views/src/index.ts index 64c621627e..f3af1e2a5e 100644 --- a/packages/@uppy/provider-views/src/index.ts +++ b/packages/@uppy/provider-views/src/index.ts @@ -4,3 +4,5 @@ export { } from './ProviderView/index.ts' export { default as SearchProviderViews } from './SearchProviderView/index.ts' + +export { default as GooglePickerView } from './GooglePicker/index.tsx' diff --git a/packages/@uppy/provider-views/tsconfig.json b/packages/@uppy/provider-views/tsconfig.json index a76c3b714a..8a0f38c687 100644 --- a/packages/@uppy/provider-views/tsconfig.json +++ b/packages/@uppy/provider-views/tsconfig.json @@ -8,6 +8,7 @@ "@uppy/core": ["../core/src/index.js"], "@uppy/core/lib/*": ["../core/src/*"], }, + "types": ["google.accounts", "google.picker", "gapi"], }, "include": ["./package.json", "./src/**/*.*"], "references": [ diff --git a/packages/@uppy/remote-sources/tsconfig.build.json b/packages/@uppy/remote-sources/tsconfig.build.json index 5399803aac..9e32144ab0 100644 --- a/packages/@uppy/remote-sources/tsconfig.build.json +++ b/packages/@uppy/remote-sources/tsconfig.build.json @@ -16,6 +16,8 @@ "@uppy/google-drive/lib/*": ["../google-drive/src/*"], "@uppy/google-photos": ["../google-photos/src/index.js"], "@uppy/google-photos/lib/*": ["../google-photos/src/*"], + "@uppy/google-picker": ["../google-picker/src/index.js"], + "@uppy/google-picker/lib/*": ["../google-picker/src/*"], "@uppy/instagram": ["../instagram/src/index.js"], "@uppy/instagram/lib/*": ["../instagram/src/*"], "@uppy/onedrive": ["../onedrive/src/index.js"], @@ -54,6 +56,9 @@ { "path": "../google-photos/tsconfig.build.json" }, + { + "path": "../google-picker/tsconfig.build.json" + }, { "path": "../instagram/tsconfig.build.json" }, diff --git a/packages/@uppy/transloadit/src/index.ts b/packages/@uppy/transloadit/src/index.ts index 6d6f60c1b6..003d63406b 100644 --- a/packages/@uppy/transloadit/src/index.ts +++ b/packages/@uppy/transloadit/src/index.ts @@ -334,6 +334,7 @@ export default class Transloadit< addPluginVersion('Facebook', 'uppy-facebook') addPluginVersion('GoogleDrive', 'uppy-google-drive') addPluginVersion('GooglePhotos', 'uppy-google-photos') + addPluginVersion('GooglePicker', 'uppy-google-picker') addPluginVersion('Instagram', 'uppy-instagram') addPluginVersion('OneDrive', 'uppy-onedrive') addPluginVersion('Zoom', 'uppy-zoom') diff --git a/packages/uppy/package.json b/packages/uppy/package.json index e7f8850abf..e2a55d9915 100644 --- a/packages/uppy/package.json +++ b/packages/uppy/package.json @@ -47,6 +47,7 @@ "@uppy/golden-retriever": "workspace:^", "@uppy/google-drive": "workspace:^", "@uppy/google-photos": "workspace:^", + "@uppy/google-picker": "workspace:^", "@uppy/image-editor": "workspace:^", "@uppy/informer": "workspace:^", "@uppy/instagram": "workspace:^", diff --git a/packages/uppy/src/bundle.ts b/packages/uppy/src/bundle.ts index a16e05078f..c995fc24c1 100644 --- a/packages/uppy/src/bundle.ts +++ b/packages/uppy/src/bundle.ts @@ -42,6 +42,7 @@ export { default as Dropbox } from '@uppy/dropbox' export { default as Facebook } from '@uppy/facebook' export { default as GoogleDrive } from '@uppy/google-drive' export { default as GooglePhotos } from '@uppy/google-photos' +export { default as GooglePicker } from '@uppy/google-picker' export { default as Instagram } from '@uppy/instagram' export { default as OneDrive } from '@uppy/onedrive' export { default as RemoteSources } from '@uppy/remote-sources' diff --git a/private/dev/Dashboard.js b/private/dev/Dashboard.js index bf6ac7c14e..ff15d31468 100644 --- a/private/dev/Dashboard.js +++ b/private/dev/Dashboard.js @@ -16,6 +16,7 @@ import Audio from '@uppy/audio' import Compressor from '@uppy/compressor' import GoogleDrive from '@uppy/google-drive' import english from '@uppy/locales/lib/en_US.js' +import GooglePicker from '@uppy/google-picker' /* eslint-enable import/no-extraneous-dependencies */ import generateSignatureIfSecret from './generateSignatureIfSecret.js' @@ -30,6 +31,9 @@ const { VITE_TRANSLOADIT_SECRET: TRANSLOADIT_SECRET, VITE_TRANSLOADIT_TEMPLATE: TRANSLOADIT_TEMPLATE, VITE_TRANSLOADIT_SERVICE_URL: TRANSLOADIT_SERVICE_URL, + VITE_GOOGLE_PICKER_API_KEY: GOOGLE_PICKER_API_KEY, + VITE_GOOGLE_PICKER_CLIENT_ID: GOOGLE_PICKER_CLIENT_ID, + VITE_GOOGLE_PICKER_APP_ID: GOOGLE_PICKER_APP_ID, } = import.meta.env const companionAllowedHosts = @@ -125,6 +129,14 @@ export default () => { // .use(Zoom, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) // .use(Url, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) // .use(Unsplash, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) + .use(GooglePicker, { + target: Dashboard, + companionUrl: COMPANION_URL, + companionAllowedHosts, + clientId: GOOGLE_PICKER_CLIENT_ID, + apiKey: GOOGLE_PICKER_API_KEY, + appId: GOOGLE_PICKER_APP_ID, + }) .use(RemoteSources, { companionUrl: COMPANION_URL, sources: [ diff --git a/tsconfig.json b/tsconfig.json index bcc25fbd62..ab23ff9580 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -49,6 +49,9 @@ { "path": "./packages/@uppy/google-photos/tsconfig.build.json", }, + { + "path": "./packages/@uppy/google-picker/tsconfig.build.json", + }, { "path": "./packages/@uppy/image-editor/tsconfig.build.json", }, diff --git a/yarn.lock b/yarn.lock index 193e4a510e..38eb434e7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7405,6 +7405,27 @@ __metadata: languageName: node linkType: hard +"@types/gapi@npm:^0.0.47": + version: 0.0.47 + resolution: "@types/gapi@npm:0.0.47" + checksum: 10/b8104688ef132190cb661b461b912a3f6f07ce589eb90ab4bff4acdfaa9bbb8a6321be1119e865db89bf46dfc00cab2141764839535518cf63a8e2caa19f475e + languageName: node + linkType: hard + +"@types/google.accounts@npm:^0.0.14": + version: 0.0.14 + resolution: "@types/google.accounts@npm:0.0.14" + checksum: 10/0332acd210eaad1904d28a9de2081da796cb8c22e4f61bbe0768729c71d1de1606355abc0615907505b9f4ac28694911b9722a6a4e6ee563c21f747e9e1c32b5 + languageName: node + linkType: hard + +"@types/google.picker@npm:^0.0.42": + version: 0.0.42 + resolution: "@types/google.picker@npm:0.0.42" + checksum: 10/7e428495807c840f30ff3eab63fbfc4b9760ba20cbf977b94915f2222f678bb29c5bf73eff6f285f661b293127ddfbbedd4d8b2075d102c272ab941f63fa7d78 + languageName: node + linkType: hard + "@types/graceful-fs@npm:^4.1.2, @types/graceful-fs@npm:^4.1.3": version: 4.1.9 resolution: "@types/graceful-fs@npm:4.1.9" @@ -8644,6 +8665,7 @@ __metadata: nock: "npm:^13.1.3" node-schedule: "npm:2.1.1" prom-client: "npm:15.1.2" + safe-stringify: "npm:^1.1.1" serialize-error: "npm:^11.0.0" serialize-javascript: "npm:^6.0.0" supertest: "npm:6.2.4" @@ -8820,6 +8842,19 @@ __metadata: languageName: unknown linkType: soft +"@uppy/google-picker@workspace:^, @uppy/google-picker@workspace:packages/@uppy/google-picker": + version: 0.0.0-use.local + resolution: "@uppy/google-picker@workspace:packages/@uppy/google-picker" + dependencies: + "@uppy/companion-client": "workspace:^" + "@uppy/provider-views": "workspace:^" + "@uppy/utils": "workspace:^" + preact: "npm:^10.5.13" + peerDependencies: + "@uppy/core": "workspace:^" + languageName: unknown + linkType: soft + "@uppy/image-editor@workspace:*, @uppy/image-editor@workspace:^, @uppy/image-editor@workspace:packages/@uppy/image-editor": version: 0.0.0-use.local resolution: "@uppy/image-editor@workspace:packages/@uppy/image-editor" @@ -8892,6 +8927,9 @@ __metadata: version: 0.0.0-use.local resolution: "@uppy/provider-views@workspace:packages/@uppy/provider-views" dependencies: + "@types/gapi": "npm:^0.0.47" + "@types/google.accounts": "npm:^0.0.14" + "@types/google.picker": "npm:^0.0.42" "@uppy/utils": "workspace:^" classnames: "npm:^2.2.6" nanoid: "npm:^5.0.0" @@ -8972,6 +9010,7 @@ __metadata: "@uppy/facebook": "workspace:^" "@uppy/google-drive": "workspace:^" "@uppy/google-photos": "workspace:^" + "@uppy/google-picker": "workspace:^" "@uppy/instagram": "workspace:^" "@uppy/onedrive": "workspace:^" "@uppy/unsplash": "workspace:^" @@ -13404,6 +13443,7 @@ __metadata: "@uppy/golden-retriever": "workspace:^" "@uppy/google-drive": "workspace:^" "@uppy/google-photos": "workspace:^" + "@uppy/google-picker": "workspace:^" "@uppy/image-editor": "workspace:^" "@uppy/informer": "workspace:^" "@uppy/instagram": "workspace:^" @@ -26303,6 +26343,13 @@ __metadata: languageName: node linkType: hard +"safe-stringify@npm:^1.1.1": + version: 1.1.1 + resolution: "safe-stringify@npm:1.1.1" + checksum: 10/5f06510a8adfa9fbc3045021b31e9eb8b5e68f5525d0ea75cb64ceaf7cf7c449abd562f2f5dbd1e5318b046159eaece2d834f4047ccedb941574a9186c0eeb70 + languageName: node + linkType: hard + "safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.0.2, safer-buffer@npm:^2.1.0, safer-buffer@npm:~2.1.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" @@ -29429,6 +29476,7 @@ __metadata: "@uppy/golden-retriever": "workspace:^" "@uppy/google-drive": "workspace:^" "@uppy/google-photos": "workspace:^" + "@uppy/google-picker": "workspace:^" "@uppy/image-editor": "workspace:^" "@uppy/informer": "workspace:^" "@uppy/instagram": "workspace:^" From f0090bc2b691276616a338234c7f95f796763438 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Fri, 15 Nov 2024 19:10:43 +0800 Subject: [PATCH 02/19] improvements - split into two plugins - implement photos picker - auto login - save access token in local storage - document - handle photos/files picked and send to companion - add new hook useStore for making it easier to use localStorage data in react - add new hook useUppyState for making it easier to use uppy state from react - add new hook useUppyPluginState for making it easier to plugin state from react - fix css error --- .env.example | 5 + docs/companion.md | 16 +- .../companion-plugins/companion-options.mdx | 69 +++ .../companion-plugins/google-drive-picker.mdx | 134 +++++ .../google-photos-picker.mdx | 120 +++++ .../companion-plugins/google-picker.mdx | 5 - docs/user-interfaces/dashboard.mdx | 6 +- e2e/package.json | 3 +- packages/@uppy/core/src/Uppy.ts | 44 +- packages/@uppy/core/src/locale.ts | 4 + packages/@uppy/core/src/useStore.ts | 28 + packages/@uppy/core/src/useUppyState.ts | 43 ++ .../.npmignore | 0 .../@uppy/google-drive-picker/CHANGELOG.md | 1 + .../LICENSE | 0 packages/@uppy/google-drive-picker/README.md | 18 + .../package.json | 4 +- .../src/GoogleDrivePicker.tsx | 155 ++++++ .../@uppy/google-drive-picker/src/index.ts | 1 + .../@uppy/google-drive-picker/src/locale.ts | 3 + .../tsconfig.build.json | 0 .../tsconfig.json | 0 .../@uppy/google-photos-picker/.npmignore | 1 + .../@uppy/google-photos-picker/CHANGELOG.md | 1 + packages/@uppy/google-photos-picker/LICENSE | 21 + packages/@uppy/google-photos-picker/README.md | 18 + .../@uppy/google-photos-picker/package.json | 33 ++ .../src/GooglePhotosPicker.tsx | 146 ++++++ .../@uppy/google-photos-picker/src/index.ts | 1 + .../@uppy/google-photos-picker/src/locale.ts | 3 + .../google-photos-picker/tsconfig.build.json | 35 ++ .../@uppy/google-photos-picker/tsconfig.json | 31 ++ packages/@uppy/google-picker/CHANGELOG.md | 1 - packages/@uppy/google-picker/README.md | 51 -- .../@uppy/google-picker/src/GooglePicker.tsx | 98 ---- packages/@uppy/google-picker/src/index.ts | 1 - packages/@uppy/google-picker/src/locale.ts | 5 - .../src/GooglePicker/GooglePickerView.tsx | 482 ++++++++++++++++++ .../provider-views/src/GooglePicker/index.css | 7 - .../provider-views/src/GooglePicker/index.tsx | 155 ------ packages/@uppy/provider-views/src/index.ts | 2 +- packages/@uppy/provider-views/src/style.scss | 8 + .../@uppy/remote-sources/tsconfig.build.json | 5 - packages/@uppy/transloadit/src/index.ts | 3 +- packages/uppy/package.json | 3 +- packages/uppy/src/bundle.ts | 3 +- private/dev/Dashboard.js | 11 +- tsconfig.json | 5 +- yarn.lock | 36 +- 49 files changed, 1448 insertions(+), 377 deletions(-) create mode 100644 docs/sources/companion-plugins/companion-options.mdx create mode 100644 docs/sources/companion-plugins/google-drive-picker.mdx create mode 100644 docs/sources/companion-plugins/google-photos-picker.mdx delete mode 100644 docs/sources/companion-plugins/google-picker.mdx create mode 100644 packages/@uppy/core/src/useStore.ts create mode 100644 packages/@uppy/core/src/useUppyState.ts rename packages/@uppy/{google-picker => google-drive-picker}/.npmignore (100%) create mode 100644 packages/@uppy/google-drive-picker/CHANGELOG.md rename packages/@uppy/{google-picker => google-drive-picker}/LICENSE (100%) create mode 100644 packages/@uppy/google-drive-picker/README.md rename packages/@uppy/{google-picker => google-drive-picker}/package.json (81%) create mode 100644 packages/@uppy/google-drive-picker/src/GoogleDrivePicker.tsx create mode 100644 packages/@uppy/google-drive-picker/src/index.ts create mode 100644 packages/@uppy/google-drive-picker/src/locale.ts rename packages/@uppy/{google-picker => google-drive-picker}/tsconfig.build.json (100%) rename packages/@uppy/{google-picker => google-drive-picker}/tsconfig.json (100%) create mode 100644 packages/@uppy/google-photos-picker/.npmignore create mode 100644 packages/@uppy/google-photos-picker/CHANGELOG.md create mode 100644 packages/@uppy/google-photos-picker/LICENSE create mode 100644 packages/@uppy/google-photos-picker/README.md create mode 100644 packages/@uppy/google-photos-picker/package.json create mode 100644 packages/@uppy/google-photos-picker/src/GooglePhotosPicker.tsx create mode 100644 packages/@uppy/google-photos-picker/src/index.ts create mode 100644 packages/@uppy/google-photos-picker/src/locale.ts create mode 100644 packages/@uppy/google-photos-picker/tsconfig.build.json create mode 100644 packages/@uppy/google-photos-picker/tsconfig.json delete mode 100644 packages/@uppy/google-picker/CHANGELOG.md delete mode 100644 packages/@uppy/google-picker/README.md delete mode 100644 packages/@uppy/google-picker/src/GooglePicker.tsx delete mode 100644 packages/@uppy/google-picker/src/index.ts delete mode 100644 packages/@uppy/google-picker/src/locale.ts create mode 100644 packages/@uppy/provider-views/src/GooglePicker/GooglePickerView.tsx delete mode 100644 packages/@uppy/provider-views/src/GooglePicker/index.css delete mode 100644 packages/@uppy/provider-views/src/GooglePicker/index.tsx diff --git a/.env.example b/.env.example index e3a3a14e54..b3a63baee1 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,8 @@ COMPANION_PREAUTH_SECRET=development2 # NOTE: Only enable this in development. Enabling it in production is a security risk COMPANION_ALLOW_LOCAL_URLS=true +COMPANION_ENABLE_GOOGLE_PICKER_ENDPOINT=true + # to enable S3 COMPANION_AWS_KEY="YOUR AWS KEY" COMPANION_AWS_SECRET="YOUR AWS SECRET" @@ -90,6 +92,9 @@ VITE_TRANSLOADIT_SERVICE_URL=https://api2.transloadit.com # Fill in if you want requests sent to Transloadit to be signed: # VITE_TRANSLOADIT_SECRET=*** +# For Google Photos Picker and Google Drive Picker: VITE_GOOGLE_PICKER_CLIENT_ID=*** + +# For Google Drive Picker VITE_GOOGLE_PICKER_API_KEY=*** VITE_GOOGLE_PICKER_APP_ID=*** diff --git a/docs/companion.md b/docs/companion.md index 4a9f0f1d80..da36f41877 100644 --- a/docs/companion.md +++ b/docs/companion.md @@ -22,9 +22,10 @@ OAuth. ## When should I use it? If you want to let users download files from [Box][], [Dropbox][], [Facebook][], -[Google Drive][googledrive], [Google Photos][googlephotos], [Google Picker][googlepicker], [Instagram][], -[OneDrive][], [Unsplash][], [Import from URL][url], or [Zoom][] — you need -Companion. +[Google Drive][googledrive], [Google Photos][googlephotos], [Google Drive +Picker][googledrivepicker], [Google Photos Picker][googlephotospicker], +[Instagram][], [OneDrive][], [Unsplash][], [Import from URL][url], or [Zoom][] — +you need Companion. Companion supports the same [uploaders](/docs/guides/choosing-uploader) as Uppy: [Tus](/docs/tus), [AWS S3](/docs/aws-s3), and [regular multipart](/docs/tus). @@ -478,7 +479,6 @@ the secret, nothing else. | Facebook | `facebook` | `COMPANION_FACEBOOK_KEY`, `COMPANION_FACEBOOK_SECRET`, `COMPANION_FACEBOOK_SECRET_FILE` | | Google Drive | `drive` | `COMPANION_GOOGLE_KEY`, `COMPANION_GOOGLE_SECRET`, `COMPANION_GOOGLE_SECRET_FILE` | | Google Photos | `googlephotos` | `COMPANION_GOOGLE_KEY`, `COMPANION_GOOGLE_SECRET`, `COMPANION_GOOGLE_SECRET_FILE` | -| Google Picker | `googlepicker` | `COMPANION_GOOGLE_PICKER_CLIENT_ID`, `COMPANION_GOOGLE_PICKER_API_KEY`, `COMPANION_GOOGLE_PICKER_APP_ID` | | Instagram | `instagram` | `COMPANION_INSTAGRAM_KEY`, `COMPANION_INSTAGRAM_SECRET`, `COMPANION_INSTAGRAM_SECRET_FILE` | | OneDrive | `onedrive` | `COMPANION_ONEDRIVE_KEY`, `COMPANION_ONEDRIVE_SECRET`, `COMPANION_ONEDRIVE_SECRET_FILE`, `COMPANION_ONEDRIVE_DOMAIN_VALIDATION` (Settings this variable to `true` enables a route that can be used to validate your app with OneDrive) | | Zoom | `zoom` | `COMPANION_ZOOM_KEY`, `COMPANION_ZOOM_SECRET`, `COMPANION_ZOOM_SECRET_FILE`, `COMPANION_ZOOM_VERIFICATION_TOKEN` | @@ -722,6 +722,11 @@ as well as Set this to `true` to enable the [URL functionalily](https://uppy.io/docs/url/). Default: `false`. +#### `enableGooglePickerEndpoint` `COMPANION_ENABLE_GOOGLE_PICKER_ENDPOINT` + +Set this to `true` to enable the Google Picker (Photos / Drive) functionality. +Default: `false`. + ### Events The object returned by `companion.app()` also has a property `companionEmitter` @@ -982,7 +987,8 @@ automatically restart when files are changed. [facebook]: /docs/facebook [googledrive]: /docs/google-drive [googlephotos]: /docs/google-photos -[googlepicker]: /docs/google-picker +[googledrivepicker]: /docs/google-drive-picker +[googlephotospicker]: /docs/google-photos-picker [instagram]: /docs/instagram [onedrive]: /docs/onedrive [unsplash]: /docs/unsplash diff --git a/docs/sources/companion-plugins/companion-options.mdx b/docs/sources/companion-plugins/companion-options.mdx new file mode 100644 index 0000000000..a1cc4e0af0 --- /dev/null +++ b/docs/sources/companion-plugins/companion-options.mdx @@ -0,0 +1,69 @@ +--- +slug: /companion-options +--- + +# Common Companion options + +These are the options common to all Uppy plugins that use Companion. + +## `id` + +A unique identifier for this plugin (`string`, default is a unique ID for each +plugin). + +## `title` + +Title / name shown in the UI, such as Dashboard tabs (`string`, default is the +name of the plugin). + +## `target` + +DOM element, CSS selector, or plugin to place the drag and drop area into +(`string`, `Element`, `Function`, or `UIPlugin`, default: +[`Dashboard`](/docs/dashboard)). + +## `companionUrl` + +URL to a [Companion](/docs/companion) instance (`string`, default: `null`). + +## `companionHeaders` + +Custom headers that should be sent along to [Companion](/docs/companion) on +every request (`Object`, default: `{}`). + +## `companionAllowedHosts` + +The valid and authorised URL(s) from which OAuth responses should be accepted +(`string` or `RegExp` or `Array`, default: `companionUrl`). + +This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is +useful when you have your [Companion](/docs/companion) running on several hosts. +Otherwise, the default value should do fine. + +## `companionCookiesRule` + +This option correlates to the +[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials) +(`string`, default: `'same-origin'`). + +This tells the plugin whether to send cookies to [Companion](/docs/companion). + +## `locale` + +An object with `strings` property containing additional i18n strings. The key is +the i18n key and the value is the English string. + +Example: + +```js +{ + strings: { + someKey: 'Some English string', + }, +} +``` + +## `storage` + +A custom storage to be used for the plugin’s persistent data. Type `AsyncStore`, +default is `LocalStorage`. diff --git a/docs/sources/companion-plugins/google-drive-picker.mdx b/docs/sources/companion-plugins/google-drive-picker.mdx new file mode 100644 index 0000000000..f411e4e84b --- /dev/null +++ b/docs/sources/companion-plugins/google-drive-picker.mdx @@ -0,0 +1,134 @@ +--- +slug: /google-drive-picker +--- + +# Google Drive Picker + +The `@uppy/google-drive` plugin lets users import files from their +[Google Drive](https://drive.google.com) account using the new +[Picker API](https://developers.google.com/drive/picker). The benefit of using +this API over the traditional Google Drive API (which the +[Google Drive plugin](./google-drive.mdx) uses) is that the Picker API requires +less verification from Google. Drawbacks of the Picker API include less control +and inability to select folders. See also +[Google Photos Picker](./google-photos-picker.mdx). + +## When should I use this? + +When you want to let users import files from their +[Google Drive](https://drive.google.com) account. + +A [Companion](/docs/companion) instance is required for the Google Drive Picker +plugin to work. Companion downloads the files from Google Drive, and uploads +them to the destination. This saves the user bandwidth, especially helpful if +they are on a mobile connection. + +You can self-host Companion or get a hosted version with any +[Transloadit plan](https://transloadit.com/pricing/). + + + + +```shell +npm install @uppy/google-drive-picker +``` + + + + + +```shell +yarn add @uppy/google-drive-picker +``` + + + + + + {` + import { Uppy, GoogleDrivePicker } from "{{UPPY_JS_URL}}" + const uppy = new Uppy() + uppy.use(GoogleDrivePicker, { + // Options + }) + `} + + + + +## Use + +Using Google Drive Picker requires setup in both Uppy and Companion. + +### Initial setup + +To sign up for API keys, go to the +[Google Developer Console](https://console.developers.google.com/). + +Create a project for your app if you don’t have one yet. + +- On the project’s dashboard, enable the Google Picker API (for Google Drive). +- Create an API key. +- Create an OAuth 2.0 Client ID with the correct Authorized JavaScript origins. + +### Use in Uppy + +```js {10-13} showLineNumbers +import Uppy from '@uppy/core'; +import Dashboard from '@uppy/dashboard'; +import GoogleDrivePicker from '@uppy/google-drive-picker'; + +import '@uppy/core/dist/style.min.css'; +import '@uppy/dashboard/dist/style.min.css'; + +new Uppy() + .use(Dashboard, { inline: true, target: '#dashboard' }) + .use(GoogleDrivePicker, { + companionUrl: 'https://your-companion.com', + clientId: 'From Google Developer Console', + apiKey: 'From Google Developer Console', + appId: 'From Google Developer Console', + }); +``` + +### Use with Transloadit + +```js +import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; +import GoogleDrivePicker from '@uppy/google-drive-picker'; + +uppy.use(GoogleDrivePicker, { + companionUrl: COMPANION_URL, + companionAllowedHosts: COMPANION_ALLOWED_HOSTS, + clientId: 'From Google Developer Console', + apiKey: 'From Google Developer Console', + appId: 'From Google Developer Console', +}); +``` + +### Use in Companion + +Companion is used to download/upload the picked files. Companion supports this +plugin out-of-the-box, however it must be enabled in Companion with the +`enableGooglePickerEndpoint` / `COMPANION_ENABLE_GOOGLE_PICKER_ENDPOINT` option. +For this plugin, all credentials are public (non-secret) and provided in the +frontend. + +## API + +### Options + +#### [Common Companion options...](./companion-options.mdx) + +#### `clientId` + +The client ID from the [Initial setup](#initial-setup) (`string`). + +#### `apiKey` + +The API key from the [Initial setup](#initial-setup) (`string`). + +#### `appId` + +The App ID is the project ID which can be found in the URL in the Google +Developer Console (`string`). diff --git a/docs/sources/companion-plugins/google-photos-picker.mdx b/docs/sources/companion-plugins/google-photos-picker.mdx new file mode 100644 index 0000000000..f7ec25e6ee --- /dev/null +++ b/docs/sources/companion-plugins/google-photos-picker.mdx @@ -0,0 +1,120 @@ +--- +slug: /google-photos-picker +--- + +# Google Photos Picker + +The `@uppy/google-photos` plugin lets users import files from their +[Google Photos](https://photos.google.com) account using the new +[Picker API](https://developers.google.com/photos/picker). The benefit of using +this API over the traditional Google Photos API (which the +[Google Photos plugin](./google-photos.mdx) uses) is that the Picker API +requires less verification from Google. Drawbacks of the Picker API include less +control and inability to select shared albums. See also +[Google Drive Picker](./google-drive-picker.mdx). + +## When should I use this? + +When you want to let users import files from their +[Google Photos](https://photos.google.com) account. + +A [Companion](/docs/companion) instance is required for the Google Photos Picker +plugin to work. Companion downloads the files from Google Photos, and uploads +them to the destination. This saves the user bandwidth, especially helpful if +they are on a mobile connection. + +You can self-host Companion or get a hosted version with any +[Transloadit plan](https://transloadit.com/pricing/). + + + + +```shell +npm install @uppy/google-photos-picker +``` + + + + + +```shell +yarn add @uppy/google-photos-picker +``` + + + + + + {` + import { Uppy, GooglePhotosPicker } from "{{UPPY_JS_URL}}" + const uppy = new Uppy() + uppy.use(GooglePhotosPicker, { + // Options + }) + `} + + + + +## Use + +Using Google Photos Picker requires setup in both Uppy and Companion. + +### Initial setup + +To sign up for API keys, go to the +[Google Developer Console](https://console.developers.google.com/). + +Create a project for your app if you don’t have one yet. + +- On the project’s dashboard, enable the Google Photos Picker API. +- Create an OAuth 2.0 Client ID with the correct Authorized JavaScript origins. + +### Use in Uppy + +```js {10-13} showLineNumbers +import Uppy from '@uppy/core'; +import Dashboard from '@uppy/dashboard'; +import GooglePhotosPicker from '@uppy/google-photos-picker'; + +import '@uppy/core/dist/style.min.css'; +import '@uppy/dashboard/dist/style.min.css'; + +new Uppy() + .use(Dashboard, { inline: true, target: '#dashboard' }) + .use(GooglePhotosPicker, { + companionUrl: 'https://your-companion.com', + clientId: 'From Google Developer Console', + }); +``` + +### Use with Transloadit + +```js +import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; +import GooglePhotosPicker from '@uppy/google-photos-picker'; + +uppy.use(GooglePhotosPicker, { + companionUrl: COMPANION_URL, + companionAllowedHosts: COMPANION_ALLOWED_HOSTS, + clientId: 'From Google Developer Console', +}); +``` + +### Use in Companion + +Companion is used to download/upload the picked files. Companion supports this +plugin out-of-the-box, however it must be enabled in Companion with the +`enableGooglePickerEndpoint` / `COMPANION_ENABLE_GOOGLE_PICKER_ENDPOINT` option. +For this plugin, all credentials are public (non-secret) and provided in the +frontend. + +## API + +### Options + +#### [Common Companion options...](./companion-options.mdx) + +#### `clientId` + +The client ID from the [Initial setup](#initial-setup) (`string`). diff --git a/docs/sources/companion-plugins/google-picker.mdx b/docs/sources/companion-plugins/google-picker.mdx deleted file mode 100644 index dd4c89a986..0000000000 --- a/docs/sources/companion-plugins/google-picker.mdx +++ /dev/null @@ -1,5 +0,0 @@ ---- -slug: /google-picker ---- - -**TODO** diff --git a/docs/user-interfaces/dashboard.mdx b/docs/user-interfaces/dashboard.mdx index 717d84b13e..07d6d4fbd2 100644 --- a/docs/user-interfaces/dashboard.mdx +++ b/docs/user-interfaces/dashboard.mdx @@ -716,8 +716,10 @@ all Uppy plugins. [Google Drive](https://drive.google.com). - [`@uppy/google-photos`](/docs/google-photos) — import from [Google Photos](https://photos.google.com). -- [`@uppy/google-picker`](/docs/google-picker) — import from - [Google Drive](https://drive.google.com). +- [`@uppy/google-drive-picker`](/docs/google-drive-picker) — import from + [Google Drive](https://drive.google.com) using the new Picker API. +- [`@uppy/google-photos-picker`](/docs/google-photos-picker) — import from + [Google Photos](https://drive.google.com) using the new Picker API. - [`@uppy/instagram`](/docs/instagram) — import from [Instagram](https://instagram.com). - [`@uppy/onedrive`](/docs/onedrive) — import from diff --git a/e2e/package.json b/e2e/package.json index 39bf019103..c835aa7c36 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -25,8 +25,9 @@ "@uppy/form": "workspace:^", "@uppy/golden-retriever": "workspace:^", "@uppy/google-drive": "workspace:^", + "@uppy/google-drive-picker": "workspace:^", "@uppy/google-photos": "workspace:^", - "@uppy/google-picker": "workspace:^", + "@uppy/google-photos-picker": "workspace:^", "@uppy/image-editor": "workspace:^", "@uppy/informer": "workspace:^", "@uppy/instagram": "workspace:^", diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index f8e25e538e..61e2bd3ed1 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -142,8 +142,25 @@ export type UnknownProviderPluginState = { currentFolderId: PartialTreeId username: string | null } + +export interface AsyncStore { + getItem: (key: string) => Promise + setItem: (key: string, value: string) => Promise + removeItem: (key: string) => Promise +} + +/** + * This is a base for a provider that does not necessarily use the Companion-assisted OAuth2 flow + */ +export interface BaseProviderPlugin { + title: string + icon: () => h.JSX.Element + storage: AsyncStore +} + /* - * UnknownProviderPlugin can be any Companion plugin (such as Google Drive). + * UnknownProviderPlugin can be any Companion plugin (such as Google Drive) + * that uses the Companion-assisted OAuth flow. * As the plugins are passed around throughout Uppy we need a generic type for this. * It may seems like duplication, but this type safe. Changing the type of `storage` * will error in the `Provider` class of @uppy/companion-client and vice versa. @@ -154,18 +171,12 @@ export type UnknownProviderPluginState = { export type UnknownProviderPlugin< M extends Meta, B extends Body, -> = UnknownPlugin & { - title: string - rootFolderId: string | null - files: UppyFile[] - icon: () => h.JSX.Element - provider: CompanionClientProvider - storage: { - getItem: (key: string) => Promise - setItem: (key: string, value: string) => Promise - removeItem: (key: string) => Promise +> = UnknownPlugin & + BaseProviderPlugin & { + rootFolderId: string | null + files: UppyFile[] + provider: CompanionClientProvider } -} /* * UnknownSearchProviderPlugin can be any search Companion plugin (such as Unsplash). @@ -185,11 +196,10 @@ export type UnknownSearchProviderPluginState = { export type UnknownSearchProviderPlugin< M extends Meta, B extends Body, -> = UnknownPlugin & { - title: string - icon: () => h.JSX.Element - provider: CompanionClientSearchProvider -} +> = UnknownPlugin & + BaseProviderPlugin & { + provider: CompanionClientSearchProvider + } export interface UploadResult { successful?: UppyFile[] diff --git a/packages/@uppy/core/src/locale.ts b/packages/@uppy/core/src/locale.ts index 39f3fee3c3..f1d91e4270 100644 --- a/packages/@uppy/core/src/locale.ts +++ b/packages/@uppy/core/src/locale.ts @@ -41,6 +41,9 @@ export default { openFolderNamed: 'Open folder %{name}', cancel: 'Cancel', logOut: 'Log out', + logIn: 'Log in', + pickFiles: 'Pick files', + pickPhotos: 'Pick photos', filter: 'Filter', resetFilter: 'Reset filter', loading: 'Loading...', @@ -63,5 +66,6 @@ export default { additionalRestrictionsFailed: '%{count} additional restrictions were not fulfilled', unnamed: 'Unnamed', + pleaseWait: 'Please wait', }, } diff --git a/packages/@uppy/core/src/useStore.ts b/packages/@uppy/core/src/useStore.ts new file mode 100644 index 0000000000..a01f6f2d85 --- /dev/null +++ b/packages/@uppy/core/src/useStore.ts @@ -0,0 +1,28 @@ +import { useCallback, useEffect, useState } from 'preact/hooks' + +import type { AsyncStore } from './Uppy' + +export default function useStore( + store: AsyncStore, + key: string, +): [string | undefined | null, (v: string | null) => Promise] { + const [value, setValueState] = useState() + useEffect(() => { + ;(async () => { + setValueState(await store.getItem(key)) + })() + }, [key, store]) + + const setValue = useCallback( + async (v: string | null) => { + setValueState(v) + if (v == null) { + return store.removeItem(key) + } + return store.setItem(key, v) + }, + [key, store], + ) + + return [value, setValue] +} diff --git a/packages/@uppy/core/src/useUppyState.ts b/packages/@uppy/core/src/useUppyState.ts new file mode 100644 index 0000000000..35518d1921 --- /dev/null +++ b/packages/@uppy/core/src/useUppyState.ts @@ -0,0 +1,43 @@ +import type { Body, Meta } from '@uppy/utils/lib/UppyFile' +import { useMemo, useCallback } from 'preact/hooks' +import { useSyncExternalStore } from 'preact/compat' + +import type { PluginOpts } from './BasePlugin' +import type BasePlugin from './BasePlugin' +import type { State, Uppy } from './Uppy' + +// todo merge with @uppy/react? +export function useUppyState< + M extends Meta = Meta, + B extends Body = Body, + T = any, +>(uppy: Uppy, selector: (state: State) => T): T { + const subscribe = useMemo( + () => uppy.store.subscribe.bind(uppy.store), + [uppy.store], + ) + const getSnapshot = useCallback(() => uppy.store.getState(), [uppy.store]) + + return selector(useSyncExternalStore(subscribe, getSnapshot)) +} + +export function useUppyPluginState< + PS extends Record, + O extends PluginOpts, + M extends Meta = Meta, + B extends Body = Body, +>( + plugin: BasePlugin, +): [Partial, (...args: Parameters) => void] { + const setPluginState = useCallback( + (...args) => plugin.setPluginState(...args), + [plugin], + ) + return [ + useUppyState( + plugin.uppy, + (state) => (state.plugins[plugin.id] ?? {}) as PS, + ), + setPluginState, + ] +} diff --git a/packages/@uppy/google-picker/.npmignore b/packages/@uppy/google-drive-picker/.npmignore similarity index 100% rename from packages/@uppy/google-picker/.npmignore rename to packages/@uppy/google-drive-picker/.npmignore diff --git a/packages/@uppy/google-drive-picker/CHANGELOG.md b/packages/@uppy/google-drive-picker/CHANGELOG.md new file mode 100644 index 0000000000..f663421d73 --- /dev/null +++ b/packages/@uppy/google-drive-picker/CHANGELOG.md @@ -0,0 +1 @@ +# @uppy/google-drive-picker diff --git a/packages/@uppy/google-picker/LICENSE b/packages/@uppy/google-drive-picker/LICENSE similarity index 100% rename from packages/@uppy/google-picker/LICENSE rename to packages/@uppy/google-drive-picker/LICENSE diff --git a/packages/@uppy/google-drive-picker/README.md b/packages/@uppy/google-drive-picker/README.md new file mode 100644 index 0000000000..40d85ae66a --- /dev/null +++ b/packages/@uppy/google-drive-picker/README.md @@ -0,0 +1,18 @@ +# @uppy/google-drive-picker + +Uppy logo: a smiling puppy above a pink upwards arrow + +[![npm version](https://img.shields.io/npm/v/@uppy/google-drive-picker.svg?style=flat-square)](https://www.npmjs.com/package/@uppy/google-drive-picker) +![CI status for Uppy tests](https://github.com/transloadit/uppy/workflows/Tests/badge.svg) +![CI status for Companion tests](https://github.com/transloadit/uppy/workflows/Companion/badge.svg) +![CI status for browser tests](https://github.com/transloadit/uppy/workflows/End-to-end%20tests/badge.svg) + +The Google Drive Picker plugin for Uppy lets users import files from their +Google Drive account using the new Picker API. + +Documentation for this plugin can be found on the +[Uppy website](https://uppy.io/docs/google-drive-picker). + +## License + +The [MIT License](./LICENSE). diff --git a/packages/@uppy/google-picker/package.json b/packages/@uppy/google-drive-picker/package.json similarity index 81% rename from packages/@uppy/google-picker/package.json rename to packages/@uppy/google-drive-picker/package.json index 3feb796012..fca1a0aea3 100644 --- a/packages/@uppy/google-picker/package.json +++ b/packages/@uppy/google-drive-picker/package.json @@ -1,6 +1,6 @@ { - "name": "@uppy/google-picker", - "description": "The Google Picker plugin for Uppy lets users import files from their Google Drive account", + "name": "@uppy/google-drive-picker", + "description": "The Google Drive Picker plugin for Uppy lets users import files from their Google Drive account", "version": "0.1.0", "license": "MIT", "main": "lib/index.js", diff --git a/packages/@uppy/google-drive-picker/src/GoogleDrivePicker.tsx b/packages/@uppy/google-drive-picker/src/GoogleDrivePicker.tsx new file mode 100644 index 0000000000..cb8864fab0 --- /dev/null +++ b/packages/@uppy/google-drive-picker/src/GoogleDrivePicker.tsx @@ -0,0 +1,155 @@ +import { h } from 'preact' +import { UIPlugin, Uppy } from '@uppy/core' +import { GooglePickerView } from '@uppy/provider-views' +import { + RequestClient, + type CompanionPluginOptions, + tokenStorage, +} from '@uppy/companion-client' + +import type { Body, Meta } from '@uppy/utils/lib/UppyFile' +import { + type PickedItem, + type PluginState, +} from '@uppy/provider-views/lib/GooglePicker/GooglePickerView.js' +import type { AsyncStore, BaseProviderPlugin } from '@uppy/core/lib/Uppy.js' + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore We don't want TS to generate types for the package.json +import packageJson from '../package.json' +import locale from './locale.ts' + +const Icon = () => ( + +) + +export type GoogleDrivePickerOptions = CompanionPluginOptions & { + clientId: string + apiKey: string + appId: string +} + +export default class GoogleDrivePicker< + M extends Meta & { width: number; height: number }, + B extends Body, + > + extends UIPlugin + implements BaseProviderPlugin +{ + static VERSION = packageJson.version + + static requestClientId = GoogleDrivePicker.name + + type = 'acquirer' + + icon = Icon + + storage: AsyncStore + + defaultLocale = locale + + constructor(uppy: Uppy, opts: GoogleDrivePickerOptions) { + super(uppy, opts) + this.id = this.opts.id || 'GoogleDrivePicker' + this.storage = this.opts.storage || tokenStorage + + this.i18nInit() + this.title = this.i18n('pluginNameGoogleDrive') + + const client = new RequestClient(uppy, { + pluginId: this.id, + provider: 'url', + companionUrl: this.opts.companionUrl, + companionHeaders: this.opts.companionHeaders, + companionCookiesRule: this.opts.companionCookiesRule, + }) + + this.uppy.registerRequestClient(GoogleDrivePicker.requestClientId, client) + } + + install(): void { + const { target } = this.opts + if (target) { + this.mount(target, this) + } + } + + uninstall(): void { + this.unmount() + } + + private handleFilesPicked = async ( + files: PickedItem[], + accessToken: string, + ) => { + this.uppy.addFiles( + files.map(({ id, mimeType, name, ...rest }) => { + return { + source: this.id, + name, + type: mimeType, + data: { + size: null, // defer to companion to determine size + }, + isRemote: true, + remote: { + companionUrl: this.opts.companionUrl, + url: `${this.opts.companionUrl}/google-picker/get`, + body: { + fileId: id, + accessToken, + ...rest, + }, + requestClientId: GoogleDrivePicker.requestClientId, + }, + } + }), + ) + } + + render = () => ( + + ) +} diff --git a/packages/@uppy/google-drive-picker/src/index.ts b/packages/@uppy/google-drive-picker/src/index.ts new file mode 100644 index 0000000000..b4f53a6d4e --- /dev/null +++ b/packages/@uppy/google-drive-picker/src/index.ts @@ -0,0 +1 @@ +export { default } from './GoogleDrivePicker.tsx' diff --git a/packages/@uppy/google-drive-picker/src/locale.ts b/packages/@uppy/google-drive-picker/src/locale.ts new file mode 100644 index 0000000000..6dcfedeef9 --- /dev/null +++ b/packages/@uppy/google-drive-picker/src/locale.ts @@ -0,0 +1,3 @@ +export default { + strings: {}, +} diff --git a/packages/@uppy/google-picker/tsconfig.build.json b/packages/@uppy/google-drive-picker/tsconfig.build.json similarity index 100% rename from packages/@uppy/google-picker/tsconfig.build.json rename to packages/@uppy/google-drive-picker/tsconfig.build.json diff --git a/packages/@uppy/google-picker/tsconfig.json b/packages/@uppy/google-drive-picker/tsconfig.json similarity index 100% rename from packages/@uppy/google-picker/tsconfig.json rename to packages/@uppy/google-drive-picker/tsconfig.json diff --git a/packages/@uppy/google-photos-picker/.npmignore b/packages/@uppy/google-photos-picker/.npmignore new file mode 100644 index 0000000000..6c816673f0 --- /dev/null +++ b/packages/@uppy/google-photos-picker/.npmignore @@ -0,0 +1 @@ +tsconfig.* diff --git a/packages/@uppy/google-photos-picker/CHANGELOG.md b/packages/@uppy/google-photos-picker/CHANGELOG.md new file mode 100644 index 0000000000..99eddaf743 --- /dev/null +++ b/packages/@uppy/google-photos-picker/CHANGELOG.md @@ -0,0 +1 @@ +# @uppy/google-photos-picker diff --git a/packages/@uppy/google-photos-picker/LICENSE b/packages/@uppy/google-photos-picker/LICENSE new file mode 100644 index 0000000000..6f25c43720 --- /dev/null +++ b/packages/@uppy/google-photos-picker/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024 Transloadit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/@uppy/google-photos-picker/README.md b/packages/@uppy/google-photos-picker/README.md new file mode 100644 index 0000000000..fb360e218c --- /dev/null +++ b/packages/@uppy/google-photos-picker/README.md @@ -0,0 +1,18 @@ +# @uppy/google-photos-picker + +Uppy logo: a smiling puppy above a pink upwards arrow + +[![npm version](https://img.shields.io/npm/v/@uppy/google-photos-picker.svg?style=flat-square)](https://www.npmjs.com/package/@uppy/google-photos-picker) +![CI status for Uppy tests](https://github.com/transloadit/uppy/workflows/Tests/badge.svg) +![CI status for Companion tests](https://github.com/transloadit/uppy/workflows/Companion/badge.svg) +![CI status for browser tests](https://github.com/transloadit/uppy/workflows/End-to-end%20tests/badge.svg) + +The Google Photos Picker plugin for Uppy lets users import photos from their +Google Photos account using the new Picker API. + +Documentation for this plugin can be found on the +[Uppy website](https://uppy.io/docs/google-photos-picker). + +## License + +The [MIT License](./LICENSE). diff --git a/packages/@uppy/google-photos-picker/package.json b/packages/@uppy/google-photos-picker/package.json new file mode 100644 index 0000000000..4c66e196f0 --- /dev/null +++ b/packages/@uppy/google-photos-picker/package.json @@ -0,0 +1,33 @@ +{ + "name": "@uppy/google-photos-picker", + "description": "The Google Photos Picker plugin for Uppy lets users import files from their Google Photos account", + "version": "0.1.0", + "license": "MIT", + "main": "lib/index.js", + "type": "module", + "keywords": [ + "file uploader", + "google photos", + "google picker", + "cloud storage", + "uppy", + "uppy-plugin" + ], + "homepage": "https://uppy.io", + "bugs": { + "url": "https://github.com/transloadit/uppy/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/transloadit/uppy.git" + }, + "dependencies": { + "@uppy/companion-client": "workspace:^", + "@uppy/provider-views": "workspace:^", + "@uppy/utils": "workspace:^", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "workspace:^" + } +} diff --git a/packages/@uppy/google-photos-picker/src/GooglePhotosPicker.tsx b/packages/@uppy/google-photos-picker/src/GooglePhotosPicker.tsx new file mode 100644 index 0000000000..8f7fa5992b --- /dev/null +++ b/packages/@uppy/google-photos-picker/src/GooglePhotosPicker.tsx @@ -0,0 +1,146 @@ +import { h } from 'preact' +import { UIPlugin, Uppy } from '@uppy/core' +import { GooglePickerView } from '@uppy/provider-views' +import { + RequestClient, + type CompanionPluginOptions, + tokenStorage, +} from '@uppy/companion-client' + +import type { Body, Meta } from '@uppy/utils/lib/UppyFile' +import { + type PickedItem, + type PluginState, +} from '@uppy/provider-views/lib/GooglePicker/GooglePickerView.js' +import type { AsyncStore, BaseProviderPlugin } from '@uppy/core/lib/Uppy.js' + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore We don't want TS to generate types for the package.json +import packageJson from '../package.json' +import locale from './locale.ts' + +const Icon = () => ( + +) + +export type GooglePhotosPickerOptions = CompanionPluginOptions & { + clientId: string +} + +export default class GooglePhotosPicker< + M extends Meta & { width: number; height: number }, + B extends Body, + > + extends UIPlugin + implements BaseProviderPlugin +{ + static VERSION = packageJson.version + + static requestClientId = GooglePhotosPicker.name + + type = 'acquirer' + + icon = Icon + + storage: AsyncStore + + defaultLocale = locale + + constructor(uppy: Uppy, opts: GooglePhotosPickerOptions) { + super(uppy, opts) + this.id = this.opts.id || 'GooglePhotosPicker' + this.storage = this.opts.storage || tokenStorage + + this.i18nInit() + this.title = this.i18n('pluginNameGooglePhotos') + + const client = new RequestClient(uppy, { + pluginId: this.id, + provider: 'url', + companionUrl: this.opts.companionUrl, + companionHeaders: this.opts.companionHeaders, + companionCookiesRule: this.opts.companionCookiesRule, + }) + + this.uppy.registerRequestClient(GooglePhotosPicker.requestClientId, client) + } + + install(): void { + const { target } = this.opts + if (target) { + this.mount(target, this) + } + } + + uninstall(): void { + this.unmount() + } + + private handleFilesPicked = async ( + files: PickedItem[], + accessToken: string, + ) => { + this.uppy.addFiles( + files.map(({ id, mimeType, name, ...rest }) => { + return { + source: this.id, + name, + type: mimeType, + data: { + size: null, // defer to companion to determine size + }, + isRemote: true, + remote: { + companionUrl: this.opts.companionUrl, + url: `${this.opts.companionUrl}/google-picker/get`, + body: { + fileId: id, + accessToken, + ...rest, + }, + requestClientId: GooglePhotosPicker.requestClientId, + }, + } + }), + ) + } + + render = () => ( + + ) +} diff --git a/packages/@uppy/google-photos-picker/src/index.ts b/packages/@uppy/google-photos-picker/src/index.ts new file mode 100644 index 0000000000..7d64a7cb5e --- /dev/null +++ b/packages/@uppy/google-photos-picker/src/index.ts @@ -0,0 +1 @@ +export { default } from './GooglePhotosPicker.tsx' diff --git a/packages/@uppy/google-photos-picker/src/locale.ts b/packages/@uppy/google-photos-picker/src/locale.ts new file mode 100644 index 0000000000..6dcfedeef9 --- /dev/null +++ b/packages/@uppy/google-photos-picker/src/locale.ts @@ -0,0 +1,3 @@ +export default { + strings: {}, +} diff --git a/packages/@uppy/google-photos-picker/tsconfig.build.json b/packages/@uppy/google-photos-picker/tsconfig.build.json new file mode 100644 index 0000000000..99aaf378de --- /dev/null +++ b/packages/@uppy/google-photos-picker/tsconfig.build.json @@ -0,0 +1,35 @@ +{ + "extends": "../../../tsconfig.shared", + "compilerOptions": { + "noImplicitAny": false, + "outDir": "./lib", + "paths": { + "@uppy/companion-client": ["../companion-client/src/index.js"], + "@uppy/companion-client/lib/*": ["../companion-client/src/*"], + "@uppy/provider-views": ["../provider-views/src/index.js"], + "@uppy/provider-views/lib/*": ["../provider-views/src/*"], + "@uppy/utils/lib/*": ["../utils/src/*"], + "@uppy/core": ["../core/src/index.js"], + "@uppy/core/lib/*": ["../core/src/*"] + }, + "resolveJsonModule": false, + "rootDir": "./src", + "skipLibCheck": true + }, + "include": ["./src/**/*.*"], + "exclude": ["./src/**/*.test.ts"], + "references": [ + { + "path": "../companion-client/tsconfig.build.json" + }, + { + "path": "../provider-views/tsconfig.build.json" + }, + { + "path": "../utils/tsconfig.build.json" + }, + { + "path": "../core/tsconfig.build.json" + } + ] +} diff --git a/packages/@uppy/google-photos-picker/tsconfig.json b/packages/@uppy/google-photos-picker/tsconfig.json new file mode 100644 index 0000000000..e5220fb5ab --- /dev/null +++ b/packages/@uppy/google-photos-picker/tsconfig.json @@ -0,0 +1,31 @@ +{ + "extends": "../../../tsconfig.shared", + "compilerOptions": { + "emitDeclarationOnly": false, + "noEmit": true, + "paths": { + "@uppy/companion-client": ["../companion-client/src/index.js"], + "@uppy/companion-client/lib/*": ["../companion-client/src/*"], + "@uppy/provider-views": ["../provider-views/src/index.js"], + "@uppy/provider-views/lib/*": ["../provider-views/src/*"], + "@uppy/utils/lib/*": ["../utils/src/*"], + "@uppy/core": ["../core/src/index.js"], + "@uppy/core/lib/*": ["../core/src/*"], + }, + }, + "include": ["./package.json", "./src/**/*.*"], + "references": [ + { + "path": "../companion-client/tsconfig.build.json", + }, + { + "path": "../provider-views/tsconfig.build.json", + }, + { + "path": "../utils/tsconfig.build.json", + }, + { + "path": "../core/tsconfig.build.json", + }, + ], +} diff --git a/packages/@uppy/google-picker/CHANGELOG.md b/packages/@uppy/google-picker/CHANGELOG.md deleted file mode 100644 index b96e5b9273..0000000000 --- a/packages/@uppy/google-picker/CHANGELOG.md +++ /dev/null @@ -1 +0,0 @@ -# @uppy/google-picker diff --git a/packages/@uppy/google-picker/README.md b/packages/@uppy/google-picker/README.md deleted file mode 100644 index 20c4ced0b9..0000000000 --- a/packages/@uppy/google-picker/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# @uppy/google-picker - -Uppy logo: a smiling puppy above a pink upwards arrow - -[![npm version](https://img.shields.io/npm/v/@uppy/google-photos.svg?style=flat-square)](https://www.npmjs.com/package/@uppy/google-photos) -![CI status for Uppy tests](https://github.com/transloadit/uppy/workflows/Tests/badge.svg) -![CI status for Companion tests](https://github.com/transloadit/uppy/workflows/Companion/badge.svg) -![CI status for browser tests](https://github.com/transloadit/uppy/workflows/End-to-end%20tests/badge.svg) - -The Google Photos plugin for Uppy lets users import photos from their Google -Photos account. - -A Companion instance is required for the GooglePhotos plugin to work. Companion -handles authentication with Google, downloads photos from Google Photos and -uploads them to the destination. This saves the user bandwidth, especially -helpful if they are on a mobile connection. - -Uppy is being developed by the folks at [Transloadit](https://transloadit.com), -a versatile file encoding service. - -## Example - -```js -import Uppy from '@uppy/core' -import GooglePhotos from '@uppy/google-photos' - -const uppy = new Uppy() -uppy.use(GooglePhotos, { - // Options -}) -``` - -## Installation - -```bash -$ npm install @uppy/google-photos -``` - -Alternatively, you can also use this plugin in a pre-built bundle from -Transloadit’s CDN: Edgly. In that case `Uppy` will attach itself to the global -`window.Uppy` object. See the -[main Uppy documentation](https://uppy.io/docs/#Installation) for instructions. - -## Documentation - -Documentation for this plugin can be found on the -[Uppy website](https://uppy.io/docs/google-photos). - -## License - -The [MIT License](./LICENSE). diff --git a/packages/@uppy/google-picker/src/GooglePicker.tsx b/packages/@uppy/google-picker/src/GooglePicker.tsx deleted file mode 100644 index 9bc13e54e0..0000000000 --- a/packages/@uppy/google-picker/src/GooglePicker.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { UIPlugin, Uppy } from '@uppy/core' -import { GooglePickerView } from '@uppy/provider-views' -import { - Provider, - getAllowedHosts, - type CompanionPluginOptions, -} from '@uppy/companion-client' -import { h, type ComponentChild } from 'preact' - -import type { UppyFile, Body, Meta } from '@uppy/utils/lib/UppyFile' -import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy.js' - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore We don't want TS to generate types for the package.json -import packageJson from '../package.json' -import locale from './locale.ts' - -export type GooglePickerOptions = CompanionPluginOptions & { - clientId: string, - apiKey: string, - appId: string, -} - -export default class GooglePicker< - M extends Meta, - B extends Body, -> extends UIPlugin { - static VERSION = packageJson.version - - icon: () => h.JSX.Element - - provider: Provider - - files: UppyFile[] - - constructor(uppy: Uppy, opts: GooglePickerOptions) { - super(uppy, opts) - this.type = 'acquirer' - this.files = [] - this.id = this.opts.id || 'GooglePicker' - this.icon = () => ( - - ) - - this.opts.companionAllowedHosts = getAllowedHosts( - this.opts.companionAllowedHosts, - this.opts.companionUrl, - ) - this.provider = new Provider(uppy, { - companionUrl: this.opts.companionUrl, - companionHeaders: this.opts.companionHeaders, - companionKeysParams: this.opts.companionKeysParams, - companionCookiesRule: this.opts.companionCookiesRule, - - provider: 'googlepicker', - pluginId: this.id, - }) - - this.defaultLocale = locale - - this.i18nInit() - this.title = this.i18n('pluginNameGooglePicker') - - this.render = this.render.bind(this) - } - - install(): void { - const { target } = this.opts - if (target) { - this.mount(target, this) - } - } - - uninstall(): void { - this.unmount() - } - - render(): ComponentChild { - return GooglePickerView({ - provider: this.provider, uppy: this.uppy, - clientId: this.opts.clientId, - apiKey: this.opts.apiKey, - appId: this.opts.appId, - }) - } -} diff --git a/packages/@uppy/google-picker/src/index.ts b/packages/@uppy/google-picker/src/index.ts deleted file mode 100644 index 7c93edd7f2..0000000000 --- a/packages/@uppy/google-picker/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './GooglePicker.tsx' diff --git a/packages/@uppy/google-picker/src/locale.ts b/packages/@uppy/google-picker/src/locale.ts deleted file mode 100644 index 525b41188b..0000000000 --- a/packages/@uppy/google-picker/src/locale.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default { - strings: { - pluginNameGooglePicker: 'Google Picker', - }, -} diff --git a/packages/@uppy/provider-views/src/GooglePicker/GooglePickerView.tsx b/packages/@uppy/provider-views/src/GooglePicker/GooglePickerView.tsx new file mode 100644 index 0000000000..74b25c1e07 --- /dev/null +++ b/packages/@uppy/provider-views/src/GooglePicker/GooglePickerView.tsx @@ -0,0 +1,482 @@ +import { h } from 'preact' +import { Fragment } from 'preact/compat' +import type { UIPlugin, Uppy } from '@uppy/core' +import useStore from '@uppy/core/lib/useStore.js' +import { useCallback, useEffect, useRef, useState } from 'preact/hooks' +import { useUppyPluginState } from '@uppy/core/lib/useUppyState' +import type { AsyncStore } from '@uppy/core/lib/Uppy.js' + +export type PluginState = { + scriptsLoaded: boolean +} + +// https://developers.google.com/photos/picker/reference/rest/v1/mediaItems +export interface MediaItemBase { + id: string + createTime: string +} + +interface MediaFileMetadataBase { + width: number + height: number + cameraMake: string + cameraModel: string +} + +interface MediaFileBase { + baseUrl: string + mimeType: string + filename: string +} + +export interface VideoMediaItem extends MediaItemBase { + type: 'VIDEO' + mediaFile: MediaFileBase & { + mediaFileMetadata: MediaFileMetadataBase & { + videoMetadata: { + fps: number + processingStatus: 'UNSPECIFIED' | 'PROCESSING' | 'READY' | 'FAILED' + } + } + } +} + +export interface PhotoMediaItem extends MediaItemBase { + type: 'PHOTO' + mediaFile: MediaFileBase & { + mediaFileMetadata: MediaFileMetadataBase & { + photoMetadata: { + focalLength: number + apertureFNumber: number + isoEquivalent: number + exposureTime: string + } + } + } +} + +export interface UnspecifiedMediaItem extends MediaItemBase { + type: 'TYPE_UNSPECIFIED' + mediaFile: MediaFileBase +} + +export type MediaItem = VideoMediaItem | PhotoMediaItem | UnspecifiedMediaItem + +// https://developers.google.com/photos/picker/reference/rest/v1/sessions +interface PickingSession { + id: string + pickerUri: string + pollingConfig: { + pollInterval: string + timeoutIn: string + } + expireTime: string + mediaItemsSet: boolean +} + +export interface PickedItemBase { + id: string + mimeType: string + name: string +} + +export interface PickedDriveItem extends PickedItemBase { + platform: 'drive' +} + +export interface PickedPhotosItem extends PickedItemBase { + platform: 'photos' + url: string +} + +export type PickedItem = PickedPhotosItem | PickedDriveItem + +export const getAuthHeader = (token: string) => ({ + authorization: `Bearer ${token}`, +}) + +const injectedScripts = new Set() + +// https://stackoverflow.com/a/39008859/6519037 +async function injectScript(src: string) { + if (injectedScripts.has(src)) return + + await new Promise((resolve, reject) => { + const script = document.createElement('script') + script.src = src + script.addEventListener('load', () => resolve()) + script.addEventListener('error', (e) => reject(e.error)) + document.head.appendChild(script) + }) + injectedScripts.add(src) +} + +export type GooglePickerViewProps = { + plugin: UIPlugin + uppy: Uppy + clientId: string + onFilesPicked: (files: PickedItem[], accessToken: string) => void + storage: AsyncStore +} & ( + | { + pickerType: 'drive' + apiKey: string + appId: string + } + | { + pickerType: 'photos' + apiKey?: undefined + appId?: undefined + } +) + +export default function GooglePickerView({ + uppy, + clientId, + onFilesPicked, + plugin, + pickerType, + apiKey, + appId, + storage, +}: GooglePickerViewProps) { + const [{ scriptsLoaded }, setPluginState] = useUppyPluginState(plugin) + const [loading, setLoading] = useState(false) + const [signedOut, setSignedOut] = useState(false) + const [accessToken, setAccessToken] = useStore( + storage, + `uppy:google-${pickerType}-picker:accessToken`, + ) + + const onPicked = useCallback( + async (picked: google.picker.ResponseObject) => { + if (picked.action === google.picker.Action.PICKED) { + // console.log('Picker response', JSON.stringify(picked, null, 2)); + if (accessToken == null) throw new Error() + onFilesPicked( + picked['docs'].map((doc) => ({ + platform: 'drive', + id: doc['id'], + name: doc['name'], + mimeType: doc['mimeType'], + })), + accessToken, + ) + } + }, + [accessToken, onFilesPicked], + ) + + const showDrivePicker = useCallback( + (token: string) => { + if (pickerType !== 'drive') throw new Error() + const picker = new google.picker.PickerBuilder() + .enableFeature(google.picker.Feature.NAV_HIDDEN) + .enableFeature(google.picker.Feature.MULTISELECT_ENABLED) + .setDeveloperKey(apiKey) + .setAppId(appId) + .setOAuthToken(token) + .addView( + new google.picker.DocsView(google.picker.ViewId.DOCS) + .setIncludeFolders(true) + // Note: setEnableDrives doesn't seem to work + // .setEnableDrives(true) + .setSelectFolderEnabled(false), + ) + // NOTE: photos is broken and results in an error being returned from Google + // .addView(google.picker.ViewId.PHOTOS) + .setCallback(onPicked) + .build() + + picker.setVisible(true) + }, + [apiKey, appId, onPicked, pickerType], + ) + + const pollStartTimeRef = useRef() + const [pickingSession, setPickingSession] = useState() + + const showPhotosPicker = useCallback( + async (token: string) => { + // https://developers.google.com/photos/picker/guides/get-started-picker + try { + setLoading(true) + + const headers = getAuthHeader(token) + + let newPickingSession = pickingSession + if (newPickingSession == null) { + const createSessionResponse = await fetch( + 'https://photospicker.googleapis.com/v1/sessions', + { method: 'post', headers }, + ) + + if (createSessionResponse.status === 401) { + const resp = await createSessionResponse.json() + if (resp.error?.status === 'UNAUTHENTICATED') { + setAccessToken(null) + setSignedOut(true) + return + } + } + + if (!createSessionResponse.ok) + throw new Error('Failed to create a session') + newPickingSession = + (await createSessionResponse.json()) as PickingSession + pollStartTimeRef.current = Date.now() + setPickingSession(newPickingSession) + } + + window.open(newPickingSession.pickerUri) + } finally { + setLoading(false) + } + }, + [pickingSession, setAccessToken], + ) + + const authorize = useCallback(async () => { + setSignedOut(false) + setLoading(true) + try { + const response = await new Promise( + (resolve, reject) => { + const scopes = + pickerType === 'drive' ? + ['https://www.googleapis.com/auth/drive.readonly'] + : [ + 'https://www.googleapis.com/auth/photospicker.mediaitems.readonly', + ] + + const tokenClient = google.accounts.oauth2.initTokenClient({ + client_id: clientId, + // Authorization scopes required by the API; multiple scopes can be included, separated by spaces. + scope: scopes.join(' '), + callback: resolve, + error_callback: reject, + }) + + if (accessToken === null) { + // Prompt the user to select a Google Account and ask for consent to share their data + // when establishing a new session. + tokenClient.requestAccessToken({ prompt: 'consent' }) + } else { + // Skip display of account chooser and consent dialog for an existing session. + tokenClient.requestAccessToken({ prompt: '' }) + } + }, + ) + if (response.error) { + throw new Error(`OAuth2 error: ${response.error}`) + } + const { access_token: newAccessToken } = response + setAccessToken(newAccessToken) + + // showDrivePicker(newAccessToken); + } catch (err) { + uppy.log(err) + } finally { + setLoading(false) + } + }, [accessToken, clientId, pickerType, setAccessToken, uppy]) + + useEffect(() => { + ;(async () => { + try { + await Promise.all([ + injectScript('https://accounts.google.com/gsi/client'), // Google Identity Services + (async () => { + await injectScript('https://apis.google.com/js/api.js') + + if (pickerType === 'drive') { + await new Promise((resolve) => + gapi.load('client:picker', () => resolve()), + ) + await gapi.client.load( + 'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest', + ) + } + + setPluginState({ scriptsLoaded: true }) + })(), + ]) + } catch (err) { + uppy.log(err) + } + })() + }, [pickerType, setPluginState, uppy]) + + const showPicker = useCallback(() => { + if (accessToken === undefined) return // not yet loaded + if (accessToken === null) { + authorize() + } else if (pickerType === 'drive') { + showDrivePicker(accessToken) + } else { + showPhotosPicker(accessToken) + } + }, [accessToken, authorize, pickerType, showDrivePicker, showPhotosPicker]) + + // eslint-disable-next-line no-shadow + const handlePhotosPicked = useCallback( + async (params: { accessToken: string; pickingSession: PickingSession }) => { + const headers = getAuthHeader(params.accessToken) + + let pageToken: string | undefined + let mediaItems: MediaItem[] = [] + do { + const pageSize = 100 + const response = await fetch( + `https://photospicker.googleapis.com/v1/mediaItems?${new URLSearchParams({ sessionId: params.pickingSession.id, pageSize: String(pageSize) }).toString()}`, + { headers }, + ) + if (!response.ok) throw new Error('Failed to get a media items') + const { + mediaItems: batchMediaItems, + nextPageToken, + }: { mediaItems: MediaItem[]; nextPageToken?: string } = + await response.json() + pageToken = nextPageToken + mediaItems.push(...batchMediaItems) + } while (pageToken) + + // todo show alert instead about invalid picked files? + mediaItems = mediaItems.flatMap((i) => + ( + i.type === 'PHOTO' || + (i.type === 'VIDEO' && + i.mediaFile.mediaFileMetadata.videoMetadata.processingStatus === + 'READY') + ) ? + [i] + : [], + ) + + onFilesPicked( + mediaItems.map( + ({ + id, + // we want the original resolution, so we don't append any parameter to the baseUrl + // https://developers.google.com/photos/library/guides/access-media-items#base-urls + mediaFile: { mimeType, filename, baseUrl }, + }) => ({ + platform: 'photos', + id, + mimeType, + url: baseUrl, + name: filename, + }), + ), + params.accessToken, + ) + }, + [onFilesPicked], + ) + + useEffect(() => { + // if we have a session, poll it until it either times out, or the user selects some photos + // note that the user can also just close the page, but we have no indication of that, + // so we just have to continue polling, in case the user opens the photo selector again + if (pickingSession == null || accessToken == null) return undefined + + const abortController = new AbortController() + + const headers = getAuthHeader(accessToken) + + ;(async () => { + // poll session for user response + for (;;) { + try { + const interval = parseFloat(pickingSession.pollingConfig.pollInterval) + + await Promise.race([ + new Promise((resolve) => setTimeout(resolve, interval * 1000)), + new Promise((_resolve, reject) => { + abortController.signal.onabort = reject + }), + ]) + + abortController.signal.throwIfAborted() + + // https://developers.google.com/photos/picker/reference/rest/v1/sessions + const response = await fetch( + `https://photospicker.googleapis.com/v1/sessions/${encodeURIComponent(pickingSession.id)}`, + { headers }, + ) + if (!response.ok) throw new Error('Failed to get session') + const json: PickingSession = await response.json() + if (json.mediaItemsSet) { + // console.log('User picked!', json) + pollStartTimeRef.current = undefined + setPickingSession(undefined) + handlePhotosPicked({ accessToken, pickingSession }) + return + } + if (pickingSession.pollingConfig.timeoutIn === '0s') { + uppy.log('Picking session timeout') + pollStartTimeRef.current = undefined + setPickingSession(undefined) + return + } + } catch (err) { + if (err instanceof Error && err.name === 'AbortError') { + return + } + uppy.log(err) + } + } + })() + + return () => abortController.abort() + }, [accessToken, handlePhotosPicked, pickingSession, uppy]) + + useEffect(() => { + if (!scriptsLoaded || signedOut) { + return + } + showPicker() + }, [scriptsLoaded, showPicker, signedOut]) + + const handleSignoutClick = useCallback(async () => { + if (accessToken == null) return + if (accessToken) { + await new Promise((resolve) => + google.accounts.oauth2.revoke(accessToken, resolve), + ) + setAccessToken(null) + setPickingSession(undefined) + setSignedOut(true) // if user signs out, don't re-authenticate automatically + } + }, [accessToken, setAccessToken]) + + if (!scriptsLoaded) { + return null + } + + // for photos, we will never go out of the loading/polling state + if (loading) { + return
{uppy.i18n('pleaseWait')}...
+ } + + if (accessToken == null) { + return ( + + ) + } + + return ( + <> + + + + ) +} diff --git a/packages/@uppy/provider-views/src/GooglePicker/index.css b/packages/@uppy/provider-views/src/GooglePicker/index.css deleted file mode 100644 index 126a640918..0000000000 --- a/packages/@uppy/provider-views/src/GooglePicker/index.css +++ /dev/null @@ -1,7 +0,0 @@ -/* https://stackoverflow.com/a/33082658/6519037 */ -.picker-dialog-bg { - z-index: 20000 !important; -} -.picker-dialog { - z-index: 20001 !important; -} diff --git a/packages/@uppy/provider-views/src/GooglePicker/index.tsx b/packages/@uppy/provider-views/src/GooglePicker/index.tsx deleted file mode 100644 index c8840fd1be..0000000000 --- a/packages/@uppy/provider-views/src/GooglePicker/index.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { h } from 'preact' -import { Fragment } from 'preact/compat' -import type { Uppy } from '@uppy/core'; -import { useCallback, useEffect, useState } from 'preact/hooks'; - -import './index.css'; - - -const injectedScripts = new Set(); - -// https://stackoverflow.com/a/39008859/6519037 -async function injectScript(src: string) { - if (injectedScripts.has(src)) return; - - await new Promise((resolve, reject) => { - const script = document.createElement('script'); - script.src = src; - script.addEventListener('load', () => resolve()); - script.addEventListener('error', e => reject(e.error)); - document.head.appendChild(script); - }); - injectedScripts.add(src); -} - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export default function GooglePickerView({ provider, uppy, clientId, apiKey, appId }: { - provider: unknown, // todo - uppy: Uppy, - clientId: string, - apiKey: string, - appId: string, -}) { - // Authorization scopes required by the API; multiple scopes can be included, separated by spaces. - const scopes = 'https://www.googleapis.com/auth/drive.file'; - // 'https://www.googleapis.com/auth/photoslibrary.readonly' would be for google photos, - // but it doesn't seem to work (see comment below) - - const [accessToken, setAccessToken] = useState(); - const [loading, setLoading] = useState(false); // todo - - useEffect(() => { - (async () => { - try { - await Promise.all([ - injectScript('https://accounts.google.com/gsi/client'), // Google Identity Services - (async () => { - await injectScript('https://apis.google.com/js/api.js'); - await new Promise((resolve) => gapi.load('client:picker', () => resolve())); - await gapi.client.load('https://www.googleapis.com/discovery/v1/apis/drive/v3/rest'); - })(), - ]); - - - // setTokenClient(newTokenClient); - - } catch (err) { - uppy.log(err) - } - })() - }, [uppy]); - - const onPicked = useCallback(async (picked: google.picker.ResponseObject) => { - if (picked.action === google.picker.Action.PICKED) { - // eslint-disable-next-line no-console - console.log('Picker response', JSON.stringify(picked, null, 2)); - // todo add these files along with any metadata needed by companion to download the files, - // like accessToken, clientId etc. - // companion needs to present an endpoint that will call gapi.client.drive.files.get, - // and stream (download/upload) the file to the destination (e.g. tus/s3 etc) - // something like this: - /* const document = picked[google.picker.Response.DOCUMENTS][0]; - const fileId = document[google.picker.Document.ID]; - console.log(fileId); - const res = await gapi.client.drive.files.get({ - fileId, - 'fields': '*', - }); - console.log('Drive API response for first document', JSON.stringify(res.result, null, 2)); - */ - } - }, []); - - const showPicker = useCallback((token: string) => { - const picker = new google.picker.PickerBuilder() - .enableFeature(google.picker.Feature.NAV_HIDDEN) - .enableFeature(google.picker.Feature.MULTISELECT_ENABLED) - .setDeveloperKey(apiKey) - .setAppId(appId) - .setOAuthToken(token) - .addView( - new google.picker.DocsView(google.picker.ViewId.DOCS) - .setIncludeFolders(true) - // Note: setEnableDrives doesn't seem to work - // .setEnableDrives(true) - .setSelectFolderEnabled(true) - ) - // NOTE: photos is broken and results in an error being returned from Google - // .addView(google.picker.ViewId.PHOTOS) - .setCallback(onPicked) - .build(); - - picker.setVisible(true); - }, [apiKey, appId, onPicked]); - - const handleAuthOrRefreshClick = useCallback(async () => { - setLoading(true); - try { - const response = await new Promise((resolve) => { - const tokenClient = google.accounts.oauth2.initTokenClient({ - client_id: clientId, - scope: scopes, - callback: resolve, - }); - - if (accessToken === null) { - // Prompt the user to select a Google Account and ask for consent to share their data - // when establishing a new session. - tokenClient.requestAccessToken({ prompt: 'consent' }); - } else { - // Skip display of account chooser and consent dialog for an existing session. - tokenClient.requestAccessToken({ prompt: '' }); - } - }) - if (response.error !== undefined) { - throw (response); - } - const { access_token: newAccessToken } = response; - setAccessToken(newAccessToken); - - showPicker(newAccessToken); - } finally { - setLoading(false); - } - }, [accessToken, clientId, showPicker]); - - const handleSignoutClick = useCallback(async () => { - if (accessToken == null) return; - if (accessToken) { - await new Promise((resolve) => google.accounts.oauth2.revoke(accessToken, resolve)); - setAccessToken(undefined); - } - }, [accessToken]); - - return ( - <> - {accessToken != null && ( - <> - - - - )} - - - ) -} diff --git a/packages/@uppy/provider-views/src/index.ts b/packages/@uppy/provider-views/src/index.ts index f3af1e2a5e..bdf3178237 100644 --- a/packages/@uppy/provider-views/src/index.ts +++ b/packages/@uppy/provider-views/src/index.ts @@ -5,4 +5,4 @@ export { export { default as SearchProviderViews } from './SearchProviderView/index.ts' -export { default as GooglePickerView } from './GooglePicker/index.tsx' +export { default as GooglePickerView } from './GooglePicker/GooglePickerView.tsx' diff --git a/packages/@uppy/provider-views/src/style.scss b/packages/@uppy/provider-views/src/style.scss index d29a8d30dd..0869ab76df 100644 --- a/packages/@uppy/provider-views/src/style.scss +++ b/packages/@uppy/provider-views/src/style.scss @@ -379,3 +379,11 @@ padding-bottom: 10px; } } + +/* https://stackoverflow.com/a/33082658/6519037 */ +.picker-dialog-bg { + z-index: 20000 !important; +} +.picker-dialog { + z-index: 20001 !important; +} diff --git a/packages/@uppy/remote-sources/tsconfig.build.json b/packages/@uppy/remote-sources/tsconfig.build.json index 9e32144ab0..5399803aac 100644 --- a/packages/@uppy/remote-sources/tsconfig.build.json +++ b/packages/@uppy/remote-sources/tsconfig.build.json @@ -16,8 +16,6 @@ "@uppy/google-drive/lib/*": ["../google-drive/src/*"], "@uppy/google-photos": ["../google-photos/src/index.js"], "@uppy/google-photos/lib/*": ["../google-photos/src/*"], - "@uppy/google-picker": ["../google-picker/src/index.js"], - "@uppy/google-picker/lib/*": ["../google-picker/src/*"], "@uppy/instagram": ["../instagram/src/index.js"], "@uppy/instagram/lib/*": ["../instagram/src/*"], "@uppy/onedrive": ["../onedrive/src/index.js"], @@ -56,9 +54,6 @@ { "path": "../google-photos/tsconfig.build.json" }, - { - "path": "../google-picker/tsconfig.build.json" - }, { "path": "../instagram/tsconfig.build.json" }, diff --git a/packages/@uppy/transloadit/src/index.ts b/packages/@uppy/transloadit/src/index.ts index 574e969273..6ec52bbc2d 100644 --- a/packages/@uppy/transloadit/src/index.ts +++ b/packages/@uppy/transloadit/src/index.ts @@ -334,7 +334,8 @@ export default class Transloadit< addPluginVersion('Facebook', 'uppy-facebook') addPluginVersion('GoogleDrive', 'uppy-google-drive') addPluginVersion('GooglePhotos', 'uppy-google-photos') - addPluginVersion('GooglePicker', 'uppy-google-picker') + addPluginVersion('GoogleDrivePicker', 'uppy-google-drive-picker') + addPluginVersion('GooglePhotosPicker', 'uppy-google-photos-picker') addPluginVersion('Instagram', 'uppy-instagram') addPluginVersion('OneDrive', 'uppy-onedrive') addPluginVersion('Zoom', 'uppy-zoom') diff --git a/packages/uppy/package.json b/packages/uppy/package.json index 6767152fa5..24d9fe4a20 100644 --- a/packages/uppy/package.json +++ b/packages/uppy/package.json @@ -46,8 +46,9 @@ "@uppy/form": "workspace:^", "@uppy/golden-retriever": "workspace:^", "@uppy/google-drive": "workspace:^", + "@uppy/google-drive-picker": "workspace:^", "@uppy/google-photos": "workspace:^", - "@uppy/google-picker": "workspace:^", + "@uppy/google-photos-picker": "workspace:^", "@uppy/image-editor": "workspace:^", "@uppy/informer": "workspace:^", "@uppy/instagram": "workspace:^", diff --git a/packages/uppy/src/bundle.ts b/packages/uppy/src/bundle.ts index c995fc24c1..a74f4bc98a 100644 --- a/packages/uppy/src/bundle.ts +++ b/packages/uppy/src/bundle.ts @@ -42,7 +42,8 @@ export { default as Dropbox } from '@uppy/dropbox' export { default as Facebook } from '@uppy/facebook' export { default as GoogleDrive } from '@uppy/google-drive' export { default as GooglePhotos } from '@uppy/google-photos' -export { default as GooglePicker } from '@uppy/google-picker' +export { default as GoogleDrivePicker } from '@uppy/google-drive-picker' +export { default as GooglePhotosPicker } from '@uppy/google-photos-picker' export { default as Instagram } from '@uppy/instagram' export { default as OneDrive } from '@uppy/onedrive' export { default as RemoteSources } from '@uppy/remote-sources' diff --git a/private/dev/Dashboard.js b/private/dev/Dashboard.js index 9157809062..9c3d2d8a1c 100644 --- a/private/dev/Dashboard.js +++ b/private/dev/Dashboard.js @@ -16,7 +16,8 @@ import Audio from '@uppy/audio' import Compressor from '@uppy/compressor' import GoogleDrive from '@uppy/google-drive' import english from '@uppy/locales/lib/en_US.js' -import GooglePicker from '@uppy/google-picker' +import GoogleDrivePicker from '@uppy/google-drive-picker' +import GooglePhotosPicker from '@uppy/google-photos-picker' /* eslint-enable import/no-extraneous-dependencies */ import generateSignatureIfSecret from './generateSignatureIfSecret.js' @@ -129,7 +130,7 @@ export default () => { // .use(Zoom, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) // .use(Url, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) // .use(Unsplash, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) - .use(GooglePicker, { + .use(GoogleDrivePicker, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts, @@ -137,6 +138,12 @@ export default () => { apiKey: GOOGLE_PICKER_API_KEY, appId: GOOGLE_PICKER_APP_ID, }) + .use(GooglePhotosPicker, { + target: Dashboard, + companionUrl: COMPANION_URL, + companionAllowedHosts, + clientId: GOOGLE_PICKER_CLIENT_ID, + }) .use(RemoteSources, { companionUrl: COMPANION_URL, sources: [ diff --git a/tsconfig.json b/tsconfig.json index ab23ff9580..c64bff1806 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -50,7 +50,10 @@ "path": "./packages/@uppy/google-photos/tsconfig.build.json", }, { - "path": "./packages/@uppy/google-picker/tsconfig.build.json", + "path": "./packages/@uppy/google-drive-picker/tsconfig.build.json", + }, + { + "path": "./packages/@uppy/google-photos-picker/tsconfig.build.json", }, { "path": "./packages/@uppy/image-editor/tsconfig.build.json", diff --git a/yarn.lock b/yarn.lock index ed452ddd92..0be0798828 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8743,7 +8743,6 @@ __metadata: nock: "npm:^13.1.3" node-schedule: "npm:2.1.1" prom-client: "npm:15.1.2" - safe-stringify: "npm:^1.1.1" serialize-error: "npm:^11.0.0" serialize-javascript: "npm:^6.0.0" supertest: "npm:6.2.4" @@ -8894,6 +8893,19 @@ __metadata: languageName: unknown linkType: soft +"@uppy/google-drive-picker@workspace:^, @uppy/google-drive-picker@workspace:packages/@uppy/google-drive-picker": + version: 0.0.0-use.local + resolution: "@uppy/google-drive-picker@workspace:packages/@uppy/google-drive-picker" + dependencies: + "@uppy/companion-client": "workspace:^" + "@uppy/provider-views": "workspace:^" + "@uppy/utils": "workspace:^" + preact: "npm:^10.5.13" + peerDependencies: + "@uppy/core": "workspace:^" + languageName: unknown + linkType: soft + "@uppy/google-drive@workspace:*, @uppy/google-drive@workspace:^, @uppy/google-drive@workspace:packages/@uppy/google-drive": version: 0.0.0-use.local resolution: "@uppy/google-drive@workspace:packages/@uppy/google-drive" @@ -8907,9 +8919,9 @@ __metadata: languageName: unknown linkType: soft -"@uppy/google-photos@workspace:*, @uppy/google-photos@workspace:^, @uppy/google-photos@workspace:packages/@uppy/google-photos": +"@uppy/google-photos-picker@workspace:^, @uppy/google-photos-picker@workspace:packages/@uppy/google-photos-picker": version: 0.0.0-use.local - resolution: "@uppy/google-photos@workspace:packages/@uppy/google-photos" + resolution: "@uppy/google-photos-picker@workspace:packages/@uppy/google-photos-picker" dependencies: "@uppy/companion-client": "workspace:^" "@uppy/provider-views": "workspace:^" @@ -8920,9 +8932,9 @@ __metadata: languageName: unknown linkType: soft -"@uppy/google-picker@workspace:^, @uppy/google-picker@workspace:packages/@uppy/google-picker": +"@uppy/google-photos@workspace:*, @uppy/google-photos@workspace:^, @uppy/google-photos@workspace:packages/@uppy/google-photos": version: 0.0.0-use.local - resolution: "@uppy/google-picker@workspace:packages/@uppy/google-picker" + resolution: "@uppy/google-photos@workspace:packages/@uppy/google-photos" dependencies: "@uppy/companion-client": "workspace:^" "@uppy/provider-views": "workspace:^" @@ -9088,7 +9100,6 @@ __metadata: "@uppy/facebook": "workspace:^" "@uppy/google-drive": "workspace:^" "@uppy/google-photos": "workspace:^" - "@uppy/google-picker": "workspace:^" "@uppy/instagram": "workspace:^" "@uppy/onedrive": "workspace:^" "@uppy/unsplash": "workspace:^" @@ -13549,8 +13560,9 @@ __metadata: "@uppy/form": "workspace:^" "@uppy/golden-retriever": "workspace:^" "@uppy/google-drive": "workspace:^" + "@uppy/google-drive-picker": "workspace:^" "@uppy/google-photos": "workspace:^" - "@uppy/google-picker": "workspace:^" + "@uppy/google-photos-picker": "workspace:^" "@uppy/image-editor": "workspace:^" "@uppy/informer": "workspace:^" "@uppy/instagram": "workspace:^" @@ -26516,13 +26528,6 @@ __metadata: languageName: node linkType: hard -"safe-stringify@npm:^1.1.1": - version: 1.1.1 - resolution: "safe-stringify@npm:1.1.1" - checksum: 10/5f06510a8adfa9fbc3045021b31e9eb8b5e68f5525d0ea75cb64ceaf7cf7c449abd562f2f5dbd1e5318b046159eaece2d834f4047ccedb941574a9186c0eeb70 - languageName: node - linkType: hard - "safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.0.2, safer-buffer@npm:^2.1.0, safer-buffer@npm:~2.1.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" @@ -29690,8 +29695,9 @@ __metadata: "@uppy/form": "workspace:^" "@uppy/golden-retriever": "workspace:^" "@uppy/google-drive": "workspace:^" + "@uppy/google-drive-picker": "workspace:^" "@uppy/google-photos": "workspace:^" - "@uppy/google-picker": "workspace:^" + "@uppy/google-photos-picker": "workspace:^" "@uppy/image-editor": "workspace:^" "@uppy/informer": "workspace:^" "@uppy/instagram": "workspace:^" From ef9ba1544eacf6c40e4b6dca2b6c076d831bdaea Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Fri, 15 Nov 2024 19:12:15 +0800 Subject: [PATCH 03/19] implement picker in companion --- .env.example | 1 + packages/@uppy/companion/src/companion.js | 2 + .../@uppy/companion/src/config/companion.js | 1 + .../src/server/controllers/googlePicker.js | 57 ++++++++++ .../companion/src/server/controllers/url.js | 25 +---- .../@uppy/companion/src/server/download.js | 28 +++++ .../companion/src/server/helpers/request.js | 4 +- .../src/server/provider/google/drive/index.js | 101 ++++++++++-------- .../companion/src/server/provider/index.js | 4 +- .../@uppy/companion/src/standalone/helper.js | 1 + 10 files changed, 153 insertions(+), 71 deletions(-) create mode 100644 packages/@uppy/companion/src/server/controllers/googlePicker.js create mode 100644 packages/@uppy/companion/src/server/download.js diff --git a/.env.example b/.env.example index b3a63baee1..a3bbe74b7f 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,7 @@ COMPANION_PREAUTH_SECRET=development2 # NOTE: Only enable this in development. Enabling it in production is a security risk COMPANION_ALLOW_LOCAL_URLS=true +COMPANION_ENABLE_URL_ENDPOINT=true COMPANION_ENABLE_GOOGLE_PICKER_ENDPOINT=true # to enable S3 diff --git a/packages/@uppy/companion/src/companion.js b/packages/@uppy/companion/src/companion.js index 01f55c2fa5..fd53ebaaf2 100644 --- a/packages/@uppy/companion/src/companion.js +++ b/packages/@uppy/companion/src/companion.js @@ -10,6 +10,7 @@ const providerManager = require('./server/provider') const controllers = require('./server/controllers') const s3 = require('./server/controllers/s3') const url = require('./server/controllers/url') +const googlePicker = require('./server/controllers/googlePicker') const createEmitter = require('./server/emitter') const redis = require('./server/redis') const jobs = require('./server/jobs') @@ -120,6 +121,7 @@ module.exports.app = (optionsArg = {}) => { app.use('*', middlewares.getCompanionMiddleware(options)) app.use('/s3', s3(options.s3)) if (options.enableUrlEndpoint) app.use('/url', url()) + if (options.enableGooglePickerEndpoint) app.use('/google-picker', googlePicker()) app.post('/:providerName/preauth', express.json(), express.urlencoded({ extended: false }), middlewares.hasSessionAndProvider, middlewares.hasBody, middlewares.hasOAuthProvider, controllers.preauth) app.get('/:providerName/connect', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, controllers.connect) diff --git a/packages/@uppy/companion/src/config/companion.js b/packages/@uppy/companion/src/config/companion.js index b847f3df7a..78858d3ec9 100644 --- a/packages/@uppy/companion/src/config/companion.js +++ b/packages/@uppy/companion/src/config/companion.js @@ -17,6 +17,7 @@ const defaultOptions = { expires: 800, // seconds }, enableUrlEndpoint: false, + enableGooglePickerEndpoint: false, allowLocalUrls: false, periodicPingUrls: [], streamingUpload: true, diff --git a/packages/@uppy/companion/src/server/controllers/googlePicker.js b/packages/@uppy/companion/src/server/controllers/googlePicker.js new file mode 100644 index 0000000000..4b3dd051ee --- /dev/null +++ b/packages/@uppy/companion/src/server/controllers/googlePicker.js @@ -0,0 +1,57 @@ +const express = require('express') +const assert = require('node:assert') + +const { startDownUpload } = require('../helpers/upload') +const { validateURL } = require('../helpers/request') +const { getURLMeta } = require('../helpers/request') +const logger = require('../logger') +const { downloadURL } = require('../download') +const { getGoogleFileSize, streamGoogleFile } = require('../provider/google/drive'); + + +const getAuthHeader = (token) => ({ authorization: `Bearer ${token}` }); + +/** + * + * @param {object} req expressJS request object + * @param {object} res expressJS response object + */ +const get = async (req, res) => { + try { + logger.debug('Google Picker file import handler running', null, req.id) + + const allowLocalUrls = false + + const { accessToken, platform, fileId } = req.body + + assert(platform === 'drive' || platform === 'photos'); + + const getSize = async () => { + if (platform === 'drive') { + return getGoogleFileSize({ id: fileId, token: accessToken }) + } + const { size } = await getURLMeta(req.body.url, allowLocalUrls, { headers: getAuthHeader(accessToken) }) + return size + } + + if (platform === 'photos' && !validateURL(req.body.url, allowLocalUrls)) { + res.status(400).json({ error: 'Invalid URL' }) + return + } + + const download = () => { + if (platform === 'drive') { + return streamGoogleFile({ token: accessToken, id: fileId }) + } + return downloadURL(req.body.url, allowLocalUrls, req.id, { headers: getAuthHeader(accessToken) }) + } + + await startDownUpload({ req, res, getSize, download }) + } catch (err) { + logger.error(err, 'controller.googlePicker.error', req.id) + res.status(err.status || 500).json({ message: 'failed to fetch Google Picker URL' }) + } +} + +module.exports = () => express.Router() + .post('/get', express.json(), get) diff --git a/packages/@uppy/companion/src/server/controllers/url.js b/packages/@uppy/companion/src/server/controllers/url.js index 79cd04ed58..838d659ee7 100644 --- a/packages/@uppy/companion/src/server/controllers/url.js +++ b/packages/@uppy/companion/src/server/controllers/url.js @@ -1,9 +1,9 @@ const express = require('express') const { startDownUpload } = require('../helpers/upload') -const { prepareStream } = require('../helpers/utils') +const { downloadURL } = require('../download') const { validateURL } = require('../helpers/request') -const { getURLMeta, getProtectedGot } = require('../helpers/request') +const { getURLMeta } = require('../helpers/request') const logger = require('../logger') /** @@ -12,27 +12,6 @@ const logger = require('../logger') * @param {string | Buffer | Buffer[]} chunk */ -/** - * Downloads the content in the specified url, and passes the data - * to the callback chunk by chunk. - * - * @param {string} url - * @param {boolean} allowLocalIPs - * @param {string} traceId - * @returns {Promise} - */ -const downloadURL = async (url, allowLocalIPs, traceId) => { - try { - const protectedGot = await getProtectedGot({ allowLocalIPs }) - const stream = protectedGot.stream.get(url, { responseType: 'json' }) - const { size } = await prepareStream(stream) - return { stream, size } - } catch (err) { - logger.error(err, 'controller.url.download.error', traceId) - throw err - } -} - /** * Fetches the size and content type of a URL * diff --git a/packages/@uppy/companion/src/server/download.js b/packages/@uppy/companion/src/server/download.js new file mode 100644 index 0000000000..e8a685577d --- /dev/null +++ b/packages/@uppy/companion/src/server/download.js @@ -0,0 +1,28 @@ +const logger = require('./logger') +const { getProtectedGot } = require('./helpers/request') +const { prepareStream } = require('./helpers/utils') + +/** + * Downloads the content in the specified url, and passes the data + * to the callback chunk by chunk. + * + * @param {string} url + * @param {boolean} allowLocalIPs + * @param {string} traceId + * @returns {Promise} + */ +const downloadURL = async (url, allowLocalIPs, traceId, options) => { + try { + const protectedGot = await getProtectedGot({ allowLocalIPs }) + const stream = protectedGot.stream.get(url, { responseType: 'json', ...options }) + const { size } = await prepareStream(stream) + return { stream, size } + } catch (err) { + logger.error(err, 'controller.url.download.error', traceId) + throw err + } +} + +module.exports = { + downloadURL, +} diff --git a/packages/@uppy/companion/src/server/helpers/request.js b/packages/@uppy/companion/src/server/helpers/request.js index c1afed5f02..2bb895695a 100644 --- a/packages/@uppy/companion/src/server/helpers/request.js +++ b/packages/@uppy/companion/src/server/helpers/request.js @@ -105,10 +105,10 @@ module.exports.getProtectedGot = getProtectedGot * @param {boolean} allowLocalIPs * @returns {Promise<{name: string, type: string, size: number}>} */ -exports.getURLMeta = async (url, allowLocalIPs = false) => { +exports.getURLMeta = async (url, allowLocalIPs = false, options) => { async function requestWithMethod (method) { const protectedGot = await getProtectedGot({ allowLocalIPs }) - const stream = protectedGot.stream(url, { method, throwHttpErrors: false }) + const stream = protectedGot.stream(url, { method, throwHttpErrors: false, ...options }) return new Promise((resolve, reject) => ( stream diff --git a/packages/@uppy/companion/src/server/provider/google/drive/index.js b/packages/@uppy/companion/src/server/provider/google/drive/index.js index e08a55c60a..a4a790f3c5 100644 --- a/packages/@uppy/companion/src/server/provider/google/drive/index.js +++ b/packages/@uppy/companion/src/server/provider/google/drive/index.js @@ -43,6 +43,53 @@ async function getStats ({ id, token }) { return stats } + +async function streamGoogleFile({ token, id: idIn }) { + const client = await getClient({ token }) + + const { mimeType, id, exportLinks } = await getStats({ id: idIn, token }) + + let stream + + if (isGsuiteFile(mimeType)) { + const mimeType2 = getGsuiteExportType(mimeType) + logger.info(`calling google file export for ${id} to ${mimeType2}`, 'provider.drive.export') + + // GSuite files exported with large converted size results in error using standard export method. + // Error message: "This file is too large to be exported.". + // Issue logged in Google APIs: https://github.com/googleapis/google-api-nodejs-client/issues/3446 + // Implemented based on the answer from StackOverflow: https://stackoverflow.com/a/59168288 + const mimeTypeExportLink = exportLinks?.[mimeType2] + if (mimeTypeExportLink) { + const gSuiteFilesClient = (await got).extend({ + headers: { + authorization: `Bearer ${token}`, + }, + }) + stream = gSuiteFilesClient.stream.get(mimeTypeExportLink, { responseType: 'json' }) + } else { + stream = client.stream.get(`files/${encodeURIComponent(id)}/export`, { searchParams: { supportsAllDrives: true, mimeType: mimeType2 }, responseType: 'json' }) + } + } else { + stream = client.stream.get(`files/${encodeURIComponent(id)}`, { searchParams: { alt: 'media', supportsAllDrives: true }, responseType: 'json' }) + } + + await prepareStream(stream) + return { stream } +} + +async function getGoogleFileSize({ id, token }) { + const { mimeType, size } = await getStats({ id, token }) + + if (isGsuiteFile(mimeType)) { + // GSuite file sizes cannot be predetermined (but are max 10MB) + // e.g. Transfer-Encoding: chunked + return undefined + } + + return parseInt(size, 10) +} + /** * Adapter for API https://developers.google.com/drive/api/v3/ */ @@ -124,7 +171,7 @@ class Drive extends Provider { } // eslint-disable-next-line class-methods-use-this - async download ({ id: idIn, token }) { + async download ({ id, token }) { if (mockAccessTokenExpiredError != null) { logger.warn(`Access token: ${token}`) @@ -135,57 +182,23 @@ class Drive extends Provider { } return withGoogleErrorHandling(Drive.oauthProvider, 'provider.drive.download.error', async () => { - const client = await getClient({ token }) - - const { mimeType, id, exportLinks } = await getStats({ id: idIn, token }) - - let stream - - if (isGsuiteFile(mimeType)) { - const mimeType2 = getGsuiteExportType(mimeType) - logger.info(`calling google file export for ${id} to ${mimeType2}`, 'provider.drive.export') - - // GSuite files exported with large converted size results in error using standard export method. - // Error message: "This file is too large to be exported.". - // Issue logged in Google APIs: https://github.com/googleapis/google-api-nodejs-client/issues/3446 - // Implemented based on the answer from StackOverflow: https://stackoverflow.com/a/59168288 - const mimeTypeExportLink = exportLinks?.[mimeType2] - if (mimeTypeExportLink) { - const gSuiteFilesClient = (await got).extend({ - headers: { - authorization: `Bearer ${token}`, - }, - }) - stream = gSuiteFilesClient.stream.get(mimeTypeExportLink, { responseType: 'json' }) - } else { - stream = client.stream.get(`files/${encodeURIComponent(id)}/export`, { searchParams: { supportsAllDrives: true, mimeType: mimeType2 }, responseType: 'json' }) - } - } else { - stream = client.stream.get(`files/${encodeURIComponent(id)}`, { searchParams: { alt: 'media', supportsAllDrives: true }, responseType: 'json' }) - } - - await prepareStream(stream) - return { stream } + return streamGoogleFile({ token, id }) }) } // eslint-disable-next-line class-methods-use-this async size ({ id, token }) { - return withGoogleErrorHandling(Drive.oauthProvider, 'provider.drive.size.error', async () => { - const { mimeType, size } = await getStats({ id, token }) - - if (isGsuiteFile(mimeType)) { - // GSuite file sizes cannot be predetermined (but are max 10MB) - // e.g. Transfer-Encoding: chunked - return undefined - } - - return parseInt(size, 10) - }) + return withGoogleErrorHandling(Drive.oauthProvider, 'provider.drive.size.error', async () => ( + getGoogleFileSize({ id, token }) + )) } } Drive.prototype.logout = logout Drive.prototype.refreshToken = refreshToken -module.exports = Drive +module.exports = { + Drive, + streamGoogleFile, + getGoogleFileSize, +} diff --git a/packages/@uppy/companion/src/server/provider/index.js b/packages/@uppy/companion/src/server/provider/index.js index 52c707dac0..55854e8b74 100644 --- a/packages/@uppy/companion/src/server/provider/index.js +++ b/packages/@uppy/companion/src/server/provider/index.js @@ -3,7 +3,7 @@ */ const dropbox = require('./dropbox') const box = require('./box') -const drive = require('./google/drive') +const { Drive } = require('./google/drive') const googlephotos = require('./google/googlephotos') const instagram = require('./instagram/graph') const facebook = require('./facebook') @@ -68,7 +68,7 @@ module.exports.getProviderMiddleware = (providers, grantConfig) => { * @returns {Record} */ module.exports.getDefaultProviders = () => { - const providers = { dropbox, box, drive, googlephotos, facebook, onedrive, zoom, instagram, unsplash } + const providers = { dropbox, box, drive: Drive, googlephotos, facebook, onedrive, zoom, instagram, unsplash } return providers } diff --git a/packages/@uppy/companion/src/standalone/helper.js b/packages/@uppy/companion/src/standalone/helper.js index e6ccd9887b..57494dadb8 100644 --- a/packages/@uppy/companion/src/standalone/helper.js +++ b/packages/@uppy/companion/src/standalone/helper.js @@ -152,6 +152,7 @@ const getConfigFromEnv = () => { validHosts, }, enableUrlEndpoint: process.env.COMPANION_ENABLE_URL_ENDPOINT === 'true', + enableGooglePickerEndpoint: process.env.COMPANION_ENABLE_GOOGLE_PICKER_ENDPOINT === 'true', periodicPingUrls: process.env.COMPANION_PERIODIC_PING_URLS ? process.env.COMPANION_PERIODIC_PING_URLS.split(',') : [], periodicPingInterval: process.env.COMPANION_PERIODIC_PING_INTERVAL ? parseInt(process.env.COMPANION_PERIODIC_PING_INTERVAL, 10) : undefined, From c9a3d89b224affbabf5b8bb064c4cbc2bd97a883 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Fri, 15 Nov 2024 19:13:09 +0800 Subject: [PATCH 04/19] type todo --- packages/@uppy/provider-views/src/utils/getTagFile.ts | 1 - packages/@uppy/utils/src/CompanionClientProvider.ts | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@uppy/provider-views/src/utils/getTagFile.ts b/packages/@uppy/provider-views/src/utils/getTagFile.ts index 84bc4c39f5..bb3a8df0c8 100644 --- a/packages/@uppy/provider-views/src/utils/getTagFile.ts +++ b/packages/@uppy/provider-views/src/utils/getTagFile.ts @@ -34,7 +34,6 @@ const getTagFile = ( }, remote: { companionUrl: plugin.opts.companionUrl, - // @ts-expect-error untyped for now url: `${provider.fileUrl(file.requestPath)}`, body: { fileId: file.id, diff --git a/packages/@uppy/utils/src/CompanionClientProvider.ts b/packages/@uppy/utils/src/CompanionClientProvider.ts index 4ef80ecbfd..98877c909d 100644 --- a/packages/@uppy/utils/src/CompanionClientProvider.ts +++ b/packages/@uppy/utils/src/CompanionClientProvider.ts @@ -26,6 +26,7 @@ export interface CompanionClientProvider { login(options?: RequestOptions): Promise logout(options?: RequestOptions): Promise fetchPreAuthToken(): Promise + fileUrl: (a: string) => string list( directory: string | null, options: RequestOptions, @@ -38,5 +39,6 @@ export interface CompanionClientProvider { export interface CompanionClientSearchProvider { name: string provider: string + fileUrl: (a: string) => string search(text: string, queries?: string): Promise } From cd6ccd643afcf110e152d79ff01bbe8efc3599ae Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Fri, 15 Nov 2024 19:13:51 +0800 Subject: [PATCH 05/19] fix ts error which occurs in dev when js has been built before build:ts gets called --- packages/uppy/src/bundle.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/uppy/src/bundle.ts b/packages/uppy/src/bundle.ts index a74f4bc98a..2973473007 100644 --- a/packages/uppy/src/bundle.ts +++ b/packages/uppy/src/bundle.ts @@ -22,7 +22,9 @@ export const views = { ProviderView } // Stores export { default as DefaultStore } from '@uppy/store-default' -// @ts-expect-error untyped +// not yet typed +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore export { default as ReduxStore } from '@uppy/store-redux' // UI plugins @@ -63,7 +65,9 @@ export { default as XHRUpload } from '@uppy/xhr-upload' export { default as Compressor } from '@uppy/compressor' export { default as Form } from '@uppy/form' export { default as GoldenRetriever } from '@uppy/golden-retriever' -// @ts-expect-error untyped +// not yet typed +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore export { default as ReduxDevTools } from '@uppy/redux-dev-tools' export { default as ThumbnailGenerator } from '@uppy/thumbnail-generator' From 42453fbc78d797a31b94b3a7918faa8f0c143e0d Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Fri, 15 Nov 2024 19:14:37 +0800 Subject: [PATCH 06/19] reuse docs --- docs/sources/companion-plugins/box.mdx | 51 +------------------ docs/sources/companion-plugins/dropbox.mdx | 51 +------------------ docs/sources/companion-plugins/facebook.mdx | 51 +------------------ .../companion-plugins/google-drive.mdx | 51 +------------------ .../companion-plugins/google-photos.mdx | 50 +----------------- docs/sources/companion-plugins/instagram.mdx | 51 +------------------ docs/sources/companion-plugins/onedrive.mdx | 51 +------------------ docs/sources/companion-plugins/unsplash.mdx | 51 +------------------ docs/sources/companion-plugins/url.mdx | 51 +------------------ docs/sources/companion-plugins/zoom.mdx | 51 +------------------ 10 files changed, 10 insertions(+), 499 deletions(-) diff --git a/docs/sources/companion-plugins/box.mdx b/docs/sources/companion-plugins/box.mdx index 9312bbdbd9..71e1407afd 100644 --- a/docs/sources/companion-plugins/box.mdx +++ b/docs/sources/companion-plugins/box.mdx @@ -171,56 +171,7 @@ companion.app({ ### Options -#### `id` - -A unique identifier for this plugin (`string`, default: `'Box'`). - -#### `title` - -Title / name shown in the UI, such as Dashboard tabs (`string`, default: -`'Box'`). - -#### `target` - -DOM element, CSS selector, or plugin to place the drag and drop area into -(`string`, `Element`, `Function`, or `UIPlugin`, default: -[`Dashboard`](/docs/dashboard)). - -#### `companionUrl` - -URL to a [Companion](/docs/companion) instance (`string`, default: `null`). - -#### `companionHeaders` - -Custom headers that should be sent along to [Companion](/docs/companion) on -every request (`Object`, default: `{}`). - -#### `companionAllowedHosts` - -The valid and authorised URL(s) from which OAuth responses should be accepted -(`string` or `RegExp` or `Array`, default: `companionUrl`). - -This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is -useful when you have your [Companion](/docs/companion) running on several hosts. -Otherwise, the default value should do fine. - -#### `companionCookiesRule` - -This option correlates to the -[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials) -(`string`, default: `'same-origin'`). - -This tells the plugin whether to send cookies to [Companion](/docs/companion). - -#### `locale` - -```js -export default { - strings: { - pluginNameBox: 'Box', - }, -}; -``` +#### [Common Companion options...](./companion-options.mdx) [template-credentials]: https://transloadit.com/docs/#how-to-create-template-credentials diff --git a/docs/sources/companion-plugins/dropbox.mdx b/docs/sources/companion-plugins/dropbox.mdx index 4a111f35d5..e7a8efc207 100644 --- a/docs/sources/companion-plugins/dropbox.mdx +++ b/docs/sources/companion-plugins/dropbox.mdx @@ -171,56 +171,7 @@ companion.app({ ### Options -#### `id` - -A unique identifier for this plugin (`string`, default: `'Dropbox'`). - -#### `title` - -Title / name shown in the UI, such as Dashboard tabs (`string`, default: -`'Dropbox'`). - -#### `target` - -DOM element, CSS selector, or plugin to place the drag and drop area into -(`string`, `Element`, `Function`, or `UIPlugin`, default: -[`Dashboard`](/docs/dashboard)). - -#### `companionUrl` - -URL to a [Companion](/docs/companion) instance (`string`, default: `null`). - -#### `companionHeaders` - -Custom headers that should be sent along to [Companion](/docs/companion) on -every request (`Object`, default: `{}`). - -#### `companionAllowedHosts` - -The valid and authorised URL(s) from which OAuth responses should be accepted -(`string` or `RegExp` or `Array`, default: `companionUrl`). - -This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is -useful when you have your [Companion](/docs/companion) running on several hosts. -Otherwise, the default value should do fine. - -#### `companionCookiesRule` - -This option correlates to the -[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials) -(`string`, default: `'same-origin'`). - -This tells the plugin whether to send cookies to [Companion](/docs/companion). - -#### `locale` - -```js -export default { - strings: { - pluginNameDropbox: 'Dropbox', - }, -}; -``` +#### [Common Companion options...](./companion-options.mdx) [template-credentials]: https://transloadit.com/docs/#how-to-create-template-credentials diff --git a/docs/sources/companion-plugins/facebook.mdx b/docs/sources/companion-plugins/facebook.mdx index dcc4968094..71f248a49a 100644 --- a/docs/sources/companion-plugins/facebook.mdx +++ b/docs/sources/companion-plugins/facebook.mdx @@ -168,56 +168,7 @@ companion.app({ ### Options -#### `id` - -A unique identifier for this plugin (`string`, default: `'Facebook'`). - -#### `title` - -Title / name shown in the UI, such as Dashboard tabs (`string`, default: -`'Facebook'`). - -#### `target` - -DOM element, CSS selector, or plugin to place the drag and drop area into -(`string`, `Element`, `Function`, or `UIPlugin`, default: -[`Dashboard`](/docs/dashboard)). - -#### `companionUrl` - -URL to a [Companion](/docs/companion) instance (`string`, default: `null`). - -#### `companionHeaders` - -Custom headers that should be sent along to [Companion](/docs/companion) on -every request (`Object`, default: `{}`). - -#### `companionAllowedHosts` - -The valid and authorised URL(s) from which OAuth responses should be accepted -(`string` or `RegExp` or `Array`, default: `companionUrl`). - -This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is -useful when you have your [Companion](/docs/companion) running on several hosts. -Otherwise, the default value should do fine. - -#### `companionCookiesRule` - -This option correlates to the -[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials) -(`string`, default: `'same-origin'`). - -This tells the plugin whether to send cookies to [Companion](/docs/companion). - -#### `locale` - -```js -export default { - strings: { - pluginNameFacebook: 'Facebook', - }, -}; -``` +#### [Common Companion options...](./companion-options.mdx) [template-credentials]: https://transloadit.com/docs/#how-to-create-template-credentials diff --git a/docs/sources/companion-plugins/google-drive.mdx b/docs/sources/companion-plugins/google-drive.mdx index c682464ecb..4a3d587bb4 100644 --- a/docs/sources/companion-plugins/google-drive.mdx +++ b/docs/sources/companion-plugins/google-drive.mdx @@ -172,56 +172,7 @@ companion.app({ ### Options -#### `id` - -A unique identifier for this plugin (`string`, default: `'GoogleDrive'`). - -#### `title` - -Title / name shown in the UI, such as Dashboard tabs (`string`, default: -`'GoogleDrive'`). - -#### `target` - -DOM element, CSS selector, or plugin to place the drag and drop area into -(`string`, `Element`, `Function`, or `UIPlugin`, default: -[`Dashboard`](/docs/dashboard)). - -#### `companionUrl` - -URL to a [Companion](/docs/companion) instance (`string`, default: `null`). - -#### `companionHeaders` - -Custom headers that should be sent along to [Companion](/docs/companion) on -every request (`Object`, default: `{}`). - -#### `companionAllowedHosts` - -The valid and authorised URL(s) from which OAuth responses should be accepted -(`string` or `RegExp` or `Array`, default: `companionUrl`). - -This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is -useful when you have your [Companion](/docs/companion) running on several hosts. -Otherwise, the default value should do fine. - -#### `companionCookiesRule` - -This option correlates to the -[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials) -(`string`, default: `'same-origin'`). - -This tells the plugin whether to send cookies to [Companion](/docs/companion). - -#### `locale` - -```js -export default { - strings: { - pluginNameGoogleDrive: 'GoogleDrive', - }, -}; -``` +#### [Common Companion options...](./companion-options.mdx) [template-credentials]: https://transloadit.com/docs/#how-to-create-template-credentials diff --git a/docs/sources/companion-plugins/google-photos.mdx b/docs/sources/companion-plugins/google-photos.mdx index e90cfb6e65..fc6fb39d8e 100644 --- a/docs/sources/companion-plugins/google-photos.mdx +++ b/docs/sources/companion-plugins/google-photos.mdx @@ -169,55 +169,7 @@ companion.app({ ### Options -#### `id` - -A unique identifier for this plugin (`string`, default: `'GooglePhotos'`). - -#### `title` - -Title / name shown in the UI, such as Dashboard tabs (`string`, default: -`'GooglePhotos'`). - -#### `target` - -DOM element, CSS selector, or plugin to place the drag and drop area into -(`string` or `Element`, default: `null`). - -#### `companionUrl` - -URL to a [Companion](/docs/companion) instance (`string`, default: `null`). - -#### `companionHeaders` - -Custom headers that should be sent along to [Companion](/docs/companion) on -every request (`Object`, default: `{}`). - -#### `companionAllowedHosts` - -The valid and authorised URL(s) from which OAuth responses should be accepted -(`string` or `RegExp` or `Array`, default: `companionUrl`). - -This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is -useful when you have your [Companion](/docs/companion) running on several hosts. -Otherwise, the default value should do fine. - -#### `companionCookiesRule` - -This option correlates to the -[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials) -(`string`, default: `'same-origin'`). - -This tells the plugin whether to send cookies to [Companion](/docs/companion). - -#### `locale` - -```js -export default { - strings: { - pluginNameGooglePhotos: 'GooglePhotos', - }, -}; -``` +#### [Common Companion options...](./companion-options.mdx) [template-credentials]: https://transloadit.com/docs/#how-to-create-template-credentials diff --git a/docs/sources/companion-plugins/instagram.mdx b/docs/sources/companion-plugins/instagram.mdx index ca920d86cf..2521555ef4 100644 --- a/docs/sources/companion-plugins/instagram.mdx +++ b/docs/sources/companion-plugins/instagram.mdx @@ -181,56 +181,7 @@ page, in the “Valid OAuth Redirect URIs” field, add ### Options -#### `id` - -A unique identifier for this plugin (`string`, default: `'Instagram'`). - -#### `title` - -Title / name shown in the UI, such as Dashboard tabs (`string`, default: -`'Instagram'`). - -#### `target` - -DOM element, CSS selector, or plugin to place the drag and drop area into -(`string`, `Element`, `Function`, or `UIPlugin`, default: -[`Dashboard`](/docs/dashboard)). - -#### `companionUrl` - -URL to a [Companion](/docs/companion) instance (`string`, default: `null`). - -#### `companionHeaders` - -Custom headers that should be sent along to [Companion](/docs/companion) on -every request (`Object`, default: `{}`). - -#### `companionAllowedHosts` - -The valid and authorised URL(s) from which OAuth responses should be accepted -(`string` or `RegExp` or `Array`, default: `companionUrl`). - -This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is -useful when you have your [Companion](/docs/companion) running on several hosts. -Otherwise, the default value should do fine. - -#### `companionCookiesRule` - -This option correlates to the -[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials) -(`string`, default: `'same-origin'`). - -This tells the plugin whether to send cookies to [Companion](/docs/companion). - -#### `locale` - -```js -export default { - strings: { - pluginNameInstagram: 'Instagram', - }, -}; -``` +#### [Common Companion options...](./companion-options.mdx) [template-credentials]: https://transloadit.com/docs/#how-to-create-template-credentials diff --git a/docs/sources/companion-plugins/onedrive.mdx b/docs/sources/companion-plugins/onedrive.mdx index b91c3931e3..97084b7419 100644 --- a/docs/sources/companion-plugins/onedrive.mdx +++ b/docs/sources/companion-plugins/onedrive.mdx @@ -169,56 +169,7 @@ companion.app({ ### Options -#### `id` - -A unique identifier for this plugin (`string`, default: `'OneDrive'`). - -#### `title` - -Title / name shown in the UI, such as Dashboard tabs (`string`, default: -`'OneDrive'`). - -#### `target` - -DOM element, CSS selector, or plugin to place the drag and drop area into -(`string`, `Element`, `Function`, or `UIPlugin`, default: -[`Dashboard`](/docs/dashboard)). - -#### `companionUrl` - -URL to a [Companion](/docs/companion) instance (`string`, default: `null`). - -#### `companionHeaders` - -Custom headers that should be sent along to [Companion](/docs/companion) on -every request (`Object`, default: `{}`). - -#### `companionAllowedHosts` - -The valid and authorised URL(s) from which OAuth responses should be accepted -(`string` or `RegExp` or `Array`, default: `companionUrl`). - -This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is -useful when you have your [Companion](/docs/companion) running on several hosts. -Otherwise, the default value should do fine. - -#### `companionCookiesRule` - -This option correlates to the -[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials) -(`string`, default: `'same-origin'`). - -This tells the plugin whether to send cookies to [Companion](/docs/companion). - -#### `locale` - -```js -export default { - strings: { - pluginNameOneDrive: 'OneDrive', - }, -}; -``` +#### [Common Companion options...](./companion-options.mdx) [template-credentials]: https://transloadit.com/docs/#how-to-create-template-credentials diff --git a/docs/sources/companion-plugins/unsplash.mdx b/docs/sources/companion-plugins/unsplash.mdx index 1287729aeb..3771f41772 100644 --- a/docs/sources/companion-plugins/unsplash.mdx +++ b/docs/sources/companion-plugins/unsplash.mdx @@ -147,56 +147,7 @@ companion.app({ ### Options -#### `id` - -A unique identifier for this plugin (`string`, default: `'Unsplash'`). - -#### `title` - -Title / name shown in the UI, such as Dashboard tabs (`string`, default: -`'Unsplash'`). - -#### `target` - -DOM element, CSS selector, or plugin to place the drag and drop area into -(`string`, `Element`, `Function`, or `UIPlugin`, default: -[`Dashboard`](/docs/dashboard)). - -#### `companionUrl` - -URL to a [Companion](/docs/companion) instance (`string`, default: `null`). - -#### `companionHeaders` - -Custom headers that should be sent along to [Companion](/docs/companion) on -every request (`Object`, default: `{}`). - -#### `companionAllowedHosts` - -The valid and authorised URL(s) from which OAuth responses should be accepted -(`string` or `RegExp` or `Array`, default: `companionUrl`). - -This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is -useful when you have your [Companion](/docs/companion) running on several hosts. -Otherwise, the default value should do fine. - -#### `companionCookiesRule` - -This option correlates to the -[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials) -(`string`, default: `'same-origin'`). - -This tells the plugin whether to send cookies to [Companion](/docs/companion). - -#### `locale` - -```js -export default { - strings: { - pluginNameUnsplash: 'Unsplash', - }, -}; -``` +#### [Common Companion options...](./companion-options.mdx) [template-credentials]: https://transloadit.com/docs/#how-to-create-template-credentials diff --git a/docs/sources/companion-plugins/url.mdx b/docs/sources/companion-plugins/url.mdx index 52892db586..de342af754 100644 --- a/docs/sources/companion-plugins/url.mdx +++ b/docs/sources/companion-plugins/url.mdx @@ -133,56 +133,7 @@ Companion with the `enableUrlEndpoint` / `COMPANION_ENABLE_URL_ENDPOINT` option. ### Options -#### `id` - -A unique identifier for this plugin (`string`, default: `'Url'`). - -#### `title` - -Title / name shown in the UI, such as Dashboard tabs (`string`, default: -`'Url'`). - -#### `target` - -DOM element, CSS selector, or plugin to place the drag and drop area into -(`string`, `Element`, `Function`, or `UIPlugin`, default: -[`Dashboard`](/docs/dashboard)). - -#### `companionUrl` - -URL to a [Companion](/docs/companion) instance (`string`, default: `null`). - -#### `companionHeaders` - -Custom headers that should be sent along to [Companion](/docs/companion) on -every request (`Object`, default: `{}`). - -#### `companionAllowedHosts` - -The valid and authorised URL(s) from which OAuth responses should be accepted -(`string` or `RegExp` or `Array`, default: `companionUrl`). - -This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is -useful when you have your [Companion](/docs/companion) running on several hosts. -Otherwise, the default value should do fine. - -#### `companionCookiesRule` - -This option correlates to the -[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials) -(`string`, default: `'same-origin'`). - -This tells the plugin whether to send cookies to [Companion](/docs/companion). - -#### `locale` - -```js -export default { - strings: { - pluginNameUrl: 'Url', - }, -}; -``` +#### [Common Companion options...](./companion-options.mdx) [template-credentials]: https://transloadit.com/docs/#how-to-create-template-credentials diff --git a/docs/sources/companion-plugins/zoom.mdx b/docs/sources/companion-plugins/zoom.mdx index 83dbbc7c04..5bac89dc52 100644 --- a/docs/sources/companion-plugins/zoom.mdx +++ b/docs/sources/companion-plugins/zoom.mdx @@ -172,56 +172,7 @@ To sign up for API keys, go through the following steps: ### Options -#### `id` - -A unique identifier for this plugin (`string`, default: `'Zoom'`). - -#### `title` - -Title / name shown in the UI, such as Dashboard tabs (`string`, default: -`'Zoom'`). - -#### `target` - -DOM element, CSS selector, or plugin to place the drag and drop area into -(`string`, `Element`, `Function`, or `UIPlugin`, default: -[`Dashboard`](/docs/dashboard)). - -#### `companionUrl` - -URL to a [Companion](/docs/companion) instance (`string`, default: `null`). - -#### `companionHeaders` - -Custom headers that should be sent along to [Companion](/docs/companion) on -every request (`Object`, default: `{}`). - -#### `companionAllowedHosts` - -The valid and authorised URL(s) from which OAuth responses should be accepted -(`string` or `RegExp` or `Array`, default: `companionUrl`). - -This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is -useful when you have your [Companion](/docs/companion) running on several hosts. -Otherwise, the default value should do fine. - -#### `companionCookiesRule` - -This option correlates to the -[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials) -(`string`, default: `'same-origin'`). - -This tells the plugin whether to send cookies to [Companion](/docs/companion). - -#### `locale` - -```js -export default { - strings: { - pluginNameZoom: 'Zoom', - }, -}; -``` +#### [Common Companion options...](./companion-options.mdx) [template-credentials]: https://transloadit.com/docs/#how-to-create-template-credentials From 9001cdd4f84da2c43ef068cf3307d7dd69580294 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Fri, 15 Nov 2024 19:15:51 +0800 Subject: [PATCH 07/19] imrpve type safety --- packages/@uppy/box/src/Box.tsx | 18 ++++++++++-------- .../src/CompanionPluginOptions.ts | 4 ++-- packages/@uppy/dropbox/src/Dropbox.tsx | 18 ++++++++++-------- packages/@uppy/facebook/src/Facebook.tsx | 18 ++++++++++-------- .../@uppy/google-drive/src/GoogleDrive.tsx | 16 ++++++++++------ .../@uppy/google-photos/src/GooglePhotos.tsx | 16 ++++++++++------ packages/@uppy/instagram/src/Instagram.tsx | 18 ++++++++++-------- packages/@uppy/onedrive/src/OneDrive.tsx | 18 ++++++++++-------- packages/@uppy/unsplash/src/Unsplash.tsx | 18 ++++++++++-------- packages/@uppy/zoom/src/Zoom.tsx | 18 ++++++++++-------- 10 files changed, 92 insertions(+), 70 deletions(-) diff --git a/packages/@uppy/box/src/Box.tsx b/packages/@uppy/box/src/Box.tsx index 74c0e903b6..e004aff38b 100644 --- a/packages/@uppy/box/src/Box.tsx +++ b/packages/@uppy/box/src/Box.tsx @@ -9,7 +9,11 @@ import { ProviderViews } from '@uppy/provider-views' import { h, type ComponentChild } from 'preact' import type { UppyFile, Body, Meta } from '@uppy/utils/lib/UppyFile' -import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy.js' +import type { + AsyncStore, + UnknownProviderPlugin, + UnknownProviderPluginState, +} from '@uppy/core/lib/Uppy.js' import locale from './locale.ts' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore We don't want TS to generate types for the package.json @@ -17,12 +21,10 @@ import packageJson from '../package.json' export type BoxOptions = CompanionPluginOptions -export default class Box extends UIPlugin< - BoxOptions, - M, - B, - UnknownProviderPluginState -> { +export default class Box + extends UIPlugin + implements UnknownProviderPlugin +{ static VERSION = packageJson.version icon: () => h.JSX.Element @@ -31,7 +33,7 @@ export default class Box extends UIPlugin< view!: ProviderViews - storage: typeof tokenStorage + storage: AsyncStore files: UppyFile[] diff --git a/packages/@uppy/companion-client/src/CompanionPluginOptions.ts b/packages/@uppy/companion-client/src/CompanionPluginOptions.ts index 751357f7f4..a923952e75 100644 --- a/packages/@uppy/companion-client/src/CompanionPluginOptions.ts +++ b/packages/@uppy/companion-client/src/CompanionPluginOptions.ts @@ -1,8 +1,8 @@ import type { UIPluginOptions } from '@uppy/core' -import type { tokenStorage } from './index.ts' +import type { AsyncStore } from '@uppy/core/lib/Uppy.js' export interface CompanionPluginOptions extends UIPluginOptions { - storage?: typeof tokenStorage + storage?: AsyncStore companionUrl: string companionHeaders?: Record companionKeysParams?: { key: string; credentialsName: string } diff --git a/packages/@uppy/dropbox/src/Dropbox.tsx b/packages/@uppy/dropbox/src/Dropbox.tsx index 17082dc561..e94c9000c5 100644 --- a/packages/@uppy/dropbox/src/Dropbox.tsx +++ b/packages/@uppy/dropbox/src/Dropbox.tsx @@ -9,7 +9,11 @@ import { ProviderViews } from '@uppy/provider-views' import { h, type ComponentChild } from 'preact' import type { UppyFile, Body, Meta } from '@uppy/utils/lib/UppyFile' -import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy.js' +import type { + AsyncStore, + UnknownProviderPlugin, + UnknownProviderPluginState, +} from '@uppy/core/lib/Uppy.js' import locale from './locale.ts' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore We don't want TS to generate types for the package.json @@ -17,12 +21,10 @@ import packageJson from '../package.json' export type DropboxOptions = CompanionPluginOptions -export default class Dropbox extends UIPlugin< - DropboxOptions, - M, - B, - UnknownProviderPluginState -> { +export default class Dropbox + extends UIPlugin + implements UnknownProviderPlugin +{ static VERSION = packageJson.version icon: () => h.JSX.Element @@ -31,7 +33,7 @@ export default class Dropbox extends UIPlugin< view!: ProviderViews - storage: typeof tokenStorage + storage: AsyncStore files: UppyFile[] diff --git a/packages/@uppy/facebook/src/Facebook.tsx b/packages/@uppy/facebook/src/Facebook.tsx index fdf253a576..41f388e825 100644 --- a/packages/@uppy/facebook/src/Facebook.tsx +++ b/packages/@uppy/facebook/src/Facebook.tsx @@ -9,7 +9,11 @@ import { ProviderViews } from '@uppy/provider-views' import { h, type ComponentChild } from 'preact' import type { UppyFile, Body, Meta } from '@uppy/utils/lib/UppyFile' -import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy.js' +import type { + AsyncStore, + UnknownProviderPlugin, + UnknownProviderPluginState, +} from '@uppy/core/lib/Uppy.js' import locale from './locale.ts' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore We don't want TS to generate types for the package.json @@ -17,12 +21,10 @@ import packageJson from '../package.json' export type FacebookOptions = CompanionPluginOptions -export default class Facebook extends UIPlugin< - FacebookOptions, - M, - B, - UnknownProviderPluginState -> { +export default class Facebook + extends UIPlugin + implements UnknownProviderPlugin +{ static VERSION = packageJson.version icon: () => h.JSX.Element @@ -31,7 +33,7 @@ export default class Facebook extends UIPlugin< view!: ProviderViews - storage: typeof tokenStorage + storage: AsyncStore files: UppyFile[] diff --git a/packages/@uppy/google-drive/src/GoogleDrive.tsx b/packages/@uppy/google-drive/src/GoogleDrive.tsx index 028d8ee287..8c902bdd98 100644 --- a/packages/@uppy/google-drive/src/GoogleDrive.tsx +++ b/packages/@uppy/google-drive/src/GoogleDrive.tsx @@ -9,7 +9,11 @@ import { ProviderViews } from '@uppy/provider-views' import { h, type ComponentChild } from 'preact' import type { UppyFile, Body, Meta } from '@uppy/utils/lib/UppyFile' -import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy.js' +import type { + AsyncStore, + UnknownProviderPlugin, + UnknownProviderPluginState, +} from '@uppy/core/lib/Uppy.js' import DriveProviderViews from './DriveProviderViews.ts' import locale from './locale.ts' // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -18,10 +22,10 @@ import packageJson from '../package.json' export type GoogleDriveOptions = CompanionPluginOptions -export default class GoogleDrive< - M extends Meta, - B extends Body, -> extends UIPlugin { +export default class GoogleDrive + extends UIPlugin + implements UnknownProviderPlugin +{ static VERSION = packageJson.version icon: () => h.JSX.Element @@ -30,7 +34,7 @@ export default class GoogleDrive< view!: ProviderViews - storage: typeof tokenStorage + storage: AsyncStore files: UppyFile[] diff --git a/packages/@uppy/google-photos/src/GooglePhotos.tsx b/packages/@uppy/google-photos/src/GooglePhotos.tsx index eba6bbb3e2..248f7e5ac1 100644 --- a/packages/@uppy/google-photos/src/GooglePhotos.tsx +++ b/packages/@uppy/google-photos/src/GooglePhotos.tsx @@ -9,7 +9,11 @@ import { import { h, type ComponentChild } from 'preact' import type { UppyFile, Body, Meta } from '@uppy/utils/lib/UppyFile' -import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy.js' +import type { + AsyncStore, + UnknownProviderPlugin, + UnknownProviderPluginState, +} from '@uppy/core/lib/Uppy.js' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore We don't want TS to generate types for the package.json @@ -18,10 +22,10 @@ import locale from './locale.ts' export type GooglePhotosOptions = CompanionPluginOptions -export default class GooglePhotos< - M extends Meta, - B extends Body, -> extends UIPlugin { +export default class GooglePhotos + extends UIPlugin + implements UnknownProviderPlugin +{ static VERSION = packageJson.version icon: () => h.JSX.Element @@ -30,7 +34,7 @@ export default class GooglePhotos< view!: ProviderViews - storage: typeof tokenStorage + storage: AsyncStore files: UppyFile[] diff --git a/packages/@uppy/instagram/src/Instagram.tsx b/packages/@uppy/instagram/src/Instagram.tsx index 4c85265c20..c0cd987d17 100644 --- a/packages/@uppy/instagram/src/Instagram.tsx +++ b/packages/@uppy/instagram/src/Instagram.tsx @@ -9,7 +9,11 @@ import { ProviderViews } from '@uppy/provider-views' import { h, type ComponentChild } from 'preact' import type { UppyFile, Body, Meta } from '@uppy/utils/lib/UppyFile' -import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy.js' +import type { + AsyncStore, + UnknownProviderPlugin, + UnknownProviderPluginState, +} from '@uppy/core/lib/Uppy.js' import locale from './locale.ts' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore We don't want TS to generate types for the package.json @@ -17,12 +21,10 @@ import packageJson from '../package.json' export type InstagramOptions = CompanionPluginOptions -export default class Instagram extends UIPlugin< - InstagramOptions, - M, - B, - UnknownProviderPluginState -> { +export default class Instagram + extends UIPlugin + implements UnknownProviderPlugin +{ static VERSION = packageJson.version icon: () => h.JSX.Element @@ -31,7 +33,7 @@ export default class Instagram extends UIPlugin< view!: ProviderViews - storage: typeof tokenStorage + storage: AsyncStore files: UppyFile[] diff --git a/packages/@uppy/onedrive/src/OneDrive.tsx b/packages/@uppy/onedrive/src/OneDrive.tsx index b954058a6f..bdfcfe2763 100644 --- a/packages/@uppy/onedrive/src/OneDrive.tsx +++ b/packages/@uppy/onedrive/src/OneDrive.tsx @@ -8,8 +8,12 @@ import { UIPlugin, Uppy } from '@uppy/core' import { ProviderViews } from '@uppy/provider-views' import { h, type ComponentChild } from 'preact' +import type { AsyncStore } from '@uppy/core/src/Uppy.js' import type { UppyFile, Body, Meta } from '@uppy/utils/lib/UppyFile' -import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy.js' +import type { + UnknownProviderPlugin, + UnknownProviderPluginState, +} from '@uppy/core/lib/Uppy.js' import locale from './locale.ts' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore We don't want TS to generate types for the package.json @@ -17,12 +21,10 @@ import packageJson from '../package.json' export type OneDriveOptions = CompanionPluginOptions -export default class OneDrive extends UIPlugin< - OneDriveOptions, - M, - B, - UnknownProviderPluginState -> { +export default class OneDrive + extends UIPlugin + implements UnknownProviderPlugin +{ static VERSION = packageJson.version icon: () => h.JSX.Element @@ -31,7 +33,7 @@ export default class OneDrive extends UIPlugin< view!: ProviderViews - storage: typeof tokenStorage + storage: AsyncStore files: UppyFile[] diff --git a/packages/@uppy/unsplash/src/Unsplash.tsx b/packages/@uppy/unsplash/src/Unsplash.tsx index fbbf5dcc5f..a91dfcc1f3 100644 --- a/packages/@uppy/unsplash/src/Unsplash.tsx +++ b/packages/@uppy/unsplash/src/Unsplash.tsx @@ -9,7 +9,11 @@ import { SearchProviderViews } from '@uppy/provider-views' import { h, type ComponentChild } from 'preact' import type { UppyFile, Body, Meta } from '@uppy/utils/lib/UppyFile' -import type { UnknownSearchProviderPluginState } from '@uppy/core/lib/Uppy.js' +import type { + AsyncStore, + UnknownSearchProviderPlugin, + UnknownSearchProviderPluginState, +} from '@uppy/core/lib/Uppy.js' import locale from './locale.ts' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore We don't want TS to generate types for the package.json @@ -17,12 +21,10 @@ import packageJson from '../package.json' export type UnsplashOptions = CompanionPluginOptions -export default class Unsplash extends UIPlugin< - UnsplashOptions, - M, - B, - UnknownSearchProviderPluginState -> { +export default class Unsplash + extends UIPlugin + implements UnknownSearchProviderPlugin +{ static VERSION = packageJson.version icon: () => h.JSX.Element @@ -31,7 +33,7 @@ export default class Unsplash extends UIPlugin< view!: SearchProviderViews - storage: typeof tokenStorage + storage: AsyncStore files: UppyFile[] diff --git a/packages/@uppy/zoom/src/Zoom.tsx b/packages/@uppy/zoom/src/Zoom.tsx index 888e359a01..4f70bcfdd0 100644 --- a/packages/@uppy/zoom/src/Zoom.tsx +++ b/packages/@uppy/zoom/src/Zoom.tsx @@ -9,7 +9,11 @@ import { ProviderViews } from '@uppy/provider-views' import { h, type ComponentChild } from 'preact' import type { UppyFile, Body, Meta } from '@uppy/utils/lib/UppyFile' -import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy.js' +import type { + AsyncStore, + UnknownProviderPlugin, + UnknownProviderPluginState, +} from '@uppy/core/lib/Uppy.js' import locale from './locale.ts' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore We don't want TS to generate types for the package.json @@ -17,12 +21,10 @@ import packageJson from '../package.json' export type ZoomOptions = CompanionPluginOptions -export default class Zoom extends UIPlugin< - ZoomOptions, - M, - B, - UnknownProviderPluginState -> { +export default class Zoom + extends UIPlugin + implements UnknownProviderPlugin +{ static VERSION = packageJson.version icon: () => h.JSX.Element @@ -31,7 +33,7 @@ export default class Zoom extends UIPlugin< view!: ProviderViews - storage: typeof tokenStorage + storage: AsyncStore files: UppyFile[] From b97a583c00e7fdaad1a77b6d8a6ee22914f14441 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Fri, 15 Nov 2024 19:16:12 +0800 Subject: [PATCH 08/19] simplify async wrapper --- .../companion-client/src/tokenStorage.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/@uppy/companion-client/src/tokenStorage.ts b/packages/@uppy/companion-client/src/tokenStorage.ts index 24de0685cd..581bdff1ea 100644 --- a/packages/@uppy/companion-client/src/tokenStorage.ts +++ b/packages/@uppy/companion-client/src/tokenStorage.ts @@ -1,20 +1,15 @@ /** * This module serves as an Async wrapper for LocalStorage + * Why? Because the Provider API `storage` option allows an async storage */ -export function setItem(key: string, value: string): Promise { - return new Promise((resolve) => { - localStorage.setItem(key, value) - resolve() - }) +export async function setItem(key: string, value: string): Promise { + localStorage.setItem(key, value) } -export function getItem(key: string): Promise { - return Promise.resolve(localStorage.getItem(key)) +export async function getItem(key: string): Promise { + return localStorage.getItem(key) } -export function removeItem(key: string): Promise { - return new Promise((resolve) => { - localStorage.removeItem(key) - resolve() - }) +export async function removeItem(key: string): Promise { + localStorage.removeItem(key) } From d76294ca4ad1aa4a60f78ad16c3fedd269084bf4 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Fri, 15 Nov 2024 19:20:14 +0800 Subject: [PATCH 09/19] improve doc --- docs/sources/companion-plugins/google-drive-picker.mdx | 7 +++++-- docs/sources/companion-plugins/google-photos-picker.mdx | 6 ++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/sources/companion-plugins/google-drive-picker.mdx b/docs/sources/companion-plugins/google-drive-picker.mdx index f411e4e84b..44e7bac0b3 100644 --- a/docs/sources/companion-plugins/google-drive-picker.mdx +++ b/docs/sources/companion-plugins/google-drive-picker.mdx @@ -67,9 +67,12 @@ To sign up for API keys, go to the Create a project for your app if you don’t have one yet. -- On the project’s dashboard, enable the Google Picker API (for Google Drive). +- On the project’s dashboard, enable the + [Google Picker API](https://console.cloud.google.com/apis/library/picker.googleapis.com) + (for Google Drive). - Create an API key. -- Create an OAuth 2.0 Client ID with the correct Authorized JavaScript origins. +- Create an OAuth 2.0 Client ID of type Web application with the correct + Authorized JavaScript origins. ### Use in Uppy diff --git a/docs/sources/companion-plugins/google-photos-picker.mdx b/docs/sources/companion-plugins/google-photos-picker.mdx index f7ec25e6ee..8138be93d8 100644 --- a/docs/sources/companion-plugins/google-photos-picker.mdx +++ b/docs/sources/companion-plugins/google-photos-picker.mdx @@ -67,8 +67,10 @@ To sign up for API keys, go to the Create a project for your app if you don’t have one yet. -- On the project’s dashboard, enable the Google Photos Picker API. -- Create an OAuth 2.0 Client ID with the correct Authorized JavaScript origins. +- On the project’s dashboard, enable the + [Google Photos Picker API](https://console.cloud.google.com/apis/library/photospicker.googleapis.com). +- Create an OAuth 2.0 Client ID of type Web application with the correct + Authorized JavaScript origins. ### Use in Uppy From 51f588406a18c45bd36186ca19ad2383b9cf9e32 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Fri, 15 Nov 2024 19:38:12 +0800 Subject: [PATCH 10/19] fix lint --- docs/sources/companion-plugins/google-drive-picker.mdx | 5 +++++ docs/sources/companion-plugins/google-photos-picker.mdx | 5 +++++ packages/@uppy/companion/src/server/helpers/request.js | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/sources/companion-plugins/google-drive-picker.mdx b/docs/sources/companion-plugins/google-drive-picker.mdx index 44e7bac0b3..3a4adf4300 100644 --- a/docs/sources/companion-plugins/google-drive-picker.mdx +++ b/docs/sources/companion-plugins/google-drive-picker.mdx @@ -2,6 +2,11 @@ slug: /google-drive-picker --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +import UppyCdnExample from '/src/components/UppyCdnExample'; + # Google Drive Picker The `@uppy/google-drive` plugin lets users import files from their diff --git a/docs/sources/companion-plugins/google-photos-picker.mdx b/docs/sources/companion-plugins/google-photos-picker.mdx index 8138be93d8..195710dba9 100644 --- a/docs/sources/companion-plugins/google-photos-picker.mdx +++ b/docs/sources/companion-plugins/google-photos-picker.mdx @@ -2,6 +2,11 @@ slug: /google-photos-picker --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +import UppyCdnExample from '/src/components/UppyCdnExample'; + # Google Photos Picker The `@uppy/google-photos` plugin lets users import files from their diff --git a/packages/@uppy/companion/src/server/helpers/request.js b/packages/@uppy/companion/src/server/helpers/request.js index 2bb895695a..6ef96a6257 100644 --- a/packages/@uppy/companion/src/server/helpers/request.js +++ b/packages/@uppy/companion/src/server/helpers/request.js @@ -105,7 +105,7 @@ module.exports.getProtectedGot = getProtectedGot * @param {boolean} allowLocalIPs * @returns {Promise<{name: string, type: string, size: number}>} */ -exports.getURLMeta = async (url, allowLocalIPs = false, options) => { +exports.getURLMeta = async (url, allowLocalIPs = false, options = undefined) => { async function requestWithMethod (method) { const protectedGot = await getProtectedGot({ allowLocalIPs }) const stream = protectedGot.stream(url, { method, throwHttpErrors: false, ...options }) From 8abc48056228deeccc58ece421cb46c5e4262804 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Sat, 16 Nov 2024 21:35:43 +0800 Subject: [PATCH 11/19] fix build error --- .../@uppy/provider-views/src/GooglePicker/GooglePickerView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@uppy/provider-views/src/GooglePicker/GooglePickerView.tsx b/packages/@uppy/provider-views/src/GooglePicker/GooglePickerView.tsx index 74b25c1e07..bca96d7bd0 100644 --- a/packages/@uppy/provider-views/src/GooglePicker/GooglePickerView.tsx +++ b/packages/@uppy/provider-views/src/GooglePicker/GooglePickerView.tsx @@ -3,7 +3,7 @@ import { Fragment } from 'preact/compat' import type { UIPlugin, Uppy } from '@uppy/core' import useStore from '@uppy/core/lib/useStore.js' import { useCallback, useEffect, useRef, useState } from 'preact/hooks' -import { useUppyPluginState } from '@uppy/core/lib/useUppyState' +import { useUppyPluginState } from '@uppy/core/lib/useUppyState.js' import type { AsyncStore } from '@uppy/core/lib/Uppy.js' export type PluginState = { From 5ff91a38ccf0eef2612f267c67250bdb7734fc64 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Tue, 26 Nov 2024 12:31:33 +0800 Subject: [PATCH 12/19] check if token is valid --- .../src/GooglePicker/GooglePickerView.tsx | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/@uppy/provider-views/src/GooglePicker/GooglePickerView.tsx b/packages/@uppy/provider-views/src/GooglePicker/GooglePickerView.tsx index bca96d7bd0..659caac4ab 100644 --- a/packages/@uppy/provider-views/src/GooglePicker/GooglePickerView.tsx +++ b/packages/@uppy/provider-views/src/GooglePicker/GooglePickerView.tsx @@ -111,6 +111,28 @@ async function injectScript(src: string) { injectedScripts.add(src) } +async function isTokenValid(accessToken: string): Promise { + try { + const response = await fetch( + `https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=${encodeURIComponent(accessToken)}`, + ) + if (response.ok) { + await response.json() + return true + } + console.warn( + 'Token is invalid or expired:', + response.status, + await response.text(), + ) + // Token is invalid or expired + return false + } catch (error) { + console.error('Error checking token validity:', error) + return false + } +} + export type GooglePickerViewProps = { plugin: UIPlugin uppy: Uppy @@ -307,11 +329,19 @@ export default function GooglePickerView({ })() }, [pickerType, setPluginState, uppy]) - const showPicker = useCallback(() => { + const showPicker = useCallback(async () => { if (accessToken === undefined) return // not yet loaded if (accessToken === null) { authorize() - } else if (pickerType === 'drive') { + return + } + // google drive picker will crash hard if given an invalid token, so we need to check it first + // https://github.com/transloadit/uppy/pull/5443#pullrequestreview-2452439265 + if (!(await isTokenValid(accessToken))) { + authorize() + return + } + if (pickerType === 'drive') { showDrivePicker(accessToken) } else { showPhotosPicker(accessToken) @@ -376,7 +406,7 @@ export default function GooglePickerView({ useEffect(() => { // if we have a session, poll it until it either times out, or the user selects some photos - // note that the user can also just close the page, but we have no indication of that, + // note that the user can also just close the page, but we get no indication of that from Google when polling, // so we just have to continue polling, in case the user opens the photo selector again if (pickingSession == null || accessToken == null) return undefined From 8fd3ae92eec8aed3851188f947832524236220e6 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Tue, 26 Nov 2024 16:55:28 +0800 Subject: [PATCH 13/19] fix broken logging code --- packages/@uppy/companion-client/src/Provider.ts | 5 +---- packages/@uppy/companion-client/src/RequestClient.ts | 8 ++++---- packages/@uppy/core/src/Uppy.ts | 5 ++--- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/@uppy/companion-client/src/Provider.ts b/packages/@uppy/companion-client/src/Provider.ts index bd0fb96214..2f6bdc066c 100644 --- a/packages/@uppy/companion-client/src/Provider.ts +++ b/packages/@uppy/companion-client/src/Provider.ts @@ -320,10 +320,7 @@ export default class Provider // Once a refresh token operation has started, we need all other request to wait for this operation (atomically) this.#refreshingTokenPromise = (async () => { try { - this.uppy.log( - `[CompanionClient] Refreshing expired auth token`, - 'info', - ) + this.uppy.log(`[CompanionClient] Refreshing expired auth token`) const response = await super.request<{ uppyAuthToken: string }>({ path: this.refreshTokenUrl(), method: 'POST', diff --git a/packages/@uppy/companion-client/src/RequestClient.ts b/packages/@uppy/companion-client/src/RequestClient.ts index 236ce62527..68a0992941 100644 --- a/packages/@uppy/companion-client/src/RequestClient.ts +++ b/packages/@uppy/companion-client/src/RequestClient.ts @@ -505,7 +505,7 @@ export default class RequestClient { }) const closeSocket = () => { - this.uppy.log(`Closing socket ${file.id}`, 'info') + this.uppy.log(`Closing socket ${file.id}`) clearTimeout(activityTimeout) if (socket) socket.close() socket = undefined @@ -524,7 +524,7 @@ export default class RequestClient { signal: socketAbortController.signal, onFailedAttempt: () => { if (socketAbortController.signal.aborted) return // don't log in this case - this.uppy.log(`Retrying websocket ${file.id}`, 'info') + this.uppy.log(`Retrying websocket ${file.id}`) }, }) })() @@ -547,14 +547,14 @@ export default class RequestClient { if (targetFile.id !== file.id) return socketSend('cancel') socketAbortController?.abort?.() - this.uppy.log(`upload ${file.id} was removed`, 'info') + this.uppy.log(`upload ${file.id} was removed`) resolve() } const onCancelAll = () => { socketSend('cancel') socketAbortController?.abort?.() - this.uppy.log(`upload ${file.id} was canceled`, 'info') + this.uppy.log(`upload ${file.id} was canceled`) resolve() } diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index 61e2bd3ed1..d20de5aaf1 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -722,8 +722,7 @@ export class Uppy< const updatedFiles = { ...this.getState().files } if (!updatedFiles[fileID]) { this.log( - 'Was trying to set metadata for a file that has been removed: ', - fileID, + `Was trying to set metadata for a file that has been removed: ${fileID}`, ) return } @@ -1958,7 +1957,7 @@ export class Uppy< * Passes messages to a function, provided in `opts.logger`. * If `opts.logger: Uppy.debugLogger` or `opts.debug: true`, logs to the browser console. */ - log(message: string | Record | Error, type?: string): void { + log(message: unknown, type?: 'error' | 'warning'): void { const { logger } = this.opts switch (type) { case 'error': From 7a5697848107595f666096bc73621931e40c3ced Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Tue, 26 Nov 2024 16:56:11 +0800 Subject: [PATCH 14/19] pull logic out from react component --- .../src/GoogleDrivePicker.tsx | 8 +- .../src/GooglePhotosPicker.tsx | 8 +- .../src/GooglePicker/GooglePickerView.tsx | 538 ++++-------------- .../src/GooglePicker/googlePicker.ts | 425 ++++++++++++++ 4 files changed, 547 insertions(+), 432 deletions(-) create mode 100644 packages/@uppy/provider-views/src/GooglePicker/googlePicker.ts diff --git a/packages/@uppy/google-drive-picker/src/GoogleDrivePicker.tsx b/packages/@uppy/google-drive-picker/src/GoogleDrivePicker.tsx index cb8864fab0..e2313710eb 100644 --- a/packages/@uppy/google-drive-picker/src/GoogleDrivePicker.tsx +++ b/packages/@uppy/google-drive-picker/src/GoogleDrivePicker.tsx @@ -7,11 +7,8 @@ import { tokenStorage, } from '@uppy/companion-client' +import type { PickedItem } from '@uppy/provider-views/lib/GooglePicker/googlePicker.js' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' -import { - type PickedItem, - type PluginState, -} from '@uppy/provider-views/lib/GooglePicker/GooglePickerView.js' import type { AsyncStore, BaseProviderPlugin } from '@uppy/core/lib/Uppy.js' // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -66,7 +63,7 @@ export default class GoogleDrivePicker< M extends Meta & { width: number; height: number }, B extends Body, > - extends UIPlugin + extends UIPlugin implements BaseProviderPlugin { static VERSION = packageJson.version @@ -144,7 +141,6 @@ export default class GoogleDrivePicker< - extends UIPlugin + extends UIPlugin implements BaseProviderPlugin { static VERSION = packageJson.version @@ -137,7 +134,6 @@ export default class GooglePhotosPicker< ({ - authorization: `Bearer ${token}`, -}) - -const injectedScripts = new Set() - -// https://stackoverflow.com/a/39008859/6519037 -async function injectScript(src: string) { - if (injectedScripts.has(src)) return - - await new Promise((resolve, reject) => { - const script = document.createElement('script') - script.src = src - script.addEventListener('load', () => resolve()) - script.addEventListener('error', (e) => reject(e.error)) - document.head.appendChild(script) - }) - injectedScripts.add(src) -} +import type { Uppy } from '@uppy/core' +import useStore from '@uppy/core/lib/useStore.js' +import type { AsyncStore } from '@uppy/core/lib/Uppy.js' -async function isTokenValid(accessToken: string): Promise { - try { - const response = await fetch( - `https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=${encodeURIComponent(accessToken)}`, - ) - if (response.ok) { - await response.json() - return true - } - console.warn( - 'Token is invalid or expired:', - response.status, - await response.text(), - ) - // Token is invalid or expired - return false - } catch (error) { - console.error('Error checking token validity:', error) - return false - } -} +import { + authorize, + ensureScriptsInjected, + InvalidTokenError, + logout, + pollPickingSession, + showDrivePicker, + showPhotosPicker, + type PickedItem, + type PickingSession, +} from './googlePicker.js' export type GooglePickerViewProps = { - plugin: UIPlugin uppy: Uppy clientId: string onFilesPicked: (files: PickedItem[], accessToken: string) => void @@ -156,342 +40,156 @@ export default function GooglePickerView({ uppy, clientId, onFilesPicked, - plugin, pickerType, apiKey, appId, storage, }: GooglePickerViewProps) { - const [{ scriptsLoaded }, setPluginState] = useUppyPluginState(plugin) const [loading, setLoading] = useState(false) - const [signedOut, setSignedOut] = useState(false) - const [accessToken, setAccessToken] = useStore( + const [accessToken, setAccessTokenStored] = useStore( storage, `uppy:google-${pickerType}-picker:accessToken`, ) - const onPicked = useCallback( - async (picked: google.picker.ResponseObject) => { - if (picked.action === google.picker.Action.PICKED) { - // console.log('Picker response', JSON.stringify(picked, null, 2)); - if (accessToken == null) throw new Error() - onFilesPicked( - picked['docs'].map((doc) => ({ - platform: 'drive', - id: doc['id'], - name: doc['name'], - mimeType: doc['mimeType'], - })), - accessToken, - ) - } - }, - [accessToken, onFilesPicked], - ) + const pickingSessionRef = useRef() + const accessTokenRef = useRef(accessToken) + const shownPickerRef = useRef(false) - const showDrivePicker = useCallback( - (token: string) => { - if (pickerType !== 'drive') throw new Error() - const picker = new google.picker.PickerBuilder() - .enableFeature(google.picker.Feature.NAV_HIDDEN) - .enableFeature(google.picker.Feature.MULTISELECT_ENABLED) - .setDeveloperKey(apiKey) - .setAppId(appId) - .setOAuthToken(token) - .addView( - new google.picker.DocsView(google.picker.ViewId.DOCS) - .setIncludeFolders(true) - // Note: setEnableDrives doesn't seem to work - // .setEnableDrives(true) - .setSelectFolderEnabled(false), - ) - // NOTE: photos is broken and results in an error being returned from Google - // .addView(google.picker.ViewId.PHOTOS) - .setCallback(onPicked) - .build() - - picker.setVisible(true) + const setAccessToken = useCallback( + (t: string | null) => { + uppy.log('Access token updated') + setAccessTokenStored(t) + accessTokenRef.current = t }, - [apiKey, appId, onPicked, pickerType], + [setAccessTokenStored, uppy], ) - const pollStartTimeRef = useRef() - const [pickingSession, setPickingSession] = useState() - - const showPhotosPicker = useCallback( - async (token: string) => { - // https://developers.google.com/photos/picker/guides/get-started-picker - try { - setLoading(true) - - const headers = getAuthHeader(token) - - let newPickingSession = pickingSession - if (newPickingSession == null) { - const createSessionResponse = await fetch( - 'https://photospicker.googleapis.com/v1/sessions', - { method: 'post', headers }, - ) - - if (createSessionResponse.status === 401) { - const resp = await createSessionResponse.json() - if (resp.error?.status === 'UNAUTHENTICATED') { - setAccessToken(null) - setSignedOut(true) - return - } + // keep access token in sync with the ref + useEffect(() => { + accessTokenRef.current = accessToken + }, [accessToken]) + + const showPicker = useCallback( + async (signal?: AbortSignal) => { + shownPickerRef.current = true + let newAccessToken = accessToken + + const doShowPicker = async (token: string) => { + if (pickerType === 'drive') { + await showDrivePicker({ token, apiKey, appId, onFilesPicked, signal }) + } else { + // photos + const onPickingSessionChange = ( + newPickingSession: PickingSession, + ) => { + pickingSessionRef.current = newPickingSession } - - if (!createSessionResponse.ok) - throw new Error('Failed to create a session') - newPickingSession = - (await createSessionResponse.json()) as PickingSession - pollStartTimeRef.current = Date.now() - setPickingSession(newPickingSession) + await showPhotosPicker({ + token, + pickingSession: pickingSessionRef.current, + onPickingSessionChange, + signal, + }) } - - window.open(newPickingSession.pickerUri) - } finally { - setLoading(false) } - }, - [pickingSession, setAccessToken], - ) - const authorize = useCallback(async () => { - setSignedOut(false) - setLoading(true) - try { - const response = await new Promise( - (resolve, reject) => { - const scopes = - pickerType === 'drive' ? - ['https://www.googleapis.com/auth/drive.readonly'] - : [ - 'https://www.googleapis.com/auth/photospicker.mediaitems.readonly', - ] + setLoading(true) + try { + try { + await ensureScriptsInjected(pickerType) - const tokenClient = google.accounts.oauth2.initTokenClient({ - client_id: clientId, - // Authorization scopes required by the API; multiple scopes can be included, separated by spaces. - scope: scopes.join(' '), - callback: resolve, - error_callback: reject, - }) + if (newAccessToken == null) { + newAccessToken = await authorize({ clientId, pickerType }) + } + if (newAccessToken == null) throw new Error() - if (accessToken === null) { - // Prompt the user to select a Google Account and ask for consent to share their data - // when establishing a new session. - tokenClient.requestAccessToken({ prompt: 'consent' }) + await doShowPicker(newAccessToken) + setAccessToken(newAccessToken) + } catch (err) { + if (err instanceof InvalidTokenError) { + uppy.log('Token is invalid or expired, reauthenticating') + newAccessToken = await authorize({ + pickerType, + accessToken: newAccessToken, + clientId, + }) + // now try again: + await doShowPicker(newAccessToken) + setAccessToken(newAccessToken) } else { - // Skip display of account chooser and consent dialog for an existing session. - tokenClient.requestAccessToken({ prompt: '' }) + throw err } - }, - ) - if (response.error) { - throw new Error(`OAuth2 error: ${response.error}`) - } - const { access_token: newAccessToken } = response - setAccessToken(newAccessToken) - - // showDrivePicker(newAccessToken); - } catch (err) { - uppy.log(err) - } finally { - setLoading(false) - } - }, [accessToken, clientId, pickerType, setAccessToken, uppy]) - - useEffect(() => { - ;(async () => { - try { - await Promise.all([ - injectScript('https://accounts.google.com/gsi/client'), // Google Identity Services - (async () => { - await injectScript('https://apis.google.com/js/api.js') - - if (pickerType === 'drive') { - await new Promise((resolve) => - gapi.load('client:picker', () => resolve()), - ) - await gapi.client.load( - 'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest', - ) - } - - setPluginState({ scriptsLoaded: true }) - })(), - ]) + } } catch (err) { - uppy.log(err) + if ( + err instanceof Error && + 'type' in err && + err.type === 'popup_closed' + ) { + // user closed the auth popup, ignore + } else { + setAccessToken(null) + uppy.log(err) + } + } finally { + setLoading(false) } - })() - }, [pickerType, setPluginState, uppy]) - - const showPicker = useCallback(async () => { - if (accessToken === undefined) return // not yet loaded - if (accessToken === null) { - authorize() - return - } - // google drive picker will crash hard if given an invalid token, so we need to check it first - // https://github.com/transloadit/uppy/pull/5443#pullrequestreview-2452439265 - if (!(await isTokenValid(accessToken))) { - authorize() - return - } - if (pickerType === 'drive') { - showDrivePicker(accessToken) - } else { - showPhotosPicker(accessToken) - } - }, [accessToken, authorize, pickerType, showDrivePicker, showPhotosPicker]) - - // eslint-disable-next-line no-shadow - const handlePhotosPicked = useCallback( - async (params: { accessToken: string; pickingSession: PickingSession }) => { - const headers = getAuthHeader(params.accessToken) - - let pageToken: string | undefined - let mediaItems: MediaItem[] = [] - do { - const pageSize = 100 - const response = await fetch( - `https://photospicker.googleapis.com/v1/mediaItems?${new URLSearchParams({ sessionId: params.pickingSession.id, pageSize: String(pageSize) }).toString()}`, - { headers }, - ) - if (!response.ok) throw new Error('Failed to get a media items') - const { - mediaItems: batchMediaItems, - nextPageToken, - }: { mediaItems: MediaItem[]; nextPageToken?: string } = - await response.json() - pageToken = nextPageToken - mediaItems.push(...batchMediaItems) - } while (pageToken) - - // todo show alert instead about invalid picked files? - mediaItems = mediaItems.flatMap((i) => - ( - i.type === 'PHOTO' || - (i.type === 'VIDEO' && - i.mediaFile.mediaFileMetadata.videoMetadata.processingStatus === - 'READY') - ) ? - [i] - : [], - ) - - onFilesPicked( - mediaItems.map( - ({ - id, - // we want the original resolution, so we don't append any parameter to the baseUrl - // https://developers.google.com/photos/library/guides/access-media-items#base-urls - mediaFile: { mimeType, filename, baseUrl }, - }) => ({ - platform: 'photos', - id, - mimeType, - url: baseUrl, - name: filename, - }), - ), - params.accessToken, - ) }, - [onFilesPicked], + [ + accessToken, + apiKey, + appId, + clientId, + onFilesPicked, + pickerType, + setAccessToken, + uppy, + ], ) useEffect(() => { - // if we have a session, poll it until it either times out, or the user selects some photos - // note that the user can also just close the page, but we get no indication of that from Google when polling, - // so we just have to continue polling, in case the user opens the photo selector again - if (pickingSession == null || accessToken == null) return undefined - const abortController = new AbortController() - const headers = getAuthHeader(accessToken) + pollPickingSession({ + pickingSessionRef, + accessTokenRef, + signal: abortController.signal, + onFilesPicked, + onError: (err) => uppy.log(err), + }) - ;(async () => { - // poll session for user response - for (;;) { - try { - const interval = parseFloat(pickingSession.pollingConfig.pollInterval) + return () => abortController.abort() + }, [onFilesPicked, uppy]) - await Promise.race([ - new Promise((resolve) => setTimeout(resolve, interval * 1000)), - new Promise((_resolve, reject) => { - abortController.signal.onabort = reject - }), - ]) + useEffect(() => { + // when mounting, once we have a token, be nice to the user and automatically show the picker + // accessToken === undefined means not yet loaded from storage, so wait for that first + if (accessToken === undefined || shownPickerRef.current) { + return undefined + } - abortController.signal.throwIfAborted() + const abortController = new AbortController() - // https://developers.google.com/photos/picker/reference/rest/v1/sessions - const response = await fetch( - `https://photospicker.googleapis.com/v1/sessions/${encodeURIComponent(pickingSession.id)}`, - { headers }, - ) - if (!response.ok) throw new Error('Failed to get session') - const json: PickingSession = await response.json() - if (json.mediaItemsSet) { - // console.log('User picked!', json) - pollStartTimeRef.current = undefined - setPickingSession(undefined) - handlePhotosPicked({ accessToken, pickingSession }) - return - } - if (pickingSession.pollingConfig.timeoutIn === '0s') { - uppy.log('Picking session timeout') - pollStartTimeRef.current = undefined - setPickingSession(undefined) - return - } - } catch (err) { - if (err instanceof Error && err.name === 'AbortError') { - return - } - uppy.log(err) - } - } - })() + showPicker(abortController.signal) return () => abortController.abort() - }, [accessToken, handlePhotosPicked, pickingSession, uppy]) - - useEffect(() => { - if (!scriptsLoaded || signedOut) { - return - } - showPicker() - }, [scriptsLoaded, showPicker, signedOut]) + }, [accessToken, showPicker]) - const handleSignoutClick = useCallback(async () => { - if (accessToken == null) return + const handleLogoutClick = useCallback(async () => { if (accessToken) { - await new Promise((resolve) => - google.accounts.oauth2.revoke(accessToken, resolve), - ) + await logout(accessToken) setAccessToken(null) - setPickingSession(undefined) - setSignedOut(true) // if user signs out, don't re-authenticate automatically + pickingSessionRef.current = undefined } }, [accessToken, setAccessToken]) - if (!scriptsLoaded) { - return null - } - - // for photos, we will never go out of the loading/polling state if (loading) { return
{uppy.i18n('pleaseWait')}...
} if (accessToken == null) { return ( - ) @@ -499,12 +197,12 @@ export default function GooglePickerView({ return ( <> - - diff --git a/packages/@uppy/provider-views/src/GooglePicker/googlePicker.ts b/packages/@uppy/provider-views/src/GooglePicker/googlePicker.ts new file mode 100644 index 0000000000..53fcaf9069 --- /dev/null +++ b/packages/@uppy/provider-views/src/GooglePicker/googlePicker.ts @@ -0,0 +1,425 @@ +import { type MutableRef } from 'preact/hooks' + +// https://developers.google.com/photos/picker/reference/rest/v1/mediaItems +export interface MediaItemBase { + id: string + createTime: string +} + +interface MediaFileMetadataBase { + width: number + height: number + cameraMake: string + cameraModel: string +} + +interface MediaFileBase { + baseUrl: string + mimeType: string + filename: string +} + +export interface VideoMediaItem extends MediaItemBase { + type: 'VIDEO' + mediaFile: MediaFileBase & { + mediaFileMetadata: MediaFileMetadataBase & { + videoMetadata: { + fps: number + processingStatus: 'UNSPECIFIED' | 'PROCESSING' | 'READY' | 'FAILED' + } + } + } +} + +export interface PhotoMediaItem extends MediaItemBase { + type: 'PHOTO' + mediaFile: MediaFileBase & { + mediaFileMetadata: MediaFileMetadataBase & { + photoMetadata: { + focalLength: number + apertureFNumber: number + isoEquivalent: number + exposureTime: string + } + } + } +} + +export interface UnspecifiedMediaItem extends MediaItemBase { + type: 'TYPE_UNSPECIFIED' + mediaFile: MediaFileBase +} + +export type MediaItem = VideoMediaItem | PhotoMediaItem | UnspecifiedMediaItem + +// https://developers.google.com/photos/picker/reference/rest/v1/sessions +export interface PickingSession { + id: string + pickerUri: string + pollingConfig: { + pollInterval: string + timeoutIn: string + } + expireTime: string + mediaItemsSet: boolean +} + +export interface PickedItemBase { + id: string + mimeType: string + name: string +} + +export interface PickedDriveItem extends PickedItemBase { + platform: 'drive' +} + +export interface PickedPhotosItem extends PickedItemBase { + platform: 'photos' + url: string +} + +export type PickedItem = PickedPhotosItem | PickedDriveItem + +type PickerType = 'drive' | 'photos' + +const getAuthHeader = (token: string) => ({ + authorization: `Bearer ${token}`, +}) + +const injectedScripts = new Set() +let driveApiLoaded = false + +// https://stackoverflow.com/a/39008859/6519037 +async function injectScript(src: string) { + if (injectedScripts.has(src)) return + + await new Promise((resolve, reject) => { + const script = document.createElement('script') + script.src = src + script.addEventListener('load', () => resolve()) + script.addEventListener('error', (e) => reject(e.error)) + document.head.appendChild(script) + }) + injectedScripts.add(src) +} + +export async function ensureScriptsInjected( + pickerType: PickerType, +): Promise { + await Promise.all([ + injectScript('https://accounts.google.com/gsi/client'), // Google Identity Services + (async () => { + await injectScript('https://apis.google.com/js/api.js') + + if (pickerType === 'drive' && !driveApiLoaded) { + await new Promise((resolve) => + gapi.load('client:picker', () => resolve()), + ) + await gapi.client.load( + 'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest', + ) + driveApiLoaded = true + } + })(), + ]) +} + +async function isTokenValid( + accessToken: string, + signal: AbortSignal | undefined, +) { + const response = await fetch( + `https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=${encodeURIComponent(accessToken)}`, + { signal }, + ) + if (response.ok) { + return true + } + // console.warn('Token is invalid or expired:', response.status, await response.text()); + // Token is invalid or expired + return false +} + +export async function authorize({ + pickerType, + clientId, + accessToken, +}: { + pickerType: PickerType + clientId: string + accessToken?: string | null | undefined +}): Promise { + const response = await new Promise( + (resolve, reject) => { + const scopes = + pickerType === 'drive' ? + ['https://www.googleapis.com/auth/drive.readonly'] + : ['https://www.googleapis.com/auth/photospicker.mediaitems.readonly'] + + const tokenClient = google.accounts.oauth2.initTokenClient({ + client_id: clientId, + // Authorization scopes required by the API; multiple scopes can be included, separated by spaces. + scope: scopes.join(' '), + callback: resolve, + error_callback: reject, + }) + + if (accessToken === null) { + // Prompt the user to select a Google Account and ask for consent to share their data + // when establishing a new session. + tokenClient.requestAccessToken({ prompt: 'consent' }) + } else { + // Skip display of account chooser and consent dialog for an existing session. + tokenClient.requestAccessToken({ prompt: '' }) + } + }, + ) + + if (response.error) { + throw new Error(`OAuth2 error: ${response.error}`) + } + return response.access_token +} + +export async function logout(accessToken: string): Promise { + await new Promise((resolve) => + google.accounts.oauth2.revoke(accessToken, resolve), + ) +} + +export class InvalidTokenError extends Error { + constructor() { + super('Invalid or expired token') + this.name = 'InvalidTokenError' + } +} + +export async function showDrivePicker({ + token, + apiKey, + appId, + onFilesPicked, + signal, +}: { + token: string + apiKey: string + appId: string + onFilesPicked: (files: PickedItem[], accessToken: string) => void + signal: AbortSignal | undefined +}): Promise { + // google drive picker will crash hard if given an invalid token, so we need to check it first + // https://github.com/transloadit/uppy/pull/5443#pullrequestreview-2452439265 + if (!(await isTokenValid(token, signal))) { + throw new InvalidTokenError() + } + + const onPicked = (picked: google.picker.ResponseObject) => { + if (picked.action === google.picker.Action.PICKED) { + // console.log('Picker response', JSON.stringify(picked, null, 2)); + onFilesPicked( + picked['docs'].map((doc) => ({ + platform: 'drive', + id: doc['id'], + name: doc['name'], + mimeType: doc['mimeType'], + })), + token, + ) + } + } + + const picker = new google.picker.PickerBuilder() + .enableFeature(google.picker.Feature.NAV_HIDDEN) + .enableFeature(google.picker.Feature.MULTISELECT_ENABLED) + .setDeveloperKey(apiKey) + .setAppId(appId) + .setOAuthToken(token) + .addView( + new google.picker.DocsView(google.picker.ViewId.DOCS) + .setIncludeFolders(true) + // Note: setEnableDrives doesn't seem to work + // .setEnableDrives(true) + .setSelectFolderEnabled(false), + ) + // NOTE: photos is broken and results in an error being returned from Google + // I think it's the old Picasa photos + // .addView(google.picker.ViewId.PHOTOS) + .setCallback(onPicked) + .build() + + picker.setVisible(true) + signal?.addEventListener('abort', () => picker.dispose()) +} + +export async function showPhotosPicker({ + token, + pickingSession, + onPickingSessionChange, + signal, +}: { + token: string + pickingSession: PickingSession | undefined + onPickingSessionChange: (ps: PickingSession) => void + signal: AbortSignal | undefined +}): Promise { + // https://developers.google.com/photos/picker/guides/get-started-picker + const headers = getAuthHeader(token) + + let newPickingSession = pickingSession + if (newPickingSession == null) { + const createSessionResponse = await fetch( + 'https://photospicker.googleapis.com/v1/sessions', + { method: 'post', headers, signal }, + ) + + if (createSessionResponse.status === 401) { + const resp = await createSessionResponse.json() + if (resp.error?.status === 'UNAUTHENTICATED') { + throw new InvalidTokenError() + } + } + + if (!createSessionResponse.ok) { + throw new Error('Failed to create a session') + } + newPickingSession = (await createSessionResponse.json()) as PickingSession + + onPickingSessionChange(newPickingSession) + } + + const w = window.open(newPickingSession.pickerUri) + signal?.addEventListener('abort', () => w?.close()) +} + +async function resolvePickedPhotos({ + accessToken, + pickingSession, + signal, +}: { + accessToken: string + pickingSession: PickingSession + signal: AbortSignal +}) { + const headers = getAuthHeader(accessToken) + + let pageToken: string | undefined + let mediaItems: MediaItem[] = [] + do { + const pageSize = 100 + const response = await fetch( + `https://photospicker.googleapis.com/v1/mediaItems?${new URLSearchParams({ sessionId: pickingSession.id, pageSize: String(pageSize) }).toString()}`, + { headers, signal }, + ) + if (!response.ok) throw new Error('Failed to get a media items') + const { + mediaItems: batchMediaItems, + nextPageToken, + }: { mediaItems: MediaItem[]; nextPageToken?: string } = + await response.json() + pageToken = nextPageToken + mediaItems.push(...batchMediaItems) + } while (pageToken) + + // todo show alert instead about invalid picked files? + mediaItems = mediaItems.flatMap((i) => + ( + i.type === 'PHOTO' || + (i.type === 'VIDEO' && + i.mediaFile.mediaFileMetadata.videoMetadata.processingStatus === + 'READY') + ) ? + [i] + : [], + ) + + return mediaItems.map( + ({ + id, + // we want the original resolution, so we don't append any parameter to the baseUrl + // https://developers.google.com/photos/library/guides/access-media-items#base-urls + mediaFile: { mimeType, filename, baseUrl }, + }) => ({ + platform: 'photos' as const, + id, + mimeType, + url: baseUrl, + name: filename, + }), + ) +} + +export async function pollPickingSession({ + pickingSessionRef, + accessTokenRef, + signal, + onFilesPicked, + onError, +}: { + pickingSessionRef: MutableRef + accessTokenRef: MutableRef + signal: AbortSignal + onFilesPicked: (files: PickedItem[], accessToken: string) => void + onError: (err: unknown) => void +}): Promise { + // if we have an active session, poll it until it either times out, or the user selects some photos. + // Note that the user can also just close the page, but we get no indication of that from Google when polling, + // so we just have to continue polling in the background, so we can react to it + // in case the user opens the photo selector again. Hence the infinite for loop + for (let interval = 1; ; ) { + try { + if (pickingSessionRef.current != null) { + interval = parseFloat( + pickingSessionRef.current.pollingConfig.pollInterval, + ) + } else { + interval = 1 + } + + await Promise.race([ + new Promise((resolve) => setTimeout(resolve, interval * 1000)), + new Promise((_resolve, reject) => { + signal.addEventListener('abort', reject) + }), + ]) + + signal.throwIfAborted() + + const accessToken = accessTokenRef.current + const pickingSession = pickingSessionRef.current + + if (pickingSession != null && accessToken != null) { + const headers = getAuthHeader(accessToken) + + // https://developers.google.com/photos/picker/reference/rest/v1/sessions + const response = await fetch( + `https://photospicker.googleapis.com/v1/sessions/${encodeURIComponent(pickingSession.id)}`, + { headers, signal }, + ) + if (!response.ok) throw new Error('Failed to get session') + const json: PickingSession = await response.json() + if (json.mediaItemsSet) { + // console.log('User picked!', json) + const resolvedPhotos = await resolvePickedPhotos({ + accessToken, + pickingSession, + signal, + }) + // eslint-disable-next-line no-param-reassign + pickingSessionRef.current = undefined + onFilesPicked(resolvedPhotos, accessToken) + } + if (pickingSession.pollingConfig.timeoutIn === '0s') { + // eslint-disable-next-line no-param-reassign + pickingSessionRef.current = undefined + } + } + } catch (err) { + if (err instanceof Error && err.name === 'AbortError') { + return + } + // just report the error and continue polling + onError(err) + } + } +} From 8550910c2dc50cf6446840440cf7a3f442f93dc6 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Tue, 26 Nov 2024 17:56:34 +0800 Subject: [PATCH 15/19] remove docs --- docs/companion.md | 999 ------------------ docs/sources/companion-plugins/box.mdx | 177 ---- .../companion-plugins/companion-options.mdx | 69 -- docs/sources/companion-plugins/dropbox.mdx | 177 ---- docs/sources/companion-plugins/facebook.mdx | 174 --- .../companion-plugins/google-drive-picker.mdx | 142 --- .../companion-plugins/google-drive.mdx | 178 ---- .../google-photos-picker.mdx | 127 --- .../companion-plugins/google-photos.mdx | 175 --- docs/sources/companion-plugins/instagram.mdx | 187 ---- docs/sources/companion-plugins/onedrive.mdx | 175 --- docs/sources/companion-plugins/unsplash.mdx | 153 --- docs/sources/companion-plugins/url.mdx | 139 --- docs/sources/companion-plugins/zoom.mdx | 178 ---- docs/user-interfaces/dashboard.mdx | 759 ------------- 15 files changed, 3809 deletions(-) delete mode 100644 docs/companion.md delete mode 100644 docs/sources/companion-plugins/box.mdx delete mode 100644 docs/sources/companion-plugins/companion-options.mdx delete mode 100644 docs/sources/companion-plugins/dropbox.mdx delete mode 100644 docs/sources/companion-plugins/facebook.mdx delete mode 100644 docs/sources/companion-plugins/google-drive-picker.mdx delete mode 100644 docs/sources/companion-plugins/google-drive.mdx delete mode 100644 docs/sources/companion-plugins/google-photos-picker.mdx delete mode 100644 docs/sources/companion-plugins/google-photos.mdx delete mode 100644 docs/sources/companion-plugins/instagram.mdx delete mode 100644 docs/sources/companion-plugins/onedrive.mdx delete mode 100644 docs/sources/companion-plugins/unsplash.mdx delete mode 100644 docs/sources/companion-plugins/url.mdx delete mode 100644 docs/sources/companion-plugins/zoom.mdx delete mode 100644 docs/user-interfaces/dashboard.mdx diff --git a/docs/companion.md b/docs/companion.md deleted file mode 100644 index da36f41877..0000000000 --- a/docs/companion.md +++ /dev/null @@ -1,999 +0,0 @@ ---- -sidebar_position: 4 ---- - -# Companion - -Companion is an open source server application which **takes away the complexity -of authentication and the cost of downloading files from remote sources**, such -as Instagram, Google Drive, and others. Companion is a server-to-server -orchestrator that streams files from a source to a destination, and files are -never stored in Companion. Companion can run either as a standalone -(self-hosted) application, [Transloadit-hosted](#hosted), or plugged in as an -Express middleware into an existing application. The Uppy client requests remote -files from Companion, which it will download and simultaneously upload to your -[Tus server](/docs/tus), [AWS bucket](/docs/aws-s3), or any server that supports -[PUT, POST or Multipart uploads](/docs/xhr-upload). - -This means a user uploading a 5GB video from Google Drive from their phone isn’t -eating into their data plans and you don’t have to worry about implementing -OAuth. - -## When should I use it? - -If you want to let users download files from [Box][], [Dropbox][], [Facebook][], -[Google Drive][googledrive], [Google Photos][googlephotos], [Google Drive -Picker][googledrivepicker], [Google Photos Picker][googlephotospicker], -[Instagram][], [OneDrive][], [Unsplash][], [Import from URL][url], or [Zoom][] — -you need Companion. - -Companion supports the same [uploaders](/docs/guides/choosing-uploader) as Uppy: -[Tus](/docs/tus), [AWS S3](/docs/aws-s3), and [regular multipart](/docs/tus). -But instead of manually setting a plugin, Uppy sends along a header with the -uploader and Companion will use the same on the server. This means if you are -using [Tus](/docs/tus) for your local uploads, you can send your remote uploads -to the same Tus server (and likewise for your AWS S3 bucket). - -:::note - -Companion only deals with _remote_ files, _local_ files are still uploaded from -the client with your upload plugin. - -::: - -## Hosted - -Using [Transloadit][] services comes with a hosted version of Companion so you -don’t have to worry about hosting your own server. Whether you are on a free or -paid Transloadit [plan](https://transloadit.com/pricing/), you can use -Companion. It’s not possible to rent a Companion server without a Transloadit -plan. - -[**Sign-up for a (free) plan**](https://transloadit.com/pricing/). - -:::tip - -Choosing Transloadit for your file services also comes with credentials for all -remote providers. This means you don’t have to waste time going through the -approval process of every app. You can still add your own credentials in the -Transloadit admin page if you want. - -::: - -:::info - -Downloading and uploading files through Companion doesn’t count towards your -[monthly quota](https://transloadit.com/docs/faq/1gb-worth/), it’s a way for -files to arrive at Transloadit servers, much like Uppy. - -::: - -To do so each provider plugin must be configured with Transloadit’s Companion -URLs: - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import Dropbox from '@uppy/dropbox'; - -uppy.use(Dropbox, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, -}); -``` - -You may also hit rate limits, because the OAuth application is shared between -everyone using Transloadit. - -To solve that, you can use your own OAuth keys with Transloadit’s hosted -Companion servers by using Transloadit Template Credentials. [Create a Template -Credential][template-credentials] on the Transloadit site. Select “Companion -OAuth” for the service, and enter the key and secret for the provider you want -to use. Then you can pass the name of the new credentials to that provider: - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import Dropbox from '@uppy/dropbox'; - -uppy.use(Dropbox, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, - companionKeysParams: { - key: 'YOUR_TRANSLOADIT_API_KEY', - credentialsName: 'my_companion_dropbox_creds', - }, -}); -``` - -## Installation & use - -Companion is installed from npm. Depending on how you want to run Companion, the -install process is slightly different. Companion can be integrated as middleware -into your [Express](https://expressjs.com/) app or as a standalone server. Most -people probably want to run it as a standalone server, while the middleware -could be used to further customise Companion or integrate it into your own HTTP -server code. - -:::note - -Since v2, you need to be running `node.js >= v10.20.1` to use Companion. More -information in the -[migrating to 2.0](/docs/guides/migration-guides/#migrate-from-uppy-1x-to-2x) -guide. - -Windows is not a supported platform right now. It may work, and we’re happy to -accept improvements in this area, but we can’t provide support. - -::: - -### Standalone mode - -You can use the standalone version if you want to run Companion as it’s own -Node.js process. It’s a configured Express server with sessions, logging, and -security best practices. First you’ll typically want to install it globally: - -```bash -npm install -g @uppy/companion -``` - -Standalone Companion will always serve HTTP (not HTTPS) and expects a reverse -proxy with SSL termination in front of it when running in production. See -[`COMPANION_PROTOCOL`](#server) for more information. - -Companion ships with an executable file (`bin/companion`) which is the -standalone server. Unlike the middleware version, options are set via -environment variables. - -:::info - -Checkout [options](#options) for the available options in JS and environment -variable formats. - -::: - -You need at least these three to get started: - -```bash -export COMPANION_SECRET="shh!Issa Secret!" -export COMPANION_DOMAIN="YOUR SERVER DOMAIN" -export COMPANION_DATADIR="PATH/TO/DOWNLOAD/DIRECTORY" -``` - -Then run: - -```bash -companion -``` - -You can also pass in the path to your JSON config file, like so: - -```bash -companion --config /path/to/companion.json -``` - -You may also want to run Companion in a process manager like -[PM2](https://pm2.keymetrics.io/) to make sure it gets restarted on upon -crashing as well as allowing scaling to many instances. - -### Express middleware mode - -First install it into your Node.js project with your favorite package manager: - -```bash -npm install @uppy/companion -``` - -To plug Companion into an existing server, call its `.app` method, passing in an -[options](#options) object as a parameter. This returns a server instance that -you can mount on a route in your Express app. Note: do **not** use the `cors` -module in your project, because Companion already includes it. Use the -`corsOrigins` Companion option to customise CORS behavior. - -```js -import express from 'express'; -import bodyParser from 'body-parser'; -import session from 'express-session'; -import companion from '@uppy/companion'; - -const app = express(); - -// Companion requires body-parser and express-session middleware. -// You can add it like this if you use those throughout your app. -// -// If you are using something else in your app, you can add these -// middlewares in the same subpath as Companion instead. -app.use(bodyParser.json()); -app.use(session({ secret: 'some secrety secret' })); - -const companionOptions = { - providerOptions: { - drive: { - key: 'GOOGLE_DRIVE_KEY', - secret: 'GOOGLE_DRIVE_SECRET', - }, - }, - server: { - host: 'localhost:3020', - protocol: 'http', - // Default installations normally don't need a path. - // However if you specify a `path`, you MUST specify - // the same path in `app.use()` below, - // e.g. app.use('/companion', companionApp) - // path: '/companion', - }, - filePath: '/path/to/folder/', -}; - -const { app: companionApp } = companion.app(companionOptions); -app.use(companionApp); -``` - -Companion uses WebSockets to communicate progress, errors, and successes to the -client. This is what Uppy listens to to update it’s internal state and UI. - -Add the Companion WebSocket server using the `companion.socket` function: - -```js -const server = app.listen(PORT); - -companion.socket(server); -``` - -If WebSockets fail for some reason Uppy and Companion will fallback to HTTP -polling. - -### Running many instances - -We recommend running at least two instances in production, so that if the -Node.js event loop gets blocked by one or more requests (due to a bug or spike -in traffic), it doesn’t also block or slow down all other requests as well (as -Node.js is single threaded). - -As an example for scale, one enterprise customer of Transloadit, who self-hosts -Companion to power an education service that is used by many universities -globally, deploys 7 Companion instances. Their earlier solution ran on 35 -instances. In our general experience Companion will saturate network interface -cards before other resources on commodity virtual servers (`c5d.2xlarge` for -instance). - -Your mileage may vary, so we recommend to add observability. You can let -Prometheus crawl the `/metrics` endpoint and graph that with Grafana for -instance. - -#### Using unique endpoints - -One option is to run many instances with each instance having its own unique -endpoint. This could be on separate ports, (sub)domain names, or IPs. With this -setup, you can either: - -1. Implement your own logic that will direct each upload to a specific Companion - endpoint by setting the `companionUrl` option -2. Setting the Companion option `COMPANION_SELF_ENDPOINT`. This option will - cause Companion to respond with a `i-am` HTTP header containing the value - from `COMPANION_SELF_ENDPOINT`. When Uppy’s sees this header, it will pin all - requests for the upload to this endpoint. - -In either case, you would then also typically configure a single Companion -instance (one endpoint) to handle all OAuth authentication requests, so that you -only need to specify a single OAuth callback URL. See also `oauthDomain` and -`validHosts`. - -#### Using a load balancer - -The other option is to set up a load balancer in front of many Companion -instances. Then Uppy will only see a single endpoint and send all requests to -the associated load balancer, which will then distribute them between Companion -instances. The companion instances coordinate their messages and events over -Redis so that any instance can serve the client’s requests. Note that sticky -sessions are **not** needed with this setup. Here are the requirements for this -setup: - -- The instances need to be connected to the same Redis server. -- You need to set `COMPANION_SECRET` to the same value on both servers. -- if you use the `companionKeysParams` feature (Transloadit), you also need - `COMPANION_PREAUTH_SECRET` to be the same on each instance. -- All other configuration needs to be the same, except if you’re running many - instances on the same machine, then `COMPANION_PORT` should be different for - each instance. - -## API - -### Options - -:::tip - -The headings display the JS and environment variable options (`option` -`ENV_OPTION`). When integrating Companion into your own server, you pass the -options to `companion.app()`. If you are using the standalone version, you -configure Companion using environment variables. Some options only exist as -environment variables or only as a JS option. - -::: - -
- Default configuration - -```javascript -const options = { - server: { - protocol: 'http', - path: '', - }, - providerOptions: {}, - s3: { - endpoint: 'https://{service}.{region}.amazonaws.com', - conditions: [], - useAccelerateEndpoint: false, - getKey: ({ filename }) => `${crypto.randomUUID()}-${filename}`, - expires: 800, // seconds - }, - allowLocalUrls: false, - logClientVersion: true, - periodicPingUrls: [], - streamingUpload: true, - clientSocketConnectTimeout: 60000, - metrics: true, -}; -``` - -
- -#### `filePath` `COMPANION_DATADIR` - -Full path to the directory to which provider files will be downloaded -temporarily. - -#### `secret` `COMPANION_SECRET` `COMPANION_SECRET_FILE` - -A secret string which Companion uses to generate authorization tokens. You -should generate a long random string for this. For example: - -```js -const crypto = require('node:crypto'); - -const secret = crypto.randomBytes(64).toString('hex'); -``` - -:::caution - -Omitting the `secret` in the standalone version will generate a secret for you, -using the above `crypto` string. But when integrating with Express you must -provide it yourself. This is an essential security measure. - -::: - -:::note - -Using a secret file means passing an absolute path to a file with any extension, -which has only the secret, nothing else. - -::: - -#### `preAuthSecret` `COMPANION_PREAUTH_SECRET` `COMPANION_PREAUTH_SECRET_FILE` - -If you are using the [Transloadit](/docs/transloadit) `companionKeysParams` -feature (Transloadit-hosted Companion using your own custom OAuth credentials), -set this variable to a strong randomly generated secret. See also -`COMPANION_SECRET` (but do not use the same secret!) - -:::note - -Using a secret file means passing an absolute path to a file with any extension, -which has only the secret, nothing else. - -::: - -#### `uploadUrls` `COMPANION_UPLOAD_URLS` - -An allowlist (array) of strings (exact URLs) or regular expressions. Companion -will only accept uploads to these URLs. This ensures that your Companion -instance is only allowed to upload to your trusted servers and prevents -[SSRF](https://en.wikipedia.org/wiki/Server-side_request_forgery) attacks. - -#### `COMPANION_PORT` - -The port on which to start the standalone server, defaults to 3020. This is a -standalone-only option. - -#### `COMPANION_COOKIE_DOMAIN` - -Allows you to customize the domain of the cookies created for Express sessions. -This is a standalone-only option. - -#### `COMPANION_HIDE_WELCOME` - -Setting this to `true` disables the welcome message shown at `/`. This is a -standalone-only option. - -#### `redisUrl` `COMPANION_REDIS_URL` - -URL to running Redis server. This can be used to scale Companion horizontally -using many instances. See [How to scale Companion](#how-to-scale-companion). - -#### `COMPANION_REDIS_EXPRESS_SESSION_PREFIX` - -Set a custom prefix for redis keys created by -[connect-redis](https://github.com/tj/connect-redis). Defaults to -`companion-session:`. Sessions are used for storing authentication state and for -allowing thumbnails to be loaded by the browser via Companion and for OAuth2. -See also `COMPANION_REDIS_PUBSUB_SCOPE`. - -#### `redisOptions` `COMPANION_REDIS_OPTIONS` - -An object of -[options supported by the `ioredis` client](https://github.com/redis/ioredis). -See also -[`RedisOptions`](https://github.com/redis/ioredis/blob/af832752040e616daf51621681bcb40cab965a9b/lib/redis/RedisOptions.ts#L8). - -#### `redisPubSubScope` `COMPANION_REDIS_PUBSUB_SCOPE` - -Use a scope for the companion events at the Redis server. Setting this option -will prefix all events with the name provided and a colon. See also -`COMPANION_REDIS_EXPRESS_SESSION_PREFIX`. - -#### `server` - -Configuration options for the underlying server. - -| Key / Environment variable | Value | Description | -| ---------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `protocol` `COMPANION_PROTOCOL` | `http` or `https` | Used to build a URL to reference the Companion instance itself, which is used for headers and cookies. Companion itself always runs as a HTTP server, so locally you should use `http`. You must to set this to `https` once you enabled SSL/HTTPS for your domain in production by running a reverse https-proxy in front of Companion, or with a built-in HTTPS feature of your hosting service. | -| `host` `COMPANION_DOMAIN` | `String` | Your server’s publicly facing hostname (for example `example.com`). | -| `oauthDomain` `COMPANION_OAUTH_DOMAIN` | `String` | If you have several instances of Companion with different (and perhaps dynamic) subdomains, you can set a single fixed subdomain and server (such as `sub1.example.com`) to handle your OAuth authentication for you. This would then redirect back to the correct instance with the required credentials on completion. This way you only need to configure a single callback URL for OAuth providers. | -| `path` `COMPANION_PATH` | `String` | The server path to where the Companion app is sitting. For instance, if Companion is at `example.com/companion`, then the path would be `/companion`). | -| `implicitPath` `COMPANION_IMPLICIT_PATH` | `String` | If the URL’s path in your reverse proxy is different from your Companion path in your express app, then you need to set this path as `implicitPath`. For instance, if your Companion URL is `example.com/mypath/companion`. Where the path `/mypath` is defined in your NGINX server, while `/companion` is set in your express app. Then you need to set the option `implicitPath` to `/mypath`, and set the `path` option to `/companion`. | -| `validHosts` `COMPANION_DOMAINS` | `Array` | If you are setting an `oauthDomain`, you need to set a list of valid hosts, so the oauth handler can validate the host of the Uppy instance requesting the authentication. This is essentially a list of valid domains running your Companion instances. The list may also contain regex patterns. e.g `['sub2.example.com', 'sub3.example.com', '(\\w+).example.com']` | - -#### `sendSelfEndpoint` `COMPANION_SELF_ENDPOINT` - -This is essentially the same as the `server.host + server.path` attributes. The -major reason for this attribute is that, when set, it adds the value as the -`i-am` header of every request response. - -#### `providerOptions` - -Object to enable providers with their keys and secrets. For example: - -```json -{ - "drive": { - "key": "***", - "secret": "***" - } -} -``` - -When using the standalone version you use the corresponding environment -variables or point to a secret file (such as `COMPANION_GOOGLE_SECRET_FILE`). - -:::note - -Secret files need an absolute path to a file with any extension which only has -the secret, nothing else. - -::: - -| Service | Key | Environment variables | -| ------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Box | `box` | `COMPANION_BOX_KEY`, `COMPANION_BOX_SECRET`, `COMPANION_BOX_SECRET_FILE` | -| Dropbox | `dropbox` | `COMPANION_DROPBOX_KEY`, `COMPANION_DROPBOX_SECRET`, `COMPANION_DROPBOX_SECRET_FILE` | -| Facebook | `facebook` | `COMPANION_FACEBOOK_KEY`, `COMPANION_FACEBOOK_SECRET`, `COMPANION_FACEBOOK_SECRET_FILE` | -| Google Drive | `drive` | `COMPANION_GOOGLE_KEY`, `COMPANION_GOOGLE_SECRET`, `COMPANION_GOOGLE_SECRET_FILE` | -| Google Photos | `googlephotos` | `COMPANION_GOOGLE_KEY`, `COMPANION_GOOGLE_SECRET`, `COMPANION_GOOGLE_SECRET_FILE` | -| Instagram | `instagram` | `COMPANION_INSTAGRAM_KEY`, `COMPANION_INSTAGRAM_SECRET`, `COMPANION_INSTAGRAM_SECRET_FILE` | -| OneDrive | `onedrive` | `COMPANION_ONEDRIVE_KEY`, `COMPANION_ONEDRIVE_SECRET`, `COMPANION_ONEDRIVE_SECRET_FILE`, `COMPANION_ONEDRIVE_DOMAIN_VALIDATION` (Settings this variable to `true` enables a route that can be used to validate your app with OneDrive) | -| Zoom | `zoom` | `COMPANION_ZOOM_KEY`, `COMPANION_ZOOM_SECRET`, `COMPANION_ZOOM_SECRET_FILE`, `COMPANION_ZOOM_VERIFICATION_TOKEN` | - -#### `s3` - -Companion comes with signature endpoints for AWS S3. These can be used by the -Uppy client to sign requests to upload files directly to S3, without exposing -secret S3 keys in the browser. Companion also supports uploading files from -providers like Dropbox and Instagram directly into S3. - -##### `s3.key` `COMPANION_AWS_KEY` - -The S3 access key ID. - -##### `s3.secret` `COMPANION_AWS_SECRET` `COMPANION_AWS_SECRET_FILE` - -The S3 secret access key. - -:::note - -Using a secret file means passing an absolute path to a file with any extension, -which has only the secret, nothing else. - -::: - -##### `s3.endpoint` `COMPANION_AWS_ENDPOINT` - -Optional URL to a custom S3 (compatible) service. Otherwise uses the default -from the AWS SDK. - -##### `s3.bucket` `COMPANION_AWS_BUCKET` - -The name of the bucket to store uploaded files in. - -A `string` or function that returns the name of the bucket as a `string` and -takes one argument which is an object with the following properties: - -- `filename`, the original name of the uploaded file; -- `metadata` provided by the user for the file (will only be provided during the - initial calls for each uploaded files, otherwise it will be `undefined`). -- `req`, Express.js `Request` object. Do not use any Companion internals from - the req object, as these might change in any minor version of Companion. - -#### `s3.forcePathStyle` `COMPANION_AWS_FORCE_PATH_STYLE` - -This adds support for setting the S3 client’s `forcePathStyle` option. That is -necessary to use Uppy/Companion alongside localstack in development -environments. **Default**: `false`. - -##### `s3.region` `COMPANION_AWS_REGION` - -The datacenter region where the target bucket is located. - -##### `COMPANION_AWS_PREFIX` - -An optional prefix for all uploaded keys. This is a standalone-only option. The -same can be achieved by the `getKey` option when using the express middleware. - -##### `s3.awsClientOptions` - -You can supply any -[S3 option supported by the AWS SDK](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#constructor-property) -in the `providerOptions.s3.awsClientOptions` object, _except for_ the below: - -- `accessKeyId`. Instead, use the `providerOptions.s3.key` property. This is to - make configuration names consistent between different Companion features. -- `secretAccessKey`. Instead, use the `providerOptions.s3.secret` property. This - is to make configuration names consistent between different Companion - features. - -Be aware that some options may cause wrong behaviour if they conflict with -Companion’s assumptions. If you find that a particular option does not work as -expected, please -[open an issue on the Uppy repository](https://github.com/transloadit/uppy/issues/new) -so we can document it here. - -##### `s3.getKey({ filename, metadata, req })` - -Get the key name for a file. The key is the file path to which the file will be -uploaded in your bucket. This option should be a function receiving three -arguments: - -- `filename`, the original name of the uploaded file; -- `metadata`, user-provided metadata for the file. -- `req`, Express.js `Request` object. Do not use any Companion internals from - the req object, as these might change in any minor version of Companion. - -This function should return a string `key`. The `req` parameter can be used to -upload to a user-specific folder in your bucket, for example: - -```js -app.use(authenticationMiddleware); -app.use( - uppy.app({ - providerOptions: { - s3: { - getKey: ({ req, filename, metadata }) => `${req.user.id}/${filename}`, - /* auth options */ - }, - }, - }), -); -``` - -The default implementation returns the `filename`, so all files will be uploaded -to the root of the bucket as their original file name. - -```js -app.use( - uppy.app({ - providerOptions: { - s3: { - getKey: ({ filename, metadata }) => filename, - }, - }, - }), -); -``` - -When signing on the client, this function will only be called for multipart -uploads. - -#### `COMPANION_AWS_USE_ACCELERATE_ENDPOINT` - -Enable S3 -[Transfer Acceleration](https://docs.aws.amazon.com/AmazonS3/latest/userguide/transfer-acceleration.html). -This is a standalone-only option. - -#### `COMPANION_AWS_EXPIRES` - -Set `X-Amz-Expires` query parameter in the presigned urls (in seconds, default: -300\). This is a standalone-only option. - -#### `COMPANION_AWS_ACL` - -Set a -[Canned ACL](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl) -for uploaded objects. This is a standalone-only option. - -#### `customProviders` - -This option enables you to add custom providers along with the already supported -providers. See [adding custom providers](#how-to-add-custom-providers) for more -information. - -#### `logClientVersion` - -A boolean flag to tell Companion whether to log its version upon startup. - -#### `metrics` `COMPANION_HIDE_METRICS` - -A boolean flag to tell Companion whether to provide an endpoint `/metrics` with -Prometheus metrics (by default metrics are enabled.) - -#### `streamingUpload` `COMPANION_STREAMING_UPLOAD` - -A boolean flag to tell Companion whether to enable streaming uploads. If -enabled, it will lead to _faster uploads_ because companion will start uploading -at the same time as downloading using `stream.pipe`. If `false`, files will be -fully downloaded first, then uploaded. Defaults to `true`. - -#### `maxFileSize` `COMPANION_MAX_FILE_SIZE` - -If this value is set, companion will limit the maximum file size to process. If -unset, it will process files without any size limit (this is the default). - -#### `periodicPingUrls` `COMPANION_PERIODIC_PING_URLS` - -If this value is set, companion will periodically send POST requests to the -specified URLs. Useful for keeping track of companion instances as a keep-alive. - -#### `periodicPingInterval` `COMPANION_PERIODIC_PING_INTERVAL` - -Interval for periodic ping requests (in ms). - -#### `periodicPingStaticPayload` `COMPANION_PERIODIC_PING_STATIC_JSON_PAYLOAD` - -A `JSON.stringify`-able JavaScript Object that will be sent as part of the JSON -body in the period ping requests. - -#### `allowLocalUrls` `COMPANION_ALLOW_LOCAL_URLS` - -A boolean flag to tell Companion whether to allow requesting local URLs -(non-internet IPs). - -:::caution - -Only enable this in development. **Enabling it in production is a security -risk.** - -::: - -#### `corsOrigins` (required) - -Allowed CORS Origins. Passed as the `origin` option in -[cors](https://github.com/expressjs/cors#configuration-options). - -Note this is used for both CORS’ `Access-Control-Allow-Origin` header, and for -the -[`targetOrigin`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#targetorigin) -for `postMessage` calls in the context of OAuth. - -Setting it to `true` treats any origin as a trusted one, making it easier to -impersonate your brand. Setting it to `false` disables cross-origin support, use -this if you’re serving Companion and Uppy from the same domain name. - -##### `COMPANION_CLIENT_ORIGINS` - -Stand-alone alternative to the `corsOrigins` option. A comma-separated string of -origins, or `'true'` (which will be interpreted as the boolean value `true`), or -`'false'` (which will be interpreted as the boolean value `false`). -`COMPANION_CLIENT_ORIGINS_REGEX` will be ignored if this option is used. - -##### `COMPANION_CLIENT_ORIGINS_REGEX` - -:::note - -In most cases, you should not be using a regex, and instead provide the list of -accepted origins to `COMPANION_CLIENT_ORIGINS`. If you have to use this option, -have in mind that this regex will be used to parse unfiltered user input, so -make sure you’re validating the entirety of the string. - -::: - -Stand-alone alternative to the `corsOrigins` option. Like -`COMPANION_CLIENT_ORIGINS`, but allows a single regex instead. - -#### `chunkSize` `COMPANION_CHUNK_SIZE` - -Controls how big the uploaded chunks are for AWS S3 Multipart and Tus. Smaller -values lead to more overhead, but larger values lead to slower retries in case -of bad network connections. Passed to tus-js-client -[`chunkSize`](https://github.com/tus/tus-js-client/blob/master/docs/api.md#chunksize) -as well as -[AWS S3 Multipart](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html) -`partSize`. - -#### `enableUrlEndpoint` `COMPANION_ENABLE_URL_ENDPOINT` - -Set this to `true` to enable the [URL functionalily](https://uppy.io/docs/url/). -Default: `false`. - -#### `enableGooglePickerEndpoint` `COMPANION_ENABLE_GOOGLE_PICKER_ENDPOINT` - -Set this to `true` to enable the Google Picker (Photos / Drive) functionality. -Default: `false`. - -### Events - -The object returned by `companion.app()` also has a property `companionEmitter` -which is an `EventEmitter` that emits the following events: - -- `upload-start` - When an upload starts, this event is emitted with an object - containing the property `token`, which is a unique ID for the upload. -- **token** - The event name is the token from `upload-start`. The event has an - object with the following properties: - - `action` - One of the following strings: - - `success` - When the upload succeeds. - - `error` - When the upload fails with an error. - - `payload` - the error or success payload. - -Example code for using the `EventEmitter` to handle a finished file upload: - -```js -const companionApp = companion.app(options); -const { companionEmitter: emitter } = companionApp; - -emitter.on('upload-start', ({ token }) => { - console.log('Upload started', token); - - function onUploadEvent({ action, payload }) { - if (action === 'success') { - emitter.off(token, onUploadEvent); // avoid listener leak - console.log('Upload finished', token, payload.url); - } else if (action === 'error') { - emitter.off(token, onUploadEvent); // avoid listener leak - console.error('Upload failed', payload); - } - } - emitter.on(token, onUploadEvent); -}); -``` - - - -## Frequently asked questions - -### Do you have a live example? - -An example server is running at . - -### How does the Authentication and Token mechanism work? - -This section describes how Authentication works between Companion and Providers. -While this behaviour is the same for all Providers (Dropbox, Instagram, Google -Drive, etc.), we are going to be referring to Dropbox in place of any Provider -throughout this section. - -The following steps describe the actions that take place when a user -Authenticates and Uploads from Dropbox through Companion: - -- The visitor to a website with Uppy clicks `Connect to Dropbox`. -- Uppy sends a request to Companion, which in turn sends an OAuth request to - Dropbox (Requires that OAuth credentials from Dropbox have been added to - Companion). -- Dropbox asks the visitor to log in, and whether the Website should be allowed - to access your files -- If the visitor agrees, Companion will receive a token from Dropbox, with which - we can temporarily download files. -- Companion encrypts the token with a secret key and sends the encrypted token - to Uppy (client) -- Every time the visitor clicks on a folder in Uppy, it asks Companion for the - new list of files, with this question, the token (still encrypted by - Companion) is sent along. -- Companion decrypts the token, requests the list of files from Dropbox and - sends it to Uppy. -- When a file is selected for upload, Companion receives the token again - according to this procedure, decrypts it again, and thereby downloads the file - from Dropbox. -- As the bytes arrive, Companion uploads the bytes to the final destination - (depending on the configuration: Apache, a Tus server, S3 bucket, etc). -- Companion reports progress to Uppy, as if it were a local upload. -- Completed! - -### How to use provider redirect URIs? - -When generating your provider API keys on their corresponding developer -platforms (e.g -[Google Developer Console](https://console.developers.google.com/)), you’d need -to provide a `redirect URI` for the OAuth authorization process. In general the -redirect URI for each provider takes the format: - -`http(s)://$YOUR_COMPANION_HOST_NAME/$PROVIDER_NAME/redirect` - -For example, if your Companion server is hosted on -`https://my.companion.server.com`, then the redirect URI you would supply for -your OneDrive provider would be: - -`https://my.companion.server.com/onedrive/redirect` - -Please see -[Supported Providers](https://uppy.io/docs/companion/#Supported-providers) for a -list of all Providers and their corresponding names. - -### How to use Companion with Kubernetes? - -We have a detailed -[guide](https://github.com/transloadit/uppy/blob/main/packages/%40uppy/companion/KUBERNETES.md) -on running Companion in Kubernetes. - -### How to add custom providers? - -As of now, Companion supports the -[providers listed here](https://uppy.io/docs/companion/#Supported-providers) out -of the box, but you may also choose to add your own custom providers. You can do -this by passing the `customProviders` option when calling the Uppy `app` method. -The custom provider is expected to support Oauth 1 or 2 for -authentication/authorization. - -```javascript -import providerModule from './path/to/provider/module'; - -const options = { - customProviders: { - myprovidername: { - config: { - authorize_url: 'https://mywebsite.com/authorize', - access_url: 'https://mywebsite.com/token', - oauth: 2, - key: '***', - secret: '***', - scope: ['read', 'write'], - }, - module: providerModule, - }, - }, -}; - -uppy.app(options); -``` - -The `customProviders` option should be an object containing each custom -provider. Each custom provider would, in turn, be an object with two keys, -`config` and `module`. The `config` option would contain Oauth API settings, -while the `module` would point to the provider module. - -To work well with Companion, the **module** must be a class with the following -methods. Note that the methods must be `async`, return a `Promise` or reject -with an `Error`): - -1. `async list ({ token, directory, query })` - Returns a object containing a - list of user files (such as a list of all the files in a particular - directory). See [example returned list data structure](#list-data). `token` - - authorization token (retrieved from oauth process) to send along with your - request - - `directory` - the id/name of the directory from which data is to be - retrieved. This may be ignored if it doesn’t apply to your provider - - `query` - expressjs query params object received by the server (in case - some data you need in there). -2. `async download ({ token, id, query })` - Downloads a particular file from - the provider. Returns an object with a single property `{ stream }` - a - [`stream.Readable`](https://nodejs.org/api/stream.html#stream_class_stream_readable), - which will be read from and uploaded to the destination. To prevent memory - leaks, make sure you release your stream if you reject this method with an - error. - - `token` - authorization token (retrieved from oauth process) to send along - with your request. - - `id` - ID of the file being downloaded. - - `query` - expressjs query params object received by the server (in case - some data you need in there). -3. `async size ({ token, id, query })` - Returns the byte size of the file that - needs to be downloaded as a `Number`. If the size of the object is not known, - `null` may be returned. - - `token` - authorization token (retrieved from oauth process) to send along - with your request. - - `id` - ID of the file being downloaded. - - `query` - expressjs query params object received by the server (in case - some data you need in there). - -The class must also have: - -- A unique `static authProvider` string property - a lowercased value which - indicates name of the [`grant`](https://github.com/simov/grant) OAuth2 - provider to use (e.g `google` for Google). If your provider doesn’t use - OAuth2, you can omit this property. -- A `static` property `static version = 2`, which is the current version of the - Companion Provider API. - -See also -[example code with a custom provider](https://github.com/transloadit/uppy/blob/main/examples/custom-provider/server). - -#### list data - -```json -{ - // username or email of the user whose provider account is being accessed - "username": "johndoe", - // list of files and folders in the directory. An item is considered a folder - // if it mainly exists as a collection to contain sub-items - "items": [ - { - // boolean value of whether or NOT it's a folder - "isFolder": false, - // icon image URL - "icon": "https://random-api.url.com/fileicon.jpg", - // name of the item - "name": "myfile.jpg", - // the mime type of the item. Only relevant if the item is NOT a folder - "mimeType": "image/jpg", - // the id (in string) of the item - "id": "uniqueitemid", - // thumbnail image URL. Only relevant if the item is NOT a folder - "thumbnail": "https://random-api.url.com/filethumbnail.jpg", - // for folders this is typically the value that will be passed as "directory" in the list(...) method. - // For files, this is the value that will be passed as id in the download(...) method. - "requestPath": "file-or-folder-requestpath", - // datetime string (in ISO 8601 format) of when this item was last modified - "modifiedDate": "2020-06-29T19:59:58Z", - // the size in bytes of the item. Only relevant if the item is NOT a folder - "size": 278940, - "custom": { - // an object that may contain some more custom fields that you may need to send to the client. Only add this object if you have a need for it. - "customData1": "the value", - "customData2": "the value" - } - // more items here - } - ], - // if the "items" list is paginated, this is the request path needed to fetch the next page. - "nextPagePath": "directory-name?cursor=cursor-to-next-page" -} -``` - -### How to run Companion locally? - -1. To set up Companion for local development, please clone the Uppy repo and - install, like so: - - ```bash - git clone https://github.com/transloadit/uppy - cd uppy - yarn install - ``` - -2. Configure your environment variables by copying the `env.example.sh` file to - `env.sh` and edit it to its correct values. - - ```bash - cp .env.example .env - $EDITOR .env - ``` - -3. To start the server, run: - - ```bash - yarn run start:companion - ``` - -This would get the Companion instance running on `http://localhost:3020`. It -uses [`node --watch`](https://nodejs.org/api/cli.html#--watch) so it will -automatically restart when files are changed. - -[box]: /docs/box -[dropbox]: /docs/dropbox -[facebook]: /docs/facebook -[googledrive]: /docs/google-drive -[googlephotos]: /docs/google-photos -[googledrivepicker]: /docs/google-drive-picker -[googlephotospicker]: /docs/google-photos-picker -[instagram]: /docs/instagram -[onedrive]: /docs/onedrive -[unsplash]: /docs/unsplash -[url]: /docs/url -[zoom]: /docs/zoom -[transloadit]: https://transloadit.com -[template-credentials]: - https://transloadit.com/docs/#how-to-create-template-credentials diff --git a/docs/sources/companion-plugins/box.mdx b/docs/sources/companion-plugins/box.mdx deleted file mode 100644 index 71e1407afd..0000000000 --- a/docs/sources/companion-plugins/box.mdx +++ /dev/null @@ -1,177 +0,0 @@ ---- -slug: /box ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -import UppyCdnExample from '/src/components/UppyCdnExample'; - -# Box - -The `@uppy/box` plugin lets users import files from their -[Box](https://www.box.com/en-nl/home) account. - -:::tip - -[Try out the live example](/examples) or take it for a spin in -[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js). - -::: - -## When should I use this? - -When you want to let users import files from their -[Box](https://www.box.com/en-nl/home) account. - -A [Companion](/docs/companion) instance is required for the Box plugin to work. -Companion handles authentication with Box, downloads the files, and uploads them -to the destination. This saves the user bandwidth, especially helpful if they -are on a mobile connection. - -You can self-host Companion or get a hosted version with any -[Transloadit plan](https://transloadit.com/pricing/). - - - - -```shell -npm install @uppy/box -``` - - - - - -```shell -yarn add @uppy/box -``` - - - - - - {` - import { Uppy, Box } from "{{UPPY_JS_URL}}" - const uppy = new Uppy() - uppy.use(Box, { - // Options - }) - `} - - - - -## Use - -Using Box requires setup in both Uppy and Companion. - -### Use in Uppy - -```js {10-13} showLineNumbers -import Uppy from '@uppy/core'; -import Dashboard from '@uppy/dashboard'; -import Box from '@uppy/box'; - -import '@uppy/core/dist/style.min.css'; -import '@uppy/dashboard/dist/style.min.css'; - -new Uppy() - .use(Dashboard, { inline: true, target: '#dashboard' }) - .use(Box, { companionUrl: 'https://your-companion.com' }); -``` - -### Use with Transloadit - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import Box from '@uppy/box'; - -uppy.use(Box, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, -}); -``` - -You may also hit rate limits, because the OAuth application is shared between -everyone using Transloadit. - -To solve that, you can use your own OAuth keys with Transloadit’s hosted -Companion servers by using Transloadit Template Credentials. [Create a Template -Credential][template-credentials] on the Transloadit site. Select “Companion -OAuth” for the service, and enter the key and secret for the provider you want -to use. Then you can pass the name of the new credentials to that provider: - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import Box from '@uppy/box'; - -uppy.use(Box, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, - companionKeysParams: { - key: 'YOUR_TRANSLOADIT_API_KEY', - credentialsName: 'my_companion_dropbox_creds', - }, -}); -``` - -### Use in Companion - -You can create a Box App on the -[Box Developers site](https://app.box.com/developers/console). - -Things to note: - -- Choose `Custom App` and select the `User Authentication (OAuth 2.0)` app type. -- You must enable full write access, or you will get - [403 when downloading files](https://support.box.com/hc/en-us/community/posts/360049195613-403-error-while-file-download-API-Call) - -You’ll be redirected to the app page. This page lists the client ID (app key) -and client secret (app secret), which you should use to configure Companion. - -The app page has a `"Redirect URIs"` field. Here, add: - -``` -https://$YOUR_COMPANION_HOST_NAME/box/redirect -``` - -If you are using Transloadit hosted Companion: - -``` -https://api2.transloadit.com/companion/box/redirect -``` - -You can only use the integration with your own account initially. Make sure to -apply for production status on the app page before you publish your app, or your -users will not be able to sign in! - -Configure the Box key and secret. With the standalone Companion server, specify -environment variables: - -```shell -export COMPANION_BOX_KEY="Box API key" -export COMPANION_BOX_SECRET="Box API secret" -``` - -When using the Companion Node.js API, configure these options: - -```js -companion.app({ - providerOptions: { - box: { - key: 'Box API key', - secret: 'Box API secret', - }, - }, -}); -``` - -## API - -### Options - -#### [Common Companion options...](./companion-options.mdx) - -[template-credentials]: - https://transloadit.com/docs/#how-to-create-template-credentials diff --git a/docs/sources/companion-plugins/companion-options.mdx b/docs/sources/companion-plugins/companion-options.mdx deleted file mode 100644 index a1cc4e0af0..0000000000 --- a/docs/sources/companion-plugins/companion-options.mdx +++ /dev/null @@ -1,69 +0,0 @@ ---- -slug: /companion-options ---- - -# Common Companion options - -These are the options common to all Uppy plugins that use Companion. - -## `id` - -A unique identifier for this plugin (`string`, default is a unique ID for each -plugin). - -## `title` - -Title / name shown in the UI, such as Dashboard tabs (`string`, default is the -name of the plugin). - -## `target` - -DOM element, CSS selector, or plugin to place the drag and drop area into -(`string`, `Element`, `Function`, or `UIPlugin`, default: -[`Dashboard`](/docs/dashboard)). - -## `companionUrl` - -URL to a [Companion](/docs/companion) instance (`string`, default: `null`). - -## `companionHeaders` - -Custom headers that should be sent along to [Companion](/docs/companion) on -every request (`Object`, default: `{}`). - -## `companionAllowedHosts` - -The valid and authorised URL(s) from which OAuth responses should be accepted -(`string` or `RegExp` or `Array`, default: `companionUrl`). - -This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is -useful when you have your [Companion](/docs/companion) running on several hosts. -Otherwise, the default value should do fine. - -## `companionCookiesRule` - -This option correlates to the -[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials) -(`string`, default: `'same-origin'`). - -This tells the plugin whether to send cookies to [Companion](/docs/companion). - -## `locale` - -An object with `strings` property containing additional i18n strings. The key is -the i18n key and the value is the English string. - -Example: - -```js -{ - strings: { - someKey: 'Some English string', - }, -} -``` - -## `storage` - -A custom storage to be used for the plugin’s persistent data. Type `AsyncStore`, -default is `LocalStorage`. diff --git a/docs/sources/companion-plugins/dropbox.mdx b/docs/sources/companion-plugins/dropbox.mdx deleted file mode 100644 index e7a8efc207..0000000000 --- a/docs/sources/companion-plugins/dropbox.mdx +++ /dev/null @@ -1,177 +0,0 @@ ---- -slug: /dropbox ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -import UppyCdnExample from '/src/components/UppyCdnExample'; - -# Dropbox - -The `@uppy/dropbox` plugin lets users import files from their -[Dropbox](https://www.dropbox.com) account. - -:::tip - -[Try out the live example](/examples) or take it for a spin in -[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js). - -::: - -## When should I use this? - -When you want to let users import files from their -[Dropbox](https://www.dropbox.com) account. - -A [Companion](/docs/companion) instance is required for the Dropbox plugin to -work. Companion handles authentication with Dropbox, downloads the files, and -uploads them to the destination. This saves the user bandwidth, especially -helpful if they are on a mobile connection. - -You can self-host Companion or get a hosted version with any -[Transloadit plan](https://transloadit.com/pricing/). - - - - -```shell -npm install @uppy/dropbox -``` - - - - - -```shell -yarn add @uppy/dropbox -``` - - - - - - {` - import { Uppy, Dropbox } from "{{UPPY_JS_URL}}" - const uppy = new Uppy() - uppy.use(Dropbox, { - // Options - }) - `} - - - - -## Use - -Using Dropbox requires setup in both Uppy and Companion. - -### Use in Uppy - -```js {10-13} showLineNumbers -import Uppy from '@uppy/core'; -import Dashboard from '@uppy/dashboard'; -import Dropbox from '@uppy/dropbox'; - -import '@uppy/core/dist/style.min.css'; -import '@uppy/dashboard/dist/style.min.css'; - -new Uppy() - .use(Dashboard, { inline: true, target: '#dashboard' }) - .use(Dropbox, { companionUrl: 'https://your-companion.com' }); -``` - -### Use with Transloadit - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import Dropbox from '@uppy/dropbox'; - -uppy.use(Dropbox, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, -}); -``` - -You may also hit rate limits, because the OAuth application is shared between -everyone using Transloadit. - -To solve that, you can use your own OAuth keys with Transloadit’s hosted -Companion servers by using Transloadit Template Credentials. [Create a Template -Credential][template-credentials] on the Transloadit site. Select “Companion -OAuth” for the service, and enter the key and secret for the provider you want -to use. Then you can pass the name of the new credentials to that provider: - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import Dropbox from '@uppy/dropbox'; - -uppy.use(Dropbox, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, - companionKeysParams: { - key: 'YOUR_TRANSLOADIT_API_KEY', - credentialsName: 'my_companion_dropbox_creds', - }, -}); -``` - -### Use in Companion - -You can create a Dropbox App on the -[Dropbox Developers site](https://www.dropbox.com/developers/apps/create). - -Things to note: - -- Choose the “Dropbox API”, not the business variant. -- Typically you’ll want “Full Dropbox” access, unless you are absolutely certain - that you need the other one. - -You’ll be redirected to the app page. This page lists the app key and app -secret, which you should use to configure Companion as shown above. - -The app page has a “Redirect URIs” field. Here, add: - -``` -https://$YOUR_COMPANION_HOST_NAME/dropbox/redirect -``` - -If you are using Transloadit hosted Companion: - -``` -https://api2.transloadit.com/companion/dropbox/redirect -``` - -You can only use the integration with your own account initially. Make sure to -apply for production status on the app page before you publish your app, or your -users will not be able to sign in! - -Configure the Dropbox key and secret. With the standalone Companion server, -specify environment variables: - -```shell -export COMPANION_DROPBOX_KEY="Dropbox API key" -export COMPANION_DROPBOX_SECRET="Dropbox API secret" -``` - -When using the Companion Node.js API, configure these options: - -```js -companion.app({ - providerOptions: { - dropbox: { - key: 'Dropbox API key', - secret: 'Dropbox API secret', - }, - }, -}); -``` - -## API - -### Options - -#### [Common Companion options...](./companion-options.mdx) - -[template-credentials]: - https://transloadit.com/docs/#how-to-create-template-credentials diff --git a/docs/sources/companion-plugins/facebook.mdx b/docs/sources/companion-plugins/facebook.mdx deleted file mode 100644 index 71f248a49a..0000000000 --- a/docs/sources/companion-plugins/facebook.mdx +++ /dev/null @@ -1,174 +0,0 @@ ---- -slug: /facebook ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -import UppyCdnExample from '/src/components/UppyCdnExample'; - -# Facebook - -The `@uppy/facebook` plugin lets users import files from their -[Facebook](https://www.facebook.com) account. - -:::tip - -[Try out the live example](/examples) or take it for a spin in -[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js). - -::: - -## When should I use this? - -When you want to let users import files from their -[Facebook](https://www.facebook.com) account. - -A [Companion](/docs/companion) instance is required for the Facebook plugin to -work. Companion handles authentication with Facebook, downloads the files, and -uploads them to the destination. This saves the user bandwidth, especially -helpful if they are on a mobile connection. - -You can self-host Companion or get a hosted version with any -[Transloadit plan](https://transloadit.com/pricing/). - - - - -```shell -npm install @uppy/facebook -``` - - - - - -```shell -yarn add @uppy/facebook -``` - - - - - - {` - import { Uppy, Facebook } from "{{UPPY_JS_URL}}" - const uppy = new Uppy() - uppy.use(Facebook, { - // Options - }) - `} - - - - -## Use - -Using Facebook requires setup in both Uppy and Companion. - -### Use in Uppy - -```js {10-13} showLineNumbers -import Uppy from '@uppy/core'; -import Dashboard from '@uppy/dashboard'; -import Facebook from '@uppy/facebook'; - -import '@uppy/core/dist/style.min.css'; -import '@uppy/dashboard/dist/style.min.css'; - -new Uppy() - .use(Dashboard, { inline: true, target: '#dashboard' }) - .use(Facebook, { companionUrl: 'https://your-companion.com' }); -``` - -### Use with Transloadit - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import Facebook from '@uppy/facebook'; - -uppy.use(Facebook, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, -}); -``` - -You may also hit rate limits, because the OAuth application is shared between -everyone using Transloadit. - -To solve that, you can use your own OAuth keys with Transloadit’s hosted -Companion servers by using Transloadit Template Credentials. [Create a Template -Credential][template-credentials] on the Transloadit site. Select “Companion -OAuth” for the service, and enter the key and secret for the provider you want -to use. Then you can pass the name of the new credentials to that provider: - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import Facebook from '@uppy/facebook'; - -uppy.use(Facebook, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, - companionKeysParams: { - key: 'YOUR_TRANSLOADIT_API_KEY', - credentialsName: 'my_companion_dropbox_creds', - }, -}); -``` - -### Use in Companion - -You can create a Facebook App on the -[Facebook Developers site](https://developers.facebook.com/apps). - -The app page has a “Redirect URIs” field. Here, add: - -``` -https://$YOUR_COMPANION_HOST_NAME/facebook/redirect -``` - -If you are using Transloadit hosted Companion: - -``` -https://api2.transloadit.com/companion/facebook/redirect -``` - -You can only use the integration with your own account initially. Make sure to -apply for production status on the app page before you publish your app, or your -users will not be able to sign in! - -You need to set up OAuth in your Facebook app for Companion to be able to -connect to users’ Facebook accounts. You have to enable “Advanced Access” for -the `user_photos` permission. A precondition of that is “Business Verification” -which involves setting up a Meta Business Account and submitting documents to -prove business ownership. - -Configure the Facebook key and secret. With the standalone Companion server, -specify environment variables: - -```shell -export COMPANION_FACEBOOK_KEY="Facebook API key" -export COMPANION_FACEBOOK_SECRET="Facebook API secret" -``` - -When using the Companion Node.js API, configure these options: - -```js -companion.app({ - providerOptions: { - facebook: { - key: 'Facebook API key', - secret: 'Facebook API secret', - }, - }, -}); -``` - -## API - -### Options - -#### [Common Companion options...](./companion-options.mdx) - -[template-credentials]: - https://transloadit.com/docs/#how-to-create-template-credentials diff --git a/docs/sources/companion-plugins/google-drive-picker.mdx b/docs/sources/companion-plugins/google-drive-picker.mdx deleted file mode 100644 index 3a4adf4300..0000000000 --- a/docs/sources/companion-plugins/google-drive-picker.mdx +++ /dev/null @@ -1,142 +0,0 @@ ---- -slug: /google-drive-picker ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -import UppyCdnExample from '/src/components/UppyCdnExample'; - -# Google Drive Picker - -The `@uppy/google-drive` plugin lets users import files from their -[Google Drive](https://drive.google.com) account using the new -[Picker API](https://developers.google.com/drive/picker). The benefit of using -this API over the traditional Google Drive API (which the -[Google Drive plugin](./google-drive.mdx) uses) is that the Picker API requires -less verification from Google. Drawbacks of the Picker API include less control -and inability to select folders. See also -[Google Photos Picker](./google-photos-picker.mdx). - -## When should I use this? - -When you want to let users import files from their -[Google Drive](https://drive.google.com) account. - -A [Companion](/docs/companion) instance is required for the Google Drive Picker -plugin to work. Companion downloads the files from Google Drive, and uploads -them to the destination. This saves the user bandwidth, especially helpful if -they are on a mobile connection. - -You can self-host Companion or get a hosted version with any -[Transloadit plan](https://transloadit.com/pricing/). - - - - -```shell -npm install @uppy/google-drive-picker -``` - - - - - -```shell -yarn add @uppy/google-drive-picker -``` - - - - - - {` - import { Uppy, GoogleDrivePicker } from "{{UPPY_JS_URL}}" - const uppy = new Uppy() - uppy.use(GoogleDrivePicker, { - // Options - }) - `} - - - - -## Use - -Using Google Drive Picker requires setup in both Uppy and Companion. - -### Initial setup - -To sign up for API keys, go to the -[Google Developer Console](https://console.developers.google.com/). - -Create a project for your app if you don’t have one yet. - -- On the project’s dashboard, enable the - [Google Picker API](https://console.cloud.google.com/apis/library/picker.googleapis.com) - (for Google Drive). -- Create an API key. -- Create an OAuth 2.0 Client ID of type Web application with the correct - Authorized JavaScript origins. - -### Use in Uppy - -```js {10-13} showLineNumbers -import Uppy from '@uppy/core'; -import Dashboard from '@uppy/dashboard'; -import GoogleDrivePicker from '@uppy/google-drive-picker'; - -import '@uppy/core/dist/style.min.css'; -import '@uppy/dashboard/dist/style.min.css'; - -new Uppy() - .use(Dashboard, { inline: true, target: '#dashboard' }) - .use(GoogleDrivePicker, { - companionUrl: 'https://your-companion.com', - clientId: 'From Google Developer Console', - apiKey: 'From Google Developer Console', - appId: 'From Google Developer Console', - }); -``` - -### Use with Transloadit - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import GoogleDrivePicker from '@uppy/google-drive-picker'; - -uppy.use(GoogleDrivePicker, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, - clientId: 'From Google Developer Console', - apiKey: 'From Google Developer Console', - appId: 'From Google Developer Console', -}); -``` - -### Use in Companion - -Companion is used to download/upload the picked files. Companion supports this -plugin out-of-the-box, however it must be enabled in Companion with the -`enableGooglePickerEndpoint` / `COMPANION_ENABLE_GOOGLE_PICKER_ENDPOINT` option. -For this plugin, all credentials are public (non-secret) and provided in the -frontend. - -## API - -### Options - -#### [Common Companion options...](./companion-options.mdx) - -#### `clientId` - -The client ID from the [Initial setup](#initial-setup) (`string`). - -#### `apiKey` - -The API key from the [Initial setup](#initial-setup) (`string`). - -#### `appId` - -The App ID is the project ID which can be found in the URL in the Google -Developer Console (`string`). diff --git a/docs/sources/companion-plugins/google-drive.mdx b/docs/sources/companion-plugins/google-drive.mdx deleted file mode 100644 index 4a3d587bb4..0000000000 --- a/docs/sources/companion-plugins/google-drive.mdx +++ /dev/null @@ -1,178 +0,0 @@ ---- -slug: /google-drive ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -import UppyCdnExample from '/src/components/UppyCdnExample'; - -# Google Drive - -The `@uppy/google-drive` plugin lets users import files from their -[Google Drive](https://drive.google.com) account. - -:::tip - -[Try out the live example](/examples) or take it for a spin in -[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js). - -::: - -## When should I use this? - -When you want to let users import files from their -[Google Drive](https://drive.google.com) account. - -A [Companion](/docs/companion) instance is required for the Google Drive plugin -to work. Companion handles authentication with Google Drive, downloads the -files, and uploads them to the destination. This saves the user bandwidth, -especially helpful if they are on a mobile connection. - -You can self-host Companion or get a hosted version with any -[Transloadit plan](https://transloadit.com/pricing/). - - - - -```shell -npm install @uppy/google-drive -``` - - - - - -```shell -yarn add @uppy/google-drive -``` - - - - - - {` - import { Uppy, GoogleDrive } from "{{UPPY_JS_URL}}" - const uppy = new Uppy() - uppy.use(GoogleDrive, { - // Options - }) - `} - - - - -## Use - -Using Google Drive requires setup in both Uppy and Companion. - -### Use in Uppy - -```js {10-13} showLineNumbers -import Uppy from '@uppy/core'; -import Dashboard from '@uppy/dashboard'; -import GoogleDrive from '@uppy/google-drive'; - -import '@uppy/core/dist/style.min.css'; -import '@uppy/dashboard/dist/style.min.css'; - -new Uppy() - .use(Dashboard, { inline: true, target: '#dashboard' }) - .use(GoogleDrive, { companionUrl: 'https://your-companion.com' }); -``` - -### Use with Transloadit - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import GoogleDrive from '@uppy/google-drive'; - -uppy.use(GoogleDrive, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, -}); -``` - -You may also hit rate limits, because the OAuth application is shared between -everyone using Transloadit. - -To solve that, you can use your own OAuth keys with Transloadit’s hosted -Companion servers by using Transloadit Template Credentials. [Create a Template -Credential][template-credentials] on the Transloadit site. Select “Companion -OAuth” for the service, and enter the key and secret for the provider you want -to use. Then you can pass the name of the new credentials to that provider: - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import GoogleDrive from '@uppy/google-drive'; - -uppy.use(GoogleDrive, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, - companionKeysParams: { - key: 'YOUR_TRANSLOADIT_API_KEY', - credentialsName: 'my_companion_dropbox_creds', - }, -}); -``` - -### Use in Companion - -To sign up for API keys, go to the -[Google Developer Console](https://console.developers.google.com/). - -Create a project for your app if you don’t have one yet. - -- On the project’s dashboard, - [enable the Google Drive API](https://developers.google.com/drive/api/v3/enable-drive-api). -- [Set up OAuth authorization](https://developers.google.com/drive/api/v3/about-auth). - - Under scopes, add the `https://www.googleapis.com/auth/drive.readonly` Drive - API scope. - - Due to this being a sensitive scope, your app must complete Google’s OAuth - app verification before being granted access. See - [OAuth App Verification Help Center](https://support.google.com/cloud/answer/13463073) - for more information. - -The app page has a `"Redirect URIs"` field. Here, add: - -``` -https://$YOUR_COMPANION_HOST_NAME/drive/redirect -``` - -If you are using Transloadit hosted Companion: - -``` -https://api2.transloadit.com/companion/drive/redirect -``` - -Google will give you an OAuth client ID and client secret. - -Configure the Google key and secret in Companion. With the standalone Companion -server, specify environment variables: - -```shell -export COMPANION_GOOGLE_KEY="Google OAuth client ID" -export COMPANION_GOOGLE_SECRET="Google OAuth client secret" -``` - -When using the Companion Node.js API, configure these options: - -```js -companion.app({ - providerOptions: { - drive: { - key: 'Google OAuth client ID', - secret: 'Google OAuth client secret', - }, - }, -}); -``` - -## API - -### Options - -#### [Common Companion options...](./companion-options.mdx) - -[template-credentials]: - https://transloadit.com/docs/#how-to-create-template-credentials diff --git a/docs/sources/companion-plugins/google-photos-picker.mdx b/docs/sources/companion-plugins/google-photos-picker.mdx deleted file mode 100644 index 195710dba9..0000000000 --- a/docs/sources/companion-plugins/google-photos-picker.mdx +++ /dev/null @@ -1,127 +0,0 @@ ---- -slug: /google-photos-picker ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -import UppyCdnExample from '/src/components/UppyCdnExample'; - -# Google Photos Picker - -The `@uppy/google-photos` plugin lets users import files from their -[Google Photos](https://photos.google.com) account using the new -[Picker API](https://developers.google.com/photos/picker). The benefit of using -this API over the traditional Google Photos API (which the -[Google Photos plugin](./google-photos.mdx) uses) is that the Picker API -requires less verification from Google. Drawbacks of the Picker API include less -control and inability to select shared albums. See also -[Google Drive Picker](./google-drive-picker.mdx). - -## When should I use this? - -When you want to let users import files from their -[Google Photos](https://photos.google.com) account. - -A [Companion](/docs/companion) instance is required for the Google Photos Picker -plugin to work. Companion downloads the files from Google Photos, and uploads -them to the destination. This saves the user bandwidth, especially helpful if -they are on a mobile connection. - -You can self-host Companion or get a hosted version with any -[Transloadit plan](https://transloadit.com/pricing/). - - - - -```shell -npm install @uppy/google-photos-picker -``` - - - - - -```shell -yarn add @uppy/google-photos-picker -``` - - - - - - {` - import { Uppy, GooglePhotosPicker } from "{{UPPY_JS_URL}}" - const uppy = new Uppy() - uppy.use(GooglePhotosPicker, { - // Options - }) - `} - - - - -## Use - -Using Google Photos Picker requires setup in both Uppy and Companion. - -### Initial setup - -To sign up for API keys, go to the -[Google Developer Console](https://console.developers.google.com/). - -Create a project for your app if you don’t have one yet. - -- On the project’s dashboard, enable the - [Google Photos Picker API](https://console.cloud.google.com/apis/library/photospicker.googleapis.com). -- Create an OAuth 2.0 Client ID of type Web application with the correct - Authorized JavaScript origins. - -### Use in Uppy - -```js {10-13} showLineNumbers -import Uppy from '@uppy/core'; -import Dashboard from '@uppy/dashboard'; -import GooglePhotosPicker from '@uppy/google-photos-picker'; - -import '@uppy/core/dist/style.min.css'; -import '@uppy/dashboard/dist/style.min.css'; - -new Uppy() - .use(Dashboard, { inline: true, target: '#dashboard' }) - .use(GooglePhotosPicker, { - companionUrl: 'https://your-companion.com', - clientId: 'From Google Developer Console', - }); -``` - -### Use with Transloadit - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import GooglePhotosPicker from '@uppy/google-photos-picker'; - -uppy.use(GooglePhotosPicker, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, - clientId: 'From Google Developer Console', -}); -``` - -### Use in Companion - -Companion is used to download/upload the picked files. Companion supports this -plugin out-of-the-box, however it must be enabled in Companion with the -`enableGooglePickerEndpoint` / `COMPANION_ENABLE_GOOGLE_PICKER_ENDPOINT` option. -For this plugin, all credentials are public (non-secret) and provided in the -frontend. - -## API - -### Options - -#### [Common Companion options...](./companion-options.mdx) - -#### `clientId` - -The client ID from the [Initial setup](#initial-setup) (`string`). diff --git a/docs/sources/companion-plugins/google-photos.mdx b/docs/sources/companion-plugins/google-photos.mdx deleted file mode 100644 index fc6fb39d8e..0000000000 --- a/docs/sources/companion-plugins/google-photos.mdx +++ /dev/null @@ -1,175 +0,0 @@ ---- -slug: /google-photos ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -import UppyCdnExample from '/src/components/UppyCdnExample'; - -# Google Photos - -The `@uppy/google-photos` plugin lets users import files from their -[Google Photos](https://photos.google.com) account. - -:::tip - -[Try out the live example](/examples) or take it for a spin in -[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js). - -::: - -## When should I use this? - -When you want to let users import files from their -[Google Photos](https://photos.google.com) account. - -A [Companion](/docs/companion) instance is required for the Google Photos plugin -to work. Companion handles authentication with Google Photos, downloads the -photos/videos, and uploads them to the destination. This saves the user -bandwidth, especially helpful if they are on a mobile connection. - -You can self-host Companion or get a hosted version with any -[Transloadit plan](https://transloadit.com/pricing/). - - - - -```shell -npm install @uppy/google-photos -``` - - - - - -```shell -yarn add @uppy/google-photos -``` - - - - - - {` - import { Uppy, GooglePhotos } from "{{UPPY_JS_URL}}" - const uppy = new Uppy() - uppy.use(GooglePhotos, { - // Options - }) - `} - - - - -## Use - -Using Google Photos requires setup in both Uppy and Companion. - -### Use in Uppy - -```js {10-13} showLineNumbers -import Uppy from '@uppy/core'; -import Dashboard from '@uppy/dashboard'; -import GooglePhotos from '@uppy/google-photos'; - -import '@uppy/core/dist/style.min.css'; -import '@uppy/dashboard/dist/style.min.css'; - -new Uppy() - .use(Dashboard, { inline: true, target: '#dashboard' }) - .use(GooglePhotos, { - target: Dashboard, - companionUrl: 'https://your-companion.com', - }); -``` - -### Use with Transloadit - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import GooglePhotos from '@uppy/google-photos'; - -uppy.use(GooglePhotos, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, -}); -``` - -You may also hit rate limits, because the OAuth application is shared between -everyone using Transloadit. - -To solve that, you can use your own OAuth keys with Transloadit’s hosted -Companion servers by using Transloadit Template Credentials. [Create a Template -Credential][template-credentials] on the Transloadit site. Select “Companion -OAuth” for the service, and enter the key and secret for the provider you want -to use. Then you can pass the name of the new credentials to that provider: - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import GooglePhotos from '@uppy/google-photos'; - -uppy.use(GooglePhotos, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, - companionKeysParams: { - key: 'YOUR_TRANSLOADIT_API_KEY', - credentialsName: 'my_companion_dropbox_creds', - }, -}); -``` - -### Use in Companion - -To sign up for API keys, go to the -[Google Developer Console](https://console.developers.google.com/). - -Create a project for your app if you don’t have one yet. - -- On the project’s dashboard, - [enable the Google Photos API](https://developers.google.com/photos). -- [Set up OAuth authorization](https://developers.google.com/photos/library/guides/authorization). - -The app page has a `"Redirect URIs"` field. Here, add: - -``` -https://$YOUR_COMPANION_HOST_NAME/googlephotos/redirect -``` - -If you are using Transloadit hosted Companion: - -``` -https://api2.transloadit.com/companion/googlephotos/redirect -``` - -Google will give you an OAuth client ID and client secret. - -Configure the Google key and secret in Companion. With the standalone Companion -server, specify environment variables: - -```shell -export COMPANION_GOOGLE_KEY="Google OAuth client ID" -export COMPANION_GOOGLE_SECRET="Google OAuth client secret" -``` - -When using the Companion Node.js API, configure these options: - -```js -companion.app({ - providerOptions: { - googlephotos: { - key: 'Google OAuth client ID', - secret: 'Google OAuth client secret', - }, - }, -}); -``` - -## API - -### Options - -#### [Common Companion options...](./companion-options.mdx) - -[template-credentials]: - https://transloadit.com/docs/#how-to-create-template-credentials diff --git a/docs/sources/companion-plugins/instagram.mdx b/docs/sources/companion-plugins/instagram.mdx deleted file mode 100644 index 2521555ef4..0000000000 --- a/docs/sources/companion-plugins/instagram.mdx +++ /dev/null @@ -1,187 +0,0 @@ ---- -slug: /instagram ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -import UppyCdnExample from '/src/components/UppyCdnExample'; - -# Instagram - -The `@uppy/instagram` plugin lets users import files from their -[Instagram](https://instagram.com) account. - -:::tip - -[Try out the live example](/examples) or take it for a spin in -[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js). - -::: - -## When should I use this? - -When you want to let users import files from their -[Instagram](https://instagram.com) account. - -A [Companion](/docs/companion) instance is required for the Instagram plugin to -work. Companion handles authentication with Instagram, downloads the files, and -uploads them to the destination. This saves the user bandwidth, especially -helpful if they are on a mobile connection. - -You can self-host Companion or get a hosted version with any -[Transloadit plan](https://transloadit.com/pricing/). - - - - -```shell -npm install @uppy/instagram -``` - - - - - -```shell -yarn add @uppy/instagram -``` - - - - - - {` - import { Uppy, Instagram } from "{{UPPY_JS_URL}}" - const uppy = new Uppy() - uppy.use(Instagram, { - // Options - }) - `} - - - - -## Use - -Using Instagram requires setup in both Uppy and Companion. - -### Use in Uppy - -```js {10-13} showLineNumbers -import Uppy from '@uppy/core'; -import Dashboard from '@uppy/dashboard'; -import Instagram from '@uppy/instagram'; - -import '@uppy/core/dist/style.min.css'; -import '@uppy/dashboard/dist/style.min.css'; - -new Uppy() - .use(Dashboard, { inline: true, target: '#dashboard' }) - .use(Instagram, { companionUrl: 'https://your-companion.com' }); -``` - -### Use with Transloadit - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import Instagram from '@uppy/instagram'; - -uppy.use(Instagram, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, -}); -``` - -You may also hit rate limits, because the OAuth application is shared between -everyone using Transloadit. - -To solve that, you can use your own OAuth keys with Transloadit’s hosted -Companion servers by using Transloadit Template Credentials. [Create a Template -Credential][template-credentials] on the Transloadit site. Select “Companion -OAuth” for the service, and enter the key and secret for the provider you want -to use. Then you can pass the name of the new credentials to that provider: - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import Instagram from '@uppy/instagram'; - -uppy.use(Instagram, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, - companionKeysParams: { - key: 'YOUR_TRANSLOADIT_API_KEY', - credentialsName: 'my_companion_dropbox_creds', - }, -}); -``` - -### Use in Companion - -To sign up for API keys, go to the -[Instagram Platform from Meta](https://developers.facebook.com/products/instagram/). - -Create a project for your app if you don’t have one yet. - -The app page has a `"Redirect URIs"` field. Here, add: - -``` -https://$YOUR_COMPANION_HOST_NAME/instagram/redirect -``` - -If you are using Transloadit hosted Companion: - -``` -https://api2.transloadit.com/companion/instagram/redirect -``` - -Meta will give you an OAuth client ID and client secret. - -Configure the Instagram key and secret in Companion. With the standalone -Companion server, specify environment variables: - -```shell -export COMPANION_INSTAGRAM_KEY="Instagram OAuth client ID" -export COMPANION_INSTAGRAM_SECRET="Instagram OAuth client secret" -``` - -When using the Companion Node.js API, configure these options: - -```js -companion.app({ - providerOptions: { - instagram: { - key: 'Instagram OAuth client ID', - secret: 'Instagram OAuth client secret', - }, - }, -}); -``` - -### Development - -Among Uppy-supported providers, Instagram is the only provider at the time of -writing that requires https even in dev mode. So, to test your integration in -development, you need to use some reverse proxy. The easiest way to do it is to -use [https://redirectmeto.com](https://redirectmeto.com). - -In your `.env`, set: - -```sh -COMPANION_DOMAIN="redirectmeto.com/http://localhost:3020" -COMPANION_PROTOCOL="https" -``` - -On -[https://developers.facebook.com/apps/.../instagram-basic-display/basic-display](https://developers.facebook.com/apps/.../instagram-basic-display/basic-display) -page, in the “Valid OAuth Redirect URIs” field, add -`https://redirectmeto.com/http://localhost:3020/instagram/redirect`. - -## API - -### Options - -#### [Common Companion options...](./companion-options.mdx) - -[template-credentials]: - https://transloadit.com/docs/#how-to-create-template-credentials diff --git a/docs/sources/companion-plugins/onedrive.mdx b/docs/sources/companion-plugins/onedrive.mdx deleted file mode 100644 index 97084b7419..0000000000 --- a/docs/sources/companion-plugins/onedrive.mdx +++ /dev/null @@ -1,175 +0,0 @@ ---- -slug: /onedrive ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -import UppyCdnExample from '/src/components/UppyCdnExample'; - -# OneDrive - -The `@uppy/onedrive` plugin lets users import files from their -[OneDrive](https://onedrive.com) account. - -:::tip - -[Try out the live example](/examples) or take it for a spin in -[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js). - -::: - -## When should I use this? - -When you want to let users import files from their -[OneDrive](https://onedrive.com) account. - -A [Companion](/docs/companion) instance is required for the OneDrive plugin to -work. Companion handles authentication with OneDrive, downloads the files, and -uploads them to the destination. This saves the user bandwidth, especially -helpful if they are on a mobile connection. - -You can self-host Companion or get a hosted version with any -[Transloadit plan](https://transloadit.com/pricing/). - - - - -```shell -npm install @uppy/onedrive -``` - - - - - -```shell -yarn add @uppy/onedrive -``` - - - - - - {` - import { Uppy, OneDrive } from "{{UPPY_JS_URL}}" - const uppy = new Uppy() - uppy.use(OneDrive, { - // Options - }) - `} - - - - -## Use - -Using OneDrive requires setup in both Uppy and Companion. - -### Use in Uppy - -```js {10-13} showLineNumbers -import Uppy from '@uppy/core'; -import Dashboard from '@uppy/dashboard'; -import OneDrive from '@uppy/onedrive'; - -import '@uppy/core/dist/style.min.css'; -import '@uppy/dashboard/dist/style.min.css'; - -new Uppy() - .use(Dashboard, { inline: true, target: '#dashboard' }) - .use(OneDrive, { companionUrl: 'https://your-companion.com' }); -``` - -### Use with Transloadit - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import OneDrive from '@uppy/onedrive'; - -uppy.use(OneDrive, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, -}); -``` - -You may also hit rate limits, because the OAuth application is shared between -everyone using Transloadit. - -To solve that, you can use your own OAuth keys with Transloadit’s hosted -Companion servers by using Transloadit Template Credentials. [Create a Template -Credential][template-credentials] on the Transloadit site. Select “Companion -OAuth” for the service, and enter the key and secret for the provider you want -to use. Then you can pass the name of the new credentials to that provider: - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import OneDrive from '@uppy/onedrive'; - -uppy.use(OneDrive, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, - companionKeysParams: { - key: 'YOUR_TRANSLOADIT_API_KEY', - credentialsName: 'my_companion_dropbox_creds', - }, -}); -``` - -### Use in Companion - -To sign up for API keys, go to the -[Azure Platform from Microsoft](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade). - -Create a project for your app if you don’t have one yet. - -The app page has a `"Redirect URIs"` field. Here, add: - -``` -https://$YOUR_COMPANION_HOST_NAME/onedrive/redirect -``` - -If you are using Transloadit hosted Companion: - -``` -https://api2.transloadit.com/companion/onedrive/redirect -``` - -Go to the “Manifest” tab, and find the `"signInAudience"` key. Change it to -`"signInAudience": "AzureADandPersonalMicrosoftAccount"`, and click “Save”. - -Go to the “Overview” tab. Copy the `Application (client) ID` field - this will -be your Oauth client ID. - -Go to the “Certificates & secrets” tab, and click “+ New client secret”. Copy -the `Value` field - this will be your OAuth client secret. - -Configure the OneDrive key and secret in Companion. With the standalone -Companion server, specify environment variables: - -```shell -export COMPANION_ONEDRIVE_KEY="OneDrive Application ID" -export COMPANION_ONEDRIVE_SECRET="OneDrive OAuth client secret value" -``` - -When using the Companion Node.js API, configure these options: - -```js -companion.app({ - providerOptions: { - onedrive: { - key: 'OneDrive Application ID', - secret: 'OneDrive OAuth client secret value', - }, - }, -}); -``` - -## API - -### Options - -#### [Common Companion options...](./companion-options.mdx) - -[template-credentials]: - https://transloadit.com/docs/#how-to-create-template-credentials diff --git a/docs/sources/companion-plugins/unsplash.mdx b/docs/sources/companion-plugins/unsplash.mdx deleted file mode 100644 index 3771f41772..0000000000 --- a/docs/sources/companion-plugins/unsplash.mdx +++ /dev/null @@ -1,153 +0,0 @@ ---- -slug: /unsplash ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -import UppyCdnExample from '/src/components/UppyCdnExample'; - -# Unsplash - -The `@uppy/unsplash` plugin lets users import files from their -[Unsplash](https://unsplash.com) account. - -:::tip - -[Try out the live example](/examples) or take it for a spin in -[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js). - -::: - -## When should I use this? - -When you want to let users import files from their -[Unsplash](https://unsplash.com) account. - -A [Companion](/docs/companion) instance is required for the Unsplash plugin to -work. Companion handles authentication with Unsplash, downloads the files, and -uploads them to the destination. This saves the user bandwidth, especially -helpful if they are on a mobile connection. - -You can self-host Companion or get a hosted version with any -[Transloadit plan](https://transloadit.com/pricing/). - - - - -```shell -npm install @uppy/unsplash -``` - - - - - -```shell -yarn add @uppy/unsplash -``` - - - - - - {` - import { Uppy, Unsplash } from "{{UPPY_JS_URL}}" - const uppy = new Uppy() - uppy.use(Unsplash, { - // Options - }) - `} - - - - -## Use - -Using Unsplash requires setup in both Uppy and Companion. - -### Use in Uppy - -```js {10-13} showLineNumbers -import Uppy from '@uppy/core'; -import Dashboard from '@uppy/dashboard'; -import Unsplash from '@uppy/unsplash'; - -import '@uppy/core/dist/style.min.css'; -import '@uppy/dashboard/dist/style.min.css'; - -new Uppy() - .use(Dashboard, { inline: true, target: '#dashboard' }) - .use(Unsplash, { companionUrl: 'https://your-companion.com' }); -``` - -### Use with Transloadit - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import Unsplash from '@uppy/unsplash'; - -uppy.use(Unsplash, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, -}); -``` - -You may also hit rate limits, because the OAuth application is shared between -everyone using Transloadit. - -To solve that, you can use your own OAuth keys with Transloadit’s hosted -Companion servers by using Transloadit Template Credentials. [Create a Template -Credential][template-credentials] on the Transloadit site. Select “Companion -OAuth” for the service, and enter the key and secret for the provider you want -to use. Then you can pass the name of the new credentials to that provider: - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import Unsplash from '@uppy/unsplash'; - -uppy.use(Unsplash, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, - companionKeysParams: { - key: 'YOUR_TRANSLOADIT_API_KEY', - credentialsName: 'my_companion_dropbox_creds', - }, -}); -``` - -### Use in Companion - -You can create a Unsplash App on the -[Unsplash Developers site](https://unsplash.com/developers). You’ll be -redirected to the app page, this page lists the app key and app secret. - -Configure the Unsplash key and secret. With the standalone Companion server, -specify environment variables: - -```shell -export COMPANION_UNSPLASH_KEY="Unsplash API key" -export COMPANION_UNSPLASH_SECRET="Unsplash API secret" -``` - -When using the Companion Node.js API, configure these options: - -```js -companion.app({ - providerOptions: { - unsplash: { - key: 'Unsplash API key', - secret: 'Unsplash API secret', - }, - }, -}); -``` - -## API - -### Options - -#### [Common Companion options...](./companion-options.mdx) - -[template-credentials]: - https://transloadit.com/docs/#how-to-create-template-credentials diff --git a/docs/sources/companion-plugins/url.mdx b/docs/sources/companion-plugins/url.mdx deleted file mode 100644 index de342af754..0000000000 --- a/docs/sources/companion-plugins/url.mdx +++ /dev/null @@ -1,139 +0,0 @@ ---- -slug: /url ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -import UppyCdnExample from '/src/components/UppyCdnExample'; - -# Import from URL - -The `@uppy/url` plugin allows users to import files from the internet. Paste any -URL and it will be added! - -:::tip - -[Try out the live example](/examples) or take it for a spin in -[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js). - -::: - -## When should I use this? - -When you want to let users import files any URL. - -A [Companion](/docs/companion) instance is required for the URL plugin to work. -This saves the user bandwidth, especially helpful if they are on a mobile -connection. - -You can self-host Companion or get a hosted version with any -[Transloadit plan](https://transloadit.com/pricing/). - -:::note - -Companion has -[Server Side Request Forgery](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery) -(SSRF) protections built-in so you don’t have to worry about the security -implications of arbitrary URLs. - -::: - - - - -```shell -npm install @uppy/url -``` - - - - - -```shell -yarn add @uppy/url -``` - - - - - - {` - import { Uppy, Url } from "{{UPPY_JS_URL}}" - const uppy = new Uppy() - uppy.use(Url, { - // Options - }) - `} - - - - -## Use - -Using `@uppy/url` only requires setup in Uppy. - -### Use in Uppy - -```js {10-13} showLineNumbers -import Uppy from '@uppy/core'; -import Dashboard from '@uppy/dashboard'; -import Url from '@uppy/url'; - -import '@uppy/core/dist/style.min.css'; -import '@uppy/dashboard/dist/style.min.css'; -import '@uppy/url/dist/style.min.css'; - -new Uppy() - .use(Dashboard, { inline: true, target: '#dashboard' }) - .use(Url, { companionUrl: 'https://your-companion.com' }); -``` - -### Use with Transloadit - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import Url from '@uppy/url'; - -uppy.use(Url, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, -}); -``` - -You may also hit rate limits, because the OAuth application is shared between -everyone using Transloadit. - -To solve that, you can use your own OAuth keys with Transloadit’s hosted -Companion servers by using Transloadit Template Credentials. [Create a Template -Credential][template-credentials] on the Transloadit site. Select “Companion -OAuth” for the service, and enter the key and secret for the provider you want -to use. Then you can pass the name of the new credentials to that provider: - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import Url from '@uppy/url'; - -uppy.use(Url, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, - companionKeysParams: { - key: 'YOUR_TRANSLOADIT_API_KEY', - credentialsName: 'my_companion_dropbox_creds', - }, -}); -``` - -### Use in Companion - -Companion supports this plugin out-of-the-box, however it must be enabled in -Companion with the `enableUrlEndpoint` / `COMPANION_ENABLE_URL_ENDPOINT` option. - -## API - -### Options - -#### [Common Companion options...](./companion-options.mdx) - -[template-credentials]: - https://transloadit.com/docs/#how-to-create-template-credentials diff --git a/docs/sources/companion-plugins/zoom.mdx b/docs/sources/companion-plugins/zoom.mdx deleted file mode 100644 index 5bac89dc52..0000000000 --- a/docs/sources/companion-plugins/zoom.mdx +++ /dev/null @@ -1,178 +0,0 @@ ---- -slug: /zoom ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -import UppyCdnExample from '/src/components/UppyCdnExample'; - -# Zoom - -The `@uppy/zoom` plugin lets users import cloud video recordings from their -[Zoom](https://zoom.com) account. Note that -[only licensed](https://support.zoom.com/hc/en/article?id=zm_kb&sysparm_article=KB0063923) -Zoom accounts can store their recordings in the cloud, so this functionality -will only be available to users with a paid Zoom account. - -:::tip - -[Try out the live example](/examples) or take it for a spin in -[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js). - -::: - -## When should I use this? - -When you want to let users import cloud video recordings from their -[Zoom](https://zoom.com) account. - -A [Companion](/docs/companion) instance is required for the Zoom plugin to work. -Companion handles authentication with Zoom, downloads the files, and uploads -them to the destination. This saves the user bandwidth, especially helpful if -they are on a mobile connection. - -You can self-host Companion or get a hosted version with any -[Transloadit plan](https://transloadit.com/pricing/). - - - - -```shell -npm install @uppy/zoom -``` - - - - - -```shell -yarn add @uppy/zoom -``` - - - - - - {` - import { Uppy, Zoom } from "{{UPPY_JS_URL}}" - const uppy = new Uppy() - uppy.use(Zoom, { - // Options - }) - `} - - - - -## Use - -Using Zoom requires setup in both Uppy and Companion. - -### Use in Uppy - -```js {10-13} showLineNumbers -import Uppy from '@uppy/core'; -import Dashboard from '@uppy/dashboard'; -import Zoom from '@uppy/zoom'; - -import '@uppy/core/dist/style.min.css'; -import '@uppy/dashboard/dist/style.min.css'; - -new Uppy() - .use(Dashboard, { inline: true, target: '#dashboard' }) - .use(Zoom, { companionUrl: 'https://your-companion.com' }); -``` - -### Use with Transloadit - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import Zoom from '@uppy/zoom'; - -uppy.use(Zoom, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, -}); -``` - -You may also hit rate limits, because the OAuth application is shared between -everyone using Transloadit. - -To solve that, you can use your own OAuth keys with Transloadit’s hosted -Companion servers by using Transloadit Template Credentials. [Create a Template -Credential][template-credentials] on the Transloadit site. Select “Companion -OAuth” for the service, and enter the key and secret for the provider you want -to use. Then you can pass the name of the new credentials to that provider: - -```js -import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit'; -import Zoom from '@uppy/zoom'; - -uppy.use(Zoom, { - companionUrl: COMPANION_URL, - companionAllowedHosts: COMPANION_ALLOWED_HOSTS, - companionKeysParams: { - key: 'YOUR_TRANSLOADIT_API_KEY', - credentialsName: 'my_companion_dropbox_creds', - }, -}); -``` - -### Use in Companion - -To sign up for API keys, go through the following steps: - -1. Sign up on [Zoom Marketplace](https://marketplace.zoom.us) - -2. Go to [https://marketplace.zoom.us](https://marketplace.zoom.us). There will - be a dropdown in the header called “Develop”. From that dropdown, select - “Build app”. - -3. In the “Basic Information” tab, Zoom shows your new “Client ID” and “Client - Secret” - copy them. - - With the standalone Companion server, specify environment variables: - - ```shell - export COMPANION_ZOOM_KEY="Zoom API key" - export COMPANION_ZOOM_SECRET="Zoom API secret" - ``` - - When using the Companion Node.js API, configure these options: - - ```js - companion.app({ - providerOptions: { - zoom: { - key: 'Zoom API key', - secret: 'Zoom API secret', - }, - }, - }); - ``` - -4. In the “Basic Information” tab, set “OAuth Redirect URL” input field to: - - ``` - https://$YOUR_COMPANION_HOST_NAME/zoom/redirect - ``` - - If you are using Transloadit hosted Companion: - - ``` - https://api2.transloadit.com/companion/zoom/redirect - ``` - -5. In the “Scopes” tab, add “cloud_recording:read:list_user_recordings” and - “user:read:user” scopes. If Zoom asks for further permissions when you - interact with your Zoom integration - add those too. - -## API - -### Options - -#### [Common Companion options...](./companion-options.mdx) - -[template-credentials]: - https://transloadit.com/docs/#how-to-create-template-credentials diff --git a/docs/user-interfaces/dashboard.mdx b/docs/user-interfaces/dashboard.mdx deleted file mode 100644 index 07d6d4fbd2..0000000000 --- a/docs/user-interfaces/dashboard.mdx +++ /dev/null @@ -1,759 +0,0 @@ ---- -sidebar_position: 1 -slug: /dashboard ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -import UppyCdnExample from '/src/components/UppyCdnExample'; - -# Dashboard - -The all you need Dashboard — powerful, responsive, and pluggable. Kickstart your -uploading experience and gradually add more functionality. Add files from -[remote sources](/docs/companion), [edit images](/docs/image-editor), -[generate thumbnails](/docs/thumbnail-generator), and more. - -Checkout [integrations](#integrations) for the full list of plugins you can -integrate. - -:::tip - -[Try out the live example with all plugins](/examples) or take it for a spin in -[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js). - -::: - -## When should I use this? - -There could be many reasons why you may want to use the Dashboard, but some -could be: - -- when you need a battle tested plug-and-play uploading UI to save time. -- when your users need to add files from [remote sources](/docs/companion), such - [Google Drive](/docs/google-drive), [Dropbox](/docs/dropbox), and others. -- when you need to collect [meta data](#metafields) from your users per file. -- when your users want to take a picture with their [webcam](/docs/webcam) or - [capture their screen](/docs/screen-capture). - -## Install - - - - -```shell -npm install @uppy/core @uppy/dashboard -``` - - - - - -```shell -yarn add @uppy/core @uppy/dashboard -``` - - - - - - {` - import { Uppy, Dashboard } from "{{UPPY_JS_URL}}" - const uppy = new Uppy() - uppy.use(Dashboard, { target: '#uppy', inline: true }) - `} - - - - -## Use - -```js showLineNumbers -import Uppy from '@uppy/core'; -import Dashboard from '@uppy/dashboard'; - -import '@uppy/core/dist/style.min.css'; -import '@uppy/dashboard/dist/style.min.css'; - -new Uppy().use(Dashboard, { inline: true, target: '#uppy-dashboard' }); -``` - -:::note - -The `@uppy/dashboard` plugin includes CSS for the Dashboard itself, and the -various plugins used by the Dashboard, such as -([`@uppy/status-bar`](/docs/status-bar) and [`@uppy/informer`](/docs/informer)). -If you also use the `@uppy/status-bar` or `@uppy/informer` plugin directly, you -should not include their CSS files, but instead only use the one from the -`@uppy/dashboard` plugin. - -::: - -:::note - -Styles for Provider plugins, like Google Drive and Instagram, are also bundled -with Dashboard styles. Styles for other plugins, such as `@uppy/url` and -`@uppy/webcam`, are not included. If you are using those, please see their docs -and make sure to include styles for them as well. - -::: - -## API - -### Options - -#### `id` - -A unique identifier for this plugin (`string`, default: `'Dashboard'`). - -Plugins that are added by the Dashboard get unique IDs based on this ID, like -`'Dashboard:StatusBar'` and `'Dashboard:Informer'`. - -#### `target` - -Where to render the Dashboard (`string` or `Element`, default: `'body'`). - -You can pass an element, class, or id as a string. Dashboard is rendered into -`body`, because it’s hidden by default and only opened as a modal when `trigger` -is clicked. - -#### `inline` - -Render the Dashboard as a modal or inline (`boolean`, default: `false`). - -When `false`, Dashboard is opened by clicking on [`trigger`](#trigger). If -`inline: true`, Dashboard will be rendered into [`target`](#target) and fit -right in. - -#### `trigger` - -A CSS selector for a button that will trigger opening the Dashboard modal -(`string`, default: `null`). - -Several buttons or links can be used, as long as they are selected using the -same selector (`.select-file-button`, for example). - -#### `width` - -Width of the Dashboard in pixels (`number`, default: `750`). Used when -`inline: true`. - -#### `height` - -Height of the Dashboard in pixels (`number`, default: `550`). Used when -`inline: true`. - -#### `waitForThumbnailsBeforeUpload` - -Whether to wait for all thumbnails from `@uppy/thumbnail-generator` to be ready -before starting the upload (`boolean`, default `false`). - -If set to `true`, Thumbnail Generator will envoke Uppy’s internal processing -stage, displaying “Generating thumbnails...” message, and wait for -`thumbnail:all-generated` event, before proceeding to the uploading stage. - -This is useful because Thumbnail Generator also adds EXIF data to images, and if -we wait until it’s done processing, this data will be available on the server -after the upload. - -#### `showLinkToFileUploadResult` - -Turn the file icon and thumbnail in the Dashboard into a link to the uploaded -file (`boolean`, default: `false`). - -Please make sure to return the `url` key (or the one set via -`responseUrlFieldName`) from your server. - -#### `showProgressDetails` - -Show or hide progress details in the status bar (`boolean`, default: `false`). - -By default, progress in Status Bar is shown as a percentage. If you would like -to also display remaining upload size and time, set this to `true`. - -`showProgressDetails: false`: Uploading: 45% - -`showProgressDetails: true`: Uploading: 45%・43 MB of 101 MB・8s left - -#### `hideUploadButton` - -Show or hide the upload button (`boolean`, default: `false`). - -Use this if you are providing a custom upload button somewhere, and are using -the `uppy.upload()` API. - -#### `hideRetryButton` - -Hide the retry button in the status bar and on each individual file (`boolean`, -default: `false`). - -Use this if you are providing a custom retry button somewhere and if you are -using the `uppy.retryAll()` or `uppy.retryUpload(fileID)` API. - -#### `hidePauseResumeButton` - -Hide the pause/resume button (for resumable uploads, via [tus](http://tus.io), -for example) in the status bar and on each individual file (`boolean`, default: -`false`). - -Use this if you are providing custom cancel or pause/resume buttons somewhere, -and using the `uppy.pauseResume(fileID)` or `uppy.removeFile(fileID)` API. - -#### `hideCancelButton` - -Hide the cancel button in status bar and on each individual file (`boolean`, -default: `false`). - -Use this if you are providing a custom retry button somewhere, and using the -`uppy.cancelAll()` API. - -#### `hideProgressAfterFinish` - -Hide the status bar after the upload has finished (`boolean`, default: `false`). - -#### `doneButtonHandler` - -This option is passed to the status bar and will render a “Done” button in place -of pause/resume/cancel buttons, once the upload/encoding is done. The behaviour -of this “Done” button is defined by this handler function, for instance to close -the file picker modals or clear the upload state. - -This is what the Dashboard sets by default: - -```js -const doneButtonHandler = () => { - this.uppy.cancelAll(); - this.requestCloseModal(); -}; -``` - -Set to `null` to disable the “Done” button. - -#### `showSelectedFiles` - -Show the list of added files with a preview and file information (`boolean`, -default: `true`). - -In case you are showing selected files in your own app’s UI and want the Uppy -Dashboard to only be a picker, the list can be hidden with this option. - -See also [`disableStatusBar`](#disablestatusbar) option, which can hide the -progress and upload button. - -#### `showRemoveButtonAfterComplete` - -Show the remove button on every file after a successful upload (`boolean`, -default: `false`). - -Enabling this option only shows the remove `X` button in the Dashboard UI, but -to actually send a request you should listen to -[`file-removed`](https://uppy.io/docs/uppy/#file-removed) event and add your -logic there. - -Example: - -```js -uppy.on('file-removed', (file, reason) => { - if (reason === 'removed-by-user') { - sendDeleteRequestForFile(file); - } -}); -``` - -For an implementation example, please see -[#2301](https://github.com/transloadit/uppy/issues/2301#issue-628931176). - -#### `singleFileFullScreen` - -When only one file is selected, its preview and meta information will be -centered and enlarged (`boolean`, default: `true`). - -Often times Uppy used for photo / profile image uploads, or maybe a single -document. Then it makes sense to occupy the whole space of the available -Dashboard UI, giving the stage to this one file. This feature is automatically -disabled when Dashboard is small in height, since there’s not enough room. - -#### `note` - -A string of text to be placed in the Dashboard UI (`string`, default: `null`). - -This could for instance be used to explain any [`restrictions`](#restrictions) -that are put in place. For example: -`'Images and video only, 2–3 files, up to 1 MB'`. - -#### `metaFields` - -Create text or custom input fields for the user to fill in (`Array` or -`Function`, default: `null`). - -This will be shown when a user clicks the “edit” button on that file. - -:::note - -The meta data will only be set on a file object if it’s entered by the user. If -the user doesn’t edit a file’s metadata, it will not have default values; -instead everything will be `undefined`. If you want to set a certain meta field -to each file regardless of user actions, set -[`meta` in the Uppy constructor options](/docs/uppy/#meta). - -::: - -Each object can contain: - -- `id`. The name of the meta field. This will also be used in CSS/HTML as part - of the `id` attribute, so it’s better to - [avoid using characters like periods, semicolons, etc](https://stackoverflow.com/a/79022). -- `name`. The label shown in the interface. -- `placeholder`. The text shown when no value is set in the field. (Not needed - when a custom render function is provided) -- `render: ({value, onChange, required, form}, h) => void` (optional). A - function for rendering a custom form element. - - `value` is the current value of the meta field - - `onChange: (newVal) => void` is a function saving the new value and `h` is - the `createElement` function from - [Preact](https://preactjs.com/guide/v10/api-reference#h--createelement). - - `required` is a boolean that’s true if the field `id` is in the - `restrictedMetaFields` restriction - - `form` is the `id` of the associated `
` element. - - `h` can be useful when using Uppy from plain JavaScript, where you cannot - write JSX. - -
-Example: meta fields configured as an `Array` - -```js -uppy.use(Dashboard, { - trigger: '#pick-files', - metaFields: [ - { id: 'name', name: 'Name', placeholder: 'file name' }, - { id: 'license', name: 'License', placeholder: 'specify license' }, - { - id: 'caption', - name: 'Caption', - placeholder: 'describe what the image is about', - }, - { - id: 'public', - name: 'Public', - render({ value, onChange, required, form }, h) { - return h('input', { - type: 'checkbox', - required, - form, - onChange: (ev) => onChange(ev.target.checked ? 'on' : ''), - defaultChecked: value === 'on', - }); - }, - }, - ], -}); -``` - -
- -
-Example: dynamic meta fields based on file type with a `Function` - -```js -uppy.use(Dashboard, { - trigger: '#pick-files', - metaFields: (file) => { - const fields = [{ id: 'name', name: 'File name' }]; - if (file.type.startsWith('image/')) { - fields.push({ id: 'location', name: 'Photo Location' }); - fields.push({ id: 'alt', name: 'Alt text' }); - fields.push({ - id: 'public', - name: 'Public', - render: ({ value, onChange, required, form }, h) => { - return h('input', { - type: 'checkbox', - onChange: (ev) => onChange(ev.target.checked ? 'on' : ''), - defaultChecked: value === 'on', - required, - form, - }); - }, - }); - } - return fields; - }, -}); -``` - -
- -#### `closeModalOnClickOutside` - -Set to true to automatically close the modal when the user clicks outside of it -(`boolean`, default: `false`). - -#### `closeAfterFinish` - -Set to true to automatically close the modal when all current uploads are -complete (`boolean`, default: `false`). - -With this option, the modal is only automatically closed when uploads are -complete _and successful_. If some uploads failed, the modal stays open so the -user can retry failed uploads or cancel the current batch and upload an entirely -different set of files instead. - -:::info - -You can use this together with the -[`allowMultipleUploads: false`](/docs/uppy/#allowmultipleuploads) option in Uppy -Core to create a smooth experience when uploading a single (batch of) file(s). - -This is recommended. With several upload batches, the auto-closing behavior can -be quite confusing for users. - -::: - -#### `disablePageScrollWhenModalOpen` - -Disable page scroll when the modal is open (`boolean`, default: `true`). - -Page scrolling is disabled by default when the Dashboard modal is open, so when -you scroll a list of files in Uppy, the website in the background stays still. -Set to false to override this behaviour and leave page scrolling intact. - -#### `animateOpenClose` - -Add animations when the modal dialog is opened or closed, for a more satisfying -user experience (`boolean`, default: `true`). - -#### `fileManagerSelectionType` - -Configure the type of selections allowed when browsing your file system via the -file manager selection window (`string`, default: `'files'`). - -May be either `'files'`, `'folders'`, or `'both'`. Selecting entire folders for -upload may not be supported on all -[browsers](https://caniuse.com/#feat=input-file-directory). - -#### `proudlyDisplayPoweredByUppy` - -Show the Uppy logo with a link (`boolean`, default: `true`). - -Uppy is provided to the world for free by the team behind -[Transloadit](https://transloadit.com). In return, we ask that you consider -keeping a tiny Uppy logo at the bottom of the Dashboard, so that more people can -discover and use Uppy. - -#### `disableStatusBar` - -Disable the status bar completely (`boolean`, default: `false`). - -Dashboard ships with the `StatusBar` plugin that shows upload progress and -pause/resume/cancel buttons. If you want, you can disable the StatusBar to -provide your own custom solution. - -#### `disableInformer` - -Disable informer (shows notifications in the form of toasts) completely -(`boolean`, default: `false`). - -Dashboard ships with the `Informer` plugin that notifies when the browser is -offline, or when it’s time to say cheese if `Webcam` is taking a picture. If you -want, you can disable the Informer and/or provide your own custom solution. - -#### `disableThumbnailGenerator` - -Disable the thumbnail generator completely (`boolean`, default: `false`). - -Dashboard ships with the `ThumbnailGenerator` plugin that adds small resized -image thumbnails to images, for preview purposes only. If you want, you can -disable the `ThumbnailGenerator` and/or provide your own custom solution. - -#### `locale` - -```js -module.exports = { - strings: { - // When `inline: false`, used as the screen reader label for the button that closes the modal. - closeModal: 'Close Modal', - // Used as the screen reader label for the plus (+) button that shows the “Add more files” screen - addMoreFiles: 'Add more files', - addingMoreFiles: 'Adding more files', - // Used as the header for import panels, e.g., “Import from Google Drive”. - importFrom: 'Import from %{name}', - // When `inline: false`, used as the screen reader label for the dashboard modal. - dashboardWindowTitle: 'Uppy Dashboard Window (Press escape to close)', - // When `inline: true`, used as the screen reader label for the dashboard area. - dashboardTitle: 'Uppy Dashboard', - // Shown in the Informer when a link to a file was copied to the clipboard. - copyLinkToClipboardSuccess: 'Link copied to clipboard.', - // Used when a link cannot be copied automatically — the user has to select the text from the - // input element below this string. - copyLinkToClipboardFallback: 'Copy the URL below', - // Used as the hover title and screen reader label for buttons that copy a file link. - copyLink: 'Copy link', - back: 'Back', - // Used as the screen reader label for buttons that remove a file. - removeFile: 'Remove file', - // Used as the screen reader label for buttons that open the metadata editor panel for a file. - editFile: 'Edit file', - // Shown in the panel header for the metadata editor. Rendered as “Editing image.png”. - editing: 'Editing %{file}', - // Used as the screen reader label for the button that saves metadata edits and returns to the - // file list view. - finishEditingFile: 'Finish editing file', - saveChanges: 'Save changes', - // Used as the label for the tab button that opens the system file selection dialog. - myDevice: 'My Device', - dropHint: 'Drop your files here', - // Used as the hover text and screen reader label for file progress indicators when - // they have been fully uploaded. - uploadComplete: 'Upload complete', - uploadPaused: 'Upload paused', - // Used as the hover text and screen reader label for the buttons to resume paused uploads. - resumeUpload: 'Resume upload', - // Used as the hover text and screen reader label for the buttons to pause uploads. - pauseUpload: 'Pause upload', - // Used as the hover text and screen reader label for the buttons to retry failed uploads. - retryUpload: 'Retry upload', - // Used as the hover text and screen reader label for the buttons to cancel uploads. - cancelUpload: 'Cancel upload', - // Used in a title, how many files are currently selected - xFilesSelected: { - 0: '%{smart_count} file selected', - 1: '%{smart_count} files selected', - }, - uploadingXFiles: { - 0: 'Uploading %{smart_count} file', - 1: 'Uploading %{smart_count} files', - }, - processingXFiles: { - 0: 'Processing %{smart_count} file', - 1: 'Processing %{smart_count} files', - }, - // The "powered by Uppy" link at the bottom of the Dashboard. - poweredBy: 'Powered by %{uppy}', - addMore: 'Add more', - editFileWithFilename: 'Edit file %{file}', - save: 'Save', - cancel: 'Cancel', - dropPasteFiles: 'Drop files here or %{browseFiles}', - dropPasteFolders: 'Drop files here or %{browseFolders}', - dropPasteBoth: 'Drop files here, %{browseFiles} or %{browseFolders}', - dropPasteImportFiles: 'Drop files here, %{browseFiles} or import from:', - dropPasteImportFolders: 'Drop files here, %{browseFolders} or import from:', - dropPasteImportBoth: - 'Drop files here, %{browseFiles}, %{browseFolders} or import from:', - importFiles: 'Import files from:', - browseFiles: 'browse files', - browseFolders: 'browse folders', - recoveredXFiles: { - 0: 'We could not fully recover 1 file. Please re-select it and resume the upload.', - 1: 'We could not fully recover %{smart_count} files. Please re-select them and resume the upload.', - }, - recoveredAllFiles: 'We restored all files. You can now resume the upload.', - sessionRestored: 'Session restored', - reSelect: 'Re-select', - missingRequiredMetaFields: { - 0: 'Missing required meta field: %{fields}.', - 1: 'Missing required meta fields: %{fields}.', - }, - }, -}; -``` - -#### `theme` - -Light or dark theme for the Dashboard (`string`, default: `'light'`). - -Uppy Dashboard supports “Dark Mode”. You can try it live on -[the Dashboard example page](https://uppy.io/examples/). - -It supports the following values: - -- `light` — the default -- `dark` -- `auto` — will respect the user’s system settings and switch automatically - -#### `autoOpen` - -Automatically open file editor for the file user just dropped/selected. -If one file is added, editor opens for that file; if 10 files are added, editor -opens only for the first file. - -This option supports the following values: - -- `null` - the default -- `"metaEditor"` - open the meta fields editor if - [meta fields](/docs/dashboard/#metafields) are enabled. -- `"imageEditor"` - open [`@uppy/image-editor`](/docs/image-editor) if the - plugin is enabled. - -#### `disabled` - -Enabling this option makes the Dashboard grayed-out and non-interactive -(`boolean`, default: `false`). - -Users won’t be able to click on buttons or drop files. Useful when you need to -conditionally enable/disable file uploading or manipulation, based on a -condition in your app. Can be set on init or via API: - -```js -const dashboard = uppy.getPlugin('Dashboard'); -dashboard.setOptions({ disabled: true }); - -userNameInput.addEventListener('change', () => { - dashboard.setOptions({ disabled: false }); -}); -``` - -#### `disableLocalFiles` - -Disable local files (`boolean`, default: `false`). - -Enabling this option will disable drag & drop, hide the “browse” and “My Device” -button, allowing only uploads from plugins, such as Webcam, Screen Capture, -Google Drive, Instagram. - -#### `onDragOver(event)` - -Callback for the [`ondragover`][ondragover] event handler. - -#### `onDrop(event)` - -Callback for the [`ondrop`][ondrop] event handler. - -#### `onDragLeave(event)` - -Callback for the [`ondragleave`][ondragleave] event handler. - -### Methods - -:::info - -Dashboard also has the methods described in -[`UIPlugin`](/docs/uppy#new-uipluginuppy-options) and -[`BasePlugin`](/docs/uppy#new-basepluginuppy-options). - -::: - -#### `openModal()` - -Shows the Dashboard modal. Use it like this: - -`uppy.getPlugin('Dashboard').openModal()` - -#### `closeModal()` - -Hides the Dashboard modal. Use it like this: - -`uppy.getPlugin('Dashboard').closeModal()` - -#### `isModalOpen()` - -Returns `true` if the Dashboard modal is open, `false` otherwise. - -```js -const dashboard = uppy.getPlugin('Dashboard'); -if (dashboard.isModalOpen()) { - dashboard.closeModal(); -} -``` - -### Events - -:::info - -You can use [`on`](/docs/uppy#onevent-action) and -[`once`](/docs/uppy#onceevent-action) to listen to these events. - -::: - -#### `dashboard:modal-open` - -Fired when the Dashboard modal is open. - -```js -uppy.on('dashboard:modal-open', () => { - console.log('Modal is open'); -}); -``` - -#### `dashboard:modal-closed` - -Fired when the Dashboard modal is closed. - -#### `dashboard:file-edit-start` - -**Parameters:** - -- `file` — The [File Object](https://uppy.io/docs/uppy/#File-Objects) - representing the file that was opened for editing. - -Fired when the user clicks “edit” icon next to a file in the Dashboard. The -FileCard panel is then open with file metadata available for editing. - -#### `dashboard:file-edit-complete` - -**Parameters:** - -- `file` — The [File Object](https://uppy.io/docs/uppy/#File-Objects) - representing the file that was edited. - -Fired when the user finished editing the file metadata. - -## Integrations - -These are the plugins specifically made for the Dashboard. This is not a list of -all Uppy plugins. - -### Sources - -- [`@uppy/audio`](/docs/audio) — record audio. -- [`@uppy/box`](/docs/box) — import files from - [Box](https://www.box.com/en-nl/home). -- [`@uppy/dropbox`](/docs/dropbox) — import from [Dropbox](https://dropbox.com). -- [`@uppy/facebook`](/docs/facebook) — import from - [Facebook](https://facebook.com). -- [`@uppy/google-drive`](/docs/google-drive) — import from - [Google Drive](https://drive.google.com). -- [`@uppy/google-photos`](/docs/google-photos) — import from - [Google Photos](https://photos.google.com). -- [`@uppy/google-drive-picker`](/docs/google-drive-picker) — import from - [Google Drive](https://drive.google.com) using the new Picker API. -- [`@uppy/google-photos-picker`](/docs/google-photos-picker) — import from - [Google Photos](https://drive.google.com) using the new Picker API. -- [`@uppy/instagram`](/docs/instagram) — import from - [Instagram](https://instagram.com). -- [`@uppy/onedrive`](/docs/onedrive) — import from - [OneDrive](https://www.microsoft.com/en-us/microsoft-365/onedrive/online-cloud-storage). -- [`@uppy/screen-capture`](/docs/screen-capture) — Record your screen, including - (optionally) your microphone. -- [`@uppy/unsplash`](/docs/unsplash) — import files from - [Unsplash](https://unsplash.com/) -- [`@uppy/url`](/docs/url) — import files from any URL. -- [`@uppy/webcam`](/docs/webcam) — Record or make a picture with your webcam. -- [`@uppy/zoom`](/docs/zoom) — import files from [Zoom](https://zoom.us). - -### UI - -- [`@uppy/image-editor`](/docs/image-editor) — allows users to crop, rotate, - zoom and flip images that are added to Uppy. -- [`@uppy/informer`](/docs/informer) — show notifications. -- [`@uppy/status-bar`](/docs/status-bar) — advanced upload progress status bar. -- [`@uppy/thumbnail-generator`](/docs/thumbnail-generator) — generate preview - thumbnails for images to be uploaded. - -### Frameworks - -- [`@uppy/angular`](/docs/angular) — Dashboard component for - [Angular](https://angular.io/). -- [`@uppy/react`](/docs/react) — Dashboard component for - [React](https://reactjs.org/). -- [`@uppy/svelte`](/docs/svelte) — Dashboard component for - [Svelte](https://svelte.dev/). -- [`@uppy/vue`](/docs/vue) — Dashboard component for [Vue](https://vuejs.org/). - -[ondragover]: - https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/ondragover -[ondragleave]: - https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/ondragleave -[ondrop]: - https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/ondrop From 1f711c14c464b3b0a9214aacc836a18cf8fb805f Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Tue, 26 Nov 2024 18:37:28 +0800 Subject: [PATCH 16/19] improve auth ui --- .../src/GoogleDrivePicker.tsx | 40 +---------- .../src/GooglePhotosPicker.tsx | 35 +--------- .../src/GooglePicker/GooglePickerView.tsx | 36 +++++++--- .../provider-views/src/GooglePicker/icons.tsx | 70 +++++++++++++++++++ 4 files changed, 102 insertions(+), 79 deletions(-) create mode 100644 packages/@uppy/provider-views/src/GooglePicker/icons.tsx diff --git a/packages/@uppy/google-drive-picker/src/GoogleDrivePicker.tsx b/packages/@uppy/google-drive-picker/src/GoogleDrivePicker.tsx index e2313710eb..b8d10ec932 100644 --- a/packages/@uppy/google-drive-picker/src/GoogleDrivePicker.tsx +++ b/packages/@uppy/google-drive-picker/src/GoogleDrivePicker.tsx @@ -1,6 +1,7 @@ import { h } from 'preact' import { UIPlugin, Uppy } from '@uppy/core' import { GooglePickerView } from '@uppy/provider-views' +import { GoogleDriveIcon } from '@uppy/provider-views/src/GooglePicker/icons.js' import { RequestClient, type CompanionPluginOptions, @@ -16,43 +17,6 @@ import type { AsyncStore, BaseProviderPlugin } from '@uppy/core/lib/Uppy.js' import packageJson from '../package.json' import locale from './locale.ts' -const Icon = () => ( - -) - export type GoogleDrivePickerOptions = CompanionPluginOptions & { clientId: string apiKey: string @@ -72,7 +36,7 @@ export default class GoogleDrivePicker< type = 'acquirer' - icon = Icon + icon = GoogleDriveIcon storage: AsyncStore diff --git a/packages/@uppy/google-photos-picker/src/GooglePhotosPicker.tsx b/packages/@uppy/google-photos-picker/src/GooglePhotosPicker.tsx index 29240f1c96..1889a758af 100644 --- a/packages/@uppy/google-photos-picker/src/GooglePhotosPicker.tsx +++ b/packages/@uppy/google-photos-picker/src/GooglePhotosPicker.tsx @@ -1,6 +1,7 @@ import { h } from 'preact' import { UIPlugin, Uppy } from '@uppy/core' import { GooglePickerView } from '@uppy/provider-views' +import { GooglePhotosIcon } from '@uppy/provider-views/src/GooglePicker/icons.js' import { RequestClient, type CompanionPluginOptions, @@ -16,38 +17,6 @@ import type { AsyncStore, BaseProviderPlugin } from '@uppy/core/lib/Uppy.js' import packageJson from '../package.json' import locale from './locale.ts' -const Icon = () => ( - -) - export type GooglePhotosPickerOptions = CompanionPluginOptions & { clientId: string } @@ -65,7 +34,7 @@ export default class GooglePhotosPicker< type = 'acquirer' - icon = Icon + icon = GooglePhotosIcon storage: AsyncStore diff --git a/packages/@uppy/provider-views/src/GooglePicker/GooglePickerView.tsx b/packages/@uppy/provider-views/src/GooglePicker/GooglePickerView.tsx index f92b3f497a..57e94552db 100644 --- a/packages/@uppy/provider-views/src/GooglePicker/GooglePickerView.tsx +++ b/packages/@uppy/provider-views/src/GooglePicker/GooglePickerView.tsx @@ -1,5 +1,4 @@ import { h } from 'preact' -import { Fragment } from 'preact/compat' import { useCallback, useEffect, useRef, useState } from 'preact/hooks' import type { Uppy } from '@uppy/core' @@ -17,6 +16,8 @@ import { type PickedItem, type PickingSession, } from './googlePicker.js' +import AuthView from '../ProviderView/AuthView.js' +import { GoogleDriveIcon, GooglePhotosIcon } from './icons.js' export type GooglePickerViewProps = { uppy: Uppy @@ -189,22 +190,41 @@ export default function GooglePickerView({ if (accessToken == null) { return ( - + ) } return ( - <> - - - + ) } diff --git a/packages/@uppy/provider-views/src/GooglePicker/icons.tsx b/packages/@uppy/provider-views/src/GooglePicker/icons.tsx new file mode 100644 index 0000000000..0a83b53679 --- /dev/null +++ b/packages/@uppy/provider-views/src/GooglePicker/icons.tsx @@ -0,0 +1,70 @@ +import { h } from 'preact' + +export const GooglePhotosIcon = () => ( + +) + +export const GoogleDriveIcon = () => ( + +) From 6ab2a995abd86171221065366a51795875aa00bd Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Tue, 26 Nov 2024 18:40:55 +0800 Subject: [PATCH 17/19] fix bug --- .../provider-views/src/GooglePicker/GooglePickerView.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/@uppy/provider-views/src/GooglePicker/GooglePickerView.tsx b/packages/@uppy/provider-views/src/GooglePicker/GooglePickerView.tsx index 57e94552db..72fc10dd47 100644 --- a/packages/@uppy/provider-views/src/GooglePicker/GooglePickerView.tsx +++ b/packages/@uppy/provider-views/src/GooglePicker/GooglePickerView.tsx @@ -72,7 +72,6 @@ export default function GooglePickerView({ const showPicker = useCallback( async (signal?: AbortSignal) => { - shownPickerRef.current = true let newAccessToken = accessToken const doShowPicker = async (token: string) => { @@ -105,6 +104,7 @@ export default function GooglePickerView({ if (newAccessToken == null) throw new Error() await doShowPicker(newAccessToken) + shownPickerRef.current = true setAccessToken(newAccessToken) } catch (err) { if (err instanceof InvalidTokenError) { @@ -116,6 +116,7 @@ export default function GooglePickerView({ }) // now try again: await doShowPicker(newAccessToken) + shownPickerRef.current = true setAccessToken(newAccessToken) } else { throw err @@ -173,7 +174,10 @@ export default function GooglePickerView({ showPicker(abortController.signal) - return () => abortController.abort() + return () => { + // only abort the picker if it's not yet shown + if (!shownPickerRef.current) abortController.abort() + } }, [accessToken, showPicker]) const handleLogoutClick = useCallback(async () => { From abb7261d87cc6f91ce255b44a32b697cf1d952d8 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Tue, 26 Nov 2024 18:48:49 +0800 Subject: [PATCH 18/19] remove unused useUppyState --- packages/@uppy/core/src/useUppyState.ts | 43 ------------------------- 1 file changed, 43 deletions(-) delete mode 100644 packages/@uppy/core/src/useUppyState.ts diff --git a/packages/@uppy/core/src/useUppyState.ts b/packages/@uppy/core/src/useUppyState.ts deleted file mode 100644 index 35518d1921..0000000000 --- a/packages/@uppy/core/src/useUppyState.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { Body, Meta } from '@uppy/utils/lib/UppyFile' -import { useMemo, useCallback } from 'preact/hooks' -import { useSyncExternalStore } from 'preact/compat' - -import type { PluginOpts } from './BasePlugin' -import type BasePlugin from './BasePlugin' -import type { State, Uppy } from './Uppy' - -// todo merge with @uppy/react? -export function useUppyState< - M extends Meta = Meta, - B extends Body = Body, - T = any, ->(uppy: Uppy, selector: (state: State) => T): T { - const subscribe = useMemo( - () => uppy.store.subscribe.bind(uppy.store), - [uppy.store], - ) - const getSnapshot = useCallback(() => uppy.store.getState(), [uppy.store]) - - return selector(useSyncExternalStore(subscribe, getSnapshot)) -} - -export function useUppyPluginState< - PS extends Record, - O extends PluginOpts, - M extends Meta = Meta, - B extends Body = Body, ->( - plugin: BasePlugin, -): [Partial, (...args: Parameters) => void] { - const setPluginState = useCallback( - (...args) => plugin.setPluginState(...args), - [plugin], - ) - return [ - useUppyState( - plugin.uppy, - (state) => (state.plugins[plugin.id] ?? {}) as PS, - ), - setPluginState, - ] -} From 988cc2d0b20d78fee9b8a607627f1188e0dd159a Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Tue, 26 Nov 2024 18:52:41 +0800 Subject: [PATCH 19/19] try to fix build error --- packages/@uppy/google-drive-picker/src/GoogleDrivePicker.tsx | 2 +- packages/@uppy/google-photos-picker/src/GooglePhotosPicker.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@uppy/google-drive-picker/src/GoogleDrivePicker.tsx b/packages/@uppy/google-drive-picker/src/GoogleDrivePicker.tsx index b8d10ec932..e8bcac0ea6 100644 --- a/packages/@uppy/google-drive-picker/src/GoogleDrivePicker.tsx +++ b/packages/@uppy/google-drive-picker/src/GoogleDrivePicker.tsx @@ -1,7 +1,7 @@ import { h } from 'preact' import { UIPlugin, Uppy } from '@uppy/core' import { GooglePickerView } from '@uppy/provider-views' -import { GoogleDriveIcon } from '@uppy/provider-views/src/GooglePicker/icons.js' +import { GoogleDriveIcon } from '@uppy/provider-views/lib/GooglePicker/icons.js' import { RequestClient, type CompanionPluginOptions, diff --git a/packages/@uppy/google-photos-picker/src/GooglePhotosPicker.tsx b/packages/@uppy/google-photos-picker/src/GooglePhotosPicker.tsx index 1889a758af..111a1864cd 100644 --- a/packages/@uppy/google-photos-picker/src/GooglePhotosPicker.tsx +++ b/packages/@uppy/google-photos-picker/src/GooglePhotosPicker.tsx @@ -1,7 +1,7 @@ import { h } from 'preact' import { UIPlugin, Uppy } from '@uppy/core' import { GooglePickerView } from '@uppy/provider-views' -import { GooglePhotosIcon } from '@uppy/provider-views/src/GooglePicker/icons.js' +import { GooglePhotosIcon } from '@uppy/provider-views/lib/GooglePicker/icons.js' import { RequestClient, type CompanionPluginOptions,