@uppy/provider-views
Version:
View library for Uppy remote provider plugins.
265 lines (260 loc) • 9.5 kB
JavaScript
// 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);
}
}
}