aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSam Wilkins <samwilkins333@gmail.com>2019-10-31 01:58:42 -0400
committerSam Wilkins <samwilkins333@gmail.com>2019-10-31 01:58:42 -0400
commitf48b2729b294d08da0c99a242f9ebb4d7aab4407 (patch)
treec70431625af15c89b99234ebe47167b2eec539fb /src
parent9c7e619fb9d3116649ec3779bd528b947235d5a4 (diff)
commented and cleaned google photos upload utils
Diffstat (limited to 'src')
-rw-r--r--src/server/DashUploadUtils.ts6
-rw-r--r--src/server/apis/google/GooglePhotosUploadUtils.ts100
-rw-r--r--src/server/index.ts2
3 files changed, 84 insertions, 24 deletions
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 46d897339..9fddb466c 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -24,9 +24,13 @@ export namespace DashUploadUtils {
const gifs = [".gif"];
const pngs = [".png"];
const jpgs = [".jpg", ".jpeg"];
- export const imageFormats = [...pngs, ...jpgs, ...gifs];
+ const imageFormats = [...pngs, ...jpgs, ...gifs];
const videoFormats = [".mov", ".mp4"];
+ export function validateExtension(url: string) {
+ return imageFormats.includes(path.extname(url).toLowerCase());
+ }
+
const size = "content-length";
const type = "content-type";
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
index d3442338b..a98399621 100644
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -1,56 +1,111 @@
import request = require('request-promise');
-import { GoogleApiServerUtils } from './GoogleApiServerUtils';
import * as path from 'path';
import { MediaItemCreationResult, NewMediaItemResult } from './SharedTypes';
import { NewMediaItem } from "../../index";
import { BatchedArray, TimeUnit } from 'array-batcher';
import { DashUploadUtils } from '../../DashUploadUtils';
+/**
+ * This namespace encompasses the logic
+ * necessary to upload images to Google's server,
+ * and then initialize / create those images in the Photos
+ * API given the upload tokens returned from the initial
+ * uploading process.
+ *
+ * https://developers.google.com/photos/library/reference/rest/v1/mediaItems/batchCreate
+ */
export namespace GooglePhotosUploadUtils {
- export interface Paths {
- uploadDirectory: string;
- credentialsPath: string;
- tokenPath: string;
- }
-
- export interface MediaInput {
+ /**
+ * Specifies the structure of the object
+ * necessary to upload bytes to Google's servers.
+ * The url is streamed to access the image's bytes,
+ * and the description is what appears in Google Photos'
+ * description field.
+ */
+ export interface UploadSource {
url: string;
description: string;
}
- const prepend = (extension: string) => `https://photoslibrary.googleapis.com/v1/${extension}`;
- const headers = (type: string, token: string) => ({
- 'Content-Type': `application/${type}`,
- 'Authorization': `Bearer ${token}`,
- });
+ /**
+ * A utility function to streamline making
+ * calls to the API's url - accentuates
+ * the relative path in the caller.
+ * @param extension the desired
+ * subset of the API
+ */
+ function prepend(extension: string): string {
+ return `https://photoslibrary.googleapis.com/v1/${extension}`;
+ }
+
+ /**
+ * Factors out the creation of the API request's
+ * authentication elements stored in the header.
+ * @param type the contents of the request
+ * @param token the user-specific Google access token
+ */
+ function headers(type: string, token: string) {
+ return {
+ 'Content-Type': `application/${type}`,
+ 'Authorization': `Bearer ${token}`,
+ };
+ }
- export const DispatchGooglePhotosUpload = async (bearerToken: string, url: string) => {
- if (!DashUploadUtils.imageFormats.includes(path.extname(url))) {
+ /**
+ * This is the first step in the remote image creation process.
+ * Here we upload the raw bytes of the image to Google's servers by
+ * setting authentication and other required header properties and including
+ * the raw bytes to the image, to be uploaded, in the body of the request.
+ * @param bearerToken the user-specific Google access token, specifies the account associated
+ * with the eventual image creation
+ * @param url the url of the image to upload
+ * @param filename an optional name associated with the uploaded image - if not specified
+ * defaults to the filename (basename) in the url
+ */
+ export const DispatchGooglePhotosUpload = async (bearerToken: string, url: string, filename?: string): Promise<any> => {
+ // check if the url points to a non-image or an unsupported format
+ if (!DashUploadUtils.validateExtension(url)) {
return undefined;
}
- const body = await request(url, { encoding: null });
const parameters = {
method: 'POST',
+ uri: prepend('uploads'),
headers: {
...headers('octet-stream', bearerToken),
- 'X-Goog-Upload-File-Name': path.basename(url),
+ 'X-Goog-Upload-File-Name': filename || path.basename(url),
'X-Goog-Upload-Protocol': 'raw'
},
- uri: prepend('uploads'),
- body
+ body: await request(url, { encoding: null }) // returns a readable stream with the unencoded binary image data
};
- return new Promise<any>((resolve, reject) => request(parameters, (error, _response, body) => {
+ return new Promise((resolve, reject) => request(parameters, (error, _response, body) => {
if (error) {
- console.log(error);
+ // on rejection, the server logs the error and the offending image
return reject(error);
}
resolve(body);
}));
};
+ /**
+ * This is the second step in the remote image creation process: having uploaded
+ * the raw bytes of the image and received / stored pointers (upload tokens) to those
+ * bytes, we can now instruct the API to finalize the creation of those images by
+ * submitting a batch create request with the list of upload tokens and the description
+ * to be associated with reach resulting new image.
+ * @param bearerToken the user-specific Google access token, specifies the account associated
+ * with the eventual image creation
+ * @param newMediaItems a list of objects containing a description and, effectively, the
+ * pointer to the uploaded bytes
+ * @param album if included, will add all of the newly created remote images to the album
+ * with the specified id
+ */
export const CreateMediaItems = async (bearerToken: string, newMediaItems: NewMediaItem[], album?: { id: string }): Promise<MediaItemCreationResult> => {
- const newMediaItemResults = await BatchedArray.from(newMediaItems, { batchSize: 50 }).batchedMapPatientInterval<NewMediaItemResult>(
+ // it's important to note that the API can't handle more than 50 items in each request and
+ // seems to need at least some latency between requests (spamming it synchronously has led to the server returning errors)...
+ const batched = BatchedArray.from(newMediaItems, { batchSize: 50 });
+ // ...so we execute them in delayed batches and await the entire execution
+ const newMediaItemResults = await batched.batchedMapPatientInterval<NewMediaItemResult>(
{ magnitude: 100, unit: TimeUnit.Milliseconds },
async (batch: NewMediaItem[], collector) => {
const parameters = {
@@ -60,6 +115,7 @@ export namespace GooglePhotosUploadUtils {
body: { newMediaItems: batch } as any,
json: true
};
+ // register the target album, if provided
album && (parameters.body.albumId = album.id);
const { newMediaItemResults } = await new Promise<MediaItemCreationResult>((resolve, reject) => {
request(parameters, (error, _response, body) => {
diff --git a/src/server/index.ts b/src/server/index.ts
index 05c866eae..9f3e34761 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -627,7 +627,7 @@ function routeSetter(router: RouteManager) {
}
let failed: GooglePhotosUploadFailure[] = [];
- const batched = BatchedArray.from<GooglePhotosUploadUtils.MediaInput>(media, { batchSize: 25 });
+ const batched = BatchedArray.from<GooglePhotosUploadUtils.UploadSource>(media, { batchSize: 25 });
const newMediaItems = await batched.batchedMapPatientInterval<NewMediaItem>(
{ magnitude: 100, unit: TimeUnit.Milliseconds },
async (batch, collector, { completedBatches }) => {