Google Picker (#5443)

* initial poc

* improvements

- split into two plugins
- implement photos picker
- auto login
- save access token in local storage
- document
- handle photos/files picked and send to companion
- add new hook useStore for making it easier to use localStorage data in react
- add new hook useUppyState for making it easier to use uppy state from react
- add new hook useUppyPluginState for making it easier to plugin state from react
- fix css error

* implement picker in companion

* type todo

* fix ts error

which occurs in dev when js has been built before build:ts gets called

* reuse docs

* imrpve type safety

* simplify async wrapper

* improve doc

* fix lint

* fix build error

* check if token is valid

* fix broken logging code

* pull logic out from react component

* remove docs

* improve auth ui

* fix bug

* remove unused useUppyState

* try to fix build error
This commit is contained in:
Mikael Finstad 2024-12-02 18:34:50 +08:00 committed by GitHub
parent 44a378af9e
commit afd4befee2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 1679 additions and 184 deletions

View file

@ -15,6 +15,9 @@ COMPANION_PREAUTH_SECRET=development2
# NOTE: Only enable this in development. Enabling it in production is a security risk
COMPANION_ALLOW_LOCAL_URLS=true
COMPANION_ENABLE_URL_ENDPOINT=true
COMPANION_ENABLE_GOOGLE_PICKER_ENDPOINT=true
# to enable S3
COMPANION_AWS_KEY="YOUR AWS KEY"
COMPANION_AWS_SECRET="YOUR AWS SECRET"
@ -89,3 +92,10 @@ VITE_TRANSLOADIT_TEMPLATE=***
VITE_TRANSLOADIT_SERVICE_URL=https://api2.transloadit.com
# Fill in if you want requests sent to Transloadit to be signed:
# VITE_TRANSLOADIT_SECRET=***
# For Google Photos Picker and Google Drive Picker:
VITE_GOOGLE_PICKER_CLIENT_ID=***
# For Google Drive Picker
VITE_GOOGLE_PICKER_API_KEY=***
VITE_GOOGLE_PICKER_APP_ID=***

View file

@ -25,7 +25,9 @@
"@uppy/form": "workspace:^",
"@uppy/golden-retriever": "workspace:^",
"@uppy/google-drive": "workspace:^",
"@uppy/google-drive-picker": "workspace:^",
"@uppy/google-photos": "workspace:^",
"@uppy/google-photos-picker": "workspace:^",
"@uppy/image-editor": "workspace:^",
"@uppy/informer": "workspace:^",
"@uppy/instagram": "workspace:^",

View file

@ -9,7 +9,11 @@ import { ProviderViews } from '@uppy/provider-views'
import { h, type ComponentChild } from 'preact'
import type { UppyFile, Body, Meta } from '@uppy/utils/lib/UppyFile'
import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy.js'
import type {
AsyncStore,
UnknownProviderPlugin,
UnknownProviderPluginState,
} from '@uppy/core/lib/Uppy.js'
import locale from './locale.ts'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore We don't want TS to generate types for the package.json
@ -17,12 +21,10 @@ import packageJson from '../package.json'
export type BoxOptions = CompanionPluginOptions
export default class Box<M extends Meta, B extends Body> extends UIPlugin<
BoxOptions,
M,
B,
UnknownProviderPluginState
> {
export default class Box<M extends Meta, B extends Body>
extends UIPlugin<BoxOptions, M, B, UnknownProviderPluginState>
implements UnknownProviderPlugin<M, B>
{
static VERSION = packageJson.version
icon: () => h.JSX.Element
@ -31,7 +33,7 @@ export default class Box<M extends Meta, B extends Body> extends UIPlugin<
view!: ProviderViews<M, B>
storage: typeof tokenStorage
storage: AsyncStore
files: UppyFile<M, B>[]

View file

@ -1,8 +1,8 @@
import type { UIPluginOptions } from '@uppy/core'
import type { tokenStorage } from './index.ts'
import type { AsyncStore } from '@uppy/core/lib/Uppy.js'
export interface CompanionPluginOptions extends UIPluginOptions {
storage?: typeof tokenStorage
storage?: AsyncStore
companionUrl: string
companionHeaders?: Record<string, string>
companionKeysParams?: { key: string; credentialsName: string }

View file

@ -320,10 +320,7 @@ export default class Provider<M extends Meta, B extends Body>
// Once a refresh token operation has started, we need all other request to wait for this operation (atomically)
this.#refreshingTokenPromise = (async () => {
try {
this.uppy.log(
`[CompanionClient] Refreshing expired auth token`,
'info',
)
this.uppy.log(`[CompanionClient] Refreshing expired auth token`)
const response = await super.request<{ uppyAuthToken: string }>({
path: this.refreshTokenUrl(),
method: 'POST',

View file

@ -505,7 +505,7 @@ export default class RequestClient<M extends Meta, B extends Body> {
})
const closeSocket = () => {
this.uppy.log(`Closing socket ${file.id}`, 'info')
this.uppy.log(`Closing socket ${file.id}`)
clearTimeout(activityTimeout)
if (socket) socket.close()
socket = undefined
@ -524,7 +524,7 @@ export default class RequestClient<M extends Meta, B extends Body> {
signal: socketAbortController.signal,
onFailedAttempt: () => {
if (socketAbortController.signal.aborted) return // don't log in this case
this.uppy.log(`Retrying websocket ${file.id}`, 'info')
this.uppy.log(`Retrying websocket ${file.id}`)
},
})
})()
@ -547,14 +547,14 @@ export default class RequestClient<M extends Meta, B extends Body> {
if (targetFile.id !== file.id) return
socketSend('cancel')
socketAbortController?.abort?.()
this.uppy.log(`upload ${file.id} was removed`, 'info')
this.uppy.log(`upload ${file.id} was removed`)
resolve()
}
const onCancelAll = () => {
socketSend('cancel')
socketAbortController?.abort?.()
this.uppy.log(`upload ${file.id} was canceled`, 'info')
this.uppy.log(`upload ${file.id} was canceled`)
resolve()
}

View file

@ -1,20 +1,15 @@
/**
* This module serves as an Async wrapper for LocalStorage
* Why? Because the Provider API `storage` option allows an async storage
*/
export function setItem(key: string, value: string): Promise<void> {
return new Promise((resolve) => {
localStorage.setItem(key, value)
resolve()
})
export async function setItem(key: string, value: string): Promise<void> {
localStorage.setItem(key, value)
}
export function getItem(key: string): Promise<string | null> {
return Promise.resolve(localStorage.getItem(key))
export async function getItem(key: string): Promise<string | null> {
return localStorage.getItem(key)
}
export function removeItem(key: string): Promise<void> {
return new Promise((resolve) => {
localStorage.removeItem(key)
resolve()
})
export async function removeItem(key: string): Promise<void> {
localStorage.removeItem(key)
}

View file

@ -10,6 +10,7 @@ const providerManager = require('./server/provider')
const controllers = require('./server/controllers')
const s3 = require('./server/controllers/s3')
const url = require('./server/controllers/url')
const googlePicker = require('./server/controllers/googlePicker')
const createEmitter = require('./server/emitter')
const redis = require('./server/redis')
const jobs = require('./server/jobs')
@ -120,6 +121,7 @@ module.exports.app = (optionsArg = {}) => {
app.use('*', middlewares.getCompanionMiddleware(options))
app.use('/s3', s3(options.s3))
if (options.enableUrlEndpoint) app.use('/url', url())
if (options.enableGooglePickerEndpoint) app.use('/google-picker', googlePicker())
app.post('/:providerName/preauth', express.json(), express.urlencoded({ extended: false }), middlewares.hasSessionAndProvider, middlewares.hasBody, middlewares.hasOAuthProvider, controllers.preauth)
app.get('/:providerName/connect', middlewares.hasSessionAndProvider, middlewares.hasOAuthProvider, controllers.connect)

View file

@ -17,6 +17,7 @@ const defaultOptions = {
expires: 800, // seconds
},
enableUrlEndpoint: false,
enableGooglePickerEndpoint: false,
allowLocalUrls: false,
periodicPingUrls: [],
streamingUpload: true,

View file

@ -0,0 +1,57 @@
const express = require('express')
const assert = require('node:assert')
const { startDownUpload } = require('../helpers/upload')
const { validateURL } = require('../helpers/request')
const { getURLMeta } = require('../helpers/request')
const logger = require('../logger')
const { downloadURL } = require('../download')
const { getGoogleFileSize, streamGoogleFile } = require('../provider/google/drive');
const getAuthHeader = (token) => ({ authorization: `Bearer ${token}` });
/**
*
* @param {object} req expressJS request object
* @param {object} res expressJS response object
*/
const get = async (req, res) => {
try {
logger.debug('Google Picker file import handler running', null, req.id)
const allowLocalUrls = false
const { accessToken, platform, fileId } = req.body
assert(platform === 'drive' || platform === 'photos');
const getSize = async () => {
if (platform === 'drive') {
return getGoogleFileSize({ id: fileId, token: accessToken })
}
const { size } = await getURLMeta(req.body.url, allowLocalUrls, { headers: getAuthHeader(accessToken) })
return size
}
if (platform === 'photos' && !validateURL(req.body.url, allowLocalUrls)) {
res.status(400).json({ error: 'Invalid URL' })
return
}
const download = () => {
if (platform === 'drive') {
return streamGoogleFile({ token: accessToken, id: fileId })
}
return downloadURL(req.body.url, allowLocalUrls, req.id, { headers: getAuthHeader(accessToken) })
}
await startDownUpload({ req, res, getSize, download })
} catch (err) {
logger.error(err, 'controller.googlePicker.error', req.id)
res.status(err.status || 500).json({ message: 'failed to fetch Google Picker URL' })
}
}
module.exports = () => express.Router()
.post('/get', express.json(), get)

View file

@ -1,9 +1,9 @@
const express = require('express')
const { startDownUpload } = require('../helpers/upload')
const { prepareStream } = require('../helpers/utils')
const { downloadURL } = require('../download')
const { validateURL } = require('../helpers/request')
const { getURLMeta, getProtectedGot } = require('../helpers/request')
const { getURLMeta } = require('../helpers/request')
const logger = require('../logger')
/**
@ -12,27 +12,6 @@ const logger = require('../logger')
* @param {string | Buffer | Buffer[]} chunk
*/
/**
* Downloads the content in the specified url, and passes the data
* to the callback chunk by chunk.
*
* @param {string} url
* @param {boolean} allowLocalIPs
* @param {string} traceId
* @returns {Promise}
*/
const downloadURL = async (url, allowLocalIPs, traceId) => {
try {
const protectedGot = await getProtectedGot({ allowLocalIPs })
const stream = protectedGot.stream.get(url, { responseType: 'json' })
const { size } = await prepareStream(stream)
return { stream, size }
} catch (err) {
logger.error(err, 'controller.url.download.error', traceId)
throw err
}
}
/**
* Fetches the size and content type of a URL
*

View file

@ -0,0 +1,28 @@
const logger = require('./logger')
const { getProtectedGot } = require('./helpers/request')
const { prepareStream } = require('./helpers/utils')
/**
* Downloads the content in the specified url, and passes the data
* to the callback chunk by chunk.
*
* @param {string} url
* @param {boolean} allowLocalIPs
* @param {string} traceId
* @returns {Promise}
*/
const downloadURL = async (url, allowLocalIPs, traceId, options) => {
try {
const protectedGot = await getProtectedGot({ allowLocalIPs })
const stream = protectedGot.stream.get(url, { responseType: 'json', ...options })
const { size } = await prepareStream(stream)
return { stream, size }
} catch (err) {
logger.error(err, 'controller.url.download.error', traceId)
throw err
}
}
module.exports = {
downloadURL,
}

View file

@ -105,10 +105,10 @@ module.exports.getProtectedGot = getProtectedGot
* @param {boolean} allowLocalIPs
* @returns {Promise<{name: string, type: string, size: number}>}
*/
exports.getURLMeta = async (url, allowLocalIPs = false) => {
exports.getURLMeta = async (url, allowLocalIPs = false, options = undefined) => {
async function requestWithMethod (method) {
const protectedGot = await getProtectedGot({ allowLocalIPs })
const stream = protectedGot.stream(url, { method, throwHttpErrors: false })
const stream = protectedGot.stream(url, { method, throwHttpErrors: false, ...options })
return new Promise((resolve, reject) => (
stream

View file

@ -43,6 +43,53 @@ async function getStats ({ id, token }) {
return stats
}
async function streamGoogleFile({ token, id: idIn }) {
const client = await getClient({ token })
const { mimeType, id, exportLinks } = await getStats({ id: idIn, token })
let stream
if (isGsuiteFile(mimeType)) {
const mimeType2 = getGsuiteExportType(mimeType)
logger.info(`calling google file export for ${id} to ${mimeType2}`, 'provider.drive.export')
// GSuite files exported with large converted size results in error using standard export method.
// Error message: "This file is too large to be exported.".
// Issue logged in Google APIs: https://github.com/googleapis/google-api-nodejs-client/issues/3446
// Implemented based on the answer from StackOverflow: https://stackoverflow.com/a/59168288
const mimeTypeExportLink = exportLinks?.[mimeType2]
if (mimeTypeExportLink) {
const gSuiteFilesClient = (await got).extend({
headers: {
authorization: `Bearer ${token}`,
},
})
stream = gSuiteFilesClient.stream.get(mimeTypeExportLink, { responseType: 'json' })
} else {
stream = client.stream.get(`files/${encodeURIComponent(id)}/export`, { searchParams: { supportsAllDrives: true, mimeType: mimeType2 }, responseType: 'json' })
}
} else {
stream = client.stream.get(`files/${encodeURIComponent(id)}`, { searchParams: { alt: 'media', supportsAllDrives: true }, responseType: 'json' })
}
await prepareStream(stream)
return { stream }
}
async function getGoogleFileSize({ id, token }) {
const { mimeType, size } = await getStats({ id, token })
if (isGsuiteFile(mimeType)) {
// GSuite file sizes cannot be predetermined (but are max 10MB)
// e.g. Transfer-Encoding: chunked
return undefined
}
return parseInt(size, 10)
}
/**
* Adapter for API https://developers.google.com/drive/api/v3/
*/
@ -124,7 +171,7 @@ class Drive extends Provider {
}
// eslint-disable-next-line class-methods-use-this
async download ({ id: idIn, token }) {
async download ({ id, token }) {
if (mockAccessTokenExpiredError != null) {
logger.warn(`Access token: ${token}`)
@ -135,57 +182,23 @@ class Drive extends Provider {
}
return withGoogleErrorHandling(Drive.oauthProvider, 'provider.drive.download.error', async () => {
const client = await getClient({ token })
const { mimeType, id, exportLinks } = await getStats({ id: idIn, token })
let stream
if (isGsuiteFile(mimeType)) {
const mimeType2 = getGsuiteExportType(mimeType)
logger.info(`calling google file export for ${id} to ${mimeType2}`, 'provider.drive.export')
// GSuite files exported with large converted size results in error using standard export method.
// Error message: "This file is too large to be exported.".
// Issue logged in Google APIs: https://github.com/googleapis/google-api-nodejs-client/issues/3446
// Implemented based on the answer from StackOverflow: https://stackoverflow.com/a/59168288
const mimeTypeExportLink = exportLinks?.[mimeType2]
if (mimeTypeExportLink) {
const gSuiteFilesClient = (await got).extend({
headers: {
authorization: `Bearer ${token}`,
},
})
stream = gSuiteFilesClient.stream.get(mimeTypeExportLink, { responseType: 'json' })
} else {
stream = client.stream.get(`files/${encodeURIComponent(id)}/export`, { searchParams: { supportsAllDrives: true, mimeType: mimeType2 }, responseType: 'json' })
}
} else {
stream = client.stream.get(`files/${encodeURIComponent(id)}`, { searchParams: { alt: 'media', supportsAllDrives: true }, responseType: 'json' })
}
await prepareStream(stream)
return { stream }
return streamGoogleFile({ token, id })
})
}
// eslint-disable-next-line class-methods-use-this
async size ({ id, token }) {
return withGoogleErrorHandling(Drive.oauthProvider, 'provider.drive.size.error', async () => {
const { mimeType, size } = await getStats({ id, token })
if (isGsuiteFile(mimeType)) {
// GSuite file sizes cannot be predetermined (but are max 10MB)
// e.g. Transfer-Encoding: chunked
return undefined
}
return parseInt(size, 10)
})
return withGoogleErrorHandling(Drive.oauthProvider, 'provider.drive.size.error', async () => (
getGoogleFileSize({ id, token })
))
}
}
Drive.prototype.logout = logout
Drive.prototype.refreshToken = refreshToken
module.exports = Drive
module.exports = {
Drive,
streamGoogleFile,
getGoogleFileSize,
}

View file

@ -3,7 +3,7 @@
*/
const dropbox = require('./dropbox')
const box = require('./box')
const drive = require('./google/drive')
const { Drive } = require('./google/drive')
const googlephotos = require('./google/googlephotos')
const instagram = require('./instagram/graph')
const facebook = require('./facebook')
@ -68,7 +68,7 @@ module.exports.getProviderMiddleware = (providers, grantConfig) => {
* @returns {Record<string, typeof Provider>}
*/
module.exports.getDefaultProviders = () => {
const providers = { dropbox, box, drive, googlephotos, facebook, onedrive, zoom, instagram, unsplash }
const providers = { dropbox, box, drive: Drive, googlephotos, facebook, onedrive, zoom, instagram, unsplash }
return providers
}

View file

@ -152,6 +152,7 @@ const getConfigFromEnv = () => {
validHosts,
},
enableUrlEndpoint: process.env.COMPANION_ENABLE_URL_ENDPOINT === 'true',
enableGooglePickerEndpoint: process.env.COMPANION_ENABLE_GOOGLE_PICKER_ENDPOINT === 'true',
periodicPingUrls: process.env.COMPANION_PERIODIC_PING_URLS ? process.env.COMPANION_PERIODIC_PING_URLS.split(',') : [],
periodicPingInterval: process.env.COMPANION_PERIODIC_PING_INTERVAL
? parseInt(process.env.COMPANION_PERIODIC_PING_INTERVAL, 10) : undefined,

View file

@ -142,8 +142,25 @@ export type UnknownProviderPluginState = {
currentFolderId: PartialTreeId
username: string | null
}
export interface AsyncStore {
getItem: (key: string) => Promise<string | null>
setItem: (key: string, value: string) => Promise<void>
removeItem: (key: string) => Promise<void>
}
/**
* This is a base for a provider that does not necessarily use the Companion-assisted OAuth2 flow
*/
export interface BaseProviderPlugin {
title: string
icon: () => h.JSX.Element
storage: AsyncStore
}
/*
* UnknownProviderPlugin can be any Companion plugin (such as Google Drive).
* UnknownProviderPlugin can be any Companion plugin (such as Google Drive)
* that uses the Companion-assisted OAuth flow.
* As the plugins are passed around throughout Uppy we need a generic type for this.
* It may seems like duplication, but this type safe. Changing the type of `storage`
* will error in the `Provider` class of @uppy/companion-client and vice versa.
@ -154,18 +171,12 @@ export type UnknownProviderPluginState = {
export type UnknownProviderPlugin<
M extends Meta,
B extends Body,
> = UnknownPlugin<M, B, UnknownProviderPluginState> & {
title: string
rootFolderId: string | null
files: UppyFile<M, B>[]
icon: () => h.JSX.Element
provider: CompanionClientProvider
storage: {
getItem: (key: string) => Promise<string | null>
setItem: (key: string, value: string) => Promise<void>
removeItem: (key: string) => Promise<void>
> = UnknownPlugin<M, B, UnknownProviderPluginState> &
BaseProviderPlugin & {
rootFolderId: string | null
files: UppyFile<M, B>[]
provider: CompanionClientProvider
}
}
/*
* UnknownSearchProviderPlugin can be any search Companion plugin (such as Unsplash).
@ -185,11 +196,10 @@ export type UnknownSearchProviderPluginState = {
export type UnknownSearchProviderPlugin<
M extends Meta,
B extends Body,
> = UnknownPlugin<M, B, UnknownSearchProviderPluginState> & {
title: string
icon: () => h.JSX.Element
provider: CompanionClientSearchProvider
}
> = UnknownPlugin<M, B, UnknownSearchProviderPluginState> &
BaseProviderPlugin & {
provider: CompanionClientSearchProvider
}
export interface UploadResult<M extends Meta, B extends Body> {
successful?: UppyFile<M, B>[]
@ -712,8 +722,7 @@ export class Uppy<
const updatedFiles = { ...this.getState().files }
if (!updatedFiles[fileID]) {
this.log(
'Was trying to set metadata for a file that has been removed: ',
fileID,
`Was trying to set metadata for a file that has been removed: ${fileID}`,
)
return
}
@ -1948,7 +1957,7 @@ export class Uppy<
* Passes messages to a function, provided in `opts.logger`.
* If `opts.logger: Uppy.debugLogger` or `opts.debug: true`, logs to the browser console.
*/
log(message: string | Record<any, any> | Error, type?: string): void {
log(message: unknown, type?: 'error' | 'warning'): void {
const { logger } = this.opts
switch (type) {
case 'error':

View file

@ -41,6 +41,9 @@ export default {
openFolderNamed: 'Open folder %{name}',
cancel: 'Cancel',
logOut: 'Log out',
logIn: 'Log in',
pickFiles: 'Pick files',
pickPhotos: 'Pick photos',
filter: 'Filter',
resetFilter: 'Reset filter',
loading: 'Loading...',
@ -63,5 +66,6 @@ export default {
additionalRestrictionsFailed:
'%{count} additional restrictions were not fulfilled',
unnamed: 'Unnamed',
pleaseWait: 'Please wait',
},
}

View file

@ -0,0 +1,28 @@
import { useCallback, useEffect, useState } from 'preact/hooks'
import type { AsyncStore } from './Uppy'
export default function useStore(
store: AsyncStore,
key: string,
): [string | undefined | null, (v: string | null) => Promise<void>] {
const [value, setValueState] = useState<string | null | undefined>()
useEffect(() => {
;(async () => {
setValueState(await store.getItem(key))
})()
}, [key, store])
const setValue = useCallback(
async (v: string | null) => {
setValueState(v)
if (v == null) {
return store.removeItem(key)
}
return store.setItem(key, v)
},
[key, store],
)
return [value, setValue]
}

View file

@ -9,7 +9,11 @@ import { ProviderViews } from '@uppy/provider-views'
import { h, type ComponentChild } from 'preact'
import type { UppyFile, Body, Meta } from '@uppy/utils/lib/UppyFile'
import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy.js'
import type {
AsyncStore,
UnknownProviderPlugin,
UnknownProviderPluginState,
} from '@uppy/core/lib/Uppy.js'
import locale from './locale.ts'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore We don't want TS to generate types for the package.json
@ -17,12 +21,10 @@ import packageJson from '../package.json'
export type DropboxOptions = CompanionPluginOptions
export default class Dropbox<M extends Meta, B extends Body> extends UIPlugin<
DropboxOptions,
M,
B,
UnknownProviderPluginState
> {
export default class Dropbox<M extends Meta, B extends Body>
extends UIPlugin<DropboxOptions, M, B, UnknownProviderPluginState>
implements UnknownProviderPlugin<M, B>
{
static VERSION = packageJson.version
icon: () => h.JSX.Element
@ -31,7 +33,7 @@ export default class Dropbox<M extends Meta, B extends Body> extends UIPlugin<
view!: ProviderViews<M, B>
storage: typeof tokenStorage
storage: AsyncStore
files: UppyFile<M, B>[]

View file

@ -9,7 +9,11 @@ import { ProviderViews } from '@uppy/provider-views'
import { h, type ComponentChild } from 'preact'
import type { UppyFile, Body, Meta } from '@uppy/utils/lib/UppyFile'
import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy.js'
import type {
AsyncStore,
UnknownProviderPlugin,
UnknownProviderPluginState,
} from '@uppy/core/lib/Uppy.js'
import locale from './locale.ts'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore We don't want TS to generate types for the package.json
@ -17,12 +21,10 @@ import packageJson from '../package.json'
export type FacebookOptions = CompanionPluginOptions
export default class Facebook<M extends Meta, B extends Body> extends UIPlugin<
FacebookOptions,
M,
B,
UnknownProviderPluginState
> {
export default class Facebook<M extends Meta, B extends Body>
extends UIPlugin<FacebookOptions, M, B, UnknownProviderPluginState>
implements UnknownProviderPlugin<M, B>
{
static VERSION = packageJson.version
icon: () => h.JSX.Element
@ -31,7 +33,7 @@ export default class Facebook<M extends Meta, B extends Body> extends UIPlugin<
view!: ProviderViews<M, B>
storage: typeof tokenStorage
storage: AsyncStore
files: UppyFile<M, B>[]

View file

@ -0,0 +1 @@
tsconfig.*

View file

@ -0,0 +1 @@
# @uppy/google-drive-picker

View file

@ -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.

View file

@ -0,0 +1,18 @@
# @uppy/google-drive-picker
<img src="https://uppy.io/img/logo.svg" width="120" alt="Uppy logo: a smiling puppy above a pink upwards arrow" align="right">
[![npm version](https://img.shields.io/npm/v/@uppy/google-drive-picker.svg?style=flat-square)](https://www.npmjs.com/package/@uppy/google-drive-picker)
![CI status for Uppy tests](https://github.com/transloadit/uppy/workflows/Tests/badge.svg)
![CI status for Companion tests](https://github.com/transloadit/uppy/workflows/Companion/badge.svg)
![CI status for browser tests](https://github.com/transloadit/uppy/workflows/End-to-end%20tests/badge.svg)
The Google Drive Picker plugin for Uppy lets users import files from their
Google Drive account using the new Picker API.
Documentation for this plugin can be found on the
[Uppy website](https://uppy.io/docs/google-drive-picker).
## License
The [MIT License](./LICENSE).

View file

@ -0,0 +1,33 @@
{
"name": "@uppy/google-drive-picker",
"description": "The Google Drive Picker plugin for Uppy lets users import files from their Google Drive account",
"version": "0.1.0",
"license": "MIT",
"main": "lib/index.js",
"type": "module",
"keywords": [
"file uploader",
"google drive",
"google picker",
"cloud storage",
"uppy",
"uppy-plugin"
],
"homepage": "https://uppy.io",
"bugs": {
"url": "https://github.com/transloadit/uppy/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/transloadit/uppy.git"
},
"dependencies": {
"@uppy/companion-client": "workspace:^",
"@uppy/provider-views": "workspace:^",
"@uppy/utils": "workspace:^",
"preact": "^10.5.13"
},
"peerDependencies": {
"@uppy/core": "workspace:^"
}
}

View file

@ -0,0 +1,115 @@
import { h } from 'preact'
import { UIPlugin, Uppy } from '@uppy/core'
import { GooglePickerView } from '@uppy/provider-views'
import { GoogleDriveIcon } from '@uppy/provider-views/lib/GooglePicker/icons.js'
import {
RequestClient,
type CompanionPluginOptions,
tokenStorage,
} from '@uppy/companion-client'
import type { PickedItem } from '@uppy/provider-views/lib/GooglePicker/googlePicker.js'
import type { Body, Meta } from '@uppy/utils/lib/UppyFile'
import type { AsyncStore, BaseProviderPlugin } from '@uppy/core/lib/Uppy.js'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore We don't want TS to generate types for the package.json
import packageJson from '../package.json'
import locale from './locale.ts'
export type GoogleDrivePickerOptions = CompanionPluginOptions & {
clientId: string
apiKey: string
appId: string
}
export default class GoogleDrivePicker<
M extends Meta & { width: number; height: number },
B extends Body,
>
extends UIPlugin<GoogleDrivePickerOptions, M, B>
implements BaseProviderPlugin
{
static VERSION = packageJson.version
static requestClientId = GoogleDrivePicker.name
type = 'acquirer'
icon = GoogleDriveIcon
storage: AsyncStore
defaultLocale = locale
constructor(uppy: Uppy<M, B>, opts: GoogleDrivePickerOptions) {
super(uppy, opts)
this.id = this.opts.id || 'GoogleDrivePicker'
this.storage = this.opts.storage || tokenStorage
this.i18nInit()
this.title = this.i18n('pluginNameGoogleDrive')
const client = new RequestClient(uppy, {
pluginId: this.id,
provider: 'url',
companionUrl: this.opts.companionUrl,
companionHeaders: this.opts.companionHeaders,
companionCookiesRule: this.opts.companionCookiesRule,
})
this.uppy.registerRequestClient(GoogleDrivePicker.requestClientId, client)
}
install(): void {
const { target } = this.opts
if (target) {
this.mount(target, this)
}
}
uninstall(): void {
this.unmount()
}
private handleFilesPicked = async (
files: PickedItem[],
accessToken: string,
) => {
this.uppy.addFiles(
files.map(({ id, mimeType, name, ...rest }) => {
return {
source: this.id,
name,
type: mimeType,
data: {
size: null, // defer to companion to determine size
},
isRemote: true,
remote: {
companionUrl: this.opts.companionUrl,
url: `${this.opts.companionUrl}/google-picker/get`,
body: {
fileId: id,
accessToken,
...rest,
},
requestClientId: GoogleDrivePicker.requestClientId,
},
}
}),
)
}
render = () => (
<GooglePickerView
storage={this.storage}
pickerType="drive"
uppy={this.uppy}
clientId={this.opts.clientId}
apiKey={this.opts.apiKey}
appId={this.opts.appId}
onFilesPicked={this.handleFilesPicked}
/>
)
}

View file

@ -0,0 +1 @@
export { default } from './GoogleDrivePicker.tsx'

View file

@ -0,0 +1,3 @@
export default {
strings: {},
}

View file

@ -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"
}
]
}

View file

@ -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",
},
],
}

View file

@ -9,7 +9,11 @@ import { ProviderViews } from '@uppy/provider-views'
import { h, type ComponentChild } from 'preact'
import type { UppyFile, Body, Meta } from '@uppy/utils/lib/UppyFile'
import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy.js'
import type {
AsyncStore,
UnknownProviderPlugin,
UnknownProviderPluginState,
} from '@uppy/core/lib/Uppy.js'
import DriveProviderViews from './DriveProviderViews.ts'
import locale from './locale.ts'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@ -18,10 +22,10 @@ import packageJson from '../package.json'
export type GoogleDriveOptions = CompanionPluginOptions
export default class GoogleDrive<
M extends Meta,
B extends Body,
> extends UIPlugin<GoogleDriveOptions, M, B, UnknownProviderPluginState> {
export default class GoogleDrive<M extends Meta, B extends Body>
extends UIPlugin<GoogleDriveOptions, M, B, UnknownProviderPluginState>
implements UnknownProviderPlugin<M, B>
{
static VERSION = packageJson.version
icon: () => h.JSX.Element
@ -30,7 +34,7 @@ export default class GoogleDrive<
view!: ProviderViews<M, B>
storage: typeof tokenStorage
storage: AsyncStore
files: UppyFile<M, B>[]

View file

@ -0,0 +1 @@
tsconfig.*

View file

@ -0,0 +1 @@
# @uppy/google-photos-picker

View file

@ -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.

View file

@ -0,0 +1,18 @@
# @uppy/google-photos-picker
<img src="https://uppy.io/img/logo.svg" width="120" alt="Uppy logo: a smiling puppy above a pink upwards arrow" align="right">
[![npm version](https://img.shields.io/npm/v/@uppy/google-photos-picker.svg?style=flat-square)](https://www.npmjs.com/package/@uppy/google-photos-picker)
![CI status for Uppy tests](https://github.com/transloadit/uppy/workflows/Tests/badge.svg)
![CI status for Companion tests](https://github.com/transloadit/uppy/workflows/Companion/badge.svg)
![CI status for browser tests](https://github.com/transloadit/uppy/workflows/End-to-end%20tests/badge.svg)
The Google Photos Picker plugin for Uppy lets users import photos from their
Google Photos account using the new Picker API.
Documentation for this plugin can be found on the
[Uppy website](https://uppy.io/docs/google-photos-picker).
## License
The [MIT License](./LICENSE).

View file

@ -0,0 +1,33 @@
{
"name": "@uppy/google-photos-picker",
"description": "The Google Photos Picker plugin for Uppy lets users import files from their Google Photos account",
"version": "0.1.0",
"license": "MIT",
"main": "lib/index.js",
"type": "module",
"keywords": [
"file uploader",
"google photos",
"google picker",
"cloud storage",
"uppy",
"uppy-plugin"
],
"homepage": "https://uppy.io",
"bugs": {
"url": "https://github.com/transloadit/uppy/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/transloadit/uppy.git"
},
"dependencies": {
"@uppy/companion-client": "workspace:^",
"@uppy/provider-views": "workspace:^",
"@uppy/utils": "workspace:^",
"preact": "^10.5.13"
},
"peerDependencies": {
"@uppy/core": "workspace:^"
}
}

View file

@ -0,0 +1,111 @@
import { h } from 'preact'
import { UIPlugin, Uppy } from '@uppy/core'
import { GooglePickerView } from '@uppy/provider-views'
import { GooglePhotosIcon } from '@uppy/provider-views/lib/GooglePicker/icons.js'
import {
RequestClient,
type CompanionPluginOptions,
tokenStorage,
} from '@uppy/companion-client'
import type { PickedItem } from '@uppy/provider-views/lib/GooglePicker/googlePicker.js'
import type { Body, Meta } from '@uppy/utils/lib/UppyFile'
import type { AsyncStore, BaseProviderPlugin } from '@uppy/core/lib/Uppy.js'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore We don't want TS to generate types for the package.json
import packageJson from '../package.json'
import locale from './locale.ts'
export type GooglePhotosPickerOptions = CompanionPluginOptions & {
clientId: string
}
export default class GooglePhotosPicker<
M extends Meta & { width: number; height: number },
B extends Body,
>
extends UIPlugin<GooglePhotosPickerOptions, M, B>
implements BaseProviderPlugin
{
static VERSION = packageJson.version
static requestClientId = GooglePhotosPicker.name
type = 'acquirer'
icon = GooglePhotosIcon
storage: AsyncStore
defaultLocale = locale
constructor(uppy: Uppy<M, B>, opts: GooglePhotosPickerOptions) {
super(uppy, opts)
this.id = this.opts.id || 'GooglePhotosPicker'
this.storage = this.opts.storage || tokenStorage
this.i18nInit()
this.title = this.i18n('pluginNameGooglePhotos')
const client = new RequestClient(uppy, {
pluginId: this.id,
provider: 'url',
companionUrl: this.opts.companionUrl,
companionHeaders: this.opts.companionHeaders,
companionCookiesRule: this.opts.companionCookiesRule,
})
this.uppy.registerRequestClient(GooglePhotosPicker.requestClientId, client)
}
install(): void {
const { target } = this.opts
if (target) {
this.mount(target, this)
}
}
uninstall(): void {
this.unmount()
}
private handleFilesPicked = async (
files: PickedItem[],
accessToken: string,
) => {
this.uppy.addFiles(
files.map(({ id, mimeType, name, ...rest }) => {
return {
source: this.id,
name,
type: mimeType,
data: {
size: null, // defer to companion to determine size
},
isRemote: true,
remote: {
companionUrl: this.opts.companionUrl,
url: `${this.opts.companionUrl}/google-picker/get`,
body: {
fileId: id,
accessToken,
...rest,
},
requestClientId: GooglePhotosPicker.requestClientId,
},
}
}),
)
}
render = () => (
<GooglePickerView
storage={this.storage}
pickerType="photos"
uppy={this.uppy}
clientId={this.opts.clientId}
onFilesPicked={this.handleFilesPicked}
/>
)
}

View file

@ -0,0 +1 @@
export { default } from './GooglePhotosPicker.tsx'

View file

@ -0,0 +1,3 @@
export default {
strings: {},
}

View file

@ -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"
}
]
}

View file

@ -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",
},
],
}

View file

@ -9,7 +9,11 @@ import {
import { h, type ComponentChild } from 'preact'
import type { UppyFile, Body, Meta } from '@uppy/utils/lib/UppyFile'
import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy.js'
import type {
AsyncStore,
UnknownProviderPlugin,
UnknownProviderPluginState,
} from '@uppy/core/lib/Uppy.js'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore We don't want TS to generate types for the package.json
@ -18,10 +22,10 @@ import locale from './locale.ts'
export type GooglePhotosOptions = CompanionPluginOptions
export default class GooglePhotos<
M extends Meta,
B extends Body,
> extends UIPlugin<GooglePhotosOptions, M, B, UnknownProviderPluginState> {
export default class GooglePhotos<M extends Meta, B extends Body>
extends UIPlugin<GooglePhotosOptions, M, B, UnknownProviderPluginState>
implements UnknownProviderPlugin<M, B>
{
static VERSION = packageJson.version
icon: () => h.JSX.Element
@ -30,7 +34,7 @@ export default class GooglePhotos<
view!: ProviderViews<M, B>
storage: typeof tokenStorage
storage: AsyncStore
files: UppyFile<M, B>[]

View file

@ -9,7 +9,11 @@ import { ProviderViews } from '@uppy/provider-views'
import { h, type ComponentChild } from 'preact'
import type { UppyFile, Body, Meta } from '@uppy/utils/lib/UppyFile'
import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy.js'
import type {
AsyncStore,
UnknownProviderPlugin,
UnknownProviderPluginState,
} from '@uppy/core/lib/Uppy.js'
import locale from './locale.ts'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore We don't want TS to generate types for the package.json
@ -17,12 +21,10 @@ import packageJson from '../package.json'
export type InstagramOptions = CompanionPluginOptions
export default class Instagram<M extends Meta, B extends Body> extends UIPlugin<
InstagramOptions,
M,
B,
UnknownProviderPluginState
> {
export default class Instagram<M extends Meta, B extends Body>
extends UIPlugin<InstagramOptions, M, B, UnknownProviderPluginState>
implements UnknownProviderPlugin<M, B>
{
static VERSION = packageJson.version
icon: () => h.JSX.Element
@ -31,7 +33,7 @@ export default class Instagram<M extends Meta, B extends Body> extends UIPlugin<
view!: ProviderViews<M, B>
storage: typeof tokenStorage
storage: AsyncStore
files: UppyFile<M, B>[]

View file

@ -8,8 +8,12 @@ import { UIPlugin, Uppy } from '@uppy/core'
import { ProviderViews } from '@uppy/provider-views'
import { h, type ComponentChild } from 'preact'
import type { AsyncStore } from '@uppy/core/src/Uppy.js'
import type { UppyFile, Body, Meta } from '@uppy/utils/lib/UppyFile'
import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy.js'
import type {
UnknownProviderPlugin,
UnknownProviderPluginState,
} from '@uppy/core/lib/Uppy.js'
import locale from './locale.ts'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore We don't want TS to generate types for the package.json
@ -17,12 +21,10 @@ import packageJson from '../package.json'
export type OneDriveOptions = CompanionPluginOptions
export default class OneDrive<M extends Meta, B extends Body> extends UIPlugin<
OneDriveOptions,
M,
B,
UnknownProviderPluginState
> {
export default class OneDrive<M extends Meta, B extends Body>
extends UIPlugin<OneDriveOptions, M, B, UnknownProviderPluginState>
implements UnknownProviderPlugin<M, B>
{
static VERSION = packageJson.version
icon: () => h.JSX.Element
@ -31,7 +33,7 @@ export default class OneDrive<M extends Meta, B extends Body> extends UIPlugin<
view!: ProviderViews<M, B>
storage: typeof tokenStorage
storage: AsyncStore
files: UppyFile<M, B>[]

View file

@ -26,6 +26,9 @@
"preact": "^10.5.13"
},
"devDependencies": {
"@types/gapi": "^0.0.47",
"@types/google.accounts": "^0.0.14",
"@types/google.picker": "^0.0.42",
"vitest": "^1.6.0"
},
"peerDependencies": {

View file

@ -0,0 +1,234 @@
import { h } from 'preact'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import type { Uppy } from '@uppy/core'
import useStore from '@uppy/core/lib/useStore.js'
import type { AsyncStore } from '@uppy/core/lib/Uppy.js'
import {
authorize,
ensureScriptsInjected,
InvalidTokenError,
logout,
pollPickingSession,
showDrivePicker,
showPhotosPicker,
type PickedItem,
type PickingSession,
} from './googlePicker.js'
import AuthView from '../ProviderView/AuthView.js'
import { GoogleDriveIcon, GooglePhotosIcon } from './icons.js'
export type GooglePickerViewProps = {
uppy: Uppy<any, any>
clientId: string
onFilesPicked: (files: PickedItem[], accessToken: string) => void
storage: AsyncStore
} & (
| {
pickerType: 'drive'
apiKey: string
appId: string
}
| {
pickerType: 'photos'
apiKey?: undefined
appId?: undefined
}
)
export default function GooglePickerView({
uppy,
clientId,
onFilesPicked,
pickerType,
apiKey,
appId,
storage,
}: GooglePickerViewProps) {
const [loading, setLoading] = useState(false)
const [accessToken, setAccessTokenStored] = useStore(
storage,
`uppy:google-${pickerType}-picker:accessToken`,
)
const pickingSessionRef = useRef<PickingSession>()
const accessTokenRef = useRef(accessToken)
const shownPickerRef = useRef(false)
const setAccessToken = useCallback(
(t: string | null) => {
uppy.log('Access token updated')
setAccessTokenStored(t)
accessTokenRef.current = t
},
[setAccessTokenStored, uppy],
)
// keep access token in sync with the ref
useEffect(() => {
accessTokenRef.current = accessToken
}, [accessToken])
const showPicker = useCallback(
async (signal?: AbortSignal) => {
let newAccessToken = accessToken
const doShowPicker = async (token: string) => {
if (pickerType === 'drive') {
await showDrivePicker({ token, apiKey, appId, onFilesPicked, signal })
} else {
// photos
const onPickingSessionChange = (
newPickingSession: PickingSession,
) => {
pickingSessionRef.current = newPickingSession
}
await showPhotosPicker({
token,
pickingSession: pickingSessionRef.current,
onPickingSessionChange,
signal,
})
}
}
setLoading(true)
try {
try {
await ensureScriptsInjected(pickerType)
if (newAccessToken == null) {
newAccessToken = await authorize({ clientId, pickerType })
}
if (newAccessToken == null) throw new Error()
await doShowPicker(newAccessToken)
shownPickerRef.current = true
setAccessToken(newAccessToken)
} catch (err) {
if (err instanceof InvalidTokenError) {
uppy.log('Token is invalid or expired, reauthenticating')
newAccessToken = await authorize({
pickerType,
accessToken: newAccessToken,
clientId,
})
// now try again:
await doShowPicker(newAccessToken)
shownPickerRef.current = true
setAccessToken(newAccessToken)
} else {
throw err
}
}
} catch (err) {
if (
err instanceof Error &&
'type' in err &&
err.type === 'popup_closed'
) {
// user closed the auth popup, ignore
} else {
setAccessToken(null)
uppy.log(err)
}
} finally {
setLoading(false)
}
},
[
accessToken,
apiKey,
appId,
clientId,
onFilesPicked,
pickerType,
setAccessToken,
uppy,
],
)
useEffect(() => {
const abortController = new AbortController()
pollPickingSession({
pickingSessionRef,
accessTokenRef,
signal: abortController.signal,
onFilesPicked,
onError: (err) => uppy.log(err),
})
return () => abortController.abort()
}, [onFilesPicked, uppy])
useEffect(() => {
// when mounting, once we have a token, be nice to the user and automatically show the picker
// accessToken === undefined means not yet loaded from storage, so wait for that first
if (accessToken === undefined || shownPickerRef.current) {
return undefined
}
const abortController = new AbortController()
showPicker(abortController.signal)
return () => {
// only abort the picker if it's not yet shown
if (!shownPickerRef.current) abortController.abort()
}
}, [accessToken, showPicker])
const handleLogoutClick = useCallback(async () => {
if (accessToken) {
await logout(accessToken)
setAccessToken(null)
pickingSessionRef.current = undefined
}
}, [accessToken, setAccessToken])
if (loading) {
return <div>{uppy.i18n('pleaseWait')}...</div>
}
if (accessToken == null) {
return (
<AuthView
pluginName={
pickerType === 'drive' ?
uppy.i18n('pluginNameGoogleDrive')
: uppy.i18n('pluginNameGooglePhotos')
}
pluginIcon={pickerType === 'drive' ? GoogleDriveIcon : GooglePhotosIcon}
handleAuth={showPicker}
i18n={uppy.i18nArray}
loading={loading}
/>
)
}
return (
<div style={{ textAlign: 'center' }}>
<button
type="button"
className="uppy-u-reset uppy-c-btn uppy-c-btn-primary"
style={{ display: 'block', marginBottom: '1em' }}
disabled={loading}
onClick={() => showPicker()}
>
{pickerType === 'drive' ?
uppy.i18n('pickFiles')
: uppy.i18n('pickPhotos')}
</button>
<button
type="button"
className="uppy-u-reset uppy-c-btn"
disabled={loading}
onClick={handleLogoutClick}
>
{uppy.i18n('logOut')}
</button>
</div>
)
}

View file

@ -0,0 +1,425 @@
import { type MutableRef } from 'preact/hooks'
// https://developers.google.com/photos/picker/reference/rest/v1/mediaItems
export interface MediaItemBase {
id: string
createTime: string
}
interface MediaFileMetadataBase {
width: number
height: number
cameraMake: string
cameraModel: string
}
interface MediaFileBase {
baseUrl: string
mimeType: string
filename: string
}
export interface VideoMediaItem extends MediaItemBase {
type: 'VIDEO'
mediaFile: MediaFileBase & {
mediaFileMetadata: MediaFileMetadataBase & {
videoMetadata: {
fps: number
processingStatus: 'UNSPECIFIED' | 'PROCESSING' | 'READY' | 'FAILED'
}
}
}
}
export interface PhotoMediaItem extends MediaItemBase {
type: 'PHOTO'
mediaFile: MediaFileBase & {
mediaFileMetadata: MediaFileMetadataBase & {
photoMetadata: {
focalLength: number
apertureFNumber: number
isoEquivalent: number
exposureTime: string
}
}
}
}
export interface UnspecifiedMediaItem extends MediaItemBase {
type: 'TYPE_UNSPECIFIED'
mediaFile: MediaFileBase
}
export type MediaItem = VideoMediaItem | PhotoMediaItem | UnspecifiedMediaItem
// https://developers.google.com/photos/picker/reference/rest/v1/sessions
export interface PickingSession {
id: string
pickerUri: string
pollingConfig: {
pollInterval: string
timeoutIn: string
}
expireTime: string
mediaItemsSet: boolean
}
export interface PickedItemBase {
id: string
mimeType: string
name: string
}
export interface PickedDriveItem extends PickedItemBase {
platform: 'drive'
}
export interface PickedPhotosItem extends PickedItemBase {
platform: 'photos'
url: string
}
export type PickedItem = PickedPhotosItem | PickedDriveItem
type PickerType = 'drive' | 'photos'
const getAuthHeader = (token: string) => ({
authorization: `Bearer ${token}`,
})
const injectedScripts = new Set<string>()
let driveApiLoaded = false
// https://stackoverflow.com/a/39008859/6519037
async function injectScript(src: string) {
if (injectedScripts.has(src)) return
await new Promise<void>((resolve, reject) => {
const script = document.createElement('script')
script.src = src
script.addEventListener('load', () => resolve())
script.addEventListener('error', (e) => reject(e.error))
document.head.appendChild(script)
})
injectedScripts.add(src)
}
export async function ensureScriptsInjected(
pickerType: PickerType,
): Promise<void> {
await Promise.all([
injectScript('https://accounts.google.com/gsi/client'), // Google Identity Services
(async () => {
await injectScript('https://apis.google.com/js/api.js')
if (pickerType === 'drive' && !driveApiLoaded) {
await new Promise<void>((resolve) =>
gapi.load('client:picker', () => resolve()),
)
await gapi.client.load(
'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest',
)
driveApiLoaded = true
}
})(),
])
}
async function isTokenValid(
accessToken: string,
signal: AbortSignal | undefined,
) {
const response = await fetch(
`https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=${encodeURIComponent(accessToken)}`,
{ signal },
)
if (response.ok) {
return true
}
// console.warn('Token is invalid or expired:', response.status, await response.text());
// Token is invalid or expired
return false
}
export async function authorize({
pickerType,
clientId,
accessToken,
}: {
pickerType: PickerType
clientId: string
accessToken?: string | null | undefined
}): Promise<string> {
const response = await new Promise<google.accounts.oauth2.TokenResponse>(
(resolve, reject) => {
const scopes =
pickerType === 'drive' ?
['https://www.googleapis.com/auth/drive.readonly']
: ['https://www.googleapis.com/auth/photospicker.mediaitems.readonly']
const tokenClient = google.accounts.oauth2.initTokenClient({
client_id: clientId,
// Authorization scopes required by the API; multiple scopes can be included, separated by spaces.
scope: scopes.join(' '),
callback: resolve,
error_callback: reject,
})
if (accessToken === null) {
// Prompt the user to select a Google Account and ask for consent to share their data
// when establishing a new session.
tokenClient.requestAccessToken({ prompt: 'consent' })
} else {
// Skip display of account chooser and consent dialog for an existing session.
tokenClient.requestAccessToken({ prompt: '' })
}
},
)
if (response.error) {
throw new Error(`OAuth2 error: ${response.error}`)
}
return response.access_token
}
export async function logout(accessToken: string): Promise<void> {
await new Promise<void>((resolve) =>
google.accounts.oauth2.revoke(accessToken, resolve),
)
}
export class InvalidTokenError extends Error {
constructor() {
super('Invalid or expired token')
this.name = 'InvalidTokenError'
}
}
export async function showDrivePicker({
token,
apiKey,
appId,
onFilesPicked,
signal,
}: {
token: string
apiKey: string
appId: string
onFilesPicked: (files: PickedItem[], accessToken: string) => void
signal: AbortSignal | undefined
}): Promise<void> {
// google drive picker will crash hard if given an invalid token, so we need to check it first
// https://github.com/transloadit/uppy/pull/5443#pullrequestreview-2452439265
if (!(await isTokenValid(token, signal))) {
throw new InvalidTokenError()
}
const onPicked = (picked: google.picker.ResponseObject) => {
if (picked.action === google.picker.Action.PICKED) {
// console.log('Picker response', JSON.stringify(picked, null, 2));
onFilesPicked(
picked['docs'].map((doc) => ({
platform: 'drive',
id: doc['id'],
name: doc['name'],
mimeType: doc['mimeType'],
})),
token,
)
}
}
const picker = new google.picker.PickerBuilder()
.enableFeature(google.picker.Feature.NAV_HIDDEN)
.enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
.setDeveloperKey(apiKey)
.setAppId(appId)
.setOAuthToken(token)
.addView(
new google.picker.DocsView(google.picker.ViewId.DOCS)
.setIncludeFolders(true)
// Note: setEnableDrives doesn't seem to work
// .setEnableDrives(true)
.setSelectFolderEnabled(false),
)
// NOTE: photos is broken and results in an error being returned from Google
// I think it's the old Picasa photos
// .addView(google.picker.ViewId.PHOTOS)
.setCallback(onPicked)
.build()
picker.setVisible(true)
signal?.addEventListener('abort', () => picker.dispose())
}
export async function showPhotosPicker({
token,
pickingSession,
onPickingSessionChange,
signal,
}: {
token: string
pickingSession: PickingSession | undefined
onPickingSessionChange: (ps: PickingSession) => void
signal: AbortSignal | undefined
}): Promise<void> {
// https://developers.google.com/photos/picker/guides/get-started-picker
const headers = getAuthHeader(token)
let newPickingSession = pickingSession
if (newPickingSession == null) {
const createSessionResponse = await fetch(
'https://photospicker.googleapis.com/v1/sessions',
{ method: 'post', headers, signal },
)
if (createSessionResponse.status === 401) {
const resp = await createSessionResponse.json()
if (resp.error?.status === 'UNAUTHENTICATED') {
throw new InvalidTokenError()
}
}
if (!createSessionResponse.ok) {
throw new Error('Failed to create a session')
}
newPickingSession = (await createSessionResponse.json()) as PickingSession
onPickingSessionChange(newPickingSession)
}
const w = window.open(newPickingSession.pickerUri)
signal?.addEventListener('abort', () => w?.close())
}
async function resolvePickedPhotos({
accessToken,
pickingSession,
signal,
}: {
accessToken: string
pickingSession: PickingSession
signal: AbortSignal
}) {
const headers = getAuthHeader(accessToken)
let pageToken: string | undefined
let mediaItems: MediaItem[] = []
do {
const pageSize = 100
const response = await fetch(
`https://photospicker.googleapis.com/v1/mediaItems?${new URLSearchParams({ sessionId: pickingSession.id, pageSize: String(pageSize) }).toString()}`,
{ headers, signal },
)
if (!response.ok) throw new Error('Failed to get a media items')
const {
mediaItems: batchMediaItems,
nextPageToken,
}: { mediaItems: MediaItem[]; nextPageToken?: string } =
await response.json()
pageToken = nextPageToken
mediaItems.push(...batchMediaItems)
} while (pageToken)
// todo show alert instead about invalid picked files?
mediaItems = mediaItems.flatMap((i) =>
(
i.type === 'PHOTO' ||
(i.type === 'VIDEO' &&
i.mediaFile.mediaFileMetadata.videoMetadata.processingStatus ===
'READY')
) ?
[i]
: [],
)
return mediaItems.map(
({
id,
// we want the original resolution, so we don't append any parameter to the baseUrl
// https://developers.google.com/photos/library/guides/access-media-items#base-urls
mediaFile: { mimeType, filename, baseUrl },
}) => ({
platform: 'photos' as const,
id,
mimeType,
url: baseUrl,
name: filename,
}),
)
}
export async function pollPickingSession({
pickingSessionRef,
accessTokenRef,
signal,
onFilesPicked,
onError,
}: {
pickingSessionRef: MutableRef<PickingSession | undefined>
accessTokenRef: MutableRef<string | null | undefined>
signal: AbortSignal
onFilesPicked: (files: PickedItem[], accessToken: string) => void
onError: (err: unknown) => void
}): Promise<void> {
// if we have an active session, poll it until it either times out, or the user selects some photos.
// Note that the user can also just close the page, but we get no indication of that from Google when polling,
// so we just have to continue polling in the background, so we can react to it
// in case the user opens the photo selector again. Hence the infinite for loop
for (let interval = 1; ; ) {
try {
if (pickingSessionRef.current != null) {
interval = parseFloat(
pickingSessionRef.current.pollingConfig.pollInterval,
)
} else {
interval = 1
}
await Promise.race([
new Promise((resolve) => setTimeout(resolve, interval * 1000)),
new Promise((_resolve, reject) => {
signal.addEventListener('abort', reject)
}),
])
signal.throwIfAborted()
const accessToken = accessTokenRef.current
const pickingSession = pickingSessionRef.current
if (pickingSession != null && accessToken != null) {
const headers = getAuthHeader(accessToken)
// https://developers.google.com/photos/picker/reference/rest/v1/sessions
const response = await fetch(
`https://photospicker.googleapis.com/v1/sessions/${encodeURIComponent(pickingSession.id)}`,
{ headers, signal },
)
if (!response.ok) throw new Error('Failed to get session')
const json: PickingSession = await response.json()
if (json.mediaItemsSet) {
// console.log('User picked!', json)
const resolvedPhotos = await resolvePickedPhotos({
accessToken,
pickingSession,
signal,
})
// eslint-disable-next-line no-param-reassign
pickingSessionRef.current = undefined
onFilesPicked(resolvedPhotos, accessToken)
}
if (pickingSession.pollingConfig.timeoutIn === '0s') {
// eslint-disable-next-line no-param-reassign
pickingSessionRef.current = undefined
}
}
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
return
}
// just report the error and continue polling
onError(err)
}
}
}

View file

@ -0,0 +1,70 @@
import { h } from 'preact'
export const GooglePhotosIcon = () => (
<svg
aria-hidden="true"
focusable="false"
width="32"
height="32"
viewBox="-7 -7 73 73"
>
<g fill="none" fill-rule="evenodd">
<path d="M-3-3h64v64H-3z" />
<g fill-rule="nonzero">
<path
fill="#FBBC04"
d="M14.8 13.4c8.1 0 14.7 6.6 14.7 14.8v1.3H1.3c-.7 0-1.3-.6-1.3-1.3C0 20 6.6 13.4 14.8 13.4z"
/>
<path
fill="#EA4335"
d="M45.6 14.8c0 8.1-6.6 14.7-14.8 14.7h-1.3V1.3c0-.7.6-1.3 1.3-1.3C39 0 45.6 6.6 45.6 14.8z"
/>
<path
fill="#4285F4"
d="M44.3 45.6c-8.2 0-14.8-6.6-14.8-14.8v-1.3h28.2c.7 0 1.3.6 1.3 1.3 0 8.2-6.6 14.8-14.8 14.8z"
/>
<path
fill="#34A853"
d="M13.4 44.3c0-8.2 6.6-14.8 14.8-14.8h1.3v28.2c0 .7-.6 1.3-1.3 1.3-8.2 0-14.8-6.6-14.8-14.8z"
/>
</g>
</g>
</svg>
)
export const GoogleDriveIcon = () => (
<svg
aria-hidden="true"
focusable="false"
width="32"
height="32"
viewBox="0 0 32 32"
>
<g fillRule="nonzero" fill="none">
<path
d="M6.663 22.284l.97 1.62c.202.34.492.609.832.804l3.465-5.798H5c0 .378.1.755.302 1.096l1.361 2.278z"
fill="#0066DA"
/>
<path
d="M16 12.09l-3.465-5.798c-.34.195-.63.463-.832.804l-6.4 10.718A2.15 2.15 0 005 18.91h6.93L16 12.09z"
fill="#00AC47"
/>
<path
d="M23.535 24.708c.34-.195.63-.463.832-.804l.403-.67 1.928-3.228c.201-.34.302-.718.302-1.096h-6.93l1.474 2.802 1.991 2.996z"
fill="#EA4335"
/>
<path
d="M16 12.09l3.465-5.798A2.274 2.274 0 0018.331 6h-4.662c-.403 0-.794.11-1.134.292L16 12.09z"
fill="#00832D"
/>
<path
d="M20.07 18.91h-8.14l-3.465 5.798c.34.195.73.292 1.134.292h12.802c.403 0 .794-.11 1.134-.292L20.07 18.91z"
fill="#2684FC"
/>
<path
d="M23.497 12.455l-3.2-5.359a2.252 2.252 0 00-.832-.804L16 12.09l4.07 6.82h6.917c0-.377-.1-.755-.302-1.096l-3.188-5.359z"
fill="#FFBA00"
/>
</g>
</svg>
)

View file

@ -4,3 +4,5 @@ export {
} from './ProviderView/index.ts'
export { default as SearchProviderViews } from './SearchProviderView/index.ts'
export { default as GooglePickerView } from './GooglePicker/GooglePickerView.tsx'

View file

@ -379,3 +379,11 @@
padding-bottom: 10px;
}
}
/* https://stackoverflow.com/a/33082658/6519037 */
.picker-dialog-bg {
z-index: 20000 !important;
}
.picker-dialog {
z-index: 20001 !important;
}

View file

@ -34,7 +34,6 @@ const getTagFile = <M extends Meta, B extends Body>(
},
remote: {
companionUrl: plugin.opts.companionUrl,
// @ts-expect-error untyped for now
url: `${provider.fileUrl(file.requestPath)}`,
body: {
fileId: file.id,

View file

@ -8,6 +8,7 @@
"@uppy/core": ["../core/src/index.js"],
"@uppy/core/lib/*": ["../core/src/*"],
},
"types": ["google.accounts", "google.picker", "gapi"],
},
"include": ["./package.json", "./src/**/*.*"],
"references": [

View file

@ -334,6 +334,8 @@ export default class Transloadit<
addPluginVersion('Facebook', 'uppy-facebook')
addPluginVersion('GoogleDrive', 'uppy-google-drive')
addPluginVersion('GooglePhotos', 'uppy-google-photos')
addPluginVersion('GoogleDrivePicker', 'uppy-google-drive-picker')
addPluginVersion('GooglePhotosPicker', 'uppy-google-photos-picker')
addPluginVersion('Instagram', 'uppy-instagram')
addPluginVersion('OneDrive', 'uppy-onedrive')
addPluginVersion('Zoom', 'uppy-zoom')

View file

@ -9,7 +9,11 @@ import { SearchProviderViews } from '@uppy/provider-views'
import { h, type ComponentChild } from 'preact'
import type { UppyFile, Body, Meta } from '@uppy/utils/lib/UppyFile'
import type { UnknownSearchProviderPluginState } from '@uppy/core/lib/Uppy.js'
import type {
AsyncStore,
UnknownSearchProviderPlugin,
UnknownSearchProviderPluginState,
} from '@uppy/core/lib/Uppy.js'
import locale from './locale.ts'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore We don't want TS to generate types for the package.json
@ -17,12 +21,10 @@ import packageJson from '../package.json'
export type UnsplashOptions = CompanionPluginOptions
export default class Unsplash<M extends Meta, B extends Body> extends UIPlugin<
UnsplashOptions,
M,
B,
UnknownSearchProviderPluginState
> {
export default class Unsplash<M extends Meta, B extends Body>
extends UIPlugin<UnsplashOptions, M, B, UnknownSearchProviderPluginState>
implements UnknownSearchProviderPlugin<M, B>
{
static VERSION = packageJson.version
icon: () => h.JSX.Element
@ -31,7 +33,7 @@ export default class Unsplash<M extends Meta, B extends Body> extends UIPlugin<
view!: SearchProviderViews<M, B>
storage: typeof tokenStorage
storage: AsyncStore
files: UppyFile<M, B>[]

View file

@ -26,6 +26,7 @@ export interface CompanionClientProvider {
login(options?: RequestOptions): Promise<void>
logout<ResBody>(options?: RequestOptions): Promise<ResBody>
fetchPreAuthToken(): Promise<void>
fileUrl: (a: string) => string
list(
directory: string | null,
options: RequestOptions,
@ -38,5 +39,6 @@ export interface CompanionClientProvider {
export interface CompanionClientSearchProvider {
name: string
provider: string
fileUrl: (a: string) => string
search<ResBody>(text: string, queries?: string): Promise<ResBody>
}

View file

@ -9,7 +9,11 @@ import { ProviderViews } from '@uppy/provider-views'
import { h, type ComponentChild } from 'preact'
import type { UppyFile, Body, Meta } from '@uppy/utils/lib/UppyFile'
import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy.js'
import type {
AsyncStore,
UnknownProviderPlugin,
UnknownProviderPluginState,
} from '@uppy/core/lib/Uppy.js'
import locale from './locale.ts'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore We don't want TS to generate types for the package.json
@ -17,12 +21,10 @@ import packageJson from '../package.json'
export type ZoomOptions = CompanionPluginOptions
export default class Zoom<M extends Meta, B extends Body> extends UIPlugin<
ZoomOptions,
M,
B,
UnknownProviderPluginState
> {
export default class Zoom<M extends Meta, B extends Body>
extends UIPlugin<ZoomOptions, M, B, UnknownProviderPluginState>
implements UnknownProviderPlugin<M, B>
{
static VERSION = packageJson.version
icon: () => h.JSX.Element
@ -31,7 +33,7 @@ export default class Zoom<M extends Meta, B extends Body> extends UIPlugin<
view!: ProviderViews<M, B>
storage: typeof tokenStorage
storage: AsyncStore
files: UppyFile<M, B>[]

View file

@ -46,7 +46,9 @@
"@uppy/form": "workspace:^",
"@uppy/golden-retriever": "workspace:^",
"@uppy/google-drive": "workspace:^",
"@uppy/google-drive-picker": "workspace:^",
"@uppy/google-photos": "workspace:^",
"@uppy/google-photos-picker": "workspace:^",
"@uppy/image-editor": "workspace:^",
"@uppy/informer": "workspace:^",
"@uppy/instagram": "workspace:^",

View file

@ -22,7 +22,9 @@ export const views = { ProviderView }
// Stores
export { default as DefaultStore } from '@uppy/store-default'
// @ts-expect-error untyped
// not yet typed
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export { default as ReduxStore } from '@uppy/store-redux'
// UI plugins
@ -42,6 +44,8 @@ export { default as Dropbox } from '@uppy/dropbox'
export { default as Facebook } from '@uppy/facebook'
export { default as GoogleDrive } from '@uppy/google-drive'
export { default as GooglePhotos } from '@uppy/google-photos'
export { default as GoogleDrivePicker } from '@uppy/google-drive-picker'
export { default as GooglePhotosPicker } from '@uppy/google-photos-picker'
export { default as Instagram } from '@uppy/instagram'
export { default as OneDrive } from '@uppy/onedrive'
export { default as RemoteSources } from '@uppy/remote-sources'
@ -61,7 +65,9 @@ export { default as XHRUpload } from '@uppy/xhr-upload'
export { default as Compressor } from '@uppy/compressor'
export { default as Form } from '@uppy/form'
export { default as GoldenRetriever } from '@uppy/golden-retriever'
// @ts-expect-error untyped
// not yet typed
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export { default as ReduxDevTools } from '@uppy/redux-dev-tools'
export { default as ThumbnailGenerator } from '@uppy/thumbnail-generator'

View file

@ -16,6 +16,8 @@ import Audio from '@uppy/audio'
import Compressor from '@uppy/compressor'
import GoogleDrive from '@uppy/google-drive'
import english from '@uppy/locales/lib/en_US.js'
import GoogleDrivePicker from '@uppy/google-drive-picker'
import GooglePhotosPicker from '@uppy/google-photos-picker'
/* eslint-enable import/no-extraneous-dependencies */
import generateSignatureIfSecret from './generateSignatureIfSecret.js'
@ -30,6 +32,9 @@ const {
VITE_TRANSLOADIT_SECRET: TRANSLOADIT_SECRET,
VITE_TRANSLOADIT_TEMPLATE: TRANSLOADIT_TEMPLATE,
VITE_TRANSLOADIT_SERVICE_URL: TRANSLOADIT_SERVICE_URL,
VITE_GOOGLE_PICKER_API_KEY: GOOGLE_PICKER_API_KEY,
VITE_GOOGLE_PICKER_CLIENT_ID: GOOGLE_PICKER_CLIENT_ID,
VITE_GOOGLE_PICKER_APP_ID: GOOGLE_PICKER_APP_ID,
} = import.meta.env
const companionAllowedHosts =
@ -125,6 +130,20 @@ export default () => {
// .use(Zoom, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts })
// .use(Url, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts })
// .use(Unsplash, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts })
.use(GoogleDrivePicker, {
target: Dashboard,
companionUrl: COMPANION_URL,
companionAllowedHosts,
clientId: GOOGLE_PICKER_CLIENT_ID,
apiKey: GOOGLE_PICKER_API_KEY,
appId: GOOGLE_PICKER_APP_ID,
})
.use(GooglePhotosPicker, {
target: Dashboard,
companionUrl: COMPANION_URL,
companionAllowedHosts,
clientId: GOOGLE_PICKER_CLIENT_ID,
})
.use(RemoteSources, {
companionUrl: COMPANION_URL,
sources: [

View file

@ -49,6 +49,12 @@
{
"path": "./packages/@uppy/google-photos/tsconfig.build.json",
},
{
"path": "./packages/@uppy/google-drive-picker/tsconfig.build.json",
},
{
"path": "./packages/@uppy/google-photos-picker/tsconfig.build.json",
},
{
"path": "./packages/@uppy/image-editor/tsconfig.build.json",
},

View file

@ -7492,6 +7492,27 @@ __metadata:
languageName: node
linkType: hard
"@types/gapi@npm:^0.0.47":
version: 0.0.47
resolution: "@types/gapi@npm:0.0.47"
checksum: 10/b8104688ef132190cb661b461b912a3f6f07ce589eb90ab4bff4acdfaa9bbb8a6321be1119e865db89bf46dfc00cab2141764839535518cf63a8e2caa19f475e
languageName: node
linkType: hard
"@types/google.accounts@npm:^0.0.14":
version: 0.0.14
resolution: "@types/google.accounts@npm:0.0.14"
checksum: 10/0332acd210eaad1904d28a9de2081da796cb8c22e4f61bbe0768729c71d1de1606355abc0615907505b9f4ac28694911b9722a6a4e6ee563c21f747e9e1c32b5
languageName: node
linkType: hard
"@types/google.picker@npm:^0.0.42":
version: 0.0.42
resolution: "@types/google.picker@npm:0.0.42"
checksum: 10/7e428495807c840f30ff3eab63fbfc4b9760ba20cbf977b94915f2222f678bb29c5bf73eff6f285f661b293127ddfbbedd4d8b2075d102c272ab941f63fa7d78
languageName: node
linkType: hard
"@types/graceful-fs@npm:^4.1.2, @types/graceful-fs@npm:^4.1.3":
version: 4.1.9
resolution: "@types/graceful-fs@npm:4.1.9"
@ -8872,6 +8893,19 @@ __metadata:
languageName: unknown
linkType: soft
"@uppy/google-drive-picker@workspace:^, @uppy/google-drive-picker@workspace:packages/@uppy/google-drive-picker":
version: 0.0.0-use.local
resolution: "@uppy/google-drive-picker@workspace:packages/@uppy/google-drive-picker"
dependencies:
"@uppy/companion-client": "workspace:^"
"@uppy/provider-views": "workspace:^"
"@uppy/utils": "workspace:^"
preact: "npm:^10.5.13"
peerDependencies:
"@uppy/core": "workspace:^"
languageName: unknown
linkType: soft
"@uppy/google-drive@workspace:*, @uppy/google-drive@workspace:^, @uppy/google-drive@workspace:packages/@uppy/google-drive":
version: 0.0.0-use.local
resolution: "@uppy/google-drive@workspace:packages/@uppy/google-drive"
@ -8885,6 +8919,19 @@ __metadata:
languageName: unknown
linkType: soft
"@uppy/google-photos-picker@workspace:^, @uppy/google-photos-picker@workspace:packages/@uppy/google-photos-picker":
version: 0.0.0-use.local
resolution: "@uppy/google-photos-picker@workspace:packages/@uppy/google-photos-picker"
dependencies:
"@uppy/companion-client": "workspace:^"
"@uppy/provider-views": "workspace:^"
"@uppy/utils": "workspace:^"
preact: "npm:^10.5.13"
peerDependencies:
"@uppy/core": "workspace:^"
languageName: unknown
linkType: soft
"@uppy/google-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"
@ -8970,6 +9017,9 @@ __metadata:
version: 0.0.0-use.local
resolution: "@uppy/provider-views@workspace:packages/@uppy/provider-views"
dependencies:
"@types/gapi": "npm:^0.0.47"
"@types/google.accounts": "npm:^0.0.14"
"@types/google.picker": "npm:^0.0.42"
"@uppy/utils": "workspace:^"
classnames: "npm:^2.2.6"
nanoid: "npm:^5.0.0"
@ -13510,7 +13560,9 @@ __metadata:
"@uppy/form": "workspace:^"
"@uppy/golden-retriever": "workspace:^"
"@uppy/google-drive": "workspace:^"
"@uppy/google-drive-picker": "workspace:^"
"@uppy/google-photos": "workspace:^"
"@uppy/google-photos-picker": "workspace:^"
"@uppy/image-editor": "workspace:^"
"@uppy/informer": "workspace:^"
"@uppy/instagram": "workspace:^"
@ -29643,7 +29695,9 @@ __metadata:
"@uppy/form": "workspace:^"
"@uppy/golden-retriever": "workspace:^"
"@uppy/google-drive": "workspace:^"
"@uppy/google-drive-picker": "workspace:^"
"@uppy/google-photos": "workspace:^"
"@uppy/google-photos-picker": "workspace:^"
"@uppy/image-editor": "workspace:^"
"@uppy/informer": "workspace:^"
"@uppy/instagram": "workspace:^"