From 34dbf6c1cecab7f8ad97d026d608eb53c3b51ad9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 10:25:35 +0200 Subject: [PATCH 1/6] Bump docker/build-push-action from 5.3.0 to 5.4.0 (#5252) --- .github/workflows/companion-deploy.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/companion-deploy.yml b/.github/workflows/companion-deploy.yml index a8c7aab090..4d62cc0ee2 100644 --- a/.github/workflows/companion-deploy.yml +++ b/.github/workflows/companion-deploy.yml @@ -63,7 +63,7 @@ jobs: username: ${{secrets.DOCKER_USERNAME}} password: ${{secrets.DOCKER_PASSWORD}} - name: Build and push - uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5.4.0 with: push: true context: . diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1c22ec21f4..a32911383c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -159,7 +159,7 @@ jobs: username: ${{secrets.DOCKER_USERNAME}} password: ${{secrets.DOCKER_PASSWORD}} - name: Build and push - uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5.4.0 with: push: true context: . From 61223e9feb657fac71b83c39fe2e1795b532f6b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 10:26:05 +0200 Subject: [PATCH 2/6] Bump docker/login-action from 3.1.0 to 3.2.0 (#5217) --- .github/workflows/companion-deploy.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/companion-deploy.yml b/.github/workflows/companion-deploy.yml index 4d62cc0ee2..f66a05800c 100644 --- a/.github/workflows/companion-deploy.yml +++ b/.github/workflows/companion-deploy.yml @@ -58,7 +58,7 @@ jobs: - uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 - uses: docker/setup-buildx-action@v3 - name: Log in to DockerHub - uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0 + uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0 with: username: ${{secrets.DOCKER_USERNAME}} password: ${{secrets.DOCKER_PASSWORD}} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a32911383c..7097cc138f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -154,7 +154,7 @@ jobs: - uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 - uses: docker/setup-buildx-action@v3 - name: Log in to DockerHub - uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0 + uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0 with: username: ${{secrets.DOCKER_USERNAME}} password: ${{secrets.DOCKER_PASSWORD}} From a5bc28acda283e7644a9f3d2c4f026246930f900 Mon Sep 17 00:00:00 2001 From: Mitchell Rhoads Date: Mon, 17 Jun 2024 01:26:44 -0700 Subject: [PATCH 3/6] updating aws-nodejs example listParts logic for resuming uploads (#5192) Co-authored-by: Mitchell Rhoads Co-authored-by: Antoine du Hamel --- examples/aws-nodejs/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/aws-nodejs/index.js b/examples/aws-nodejs/index.js index 2f13cc770d..c067565d50 100644 --- a/examples/aws-nodejs/index.js +++ b/examples/aws-nodejs/index.js @@ -202,12 +202,12 @@ app.get('/s3/multipart/:uploadId', (req, res, next) => { const parts = [] - function listPartsPage (startAt) { + function listPartsPage(startsAt = undefined) { client.send(new ListPartsCommand({ Bucket: process.env.COMPANION_AWS_BUCKET, Key: key, UploadId: uploadId, - PartNumberMarker: startAt, + PartNumberMarker: startsAt, }), (err, data) => { if (err) { next(err) @@ -216,15 +216,15 @@ app.get('/s3/multipart/:uploadId', (req, res, next) => { parts.push(...data.Parts) + // continue to get list of all uploaded parts until the IsTruncated flag is false if (data.IsTruncated) { - // Get the next page. listPartsPage(data.NextPartNumberMarker) } else { res.json(parts) } }) } - listPartsPage(0) + listPartsPage() }) function isValidPart (part) { From e6674a1eee8c61308df71bba412715a7f9586397 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Tue, 18 Jun 2024 11:13:23 +0200 Subject: [PATCH 4/6] @uppy/google-photos: add plugin (#5061) --- .eslintrc.js | 1 + docs/companion.md | 25 +-- docs/guides/migration-guides.md | 17 +- .../companion-plugins/google-drive.mdx | 16 +- .../companion-plugins/google-photos.mdx | 185 ++++++++++++++++++ docs/sources/companion-plugins/instagram.mdx | 2 +- docs/uploader/transloadit.mdx | 4 +- docs/user-interfaces/dashboard.mdx | 2 + .../integration/dashboard-transloadit.spec.ts | 2 +- e2e/package.json | 1 + examples/angular-example/package.json | 1 + packages/@uppy/box/src/Box.tsx | 1 + packages/@uppy/companion/src/config/grant.js | 39 ++-- .../companion/src/server/controllers/get.js | 5 +- .../companion/src/server/controllers/url.js | 8 +- .../src/server/helpers/oauth-state.js | 4 +- .../companion/src/server/helpers/upload.js | 16 +- .../companion/src/server/helpers/utils.js | 7 +- .../provider/{ => google}/drive/adapter.js | 0 .../provider/{ => google}/drive/index.js | 67 ++----- .../provider/google/googlephotos/index.js | 172 ++++++++++++++++ .../src/server/provider/google/index.js | 36 ++++ .../companion/src/server/provider/index.js | 5 +- .../src/server/provider/providerErrors.js | 15 +- .../@uppy/companion/src/standalone/helper.js | 5 + .../companion/test/__tests__/companion.js | 28 +-- .../test/__tests__/provider-manager.js | 46 ++++- .../companion/test/__tests__/providers.js | 144 ++++++++++---- .../@uppy/companion/test/fixtures/drive.js | 2 +- packages/@uppy/core/src/locale.ts | 1 + .../dashboard/src/utils/copyToClipboard.ts | 6 +- packages/@uppy/dropbox/src/Dropbox.tsx | 1 + .../@uppy/google-drive/src/GoogleDrive.tsx | 1 + packages/@uppy/google-photos/.npmignore | 1 + packages/@uppy/google-photos/CHANGELOG.md | 1 + packages/@uppy/google-photos/LICENSE | 21 ++ packages/@uppy/google-photos/README.md | 51 +++++ packages/@uppy/google-photos/package.json | 33 ++++ .../@uppy/google-photos/src/GooglePhotos.tsx | 135 +++++++++++++ packages/@uppy/google-photos/src/index.ts | 1 + packages/@uppy/google-photos/src/locale.ts | 5 + .../@uppy/google-photos/tsconfig.build.json | 35 ++++ packages/@uppy/google-photos/tsconfig.json | 31 +++ packages/@uppy/google-photos/types/index.d.ts | 17 ++ .../@uppy/google-photos/types/index.test-d.ts | 12 ++ packages/@uppy/onedrive/src/OneDrive.tsx | 1 + packages/@uppy/provider-views/src/Browser.tsx | 9 +- .../src/Item/components/ListLi.tsx | 4 +- .../src/ProviderView/ProviderView.tsx | 3 + packages/@uppy/provider-views/src/View.ts | 7 +- packages/@uppy/react/types/index.test-d.tsx | 4 + packages/@uppy/remote-sources/package.json | 2 + .../@uppy/remote-sources/src/index.test.ts | 2 +- packages/@uppy/remote-sources/src/index.ts | 2 + .../@uppy/remote-sources/tsconfig.build.json | 5 + packages/@uppy/remote-sources/tsconfig.json | 5 + packages/@uppy/transloadit/src/index.ts | 1 + packages/uppy/index.mjs | 1 + packages/uppy/package.json | 1 + packages/uppy/types/index.d.ts | 1 + private/dev/Dashboard.js | 2 +- yarn.lock | 17 ++ 62 files changed, 1091 insertions(+), 184 deletions(-) create mode 100644 docs/sources/companion-plugins/google-photos.mdx rename packages/@uppy/companion/src/server/provider/{ => google}/drive/adapter.js (100%) rename packages/@uppy/companion/src/server/provider/{ => google}/drive/index.js (75%) create mode 100644 packages/@uppy/companion/src/server/provider/google/googlephotos/index.js create mode 100644 packages/@uppy/companion/src/server/provider/google/index.js create mode 100644 packages/@uppy/google-photos/.npmignore create mode 100644 packages/@uppy/google-photos/CHANGELOG.md create mode 100644 packages/@uppy/google-photos/LICENSE create mode 100644 packages/@uppy/google-photos/README.md create mode 100644 packages/@uppy/google-photos/package.json create mode 100644 packages/@uppy/google-photos/src/GooglePhotos.tsx create mode 100644 packages/@uppy/google-photos/src/index.ts create mode 100644 packages/@uppy/google-photos/src/locale.ts create mode 100644 packages/@uppy/google-photos/tsconfig.build.json create mode 100644 packages/@uppy/google-photos/tsconfig.json create mode 100644 packages/@uppy/google-photos/types/index.d.ts create mode 100644 packages/@uppy/google-photos/types/index.test-d.ts diff --git a/.eslintrc.js b/.eslintrc.js index c47834e6ea..151b4c4f90 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -222,6 +222,7 @@ module.exports = { 'packages/@uppy/form/src/**/*.js', 'packages/@uppy/golden-retriever/src/**/*.js', 'packages/@uppy/google-drive/src/**/*.js', + 'packages/@uppy/google-photos/src/**/*.js', 'packages/@uppy/image-editor/src/**/*.js', 'packages/@uppy/informer/src/**/*.js', 'packages/@uppy/instagram/src/**/*.js', diff --git a/docs/companion.md b/docs/companion.md index 15ca91cacd..8bd6462a39 100644 --- a/docs/companion.md +++ b/docs/companion.md @@ -22,8 +22,9 @@ OAuth. ## When should I use it? If you want to let users download files from [Box][], [Dropbox][], [Facebook][], -[Google Drive][googledrive], [Instagram][], [OneDrive][], [Unsplash][], [Import -from URL][url], or [Zoom][] — you need Companion. +[Google Drive][googledrive], [Google Photos][googlephotos], [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). @@ -435,15 +436,16 @@ 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` | -| 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` | +| 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` @@ -918,6 +920,7 @@ when files are changed. [dropbox]: /docs/dropbox [facebook]: /docs/facebook [googledrive]: /docs/google-drive +[googlephotos]: /docs/google-photos [instagram]: /docs/instagram [onedrive]: /docs/onedrive [unsplash]: /docs/unsplash diff --git a/docs/guides/migration-guides.md b/docs/guides/migration-guides.md index 4413da8fea..c0697070f9 100644 --- a/docs/guides/migration-guides.md +++ b/docs/guides/migration-guides.md @@ -626,14 +626,15 @@ to:
-| Provider | New Redirect URI | -| ------------ | ------------------------------------------------- | -| Dropbox | `https://$COMPANION_HOST_NAME/dropbox/redirect` | -| Google Drive | `https://$COMPANION_HOST_NAME/drive/redirect` | -| OneDrive | `https://$COMPANION_HOST_NAME/onedrive/redirect` | -| Box | `https://$YOUR_COMPANION_HOST_NAME/box/redirect` | -| Facebook | `https://$COMPANION_HOST_NAME/facebook/redirect` | -| Instagram | `https://$COMPANION_HOST_NAME/instagram/redirect` | +| Provider | New Redirect URI | +| ------------- | ---------------------------------------------------- | +| Dropbox | `https://$COMPANION_HOST_NAME/dropbox/redirect` | +| Google Drive | `https://$COMPANION_HOST_NAME/drive/redirect` | +| Google Photos | `https://$COMPANION_HOST_NAME/googlephotos/redirect` | +| OneDrive | `https://$COMPANION_HOST_NAME/onedrive/redirect` | +| Box | `https://$YOUR_COMPANION_HOST_NAME/box/redirect` | +| Facebook | `https://$COMPANION_HOST_NAME/facebook/redirect` | +| Instagram | `https://$COMPANION_HOST_NAME/instagram/redirect` |
diff --git a/docs/sources/companion-plugins/google-drive.mdx b/docs/sources/companion-plugins/google-drive.mdx index 3685c7ddc9..b7332e5c56 100644 --- a/docs/sources/companion-plugins/google-drive.mdx +++ b/docs/sources/companion-plugins/google-drive.mdx @@ -10,7 +10,7 @@ import UppyCdnExample from '/src/components/UppyCdnExample'; # Google Drive The `@uppy/google-drive` plugin lets users import files from their -[Google Drive](https://www.drive.google.com) account. +[Google Drive](https://drive.google.com) account. :::tip @@ -22,7 +22,7 @@ The `@uppy/google-drive` plugin lets users import files from their ## When should I use this? When you want to let users import files from their -[Google Drive](https://www.drive.google.com) account. +[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 @@ -112,12 +112,12 @@ https://api2.transloadit.com/companion/drive/redirect Google will give you an OAuth client ID and client secret. -Configure the Google Drive key and secret in Companion. With the standalone -Companion server, specify environment variables: +Configure the Google key and secret in Companion. With the standalone Companion +server, specify environment variables: ```shell -export COMPANION_GOOGLE_KEY="Google Drive OAuth client ID" -export COMPANION_GOOGLE_SECRET="Google Drive OAuth client secret" +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: @@ -126,8 +126,8 @@ When using the Companion Node.js API, configure these options: companion.app({ providerOptions: { drive: { - key: 'Google Drive OAuth client ID', - secret: 'Google Drive OAuth client secret', + key: 'Google OAuth client ID', + secret: 'Google OAuth client secret', }, }, }); diff --git a/docs/sources/companion-plugins/google-photos.mdx b/docs/sources/companion-plugins/google-photos.mdx new file mode 100644 index 0000000000..23d92928a3 --- /dev/null +++ b/docs/sources/companion-plugins/google-photos.mdx @@ -0,0 +1,185 @@ +--- +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 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 + +#### `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', + }, +}; +``` diff --git a/docs/sources/companion-plugins/instagram.mdx b/docs/sources/companion-plugins/instagram.mdx index 358f184940..dbda298e72 100644 --- a/docs/sources/companion-plugins/instagram.mdx +++ b/docs/sources/companion-plugins/instagram.mdx @@ -115,7 +115,7 @@ When using the Companion Node.js API, configure these options: ```js companion.app({ providerOptions: { - drive: { + instagram: { key: 'Instagram OAuth client ID', secret: 'Instagram OAuth client secret', }, diff --git a/docs/uploader/transloadit.mdx b/docs/uploader/transloadit.mdx index 43502a7e9d..a772555798 100644 --- a/docs/uploader/transloadit.mdx +++ b/docs/uploader/transloadit.mdx @@ -100,8 +100,8 @@ uppy.on('transloadit:complete', (assembly) => {}); :::note -All [Transloadit plans](https://transloadit.com/pricing/) come with a hosted version -of Companion. +All [Transloadit plans](https://transloadit.com/pricing/) come with a hosted +version of Companion. ::: diff --git a/docs/user-interfaces/dashboard.mdx b/docs/user-interfaces/dashboard.mdx index 79ed5c39c5..340efbd4e4 100644 --- a/docs/user-interfaces/dashboard.mdx +++ b/docs/user-interfaces/dashboard.mdx @@ -714,6 +714,8 @@ all Uppy plugins. [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/instagram`](/docs/instagram) — import from [Instagram](https://instagram.com). - [`@uppy/onedrive`](/docs/onedrive) — import from diff --git a/e2e/cypress/integration/dashboard-transloadit.spec.ts b/e2e/cypress/integration/dashboard-transloadit.spec.ts index d541e35b0b..68c781b549 100644 --- a/e2e/cypress/integration/dashboard-transloadit.spec.ts +++ b/e2e/cypress/integration/dashboard-transloadit.spec.ts @@ -258,7 +258,7 @@ describe('Dashboard with Transloadit', () => { client_ip: null, client_referer: null, transloadit_client: - 'uppy-core:3.2.0,uppy-transloadit:3.1.3,uppy-tus:3.1.0,uppy-dropbox:3.1.1,uppy-box:2.1.1,uppy-facebook:3.1.1,uppy-google-drive:3.1.1,uppy-instagram:3.1.1,uppy-onedrive:3.1.1,uppy-zoom:2.1.1,uppy-url:3.3.1', + 'uppy-core:3.2.0,uppy-transloadit:3.1.3,uppy-tus:3.1.0,uppy-dropbox:3.1.1,uppy-box:2.1.1,uppy-facebook:3.1.1,uppy-google-drive:3.1.1,uppy-google-photos:0.0.1,uppy-instagram:3.1.1,uppy-onedrive:3.1.1,uppy-zoom:2.1.1,uppy-url:3.3.1', start_date: new Date().toISOString(), upload_meta_data_extracted: false, warnings: [], diff --git a/e2e/package.json b/e2e/package.json index 89a004d932..b9ec22401c 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -25,6 +25,7 @@ "@uppy/form": "workspace:^", "@uppy/golden-retriever": "workspace:^", "@uppy/google-drive": "workspace:^", + "@uppy/google-photos": "workspace:^", "@uppy/image-editor": "workspace:^", "@uppy/informer": "workspace:^", "@uppy/instagram": "workspace:^", diff --git a/examples/angular-example/package.json b/examples/angular-example/package.json index a7eb2a3872..6e6d7947ea 100644 --- a/examples/angular-example/package.json +++ b/examples/angular-example/package.json @@ -23,6 +23,7 @@ "@uppy/core": "workspace:*", "@uppy/drag-drop": "workspace:*", "@uppy/google-drive": "workspace:*", + "@uppy/google-photos": "workspace:*", "@uppy/progress-bar": "workspace:*", "@uppy/tus": "workspace:*", "@uppy/webcam": "workspace:*", diff --git a/packages/@uppy/box/src/Box.tsx b/packages/@uppy/box/src/Box.tsx index d6739c853c..68c9d72bd4 100644 --- a/packages/@uppy/box/src/Box.tsx +++ b/packages/@uppy/box/src/Box.tsx @@ -84,6 +84,7 @@ export default class Box extends UIPlugin< this.view = new ProviderViews(this, { provider: this.provider, loadAllFiles: true, + virtualList: true, }) const { target } = this.opts diff --git a/packages/@uppy/companion/src/config/grant.js b/packages/@uppy/companion/src/config/grant.js index 39f80b8328..ad4983c24d 100644 --- a/packages/@uppy/companion/src/config/grant.js +++ b/packages/@uppy/companion/src/config/grant.js @@ -1,19 +1,34 @@ +const google = { + transport: 'session', + + // access_type: offline is needed in order to get refresh tokens. + // prompt: 'consent' is needed because sometimes a user will get stuck in an authenticated state where we will + // receive no refresh tokens from them. This seems to be happen when running on different subdomains. + // therefore to be safe that we always get refresh tokens, we set this. + // https://stackoverflow.com/questions/10827920/not-receiving-google-oauth-refresh-token/65108513#65108513 + custom_params: { access_type : 'offline', prompt: 'consent' }, + + // copied from https://github.com/simov/grant/blob/master/config/oauth.json + "authorize_url": "https://accounts.google.com/o/oauth2/v2/auth", + "access_url": "https://oauth2.googleapis.com/token", + "oauth": 2, + "scope_delimiter": " " +} + // oauth configuration for provider services that are used. module.exports = () => { return { - // for drive - google: { - transport: 'session', - scope: [ - 'https://www.googleapis.com/auth/drive.readonly', - ], + // we need separate auth providers because scopes are different, + // and because it would be a too big rewrite to allow reuse of the same provider. + googledrive: { + ...google, callback: '/drive/callback', - // access_type: offline is needed in order to get refresh tokens. - // prompt: 'consent' is needed because sometimes a user will get stuck in an authenticated state where we will - // receive no refresh tokens from them. This seems to be happen when running on different subdomains. - // therefore to be safe that we always get refresh tokens, we set this. - // https://stackoverflow.com/questions/10827920/not-receiving-google-oauth-refresh-token/65108513#65108513 - custom_params: { access_type : 'offline', prompt: 'consent' }, + scope: ['https://www.googleapis.com/auth/drive.readonly'], + }, + googlephotos: { + ...google, + callback: '/googlephotos/callback', + scope: ['https://www.googleapis.com/auth/photoslibrary.readonly', 'https://www.googleapis.com/auth/userinfo.email'], // if name is needed, then add https://www.googleapis.com/auth/userinfo.profile too }, dropbox: { transport: 'session', diff --git a/packages/@uppy/companion/src/server/controllers/get.js b/packages/@uppy/companion/src/server/controllers/get.js index e3bd4da759..1ba7f916ae 100644 --- a/packages/@uppy/companion/src/server/controllers/get.js +++ b/packages/@uppy/companion/src/server/controllers/get.js @@ -11,10 +11,7 @@ async function get (req, res) { return provider.size({ id, token: accessToken, query: req.query }) } - async function download () { - const { stream } = await provider.download({ id, token: accessToken, providerUserSession, query: req.query }) - return stream - } + const download = () => provider.download({ id, token: accessToken, providerUserSession, query: req.query }) try { await startDownUpload({ req, res, getSize, download }) diff --git a/packages/@uppy/companion/src/server/controllers/url.js b/packages/@uppy/companion/src/server/controllers/url.js index 0a2a3b0bde..a401e24545 100644 --- a/packages/@uppy/companion/src/server/controllers/url.js +++ b/packages/@uppy/companion/src/server/controllers/url.js @@ -27,8 +27,8 @@ const downloadURL = async (url, blockLocalIPs, traceId) => { try { const protectedGot = getProtectedGot({ blockLocalIPs }) const stream = protectedGot.stream.get(url, { responseType: 'json' }) - await prepareStream(stream) - return stream + const { size } = await prepareStream(stream) + return { stream, size } } catch (err) { logger.error(err, 'controller.url.download.error', traceId) throw err @@ -79,9 +79,7 @@ const get = async (req, res) => { return size } - async function download () { - return downloadURL(req.body.url, !allowLocalUrls, req.id) - } + const download = () => downloadURL(req.body.url, !allowLocalUrls, req.id) try { await startDownUpload({ req, res, getSize, download }) diff --git a/packages/@uppy/companion/src/server/helpers/oauth-state.js b/packages/@uppy/companion/src/server/helpers/oauth-state.js index 4a7d1a9b9c..023ed423b3 100644 --- a/packages/@uppy/companion/src/server/helpers/oauth-state.js +++ b/packages/@uppy/companion/src/server/helpers/oauth-state.js @@ -7,7 +7,7 @@ module.exports.encodeState = (state, secret) => { return encrypt(encodedState, secret) } -const decodeState = (state, secret) => { +module.exports.decodeState = (state, secret) => { const encodedState = decrypt(state, secret) return JSON.parse(atob(encodedState)) } @@ -19,7 +19,7 @@ module.exports.generateState = () => { } module.exports.getFromState = (state, name, secret) => { - return decodeState(state, secret)[name] + return module.exports.decodeState(state, secret)[name] } module.exports.getGrantDynamicFromRequest = (req) => { diff --git a/packages/@uppy/companion/src/server/helpers/upload.js b/packages/@uppy/companion/src/server/helpers/upload.js index 5f637c43bf..510ac48f2b 100644 --- a/packages/@uppy/companion/src/server/helpers/upload.js +++ b/packages/@uppy/companion/src/server/helpers/upload.js @@ -4,15 +4,23 @@ const { respondWithError } = require('../provider/error') async function startDownUpload({ req, res, getSize, download }) { try { - const size = await getSize() + logger.debug('Starting download stream.', null, req.id) + const { stream, size: maybeSize } = await download() + + let size + // if the provider already knows the size, we can use that + if (typeof maybeSize === 'number' && !Number.isNaN(maybeSize) && maybeSize > 0) { + size = maybeSize + } + // if not we need to get the size + if (size == null) { + size = await getSize() + } const { clientSocketConnectTimeout } = req.companion.options logger.debug('Instantiating uploader.', null, req.id) const uploader = new Uploader(Uploader.reqToOptions(req, size)) - logger.debug('Starting download stream.', null, req.id) - const stream = await download() - // "Forking" off the upload operation to background, so we can return the http request: ; (async () => { // wait till the client has connected to the socket, before starting diff --git a/packages/@uppy/companion/src/server/helpers/utils.js b/packages/@uppy/companion/src/server/helpers/utils.js index 627d87fcb1..cedc77176e 100644 --- a/packages/@uppy/companion/src/server/helpers/utils.js +++ b/packages/@uppy/companion/src/server/helpers/utils.js @@ -165,11 +165,14 @@ module.exports.StreamHttpJsonError = StreamHttpJsonError module.exports.prepareStream = async (stream) => new Promise((resolve, reject) => { stream - .on('response', () => { + .on('response', (response) => { + const contentLengthStr = response.headers['content-length'] + const contentLength = parseInt(contentLengthStr, 10); + const size = !Number.isNaN(contentLength) && contentLength >= 0 ? contentLength : undefined; // Don't allow any more data to flow yet. // https://github.com/request/request/issues/1990#issuecomment-184712275 stream.pause() - resolve() + resolve({ size }) }) .on('error', (err) => { // In this case the error object is not a normal GOT HTTPError where json is already parsed, diff --git a/packages/@uppy/companion/src/server/provider/drive/adapter.js b/packages/@uppy/companion/src/server/provider/google/drive/adapter.js similarity index 100% rename from packages/@uppy/companion/src/server/provider/drive/adapter.js rename to packages/@uppy/companion/src/server/provider/google/drive/adapter.js diff --git a/packages/@uppy/companion/src/server/provider/drive/index.js b/packages/@uppy/companion/src/server/provider/google/drive/index.js similarity index 75% rename from packages/@uppy/companion/src/server/provider/drive/index.js rename to packages/@uppy/companion/src/server/provider/google/drive/index.js index e17e067c5e..c752f295dc 100644 --- a/packages/@uppy/companion/src/server/provider/drive/index.js +++ b/packages/@uppy/companion/src/server/provider/google/drive/index.js @@ -1,12 +1,13 @@ const got = require('got').default -const Provider = require('../Provider') -const logger = require('../../logger') +const { logout, refreshToken } = require('../index') +const logger = require('../../../logger') const { VIRTUAL_SHARED_DIR, adaptData, isShortcut, isGsuiteFile, getGsuiteExportType } = require('./adapter') -const { withProviderErrorHandling } = require('../providerErrors') -const { prepareStream } = require('../../helpers/utils') -const { MAX_AGE_REFRESH_TOKEN } = require('../../helpers/jwt') -const { ProviderAuthError } = require('../error') +const { prepareStream } = require('../../../helpers/utils') +const { MAX_AGE_REFRESH_TOKEN } = require('../../../helpers/jwt') +const { ProviderAuthError } = require('../../error') +const { withGoogleErrorHandling } = require('../../providerErrors') +const Provider = require('../../Provider') // For testing refresh token: @@ -29,10 +30,6 @@ const getClient = ({ token }) => got.extend({ }, }) -const getOauthClient = () => got.extend({ - prefixUrl: 'https://oauth2.googleapis.com', -}) - async function getStats ({ id, token }) { const client = getClient({ token }) @@ -52,15 +49,16 @@ async function getStats ({ id, token }) { */ class Drive extends Provider { static get authProvider () { - return 'google' + return 'googledrive' } static get authStateExpiry () { return MAX_AGE_REFRESH_TOKEN } + // eslint-disable-next-line class-methods-use-this async list (options) { - return this.#withErrorHandling('provider.drive.list.error', async () => { + return withGoogleErrorHandling(Drive.authProvider, 'provider.drive.list.error', async () => { const directory = options.directory || 'root' const query = options.query || {} const { token } = options @@ -126,6 +124,7 @@ class Drive extends Provider { }) } + // eslint-disable-next-line class-methods-use-this async download ({ id: idIn, token }) { if (mockAccessTokenExpiredError != null) { logger.warn(`Access token: ${token}`) @@ -136,7 +135,7 @@ class Drive extends Provider { } } - return this.#withErrorHandling('provider.drive.download.error', async () => { + return withGoogleErrorHandling(Drive.authProvider, 'provider.drive.download.error', async () => { const client = getClient({ token }) const { mimeType, id, exportLinks } = await getStats({ id: idIn, token }) @@ -172,14 +171,8 @@ class Drive extends Provider { } // eslint-disable-next-line class-methods-use-this - async thumbnail () { - // not implementing this because a public thumbnail from googledrive will be used instead - logger.error('call to thumbnail is not implemented', 'provider.drive.thumbnail.error') - throw new Error('call to thumbnail is not implemented') - } - async size ({ id, token }) { - return this.#withErrorHandling('provider.drive.size.error', async () => { + return withGoogleErrorHandling(Drive.authProvider, 'provider.drive.size.error', async () => { const { mimeType, size } = await getStats({ id, token }) if (isGsuiteFile(mimeType)) { @@ -192,37 +185,15 @@ class Drive extends Provider { }) } - logout ({ token }) { - return this.#withErrorHandling('provider.drive.logout.error', async () => { - await got.post('https://accounts.google.com/o/oauth2/revoke', { - searchParams: { token }, - responseType: 'json', - }) - - return { revoked: true } - }) - } - - async refreshToken ({ clientId, clientSecret, refreshToken }) { - return this.#withErrorHandling('provider.drive.token.refresh.error', async () => { - const { access_token: accessToken } = await getOauthClient().post('token', { responseType: 'json', form: { refresh_token: refreshToken, grant_type: 'refresh_token', client_id: clientId, client_secret: clientSecret } }).json() - return { accessToken } - }) + // eslint-disable-next-line class-methods-use-this + async logout(...args) { + return logout(...args) } // eslint-disable-next-line class-methods-use-this - async #withErrorHandling (tag, fn) { - return withProviderErrorHandling({ - fn, - tag, - providerName: Drive.authProvider, - isAuthError: (response) => ( - response.statusCode === 401 - || (response.statusCode === 400 && response.body?.error === 'invalid_grant') // Refresh token has expired or been revoked - ), - getJsonErrorMessage: (body) => body?.error?.message, - }) - } } +Drive.prototype.logout = logout +Drive.prototype.refreshToken = refreshToken + module.exports = Drive diff --git a/packages/@uppy/companion/src/server/provider/google/googlephotos/index.js b/packages/@uppy/companion/src/server/provider/google/googlephotos/index.js new file mode 100644 index 0000000000..6b213ee201 --- /dev/null +++ b/packages/@uppy/companion/src/server/provider/google/googlephotos/index.js @@ -0,0 +1,172 @@ +const got = require('got').default + +const { logout, refreshToken } = require('../index') +const { withGoogleErrorHandling } = require('../../providerErrors') +const { prepareStream } = require('../../../helpers/utils') +const { MAX_AGE_REFRESH_TOKEN } = require('../../../helpers/jwt') +const logger = require('../../../logger') +const Provider = require('../../Provider') + + +const getBaseClient = ({ token }) => got.extend({ + headers: { + authorization: `Bearer ${token}`, + }, +}) + +const getPhotosClient = ({ token }) => getBaseClient({ token }).extend({ + prefixUrl: 'https://photoslibrary.googleapis.com/v1', +}) + +const getOauthClient = ({ token }) => getBaseClient({ token }).extend({ + prefixUrl: 'https://www.googleapis.com/oauth2/v1', +}) + +async function paginate(fn, getter, limit = 5) { + const items = [] + let pageToken + + for (let i = 0; (i === 0 || pageToken != null); i++) { + if (i >= limit) { + logger.warn(`Hit pagination limit of ${limit}`) + break; + } + const response = await fn(pageToken); + items.push(...getter(response)); + pageToken = response.nextPageToken + } + return items +} + +/** + * Provider for Google Photos API + */ +class GooglePhotos extends Provider { + static get authProvider () { + return 'googlephotos' + } + + static get authStateExpiry () { + return MAX_AGE_REFRESH_TOKEN + } + + // eslint-disable-next-line class-methods-use-this + async list (options) { + return withGoogleErrorHandling(GooglePhotos.authProvider, 'provider.photos.list.error', async () => { + const { directory, query } = options + const { token } = options + + const isRoot = !directory + + const client = getPhotosClient({ token }) + + + async function fetchAlbums () { + if (!isRoot) return [] // albums are only in the root + + return paginate( + (pageToken) => client.get('albums', { searchParams: { pageToken, pageSize: 50 }, responseType: 'json' }).json(), + (response) => response.albums, + ) + } + + async function fetchSharedAlbums () { + if (!isRoot) return [] // albums are only in the root + + return paginate( + (pageToken) => client.get('sharedAlbums', { searchParams: { pageToken, pageSize: 50 }, responseType: 'json' }).json(), + (response) => response.sharedAlbums ?? [], // seems to be undefined if no shared albums + ) + } + + async function fetchMediaItems () { + if (isRoot) return { mediaItems: [] } // no images in root (album list only) + const resp = await client.post('mediaItems:search', { json: { pageToken: query?.cursor, albumId: directory, pageSize: 50 }, responseType: 'json' }).json(); + return resp + } + + const [sharedAlbums, albums, { mediaItems, nextPageToken }] = await Promise.all([ + fetchSharedAlbums(), fetchAlbums(), fetchMediaItems() + ]) + + const newSp = new URLSearchParams(Object.entries(query)); + if (nextPageToken) newSp.set('cursor', nextPageToken); + + const iconSize = 64 + const thumbSize = 300 + const getIcon = (baseUrl) => `${baseUrl}=w${iconSize}-h${iconSize}-c` + const getThumbnail = (baseUrl) => `${baseUrl}=w${thumbSize}-h${thumbSize}-c` + const adaptedItems = [ + ...albums.map((album) => ({ + isFolder: true, + icon: 'https://drive-thirdparty.googleusercontent.com/32/type/application/vnd.google-apps.folder', + mimeType: 'application/vnd.google-apps.folder', + thumbnail: getThumbnail(album.coverPhotoBaseUrl), + name: album.title, + id: album.id, + requestPath: album.id, + })), + ...sharedAlbums.map((sharedAlbum) => ({ + isFolder: true, + icon: 'https://drive-thirdparty.googleusercontent.com/32/type/application/vnd.google-apps.folder', + mimeType: 'application/vnd.google-apps.folder', + thumbnail: getThumbnail(sharedAlbum.coverPhotoBaseUrl), + name: sharedAlbum.title, + id: sharedAlbum.id, + requestPath: sharedAlbum.id, + })), + ...mediaItems.map((mediaItem) => ({ + isFolder: false, + icon: getIcon(mediaItem.baseUrl), + thumbnail: getThumbnail(mediaItem.baseUrl), + name: mediaItem.filename, + id: mediaItem.id, + mimeType: mediaItem.mimeType, + modifiedDate: mediaItem.creationTime, + requestPath: mediaItem.id, + custom: { + imageWidth: mediaItem.photo ? mediaItem.width : undefined, + imageHeight: mediaItem.photo ? mediaItem.height : undefined, + videoWidth: mediaItem.video ? mediaItem.width : undefined, + videoHeight: mediaItem.video ? mediaItem.height : undefined, + }, + })), + ]; + + const { email: username } = await getOauthClient({ token }).get('userinfo').json() + + return { + username, + items: adaptedItems, + nextPagePath: newSp.size > 0 ? `${directory ?? ''}?${newSp.toString()}` : null, + } + }) + } + + // eslint-disable-next-line class-methods-use-this + async download ({ id, token }) { + return withGoogleErrorHandling(GooglePhotos.authProvider, 'provider.photos.download.error', async () => { + const client = getPhotosClient({ token }) + + const { baseUrl } = await client.get(`mediaItems/${encodeURIComponent(id)}`, { responseType: 'json' }).json() + + const url = `${baseUrl}=d`; + const stream = got.stream.get(url, { responseType: 'json' }) + const { size } = await prepareStream(stream) + + return { stream, size } + }) + } + + // eslint-disable-next-line class-methods-use-this + async logout(...args) { + return logout(...args) + } + + // eslint-disable-next-line class-methods-use-this + async refreshToken(...args) { + return refreshToken(...args) + } +} + +module.exports = GooglePhotos diff --git a/packages/@uppy/companion/src/server/provider/google/index.js b/packages/@uppy/companion/src/server/provider/google/index.js new file mode 100644 index 0000000000..e946c7cd04 --- /dev/null +++ b/packages/@uppy/companion/src/server/provider/google/index.js @@ -0,0 +1,36 @@ +const got = require('got').default + + +const { withGoogleErrorHandling } = require('../providerErrors') + + +/** + * Reusable google stuff + */ + +const getOauthClient = () => got.extend({ + prefixUrl: 'https://oauth2.googleapis.com', +}) + +async function refreshToken({ clientId, clientSecret, refreshToken: theRefreshToken }) { + return withGoogleErrorHandling('google', 'provider.google.token.refresh.error', async () => { + const { access_token: accessToken } = await getOauthClient().post('token', { responseType: 'json', form: { refresh_token: theRefreshToken, grant_type: 'refresh_token', client_id: clientId, client_secret: clientSecret } }).json() + return { accessToken } + }) +} + +async function logout({ token }) { + return withGoogleErrorHandling('google', 'provider.google.logout.error', async () => { + await got.post('https://accounts.google.com/o/oauth2/revoke', { + searchParams: { token }, + responseType: 'json', + }) + + return { revoked: true } + }) +} + +module.exports = { + refreshToken, + logout, +} diff --git a/packages/@uppy/companion/src/server/provider/index.js b/packages/@uppy/companion/src/server/provider/index.js index fb7d4b1b59..fe2316cdec 100644 --- a/packages/@uppy/companion/src/server/provider/index.js +++ b/packages/@uppy/companion/src/server/provider/index.js @@ -3,7 +3,8 @@ */ const dropbox = require('./dropbox') const box = require('./box') -const drive = require('./drive') +const drive = require('./google/drive') +const googlephotos = require('./google/googlephotos') const instagram = require('./instagram/graph') const facebook = require('./facebook') const onedrive = require('./onedrive') @@ -66,7 +67,7 @@ module.exports.getProviderMiddleware = (providers, grantConfig) => { * @returns {Record} */ module.exports.getDefaultProviders = () => { - const providers = { dropbox, box, drive, facebook, onedrive, zoom, instagram, unsplash } + const providers = { dropbox, box, drive, googlephotos, facebook, onedrive, zoom, instagram, unsplash } return providers } diff --git a/packages/@uppy/companion/src/server/provider/providerErrors.js b/packages/@uppy/companion/src/server/provider/providerErrors.js index 715de19592..0f4f9f30f3 100644 --- a/packages/@uppy/companion/src/server/provider/providerErrors.js +++ b/packages/@uppy/companion/src/server/provider/providerErrors.js @@ -68,4 +68,17 @@ async function withProviderErrorHandling({ } } -module.exports = { withProviderErrorHandling } +async function withGoogleErrorHandling (providerName, tag, fn) { + return withProviderErrorHandling({ + fn, + tag, + providerName, + isAuthError: (response) => ( + response.statusCode === 401 + || (response.statusCode === 400 && response.body?.error === 'invalid_grant') // Refresh token has expired or been revoked + ), + getJsonErrorMessage: (body) => body?.error?.message, + }) +} + +module.exports = { withProviderErrorHandling, withGoogleErrorHandling } diff --git a/packages/@uppy/companion/src/standalone/helper.js b/packages/@uppy/companion/src/standalone/helper.js index 1a77119e16..e8d7d7c0b9 100644 --- a/packages/@uppy/companion/src/standalone/helper.js +++ b/packages/@uppy/companion/src/standalone/helper.js @@ -81,6 +81,11 @@ const getConfigFromEnv = () => { secret: getSecret('COMPANION_GOOGLE_SECRET'), credentialsURL: process.env.COMPANION_GOOGLE_KEYS_ENDPOINT, }, + googlephotos: { + key: process.env.COMPANION_GOOGLE_KEY, + secret: getSecret('COMPANION_GOOGLE_SECRET'), + credentialsURL: process.env.COMPANION_GOOGLE_KEYS_ENDPOINT, + }, dropbox: { key: process.env.COMPANION_DROPBOX_KEY, secret: getSecret('COMPANION_DROPBOX_SECRET'), diff --git a/packages/@uppy/companion/test/__tests__/companion.js b/packages/@uppy/companion/test/__tests__/companion.js index 5a9ed228b2..71ca178c62 100644 --- a/packages/@uppy/companion/test/__tests__/companion.js +++ b/packages/@uppy/companion/test/__tests__/companion.js @@ -19,10 +19,10 @@ jest.mock('node:dns', () => { return { ...actual, lookup: (hostname, options, callback) => { - if (fakeLocalhost === hostname) { + if (fakeLocalhost === hostname || hostname === 'localhost') { return callback(null, '127.0.0.1', 4) } - return actual.lookup(hostname, options, callback) + return callback(new Error(`Unexpected call to hostname ${hostname}`)) }, } }) @@ -52,7 +52,7 @@ describe('validate upload data', () => { mimeType: 'video/mp4', id: defaults.ITEM_ID, } - nock('https://www.googleapis.com').get(`/drive/v3/files/${defaults.ITEM_ID}`).query(() => true).times(2).reply(200, meta) + nock('https://www.googleapis.com').get(`/drive/v3/files/${defaults.ITEM_ID}`).query(() => true).reply(200, meta) nock('https://www.googleapis.com').get(`/drive/v3/files/${defaults.ITEM_ID}?alt=media&supportsAllDrives=true`).reply(401, { "error": { @@ -155,7 +155,7 @@ describe('validate upload data', () => { }) test('valid upload data is allowed - tus', () => { - nockGoogleDownloadFile({ times: 2 }) + nockGoogleDownloadFile() return request(authServer) .post('/drive/get/DUMMY-FILE-ID') @@ -177,7 +177,7 @@ describe('validate upload data', () => { }) test('valid upload data is allowed - s3-multipart', () => { - nockGoogleDownloadFile({ times: 2 }) + nockGoogleDownloadFile() return request(authServer) .post('/drive/get/DUMMY-FILE-ID') @@ -268,12 +268,16 @@ it('respects allowLocalUrls, localhost', async () => { expect(res.body).toEqual({ error: 'Invalid request body' }) }) -it('respects allowLocalUrls, valid hostname that resolves to localhost', async () => { - let res = await runUrlMetaTest(`http://${fakeLocalhost}/`) - expect(res.statusCode).toBe(500) - expect(res.body).toEqual({ message: 'failed to fetch URL metadata' }) +describe('respects allowLocalUrls, valid hostname that resolves to localhost', () => { + test('meta', async () => { + const res = await runUrlMetaTest(`http://${fakeLocalhost}/`) + expect(res.statusCode).toBe(500) + expect(res.body).toEqual({ message: 'failed to fetch URL metadata' }) + }) - res = await runUrlGetTest(`http://${fakeLocalhost}/`) - expect(res.statusCode).toBe(500) - expect(res.body).toEqual({ message: 'failed to fetch URL' }) + test('get', async () => { + const res = await runUrlGetTest(`http://${fakeLocalhost}/`) + expect(res.statusCode).toBe(500) + expect(res.body).toEqual({ message: 'failed to fetch URL' }) + }) }) diff --git a/packages/@uppy/companion/test/__tests__/provider-manager.js b/packages/@uppy/companion/test/__tests__/provider-manager.js index 084e2ec9f1..1f21129e7b 100644 --- a/packages/@uppy/companion/test/__tests__/provider-manager.js +++ b/packages/@uppy/companion/test/__tests__/provider-manager.js @@ -23,8 +23,11 @@ describe('Test Provider options', () => { expect(grantConfig.box.key).toBe('box_key') expect(grantConfig.box.secret).toBe('box_secret') - expect(grantConfig.google.key).toBe('google_key') - expect(grantConfig.google.secret).toBe('google_secret') + expect(grantConfig.googledrive.key).toBe('google_key') + expect(grantConfig.googledrive.secret).toBe('google_secret') + + expect(grantConfig.googlephotos.key).toBe('google_key') + expect(grantConfig.googledrive.secret).toBe('google_secret') expect(grantConfig.instagram.key).toBe('instagram_key') expect(grantConfig.instagram.secret).toBe('instagram_secret') @@ -69,7 +72,12 @@ describe('Test Provider options', () => { callback: '/box/callback', }) - expect(grantConfig.google).toEqual({ + expect(grantConfig.googledrive).toEqual({ + access_url: "https://oauth2.googleapis.com/token", + authorize_url: "https://accounts.google.com/o/oauth2/v2/auth", + oauth: 2, + scope_delimiter: " ", + key: 'google_key', secret: 'google_secret', transport: 'session', @@ -83,6 +91,25 @@ describe('Test Provider options', () => { prompt: 'consent', }, }) + + expect(grantConfig.googlephotos).toEqual({ + access_url: "https://oauth2.googleapis.com/token", + authorize_url: "https://accounts.google.com/o/oauth2/v2/auth", + oauth: 2, + scope_delimiter: " ", + + key: 'google_key', + secret: 'google_secret', + transport: 'session', + redirect_uri: 'http://localhost:3020/googlephotos/redirect', + scope: ['https://www.googleapis.com/auth/photoslibrary.readonly', 'https://www.googleapis.com/auth/userinfo.email'], + callback: '/googlephotos/callback', + custom_params: { + access_type: 'offline', + prompt: 'consent', + }, + }) + expect(grantConfig.zoom).toEqual({ key: 'zoom_key', secret: 'zoom_secret', @@ -108,7 +135,8 @@ describe('Test Provider options', () => { expect(grantConfig.dropbox.secret).toBe('xobpord') expect(grantConfig.box.secret).toBe('xwbepqd') - expect(grantConfig.google.secret).toBe('elgoog') + expect(grantConfig.googledrive.secret).toBe('elgoog') + expect(grantConfig.googlephotos.secret).toBe('elgoog') expect(grantConfig.instagram.secret).toBe('margatsni') expect(grantConfig.zoom.secret).toBe('u8Z5ceq') expect(companionOptions.providerOptions.zoom.verificationToken).toBe('o0u8Z5c') @@ -125,8 +153,11 @@ describe('Test Provider options', () => { expect(grantConfig.box.key).toBeUndefined() expect(grantConfig.box.secret).toBeUndefined() - expect(grantConfig.google.key).toBeUndefined() - expect(grantConfig.google.secret).toBeUndefined() + expect(grantConfig.googledrive.key).toBeUndefined() + expect(grantConfig.googledrive.secret).toBeUndefined() + + expect(grantConfig.googlephotos.key).toBeUndefined() + expect(grantConfig.googlephotos.secret).toBeUndefined() expect(grantConfig.instagram.key).toBeUndefined() expect(grantConfig.instagram.secret).toBeUndefined() @@ -141,7 +172,8 @@ describe('Test Provider options', () => { expect(grantConfig.dropbox.redirect_uri).toBe('http://domain.com/dropbox/redirect') expect(grantConfig.box.redirect_uri).toBe('http://domain.com/box/redirect') - expect(grantConfig.google.redirect_uri).toBe('http://domain.com/drive/redirect') + expect(grantConfig.googledrive.redirect_uri).toBe('http://domain.com/drive/redirect') + expect(grantConfig.googlephotos.redirect_uri).toBe('http://domain.com/googlephotos/redirect') expect(grantConfig.instagram.redirect_uri).toBe('http://domain.com/instagram/redirect') expect(grantConfig.zoom.redirect_uri).toBe('http://domain.com/zoom/redirect') }) diff --git a/packages/@uppy/companion/test/__tests__/providers.js b/packages/@uppy/companion/test/__tests__/providers.js index bd1c31ae8c..75f5b9a42d 100644 --- a/packages/@uppy/companion/test/__tests__/providers.js +++ b/packages/@uppy/companion/test/__tests__/providers.js @@ -26,7 +26,8 @@ const providers = require('../../src/server/provider').getDefaultProviders() const providerNames = Object.keys(providers) const AUTH_PROVIDERS = { - drive: 'google', + drive: 'googledrive', + googlephotos: 'googlephotos', onedrive: 'microsoft', } const authData = {} @@ -55,39 +56,35 @@ afterAll(() => { describe('list provider files', () => { async function runTest (providerName) { - const providerFixtures = fixtures.providers[providerName].expects + const providerFixture = fixtures.providers[providerName]?.expects ?? {} return request(authServer) - .get(`/${providerName}/list/${providerFixtures.listPath || ''}`) + .get(`/${providerName}/list/${providerFixture.listPath || ''}`) .set('uppy-auth-token', token) .expect(200) .then((res) => { expect(res.header['i-am']).toBe('http://localhost:3020') - expect(res.body.username).toBe(defaults.USERNAME) - - const items = [...res.body.items] - - // Drive has a virtual "shared-with-me" folder as the first item - if (providerName === 'drive') { - const item0 = items.shift() - expect(item0.isFolder).toBe(true) - expect(item0.name).toBe('Shared with me') - expect(item0.mimeType).toBe('application/vnd.google-apps.folder') - expect(item0.id).toBe('shared-with-me') - expect(item0.requestPath).toBe('shared-with-me') - expect(item0.icon).toBe('folder') - } - const item = items[0] - expect(item.isFolder).toBe(false) - expect(item.name).toBe(providerFixtures.itemName || defaults.ITEM_NAME) - expect(item.mimeType).toBe(providerFixtures.itemMimeType || defaults.MIME_TYPE) - expect(item.id).toBe(providerFixtures.itemId || defaults.ITEM_ID) - expect(item.size).toBe(thisOrThat(providerFixtures.itemSize, defaults.FILE_SIZE)) - expect(item.requestPath).toBe(providerFixtures.itemRequestPath || defaults.ITEM_ID) - expect(item.icon).toBe(providerFixtures.itemIcon || defaults.THUMBNAIL_URL) + return { + username: res.body.username, + items: res.body.items, + providerFixture, + } }) } + function expect1({ username, items, providerFixture }) { + expect(username).toBe(defaults.USERNAME) + + const item = items[0] + expect(item.isFolder).toBe(false) + expect(item.name).toBe(providerFixture.itemName || defaults.ITEM_NAME) + expect(item.mimeType).toBe(providerFixture.itemMimeType || defaults.MIME_TYPE) + expect(item.id).toBe(providerFixture.itemId || defaults.ITEM_ID) + expect(item.size).toBe(thisOrThat(providerFixture.itemSize, defaults.FILE_SIZE)) + expect(item.requestPath).toBe(providerFixture.itemRequestPath || defaults.ITEM_ID) + expect(item.icon).toBe(providerFixture.itemIcon || defaults.THUMBNAIL_URL) + } + test('dropbox', async () => { nock('https://api.dropboxapi.com').post('/2/users/get_current_account').reply(200, { name: { @@ -130,7 +127,8 @@ describe('list provider files', () => { has_more: false, }) - await runTest('dropbox') + const { username, items, providerFixture } = await runTest('dropbox') + expect1({ username, items, providerFixture }) }) test('box', async () => { @@ -149,7 +147,8 @@ describe('list provider files', () => { ], }) - await runTest('box') + const { username, items, providerFixture } = await runTest('box') + expect1({ username, items, providerFixture }) }) test('drive', async () => { @@ -178,7 +177,60 @@ describe('list provider files', () => { nock('https://www.googleapis.com').get((uri) => uri.includes('about')).reply(200, { user: { emailAddress: 'john.doe@transloadit.com' } }) - await runTest('drive') + const { username, items, providerFixture } = await runTest('drive') + + // Drive has a virtual "shared-with-me" folder as the first item + const [item0, ...rest] = items + expect(item0.isFolder).toBe(true) + expect(item0.name).toBe('Shared with me') + expect(item0.mimeType).toBe('application/vnd.google-apps.folder') + expect(item0.id).toBe('shared-with-me') + expect(item0.requestPath).toBe('shared-with-me') + expect(item0.icon).toBe('folder') + + expect1({ username, items: rest, providerFixture }) + }) + + test('googlephotos', async () => { + nock('https://photoslibrary.googleapis.com').get('/v1/albums?pageSize=50').reply(200, { + albums: [ + { + coverPhotoBaseUrl: 'https://test', + title: 'album', + id: '1', + } + ] + }) + + nock('https://photoslibrary.googleapis.com').get('/v1/sharedAlbums?pageSize=50').reply(200, { + sharedAlbums: [ + { + coverPhotoBaseUrl: 'https://test2', + title: 'shared album', + id: '2', + } + ] + }) + + nock('https://www.googleapis.com').get('/oauth2/v1/userinfo').reply(200, { + email: defaults.USERNAME, + }) + + const { items } = await runTest('googlephotos') + + expect(items[0].isFolder).toBe(true) + expect(items[0].name).toBe('album') + expect(items[0].id).toBe('1') + expect(items[0].requestPath).toBe('1') + expect(items[0].icon).toBe('https://drive-thirdparty.googleusercontent.com/32/type/application/vnd.google-apps.folder') + expect(items[0].thumbnail).toBe('https://test=w300-h300-c') + + expect(items[1].isFolder).toBe(true) + expect(items[1].name).toBe('shared album') + expect(items[1].id).toBe('2') + expect(items[1].requestPath).toBe('2') + expect(items[1].icon).toBe('https://drive-thirdparty.googleusercontent.com/32/type/application/vnd.google-apps.folder') + expect(items[1].thumbnail).toBe('https://test2=w300-h300-c') }) test('facebook', async () => { @@ -206,7 +258,8 @@ describe('list provider files', () => { paging: {}, }) - await runTest('facebook') + const { username, items, providerFixture } = await runTest('facebook') + expect1({ username, items, providerFixture }) }) test('instagram', async () => { @@ -225,7 +278,8 @@ describe('list provider files', () => { ], }) - await runTest('instagram') + const { username, items, providerFixture } = await runTest('instagram') + expect1({ username, items, providerFixture }) }) test('onedrive', async () => { @@ -271,7 +325,8 @@ describe('list provider files', () => { ], }) - await runTest('onedrive') + const { username, items, providerFixture } = await runTest('onedrive') + expect1({ username, items, providerFixture }) }) test('zoom', async () => { @@ -291,15 +346,16 @@ describe('list provider files', () => { }) nockZoomRecordings() - await runTest('zoom') + const { username, items, providerFixture } = await runTest('zoom') + expect1({ username, items, providerFixture }) }) }) describe('provider file gets downloaded from', () => { async function runTest (providerName) { - const providerFixtures = fixtures.providers[providerName].expects + const providerFixture = fixtures.providers[providerName]?.expects ?? {} const res = await request(authServer) - .post(`/${providerName}/get/${providerFixtures.itemRequestPath || defaults.ITEM_ID}`) + .post(`/${providerName}/get/${providerFixture.itemRequestPath || defaults.ITEM_ID}`) .set('uppy-auth-token', token) .set('Content-Type', 'application/json') .send({ @@ -324,11 +380,20 @@ describe('provider file gets downloaded from', () => { }) test('drive', async () => { - // times(2) because of size request - nockGoogleDownloadFile({ times: 2 }) + nockGoogleDownloadFile() await runTest('drive') }) + test('googlephotos', async () => { + nock('https://photoslibrary.googleapis.com').get(`/v1/mediaItems/${defaults.ITEM_ID}`).reply(200, { + baseUrl: 'https://lh3.googleusercontent.com/test', + }) + + nock('https://lh3.googleusercontent.com').get(`/test=d`).reply(200, ' ', { 'content-length': 1 }) + + await runTest('googlephotos') + }) + test('facebook', async () => { // times(2) because of size request nock('https://graph.facebook.com').get(`/${defaults.ITEM_ID}?fields=images`).times(2).reply(200, { @@ -393,7 +458,7 @@ describe('logout of provider', () => { .expect(200) // only some providers can actually be revoked - const expectRevoked = ['box', 'dropbox', 'drive', 'facebook', 'zoom'].includes(providerName) + const expectRevoked = ['box', 'dropbox', 'drive', 'googlephotos', 'facebook', 'zoom'].includes(providerName) expect(res.body).toMatchObject({ ok: true, @@ -421,6 +486,11 @@ describe('logout of provider', () => { await runTest('drive') }) + test('googlephotos', async () => { + nock('https://accounts.google.com').post('/o/oauth2/revoke?token=token+value').reply(200, {}) + await runTest('googlephotos') + }) + test('facebook', async () => { nock('https://graph.facebook.com').delete('/me/permissions').reply(200, {}) await runTest('facebook') diff --git a/packages/@uppy/companion/test/fixtures/drive.js b/packages/@uppy/companion/test/fixtures/drive.js index 8b136f3a1a..28f3ffdafe 100644 --- a/packages/@uppy/companion/test/fixtures/drive.js +++ b/packages/@uppy/companion/test/fixtures/drive.js @@ -5,7 +5,7 @@ module.exports.expects = {} module.exports.nockGoogleDriveAboutCall = () => nock('https://www.googleapis.com').get((uri) => uri.includes('about')).reply(200, { user: { emailAddress: 'john.doe@transloadit.com' } }) -module.exports.nockGoogleDownloadFile = ({ times = 1 } = {}) => { +module.exports.nockGoogleDownloadFile = ({ times = 2 } = {}) => { nock('https://www.googleapis.com').get(`/drive/v3/files/${defaults.ITEM_ID}?fields=kind%2Cid%2CimageMediaMetadata%2Cname%2CmimeType%2CownedByMe%2Csize%2CmodifiedTime%2CiconLink%2CthumbnailLink%2CteamDriveId%2CvideoMediaMetadata%2CexportLinks%2CshortcutDetails%28targetId%2CtargetMimeType%29&supportsAllDrives=true`).times(times).reply(200, { kind: 'drive#file', id: defaults.ITEM_ID, diff --git a/packages/@uppy/core/src/locale.ts b/packages/@uppy/core/src/locale.ts index 90a9974499..39f3fee3c3 100644 --- a/packages/@uppy/core/src/locale.ts +++ b/packages/@uppy/core/src/locale.ts @@ -62,5 +62,6 @@ export default { }, additionalRestrictionsFailed: '%{count} additional restrictions were not fulfilled', + unnamed: 'Unnamed', }, } diff --git a/packages/@uppy/dashboard/src/utils/copyToClipboard.ts b/packages/@uppy/dashboard/src/utils/copyToClipboard.ts index 6720cec9b7..ba4f276b8e 100644 --- a/packages/@uppy/dashboard/src/utils/copyToClipboard.ts +++ b/packages/@uppy/dashboard/src/utils/copyToClipboard.ts @@ -34,7 +34,7 @@ export default function copyToClipboard( document.body.appendChild(textArea) textArea.select() - const magicCopyFailed = (cause?: unknown) => { + const magicCopyFailed = () => { document.body.removeChild(textArea) // eslint-disable-next-line no-alert window.prompt(fallbackString, textToCopy) @@ -44,13 +44,13 @@ export default function copyToClipboard( try { const successful = document.execCommand('copy') if (!successful) { - return magicCopyFailed('copy command unavailable') + return magicCopyFailed() } document.body.removeChild(textArea) return resolve() } catch (err) { document.body.removeChild(textArea) - return magicCopyFailed(err) + return magicCopyFailed() } }) } diff --git a/packages/@uppy/dropbox/src/Dropbox.tsx b/packages/@uppy/dropbox/src/Dropbox.tsx index 6b53078519..c3ebc86b8e 100644 --- a/packages/@uppy/dropbox/src/Dropbox.tsx +++ b/packages/@uppy/dropbox/src/Dropbox.tsx @@ -85,6 +85,7 @@ export default class Dropbox extends UIPlugin< this.view = new ProviderViews(this, { provider: this.provider, loadAllFiles: true, + virtualList: true, }) const { target } = this.opts diff --git a/packages/@uppy/google-drive/src/GoogleDrive.tsx b/packages/@uppy/google-drive/src/GoogleDrive.tsx index 441c05fa20..7127cf8f81 100644 --- a/packages/@uppy/google-drive/src/GoogleDrive.tsx +++ b/packages/@uppy/google-drive/src/GoogleDrive.tsx @@ -104,6 +104,7 @@ export default class GoogleDrive< this.view = new DriveProviderViews(this, { provider: this.provider, loadAllFiles: true, + virtualList: true, }) const { target } = this.opts diff --git a/packages/@uppy/google-photos/.npmignore b/packages/@uppy/google-photos/.npmignore new file mode 100644 index 0000000000..6c816673f0 --- /dev/null +++ b/packages/@uppy/google-photos/.npmignore @@ -0,0 +1 @@ +tsconfig.* diff --git a/packages/@uppy/google-photos/CHANGELOG.md b/packages/@uppy/google-photos/CHANGELOG.md new file mode 100644 index 0000000000..ba99657215 --- /dev/null +++ b/packages/@uppy/google-photos/CHANGELOG.md @@ -0,0 +1 @@ +# @uppy/google-photos diff --git a/packages/@uppy/google-photos/LICENSE b/packages/@uppy/google-photos/LICENSE new file mode 100644 index 0000000000..6f25c43720 --- /dev/null +++ b/packages/@uppy/google-photos/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/README.md b/packages/@uppy/google-photos/README.md new file mode 100644 index 0000000000..4cc652ee12 --- /dev/null +++ b/packages/@uppy/google-photos/README.md @@ -0,0 +1,51 @@ +# @uppy/google-photos + +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-photos/package.json b/packages/@uppy/google-photos/package.json new file mode 100644 index 0000000000..93ea7cec95 --- /dev/null +++ b/packages/@uppy/google-photos/package.json @@ -0,0 +1,33 @@ +{ + "name": "@uppy/google-photos", + "description": "The Google Photos plugin for Uppy lets users import photos from their Google Photos account", + "version": "0.0.1", + "license": "MIT", + "main": "lib/index.js", + "types": "types/index.d.ts", + "type": "module", + "keywords": [ + "file uploader", + "google photos", + "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/src/GooglePhotos.tsx b/packages/@uppy/google-photos/src/GooglePhotos.tsx new file mode 100644 index 0000000000..208b80a2d8 --- /dev/null +++ b/packages/@uppy/google-photos/src/GooglePhotos.tsx @@ -0,0 +1,135 @@ +import { UIPlugin, Uppy } from '@uppy/core' +import { ProviderViews } from '@uppy/provider-views' +import { + Provider, + tokenStorage, + 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.ts' + +// 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 GooglePhotosOptions = CompanionPluginOptions + +export default class GooglePhotos< + M extends Meta, + B extends Body, +> extends UIPlugin { + static VERSION = packageJson.version + + icon: () => h.JSX.Element + + provider: Provider + + view: ProviderViews + + storage: typeof tokenStorage + + files: UppyFile[] + + constructor(uppy: Uppy, opts: GooglePhotosOptions) { + super(uppy, opts) + this.type = 'acquirer' + this.storage = this.opts.storage || tokenStorage + this.files = [] + this.id = this.opts.id || 'GooglePhotos' + 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: 'googlephotos', + pluginId: this.id, + supportsRefreshToken: true, + }) + + this.defaultLocale = locale + + this.i18nInit() + this.title = this.i18n('pluginNameGooglePhotos') + + this.onFirstRender = this.onFirstRender.bind(this) + this.render = this.render.bind(this) + } + + install(): void { + this.view = new ProviderViews(this, { + provider: this.provider, + loadAllFiles: true, + }) + + const { target } = this.opts + if (target) { + this.mount(target, this) + } + } + + uninstall(): void { + this.view.tearDown() + this.unmount() + } + + async onFirstRender(): Promise { + await Promise.all([ + this.provider.fetchPreAuthToken(), + this.view.getFolder(), + ]) + } + + render(state: unknown): ComponentChild { + if ( + this.getPluginState().files.length && + !this.getPluginState().folders.length + ) { + return this.view.render(state, { + viewType: 'grid', + showFilter: false, + showTitles: false, + }) + } + return this.view.render(state) + } +} diff --git a/packages/@uppy/google-photos/src/index.ts b/packages/@uppy/google-photos/src/index.ts new file mode 100644 index 0000000000..7535efaaca --- /dev/null +++ b/packages/@uppy/google-photos/src/index.ts @@ -0,0 +1 @@ +export { default } from './GooglePhotos.tsx' diff --git a/packages/@uppy/google-photos/src/locale.ts b/packages/@uppy/google-photos/src/locale.ts new file mode 100644 index 0000000000..e12b583018 --- /dev/null +++ b/packages/@uppy/google-photos/src/locale.ts @@ -0,0 +1,5 @@ +export default { + strings: { + pluginNameGooglePhotos: 'Google Photos', + }, +} diff --git a/packages/@uppy/google-photos/tsconfig.build.json b/packages/@uppy/google-photos/tsconfig.build.json new file mode 100644 index 0000000000..99aaf378de --- /dev/null +++ b/packages/@uppy/google-photos/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/tsconfig.json b/packages/@uppy/google-photos/tsconfig.json new file mode 100644 index 0000000000..e5220fb5ab --- /dev/null +++ b/packages/@uppy/google-photos/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-photos/types/index.d.ts b/packages/@uppy/google-photos/types/index.d.ts new file mode 100644 index 0000000000..e37c2591cd --- /dev/null +++ b/packages/@uppy/google-photos/types/index.d.ts @@ -0,0 +1,17 @@ +import type { PluginTarget, UIPlugin, UIPluginOptions } from '@uppy/core' +import type { + PublicProviderOptions, + TokenStorage, +} from '@uppy/companion-client' + +export interface GooglePhotosOptions + extends UIPluginOptions, + PublicProviderOptions { + target?: PluginTarget + title?: string + storage?: TokenStorage +} + +declare class GooglePhotos extends UIPlugin {} + +export default GooglePhotos diff --git a/packages/@uppy/google-photos/types/index.test-d.ts b/packages/@uppy/google-photos/types/index.test-d.ts new file mode 100644 index 0000000000..0847992597 --- /dev/null +++ b/packages/@uppy/google-photos/types/index.test-d.ts @@ -0,0 +1,12 @@ +import Uppy, { UIPlugin, type UIPluginOptions } from '@uppy/core' +import GooglePhotos from '..' + +class SomePlugin extends UIPlugin {} + +const uppy = new Uppy() +uppy.use(GooglePhotos, { companionUrl: '' }) +uppy.use(GooglePhotos, { target: SomePlugin, companionUrl: '' }) +uppy.use(GooglePhotos, { + target: document.querySelector('#gphotos') || (undefined as never), + companionUrl: '', +}) diff --git a/packages/@uppy/onedrive/src/OneDrive.tsx b/packages/@uppy/onedrive/src/OneDrive.tsx index 1b329e6999..fe15f04f5d 100644 --- a/packages/@uppy/onedrive/src/OneDrive.tsx +++ b/packages/@uppy/onedrive/src/OneDrive.tsx @@ -97,6 +97,7 @@ export default class OneDrive extends UIPlugin< this.view = new ProviderViews(this, { provider: this.provider, loadAllFiles: true, + virtualList: true, }) const { target } = this.opts diff --git a/packages/@uppy/provider-views/src/Browser.tsx b/packages/@uppy/provider-views/src/Browser.tsx index f820491da1..c1ccf61b63 100644 --- a/packages/@uppy/provider-views/src/Browser.tsx +++ b/packages/@uppy/provider-views/src/Browser.tsx @@ -74,7 +74,8 @@ function ListItem(props: ListItemProps) { id: f.id, title: f.name, author: f.author, - getItemIcon: () => f.icon, + getItemIcon: () => + viewType === 'grid' && f.thumbnail ? f.thumbnail : f.icon, isChecked: isChecked(f), toggleCheckbox: (event: Event) => toggleCheckbox(event, f), isCheckboxDisabled: false, @@ -115,7 +116,7 @@ type BrowserProps = { cancel: () => void done: () => void noResultsLabel: string - loadAllFiles?: boolean + virtualList?: boolean } function Browser(props: BrowserProps) { @@ -146,7 +147,7 @@ function Browser(props: BrowserProps) { cancel, done, noResultsLabel, - loadAllFiles, + virtualList, } = props const selected = currentSelection.length @@ -202,7 +203,7 @@ function Browser(props: BrowserProps) { return
{noResultsLabel}
} - if (loadAllFiles) { + if (virtualList) { return (
    diff --git a/packages/@uppy/provider-views/src/Item/components/ListLi.tsx b/packages/@uppy/provider-views/src/Item/components/ListLi.tsx index c07d68309a..38e95eeb16 100644 --- a/packages/@uppy/provider-views/src/Item/components/ListLi.tsx +++ b/packages/@uppy/provider-views/src/Item/components/ListLi.tsx @@ -95,7 +95,9 @@ export default function ListItem(
    {itemIconEl}
    - {showTitles && {title}} + {showTitles && title ? + {title} + : i18n('unnamed')} } diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index f89ac115b1..be2b5cd08e 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -58,6 +58,7 @@ const defaultOptions = { showFilter: true, showBreadcrumbs: true, loadAllFiles: false, + virtualList: false, } export interface ProviderViewOptions @@ -68,6 +69,7 @@ export interface ProviderViewOptions loading: boolean | string onAuth: (authFormData: unknown) => Promise }) => h.JSX.Element + virtualList?: boolean } type Opts = DefinePluginOpts< @@ -583,6 +585,7 @@ export default class ProviderView extends View< getNextFolder: this.getNextFolder, getFolder: this.getFolder, loadAllFiles: this.opts.loadAllFiles, + virtualList: this.opts.virtualList, // For SearchFilterInput component showSearchFilter: targetViewOptions.showFilter, diff --git a/packages/@uppy/provider-views/src/View.ts b/packages/@uppy/provider-views/src/View.ts index 98ea88e1bb..fca17e0e4f 100644 --- a/packages/@uppy/provider-views/src/View.ts +++ b/packages/@uppy/provider-views/src/View.ts @@ -4,8 +4,6 @@ import type { } from '@uppy/core/lib/Uppy' import type { Body, Meta, TagFile } from '@uppy/utils/lib/UppyFile' import type { CompanionFile } from '@uppy/utils/lib/CompanionFile' -import getFileType from '@uppy/utils/lib/getFileType' -import isPreviewSupported from '@uppy/utils/lib/isPreviewSupported' import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal' type PluginType = 'Provider' | 'SearchProvider' @@ -148,10 +146,7 @@ export default class View< }, } - const fileType = getFileType(tagFile) - - // TODO Should we just always use the thumbnail URL if it exists? - if (fileType && isPreviewSupported(fileType)) { + if (file.thumbnail) { tagFile.preview = file.thumbnail } diff --git a/packages/@uppy/react/types/index.test-d.tsx b/packages/@uppy/react/types/index.test-d.tsx index d408d306a9..154796050d 100644 --- a/packages/@uppy/react/types/index.test-d.tsx +++ b/packages/@uppy/react/types/index.test-d.tsx @@ -8,6 +8,7 @@ const { useUppy } = components const uppy = new Uppy() { + // eslint-disable-next-line @typescript-eslint/no-unused-vars function TestComponent() { return ( @@ -27,6 +28,7 @@ const uppy = new Uppy() } { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const el = ( (useUppy(() => uppy)) expectType(useUppy(() => new Uppy())) diff --git a/packages/@uppy/remote-sources/package.json b/packages/@uppy/remote-sources/package.json index 7c7799c422..8d72ceb9d5 100644 --- a/packages/@uppy/remote-sources/package.json +++ b/packages/@uppy/remote-sources/package.json @@ -10,6 +10,7 @@ "file uploader", "instagram", "google-drive", + "google-photos", "facebook", "dropbox", "onedrive", @@ -32,6 +33,7 @@ "@uppy/dropbox": "workspace:^", "@uppy/facebook": "workspace:^", "@uppy/google-drive": "workspace:^", + "@uppy/google-photos": "workspace:^", "@uppy/instagram": "workspace:^", "@uppy/onedrive": "workspace:^", "@uppy/unsplash": "workspace:^", diff --git a/packages/@uppy/remote-sources/src/index.test.ts b/packages/@uppy/remote-sources/src/index.test.ts index b99ae2af89..261bc89a67 100644 --- a/packages/@uppy/remote-sources/src/index.test.ts +++ b/packages/@uppy/remote-sources/src/index.test.ts @@ -47,7 +47,7 @@ describe('RemoteSources', () => { sources: ['Webcam'], }) }).toThrow( - 'Invalid plugin: "Webcam" is not one of: Box, Dropbox, Facebook, GoogleDrive, Instagram, OneDrive, Unsplash, Url, or Zoom.', + 'Invalid plugin: "Webcam" is not one of: Box, Dropbox, Facebook, GoogleDrive, GooglePhotos, Instagram, OneDrive, Unsplash, Url, or Zoom.', ) }) }) diff --git a/packages/@uppy/remote-sources/src/index.ts b/packages/@uppy/remote-sources/src/index.ts index 84e9f44b05..9cc9c78108 100644 --- a/packages/@uppy/remote-sources/src/index.ts +++ b/packages/@uppy/remote-sources/src/index.ts @@ -6,6 +6,7 @@ import { } from '@uppy/core' import Dropbox from '@uppy/dropbox' import GoogleDrive from '@uppy/google-drive' +import GooglePhotos from '@uppy/google-photos' import Instagram from '@uppy/instagram' import Facebook from '@uppy/facebook' import OneDrive from '@uppy/onedrive' @@ -27,6 +28,7 @@ const availablePlugins = { Dropbox, Facebook, GoogleDrive, + GooglePhotos, Instagram, OneDrive, Unsplash, diff --git a/packages/@uppy/remote-sources/tsconfig.build.json b/packages/@uppy/remote-sources/tsconfig.build.json index 97b71eb43c..5399803aac 100644 --- a/packages/@uppy/remote-sources/tsconfig.build.json +++ b/packages/@uppy/remote-sources/tsconfig.build.json @@ -14,6 +14,8 @@ "@uppy/facebook/lib/*": ["../facebook/src/*"], "@uppy/google-drive": ["../google-drive/src/index.js"], "@uppy/google-drive/lib/*": ["../google-drive/src/*"], + "@uppy/google-photos": ["../google-photos/src/index.js"], + "@uppy/google-photos/lib/*": ["../google-photos/src/*"], "@uppy/instagram": ["../instagram/src/index.js"], "@uppy/instagram/lib/*": ["../instagram/src/*"], "@uppy/onedrive": ["../onedrive/src/index.js"], @@ -49,6 +51,9 @@ { "path": "../google-drive/tsconfig.build.json" }, + { + "path": "../google-photos/tsconfig.build.json" + }, { "path": "../instagram/tsconfig.build.json" }, diff --git a/packages/@uppy/remote-sources/tsconfig.json b/packages/@uppy/remote-sources/tsconfig.json index 8a51722f0b..73052cf39c 100644 --- a/packages/@uppy/remote-sources/tsconfig.json +++ b/packages/@uppy/remote-sources/tsconfig.json @@ -14,6 +14,8 @@ "@uppy/facebook/lib/*": ["../facebook/src/*"], "@uppy/google-drive": ["../google-drive/src/index.js"], "@uppy/google-drive/lib/*": ["../google-drive/src/*"], + "@uppy/google-photos": ["../google-photos/src/index.js"], + "@uppy/google-photos/lib/*": ["../google-photos/src/*"], "@uppy/instagram": ["../instagram/src/index.js"], "@uppy/instagram/lib/*": ["../instagram/src/*"], "@uppy/onedrive": ["../onedrive/src/index.js"], @@ -45,6 +47,9 @@ { "path": "../google-drive/tsconfig.build.json", }, + { + "path": "../google-photos/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 7a8ddb37e7..7fcfbb19ca 100644 --- a/packages/@uppy/transloadit/src/index.ts +++ b/packages/@uppy/transloadit/src/index.ts @@ -379,6 +379,7 @@ export default class Transloadit< addPluginVersion('Box', 'uppy-box') addPluginVersion('Facebook', 'uppy-facebook') addPluginVersion('GoogleDrive', 'uppy-google-drive') + addPluginVersion('GooglePhotos', 'uppy-google-photos') addPluginVersion('Instagram', 'uppy-instagram') addPluginVersion('OneDrive', 'uppy-onedrive') addPluginVersion('Zoom', 'uppy-zoom') diff --git a/packages/uppy/index.mjs b/packages/uppy/index.mjs index 17a0a3077f..d25dbb5e56 100644 --- a/packages/uppy/index.mjs +++ b/packages/uppy/index.mjs @@ -38,6 +38,7 @@ export { default as Box } from '@uppy/box' 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 Instagram } from '@uppy/instagram' export { default as OneDrive } from '@uppy/onedrive' export { default as RemoteSources } from '@uppy/remote-sources' diff --git a/packages/uppy/package.json b/packages/uppy/package.json index 3a7b3c5053..969846e08c 100644 --- a/packages/uppy/package.json +++ b/packages/uppy/package.json @@ -47,6 +47,7 @@ "@uppy/form": "workspace:^", "@uppy/golden-retriever": "workspace:^", "@uppy/google-drive": "workspace:^", + "@uppy/google-photos": "workspace:^", "@uppy/image-editor": "workspace:^", "@uppy/informer": "workspace:^", "@uppy/instagram": "workspace:^", diff --git a/packages/uppy/types/index.d.ts b/packages/uppy/types/index.d.ts index 76500aab0a..f9c370a318 100644 --- a/packages/uppy/types/index.d.ts +++ b/packages/uppy/types/index.d.ts @@ -22,6 +22,7 @@ export { default as StatusBar } from '@uppy/status-bar' export { default as Dropbox } from '@uppy/dropbox' export { default as Box } from '@uppy/box' export { default as GoogleDrive } from '@uppy/google-drive' +export { default as GooglePhotos } from '@uppy/google-photos' export { default as Instagram } from '@uppy/instagram' export { default as Url } from '@uppy/url' export { default as Webcam } from '@uppy/webcam' diff --git a/private/dev/Dashboard.js b/private/dev/Dashboard.js index 6b20714a98..d750b783af 100644 --- a/private/dev/Dashboard.js +++ b/private/dev/Dashboard.js @@ -113,7 +113,7 @@ export default () => { // .use(Unsplash, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts }) .use(RemoteSources, { companionUrl: COMPANION_URL, - sources: ['Box', 'Dropbox', 'Facebook', 'Instagram', 'OneDrive', 'Unsplash', 'Zoom', 'Url'], + sources: ['GooglePhotos', 'Box', 'Dropbox', 'Facebook', 'Instagram', 'OneDrive', 'Unsplash', 'Zoom', 'Url'], companionAllowedHosts, }) .use(Webcam, { diff --git a/yarn.lock b/yarn.lock index 40eebd7fef..8338adb9a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8735,6 +8735,7 @@ __metadata: "@uppy/core": "workspace:*" "@uppy/drag-drop": "workspace:*" "@uppy/google-drive": "workspace:*" + "@uppy/google-photos": "workspace:*" "@uppy/progress-bar": "workspace:*" "@uppy/tus": "workspace:*" "@uppy/webcam": "workspace:*" @@ -9400,6 +9401,19 @@ __metadata: languageName: unknown linkType: soft +"@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-photos@workspace:packages/@uppy/google-photos" + dependencies: + "@uppy/companion-client": "workspace:^" + "@uppy/provider-views": "workspace:^" + "@uppy/utils": "workspace:^" + preact: ^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" @@ -9546,6 +9560,7 @@ __metadata: "@uppy/dropbox": "workspace:^" "@uppy/facebook": "workspace:^" "@uppy/google-drive": "workspace:^" + "@uppy/google-photos": "workspace:^" "@uppy/instagram": "workspace:^" "@uppy/onedrive": "workspace:^" "@uppy/unsplash": "workspace:^" @@ -14234,6 +14249,7 @@ __metadata: "@uppy/form": "workspace:^" "@uppy/golden-retriever": "workspace:^" "@uppy/google-drive": "workspace:^" + "@uppy/google-photos": "workspace:^" "@uppy/image-editor": "workspace:^" "@uppy/informer": "workspace:^" "@uppy/instagram": "workspace:^" @@ -31100,6 +31116,7 @@ __metadata: "@uppy/form": "workspace:^" "@uppy/golden-retriever": "workspace:^" "@uppy/google-drive": "workspace:^" + "@uppy/google-photos": "workspace:^" "@uppy/image-editor": "workspace:^" "@uppy/informer": "workspace:^" "@uppy/instagram": "workspace:^" From b0d2ef5026377e1e2e339a2a3c1ee738d4cc8eb2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Jun 2024 11:17:58 +0200 Subject: [PATCH 5/6] Bump ws from 8.8.1 to 8.17.1 (#5256) Bumps [ws](https://github.com/websockets/ws) from 8.8.1 to 8.17.1. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/8.8.1...8.17.1) --- updated-dependencies: - dependency-name: ws dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- packages/@uppy/companion/package.json | 2 +- yarn.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/@uppy/companion/package.json b/packages/@uppy/companion/package.json index 20bdebf1b6..3ce2247539 100644 --- a/packages/@uppy/companion/package.json +++ b/packages/@uppy/companion/package.json @@ -67,7 +67,7 @@ "serialize-javascript": "^6.0.0", "tus-js-client": "^3.1.3", "validator": "^13.0.0", - "ws": "8.8.1" + "ws": "8.17.1" }, "devDependencies": { "@types/compression": "1.7.0", diff --git a/yarn.lock b/yarn.lock index 8338adb9a4..b2f8553cba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9245,7 +9245,7 @@ __metadata: tus-js-client: ^3.1.3 typescript: ~5.1 validator: ^13.0.0 - ws: 8.8.1 + ws: 8.17.1 bin: companion: ./bin/companion languageName: unknown @@ -32277,18 +32277,18 @@ __metadata: languageName: node linkType: hard -"ws@npm:8.8.1": - version: 8.8.1 - resolution: "ws@npm:8.8.1" +"ws@npm:8.17.1": + version: 8.17.1 + resolution: "ws@npm:8.17.1" peerDependencies: bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 + utf-8-validate: ">=5.0.2" peerDependenciesMeta: bufferutil: optional: true utf-8-validate: optional: true - checksum: 2152cf862cae0693f3775bc688a6afb2e989d19d626d215e70f5fcd8eb55b1c3b0d3a6a4052905ec320e2d7734e20aeedbf9744496d62f15a26ad79cf4cf7dae + checksum: 442badcce1f1178ec87a0b5372ae2e9771e07c4929a3180321901f226127f252441e8689d765aa5cfba5f50ac60dd830954afc5aeae81609aefa11d3ddf5cecf languageName: node linkType: hard From b5df7d05c6c1016b7895f2959dd34718e3960905 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 18 Jun 2024 09:26:52 +0000 Subject: [PATCH 6/6] Release: uppy@3.27.0 (#5257) | Package | Version | Package | Version | | -------------------- | ------- | -------------------- | ------- | | @uppy/box | 2.4.0 | @uppy/onedrive | 3.4.0 | | @uppy/companion | 4.14.0 | @uppy/provider-views | 3.13.0 | | @uppy/core | 3.13.0 | @uppy/react | 3.4.0 | | @uppy/dashboard | 3.9.0 | @uppy/remote-sources | 1.3.0 | | @uppy/dropbox | 3.4.0 | @uppy/transloadit | 3.8.0 | | @uppy/google-drive | 3.6.0 | uppy | 3.27.0 | | @uppy/google-photos | 0.1.0 | | | - @uppy/google-photos: add plugin (Mikael Finstad / #5061) - examples: updating aws-nodejs example listParts logic for resuming uploads (Mitchell Rhoads / #5192) - meta: Bump docker/login-action from 3.1.0 to 3.2.0 (dependabot\[bot] / #5217) - meta: Bump docker/build-push-action from 5.3.0 to 5.4.0 (dependabot\[bot] / #5252) - @uppy/transloadit: also fix outdated assembly transloadit:result (Merlijn Vos / #5246) - docs: fix typo in the url (Evgenia Karunus) - @uppy/companion: Bump ws from 8.8.1 to 8.17.1 (dependabot\[bot] / #5256) --- BUNDLE-README.md | 2 +- CHANGELOG.md | 23 +++++ README.md | 84 +++++++++---------- examples/aws-nodejs/public/index.html | 4 +- examples/cdn-example/index.html | 6 +- .../uppy-with-companion/client/index.html | 4 +- packages/@uppy/box/package.json | 2 +- packages/@uppy/companion/CHANGELOG.md | 8 ++ packages/@uppy/companion/package.json | 2 +- packages/@uppy/core/package.json | 2 +- packages/@uppy/dashboard/package.json | 2 +- packages/@uppy/dropbox/package.json | 2 +- packages/@uppy/google-drive/package.json | 2 +- packages/@uppy/google-photos/CHANGELOG.md | 7 ++ packages/@uppy/google-photos/package.json | 2 +- packages/@uppy/onedrive/package.json | 2 +- packages/@uppy/provider-views/package.json | 2 +- packages/@uppy/react/package.json | 2 +- packages/@uppy/remote-sources/package.json | 2 +- packages/@uppy/transloadit/CHANGELOG.md | 7 ++ packages/@uppy/transloadit/package.json | 2 +- packages/uppy/package.json | 2 +- 22 files changed, 108 insertions(+), 63 deletions(-) diff --git a/BUNDLE-README.md b/BUNDLE-README.md index 4fd4d79150..004e2ab48a 100644 --- a/BUNDLE-README.md +++ b/BUNDLE-README.md @@ -2,7 +2,7 @@ Hi, thanks for trying out the bundled version of the Uppy File Uploader. You can use this from a CDN -(``) +(``) or bundle it with your webapp. Note that the recommended way to use Uppy is to install it with yarn/npm and use diff --git a/CHANGELOG.md b/CHANGELOG.md index 257c602065..0f8b80f88c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,29 @@ Please add your entries in this format: In the current stage we aim to release a new version at least every month. +## 3.27.0 + +Released: 2024-06-18 + +| Package | Version | Package | Version | +| -------------------- | ------- | -------------------- | ------- | +| @uppy/box | 2.4.0 | @uppy/onedrive | 3.4.0 | +| @uppy/companion | 4.14.0 | @uppy/provider-views | 3.13.0 | +| @uppy/core | 3.13.0 | @uppy/react | 3.4.0 | +| @uppy/dashboard | 3.9.0 | @uppy/remote-sources | 1.3.0 | +| @uppy/dropbox | 3.4.0 | @uppy/transloadit | 3.8.0 | +| @uppy/google-drive | 3.6.0 | uppy | 3.27.0 | +| @uppy/google-photos | 0.1.0 | | | + +- @uppy/google-photos: add plugin (Mikael Finstad / #5061) +- examples: updating aws-nodejs example listParts logic for resuming uploads (Mitchell Rhoads / #5192) +- meta: Bump docker/login-action from 3.1.0 to 3.2.0 (dependabot\[bot] / #5217) +- meta: Bump docker/build-push-action from 5.3.0 to 5.4.0 (dependabot\[bot] / #5252) +- @uppy/transloadit: also fix outdated assembly transloadit:result (Merlijn Vos / #5246) +- docs: fix typo in the url (Evgenia Karunus) +- @uppy/companion: Bump ws from 8.8.1 to 8.17.1 (dependabot\[bot] / #5256) + + ## 3.26.1 Released: 2024-06-11 diff --git a/README.md b/README.md index 4d31e7666e..1440243ff3 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ npm install @uppy/core @uppy/dashboard @uppy/tus ``` Add CSS -[uppy.min.css](https://releases.transloadit.com/uppy/v3.26.1/uppy.min.css), +[uppy.min.css](https://releases.transloadit.com/uppy/v3.27.0/uppy.min.css), either to your HTML page’s `` or include in JS, if your bundler of choice supports it. @@ -94,7 +94,7 @@ object. ```html @@ -105,7 +105,7 @@ object. Uppy, Dashboard, Tus, - } from 'https://releases.transloadit.com/uppy/v3.26.1/uppy.min.mjs' + } from 'https://releases.transloadit.com/uppy/v3.27.0/uppy.min.mjs' const uppy = new Uppy() uppy.use(Dashboard, { target: '#files-drag-drop' }) @@ -240,7 +240,7 @@ If you’re using Uppy from CDN, those polyfills are already included in the legacy bundle, so no need to include anything additionally: ```html - + ``` ## FAQ @@ -347,57 +347,57 @@ Use Uppy in your project? darthf1fortriebfrederikhorsheocoijareymuhammadInam rettgerstmkabatekjukakoskiolemoignbtrice5idereal AndrwMbehnammodiBePo65bradedelmancamiloforerocommand-tab -craig-jenningsdavekissdenysdesignethanwillisfrobinsonjrichartkeil -paescujrichmeijmsandmartiuslimMartin005mskelton -mactavishzlafedogrockerjedwoodjasonboscoghasrfakhri -geertclerxluarmrraulibanezrefoSxDxrobwilson1 -scherromanrossngrarteman8519Pzocoppadmavilasom +craig-jenningsdavekissdenysdesignethanwillisfrobinsonjrichmeij +richartkeilpaescujmsandmartiuslimMartin005mskelton +mactavishzlafedogrockerjedwoodgeertclerxjasonbosco +ghasrfakhrirartrossngscherromanrobwilson1SxDx +reforaulibanezluarmreman8519Pzocoppadmavilasom phillipalexanderpmusarajpedrofsplnetopatricklindsaypascalwengerter -tcgjTashowstajstrayersjauldsteverob +JimmyLvTashowstajstrayersjauldsteverob amaituquigebowaptikSpazzMarticusszhsergei-zelinsky sebasegovia01sdebackerRattonesamuelcolburnfortunto2GNURub -ken-kuromilannakummkopinskymhulethrshmauricioribeiro -matthewhartstongemjesuelemattfikmateuscruzmasumulu28masaok -martin-brennanmarcusforsbergmarcosthejewmperrandoonhateParsaArvanehPA +ParsaArvanehPAken-kuromilannakummkopinskymhulethrsh +mauricioribeiromatthewhartstongemjesuelemattfikmateuscruzmasumulu28 +masaokmartin-brennanmarcusforsbergmarcosthejewmperrandoonhate cryptic022Ozodbek1405leftdevelnil1511coreprocessnicojones trungcva10a6tnnaveed-ahmadpleasespammelatermarton-laszlo-attilanavruzmmogzol -shahimcltmnafeesboudraachmiralneuronet77netdown +shahimcltmnafeesboudraMitchell8210achmiralnetdown mosi-khamaddy-jomdxiaohumagumbojx-zyfkode-ninja sontixyoujur-ngjohnmanjiro13jyoungbloodgreen-mikegaelicwinter -francklfingulelliotsayesdzcpyJimmyLvzanzlender +francklfingulelliotsayesdzcpydkisiczanzlender olitomasyoann-hellopretvedran555tusharjkhuntthanhthotstduhpf slawexxx44rtaiebrmoura-92rlebosserhymesluntta -phil714ordagoodselsevierninesaltxhocquetwillycamargo +phil714ordagoodselsevierninesaltneuronet77willycamargo weston-sankey-mark43dwnstenagyvstiigvalentinolivially bodryitrivikrtop-mastertvaliasektomekptomsaleeba -WIStudenttmaierTiarhaitwarlopdkisiccraigcbrunner +WIStudenttmaierTiarhaitwarloptcgjcraigcbrunner codehero7386christianwengertcgoinglovecanvasbhc0b41avalla argghalfatvagreene-courseraaduh95-test-accountsartoshi-foot-daozackbloom -zlawson-utzachconneryafkariYehudaKremersercraigardeois -CommanderRootczjcbush06Aarbelcfracsprance -prattcmpsubvertallchrischarlybillaudCretezychaocellvinchung -cartfiskcyubryanjswiftbedgerottoeliOcsyoldar -efbautistaemuellEdgarSantiago93sweetrojeetissDennisKofflard -hoangsvitdavilima6akizorKaminskiDaniellCantabarmrboomer -danilatdanschalowdanmichaeloCruaierfunctinoamitport -tekacsDogfaloalirezahiaalepisalexnjasmt3 -ahmadissaadritasharmaAdrreiadityapatadiaadamvigneaultajh-sr -adamdottvabannachsuperhawk610ajschmidt8wbaaronQuorafind -bducharmeazizkazeembaayhankesiciogluavneetmalhotraatsawin -ash-jc-allenapuyouarthurdennerAbourasstyndriaanthony0030 -andychongyzandrii-bodnarsuperandrew213radarherekevin-west-10xkergekacsa -firesharkstudioskaspermeinematykaroljveltenmellow-fellowjmontoyaa -jcalonsojbelejjszobodyjorgeepcjondewoojonathanarbely -jsanchez034Jokcychromacomaprofsmallpinemarc-mabeLucklj521 -lucax88xlucaperretombrlouimdolphinigleleomelzer -leods92galli-leodvirylarowlanleaanthonyhoangbits -labohkip81kyleparisielkebabkidonngIanVShuydod -HussainAlkhalifahHughbertDhiromi2424giacomocerquoneroenschggjungb -geoffapplefordgabiganamfuadscodesdtrucsferdiusafgallinari -GkleinerevaepexaEnricoSottileelliotdickisontheJoeBizJmales -jessica-courseravithjanwiltsjanklimojamestiotiojcjmcclean -JbithellJakubHaladejjakemcallistergaejabongJacobMGEvansmazoruss -GreenJimmyintenziveNaxYoishendyweb +zlawson-utzachconneryafkariYehudaKremerxhocquetsercraig +ardeoisCommanderRootczjcbush06Aarbelcfra +cspranceprattcmpsubvertallchrischarlybillaudCretezychao +cellvinchungcartfiskcyubryanjswiftbedgerottoeliOcs +yoldarefbautistaemuellEdgarSantiago93sweetrojeetiss +DennisKofflardhoangsvitdavilima6akizorKaminskiDaniellCantabar +mrboomerdanilatdanschalowdanmichaeloCruaierfunctino +amitporttekacsDogfaloalirezahiaalepisalexnj +asmt3ahmadissaadritasharmaAdrreiadityapatadiaadamvigneault +ajh-sradamdottvabannachsuperhawk610ajschmidt8wbaaron +Quorafindbducharmeazizkazeembaayhankesiciogluavneetmalhotra +atsawinash-jc-allenapuyouarthurdennerAbourasstyndria +anthony0030andychongyzandrii-bodnarsuperandrew213radarherekevin-west-10x +kergekacsafiresharkstudioskaspermeinematykaroljveltenmellow-fellow +jmontoyaajcalonsojbelejjszobodyjorgeepcjondewoo +jonathanarbelyjsanchez034Jokcychromacomaprofsmallpinemarc-mabe +Lucklj521lucax88xlucaperretombrlouimdolphinigle +leomelzerleods92galli-leodvirylarowlanleaanthony +hoangbitslabohkip81kyleparisielkebabkidonngIanVS +huydodHussainAlkhalifahHughbertDhiromi2424giacomocerquoneroenschg +gjungbgeoffapplefordgabiganamfuadscodesdtrucsferdiusa +fgallinariGkleinerevaepexaEnricoSottileelliotdickisontheJoeBiz +Jmalesjessica-courseravithjanwiltsjanklimojamestiotio +jcjmccleanJbithellJakubHaladejjakemcallistergaejabongJacobMGEvans +mazorussGreenJimmyintenziveNaxYoishendyweb diff --git a/examples/aws-nodejs/public/index.html b/examples/aws-nodejs/public/index.html index 884d3b7b66..2d7c4b0697 100644 --- a/examples/aws-nodejs/public/index.html +++ b/examples/aws-nodejs/public/index.html @@ -4,7 +4,7 @@ Uppy – AWS upload example @@ -16,7 +16,7 @@

    AWS upload example

    Uppy, Dashboard, AwsS3, - } from 'https://releases.transloadit.com/uppy/v3.26.1/uppy.min.mjs' + } from 'https://releases.transloadit.com/uppy/v3.27.0/uppy.min.mjs' /** * This generator transforms a deep object into URL-encodable pairs * to work with `URLSearchParams` on the client and `body-parser` on the server. diff --git a/examples/cdn-example/index.html b/examples/cdn-example/index.html index f00a344c33..435f65930c 100644 --- a/examples/cdn-example/index.html +++ b/examples/cdn-example/index.html @@ -5,7 +5,7 @@ @@ -19,7 +19,7 @@ Dashboard, Webcam, Tus, - } from 'https://releases.transloadit.com/uppy/v3.26.1/uppy.min.mjs' + } from 'https://releases.transloadit.com/uppy/v3.27.0/uppy.min.mjs' const uppy = new Uppy({ debug: true, autoProceed: false }) .use(Dashboard, { trigger: '#uppyModalOpener' }) @@ -34,7 +34,7 @@