Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Google Picker #5443

Merged
merged 22 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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=***
2 changes: 2 additions & 0 deletions e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:^",
Expand Down
18 changes: 10 additions & 8 deletions packages/@uppy/box/src/Box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,22 @@ 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
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
Expand All @@ -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>[]

Expand Down
4 changes: 2 additions & 2 deletions packages/@uppy/companion-client/src/CompanionPluginOptions.ts
Original file line number Diff line number Diff line change
@@ -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 }
Expand Down
5 changes: 1 addition & 4 deletions packages/@uppy/companion-client/src/Provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
8 changes: 4 additions & 4 deletions packages/@uppy/companion-client/src/RequestClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}`)
},
})
})()
Expand All @@ -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()
}

Expand Down
19 changes: 7 additions & 12 deletions packages/@uppy/companion-client/src/tokenStorage.ts
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 2 additions & 0 deletions packages/@uppy/companion/src/companion.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions packages/@uppy/companion/src/config/companion.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const defaultOptions = {
expires: 800, // seconds
},
enableUrlEndpoint: false,
enableGooglePickerEndpoint: false,
allowLocalUrls: false,
periodicPingUrls: [],
streamingUpload: true,
Expand Down
57 changes: 57 additions & 0 deletions packages/@uppy/companion/src/server/controllers/googlePicker.js
Original file line number Diff line number Diff line change
@@ -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)
25 changes: 2 additions & 23 deletions packages/@uppy/companion/src/server/controllers/url.js
Original file line number Diff line number Diff line change
@@ -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')

/**
Expand All @@ -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
*
Expand Down
28 changes: 28 additions & 0 deletions packages/@uppy/companion/src/server/download.js
Original file line number Diff line number Diff line change
@@ -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,
}
4 changes: 2 additions & 2 deletions packages/@uppy/companion/src/server/helpers/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading