aboutsummaryrefslogtreecommitdiff
path: root/src/server/apis/google
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/apis/google')
-rw-r--r--src/server/apis/google/GoogleApiServerUtils.ts130
-rw-r--r--src/server/apis/google/GooglePhotosUploadUtils.ts176
2 files changed, 306 insertions, 0 deletions
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
new file mode 100644
index 000000000..8785cd974
--- /dev/null
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -0,0 +1,130 @@
+import { google, docs_v1, slides_v1 } from "googleapis";
+import { createInterface } from "readline";
+import { readFile, writeFile } from "fs";
+import { OAuth2Client } from "google-auth-library";
+import { Opt } from "../../../new_fields/Doc";
+import { GlobalOptions } from "googleapis-common";
+import { GaxiosResponse } from "gaxios";
+
+/**
+ * Server side authentication for Google Api queries.
+ */
+export namespace GoogleApiServerUtils {
+
+ // If modifying these scopes, delete token.json.
+ const prefix = 'https://www.googleapis.com/auth/';
+ const SCOPES = [
+ 'documents.readonly',
+ 'documents',
+ 'presentations',
+ 'presentations.readonly',
+ 'drive',
+ 'drive.file',
+ ];
+
+ export const parseBuffer = (data: Buffer) => JSON.parse(data.toString());
+
+ export enum Service {
+ Documents = "Documents",
+ Slides = "Slides"
+ }
+
+
+ export interface CredentialPaths {
+ credentials: string;
+ token: string;
+ }
+
+ export type ApiResponse = Promise<GaxiosResponse>;
+ export type ApiRouter = (endpoint: Endpoint, paramters: any) => ApiResponse;
+ export type ApiHandler = (parameters: any) => ApiResponse;
+ export type Action = "create" | "retrieve" | "update";
+
+ export type Endpoint = { get: ApiHandler, create: ApiHandler, batchUpdate: ApiHandler };
+ export type EndpointParameters = GlobalOptions & { version: "v1" };
+
+ export const GetEndpoint = async (sector: string, paths: CredentialPaths) => {
+ return new Promise<Opt<Endpoint>>((resolve, reject) => {
+ readFile(paths.credentials, (err, credentials) => {
+ if (err) {
+ reject(err);
+ return console.log('Error loading client secret file:', err);
+ }
+ return authorize(parseBuffer(credentials), paths.token).then(auth => {
+ let routed: Opt<Endpoint>;
+ let parameters: EndpointParameters = { auth, version: "v1" };
+ switch (sector) {
+ case Service.Documents:
+ routed = google.docs(parameters).documents;
+ break;
+ case Service.Slides:
+ routed = google.slides(parameters).presentations;
+ break;
+ }
+ resolve(routed);
+ });
+ });
+ });
+ };
+
+
+ /**
+ * Create an OAuth2 client with the given credentials, and returns the promise resolving to the authenticated client
+ * @param {Object} credentials The authorization client credentials.
+ */
+ export function authorize(credentials: any, token_path: string): Promise<OAuth2Client> {
+ const { client_secret, client_id, redirect_uris } = credentials.installed;
+ const oAuth2Client = new google.auth.OAuth2(
+ client_id, client_secret, redirect_uris[0]);
+
+ return new Promise<OAuth2Client>((resolve, reject) => {
+ readFile(token_path, (err, token) => {
+ // Check if we have previously stored a token.
+ if (err) {
+ return getNewToken(oAuth2Client, token_path).then(resolve, reject);
+ }
+ oAuth2Client.setCredentials(parseBuffer(token));
+ resolve(oAuth2Client);
+ });
+ });
+ }
+
+ /**
+ * Get and store new token after prompting for user authorization, and then
+ * execute the given callback with the authorized OAuth2 client.
+ * @param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for.
+ * @param {getEventsCallback} callback The callback for the authorized client.
+ */
+ function getNewToken(oAuth2Client: OAuth2Client, token_path: string) {
+ return new Promise<OAuth2Client>((resolve, reject) => {
+ const authUrl = oAuth2Client.generateAuthUrl({
+ access_type: 'offline',
+ scope: SCOPES.map(relative => prefix + relative),
+ });
+ console.log('Authorize this app by visiting this url:', authUrl);
+ const rl = createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ });
+ rl.question('Enter the code from that page here: ', (code) => {
+ rl.close();
+ oAuth2Client.getToken(code, (err, token) => {
+ if (err || !token) {
+ reject(err);
+ return console.error('Error retrieving access token', err);
+ }
+ oAuth2Client.setCredentials(token);
+ // Store the token to disk for later program executions
+ writeFile(token_path, JSON.stringify(token), (err) => {
+ if (err) {
+ console.error(err);
+ reject(err);
+ }
+ console.log('Token stored to', token_path);
+ });
+ resolve(oAuth2Client);
+ });
+ });
+ });
+ }
+} \ No newline at end of file
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
new file mode 100644
index 000000000..35f986250
--- /dev/null
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -0,0 +1,176 @@
+import request = require('request-promise');
+import { GoogleApiServerUtils } from './GoogleApiServerUtils';
+import * as fs from 'fs';
+import { Utils } from '../../../Utils';
+import * as path from 'path';
+import { Opt } from '../../../new_fields/Doc';
+import * as sharp from 'sharp';
+
+const uploadDirectory = path.join(__dirname, "../../public/files/");
+
+export namespace GooglePhotosUploadUtils {
+
+ export interface Paths {
+ uploadDirectory: string;
+ credentialsPath: string;
+ tokenPath: string;
+ }
+
+ export interface MediaInput {
+ url: string;
+ description: string;
+ }
+
+ const prepend = (extension: string) => `https://photoslibrary.googleapis.com/v1/${extension}`;
+ const headers = (type: string) => ({
+ 'Content-Type': `application/${type}`,
+ 'Authorization': Bearer,
+ });
+
+ let Bearer: string;
+ let Paths: Paths;
+
+ export const initialize = async (paths: Paths) => {
+ Paths = paths;
+ const { tokenPath, credentialsPath } = paths;
+ const token = await GoogleApiServerUtils.RetrieveAccessToken({ tokenPath, credentialsPath });
+ Bearer = `Bearer ${token}`;
+ };
+
+ export const DispatchGooglePhotosUpload = async (url: string) => {
+ const body = await request(url, { encoding: null });
+ const parameters = {
+ method: 'POST',
+ headers: {
+ ...headers('octet-stream'),
+ 'X-Goog-Upload-File-Name': path.basename(url),
+ 'X-Goog-Upload-Protocol': 'raw'
+ },
+ uri: prepend('uploads'),
+ body
+ };
+ return new Promise<any>(resolve => request(parameters, (error, _response, body) => resolve(error ? undefined : body)));
+ };
+
+ export const CreateMediaItems = (newMediaItems: any[], album?: { id: string }) => {
+ return new Promise<any>((resolve, reject) => {
+ const parameters = {
+ method: 'POST',
+ headers: headers('json'),
+ uri: prepend('mediaItems:batchCreate'),
+ body: { newMediaItems } as any,
+ json: true
+ };
+ album && (parameters.body.albumId = album.id);
+ request(parameters, (error, _response, body) => {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(body);
+ }
+ });
+ });
+ };
+
+}
+
+export namespace DownloadUtils {
+
+ export interface Size {
+ width: number;
+ suffix: string;
+ }
+
+ export const Sizes: { [size: string]: Size } = {
+ SMALL: { width: 100, suffix: "_s" },
+ MEDIUM: { width: 400, suffix: "_m" },
+ LARGE: { width: 900, suffix: "_l" },
+ };
+
+ const png = ".png";
+ const pngs = [".png", ".PNG"];
+ const jpgs = [".jpg", ".JPG", ".jpeg", ".JPEG"];
+ const formats = [".jpg", ".png", ".gif"];
+ const size = "content-length";
+ const type = "content-type";
+
+ export interface UploadInformation {
+ mediaPaths: string[];
+ fileNames: { [key: string]: string };
+ contentSize?: number;
+ contentType?: string;
+ }
+
+ const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`;
+ const sanitize = (filename: string) => filename.replace(/\s+/g, "_");
+
+ export const UploadImage = async (url: string, filename?: string, prefix = ""): Promise<Opt<UploadInformation>> => {
+ const resolved = filename ? sanitize(filename) : generate(prefix, url);
+ const extension = path.extname(url) || path.extname(resolved) || png;
+ let information: UploadInformation = {
+ mediaPaths: [],
+ fileNames: { clean: resolved }
+ };
+ const { isLocal, stream, normalized } = classify(url);
+ url = normalized;
+ if (!isLocal) {
+ const metadata = (await new Promise<any>((resolve, reject) => {
+ request.head(url, async (error, res) => {
+ if (error) {
+ return reject(error);
+ }
+ resolve(res);
+ });
+ })).headers;
+ information.contentSize = parseInt(metadata[size]);
+ information.contentType = metadata[type];
+ }
+ return new Promise<UploadInformation>(async (resolve, reject) => {
+ const resizers = [
+ { resizer: sharp().rotate(), suffix: "_o" },
+ ...Object.values(Sizes).map(size => ({
+ resizer: sharp().resize(size.width, undefined, { withoutEnlargement: true }).rotate(),
+ suffix: size.suffix
+ }))
+ ];
+ if (pngs.includes(extension)) {
+ resizers.forEach(element => element.resizer = element.resizer.png());
+ } else if (jpgs.includes(extension)) {
+ resizers.forEach(element => element.resizer = element.resizer.jpeg());
+ } else if (!formats.includes(extension.toLowerCase())) {
+ return reject();
+ }
+ for (let resizer of resizers) {
+ const suffix = resizer.suffix;
+ let mediaPath: string;
+ await new Promise<void>(resolve => {
+ const filename = resolved.substring(0, resolved.length - extension.length) + suffix + extension;
+ information.mediaPaths.push(mediaPath = uploadDirectory + filename);
+ information.fileNames[suffix] = filename;
+ stream(url).pipe(resizer.resizer).pipe(fs.createWriteStream(mediaPath))
+ .on('close', resolve)
+ .on('error', reject);
+ });
+ }
+ resolve(information);
+ });
+ };
+
+ const classify = (url: string) => {
+ const isLocal = /Dash-Web(\\|\/)src(\\|\/)server(\\|\/)public(\\|\/)files/g.test(url);
+ return {
+ isLocal,
+ stream: isLocal ? fs.createReadStream : request,
+ normalized: isLocal ? path.normalize(url) : url
+ };
+ };
+
+ export const createIfNotExists = async (path: string) => {
+ if (await new Promise<boolean>(resolve => fs.exists(path, resolve))) {
+ return true;
+ }
+ return new Promise<boolean>(resolve => fs.mkdir(path, error => resolve(error === null)));
+ };
+
+ export const Destroy = (mediaPath: string) => new Promise<boolean>(resolve => fs.unlink(mediaPath, error => resolve(error === null)));
+} \ No newline at end of file