UNPKG

@uppy/provider-views

Version:

View library for Uppy remote provider plugins.

265 lines (260 loc) 9.5 kB
// https://developers.google.com/photos/picker/reference/rest/v1/mediaItems // https://developers.google.com/photos/picker/reference/rest/v1/sessions const getAuthHeader = token => ({ authorization: `Bearer ${token}` }); const injectedScripts = new Set(); let driveApiLoaded = false; // https://stackoverflow.com/a/39008859/6519037 async function injectScript(src) { if (injectedScripts.has(src)) return; await new Promise((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) { 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(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, signal) { 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(_ref) { let { pickerType, clientId, accessToken } = _ref; const response = await new Promise((resolve, reject) => { const scopes = pickerType === 'drive' ? ['https://www.googleapis.com/auth/drive.file'] : ['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) { await new Promise(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(_ref2) { let { token, apiKey, appId, onFilesPicked, signal } = _ref2; // 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 => { 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).setMode(google.picker.DocsViewMode.LIST)) // 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 == null || signal.addEventListener('abort', () => picker.dispose()); } export async function showPhotosPicker(_ref3) { let { token, pickingSession, onPickingSessionChange, signal } = _ref3; // 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) { var _resp$error; const resp = await createSessionResponse.json(); if (((_resp$error = resp.error) == null ? void 0 : _resp$error.status) === 'UNAUTHENTICATED') { throw new InvalidTokenError(); } } if (!createSessionResponse.ok) { throw new Error('Failed to create a session'); } newPickingSession = await createSessionResponse.json(); onPickingSessionChange(newPickingSession); } const w = window.open(newPickingSession.pickerUri); signal == null || signal.addEventListener('abort', () => w == null ? void 0 : w.close()); } async function resolvePickedPhotos(_ref4) { let { accessToken, pickingSession, signal } = _ref4; const headers = getAuthHeader(accessToken); let pageToken; let mediaItems = []; 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 } = 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(_ref5 => { let { id, type, // 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 } } = _ref5; return { platform: 'photos', id, mimeType, url: type === 'VIDEO' ? `${baseUrl}=dv` : baseUrl, // dv to download video name: filename }; }); } export async function pollPickingSession(_ref6) { let { pickingSessionRef, accessTokenRef, signal, onFilesPicked, onError } = _ref6; // 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 = 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); } } }