diff --git a/packages/@uppy/companion/src/server/provider/drive/index.js b/packages/@uppy/companion/src/server/provider/drive/index.js index 4305514791..5c2b4ff327 100644 --- a/packages/@uppy/companion/src/server/provider/drive/index.js +++ b/packages/@uppy/companion/src/server/provider/drive/index.js @@ -9,7 +9,7 @@ const { ProviderAuthError } = require('../error') const got = require('../../got') // For testing refresh token: -// first run a download with mockAccessTokenExpiredError = true +// first run a download with mockAccessTokenExpiredError = true // then when you want to test expiry, set to mockAccessTokenExpiredError to the logged access token // This will trigger companion/nodemon to restart, and it will respond with a simulated invalid token response const mockAccessTokenExpiredError = undefined diff --git a/packages/@uppy/companion/src/server/provider/google/drive/index.js b/packages/@uppy/companion/src/server/provider/google/drive/index.js index c752f295dc..b63e93a07a 100644 --- a/packages/@uppy/companion/src/server/provider/google/drive/index.js +++ b/packages/@uppy/companion/src/server/provider/google/drive/index.js @@ -1,4 +1,4 @@ -const got = require('got').default +const got = require('../../../got') const { logout, refreshToken } = require('../index') const logger = require('../../../logger') @@ -23,7 +23,7 @@ const DRIVE_FILES_FIELDS = `kind,nextPageToken,incompleteSearch,files(${DRIVE_FI // using wildcard to get all 'drive' fields because specifying fields seems no to work for the /drives endpoint const SHARED_DRIVE_FIELDS = '*' -const getClient = ({ token }) => got.extend({ +const getClient = async ({ token }) => (await got).extend({ prefixUrl: 'https://www.googleapis.com/drive/v3', headers: { authorization: `Bearer ${token}`, @@ -31,7 +31,7 @@ const getClient = ({ token }) => got.extend({ }) async function getStats ({ id, token }) { - const client = getClient({ token }) + const client = await getClient({ token }) const getStatsInner = async (statsOfId) => ( client.get(`files/${encodeURIComponent(statsOfId)}`, { searchParams: { fields: DRIVE_FILE_FIELDS, supportsAllDrives: true }, responseType: 'json' }).json() @@ -66,7 +66,7 @@ class Drive extends Provider { const isRoot = directory === 'root' const isVirtualSharedDirRoot = directory === VIRTUAL_SHARED_DIR - const client = getClient({ token }) + const client = await getClient({ token }) async function fetchSharedDrives (pageToken = null) { const shouldListSharedDrives = isRoot && !query.cursor @@ -136,7 +136,7 @@ class Drive extends Provider { } return withGoogleErrorHandling(Drive.authProvider, 'provider.drive.download.error', async () => { - const client = getClient({ token }) + const client = await getClient({ token }) const { mimeType, id, exportLinks } = await getStats({ id: idIn, token }) @@ -152,7 +152,7 @@ class Drive extends Provider { // Implemented based on the answer from StackOverflow: https://stackoverflow.com/a/59168288 const mimeTypeExportLink = exportLinks?.[mimeType2] if (mimeTypeExportLink) { - const gSuiteFilesClient = got.extend({ + const gSuiteFilesClient = (await got).extend({ headers: { authorization: `Bearer ${token}`, }, diff --git a/packages/@uppy/companion/src/server/provider/google/googlephotos/index.js b/packages/@uppy/companion/src/server/provider/google/googlephotos/index.js deleted file mode 100644 index 6b213ee201..0000000000 --- a/packages/@uppy/companion/src/server/provider/google/googlephotos/index.js +++ /dev/null @@ -1,172 +0,0 @@ -const got = require('got').default - -const { logout, refreshToken } = require('../index') -const { withGoogleErrorHandling } = require('../../providerErrors') -const { prepareStream } = require('../../../helpers/utils') -const { MAX_AGE_REFRESH_TOKEN } = require('../../../helpers/jwt') -const logger = require('../../../logger') -const Provider = require('../../Provider') - - -const getBaseClient = ({ token }) => got.extend({ - headers: { - authorization: `Bearer ${token}`, - }, -}) - -const getPhotosClient = ({ token }) => getBaseClient({ token }).extend({ - prefixUrl: 'https://photoslibrary.googleapis.com/v1', -}) - -const getOauthClient = ({ token }) => getBaseClient({ token }).extend({ - prefixUrl: 'https://www.googleapis.com/oauth2/v1', -}) - -async function paginate(fn, getter, limit = 5) { - const items = [] - let pageToken - - for (let i = 0; (i === 0 || pageToken != null); i++) { - if (i >= limit) { - logger.warn(`Hit pagination limit of ${limit}`) - break; - } - const response = await fn(pageToken); - items.push(...getter(response)); - pageToken = response.nextPageToken - } - return items -} - -/** - * Provider for Google Photos API - */ -class GooglePhotos extends Provider { - static get authProvider () { - return 'googlephotos' - } - - static get authStateExpiry () { - return MAX_AGE_REFRESH_TOKEN - } - - // eslint-disable-next-line class-methods-use-this - async list (options) { - return withGoogleErrorHandling(GooglePhotos.authProvider, 'provider.photos.list.error', async () => { - const { directory, query } = options - const { token } = options - - const isRoot = !directory - - const client = getPhotosClient({ token }) - - - async function fetchAlbums () { - if (!isRoot) return [] // albums are only in the root - - return paginate( - (pageToken) => client.get('albums', { searchParams: { pageToken, pageSize: 50 }, responseType: 'json' }).json(), - (response) => response.albums, - ) - } - - async function fetchSharedAlbums () { - if (!isRoot) return [] // albums are only in the root - - return paginate( - (pageToken) => client.get('sharedAlbums', { searchParams: { pageToken, pageSize: 50 }, responseType: 'json' }).json(), - (response) => response.sharedAlbums ?? [], // seems to be undefined if no shared albums - ) - } - - async function fetchMediaItems () { - if (isRoot) return { mediaItems: [] } // no images in root (album list only) - const resp = await client.post('mediaItems:search', { json: { pageToken: query?.cursor, albumId: directory, pageSize: 50 }, responseType: 'json' }).json(); - return resp - } - - const [sharedAlbums, albums, { mediaItems, nextPageToken }] = await Promise.all([ - fetchSharedAlbums(), fetchAlbums(), fetchMediaItems() - ]) - - const newSp = new URLSearchParams(Object.entries(query)); - if (nextPageToken) newSp.set('cursor', nextPageToken); - - const iconSize = 64 - const thumbSize = 300 - const getIcon = (baseUrl) => `${baseUrl}=w${iconSize}-h${iconSize}-c` - const getThumbnail = (baseUrl) => `${baseUrl}=w${thumbSize}-h${thumbSize}-c` - const adaptedItems = [ - ...albums.map((album) => ({ - isFolder: true, - icon: 'https://drive-thirdparty.googleusercontent.com/32/type/application/vnd.google-apps.folder', - mimeType: 'application/vnd.google-apps.folder', - thumbnail: getThumbnail(album.coverPhotoBaseUrl), - name: album.title, - id: album.id, - requestPath: album.id, - })), - ...sharedAlbums.map((sharedAlbum) => ({ - isFolder: true, - icon: 'https://drive-thirdparty.googleusercontent.com/32/type/application/vnd.google-apps.folder', - mimeType: 'application/vnd.google-apps.folder', - thumbnail: getThumbnail(sharedAlbum.coverPhotoBaseUrl), - name: sharedAlbum.title, - id: sharedAlbum.id, - requestPath: sharedAlbum.id, - })), - ...mediaItems.map((mediaItem) => ({ - isFolder: false, - icon: getIcon(mediaItem.baseUrl), - thumbnail: getThumbnail(mediaItem.baseUrl), - name: mediaItem.filename, - id: mediaItem.id, - mimeType: mediaItem.mimeType, - modifiedDate: mediaItem.creationTime, - requestPath: mediaItem.id, - custom: { - imageWidth: mediaItem.photo ? mediaItem.width : undefined, - imageHeight: mediaItem.photo ? mediaItem.height : undefined, - videoWidth: mediaItem.video ? mediaItem.width : undefined, - videoHeight: mediaItem.video ? mediaItem.height : undefined, - }, - })), - ]; - - const { email: username } = await getOauthClient({ token }).get('userinfo').json() - - return { - username, - items: adaptedItems, - nextPagePath: newSp.size > 0 ? `${directory ?? ''}?${newSp.toString()}` : null, - } - }) - } - - // eslint-disable-next-line class-methods-use-this - async download ({ id, token }) { - return withGoogleErrorHandling(GooglePhotos.authProvider, 'provider.photos.download.error', async () => { - const client = getPhotosClient({ token }) - - const { baseUrl } = await client.get(`mediaItems/${encodeURIComponent(id)}`, { responseType: 'json' }).json() - - const url = `${baseUrl}=d`; - const stream = got.stream.get(url, { responseType: 'json' }) - const { size } = await prepareStream(stream) - - return { stream, size } - }) - } - - // eslint-disable-next-line class-methods-use-this - async logout(...args) { - return logout(...args) - } - - // eslint-disable-next-line class-methods-use-this - async refreshToken(...args) { - return refreshToken(...args) - } -} - -module.exports = GooglePhotos diff --git a/packages/@uppy/companion/src/server/provider/google/index.js b/packages/@uppy/companion/src/server/provider/google/index.js index e946c7cd04..9968199054 100644 --- a/packages/@uppy/companion/src/server/provider/google/index.js +++ b/packages/@uppy/companion/src/server/provider/google/index.js @@ -1,4 +1,4 @@ -const got = require('got').default +const got = require('../../got') const { withGoogleErrorHandling } = require('../providerErrors') @@ -8,20 +8,20 @@ const { withGoogleErrorHandling } = require('../providerErrors') * Reusable google stuff */ -const getOauthClient = () => got.extend({ +const getOauthClient = async () => (await got).extend({ prefixUrl: 'https://oauth2.googleapis.com', }) async function refreshToken({ clientId, clientSecret, refreshToken: theRefreshToken }) { return withGoogleErrorHandling('google', 'provider.google.token.refresh.error', async () => { - const { access_token: accessToken } = await getOauthClient().post('token', { responseType: 'json', form: { refresh_token: theRefreshToken, grant_type: 'refresh_token', client_id: clientId, client_secret: clientSecret } }).json() + const { access_token: accessToken } = await (await getOauthClient()).post('token', { responseType: 'json', form: { refresh_token: theRefreshToken, grant_type: 'refresh_token', client_id: clientId, client_secret: clientSecret } }).json() return { accessToken } }) } async function logout({ token }) { return withGoogleErrorHandling('google', 'provider.google.logout.error', async () => { - await got.post('https://accounts.google.com/o/oauth2/revoke', { + await (await got).post('https://accounts.google.com/o/oauth2/revoke', { searchParams: { token }, responseType: 'json', }) diff --git a/packages/@uppy/companion/src/server/provider/providerErrors.js b/packages/@uppy/companion/src/server/provider/providerErrors.js index 715de19592..0f4f9f30f3 100644 --- a/packages/@uppy/companion/src/server/provider/providerErrors.js +++ b/packages/@uppy/companion/src/server/provider/providerErrors.js @@ -68,4 +68,17 @@ async function withProviderErrorHandling({ } } -module.exports = { withProviderErrorHandling } +async function withGoogleErrorHandling (providerName, tag, fn) { + return withProviderErrorHandling({ + fn, + tag, + providerName, + isAuthError: (response) => ( + response.statusCode === 401 + || (response.statusCode === 400 && response.body?.error === 'invalid_grant') // Refresh token has expired or been revoked + ), + getJsonErrorMessage: (body) => body?.error?.message, + }) +} + +module.exports = { withProviderErrorHandling, withGoogleErrorHandling }