gristlabs_grist-core/app/server/lib/GoogleImport.ts
Jarosław Sadziński 42910cb8f7 (core) Extending Google Drive integration scope
Summary:
New environmental variable GOOGLE_DRIVE_SCOPE that modifies the scope
requested for Google Drive integration.
For prod it has value https://www.googleapis.com/auth/drive.file which leaves
current behavior (Grist is allowed only to access public files and for private
files - it fallbacks to Picker).
For staging it has value https://www.googleapis.com/auth/drive.readonly which
allows Grist to access all private files, and fallbacks to Picker only when the file is
neither public nor private).
Default value is https://www.googleapis.com/auth/drive.file

Test Plan: manual and existing tests

Reviewers: dsagal

Reviewed By: dsagal

Subscribers: dsagal

Differential Revision: https://phab.getgrist.com/D3038
2021-10-01 10:47:12 +02:00

96 lines
3.1 KiB
TypeScript

import {drive} from '@googleapis/drive';
import {Readable} from 'form-data';
import {GaxiosError, GaxiosPromise} from 'gaxios';
import {FetchError, Response as FetchResponse, Headers} from 'node-fetch';
import {getGoogleAuth} from "app/server/lib/GoogleAuth";
import * as contentDisposition from 'content-disposition';
const
SPREADSHEETS_MIMETYPE = 'application/vnd.google-apps.spreadsheet',
XLSX_MIMETYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
export async function downloadFromGDrive(url: string, code?: string) {
const fileId = fileIdFromUrl(url);
const key = process.env.GOOGLE_API_KEY;
if (!key) {
throw new Error("Can't download file from Google Drive. Api key is not configured");
}
if (!fileId) {
throw new Error(`Can't download from ${url}. Url is not valid`);
}
const googleDrive = await initDriveApi(code);
const fileRes = await googleDrive.files.get({
key,
fileId
});
if (fileRes.data.mimeType === SPREADSHEETS_MIMETYPE) {
let filename = fileRes.data.name;
if (filename && !filename.includes(".")) {
filename = `${filename}.xlsx`;
}
return await asFetchResponse(googleDrive.files.export(
{key, fileId, alt: 'media', mimeType: XLSX_MIMETYPE},
{responseType: 'stream'}
), filename);
} else {
return await asFetchResponse(googleDrive.files.get(
{key, fileId, alt: 'media'},
{responseType: 'stream'}
), fileRes.data.name);
}
}
async function initDriveApi(code?: string) {
if (code) {
// Create drive with access token.
const auth = getGoogleAuth();
const token = await auth.getToken(code);
if (token.tokens) {
auth.setCredentials(token.tokens);
}
return drive({version: 'v3', auth: code ? auth : undefined});
}
// Create drive for public access.
return drive({version: 'v3'});
}
async function asFetchResponse(req: GaxiosPromise<Readable>, filename?: string | null) {
try {
const res = await req;
const headers = new Headers(res.headers);
if (filename) {
headers.set("content-disposition", contentDisposition(filename));
}
return new FetchResponse(res.data, {
headers,
status: res.status,
statusText: res.statusText
});
} catch (err) {
const error: GaxiosError<Readable> = err;
if (!error.response) {
// Fetch throws exception on network error.
// https://github.com/node-fetch/node-fetch/blob/master/docs/ERROR-HANDLING.md
throw new FetchError(error.message, "system", error.code || "unknown");
} else {
// Fetch returns failure response on http error
const resInit = error.response ? {
status: error.response.status,
headers: new Headers(error.response.headers),
statusText: error.response.statusText
} : undefined;
return new FetchResponse(error.response.data, resInit);
}
}
}
export function isDriveUrl(url: string) {
return !!fileIdFromUrl(url);
}
function fileIdFromUrl(url: string) {
if (!url) { return null; }
const match = /^https:\/\/(docs|drive).google.com\/(spreadsheets|file)\/d\/([^/?]*)/i.exec(url);
return match ? match[3] : null;
}