aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/server/ApiManagers/GeneralGoogleManager.ts48
-rw-r--r--src/server/ApiManagers/GooglePhotosManager.ts108
-rw-r--r--src/server/Initialization.ts5
-rw-r--r--src/server/index.ts141
4 files changed, 160 insertions, 142 deletions
diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts
new file mode 100644
index 000000000..cb37b0dce
--- /dev/null
+++ b/src/server/ApiManagers/GeneralGoogleManager.ts
@@ -0,0 +1,48 @@
+import ApiManager, { Registration } from "./ApiManager";
+import { Method, _permission_denied } from "../RouteManager";
+import { uploadDirectory } from "..";
+import { path } from "animejs";
+import { RouteStore } from "../RouteStore";
+import { GoogleApiServerUtils } from "../apis/google/GoogleApiServerUtils";
+import { Database } from "../database";
+
+const deletionPermissionError = "Cannot perform specialized delete outside of the development environment!";
+
+export default class GeneralGoogleManager extends ApiManager {
+
+ protected initialize(register: Registration): void {
+
+ register({
+ method: Method.GET,
+ subscription: RouteStore.readGoogleAccessToken,
+ onValidation: async ({ user, res }) => {
+ const userId = user.id;
+ const token = await GoogleApiServerUtils.retrieveAccessToken(userId);
+ if (!token) {
+ return res.send(GoogleApiServerUtils.generateAuthenticationUrl());
+ }
+ return res.send(token);
+ }
+ });
+
+ register({
+ method: Method.POST,
+ subscription: RouteStore.writeGoogleAccessToken,
+ onValidation: async ({ user, req, res }) => {
+ res.send(await GoogleApiServerUtils.processNewUser(user.id, req.body.authenticationCode));
+ }
+ });
+
+ register({
+ method: Method.GET,
+ subscription: "/deleteWithGoogleCredentials",
+ onValidation: async ({ res, isRelease }) => {
+ if (isRelease) {
+ return _permission_denied(res, deletionPermissionError);
+ }
+ await Database.Auxiliary.GoogleAuthenticationToken.DeleteAll();
+ res.redirect(RouteStore.delete);
+ }
+ });
+ }
+} \ No newline at end of file
diff --git a/src/server/ApiManagers/GooglePhotosManager.ts b/src/server/ApiManagers/GooglePhotosManager.ts
new file mode 100644
index 000000000..b5e9caa38
--- /dev/null
+++ b/src/server/ApiManagers/GooglePhotosManager.ts
@@ -0,0 +1,108 @@
+import ApiManager, { Registration } from "./ApiManager";
+import { Method, _error, _success, _invalid } from "../RouteManager";
+import { uploadDirectory, NewMediaItem } from "..";
+import { path } from "animejs";
+import { RouteStore } from "../RouteStore";
+import { GoogleApiServerUtils } from "../apis/google/GoogleApiServerUtils";
+import { BatchedArray, TimeUnit } from "array-batcher";
+import { GooglePhotosUploadUtils } from "../apis/google/GooglePhotosUploadUtils";
+import { MediaItem } from "../apis/google/SharedTypes";
+import { Opt } from "../../new_fields/Doc";
+import { DashUploadUtils } from "../DashUploadUtils";
+import { Database } from "../database";
+import { prefix } from "@fortawesome/free-solid-svg-icons";
+
+const authenticationError = "Unable to authenticate Google credentials before uploading to Google Photos!";
+const mediaError = "Unable to convert all uploaded bytes to media items!";
+const UploadError = (count: number) => `Unable to upload ${count} images to Dash's server`;
+const requestError = "Unable to execute download: the body's media items were malformed.";
+const downloadError = "Encountered an error while executing downloads.";
+interface GooglePhotosUploadFailure {
+ batch: number;
+ index: number;
+ url: string;
+ reason: string;
+}
+
+export default class GooglePhotosManager extends ApiManager {
+
+ protected initialize(register: Registration): void {
+
+ register({
+ method: Method.POST,
+ subscription: RouteStore.googlePhotosMediaUpload,
+ onValidation: async ({ user, req, res }) => {
+ const { media } = req.body;
+
+ const token = await GoogleApiServerUtils.retrieveAccessToken(user.id);
+ if (!token) {
+ return _error(res, authenticationError);
+ }
+
+ let failed: GooglePhotosUploadFailure[] = [];
+ const batched = BatchedArray.from<GooglePhotosUploadUtils.UploadSource>(media, { batchSize: 25 });
+ const newMediaItems = await batched.batchedMapPatientInterval<NewMediaItem>(
+ { magnitude: 100, unit: TimeUnit.Milliseconds },
+ async (batch, collector, { completedBatches }) => {
+ for (let index = 0; index < batch.length; index++) {
+ const { url, description } = batch[index];
+ const fail = (reason: string) => failed.push({ reason, batch: completedBatches + 1, index, url });
+ const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(token, url).catch(fail);
+ if (!uploadToken) {
+ fail(`${path.extname(url)} is not an accepted extension`);
+ } else {
+ collector.push({
+ description,
+ simpleMediaItem: { uploadToken }
+ });
+ }
+ }
+ }
+ );
+
+ const failedCount = failed.length;
+ if (failedCount) {
+ console.error(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`);
+ console.log(failed.map(({ reason, batch, index, url }) => `@${batch}.${index}: ${url} failed:\n${reason}`).join('\n\n'));
+ }
+
+ return GooglePhotosUploadUtils.CreateMediaItems(token, newMediaItems, req.body.album).then(
+ results => _success(res, { results, failed }),
+ error => _error(res, mediaError, error)
+ );
+ }
+ });
+
+ register({
+ method: Method.POST,
+ subscription: RouteStore.googlePhotosMediaDownload,
+ onValidation: async ({ req, res }) => {
+ const contents: { mediaItems: MediaItem[] } = req.body;
+ let failed = 0;
+ if (contents) {
+ const completed: Opt<DashUploadUtils.UploadInformation>[] = [];
+ for (let item of contents.mediaItems) {
+ const { contentSize, ...attributes } = await DashUploadUtils.InspectImage(item.baseUrl);
+ const found: Opt<DashUploadUtils.UploadInformation> = await Database.Auxiliary.QueryUploadHistory(contentSize!);
+ if (!found) {
+ const upload = await DashUploadUtils.UploadInspectedImage({ contentSize, ...attributes }, item.filename, prefix).catch(error => _error(res, downloadError, error));
+ if (upload) {
+ completed.push(upload);
+ await Database.Auxiliary.LogUpload(upload);
+ } else {
+ failed++;
+ }
+ } else {
+ completed.push(found);
+ }
+ }
+ if (failed) {
+ return _error(res, UploadError(failed));
+ }
+ return _success(res, completed);
+ }
+ _invalid(res, requestError);
+ }
+ });
+ }
+} \ No newline at end of file
diff --git a/src/server/Initialization.ts b/src/server/Initialization.ts
index 306058d81..6ac0787c5 100644
--- a/src/server/Initialization.ts
+++ b/src/server/Initialization.ts
@@ -20,6 +20,8 @@ import * as request from 'request';
import RouteSubscriber from './RouteSubscriber';
import { publicDirectory } from '.';
+/* RouteSetter is a wrapper around the server that prevents the server
+ from being exposed. */
export type RouteSetter = (server: RouteManager) => void;
export interface InitializationOptions {
listenAtPort: number;
@@ -39,7 +41,7 @@ export default async function InitializeServer(options: InitializationOptions) {
registerAuthenticationRoutes(server);
registerCorsProxy(server);
- const isRelease = determineEnvironment();
+ const isRelease = determineEnvironment(); //vs. dev mode
routeSetter(new RouteManager(server, isRelease));
server.listen(listenAtPort, () => console.log(`server started at http://localhost:${listenAtPort}`));
@@ -74,6 +76,7 @@ function buildWithMiddleware(server: express.Express) {
return server;
}
+/* Determine if the enviroment is dev mode or release mode. */
function determineEnvironment() {
const isRelease = process.env.RELEASE === "true";
diff --git a/src/server/index.ts b/src/server/index.ts
index d68e9faa1..59752d6de 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -99,147 +99,6 @@ function routeSetter(router: RouteManager) {
}
}
});
-
- const EndpointHandlerMap = new Map<GoogleApiServerUtils.Action, GoogleApiServerUtils.ApiRouter>([
- ["create", (api, params) => api.create(params)],
- ["retrieve", (api, params) => api.get(params)],
- ["update", (api, params) => api.batchUpdate(params)],
- ]);
-
- router.addSupervisedRoute({
- method: Method.POST,
- subscription: new RouteSubscriber("googleDocs").add("sector", "action"),
- onValidation: async ({ req, res, user }) => {
- let sector: GoogleApiServerUtils.Service = req.params.sector as GoogleApiServerUtils.Service;
- let action: GoogleApiServerUtils.Action = req.params.action as GoogleApiServerUtils.Action;
- const endpoint = await GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], user.id);
- let handler = EndpointHandlerMap.get(action);
- if (endpoint && handler) {
- handler(endpoint, req.body)
- .then(response => res.send(response.data))
- .catch(exception => res.send(exception));
- return;
- }
- res.send(undefined);
- }
- });
-
- router.addSupervisedRoute({
- method: Method.GET,
- subscription: "/readGoogleAccessToken",
- onValidation: async ({ user, res }) => {
- const userId = user.id;
- const token = await GoogleApiServerUtils.retrieveAccessToken(userId);
- if (!token) {
- return res.send(GoogleApiServerUtils.generateAuthenticationUrl());
- }
- return res.send(token);
- }
- });
-
- router.addSupervisedRoute({
- method: Method.POST,
- subscription: "/writeGoogleAccessToken",
- onValidation: async ({ user, req, res }) => {
- res.send(await GoogleApiServerUtils.processNewUser(user.id, req.body.authenticationCode));
- }
- });
-
- const authenticationError = "Unable to authenticate Google credentials before uploading to Google Photos!";
- const mediaError = "Unable to convert all uploaded bytes to media items!";
- interface GooglePhotosUploadFailure {
- batch: number;
- index: number;
- url: string;
- reason: string;
- }
-
- router.addSupervisedRoute({
- method: Method.POST,
- subscription: "/googlePhotosMediaUpload",
- onValidation: async ({ user, req, res }) => {
- const { media } = req.body;
-
- const token = await GoogleApiServerUtils.retrieveAccessToken(user.id);
- if (!token) {
- return _error(res, authenticationError);
- }
-
- let failed: GooglePhotosUploadFailure[] = [];
- const batched = BatchedArray.from<GooglePhotosUploadUtils.UploadSource>(media, { batchSize: 25 });
- const newMediaItems = await batched.batchedMapPatientInterval<GooglePhotosUploadUtils.NewMediaItem>(
- { magnitude: 100, unit: TimeUnit.Milliseconds },
- async (batch, collector, { completedBatches }) => {
- for (let index = 0; index < batch.length; index++) {
- const { url, description } = batch[index];
- const fail = (reason: string) => failed.push({ reason, batch: completedBatches + 1, index, url });
- const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(token, url).catch(fail);
- if (!uploadToken) {
- fail(`${path.extname(url)} is not an accepted extension`);
- } else {
- collector.push({
- description,
- simpleMediaItem: { uploadToken }
- });
- }
- }
- }
- );
-
- const failedCount = failed.length;
- if (failedCount) {
- console.error(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`);
- console.log(failed.map(({ reason, batch, index, url }) => `@${batch}.${index}: ${url} failed:\n${reason}`).join('\n\n'));
- }
-
- return GooglePhotosUploadUtils.CreateMediaItems(token, newMediaItems, req.body.album).then(
- results => _success(res, { results, failed }),
- error => _error(res, mediaError, error)
- );
- }
- });
-
- interface MediaItem {
- baseUrl: string;
- filename: string;
- }
- const prefix = "google_photos_";
-
- const downloadError = "Encountered an error while executing downloads.";
- const requestError = "Unable to execute download: the body's media items were malformed.";
-
- const UploadError = (count: number) => `Unable to upload ${count} images to Dash's server`;
- router.addSupervisedRoute({
- method: Method.POST,
- subscription: "/googlePhotosMediaDownload",
- onValidation: async ({ req, res }) => {
- const contents: { mediaItems: MediaItem[] } = req.body;
- let failed = 0;
- if (contents) {
- const completed: Opt<DashUploadUtils.UploadInformation>[] = [];
- for (let item of contents.mediaItems) {
- const { contentSize, ...attributes } = await DashUploadUtils.InspectImage(item.baseUrl);
- const found: Opt<DashUploadUtils.UploadInformation> = await Database.Auxiliary.QueryUploadHistory(contentSize!);
- if (!found) {
- const upload = await DashUploadUtils.UploadInspectedImage({ contentSize, ...attributes }, item.filename, prefix).catch(error => _error(res, downloadError, error));
- if (upload) {
- completed.push(upload);
- await Database.Auxiliary.LogUpload(upload);
- } else {
- failed++;
- }
- } else {
- completed.push(found);
- }
- }
- if (failed) {
- return _error(res, UploadError(failed));
- }
- return _success(res, completed);
- }
- _invalid(res, requestError);
- }
- });
}
(async function start() {