UNPKG

@uppy/companion

Version:

OAuth helper and remote fetcher for Uppy's (https://uppy.io) extensible file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Dropbox and Google Drive, S3 and more :dog:

283 lines (254 loc) 7.96 kB
// From https://www.dropbox.com/developers/reference/json-encoding: // // This function is simple and has OK performance compared to more // complicated ones: http://jsperf.com/json-escape-unicode/4 import got from 'got' import { MAX_AGE_REFRESH_TOKEN } from '../../helpers/jwt.js' import { prepareStream } from '../../helpers/utils.js' import logger from '../../logger.js' import Provider from '../Provider.js' import { withProviderErrorHandling } from '../providerErrors.js' import adaptData from './adapter.js' const charsToEncode = /[\u007f-\uffff]/g function httpHeaderSafeJson(v) { return JSON.stringify(v).replace(charsToEncode, (c) => { return `\\u${`000${c.charCodeAt(0).toString(16)}`.slice(-4)}` }) } async function getUserInfo({ client }) { return client .post('users/get_current_account', { responseType: 'json' }) .json() } async function getClient({ token, namespaced }) { const makeClient = (namespace) => got.extend({ prefixUrl: 'https://api.dropboxapi.com/2', headers: { authorization: `Bearer ${token}`, ...(namespace ? { 'Dropbox-API-Path-Root': JSON.stringify({ '.tag': 'root', root: namespace, }), } : {}), }, }) let client = makeClient() const userInfo = await getUserInfo({ client }) // console.log('userInfo', userInfo) // https://www.dropboxforum.com/discussions/101000014/how-to-list-the-contents-of-a-team-folder/258310 // https://developers.dropbox.com/dbx-team-files-guide#namespaces // https://www.dropbox.com/developers/reference/path-root-header-modes if ( namespaced && userInfo.root_info != null && userInfo.root_info.root_namespace_id !== userInfo.root_info.home_namespace_id ) { logger.debug( 'using root_namespace_id', userInfo.root_info.root_namespace_id, ) client = makeClient(userInfo.root_info.root_namespace_id) } return { client, userInfo, } } const getOauthClient = () => got.extend({ prefixUrl: 'https://api.dropboxapi.com/oauth2', }) async function list({ client, directory, query }) { if (query.cursor) { return client .post('files/list_folder/continue', { json: { cursor: query.cursor }, responseType: 'json', }) .json() } return client .post('files/list_folder', { searchParams: query, json: { path: `${directory || ''}`, include_non_downloadable_files: false, // min=1, max=2000 (default: 500): The maximum number of results to return per request. limit: 2000, }, responseType: 'json', }) .json() } async function fetchSearchEntries({ client, query }) { const scopePath = typeof query.path === 'string' ? decodeURIComponent(query.path) : undefined const searchRes = await client .post('files/search_v2', { json: { query: query.q.trim(), options: { path: scopePath || '', max_results: 1000, file_status: 'active', filename_only: false, }, }, responseType: 'json', }) .json() const entries = searchRes.matches.map((m) => m.metadata.metadata) return { entries, has_more: searchRes.has_more, cursor: searchRes.cursor, } } /** * Adapter for API https://www.dropbox.com/developers/documentation/http/documentation */ export default class Dropbox extends Provider { constructor(options) { super(options) this.needsCookieAuth = true } static get oauthProvider() { return 'dropbox' } static get authStateExpiry() { return MAX_AGE_REFRESH_TOKEN } /** * Search entries */ async search(options) { return this.#withErrorHandling( 'provider.dropbox.search.error', async () => { const { client, userInfo } = await getClient({ token: options.providerUserSession.accessToken, namespaced: true, }) const stats = await fetchSearchEntries({ client, query: options.query }) console.log(stats) const { email } = userInfo // we don't really need email, but let's mimic `list` response shape for consistency return adaptData(stats, email, options.companion.buildURL) }, ) } /** * List folder entries */ async list(options) { return this.#withErrorHandling('provider.dropbox.list.error', async () => { const { client, userInfo } = await getClient({ token: options.providerUserSession.accessToken, namespaced: true, }) const stats = await list({ ...options, client }) const { email } = userInfo return adaptData(stats, email, options.companion.buildURL) }) } async download({ id, providerUserSession: { accessToken: token } }) { return this.#withErrorHandling( 'provider.dropbox.download.error', async () => { const stream = ( await getClient({ token, namespaced: true }) ).client.stream.post('files/download', { prefixUrl: 'https://content.dropboxapi.com/2', headers: { 'Dropbox-API-Arg': httpHeaderSafeJson({ path: String(id) }), Connection: 'keep-alive', // important because https://github.com/transloadit/uppy/issues/4357 }, body: Buffer.alloc(0), // if not, it will hang waiting for the writable stream responseType: 'json', }) const { size } = await prepareStream(stream) return { stream, size } }, ) } async thumbnail({ id, providerUserSession: { accessToken: token } }) { return this.#withErrorHandling( 'provider.dropbox.thumbnail.error', async () => { const stream = ( await getClient({ token, namespaced: true }) ).client.stream.post('files/get_thumbnail_v2', { prefixUrl: 'https://content.dropboxapi.com/2', headers: { 'Dropbox-API-Arg': httpHeaderSafeJson({ resource: { '.tag': 'path', path: `${id}` }, size: 'w256h256', format: 'jpeg', }), }, body: Buffer.alloc(0), responseType: 'json', }) await prepareStream(stream) return { stream, contentType: 'image/jpeg' } }, ) } async size({ id, providerUserSession: { accessToken: token } }) { return this.#withErrorHandling('provider.dropbox.size.error', async () => { const { size } = await ( await getClient({ token, namespaced: true }) ).client .post('files/get_metadata', { json: { path: id }, responseType: 'json', }) .json() return parseInt(size, 10) }) } async logout({ providerUserSession: { accessToken: token } }) { return this.#withErrorHandling( 'provider.dropbox.logout.error', async () => { await (await getClient({ token, namespaced: false })).client.post( 'auth/token/revoke', { responseType: 'json' }, ) return { revoked: true } }, ) } async refreshToken({ clientId, clientSecret, refreshToken }) { return this.#withErrorHandling( 'provider.dropbox.token.refresh.error', async () => { const { access_token: accessToken } = await getOauthClient() .post('token', { form: { refresh_token: refreshToken, grant_type: 'refresh_token', client_id: clientId, client_secret: clientSecret, }, }) .json() return { accessToken } }, ) } async #withErrorHandling(tag, fn) { return withProviderErrorHandling({ fn, tag, providerName: Dropbox.oauthProvider, isAuthError: (response) => response.statusCode === 401, getJsonErrorMessage: (body) => body?.error_summary, }) } }