mirror of
https://github.com/transloadit/uppy.git
synced 2026-01-23 02:25:07 +00:00
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:
parent
44a378af9e
commit
afd4befee2
62 changed files with 1679 additions and 184 deletions
10
.env.example
10
.env.example
|
|
@ -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=***
|
||||
|
|
|
|||
|
|
@ -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:^",
|
||||
|
|
|
|||
|
|
@ -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>[]
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const defaultOptions = {
|
|||
expires: 800, // seconds
|
||||
},
|
||||
enableUrlEndpoint: false,
|
||||
enableGooglePickerEndpoint: false,
|
||||
allowLocalUrls: false,
|
||||
periodicPingUrls: [],
|
||||
streamingUpload: true,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
28
packages/@uppy/companion/src/server/download.js
Normal file
28
packages/@uppy/companion/src/server/download.js
Normal 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,
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
}
|
||||
|
|
|
|||
28
packages/@uppy/core/src/useStore.ts
Normal file
28
packages/@uppy/core/src/useStore.ts
Normal 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]
|
||||
}
|
||||
|
|
@ -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>[]
|
||||
|
||||
|
|
|
|||
|
|
@ -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>[]
|
||||
|
||||
|
|
|
|||
1
packages/@uppy/google-drive-picker/.npmignore
Normal file
1
packages/@uppy/google-drive-picker/.npmignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
tsconfig.*
|
||||
1
packages/@uppy/google-drive-picker/CHANGELOG.md
Normal file
1
packages/@uppy/google-drive-picker/CHANGELOG.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# @uppy/google-drive-picker
|
||||
21
packages/@uppy/google-drive-picker/LICENSE
Normal file
21
packages/@uppy/google-drive-picker/LICENSE
Normal 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.
|
||||
18
packages/@uppy/google-drive-picker/README.md
Normal file
18
packages/@uppy/google-drive-picker/README.md
Normal 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">
|
||||
|
||||
[](https://www.npmjs.com/package/@uppy/google-drive-picker)
|
||||

|
||||

|
||||

|
||||
|
||||
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).
|
||||
33
packages/@uppy/google-drive-picker/package.json
Normal file
33
packages/@uppy/google-drive-picker/package.json
Normal 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:^"
|
||||
}
|
||||
}
|
||||
115
packages/@uppy/google-drive-picker/src/GoogleDrivePicker.tsx
Normal file
115
packages/@uppy/google-drive-picker/src/GoogleDrivePicker.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
1
packages/@uppy/google-drive-picker/src/index.ts
Normal file
1
packages/@uppy/google-drive-picker/src/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './GoogleDrivePicker.tsx'
|
||||
3
packages/@uppy/google-drive-picker/src/locale.ts
Normal file
3
packages/@uppy/google-drive-picker/src/locale.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export default {
|
||||
strings: {},
|
||||
}
|
||||
35
packages/@uppy/google-drive-picker/tsconfig.build.json
Normal file
35
packages/@uppy/google-drive-picker/tsconfig.build.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
31
packages/@uppy/google-drive-picker/tsconfig.json
Normal file
31
packages/@uppy/google-drive-picker/tsconfig.json
Normal 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",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
@ -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>[]
|
||||
|
||||
|
|
|
|||
1
packages/@uppy/google-photos-picker/.npmignore
Normal file
1
packages/@uppy/google-photos-picker/.npmignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
tsconfig.*
|
||||
1
packages/@uppy/google-photos-picker/CHANGELOG.md
Normal file
1
packages/@uppy/google-photos-picker/CHANGELOG.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# @uppy/google-photos-picker
|
||||
21
packages/@uppy/google-photos-picker/LICENSE
Normal file
21
packages/@uppy/google-photos-picker/LICENSE
Normal 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.
|
||||
18
packages/@uppy/google-photos-picker/README.md
Normal file
18
packages/@uppy/google-photos-picker/README.md
Normal 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">
|
||||
|
||||
[](https://www.npmjs.com/package/@uppy/google-photos-picker)
|
||||

|
||||

|
||||

|
||||
|
||||
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).
|
||||
33
packages/@uppy/google-photos-picker/package.json
Normal file
33
packages/@uppy/google-photos-picker/package.json
Normal 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:^"
|
||||
}
|
||||
}
|
||||
111
packages/@uppy/google-photos-picker/src/GooglePhotosPicker.tsx
Normal file
111
packages/@uppy/google-photos-picker/src/GooglePhotosPicker.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
1
packages/@uppy/google-photos-picker/src/index.ts
Normal file
1
packages/@uppy/google-photos-picker/src/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './GooglePhotosPicker.tsx'
|
||||
3
packages/@uppy/google-photos-picker/src/locale.ts
Normal file
3
packages/@uppy/google-photos-picker/src/locale.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export default {
|
||||
strings: {},
|
||||
}
|
||||
35
packages/@uppy/google-photos-picker/tsconfig.build.json
Normal file
35
packages/@uppy/google-photos-picker/tsconfig.build.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
31
packages/@uppy/google-photos-picker/tsconfig.json
Normal file
31
packages/@uppy/google-photos-picker/tsconfig.json
Normal 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",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
@ -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>[]
|
||||
|
||||
|
|
|
|||
|
|
@ -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>[]
|
||||
|
||||
|
|
|
|||
|
|
@ -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>[]
|
||||
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
425
packages/@uppy/provider-views/src/GooglePicker/googlePicker.ts
Normal file
425
packages/@uppy/provider-views/src/GooglePicker/googlePicker.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
70
packages/@uppy/provider-views/src/GooglePicker/icons.tsx
Normal file
70
packages/@uppy/provider-views/src/GooglePicker/icons.tsx
Normal 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>
|
||||
)
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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>[]
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>[]
|
||||
|
||||
|
|
|
|||
|
|
@ -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:^",
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
|
|
|
|||
54
yarn.lock
54
yarn.lock
|
|
@ -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:^"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue