From af25eaf2a848278a58f0993cba2e68c05da0760c Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Tue, 29 Oct 2019 20:00:54 -0400
Subject: comments and fixes for google photos server sid

---
 src/client/util/Import & Export/DirectoryImportBox.tsx | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

(limited to 'src/client/util/Import & Export/DirectoryImportBox.tsx')

diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index d74b51993..2d1b6fe20 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -20,9 +20,8 @@ import { listSpec } from "../../../new_fields/Schema";
 import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
 import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
 import "./DirectoryImportBox.scss";
-import { Identified } from "../../Network";
+import { Networking } from "../../Network";
 import { BatchedArray } from "array-batcher";
-import { ExifData } from "exif";
 
 const unsupported = ["text/html", "text/plain"];
 
@@ -117,7 +116,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
                 formData.append(Utils.GenerateGuid(), file);
             });
 
-            const responses = await Identified.PostFormDataToServer(RouteStore.upload, formData);
+            const responses = await Networking.PostFormDataToServer(RouteStore.upload, formData);
             runInAction(() => this.completed += batch.length);
             return responses as ImageUploadResponse[];
         });
-- 
cgit v1.2.3-70-g09d2


From 9c7e619fb9d3116649ec3779bd528b947235d5a4 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Wed, 30 Oct 2019 15:51:29 -0400
Subject: updated array batcher

---
 package.json                                       |  2 +-
 .../util/Import & Export/DirectoryImportBox.tsx    |  5 +--
 src/server/RouteManager.ts                         |  1 +
 src/server/apis/google/GooglePhotosUploadUtils.ts  | 11 ++---
 src/server/index.ts                                | 48 ++++++++++++----------
 5 files changed, 36 insertions(+), 31 deletions(-)

(limited to 'src/client/util/Import & Export/DirectoryImportBox.tsx')

diff --git a/package.json b/package.json
index 8cbbb84af..4572a3f73 100644
--- a/package.json
+++ b/package.json
@@ -115,7 +115,7 @@
         "@types/youtube": "0.0.38",
         "adm-zip": "^0.4.13",
         "archiver": "^3.0.3",
-        "array-batcher": "^1.1.3",
+        "array-batcher": "^1.2.3",
         "async": "^2.6.2",
         "babel-runtime": "^6.26.0",
         "bcrypt-nodejs": "0.0.3",
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 2d1b6fe20..bdd59cb16 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -107,7 +107,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
 
         runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`);
 
-        const uploads = await BatchedArray.from(validated, { batchSize: 15 }).batchedMapAsync(async batch => {
+        const uploads = await BatchedArray.from(validated, { batchSize: 15 }).batchedMapAsync<ImageUploadResponse>(async (batch, collector) => {
             const formData = new FormData();
 
             batch.forEach(file => {
@@ -116,9 +116,8 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
                 formData.append(Utils.GenerateGuid(), file);
             });
 
-            const responses = await Networking.PostFormDataToServer(RouteStore.upload, formData);
+            collector.push(...(await Networking.PostFormDataToServer(RouteStore.upload, formData)));
             runInAction(() => this.completed += batch.length);
-            return responses as ImageUploadResponse[];
         });
 
         await Promise.all(uploads.map(async upload => {
diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts
index eda2a49d2..c1d38327f 100644
--- a/src/server/RouteManager.ts
+++ b/src/server/RouteManager.ts
@@ -114,6 +114,7 @@ export const STATUS = {
 };
 
 export function _error(res: express.Response, message: string, error?: any) {
+    console.error(message);
     res.statusMessage = message;
     res.status(STATUS.EXECUTION_ERROR).send(error);
 }
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
index 172fa8d46..d3442338b 100644
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -1,7 +1,7 @@
 import request = require('request-promise');
 import { GoogleApiServerUtils } from './GoogleApiServerUtils';
 import * as path from 'path';
-import { MediaItemCreationResult } from './SharedTypes';
+import { MediaItemCreationResult, NewMediaItemResult } from './SharedTypes';
 import { NewMediaItem } from "../../index";
 import { BatchedArray, TimeUnit } from 'array-batcher';
 import { DashUploadUtils } from '../../DashUploadUtils';
@@ -50,9 +50,9 @@ export namespace GooglePhotosUploadUtils {
     };
 
     export const CreateMediaItems = async (bearerToken: string, newMediaItems: NewMediaItem[], album?: { id: string }): Promise<MediaItemCreationResult> => {
-        const newMediaItemResults = await BatchedArray.from(newMediaItems, { batchSize: 50 }).batchedMapPatientInterval(
+        const newMediaItemResults = await BatchedArray.from(newMediaItems, { batchSize: 50 }).batchedMapPatientInterval<NewMediaItemResult>(
             { magnitude: 100, unit: TimeUnit.Milliseconds },
-            async (batch: NewMediaItem[]) => {
+            async (batch: NewMediaItem[], collector) => {
                 const parameters = {
                     method: 'POST',
                     headers: headers('json', bearerToken),
@@ -61,7 +61,7 @@ export namespace GooglePhotosUploadUtils {
                     json: true
                 };
                 album && (parameters.body.albumId = album.id);
-                return (await new Promise<MediaItemCreationResult>((resolve, reject) => {
+                const { newMediaItemResults } = await new Promise<MediaItemCreationResult>((resolve, reject) => {
                     request(parameters, (error, _response, body) => {
                         if (error) {
                             reject(error);
@@ -69,7 +69,8 @@ export namespace GooglePhotosUploadUtils {
                             resolve(body);
                         }
                     });
-                })).newMediaItemResults;
+                });
+                collector.push(...newMediaItemResults);
             }
         );
         return { newMediaItemResults };
diff --git a/src/server/index.ts b/src/server/index.ts
index 860cde3b5..05c866eae 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -573,18 +573,15 @@ function routeSetter(router: RouteManager) {
         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;
-            return GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], user.id).then(endpoint => {
-                let handler = EndpointHandlerMap.get(action);
-                if (endpoint && handler) {
-                    let execute = handler(endpoint, req.body).then(
-                        response => res.send(response.data),
-                        rejection => res.send(rejection)
-                    );
-                    execute.catch(exception => res.send(exception));
-                    return;
-                }
-                res.send(undefined);
-            });
+            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);
         }
     });
 
@@ -611,6 +608,12 @@ function routeSetter(router: RouteManager) {
 
     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,
@@ -623,30 +626,31 @@ function routeSetter(router: RouteManager) {
                 return _error(res, authenticationError);
             }
 
-            let failed: number[] = [];
-            const newMediaItems = await BatchedArray.from<GooglePhotosUploadUtils.MediaInput>(media, { batchSize: 25 }).batchedMapPatientInterval(
+            let failed: GooglePhotosUploadFailure[] = [];
+            const batched = BatchedArray.from<GooglePhotosUploadUtils.MediaInput>(media, { batchSize: 25 });
+            const newMediaItems = await batched.batchedMapPatientInterval<NewMediaItem>(
                 { magnitude: 100, unit: TimeUnit.Milliseconds },
-                async (batch: GooglePhotosUploadUtils.MediaInput[]) => {
-                    const newMediaItems: NewMediaItem[] = [];
+                async (batch, collector, { completedBatches }) => {
                     for (let index = 0; index < batch.length; index++) {
-                        const element = batch[index];
-                        const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(token, element.url);
+                        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) {
-                            failed.push(index);
+                            fail(`${path.extname(url)} is not an accepted extension`);
                         } else {
-                            newMediaItems.push({
-                                description: element.description,
+                            collector.push({
+                                description,
                                 simpleMediaItem: { uploadToken }
                             });
                         }
                     }
-                    return newMediaItems;
                 }
             );
 
             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: ${reason}`).join('\n'));
             }
 
             return GooglePhotosUploadUtils.CreateMediaItems(token, newMediaItems, req.body.album).then(
-- 
cgit v1.2.3-70-g09d2


From 0b72a27ead9d1e933ae349b8a3e9e9b8702664d1 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sat, 9 Nov 2019 16:18:23 -0500
Subject: factored out all but google resources into managers

---
 client_secret.json                                 |   1 -
 .../util/Import & Export/DirectoryImportBox.tsx    |  10 +-
 src/server/ApiManagers/DeleteManager.ts            |  65 +++
 src/server/ApiManagers/ExportManager.ts            | 157 +++++-
 src/server/ApiManagers/PDFManager.ts               | 107 ++++
 src/server/ApiManagers/SearchManager.ts            |   4 +-
 src/server/ApiManagers/UploadManager.ts            | 227 ++++++++
 src/server/ApiManagers/UserManager.ts              |  47 +-
 src/server/DashUploadUtils.ts                      |  17 +-
 src/server/SharedMediaTypes.ts                     |   9 +
 src/server/Websocket/Websocket.ts                  |  19 +-
 src/server/apis/google/GoogleApiServerUtils.ts     |  46 +-
 src/server/apis/google/GooglePhotosUploadUtils.ts  |  19 +-
 src/server/credentials/CredentialsLoader.ts        |  29 ++
 .../credentials/google_docs_credentials.json       |  11 -
 .../credentials/google_project_credentials.json    |  14 +
 src/server/credentials/test.json                   |  14 +
 src/server/index.ts                                | 570 ++-------------------
 18 files changed, 770 insertions(+), 596 deletions(-)
 delete mode 100644 client_secret.json
 create mode 100644 src/server/ApiManagers/DeleteManager.ts
 create mode 100644 src/server/ApiManagers/PDFManager.ts
 create mode 100644 src/server/ApiManagers/UploadManager.ts
 create mode 100644 src/server/SharedMediaTypes.ts
 create mode 100644 src/server/credentials/CredentialsLoader.ts
 delete mode 100644 src/server/credentials/google_docs_credentials.json
 create mode 100644 src/server/credentials/google_project_credentials.json
 create mode 100644 src/server/credentials/test.json

(limited to 'src/client/util/Import & Export/DirectoryImportBox.tsx')

diff --git a/client_secret.json b/client_secret.json
deleted file mode 100644
index a9c698421..000000000
--- a/client_secret.json
+++ /dev/null
@@ -1 +0,0 @@
-{"installed":{"client_id":"1005546247619-kqpnvh42mpa803tem8556b87umi4j9r0.apps.googleusercontent.com","project_id":"brown-dash","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"WshLb5TH9SdFVGGbQcnYj7IU","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}
\ No newline at end of file
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index bdd59cb16..2e0ba25eb 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -22,6 +22,9 @@ import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
 import "./DirectoryImportBox.scss";
 import { Networking } from "../../Network";
 import { BatchedArray } from "array-batcher";
+import * as path from 'path';
+import { DashUploadUtils } from "../../../server/DashUploadUtils";
+import { SharedMediaTypes } from "../../../server/SharedMediaTypes";
 
 const unsupported = ["text/html", "text/plain"];
 
@@ -94,7 +97,12 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
         let validated: File[] = [];
         for (let i = 0; i < files.length; i++) {
             let file = files.item(i);
-            file && !unsupported.includes(file.type) && validated.push(file);
+            if (file && !unsupported.includes(file.type)) {
+                const ext = path.extname(file.name).toLowerCase();
+                if (SharedMediaTypes.imageFormats.includes(ext)) {
+                    validated.push(file);
+                }
+            }
         }
 
         runInAction(() => {
diff --git a/src/server/ApiManagers/DeleteManager.ts b/src/server/ApiManagers/DeleteManager.ts
new file mode 100644
index 000000000..bbf1d0425
--- /dev/null
+++ b/src/server/ApiManagers/DeleteManager.ts
@@ -0,0 +1,65 @@
+import ApiManager, { Registration } from "./ApiManager";
+import { Method, _permission_denied } from "../RouteManager";
+import { RouteStore } from "../RouteStore";
+import { WebSocket } from "../Websocket/Websocket";
+import { Database } from "../database";
+
+export default class DeleteManager extends ApiManager {
+
+    protected initialize(register: Registration): void {
+
+        register({
+            method: Method.GET,
+            subscription: RouteStore.delete,
+            onValidation: async ({ res, isRelease }) => {
+                if (isRelease) {
+                    return _permission_denied(res, deletionPermissionError);
+                }
+                await WebSocket.deleteFields();
+                res.redirect(RouteStore.home);
+            }
+        });
+
+        register({
+            method: Method.GET,
+            subscription: RouteStore.deleteAll,
+            onValidation: async ({ res, isRelease }) => {
+                if (isRelease) {
+                    return _permission_denied(res, deletionPermissionError);
+                }
+                await WebSocket.deleteAll();
+                res.redirect(RouteStore.home);
+            }
+        });
+
+
+        register({
+            method: Method.GET,
+            subscription: "/deleteWithAux",
+            onValidation: async ({ res, isRelease }) => {
+                if (isRelease) {
+                    return _permission_denied(res, deletionPermissionError);
+                }
+                await Database.Auxiliary.DeleteAll();
+                res.redirect(RouteStore.delete);
+            }
+        });
+
+        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);
+            }
+        });
+
+
+    }
+
+}
+
+const deletionPermissionError = "Cannot perform a delete operation outside of the development environment!";
diff --git a/src/server/ApiManagers/ExportManager.ts b/src/server/ApiManagers/ExportManager.ts
index 14ac7dd5b..d42db1056 100644
--- a/src/server/ApiManagers/ExportManager.ts
+++ b/src/server/ApiManagers/ExportManager.ts
@@ -1,5 +1,5 @@
 import ApiManager, { Registration } from "./ApiManager";
-import RouteManager, { Method } from "../RouteManager";
+import { Method } from "../RouteManager";
 import RouteSubscriber from "../RouteSubscriber";
 import { RouteStore } from "../RouteStore";
 import * as Archiver from 'archiver';
@@ -7,6 +7,7 @@ import * as express from 'express';
 import { Database } from "../database";
 import * as path from "path";
 import { DashUploadUtils } from "../DashUploadUtils";
+import { publicDirectory } from "..";
 
 export type Hierarchy = { [id: string]: string | Hierarchy };
 export type ZipMutator = (file: Archiver.Archiver) => void | Promise<void>;
@@ -15,10 +16,20 @@ export interface DocumentElements {
     title: string;
 }
 
-export default class ExportManager extends ApiManager {
+export default class DownloadManager extends ApiManager {
 
     protected initialize(register: Registration): void {
 
+        /**
+         * Let's say someone's using Dash to organize images in collections.
+         * This lets them export the hierarchy they've built to their
+         * own file system in a useful format.
+         * 
+         * This handler starts with a single document id (interesting only
+         * if it's that of a collection). It traverses the database, captures
+         * the nesting of only nested images or collections, writes
+         * that to a zip file and returns it to the client for download.
+         */
         register({
             method: Method.GET,
             subscription: new RouteSubscriber(RouteStore.imageHierarchyExport).add('docId'),
@@ -29,10 +40,101 @@ export default class ExportManager extends ApiManager {
                 return BuildAndDispatchZip(res, zip => writeHierarchyRecursive(zip, hierarchy));
             }
         });
+
+        register({
+            method: Method.GET,
+            subscription: new RouteSubscriber("/downloadId").add("docId"),
+            onValidation: async ({ req, res }) => {
+                return BuildAndDispatchZip(res, async zip => {
+                    const { id, docs, files } = await getDocs(req.params.docId);
+                    const docString = JSON.stringify({ id, docs });
+                    zip.append(docString, { name: "doc.json" });
+                    files.forEach(val => {
+                        zip.file(publicDirectory + val, { name: val.substring(1) });
+                    });
+                });
+            }
+        });
+
+        register({
+            method: Method.GET,
+            subscription: new RouteSubscriber("/serializeDoc").add("docId"),
+            onValidation: async ({ req, res }) => {
+                const { docs, files } = await getDocs(req.params.docId);
+                res.send({ docs, files: Array.from(files) });
+            }
+        });
+
+
     }
 
 }
 
+async function getDocs(id: string) {
+    const files = new Set<string>();
+    const docs: { [id: string]: any } = {};
+    const fn = (doc: any): string[] => {
+        const id = doc.id;
+        if (typeof id === "string" && id.endsWith("Proto")) {
+            //Skip protos
+            return [];
+        }
+        const ids: string[] = [];
+        for (const key in doc.fields) {
+            if (!doc.fields.hasOwnProperty(key)) {
+                continue;
+            }
+            const field = doc.fields[key];
+            if (field === undefined || field === null) {
+                continue;
+            }
+
+            if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
+                ids.push(field.fieldId);
+            } else if (field.__type === "script" || field.__type === "computed") {
+                if (field.captures) {
+                    ids.push(field.captures.fieldId);
+                }
+            } else if (field.__type === "list") {
+                ids.push(...fn(field));
+            } else if (typeof field === "string") {
+                const re = /"(?:dataD|d)ocumentId"\s*:\s*"([\w\-]*)"/g;
+                let match: string[] | null;
+                while ((match = re.exec(field)) !== null) {
+                    ids.push(match[1]);
+                }
+            } else if (field.__type === "RichTextField") {
+                const re = /"href"\s*:\s*"(.*?)"/g;
+                let match: string[] | null;
+                while ((match = re.exec(field.Data)) !== null) {
+                    const urlString = match[1];
+                    const split = new URL(urlString).pathname.split("doc/");
+                    if (split.length > 1) {
+                        ids.push(split[split.length - 1]);
+                    }
+                }
+                const re2 = /"src"\s*:\s*"(.*?)"/g;
+                while ((match = re2.exec(field.Data)) !== null) {
+                    const urlString = match[1];
+                    const pathname = new URL(urlString).pathname;
+                    files.add(pathname);
+                }
+            } else if (["audio", "image", "video", "pdf", "web"].includes(field.__type)) {
+                const url = new URL(field.url);
+                const pathname = url.pathname;
+                files.add(pathname);
+            }
+        }
+
+        if (doc.id) {
+            docs[doc.id] = doc;
+        }
+        return ids;
+    };
+    await Database.Instance.visit([id], fn);
+    return { id, docs, files };
+}
+
 /**
  * This utility function factors out the process
  * of creating a zip file and sending it back to the client
@@ -45,6 +147,8 @@ export default class ExportManager extends ApiManager {
  * @param mutator the callback function used to actually modify and insert information into the zip instance
  */
 export async function BuildAndDispatchZip(res: express.Response, mutator: ZipMutator): Promise<void> {
+    res.set('Content-disposition', `attachment;`);
+    res.set('Content-Type', "application/zip");
     const zip = Archiver('zip');
     zip.pipe(res);
     await mutator(zip);
@@ -76,7 +180,6 @@ following the general recursive structure shown immediately below
     }
 }
 */
-
 async function buildHierarchyRecursive(seedId: string, hierarchy: Hierarchy): Promise<void> {
     const { title, data } = await getData(seedId);
     const label = `${title} (${seedId})`;
@@ -93,9 +196,20 @@ async function buildHierarchyRecursive(seedId: string, hierarchy: Hierarchy): Pr
     }
 }
 
-async function getData(seedId: string): Promise<DocumentElements> {
+/**
+ * This is a very specific utility method to help traverse the database
+ * to parse data and titles out of images and collections alone.
+ * 
+ * We don't know if the document id given to is corresponds to a view document or a data
+ * document. If it's a data document, the response from the database will have
+ * a data field. If not, call recursively on the proto, and resolve with *its* data
+ * 
+ * @param targetId the id of the Dash document whose data is being requests
+ * @returns the data of the document, as well as its title
+ */
+async function getData(targetId: string): Promise<DocumentElements> {
     return new Promise<DocumentElements>((resolve, reject) => {
-        Database.Instance.getDocument(seedId, async (result: any) => {
+        Database.Instance.getDocument(targetId, async (result: any) => {
             const { data, proto, title } = result.fields;
             if (data) {
                 if (data.url) {
@@ -105,29 +219,50 @@ async function getData(seedId: string): Promise<DocumentElements> {
                 } else {
                     reject();
                 }
-            }
-            if (proto) {
+            } else if (proto) {
                 getData(proto.fieldId).then(resolve, reject);
+            } else {
+                reject();
             }
         });
     });
 }
 
+/**
+ * 
+ * @param file the zip file to which we write the files
+ * @param hierarchy the data structure from which we read, defining the nesting of the documents in the zip
+ * @param prefix lets us create nested folders in the zip file by continually appending to the end
+ * of the prefix with each layer of recursion.
+ * 
+ * Function Call #1 => "Dash Export"
+ * Function Call #2 => "Dash Export/a nested collection"
+ * Function Call #3 => "Dash Export/a nested collection/lowest level collection"
+ * ...
+ */
 async function writeHierarchyRecursive(file: Archiver.Archiver, hierarchy: Hierarchy, prefix = "Dash Export"): Promise<void> {
-    for (const key of Object.keys(hierarchy)) {
-        const result = hierarchy[key];
+    for (const documentTitle of Object.keys(hierarchy)) {
+        const result = hierarchy[documentTitle];
+        // base case or leaf node, we've hit a url (image)
         if (typeof result === "string") {
             let path: string;
             let matches: RegExpExecArray | null;
             if ((matches = /\:1050\/files\/(upload\_[\da-z]{32}.*)/g.exec(result)) !== null) {
+                // image already exists on our server
                 path = `${__dirname}/public/files/${matches[1]}`;
             } else {
+                // the image doesn't already exist on our server (may have been dragged
+                // and dropped in the browser and thus hosted remotely) so we upload it
+                // to our server and point the zip file to it, so it can bundle up the bytes
                 const information = await DashUploadUtils.UploadImage(result);
                 path = information.mediaPaths[0];
             }
-            file.file(path, { name: key, prefix });
+            // write the file specified by the path to the directory in the
+            // zip file given by the prefix.
+            file.file(path, { name: documentTitle, prefix });
         } else {
-            await writeHierarchyRecursive(file, result, `${prefix}/${key}`);
+            // we've hit a collection, so we have to recurse
+            await writeHierarchyRecursive(file, result, `${prefix}/${documentTitle}`);
         }
     }
 }
\ No newline at end of file
diff --git a/src/server/ApiManagers/PDFManager.ts b/src/server/ApiManagers/PDFManager.ts
new file mode 100644
index 000000000..f328557b4
--- /dev/null
+++ b/src/server/ApiManagers/PDFManager.ts
@@ -0,0 +1,107 @@
+import ApiManager, { Registration } from "./ApiManager";
+import { Method } from "../RouteManager";
+import RouteSubscriber from "../RouteSubscriber";
+import { exists, createReadStream, createWriteStream } from "fs";
+import { filesDirectory } from "..";
+import * as Pdfjs from 'pdfjs-dist';
+import { createCanvas } from "canvas";
+const probe = require("probe-image-size");
+import * as express from "express";
+import * as path from "path";
+
+export default class PDFManager extends ApiManager {
+
+    protected initialize(register: Registration): void {
+
+        register({
+            method: Method.GET,
+            subscription: new RouteSubscriber("/thumbnail").add("filename"),
+            onValidation: ({ req, res }) => {
+                let filename = req.params.filename;
+                let noExt = filename.substring(0, filename.length - ".png".length);
+                let pagenumber = parseInt(noExt.split('-')[1]);
+                return new Promise<void>(resolve => {
+                    exists(filesDirectory + filename, (exists: boolean) => {
+                        console.log(`${filesDirectory + filename} ${exists ? "exists" : "does not exist"}`);
+                        if (exists) {
+                            let input = createReadStream(filesDirectory + filename);
+                            probe(input, (err: any, result: any) => {
+                                if (err) {
+                                    console.log(err);
+                                    console.log(`error on ${filename}`);
+                                    return;
+                                }
+                                res.send({ path: "/files/" + filename, width: result.width, height: result.height });
+                            });
+                        }
+                        else {
+                            LoadPage(filesDirectory + filename.substring(0, filename.length - noExt.split('-')[1].length - ".PNG".length - 1) + ".pdf", pagenumber, res);
+                        }
+                        resolve();
+                    });
+                });
+            }
+        });
+
+        function LoadPage(file: string, pageNumber: number, res: express.Response) {
+            console.log(file);
+            Pdfjs.getDocument(file).promise
+                .then((pdf: Pdfjs.PDFDocumentProxy) => {
+                    let factory = new NodeCanvasFactory();
+                    console.log(pageNumber);
+                    pdf.getPage(pageNumber).then((page: Pdfjs.PDFPageProxy) => {
+                        console.log("reading " + page);
+                        let viewport = page.getViewport(1 as any);
+                        let canvasAndContext = factory.create(viewport.width, viewport.height);
+                        let renderContext = {
+                            canvasContext: canvasAndContext.context,
+                            viewport: viewport,
+                            canvasFactory: factory
+                        };
+                        console.log("read " + pageNumber);
+
+                        page.render(renderContext).promise
+                            .then(() => {
+                                console.log("saving " + pageNumber);
+                                let stream = canvasAndContext.canvas.createPNGStream();
+                                let pngFile = `${file.substring(0, file.length - ".pdf".length)}-${pageNumber}.PNG`;
+                                let out = createWriteStream(pngFile);
+                                stream.pipe(out);
+                                out.on("finish", () => {
+                                    console.log(`Success! Saved to ${pngFile}`);
+                                    let name = path.basename(pngFile);
+                                    res.send({ path: "/files/" + name, width: viewport.width, height: viewport.height });
+                                });
+                            }, (reason: string) => {
+                                console.error(reason + ` ${pageNumber}`);
+                            });
+                    });
+                });
+        }
+
+    }
+
+}
+
+class NodeCanvasFactory {
+    create = (width: number, height: number) => {
+        var canvas = createCanvas(width, height);
+        var context = canvas.getContext('2d');
+        return {
+            canvas,
+            context
+        };
+    }
+
+    reset = (canvasAndContext: any, width: number, height: number) => {
+        canvasAndContext.canvas.width = width;
+        canvasAndContext.canvas.height = height;
+    }
+
+    destroy = (canvasAndContext: any) => {
+        canvasAndContext.canvas.width = 0;
+        canvasAndContext.canvas.height = 0;
+        canvasAndContext.canvas = null;
+        canvasAndContext.context = null;
+    }
+}
\ No newline at end of file
diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts
index 1c4b805e5..1c801715a 100644
--- a/src/server/ApiManagers/SearchManager.ts
+++ b/src/server/ApiManagers/SearchManager.ts
@@ -3,7 +3,7 @@ import { Method } from "../RouteManager";
 import { Search } from "../Search";
 var findInFiles = require('find-in-files');
 import * as path from 'path';
-import { uploadDirectory } from "..";
+import { filesDirectory } from "..";
 
 export default class SearchManager extends ApiManager {
 
@@ -18,7 +18,7 @@ export default class SearchManager extends ApiManager {
                     res.send([]);
                     return;
                 }
-                let results = await findInFiles.find({ 'term': q, 'flags': 'ig' }, uploadDirectory + "text", ".txt$");
+                let results = await findInFiles.find({ 'term': q, 'flags': 'ig' }, filesDirectory + "text", ".txt$");
                 let resObj: { ids: string[], numFound: number, lines: string[] } = { ids: [], numFound: 0, lines: [] };
                 for (var result in results) {
                     resObj.ids.push(path.basename(result, ".txt").replace(/upload_/, ""));
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
new file mode 100644
index 000000000..38635eda5
--- /dev/null
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -0,0 +1,227 @@
+import ApiManager, { Registration } from "./ApiManager";
+import { Method, _success } from "../RouteManager";
+import * as formidable from 'formidable';
+import v4 = require('uuid/v4');
+var AdmZip = require('adm-zip');
+import * as path from 'path';
+import { createReadStream, createWriteStream, unlink, readFileSync } from "fs";
+import { publicDirectory, filesDirectory, Partitions } from "..";
+import { RouteStore } from "../RouteStore";
+import { Database } from "../database";
+import { DashUploadUtils } from "../DashUploadUtils";
+import { Opt } from "../../new_fields/Doc";
+import { ParsedPDF } from "../PdfTypes";
+const pdf = require('pdf-parse');
+import * as sharp from 'sharp';
+import { SharedMediaTypes } from "../SharedMediaTypes";
+const imageDataUri = require('image-data-uri');
+
+export default class UploadManager extends ApiManager {
+
+    protected initialize(register: Registration): void {
+
+        register({
+            method: Method.POST,
+            subscription: "/uploadDoc",
+            onValidation: ({ req, res }) => {
+                let form = new formidable.IncomingForm();
+                form.keepExtensions = true;
+                // let path = req.body.path;
+                const ids: { [id: string]: string } = {};
+                let remap = true;
+                const getId = (id: string): string => {
+                    if (!remap) return id;
+                    if (id.endsWith("Proto")) return id;
+                    if (id in ids) {
+                        return ids[id];
+                    } else {
+                        return ids[id] = v4();
+                    }
+                };
+                const mapFn = (doc: any) => {
+                    if (doc.id) {
+                        doc.id = getId(doc.id);
+                    }
+                    for (const key in doc.fields) {
+                        if (!doc.fields.hasOwnProperty(key)) {
+                            continue;
+                        }
+                        const field = doc.fields[key];
+                        if (field === undefined || field === null) {
+                            continue;
+                        }
+
+                        if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
+                            field.fieldId = getId(field.fieldId);
+                        } else if (field.__type === "script" || field.__type === "computed") {
+                            if (field.captures) {
+                                field.captures.fieldId = getId(field.captures.fieldId);
+                            }
+                        } else if (field.__type === "list") {
+                            mapFn(field);
+                        } else if (typeof field === "string") {
+                            const re = /("(?:dataD|d)ocumentId"\s*:\s*")([\w\-]*)"/g;
+                            doc.fields[key] = (field as any).replace(re, (match: any, p1: string, p2: string) => {
+                                return `${p1}${getId(p2)}"`;
+                            });
+                        } else if (field.__type === "RichTextField") {
+                            const re = /("href"\s*:\s*")(.*?)"/g;
+                            field.Data = field.Data.replace(re, (match: any, p1: string, p2: string) => {
+                                return `${p1}${getId(p2)}"`;
+                            });
+                        }
+                    }
+                };
+                return new Promise<void>(resolve => {
+                    form.parse(req, async (_err, fields, files) => {
+                        remap = fields.remap !== "false";
+                        let id: string = "";
+                        try {
+                            for (const name in files) {
+                                const path_2 = files[name].path;
+                                const zip = new AdmZip(path_2);
+                                zip.getEntries().forEach((entry: any) => {
+                                    if (!entry.entryName.startsWith("files/")) return;
+                                    let dirname = path.dirname(entry.entryName) + "/";
+                                    let extname = path.extname(entry.entryName);
+                                    let basename = path.basename(entry.entryName).split(".")[0];
+                                    // zip.extractEntryTo(dirname + basename + "_o" + extname, __dirname + RouteStore.public, true, false);
+                                    // zip.extractEntryTo(dirname + basename + "_s" + extname, __dirname + RouteStore.public, true, false);
+                                    // zip.extractEntryTo(dirname + basename + "_m" + extname, __dirname + RouteStore.public, true, false);
+                                    // zip.extractEntryTo(dirname + basename + "_l" + extname, __dirname + RouteStore.public, true, false);
+                                    try {
+                                        zip.extractEntryTo(entry.entryName, __dirname + RouteStore.public, true, false);
+                                        dirname = "/" + dirname;
+
+                                        createReadStream(publicDirectory + dirname + basename + extname).pipe(createWriteStream(publicDirectory + dirname + basename + "_o" + extname));
+                                        createReadStream(publicDirectory + dirname + basename + extname).pipe(createWriteStream(publicDirectory + dirname + basename + "_s" + extname));
+                                        createReadStream(publicDirectory + dirname + basename + extname).pipe(createWriteStream(publicDirectory + dirname + basename + "_m" + extname));
+                                        createReadStream(publicDirectory + dirname + basename + extname).pipe(createWriteStream(publicDirectory + dirname + basename + "_l" + extname));
+                                    } catch (e) {
+                                        console.log(e);
+                                    }
+                                });
+                                const json = zip.getEntry("doc.json");
+                                let docs: any;
+                                try {
+                                    let data = JSON.parse(json.getData().toString("utf8"));
+                                    docs = data.docs;
+                                    id = data.id;
+                                    docs = Object.keys(docs).map(key => docs[key]);
+                                    docs.forEach(mapFn);
+                                    await Promise.all(docs.map((doc: any) => new Promise(res => Database.Instance.replace(doc.id, doc, (err, r) => {
+                                        err && console.log(err);
+                                        res();
+                                    }, true, "newDocuments"))));
+                                } catch (e) { console.log(e); }
+                                unlink(path_2, () => { });
+                            }
+                            if (id) {
+                                res.send(JSON.stringify(getId(id)));
+                            } else {
+                                res.send(JSON.stringify("error"));
+                            }
+                        } catch (e) { console.log(e); }
+                        resolve();
+                    });
+                });
+            }
+        });
+
+
+        register({
+            method: Method.POST,
+            subscription: RouteStore.upload,
+            onValidation: async ({ req, res }) => {
+                let form = new formidable.IncomingForm();
+                form.uploadDir = filesDirectory;
+                form.keepExtensions = true;
+                return new Promise<void>(resolve => {
+                    form.parse(req, async (_err, _fields, files) => {
+                        let results: DashUploadUtils.ImageFileResponse[] = [];
+                        for (const key in files) {
+                            const { type, path: location, name } = files[key];
+                            const filename = path.basename(location);
+                            let uploadInformation: Opt<DashUploadUtils.UploadInformation>;
+                            if (filename.endsWith(".pdf")) {
+                                let dataBuffer = readFileSync(filesDirectory + filename);
+                                const result: ParsedPDF = await pdf(dataBuffer);
+                                await new Promise<void>((resolve, reject) => {
+                                    const path = filesDirectory + Partitions.PdfText + "/" + filename.substring(0, filename.length - ".pdf".length) + ".txt";
+                                    createWriteStream(path).write(result.text, error => {
+                                        if (!error) {
+                                            resolve();
+                                        } else {
+                                            reject(error);
+                                        }
+                                    });
+                                });
+                            } else {
+                                uploadInformation = await DashUploadUtils.UploadImage(filesDirectory + filename, filename);
+                            }
+                            const exif = uploadInformation ? uploadInformation.exifData : undefined;
+                            results.push({ name, type, path: `/files/${filename}`, exif });
+                        }
+                        _success(res, results);
+                        resolve();
+                    });
+                });
+            }
+        });
+
+        register({
+            method: Method.POST,
+            subscription: RouteStore.inspectImage,
+            onValidation: async ({ req, res }) => {
+                const { source } = req.body;
+                if (typeof source === "string") {
+                    const uploadInformation = await DashUploadUtils.UploadImage(source);
+                    return res.send(await DashUploadUtils.InspectImage(uploadInformation.mediaPaths[0]));
+                }
+                res.send({});
+            }
+        });
+
+        register({
+            method: Method.POST,
+            subscription: RouteStore.dataUriToImage,
+            onValidation: ({ req, res }) => {
+                const uri = req.body.uri;
+                const filename = req.body.name;
+                if (!uri || !filename) {
+                    res.status(401).send("incorrect parameters specified");
+                    return;
+                }
+                return imageDataUri.outputFile(uri, filesDirectory + filename).then((savedName: string) => {
+                    const ext = path.extname(savedName).toLowerCase();
+                    const { pngs, jpgs } = SharedMediaTypes;
+                    let resizers = [
+                        { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
+                        { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" },
+                        { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" },
+                    ];
+                    let isImage = false;
+                    if (pngs.includes(ext)) {
+                        resizers.forEach(element => {
+                            element.resizer = element.resizer.png();
+                        });
+                        isImage = true;
+                    } else if (jpgs.includes(ext)) {
+                        resizers.forEach(element => {
+                            element.resizer = element.resizer.jpeg();
+                        });
+                        isImage = true;
+                    }
+                    if (isImage) {
+                        resizers.forEach(resizer => {
+                            createReadStream(savedName).pipe(resizer.resizer).pipe(createWriteStream(filesDirectory + filename + resizer.suffix + ext));
+                        });
+                    }
+                    res.send("/files/" + filename + ext);
+                });
+            }
+        });
+
+    }
+
+}
\ No newline at end of file
diff --git a/src/server/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts
index dd1e50133..fe1ce7f2b 100644
--- a/src/server/ApiManagers/UserManager.ts
+++ b/src/server/ApiManagers/UserManager.ts
@@ -1,11 +1,36 @@
 import ApiManager, { Registration } from "./ApiManager";
 import { Method } from "../RouteManager";
 import { WebSocket } from "../Websocket/Websocket";
+import { RouteStore } from "../RouteStore";
+import { Database } from "../database";
 
 export default class UserManager extends ApiManager {
 
     protected initialize(register: Registration): void {
 
+        register({
+            method: Method.GET,
+            subscription: RouteStore.getUsers,
+            onValidation: async ({ res }) => {
+                const cursor = await Database.Instance.query({}, { email: 1, userDocumentId: 1 }, "users");
+                const results = await cursor.toArray();
+                res.send(results.map(user => ({ email: user.email, userDocumentId: user.userDocumentId })));
+            }
+        });
+
+        register({
+            method: Method.GET,
+            subscription: RouteStore.getUserDocumentId,
+            onValidation: ({ res, user }) => res.send(user.userDocumentId)
+        });
+
+        register({
+            method: Method.GET,
+            subscription: RouteStore.getCurrUser,
+            onValidation: ({ res, user }) => res.send(JSON.stringify(user)),
+            onUnauthenticated: ({ res }) => res.send(JSON.stringify({ id: "__guest__", email: "" }))
+        });
+
         register({
             method: Method.GET,
             subscription: "/whosOnline",
@@ -17,7 +42,7 @@ export default class UserManager extends ApiManager {
                 for (const user in timeMap) {
                     const time = timeMap[user];
                     const key = ((now - time) / 1000) < (60 * 5) ? "active" : "inactive";
-                    users[key][user] = `Last active ${this.msToTime(now - time)} ago`;
+                    users[key][user] = `Last active ${msToTime(now - time)} ago`;
                 }
 
                 res.send(users);
@@ -26,17 +51,17 @@ export default class UserManager extends ApiManager {
 
     }
 
-    private msToTime(duration: number) {
-        let milliseconds = Math.floor((duration % 1000) / 100),
-            seconds = Math.floor((duration / 1000) % 60),
-            minutes = Math.floor((duration / (1000 * 60)) % 60),
-            hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
+}
 
-        let hoursS = (hours < 10) ? "0" + hours : hours;
-        let minutesS = (minutes < 10) ? "0" + minutes : minutes;
-        let secondsS = (seconds < 10) ? "0" + seconds : seconds;
+function msToTime(duration: number) {
+    let milliseconds = Math.floor((duration % 1000) / 100),
+        seconds = Math.floor((duration / 1000) % 60),
+        minutes = Math.floor((duration / (1000 * 60)) % 60),
+        hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
 
-        return hoursS + ":" + minutesS + ":" + secondsS + "." + milliseconds;
-    }
+    let hoursS = (hours < 10) ? "0" + hours : hours;
+    let minutesS = (minutes < 10) ? "0" + minutes : minutes;
+    let secondsS = (seconds < 10) ? "0" + seconds : seconds;
 
+    return hoursS + ":" + minutesS + ":" + secondsS + "." + milliseconds;
 }
\ No newline at end of file
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 9fddb466c..8f5b0e1a8 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -5,6 +5,7 @@ import * as sharp from 'sharp';
 import request = require('request-promise');
 import { ExifData, ExifImage } from 'exif';
 import { Opt } from '../new_fields/Doc';
+import { SharedMediaTypes } from './SharedMediaTypes';
 
 const uploadDirectory = path.join(__dirname, './public/files/');
 
@@ -15,20 +16,21 @@ export namespace DashUploadUtils {
         suffix: string;
     }
 
+    export interface ImageFileResponse {
+        name: string;
+        path: string;
+        type: string;
+        exif: Opt<DashUploadUtils.EnrichedExifData>;
+    }
+
     export const Sizes: { [size: string]: Size } = {
         SMALL: { width: 100, suffix: "_s" },
         MEDIUM: { width: 400, suffix: "_m" },
         LARGE: { width: 900, suffix: "_l" },
     };
 
-    const gifs = [".gif"];
-    const pngs = [".png"];
-    const jpgs = [".jpg", ".jpeg"];
-    const imageFormats = [...pngs, ...jpgs, ...gifs];
-    const videoFormats = [".mov", ".mp4"];
-
     export function validateExtension(url: string) {
-        return imageFormats.includes(path.extname(url).toLowerCase());
+        return SharedMediaTypes.imageFormats.includes(path.extname(url).toLowerCase());
     }
 
     const size = "content-length";
@@ -132,6 +134,7 @@ export namespace DashUploadUtils {
             contentSize,
             contentType,
         };
+        const { pngs, imageFormats, jpgs, videoFormats } = SharedMediaTypes;
         return new Promise<UploadInformation>(async (resolve, reject) => {
             const resizers = [
                 { resizer: sharp().rotate(), suffix: "_o" },
diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts
new file mode 100644
index 000000000..3d3234125
--- /dev/null
+++ b/src/server/SharedMediaTypes.ts
@@ -0,0 +1,9 @@
+export namespace SharedMediaTypes {
+
+    export const gifs = [".gif"];
+    export const pngs = [".png"];
+    export const jpgs = [".jpg", ".jpeg"];
+    export const imageFormats = [...pngs, ...jpgs, ...gifs];
+    export const videoFormats = [".mov", ".mp4"];
+
+}
\ No newline at end of file
diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts
index cd2813d99..f6a6c8718 100644
--- a/src/server/Websocket/Websocket.ts
+++ b/src/server/Websocket/Websocket.ts
@@ -6,7 +6,9 @@ import { Database } from "../database";
 import { Search } from "../Search";
 import * as io from 'socket.io';
 import YoutubeApi from "../apis/youtube/youtubeApiSample";
-import { youtubeApiKey } from "..";
+import { readFile } from "fs";
+import { Credentials } from "google-auth-library";
+import { GoogleCredentialsLoader } from "../credentials/CredentialsLoader";
 
 export namespace WebSocket {
 
@@ -18,6 +20,14 @@ export namespace WebSocket {
     export const socketMap = new Map<SocketIO.Socket, string>();
     export const timeMap: { [id: string]: number } = {};
 
+    export async function start(serverPort: number, isRelease: boolean) {
+        await preliminaryFunctions();
+        initialize(serverPort, isRelease);
+    }
+
+    async function preliminaryFunctions() {
+    }
+
     export function initialize(serverPort: number, isRelease: boolean) {
         const endpoint = io();
         endpoint.listen(serverPort);
@@ -54,14 +64,15 @@ export namespace WebSocket {
     }
 
     function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any[]) => void]) {
+        const { ProjectCredentials } = GoogleCredentialsLoader;
         switch (query.type) {
             case YoutubeQueryTypes.Channels:
-                YoutubeApi.authorizedGetChannel(youtubeApiKey);
+                YoutubeApi.authorizedGetChannel(ProjectCredentials);
                 break;
             case YoutubeQueryTypes.SearchVideo:
-                YoutubeApi.authorizedGetVideos(youtubeApiKey, query.userInput, callback);
+                YoutubeApi.authorizedGetVideos(ProjectCredentials, query.userInput, callback);
             case YoutubeQueryTypes.VideoDetails:
-                YoutubeApi.authorizedGetVideoDetails(youtubeApiKey, query.videoIds, callback);
+                YoutubeApi.authorizedGetVideoDetails(ProjectCredentials, query.videoIds, callback);
         }
     }
 
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index 35a2541a9..b3657ee43 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -1,12 +1,11 @@
 import { google } from "googleapis";
-import { readFile } from "fs";
 import { OAuth2Client, Credentials, OAuth2ClientOptions } from "google-auth-library";
 import { Opt } from "../../../new_fields/Doc";
 import { GaxiosResponse } from "gaxios";
 import request = require('request-promise');
 import * as qs from 'query-string';
 import { Database } from "../../database";
-import * as path from "path";
+import { GoogleCredentialsLoader } from "../../credentials/CredentialsLoader";
 
 /**
  * Scopes give Google users fine granularity of control
@@ -61,6 +60,23 @@ export namespace GoogleApiServerUtils {
      */
     let worker: OAuth2Client;
 
+    /**
+     * This function is called once before the server is started,
+     * reading in Dash's project-specific credentials (client secret
+     * and client id) for later repeated access. It also sets up the
+     * global, intentionally unauthenticated worker OAuth2 client instance. 
+     */
+    export function processProjectCredentials(): void {
+        const { client_secret, client_id, redirect_uris } = GoogleCredentialsLoader.ProjectCredentials;
+        // initialize the global authorization client
+        installed = {
+            clientId: client_id,
+            clientSecret: client_secret,
+            redirectUri: redirect_uris[0]
+        };
+        worker = generateClient();
+    }
+
     /**
      * A briefer format for the response from a 'googleapis' API request
      */
@@ -96,32 +112,6 @@ export namespace GoogleApiServerUtils {
         batchUpdate: ApiHandler;
     }
 
-    /**
-     * This function is called once before the server is started,
-     * reading in Dash's project-specific credentials (client secret
-     * and client id) for later repeated access. It also sets up the
-     * global, intentionally unauthenticated worker OAuth2 client instance. 
-     */
-    export async function loadClientSecret(): Promise<void> {
-        return new Promise((resolve, reject) => {
-            readFile(path.join(__dirname, "../../credentials/google_docs_credentials.json"), async (err, projectCredentials) => {
-                if (err) {
-                    reject(err);
-                    return console.log('Error loading client secret file:', err);
-                }
-                const { client_secret, client_id, redirect_uris } = JSON.parse(projectCredentials.toString()).installed;
-                // initialize the global authorization client
-                installed = {
-                    clientId: client_id,
-                    clientSecret: client_secret,
-                    redirectUri: redirect_uris[0]
-                };
-                worker = generateClient();
-                resolve();
-            });
-        });
-    }
-
     /**
      * Maps the Dash user id of a given user to their single
      * associated OAuth2 client, mitigating the creation
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
index d8cf795b5..0abed3f1d 100644
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -1,7 +1,6 @@
 import request = require('request-promise');
 import * as path from 'path';
-import { MediaItemCreationResult, NewMediaItemResult } from './SharedTypes';
-import { NewMediaItem } from "../../index";
+import { NewMediaItemResult } from './SharedTypes';
 import { BatchedArray, TimeUnit } from 'array-batcher';
 import { DashUploadUtils } from '../../DashUploadUtils';
 
@@ -28,6 +27,22 @@ export namespace GooglePhotosUploadUtils {
         description: string;
     }
 
+    /**
+     * This is the format needed to pass
+     * into the BatchCreate API request
+     * to take a reference to raw uploaded bytes
+     * and actually create an image in Google Photos.
+     * 
+     * So, to instantiate this interface you must have already dispatched an upload
+     * and received an upload token.
+     */
+    export interface NewMediaItem {
+        description: string;
+        simpleMediaItem: {
+            uploadToken: string;
+        };
+    }
+
     /**
      * A utility function to streamline making
      * calls to the API's url - accentuates
diff --git a/src/server/credentials/CredentialsLoader.ts b/src/server/credentials/CredentialsLoader.ts
new file mode 100644
index 000000000..e3f4d167b
--- /dev/null
+++ b/src/server/credentials/CredentialsLoader.ts
@@ -0,0 +1,29 @@
+import { readFile } from "fs";
+
+export namespace GoogleCredentialsLoader {
+
+    export interface InstalledCredentials {
+        client_id: string;
+        project_id: string;
+        auth_uri: string;
+        token_uri: string;
+        auth_provider_x509_cert_url: string;
+        client_secret: string;
+        redirect_uris: string[];
+    }
+
+    export let ProjectCredentials: InstalledCredentials;
+
+    export async function loadCredentials() {
+        ProjectCredentials = await new Promise<InstalledCredentials>(resolve => {
+            readFile(__dirname + '/google_project_credentials.json', function processClientSecrets(err, content) {
+                if (err) {
+                    console.log('Error loading client secret file: ' + err);
+                    return;
+                }
+                resolve(JSON.parse(content.toString()).installed);
+            });
+        });
+    }
+
+}
diff --git a/src/server/credentials/google_docs_credentials.json b/src/server/credentials/google_docs_credentials.json
deleted file mode 100644
index 955c5a3c1..000000000
--- a/src/server/credentials/google_docs_credentials.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
-    "installed": {
-        "client_id": "343179513178-ud6tvmh275r2fq93u9eesrnc66t6akh9.apps.googleusercontent.com",
-        "project_id": "quickstart-1565056383187",
-        "auth_uri": "https://accounts.google.com/o/oauth2/auth",
-        "token_uri": "https://oauth2.googleapis.com/token",
-        "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
-        "client_secret": "w8KIFSc0MQpmUYHed4qEzn8b",
-        "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost"]
-    }
-}
\ No newline at end of file
diff --git a/src/server/credentials/google_project_credentials.json b/src/server/credentials/google_project_credentials.json
new file mode 100644
index 000000000..5d9c62eb1
--- /dev/null
+++ b/src/server/credentials/google_project_credentials.json
@@ -0,0 +1,14 @@
+{
+    "installed": {
+        "client_id": "1005546247619-kqpnvh42mpa803tem8556b87umi4j9r0.apps.googleusercontent.com",
+        "project_id": "brown-dash",
+        "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+        "token_uri": "https://oauth2.googleapis.com/token",
+        "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+        "client_secret": "WshLb5TH9SdFVGGbQcnYj7IU",
+        "redirect_uris": [
+            "urn:ietf:wg:oauth:2.0:oob",
+            "http://localhost"
+        ]
+    }
+}
\ No newline at end of file
diff --git a/src/server/credentials/test.json b/src/server/credentials/test.json
new file mode 100644
index 000000000..0a032cc2d
--- /dev/null
+++ b/src/server/credentials/test.json
@@ -0,0 +1,14 @@
+{
+    "installed": {
+        "client_id": "343179513178-ud6tvmh275r2fq93u9eesrnc66t6akh9.apps.googleusercontent.com",
+        "project_id": "quickstart-1565056383187",
+        "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+        "token_uri": "https://oauth2.googleapis.com/token",
+        "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+        "client_secret": "w8KIFSc0MQpmUYHed4qEzn8b",
+        "redirect_uris": [
+            "urn:ietf:wg:oauth:2.0:oob",
+            "http://localhost"
+        ]
+    }
+}
\ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 25697e71f..8eb88cf8b 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -1,352 +1,75 @@
 require('dotenv').config();
-import * as formidable from 'formidable';
-import * as fs from 'fs';
-import * as sharp from 'sharp';
-import * as Pdfjs from 'pdfjs-dist';
-const imageDataUri = require('image-data-uri');
+import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils";
 import * as mobileDetect from 'mobile-detect';
 import * as path from 'path';
 import { Database } from './database';
 import { RouteStore } from './RouteStore';
-import v4 = require('uuid/v4');
-import { createCanvas } from "canvas";
 const serverPort = 4321;
-import { Search } from './Search';
-import * as Archiver from 'archiver';
-var AdmZip = require('adm-zip');
-import * as YoutubeApi from "./apis/youtube/youtubeApiSample";
-import { Response } from 'express-serve-static-core';
-import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils";
-const probe = require("probe-image-size");
-const pdf = require('pdf-parse');
 import { GooglePhotosUploadUtils } from './apis/google/GooglePhotosUploadUtils';
 import { Opt } from '../new_fields/Doc';
 import { DashUploadUtils } from './DashUploadUtils';
 import { BatchedArray, TimeUnit } from 'array-batcher';
-import { ParsedPDF } from "./PdfTypes";
-import { reject } from 'bluebird';
 import RouteSubscriber from './RouteSubscriber';
-import InitializeServer from './Initialization';
+import initializeServer from './Initialization';
 import RouteManager, { Method, _success, _permission_denied, _error, _invalid, OnUnauthenticated } from './RouteManager';
 import * as qs from 'query-string';
 import UtilManager from './ApiManagers/UtilManager';
 import SearchManager from './ApiManagers/SearchManager';
 import UserManager from './ApiManagers/UserManager';
 import { WebSocket } from './Websocket/Websocket';
-import ExportManager from './ApiManagers/ExportManager';
-import ApiManager from './ApiManagers/ApiManager';
-
-export let youtubeApiKey: string;
-
-export interface NewMediaItem {
-    description: string;
-    simpleMediaItem: {
-        uploadToken: string;
-    };
+import DownloadManager from './ApiManagers/ExportManager';
+import { GoogleCredentialsLoader } from './credentials/CredentialsLoader';
+import DeleteManager from "./ApiManagers/DeleteManager";
+import PDFManager from "./ApiManagers/PDFManager";
+import UploadManager from "./ApiManagers/UploadManager";
+
+export const publicDirectory = __dirname + RouteStore.public;
+export const filesDirectory = publicDirectory + "/files/";
+export enum Partitions {
+    PdfText = "pdf_text"
 }
 
-const pngTypes = [".png", ".PNG"];
-const jpgTypes = [".jpg", ".JPG", ".jpeg", ".JPEG"];
-export const uploadDirectory = __dirname + "/public/files/";
-const pdfDirectory = uploadDirectory + "text";
-const solrURL = "http://localhost:8983/solr/#/dash";
-
-start();
-
-async function start() {
-    await PreliminaryFunctions();
-    await InitializeServer({ listenAtPort: 1050, routeSetter });
-}
-
-async function PreliminaryFunctions() {
-    await new Promise<void>(resolve => {
-        YoutubeApi.readApiKey((apiKey: string) => {
-            youtubeApiKey = apiKey;
-            resolve();
-        });
-    });
-    await GoogleApiServerUtils.loadClientSecret();
-    await DashUploadUtils.createIfNotExists(pdfDirectory);
+/**
+ * These are the functions run before the server starts
+ * listening. Anything that must be complete
+ * before clients can access the server should be run or awaited here.
+ */
+async function preliminaryFunctions() {
+    // make project credentials globally accessible
+    await GoogleCredentialsLoader.loadCredentials();
+    // read the resulting credentials into a different namespace
+    GoogleApiServerUtils.processProjectCredentials();
+    // divide the public directory based on type
+    await Promise.all(Object.keys(Partitions).map(partition => DashUploadUtils.createIfNotExists(filesDirectory + partition)));
+    // connect to the database
     await Database.tryInitializeConnection();
 }
 
+/**
+ * Either clustered together as an API manager
+ * or individually referenced below, by the completion
+ * of this function's execution, all routes will
+ * be registered on the server 
+ * @param router the instance of the route manager
+ * that will manage the registration of new routes
+ * with the server
+ */
 function routeSetter(router: RouteManager) {
-    const managers: ApiManager[] = [
-        new UtilManager(),
-        new SearchManager(),
+    // initialize API Managers
+    [
         new UserManager(),
-        new ExportManager()
-    ];
-    managers.forEach(manager => manager.register(router));
+        new UploadManager(),
+        new DownloadManager(),
+        new SearchManager(),
+        new PDFManager(),
+        new DeleteManager(),
+        new UtilManager()
+    ].forEach(manager => manager.register(router));
 
+    // initialize the web socket (bidirectional communication: if a user changes
+    // a field on one client, that change must be broadcast to all other clients)
     WebSocket.initialize(serverPort, router.isRelease);
 
-    async function getDocs(id: string) {
-        const files = new Set<string>();
-        const docs: { [id: string]: any } = {};
-        const fn = (doc: any): string[] => {
-            const id = doc.id;
-            if (typeof id === "string" && id.endsWith("Proto")) {
-                //Skip protos
-                return [];
-            }
-            const ids: string[] = [];
-            for (const key in doc.fields) {
-                if (!doc.fields.hasOwnProperty(key)) {
-                    continue;
-                }
-                const field = doc.fields[key];
-                if (field === undefined || field === null) {
-                    continue;
-                }
-
-                if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
-                    ids.push(field.fieldId);
-                } else if (field.__type === "script" || field.__type === "computed") {
-                    if (field.captures) {
-                        ids.push(field.captures.fieldId);
-                    }
-                } else if (field.__type === "list") {
-                    ids.push(...fn(field));
-                } else if (typeof field === "string") {
-                    const re = /"(?:dataD|d)ocumentId"\s*:\s*"([\w\-]*)"/g;
-                    let match: string[] | null;
-                    while ((match = re.exec(field)) !== null) {
-                        ids.push(match[1]);
-                    }
-                } else if (field.__type === "RichTextField") {
-                    const re = /"href"\s*:\s*"(.*?)"/g;
-                    let match: string[] | null;
-                    while ((match = re.exec(field.Data)) !== null) {
-                        const urlString = match[1];
-                        const split = new URL(urlString).pathname.split("doc/");
-                        if (split.length > 1) {
-                            ids.push(split[split.length - 1]);
-                        }
-                    }
-                    const re2 = /"src"\s*:\s*"(.*?)"/g;
-                    while ((match = re2.exec(field.Data)) !== null) {
-                        const urlString = match[1];
-                        const pathname = new URL(urlString).pathname;
-                        files.add(pathname);
-                    }
-                } else if (["audio", "image", "video", "pdf", "web"].includes(field.__type)) {
-                    const url = new URL(field.url);
-                    const pathname = url.pathname;
-                    files.add(pathname);
-                }
-            }
-
-            if (doc.id) {
-                docs[doc.id] = doc;
-            }
-            return ids;
-        };
-        await Database.Instance.visit([id], fn);
-        return { id, docs, files };
-    }
-
-    router.addSupervisedRoute({
-        method: Method.GET,
-        subscription: new RouteSubscriber("/serializeDoc").add("docId"),
-        onValidation: async ({ req, res }) => {
-            const { docs, files } = await getDocs(req.params.docId);
-            res.send({ docs, files: Array.from(files) });
-        }
-    });
-
-    router.addSupervisedRoute({
-        method: Method.GET,
-        subscription: new RouteSubscriber("/downloadId").add("docId"),
-        onValidation: async ({ req, res }) => {
-            res.set('Content-disposition', `attachment;`);
-            res.set('Content-Type', "application/zip");
-            const { id, docs, files } = await getDocs(req.params.docId);
-            const docString = JSON.stringify({ id, docs });
-            const zip = Archiver('zip');
-            zip.pipe(res);
-            zip.append(docString, { name: "doc.json" });
-            files.forEach(val => {
-                zip.file(__dirname + RouteStore.public + val, { name: val.substring(1) });
-            });
-            zip.finalize();
-        }
-    });
-
-    router.addSupervisedRoute({
-        method: Method.POST,
-        subscription: "/uploadDoc",
-        onValidation: ({ req, res }) => {
-            let form = new formidable.IncomingForm();
-            form.keepExtensions = true;
-            // let path = req.body.path;
-            const ids: { [id: string]: string } = {};
-            let remap = true;
-            const getId = (id: string): string => {
-                if (!remap) return id;
-                if (id.endsWith("Proto")) return id;
-                if (id in ids) {
-                    return ids[id];
-                } else {
-                    return ids[id] = v4();
-                }
-            };
-            const mapFn = (doc: any) => {
-                if (doc.id) {
-                    doc.id = getId(doc.id);
-                }
-                for (const key in doc.fields) {
-                    if (!doc.fields.hasOwnProperty(key)) {
-                        continue;
-                    }
-                    const field = doc.fields[key];
-                    if (field === undefined || field === null) {
-                        continue;
-                    }
-
-                    if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
-                        field.fieldId = getId(field.fieldId);
-                    } else if (field.__type === "script" || field.__type === "computed") {
-                        if (field.captures) {
-                            field.captures.fieldId = getId(field.captures.fieldId);
-                        }
-                    } else if (field.__type === "list") {
-                        mapFn(field);
-                    } else if (typeof field === "string") {
-                        const re = /("(?:dataD|d)ocumentId"\s*:\s*")([\w\-]*)"/g;
-                        doc.fields[key] = (field as any).replace(re, (match: any, p1: string, p2: string) => {
-                            return `${p1}${getId(p2)}"`;
-                        });
-                    } else if (field.__type === "RichTextField") {
-                        const re = /("href"\s*:\s*")(.*?)"/g;
-                        field.Data = field.Data.replace(re, (match: any, p1: string, p2: string) => {
-                            return `${p1}${getId(p2)}"`;
-                        });
-                    }
-                }
-            };
-            return new Promise<void>(resolve => {
-                form.parse(req, async (_err, fields, files) => {
-                    remap = fields.remap !== "false";
-                    let id: string = "";
-                    try {
-                        for (const name in files) {
-                            const path_2 = files[name].path;
-                            const zip = new AdmZip(path_2);
-                            zip.getEntries().forEach((entry: any) => {
-                                if (!entry.entryName.startsWith("files/")) return;
-                                let dirname = path.dirname(entry.entryName) + "/";
-                                let extname = path.extname(entry.entryName);
-                                let basename = path.basename(entry.entryName).split(".")[0];
-                                // zip.extractEntryTo(dirname + basename + "_o" + extname, __dirname + RouteStore.public, true, false);
-                                // zip.extractEntryTo(dirname + basename + "_s" + extname, __dirname + RouteStore.public, true, false);
-                                // zip.extractEntryTo(dirname + basename + "_m" + extname, __dirname + RouteStore.public, true, false);
-                                // zip.extractEntryTo(dirname + basename + "_l" + extname, __dirname + RouteStore.public, true, false);
-                                try {
-                                    zip.extractEntryTo(entry.entryName, __dirname + RouteStore.public, true, false);
-                                    dirname = "/" + dirname;
-
-                                    fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_o" + extname));
-                                    fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_s" + extname));
-                                    fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_m" + extname));
-                                    fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_l" + extname));
-                                } catch (e) {
-                                    console.log(e);
-                                }
-                            });
-                            const json = zip.getEntry("doc.json");
-                            let docs: any;
-                            try {
-                                let data = JSON.parse(json.getData().toString("utf8"));
-                                docs = data.docs;
-                                id = data.id;
-                                docs = Object.keys(docs).map(key => docs[key]);
-                                docs.forEach(mapFn);
-                                await Promise.all(docs.map((doc: any) => new Promise(res => Database.Instance.replace(doc.id, doc, (err, r) => {
-                                    err && console.log(err);
-                                    res();
-                                }, true, "newDocuments"))));
-                            } catch (e) { console.log(e); }
-                            fs.unlink(path_2, () => { });
-                        }
-                        if (id) {
-                            res.send(JSON.stringify(getId(id)));
-                        } else {
-                            res.send(JSON.stringify("error"));
-                        }
-                    } catch (e) { console.log(e); }
-                    resolve();
-                });
-            });
-        }
-    });
-
-    router.addSupervisedRoute({
-        method: Method.GET,
-        subscription: new RouteSubscriber("/thumbnail").add("filename"),
-        onValidation: ({ req, res }) => {
-            let filename = req.params.filename;
-            let noExt = filename.substring(0, filename.length - ".png".length);
-            let pagenumber = parseInt(noExt.split('-')[1]);
-            return new Promise<void>(resolve => {
-                fs.exists(uploadDirectory + filename, (exists: boolean) => {
-                    console.log(`${uploadDirectory + filename} ${exists ? "exists" : "does not exist"}`);
-                    if (exists) {
-                        let input = fs.createReadStream(uploadDirectory + filename);
-                        probe(input, (err: any, result: any) => {
-                            if (err) {
-                                console.log(err);
-                                console.log(`error on ${filename}`);
-                                return;
-                            }
-                            res.send({ path: "/files/" + filename, width: result.width, height: result.height });
-                        });
-                    }
-                    else {
-                        LoadPage(uploadDirectory + filename.substring(0, filename.length - noExt.split('-')[1].length - ".PNG".length - 1) + ".pdf", pagenumber, res);
-                    }
-                    resolve();
-                });
-            });
-        }
-    });
-
-    function LoadPage(file: string, pageNumber: number, res: Response) {
-        console.log(file);
-        Pdfjs.getDocument(file).promise
-            .then((pdf: Pdfjs.PDFDocumentProxy) => {
-                let factory = new NodeCanvasFactory();
-                console.log(pageNumber);
-                pdf.getPage(pageNumber).then((page: Pdfjs.PDFPageProxy) => {
-                    console.log("reading " + page);
-                    let viewport = page.getViewport(1 as any);
-                    let canvasAndContext = factory.create(viewport.width, viewport.height);
-                    let renderContext = {
-                        canvasContext: canvasAndContext.context,
-                        viewport: viewport,
-                        canvasFactory: factory
-                    };
-                    console.log("read " + pageNumber);
-
-                    page.render(renderContext).promise
-                        .then(() => {
-                            console.log("saving " + pageNumber);
-                            let stream = canvasAndContext.canvas.createPNGStream();
-                            let pngFile = `${file.substring(0, file.length - ".pdf".length)}-${pageNumber}.PNG`;
-                            let out = fs.createWriteStream(pngFile);
-                            stream.pipe(out);
-                            out.on("finish", () => {
-                                console.log(`Success! Saved to ${pngFile}`);
-                                let name = path.basename(pngFile);
-                                res.send({ path: "/files/" + name, width: viewport.width, height: viewport.height });
-                            });
-                        }, (reason: string) => {
-                            console.error(reason + ` ${pageNumber}`);
-                        });
-                });
-            });
-    }
-
     /**
      * Anyone attempting to navigate to localhost at this port will
      * first have to log in.
@@ -357,16 +80,6 @@ function routeSetter(router: RouteManager) {
         onValidation: ({ res }) => res.redirect(RouteStore.home)
     });
 
-    router.addSupervisedRoute({
-        method: Method.GET,
-        subscription: RouteStore.getUsers,
-        onValidation: async ({ res }) => {
-            const cursor = await Database.Instance.query({}, { email: 1, userDocumentId: 1 }, "users");
-            const results = await cursor.toArray();
-            res.send(results.map(user => ({ email: user.email, userDocumentId: user.userDocumentId })));
-        }
-    });
-
     const serve: OnUnauthenticated = ({ req, res }) => {
         let detector = new mobileDetect(req.headers['user-agent'] || "");
         let filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html';
@@ -387,19 +100,6 @@ function routeSetter(router: RouteManager) {
         }
     });
 
-    router.addSupervisedRoute({
-        method: Method.GET,
-        subscription: RouteStore.getUserDocumentId,
-        onValidation: ({ res, user }) => res.send(user.userDocumentId)
-    });
-
-    router.addSupervisedRoute({
-        method: Method.GET,
-        subscription: RouteStore.getCurrUser,
-        onValidation: ({ res, user }) => res.send(JSON.stringify(user)),
-        onUnauthenticated: ({ res }) => res.send(JSON.stringify({ id: "__guest__", email: "" }))
-    });
-
     const ServicesApiKeyMap = new Map<string, string | undefined>([
         ["face", process.env.FACE],
         ["vision", process.env.VISION],
@@ -415,152 +115,6 @@ function routeSetter(router: RouteManager) {
         }
     });
 
-    class NodeCanvasFactory {
-        create = (width: number, height: number) => {
-            var canvas = createCanvas(width, height);
-            var context = canvas.getContext('2d');
-            return {
-                canvas,
-                context
-            };
-        }
-
-        reset = (canvasAndContext: any, width: number, height: number) => {
-            canvasAndContext.canvas.width = width;
-            canvasAndContext.canvas.height = height;
-        }
-
-        destroy = (canvasAndContext: any) => {
-            canvasAndContext.canvas.width = 0;
-            canvasAndContext.canvas.height = 0;
-            canvasAndContext.canvas = null;
-            canvasAndContext.context = null;
-        }
-    }
-
-    interface ImageFileResponse {
-        name: string;
-        path: string;
-        type: string;
-        exif: Opt<DashUploadUtils.EnrichedExifData>;
-    }
-
-    router.addSupervisedRoute({
-        method: Method.POST,
-        subscription: RouteStore.upload,
-        onValidation: async ({ req, res }) => {
-            let form = new formidable.IncomingForm();
-            form.uploadDir = uploadDirectory;
-            form.keepExtensions = true;
-            return new Promise<void>(resolve => {
-                form.parse(req, async (_err, _fields, files) => {
-                    let results: ImageFileResponse[] = [];
-                    for (const key in files) {
-                        const { type, path: location, name } = files[key];
-                        const filename = path.basename(location);
-                        let uploadInformation: Opt<DashUploadUtils.UploadInformation>;
-                        if (filename.endsWith(".pdf")) {
-                            let dataBuffer = fs.readFileSync(uploadDirectory + filename);
-                            const result: ParsedPDF = await pdf(dataBuffer);
-                            await new Promise<void>(resolve => {
-                                const path = pdfDirectory + "/" + filename.substring(0, filename.length - ".pdf".length) + ".txt";
-                                fs.createWriteStream(path).write(result.text, error => {
-                                    if (!error) {
-                                        resolve();
-                                    } else {
-                                        reject(error);
-                                    }
-                                });
-                            });
-                        } else {
-                            uploadInformation = await DashUploadUtils.UploadImage(uploadDirectory + filename, filename);
-                        }
-                        const exif = uploadInformation ? uploadInformation.exifData : undefined;
-                        results.push({ name, type, path: `/files/${filename}`, exif });
-                    }
-                    _success(res, results);
-                    resolve();
-                });
-            });
-        }
-    });
-
-    router.addSupervisedRoute({
-        method: Method.POST,
-        subscription: RouteStore.inspectImage,
-        onValidation: async ({ req, res }) => {
-            const { source } = req.body;
-            if (typeof source === "string") {
-                const uploadInformation = await DashUploadUtils.UploadImage(source);
-                return res.send(await DashUploadUtils.InspectImage(uploadInformation.mediaPaths[0]));
-            }
-            res.send({});
-        }
-    });
-
-    router.addSupervisedRoute({
-        method: Method.POST,
-        subscription: RouteStore.dataUriToImage,
-        onValidation: ({ req, res }) => {
-            const uri = req.body.uri;
-            const filename = req.body.name;
-            if (!uri || !filename) {
-                res.status(401).send("incorrect parameters specified");
-                return;
-            }
-            return imageDataUri.outputFile(uri, uploadDirectory + filename).then((savedName: string) => {
-                const ext = path.extname(savedName);
-                let resizers = [
-                    { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
-                    { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" },
-                    { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" },
-                ];
-                let isImage = false;
-                if (pngTypes.includes(ext)) {
-                    resizers.forEach(element => {
-                        element.resizer = element.resizer.png();
-                    });
-                    isImage = true;
-                } else if (jpgTypes.includes(ext)) {
-                    resizers.forEach(element => {
-                        element.resizer = element.resizer.jpeg();
-                    });
-                    isImage = true;
-                }
-                if (isImage) {
-                    resizers.forEach(resizer => {
-                        fs.createReadStream(savedName).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDirectory + filename + resizer.suffix + ext));
-                    });
-                }
-                res.send("/files/" + filename + ext);
-            });
-        }
-    });
-
-    router.addSupervisedRoute({
-        method: Method.GET,
-        subscription: RouteStore.delete,
-        onValidation: async ({ res, isRelease }) => {
-            if (isRelease) {
-                return _permission_denied(res, deletionPermissionError);
-            }
-            await WebSocket.deleteFields();
-            res.redirect(RouteStore.home);
-        }
-    });
-
-    router.addSupervisedRoute({
-        method: Method.GET,
-        subscription: RouteStore.deleteAll,
-        onValidation: async ({ res, isRelease }) => {
-            if (isRelease) {
-                return _permission_denied(res, deletionPermissionError);
-            }
-            await WebSocket.deleteAll();
-            res.redirect(RouteStore.home);
-        }
-    });
-
     const EndpointHandlerMap = new Map<GoogleApiServerUtils.Action, GoogleApiServerUtils.ApiRouter>([
         ["create", (api, params) => api.create(params)],
         ["retrieve", (api, params) => api.get(params)],
@@ -628,7 +182,7 @@ function routeSetter(router: RouteManager) {
 
             let failed: GooglePhotosUploadFailure[] = [];
             const batched = BatchedArray.from<GooglePhotosUploadUtils.UploadSource>(media, { batchSize: 25 });
-            const newMediaItems = await batched.batchedMapPatientInterval<NewMediaItem>(
+            const newMediaItems = await batched.batchedMapPatientInterval<GooglePhotosUploadUtils.NewMediaItem>(
                 { magnitude: 100, unit: TimeUnit.Milliseconds },
                 async (batch, collector, { completedBatches }) => {
                     for (let index = 0; index < batch.length; index++) {
@@ -668,31 +222,6 @@ function routeSetter(router: RouteManager) {
 
     const downloadError = "Encountered an error while executing downloads.";
     const requestError = "Unable to execute download: the body's media items were malformed.";
-    const deletionPermissionError = "Cannot perform specialized delete outside of the development environment!";
-
-    router.addSupervisedRoute({
-        method: Method.GET,
-        subscription: "/deleteWithAux",
-        onValidation: async ({ res, isRelease }) => {
-            if (isRelease) {
-                return _permission_denied(res, deletionPermissionError);
-            }
-            await Database.Auxiliary.DeleteAll();
-            res.redirect(RouteStore.delete);
-        }
-    });
-
-    router.addSupervisedRoute({
-        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);
-        }
-    });
 
     const UploadError = (count: number) => `Unable to upload ${count} images to Dash's server`;
     router.addSupervisedRoute({
@@ -726,4 +255,9 @@ function routeSetter(router: RouteManager) {
             _invalid(res, requestError);
         }
     });
-}
\ No newline at end of file
+}
+
+(async function start() {
+    await preliminaryFunctions();
+    await initializeServer({ listenAtPort: 1050, routeSetter });
+})();
-- 
cgit v1.2.3-70-g09d2


From 36ad83493d2bd58dc6fe62df6002789ccc1b06a1 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sun, 10 Nov 2019 14:56:58 -0500
Subject: no more RouteStore

---
 src/Utils.ts                                       |  3 +-
 src/client/apis/GoogleAuthenticationManager.tsx    |  5 +--
 .../apis/google_docs/GoogleApiClientUtils.ts       |  7 ++--
 .../apis/google_docs/GooglePhotosClientUtils.ts    |  5 +--
 src/client/cognitive_services/CognitiveServices.ts |  3 +-
 src/client/util/History.ts                         |  3 +-
 .../util/Import & Export/DirectoryImportBox.tsx    |  3 +-
 src/client/util/Import & Export/ImageUtils.ts      |  7 ++--
 src/client/util/SharingManager.tsx                 |  3 +-
 src/client/views/MainView.tsx                      |  5 +--
 src/client/views/collections/CollectionSubView.tsx |  4 +-
 src/client/views/nodes/ImageBox.tsx                |  3 +-
 src/client/views/nodes/VideoBox.tsx                |  3 +-
 src/client/views/search/SearchBox.tsx              |  3 +-
 src/mobile/ImageUpload.tsx                         |  3 +-
 src/new_fields/RichTextUtils.ts                    |  3 +-
 src/server/ApiManagers/DeleteManager.ts            | 13 +++----
 src/server/ApiManagers/ExportManager.ts            |  5 +--
 src/server/ApiManagers/PDFManager.ts               |  2 +-
 src/server/ApiManagers/UploadManager.ts            | 15 +++-----
 src/server/ApiManagers/UserManager.ts              |  7 ++--
 src/server/ApiManagers/UtilManager.ts              |  7 ++++
 src/server/Initialization.ts                       | 28 +++++++-------
 src/server/RouteManager.ts                         |  5 +--
 src/server/RouteStore.ts                           | 45 ----------------------
 src/server/RouteSubscriber.ts                      |  2 +-
 src/server/authentication/config/passport.ts       |  5 +--
 .../authentication/controllers/user_controller.ts  | 32 +++++++--------
 .../authentication/models/current_user_utils.ts    |  7 ++--
 src/server/index.ts                                | 26 ++++++-------
 30 files changed, 96 insertions(+), 166 deletions(-)
 delete mode 100644 src/server/RouteStore.ts

(limited to 'src/client/util/Import & Export/DirectoryImportBox.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index 9a2f01f80..abff2eaba 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -2,7 +2,6 @@ import v4 = require('uuid/v4');
 import v5 = require("uuid/v5");
 import { Socket } from 'socket.io';
 import { Message } from './server/Message';
-import { RouteStore } from './server/RouteStore';
 
 export namespace Utils {
 
@@ -46,7 +45,7 @@ export namespace Utils {
     }
 
     export function CorsProxy(url: string): string {
-        return prepend(RouteStore.corsProxy + "/") + encodeURIComponent(url);
+        return prepend("/corsProxy/") + encodeURIComponent(url);
     }
 
     export function CopyText(text: string) {
diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx
index 1ec9d8412..ae77c4b7b 100644
--- a/src/client/apis/GoogleAuthenticationManager.tsx
+++ b/src/client/apis/GoogleAuthenticationManager.tsx
@@ -4,7 +4,6 @@ import * as React from "react";
 import MainViewModal from "../views/MainViewModal";
 import { Opt } from "../../new_fields/Doc";
 import { Networking } from "../Network";
-import { RouteStore } from "../../server/RouteStore";
 import "./GoogleAuthenticationManager.scss";
 
 const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth";
@@ -31,7 +30,7 @@ export default class GoogleAuthenticationManager extends React.Component<{}> {
     }
 
     public fetchOrGenerateAccessToken = async () => {
-        let response = await Networking.FetchFromServer(RouteStore.readGoogleAccessToken);
+        let response = await Networking.FetchFromServer("/readGoogleAccessToken");
         // if this is an authentication url, activate the UI to register the new access token
         if (new RegExp(AuthenticationUrl).test(response)) {
             this.isOpen = true;
@@ -44,7 +43,7 @@ export default class GoogleAuthenticationManager extends React.Component<{}> {
                             return;
                         }
                         const { access_token, avatar, name } = await Networking.PostToServer(
-                            RouteStore.writeGoogleAccessToken,
+                            "/writeGoogleAccessToken",
                             { authenticationCode }
                         );
                         runInAction(() => {
diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts
index 183679317..26c7f8d2e 100644
--- a/src/client/apis/google_docs/GoogleApiClientUtils.ts
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -1,5 +1,4 @@
 import { docs_v1, slides_v1 } from "googleapis";
-import { RouteStore } from "../../../server/RouteStore";
 import { Opt } from "../../../new_fields/Doc";
 import { isArray } from "util";
 import { EditorState } from "prosemirror-state";
@@ -77,7 +76,7 @@ export namespace GoogleApiClientUtils {
         * @returns the documentId of the newly generated document, or undefined if the creation process fails.
         */
         export const create = async (options: CreateOptions): Promise<CreationResult> => {
-            const path = `${RouteStore.googleDocs}/Documents/${Actions.Create}`;
+            const path = `/googleDocs/Documents/${Actions.Create}`;
             const parameters = {
                 requestBody: {
                     title: options.title || `Dash Export (${new Date().toDateString()})`
@@ -154,7 +153,7 @@ export namespace GoogleApiClientUtils {
         }
 
         export const retrieve = async (options: RetrieveOptions): Promise<RetrievalResult> => {
-            const path = `${RouteStore.googleDocs}/Documents/${Actions.Retrieve}`;
+            const path = `/googleDocs/Documents/${Actions.Retrieve}`;
             try {
                 const parameters = { documentId: options.documentId };
                 const schema: RetrievalResult = await Networking.PostToServer(path, parameters);
@@ -165,7 +164,7 @@ export namespace GoogleApiClientUtils {
         };
 
         export const update = async (options: UpdateOptions): Promise<UpdateResult> => {
-            const path = `${RouteStore.googleDocs}/Documents/${Actions.Update}`;
+            const path = `/googleDocs/Documents/${Actions.Update}`;
             const parameters = {
                 documentId: options.documentId,
                 requestBody: {
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index 402fc64b5..bf8897061 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -1,5 +1,4 @@
 import { Utils } from "../../../Utils";
-import { RouteStore } from "../../../server/RouteStore";
 import { ImageField } from "../../../new_fields/URLField";
 import { Cast, StrCast } from "../../../new_fields/Types";
 import { Doc, Opt, DocListCastAsync } from "../../../new_fields/Doc";
@@ -307,7 +306,7 @@ export namespace GooglePhotos {
         };
 
         export const WriteMediaItemsToServer = async (body: { mediaItems: any[] }): Promise<UploadInformation[]> => {
-            const uploads = await Networking.PostToServer(RouteStore.googlePhotosMediaDownload, body);
+            const uploads = await Networking.PostToServer("/googlePhotosMediaDownload", body);
             return uploads;
         };
 
@@ -345,7 +344,7 @@ export namespace GooglePhotos {
                 media.push({ url, description });
             }
             if (media.length) {
-                const results = await Networking.PostToServer(RouteStore.googlePhotosMediaUpload, { media, album });
+                const results = await Networking.PostToServer("/googlePhotosMediaUpload", { media, album });
                 return results;
             }
         };
diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts
index 08fcb4883..af5fb39fc 100644
--- a/src/client/cognitive_services/CognitiveServices.ts
+++ b/src/client/cognitive_services/CognitiveServices.ts
@@ -2,7 +2,6 @@ import * as request from "request-promise";
 import { Doc, Field, Opt } from "../../new_fields/Doc";
 import { Cast } from "../../new_fields/Types";
 import { Docs } from "../documents/Documents";
-import { RouteStore } from "../../server/RouteStore";
 import { Utils } from "../../Utils";
 import { InkData } from "../../new_fields/InkField";
 import { UndoManager } from "../util/UndoManager";
@@ -39,7 +38,7 @@ export enum Confidence {
 export namespace CognitiveServices {
 
     const ExecuteQuery = async <D>(service: Service, manager: APIManager<D>, data: D): Promise<any> => {
-        return fetch(Utils.prepend(`${RouteStore.cognitiveServices}/${service}`)).then(async response => {
+        return fetch(Utils.prepend(`cognitiveServices/${service}`)).then(async response => {
             let apiKey = await response.text();
             if (!apiKey) {
                 console.log(`No API key found for ${service}: ensure index.ts has access to a .env file in your root directory`);
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index 899abbe40..1c51236cb 100644
--- a/src/client/util/History.ts
+++ b/src/client/util/History.ts
@@ -1,6 +1,5 @@
 import { Doc, Opt, Field } from "../../new_fields/Doc";
 import { DocServer } from "../DocServer";
-import { RouteStore } from "../../server/RouteStore";
 import { MainView } from "../views/MainView";
 import * as qs from 'query-string';
 import { Utils, OmitKeys } from "../../Utils";
@@ -26,7 +25,7 @@ export namespace HistoryUtil {
 
     // const handlers: ((state: ParsedUrl | null) => void)[] = [];
     function onHistory(e: PopStateEvent) {
-        if (window.location.pathname !== RouteStore.home) {
+        if (window.location.pathname !== "/home") {
             const url = e.state as ParsedUrl || parseUrl(window.location);
             if (url) {
                 switch (url.type) {
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 2e0ba25eb..437e7766b 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -1,7 +1,6 @@
 import "fs";
 import React = require("react");
 import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
-import { RouteStore } from "../../../server/RouteStore";
 import { action, observable, autorun, runInAction, computed, reaction, IReactionDisposer } from "mobx";
 import { FieldViewProps, FieldView } from "../../views/nodes/FieldView";
 import Measure, { ContentRect } from "react-measure";
@@ -124,7 +123,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
                 formData.append(Utils.GenerateGuid(), file);
             });
 
-            collector.push(...(await Networking.PostFormDataToServer(RouteStore.upload, formData)));
+            collector.push(...(await Networking.PostFormDataToServer("/upload", formData)));
             runInAction(() => this.completed += batch.length);
         });
 
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
index 914f4870a..ca80f3bca 100644
--- a/src/client/util/Import & Export/ImageUtils.ts	
+++ b/src/client/util/Import & Export/ImageUtils.ts	
@@ -1,7 +1,6 @@
-import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
+import { Doc } from "../../../new_fields/Doc";
 import { ImageField } from "../../../new_fields/URLField";
 import { Cast, StrCast } from "../../../new_fields/Types";
-import { RouteStore } from "../../../server/RouteStore";
 import { Docs } from "../../documents/Documents";
 import { Networking } from "../../Network";
 import { Id } from "../../../new_fields/FieldSymbols";
@@ -15,7 +14,7 @@ export namespace ImageUtils {
             return false;
         }
         const source = field.url.href;
-        const response = await Networking.PostToServer(RouteStore.inspectImage, { source });
+        const response = await Networking.PostToServer("/inspectImage", { source });
         const { error, data } = response.exifData;
         document.exif = error || Docs.Get.DocumentHierarchyFromJson(data);
         return data !== undefined;
@@ -23,7 +22,7 @@ export namespace ImageUtils {
 
     export const ExportHierarchyToFileSystem = async (collection: Doc): Promise<void> => {
         const a = document.createElement("a");
-        a.href = Utils.prepend(`${RouteStore.imageHierarchyExport}/${collection[Id]}`);
+        a.href = Utils.prepend(`imageHierarchyExport/${collection[Id]}`);
         a.download = `Dash Export [${StrCast(collection.title)}].zip`;
         a.click();
     };
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 2082d6324..cc1d628b1 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -4,7 +4,6 @@ import MainViewModal from "../views/MainViewModal";
 import { Doc, Opt, DocCastAsync } from "../../new_fields/Doc";
 import { DocServer } from "../DocServer";
 import { Cast, StrCast } from "../../new_fields/Types";
-import { RouteStore } from "../../server/RouteStore";
 import * as RequestPromise from "request-promise";
 import { Utils } from "../../Utils";
 import "./SharingManager.scss";
@@ -104,7 +103,7 @@ export default class SharingManager extends React.Component<{}> {
     }
 
     populateUsers = async () => {
-        let userList = await RequestPromise.get(Utils.prepend(RouteStore.getUsers));
+        let userList = await RequestPromise.get(Utils.prepend("/getUsers"));
         const raw = JSON.parse(userList) as User[];
         const evaluating = raw.map(async user => {
             let isCandidate = user.email !== Doc.CurrentUserEmail;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 39585113b..0c5a1003b 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -12,7 +12,6 @@ import { List } from '../../new_fields/List';
 import { listSpec } from '../../new_fields/Schema';
 import { Cast, FieldValue, StrCast } from '../../new_fields/Types';
 import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
-import { RouteStore } from '../../server/RouteStore';
 import { emptyFunction, returnEmptyString, returnFalse, returnOne, returnTrue, Utils } from '../../Utils';
 import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager';
 import { DocServer } from '../DocServer';
@@ -74,7 +73,7 @@ export class MainView extends React.Component {
         this._urlState = HistoryUtil.parseUrl(window.location) || {} as any;
         // causes errors to be generated when modifying an observable outside of an action
         configure({ enforceActions: "observed" });
-        if (window.location.pathname !== RouteStore.home) {
+        if (window.location.pathname !== "/home") {
             let pathname = window.location.pathname.substr(1).split("/");
             if (pathname.length > 1) {
                 let type = pathname[0];
@@ -395,7 +394,7 @@ export class MainView extends React.Component {
                     zoomToScale={emptyFunction}
                     getScale={returnOne}>
                 </DocumentView>
-                <button className="mainView-logout" key="logout" onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>
+                <button className="mainView-logout" key="logout" onClick={() => window.location.assign(Utils.prepend("/logout"))}>
                     {CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"}
                 </button>
             </div></div>;
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 6e8e4fa12..306f8e052 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -8,7 +8,6 @@ import { listSpec } from "../../../new_fields/Schema";
 import { ScriptField } from "../../../new_fields/ScriptField";
 import { Cast } from "../../../new_fields/Types";
 import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { RouteStore } from "../../../server/RouteStore";
 import { Utils } from "../../../Utils";
 import { DocServer } from "../../DocServer";
 import { DocumentType } from "../../documents/DocumentTypes";
@@ -243,7 +242,6 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
             let promises: Promise<void>[] = [];
             // tslint:disable-next-line:prefer-for-of
             for (let i = 0; i < e.dataTransfer.items.length; i++) {
-                const upload = window.location.origin + RouteStore.upload;
                 let item = e.dataTransfer.items[i];
                 if (item.kind === "string" && item.type.indexOf("uri") !== -1) {
                     let str: string;
@@ -268,7 +266,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                     }
                     let dropFileName = file ? file.name : "-empty-";
 
-                    let prom = fetch(upload, {
+                    let prom = fetch(Utils.prepend("/upload"), {
                         method: 'POST',
                         body: formData
                     }).then(async (res: Response) => {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 9f39eccea..07fd832be 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -12,7 +12,6 @@ import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schem
 import { ComputedField } from '../../../new_fields/ScriptField';
 import { BoolCast, Cast, FieldValue, NumCast, StrCast } from '../../../new_fields/Types';
 import { AudioField, ImageField } from '../../../new_fields/URLField';
-import { RouteStore } from '../../../server/RouteStore';
 import { Utils, returnOne, emptyFunction } from '../../../Utils';
 import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
 import { Docs } from '../../documents/Documents';
@@ -152,7 +151,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
             recorder.ondataavailable = async function (e: any) {
                 const formData = new FormData();
                 formData.append("file", e.data);
-                const res = await fetch(Utils.prepend(RouteStore.upload), {
+                const res = await fetch(Utils.prepend("/upload"), {
                     method: 'POST',
                     body: formData
                 });
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index aa9b28118..4884fa2bf 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -6,7 +6,6 @@ import { InkTool } from "../../../new_fields/InkField";
 import { makeInterface, createSchema, listSpec } from "../../../new_fields/Schema";
 import { Cast, FieldValue, NumCast, BoolCast, StrCast } from "../../../new_fields/Types";
 import { VideoField } from "../../../new_fields/URLField";
-import { RouteStore } from "../../../server/RouteStore";
 import { Utils, emptyFunction, returnOne } from "../../../Utils";
 import { Docs, DocUtils } from "../../documents/Documents";
 import { ContextMenu } from "../ContextMenu";
@@ -183,7 +182,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
 
     public static async convertDataUri(imageUri: string, returnedFilename: string) {
         try {
-            let posting = Utils.prepend(RouteStore.dataUriToImage);
+            let posting = Utils.prepend("/uploadURI");
             const returnedUri = await rp.post(posting, {
                 body: {
                     uri: imageUri,
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index b728ffaa9..6100c74df 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -8,7 +8,6 @@ import * as rp from 'request-promise';
 import { Doc } from '../../../new_fields/Doc';
 import { Id } from '../../../new_fields/FieldSymbols';
 import { Cast, NumCast } from '../../../new_fields/Types';
-import { RouteStore } from '../../../server/RouteStore';
 import { Utils } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { SetupDrag } from '../../util/DragManager';
@@ -90,7 +89,7 @@ export class SearchBox extends React.Component {
 
     public static async convertDataUri(imageUri: string, returnedFilename: string) {
         try {
-            let posting = Utils.prepend(RouteStore.dataUriToImage);
+            let posting = Utils.prepend("uploadURI");
             const returnedUri = await rp.post(posting, {
                 body: {
                     uri: imageUri,
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index 33a615cbf..9fdaac66e 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -1,7 +1,6 @@
 import * as ReactDOM from 'react-dom';
 import * as rp from 'request-promise';
 import { Docs } from '../client/documents/Documents';
-import { RouteStore } from '../server/RouteStore';
 import "./ImageUpload.scss";
 import React = require('react');
 import { DocServer } from '../client/DocServer';
@@ -58,7 +57,7 @@ class Uploader extends React.Component {
 
                         this.status = "getting user document";
 
-                        const res = await rp.get(Utils.prepend(RouteStore.getUserDocumentId));
+                        const res = await rp.get(Utils.prepend("/getUserDocumentId"));
                         if (!res) {
                             throw new Error("No user id returned");
                         }
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index 63d718ce8..238b27439 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -8,7 +8,6 @@ import { Opt, Doc } from "./Doc";
 import Color = require('color');
 import { sinkListItem } from "prosemirror-schema-list";
 import { Utils } from "../Utils";
-import { RouteStore } from "../server/RouteStore";
 import { Docs } from "../client/documents/Documents";
 import { schema } from "../client/util/RichTextSchema";
 import { GooglePhotos } from "../client/apis/google_docs/GooglePhotosClientUtils";
@@ -129,7 +128,7 @@ export namespace RichTextUtils {
                     return { baseUrl, filename };
                 });
 
-                const uploads = await Networking.PostToServer(RouteStore.googlePhotosMediaDownload, { mediaItems });
+                const uploads = await Networking.PostToServer("/googlePhotosMediaDownload", { mediaItems });
 
                 if (uploads.length !== mediaItems.length) {
                     throw new AssertionError({ expected: mediaItems.length, actual: uploads.length, message: "Error with internally uploading inlineObjects!" });
diff --git a/src/server/ApiManagers/DeleteManager.ts b/src/server/ApiManagers/DeleteManager.ts
index bbf1d0425..f58a28ce5 100644
--- a/src/server/ApiManagers/DeleteManager.ts
+++ b/src/server/ApiManagers/DeleteManager.ts
@@ -1,6 +1,5 @@
 import ApiManager, { Registration } from "./ApiManager";
 import { Method, _permission_denied } from "../RouteManager";
-import { RouteStore } from "../RouteStore";
 import { WebSocket } from "../Websocket/Websocket";
 import { Database } from "../database";
 
@@ -10,25 +9,25 @@ export default class DeleteManager extends ApiManager {
 
         register({
             method: Method.GET,
-            subscription: RouteStore.delete,
+            subscription: "/delete",
             onValidation: async ({ res, isRelease }) => {
                 if (isRelease) {
                     return _permission_denied(res, deletionPermissionError);
                 }
                 await WebSocket.deleteFields();
-                res.redirect(RouteStore.home);
+                res.redirect("/home");
             }
         });
 
         register({
             method: Method.GET,
-            subscription: RouteStore.deleteAll,
+            subscription: "/deleteAll",
             onValidation: async ({ res, isRelease }) => {
                 if (isRelease) {
                     return _permission_denied(res, deletionPermissionError);
                 }
                 await WebSocket.deleteAll();
-                res.redirect(RouteStore.home);
+                res.redirect("/home");
             }
         });
 
@@ -41,7 +40,7 @@ export default class DeleteManager extends ApiManager {
                     return _permission_denied(res, deletionPermissionError);
                 }
                 await Database.Auxiliary.DeleteAll();
-                res.redirect(RouteStore.delete);
+                res.redirect("/delete");
             }
         });
 
@@ -53,7 +52,7 @@ export default class DeleteManager extends ApiManager {
                     return _permission_denied(res, deletionPermissionError);
                 }
                 await Database.Auxiliary.GoogleAuthenticationToken.DeleteAll();
-                res.redirect(RouteStore.delete);
+                res.redirect("/delete");
             }
         });
 
diff --git a/src/server/ApiManagers/ExportManager.ts b/src/server/ApiManagers/ExportManager.ts
index d42db1056..fc6ba0d22 100644
--- a/src/server/ApiManagers/ExportManager.ts
+++ b/src/server/ApiManagers/ExportManager.ts
@@ -1,7 +1,6 @@
 import ApiManager, { Registration } from "./ApiManager";
 import { Method } from "../RouteManager";
 import RouteSubscriber from "../RouteSubscriber";
-import { RouteStore } from "../RouteStore";
 import * as Archiver from 'archiver';
 import * as express from 'express';
 import { Database } from "../database";
@@ -32,7 +31,7 @@ export default class DownloadManager extends ApiManager {
          */
         register({
             method: Method.GET,
-            subscription: new RouteSubscriber(RouteStore.imageHierarchyExport).add('docId'),
+            subscription: new RouteSubscriber("imageHierarchyExport").add('docId'),
             onValidation: async ({ req, res }) => {
                 const id = req.params.docId;
                 const hierarchy: Hierarchy = {};
@@ -43,7 +42,7 @@ export default class DownloadManager extends ApiManager {
 
         register({
             method: Method.GET,
-            subscription: new RouteSubscriber("/downloadId").add("docId"),
+            subscription: new RouteSubscriber("downloadId").add("docId"),
             onValidation: async ({ req, res }) => {
                 return BuildAndDispatchZip(res, async zip => {
                     const { id, docs, files } = await getDocs(req.params.docId);
diff --git a/src/server/ApiManagers/PDFManager.ts b/src/server/ApiManagers/PDFManager.ts
index f328557b4..632b4965a 100644
--- a/src/server/ApiManagers/PDFManager.ts
+++ b/src/server/ApiManagers/PDFManager.ts
@@ -15,7 +15,7 @@ export default class PDFManager extends ApiManager {
 
         register({
             method: Method.GET,
-            subscription: new RouteSubscriber("/thumbnail").add("filename"),
+            subscription: new RouteSubscriber("thumbnail").add("filename"),
             onValidation: ({ req, res }) => {
                 let filename = req.params.filename;
                 let noExt = filename.substring(0, filename.length - ".png".length);
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 38635eda5..01abdab54 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -6,7 +6,6 @@ var AdmZip = require('adm-zip');
 import * as path from 'path';
 import { createReadStream, createWriteStream, unlink, readFileSync } from "fs";
 import { publicDirectory, filesDirectory, Partitions } from "..";
-import { RouteStore } from "../RouteStore";
 import { Database } from "../database";
 import { DashUploadUtils } from "../DashUploadUtils";
 import { Opt } from "../../new_fields/Doc";
@@ -85,12 +84,8 @@ export default class UploadManager extends ApiManager {
                                     let dirname = path.dirname(entry.entryName) + "/";
                                     let extname = path.extname(entry.entryName);
                                     let basename = path.basename(entry.entryName).split(".")[0];
-                                    // zip.extractEntryTo(dirname + basename + "_o" + extname, __dirname + RouteStore.public, true, false);
-                                    // zip.extractEntryTo(dirname + basename + "_s" + extname, __dirname + RouteStore.public, true, false);
-                                    // zip.extractEntryTo(dirname + basename + "_m" + extname, __dirname + RouteStore.public, true, false);
-                                    // zip.extractEntryTo(dirname + basename + "_l" + extname, __dirname + RouteStore.public, true, false);
                                     try {
-                                        zip.extractEntryTo(entry.entryName, __dirname + RouteStore.public, true, false);
+                                        zip.extractEntryTo(entry.entryName, publicDirectory, true, false);
                                         dirname = "/" + dirname;
 
                                         createReadStream(publicDirectory + dirname + basename + extname).pipe(createWriteStream(publicDirectory + dirname + basename + "_o" + extname));
@@ -131,7 +126,7 @@ export default class UploadManager extends ApiManager {
 
         register({
             method: Method.POST,
-            subscription: RouteStore.upload,
+            subscription: "/upload",
             onValidation: async ({ req, res }) => {
                 let form = new formidable.IncomingForm();
                 form.uploadDir = filesDirectory;
@@ -147,7 +142,7 @@ export default class UploadManager extends ApiManager {
                                 let dataBuffer = readFileSync(filesDirectory + filename);
                                 const result: ParsedPDF = await pdf(dataBuffer);
                                 await new Promise<void>((resolve, reject) => {
-                                    const path = filesDirectory + Partitions.PdfText + "/" + filename.substring(0, filename.length - ".pdf".length) + ".txt";
+                                    const path = filesDirectory + Partitions.pdf_text + "/" + filename.substring(0, filename.length - ".pdf".length) + ".txt";
                                     createWriteStream(path).write(result.text, error => {
                                         if (!error) {
                                             resolve();
@@ -171,7 +166,7 @@ export default class UploadManager extends ApiManager {
 
         register({
             method: Method.POST,
-            subscription: RouteStore.inspectImage,
+            subscription: "/inspectImage",
             onValidation: async ({ req, res }) => {
                 const { source } = req.body;
                 if (typeof source === "string") {
@@ -184,7 +179,7 @@ export default class UploadManager extends ApiManager {
 
         register({
             method: Method.POST,
-            subscription: RouteStore.dataUriToImage,
+            subscription: "/uploadURI",
             onValidation: ({ req, res }) => {
                 const uri = req.body.uri;
                 const filename = req.body.name;
diff --git a/src/server/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts
index fe1ce7f2b..51a434fcf 100644
--- a/src/server/ApiManagers/UserManager.ts
+++ b/src/server/ApiManagers/UserManager.ts
@@ -1,7 +1,6 @@
 import ApiManager, { Registration } from "./ApiManager";
 import { Method } from "../RouteManager";
 import { WebSocket } from "../Websocket/Websocket";
-import { RouteStore } from "../RouteStore";
 import { Database } from "../database";
 
 export default class UserManager extends ApiManager {
@@ -10,7 +9,7 @@ export default class UserManager extends ApiManager {
 
         register({
             method: Method.GET,
-            subscription: RouteStore.getUsers,
+            subscription: "/getUsers",
             onValidation: async ({ res }) => {
                 const cursor = await Database.Instance.query({}, { email: 1, userDocumentId: 1 }, "users");
                 const results = await cursor.toArray();
@@ -20,13 +19,13 @@ export default class UserManager extends ApiManager {
 
         register({
             method: Method.GET,
-            subscription: RouteStore.getUserDocumentId,
+            subscription: "/getUserDocumentId",
             onValidation: ({ res, user }) => res.send(user.userDocumentId)
         });
 
         register({
             method: Method.GET,
-            subscription: RouteStore.getCurrUser,
+            subscription: "/getCurrentUser",
             onValidation: ({ res, user }) => res.send(JSON.stringify(user)),
             onUnauthenticated: ({ res }) => res.send(JSON.stringify({ id: "__guest__", email: "" }))
         });
diff --git a/src/server/ApiManagers/UtilManager.ts b/src/server/ApiManagers/UtilManager.ts
index 61cda2e9b..c1234be6c 100644
--- a/src/server/ApiManagers/UtilManager.ts
+++ b/src/server/ApiManagers/UtilManager.ts
@@ -2,11 +2,18 @@ import ApiManager, { Registration } from "./ApiManager";
 import { Method } from "../RouteManager";
 import { exec } from 'child_process';
 import { command_line } from "../ActionUtilities";
+import RouteSubscriber from "../RouteSubscriber";
 
 export default class UtilManager extends ApiManager {
 
     protected initialize(register: Registration): void {
 
+        register({
+            method: Method.GET,
+            subscription: new RouteSubscriber("environment").add("key"),
+            onValidation: ({ req, res }) => res.send(process.env[req.params.key])
+        });
+
         register({
             method: Method.GET,
             subscription: "/pull",
diff --git a/src/server/Initialization.ts b/src/server/Initialization.ts
index fbb5ae7a6..306058d81 100644
--- a/src/server/Initialization.ts
+++ b/src/server/Initialization.ts
@@ -9,7 +9,6 @@ import flash = require('connect-flash');
 import { Database } from './database';
 import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLogin, postReset, postSignup } from './authentication/controllers/user_controller';
 const MongoStore = require('connect-mongo')(session);
-import { RouteStore } from './RouteStore';
 import RouteManager from './RouteManager';
 import * as webpack from 'webpack';
 const config = require('../../webpack.config');
@@ -18,6 +17,8 @@ import * as wdm from 'webpack-dev-middleware';
 import * as whm from 'webpack-hot-middleware';
 import * as fs from 'fs';
 import * as request from 'request';
+import RouteSubscriber from './RouteSubscriber';
+import { publicDirectory } from '.';
 
 export type RouteSetter = (server: RouteManager) => void;
 export interface InitializationOptions {
@@ -29,8 +30,8 @@ export default async function InitializeServer(options: InitializationOptions) {
     const { listenAtPort, routeSetter } = options;
     const server = buildWithMiddleware(express());
 
-    server.use(express.static(__dirname + RouteStore.public));
-    server.use(RouteStore.images, express.static(__dirname + RouteStore.public));
+    server.use(express.static(publicDirectory));
+    server.use("/images", express.static(publicDirectory));
 
     server.use(wdm(compiler, { publicPath: config.output.publicPath }));
     server.use(whm(compiler));
@@ -87,24 +88,25 @@ function determineEnvironment() {
 }
 
 function registerAuthenticationRoutes(server: express.Express) {
-    server.get(RouteStore.signup, getSignup);
-    server.post(RouteStore.signup, postSignup);
+    server.get("/signup", getSignup);
+    server.post("/signup", postSignup);
 
-    server.get(RouteStore.login, getLogin);
-    server.post(RouteStore.login, postLogin);
+    server.get("/login", getLogin);
+    server.post("/login", postLogin);
 
-    server.get(RouteStore.logout, getLogout);
+    server.get("/logout", getLogout);
 
-    server.get(RouteStore.forgot, getForgot);
-    server.post(RouteStore.forgot, postForgot);
+    server.get("/forgotPassword", getForgot);
+    server.post("/forgotPassword", postForgot);
 
-    server.get(RouteStore.reset, getReset);
-    server.post(RouteStore.reset, postReset);
+    const reset = new RouteSubscriber("resetPassword").add("token").build;
+    server.get(reset, getReset);
+    server.post(reset, postReset);
 }
 
 function registerCorsProxy(server: express.Express) {
     const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
-    server.use(RouteStore.corsProxy, (req, res) => {
+    server.use("/corsProxy", (req, res) => {
         req.pipe(request(decodeURIComponent(req.url.substring(1)))).on("response", res => {
             const headers = Object.keys(res.headers);
             headers.forEach(headerName => {
diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts
index c1d38327f..3aae5734a 100644
--- a/src/server/RouteManager.ts
+++ b/src/server/RouteManager.ts
@@ -1,5 +1,4 @@
 import RouteSubscriber from "./RouteSubscriber";
-import { RouteStore } from "./RouteStore";
 import { DashUserModel } from "./authentication/models/user_model";
 import * as express from 'express';
 
@@ -67,10 +66,10 @@ export default class RouteManager {
                 if (onUnauthenticated) {
                     await tryExecute(onUnauthenticated, core);
                     if (!res.headersSent) {
-                        res.redirect(RouteStore.login);
+                        res.redirect("/login");
                     }
                 } else {
-                    res.redirect(RouteStore.login);
+                    res.redirect("/login");
                 }
             }
             setTimeout(() => {
diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts
deleted file mode 100644
index a310d0c95..000000000
--- a/src/server/RouteStore.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-// PREPEND ALL ROUTES WITH FORWARD SLASHES!
-
-export enum RouteStore {
-    // GENERAL
-    root = "/",
-    home = "/home",
-    corsProxy = "/corsProxy",
-    delete = "/delete",
-    deleteAll = "/deleteAll",
-    pull = "/pull",
-
-    // UPLOAD AND STATIC FILE SERVING
-    public = "/public",
-    upload = "/upload",
-    dataUriToImage = "/uploadURI",
-    images = "/images",
-    inspectImage = "/inspectImage",
-    imageHierarchyExport = "/imageHierarchyExport",
-
-    // USER AND WORKSPACES
-    getCurrUser = "/getCurrentUser",
-    getUsers = "/getUsers",
-    getUserDocumentId = "/getUserDocumentId",
-    updateCursor = "/updateCursor",
-
-    openDocumentWithId = "/doc/:docId",
-
-    // AUTHENTICATION
-    signup = "/signup",
-    login = "/login",
-    logout = "/logout",
-    forgot = "/forgotpassword",
-    reset = "/reset/:token",
-
-    // APIS
-    cognitiveServices = "/cognitiveservices",
-    googleDocs = "/googleDocs",
-    readGoogleAccessToken = "/readGoogleAccessToken",
-    writeGoogleAccessToken = "/writeGoogleAccessToken",
-    googlePhotosMediaUpload = "/googlePhotosMediaUpload",
-    googlePhotosMediaDownload = "/googlePhotosMediaDownload",
-    googleDocsGet = "/googleDocsGet",
-    checkGoogle = "/checkGoogleAuthentication"
-
-}
\ No newline at end of file
diff --git a/src/server/RouteSubscriber.ts b/src/server/RouteSubscriber.ts
index e49be8af5..a1cf7c1c4 100644
--- a/src/server/RouteSubscriber.ts
+++ b/src/server/RouteSubscriber.ts
@@ -3,7 +3,7 @@ export default class RouteSubscriber {
     private requestParameters: string[] = [];
 
     constructor(root: string) {
-        this._root = root;
+        this._root = `/${root}`;
     }
 
     add(...parameters: string[]) {
diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts
index 8915a4abf..0b15c3a36 100644
--- a/src/server/authentication/config/passport.ts
+++ b/src/server/authentication/config/passport.ts
@@ -3,7 +3,6 @@ import * as passportLocal from 'passport-local';
 import _ from "lodash";
 import { default as User } from '../models/user_model';
 import { Request, Response, NextFunction } from "express";
-import { RouteStore } from '../../RouteStore';
 
 const LocalStrategy = passportLocal.Strategy;
 
@@ -35,13 +34,13 @@ export let isAuthenticated = (req: Request, res: Response, next: NextFunction) =
     if (req.isAuthenticated()) {
         return next();
     }
-    return res.redirect(RouteStore.login);
+    return res.redirect("/login");
 };
 
 export let isAuthorized = (req: Request, res: Response, next: NextFunction) => {
     const provider = req.path.split("/").slice(-1)[0];
 
-    if (_.find((req.user as any).tokens, { kind: provider })) {
+    if (_.find((req.user).tokens, { kind: provider })) {
         next();
     } else {
         res.redirect(`/auth/${provider}`);
diff --git a/src/server/authentication/controllers/user_controller.ts b/src/server/authentication/controllers/user_controller.ts
index f5c6e1610..b2b9d33f6 100644
--- a/src/server/authentication/controllers/user_controller.ts
+++ b/src/server/authentication/controllers/user_controller.ts
@@ -10,10 +10,7 @@ import * as pug from 'pug';
 import * as async from 'async';
 import * as nodemailer from 'nodemailer';
 import c = require("crypto");
-import { RouteStore } from "../../RouteStore";
 import { Utils } from "../../../Utils";
-import { Schema } from "mongoose";
-import { Opt } from "../../../new_fields/Doc";
 import { MailOptions } from "nodemailer/lib/stream-transport";
 
 /**
@@ -23,8 +20,7 @@ import { MailOptions } from "nodemailer/lib/stream-transport";
  */
 export let getSignup = (req: Request, res: Response) => {
     if (req.user) {
-        let user = req.user;
-        return res.redirect(RouteStore.home);
+        return res.redirect("/home");
     }
     res.render("signup.pug", {
         title: "Sign Up",
@@ -45,7 +41,7 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => {
     const errors = req.validationErrors();
 
     if (errors) {
-        return res.redirect(RouteStore.signup);
+        return res.redirect("/signup");
     }
 
     const email = req.body.email as String;
@@ -62,7 +58,7 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => {
     User.findOne({ email }, (err, existingUser) => {
         if (err) { return next(err); }
         if (existingUser) {
-            return res.redirect(RouteStore.login);
+            return res.redirect("/login");
         }
         user.save((err: any) => {
             if (err) { return next(err); }
@@ -81,7 +77,7 @@ let tryRedirectToTarget = (req: Request, res: Response) => {
         req.session.target = undefined;
         res.redirect(target);
     } else {
-        res.redirect(RouteStore.home);
+        res.redirect("/home");
     }
 };
 
@@ -93,7 +89,7 @@ let tryRedirectToTarget = (req: Request, res: Response) => {
 export let getLogin = (req: Request, res: Response) => {
     if (req.user) {
         req.session!.target = undefined;
-        return res.redirect(RouteStore.home);
+        return res.redirect("/home");
     }
     res.render("login.pug", {
         title: "Log In",
@@ -115,13 +111,13 @@ export let postLogin = (req: Request, res: Response, next: NextFunction) => {
 
     if (errors) {
         req.flash("errors", "Unable to login at this time. Please try again.");
-        return res.redirect(RouteStore.signup);
+        return res.redirect("/signup");
     }
 
     passport.authenticate("local", (err: Error, user: DashUserModel, info: IVerifyOptions) => {
         if (err) { next(err); return; }
         if (!user) {
-            return res.redirect(RouteStore.signup);
+            return res.redirect("/signup");
         }
         req.logIn(user, (err) => {
             if (err) { next(err); return; }
@@ -141,7 +137,7 @@ export let getLogout = (req: Request, res: Response) => {
     if (sess) {
         sess.destroy((err) => { if (err) { console.log(err); } });
     }
-    res.redirect(RouteStore.login);
+    res.redirect("/login");
 };
 
 export let getForgot = function (req: Request, res: Response) {
@@ -168,7 +164,7 @@ export let postForgot = function (req: Request, res: Response, next: NextFunctio
             User.findOne({ email }, function (err, user: DashUserModel) {
                 if (!user) {
                     // NO ACCOUNT WITH SUBMITTED EMAIL
-                    res.redirect(RouteStore.forgot);
+                    res.redirect("/forgotPassword");
                     return;
                 }
                 user.passwordResetToken = token;
@@ -192,7 +188,7 @@ export let postForgot = function (req: Request, res: Response, next: NextFunctio
                 subject: 'Dash Password Reset',
                 text: 'You are receiving this because you (or someone else) have requested the reset of the password for your account.\n\n' +
                     'Please click on the following link, or paste this into your browser to complete the process:\n\n' +
-                    'http://' + req.headers.host + '/reset/' + token + '\n\n' +
+                    'http://' + req.headers.host + '/resetPassword/' + token + '\n\n' +
                     'If you did not request this, please ignore this email and your password will remain unchanged.\n'
             } as MailOptions;
             smtpTransport.sendMail(mailOptions, function (err: Error | null) {
@@ -202,14 +198,14 @@ export let postForgot = function (req: Request, res: Response, next: NextFunctio
         }
     ], function (err) {
         if (err) return next(err);
-        res.redirect(RouteStore.forgot);
+        res.redirect("/forgotPassword");
     });
 };
 
 export let getReset = function (req: Request, res: Response) {
     User.findOne({ passwordResetToken: req.params.token, passwordResetExpires: { $gt: Date.now() } }, function (err, user: DashUserModel) {
         if (!user || err) {
-            return res.redirect(RouteStore.forgot);
+            return res.redirect("/forgotPassword");
         }
         res.render("reset.pug", {
             title: "Reset Password",
@@ -239,7 +235,7 @@ export let postReset = function (req: Request, res: Response) {
 
                 user.save(function (err) {
                     if (err) {
-                        res.redirect(RouteStore.login);
+                        res.redirect("/login");
                         return;
                     }
                     req.logIn(user, function (err) {
@@ -271,6 +267,6 @@ export let postReset = function (req: Request, res: Response) {
             });
         }
     ], function (err) {
-        res.redirect(RouteStore.login);
+        res.redirect("/login");
     });
 };
\ No newline at end of file
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 73cac879e..5a8815983 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -11,7 +11,6 @@ import { List } from "../../../new_fields/List";
 import { listSpec } from "../../../new_fields/Schema";
 import { Cast, StrCast, PromiseValue } from "../../../new_fields/Types";
 import { Utils } from "../../../Utils";
-import { RouteStore } from "../../RouteStore";
 import { ScriptField } from "../../../new_fields/ScriptField";
 import { ButtonBox } from "../../../client/views/nodes/ButtonBox";
 import { UndoManager } from "../../../client/util/UndoManager";
@@ -198,8 +197,8 @@ export class CurrentUserUtils {
         return doc;
     }
 
-    public static loadCurrentUser() {
-        return rp.get(Utils.prepend(RouteStore.getCurrUser)).then(response => {
+    public static async loadCurrentUser() {
+        return rp.get(Utils.prepend("/getCurrentUser")).then(response => {
             if (response) {
                 const result: { id: string, email: string } = JSON.parse(response);
                 return result;
@@ -212,7 +211,7 @@ export class CurrentUserUtils {
     public static async loadUserDocument({ id, email }: { id: string, email: string }) {
         this.curr_id = id;
         Doc.CurrentUserEmail = email;
-        await rp.get(Utils.prepend(RouteStore.getUserDocumentId)).then(id => {
+        await rp.get(Utils.prepend("/getUserDocumentId")).then(id => {
             if (id && id !== "guest") {
                 return DocServer.GetRefField(id).then(async field => {
                     if (field instanceof Doc) {
diff --git a/src/server/index.ts b/src/server/index.ts
index aec301a74..8fc402cc9 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -3,7 +3,6 @@ import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils";
 import * as mobileDetect from 'mobile-detect';
 import * as path from 'path';
 import { Database } from './database';
-import { RouteStore } from './RouteStore';
 const serverPort = 4321;
 import { GooglePhotosUploadUtils } from './apis/google/GooglePhotosUploadUtils';
 import { Opt } from '../new_fields/Doc';
@@ -23,7 +22,7 @@ import DeleteManager from "./ApiManagers/DeleteManager";
 import PDFManager from "./ApiManagers/PDFManager";
 import UploadManager from "./ApiManagers/UploadManager";
 
-export const publicDirectory = __dirname + RouteStore.public;
+export const publicDirectory = __dirname + "/public";
 export const filesDirectory = publicDirectory + "/files/";
 export enum Partitions {
     pdf_text,
@@ -73,13 +72,12 @@ function routeSetter(router: RouteManager) {
     WebSocket.initialize(serverPort, router.isRelease);
 
     /**
-     * Anyone attempting to navigate to localhost at this port will
-     * first have to log in.
+     * Accessing root index redirects to home
      */
     router.addSupervisedRoute({
         method: Method.GET,
-        subscription: RouteStore.root,
-        onValidation: ({ res }) => res.redirect(RouteStore.home)
+        subscription: "/",
+        onValidation: ({ res }) => res.redirect("/home")
     });
 
     const serve: OnUnauthenticated = ({ req, res }) => {
@@ -90,7 +88,7 @@ function routeSetter(router: RouteManager) {
 
     router.addSupervisedRoute({
         method: Method.GET,
-        subscription: [RouteStore.home, new RouteSubscriber("/doc").add("docId")],
+        subscription: ["/home", new RouteSubscriber("doc").add("docId")],
         onValidation: serve,
         onUnauthenticated: ({ req, ...remaining }) => {
             const { originalUrl: target } = req;
@@ -110,9 +108,9 @@ function routeSetter(router: RouteManager) {
 
     router.addSupervisedRoute({
         method: Method.GET,
-        subscription: new RouteSubscriber(RouteStore.cognitiveServices).add('requestedservice'),
+        subscription: new RouteSubscriber("cognitiveServices").add('requestedService'),
         onValidation: ({ req, res }) => {
-            let service = req.params.requestedservice;
+            let service = req.params.requestedService;
             res.send(ServicesApiKeyMap.get(service));
         }
     });
@@ -125,7 +123,7 @@ function routeSetter(router: RouteManager) {
 
     router.addSupervisedRoute({
         method: Method.POST,
-        subscription: new RouteSubscriber(RouteStore.googleDocs).add("sector", "action"),
+        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;
@@ -143,7 +141,7 @@ function routeSetter(router: RouteManager) {
 
     router.addSupervisedRoute({
         method: Method.GET,
-        subscription: RouteStore.readGoogleAccessToken,
+        subscription: "/readGoogleAccessToken",
         onValidation: async ({ user, res }) => {
             const userId = user.id;
             const token = await GoogleApiServerUtils.retrieveAccessToken(userId);
@@ -156,7 +154,7 @@ function routeSetter(router: RouteManager) {
 
     router.addSupervisedRoute({
         method: Method.POST,
-        subscription: RouteStore.writeGoogleAccessToken,
+        subscription: "/writeGoogleAccessToken",
         onValidation: async ({ user, req, res }) => {
             res.send(await GoogleApiServerUtils.processNewUser(user.id, req.body.authenticationCode));
         }
@@ -173,7 +171,7 @@ function routeSetter(router: RouteManager) {
 
     router.addSupervisedRoute({
         method: Method.POST,
-        subscription: RouteStore.googlePhotosMediaUpload,
+        subscription: "/googlePhotosMediaUpload",
         onValidation: async ({ user, req, res }) => {
             const { media } = req.body;
 
@@ -228,7 +226,7 @@ function routeSetter(router: RouteManager) {
     const UploadError = (count: number) => `Unable to upload ${count} images to Dash's server`;
     router.addSupervisedRoute({
         method: Method.POST,
-        subscription: RouteStore.googlePhotosMediaDownload,
+        subscription: "/googlePhotosMediaDownload",
         onValidation: async ({ req, res }) => {
             const contents: { mediaItems: MediaItem[] } = req.body;
             let failed = 0;
-- 
cgit v1.2.3-70-g09d2


From df5584ccd40bd83f1362b32db67969e7ffbf2e3f Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Wed, 27 Nov 2019 04:03:30 -0500
Subject: improved file partitioning in server and generified upload method

---
 package.json                                       |   2 +
 src/client/documents/Documents.ts                  |   1 -
 src/client/util/ClientDiagnostics.ts               |  11 +-
 .../util/Import & Export/DirectoryImportBox.tsx    |  19 +-
 src/client/views/Main.tsx                          |   2 +-
 src/client/views/collections/CollectionSubView.tsx |  22 +-
 src/client/views/pdf/PDFViewer.tsx                 |   3 +-
 src/server/ActionUtilities.ts                      |  17 +-
 src/server/ApiManagers/DownloadManager.ts          |   4 +-
 src/server/ApiManagers/GooglePhotosManager.ts      |   4 +-
 src/server/ApiManagers/PDFManager.ts               |  34 +-
 src/server/ApiManagers/SearchManager.ts            |   4 +-
 src/server/ApiManagers/UploadManager.ts            |  47 ++-
 src/server/DashUploadUtils.ts                      | 388 +++++++++++----------
 src/server/SharedMediaTypes.ts                     |   5 +-
 src/server/database.ts                             |   4 +-
 src/server/index.ts                                |   4 +-
 17 files changed, 314 insertions(+), 257 deletions(-)

(limited to 'src/client/util/Import & Export/DirectoryImportBox.tsx')

diff --git a/package.json b/package.json
index 393df8574..3725d76eb 100644
--- a/package.json
+++ b/package.json
@@ -105,6 +105,7 @@
         "@types/react-table": "^6.7.22",
         "@types/request": "^2.48.1",
         "@types/request-promise": "^4.1.42",
+        "@types/rimraf": "^2.0.3",
         "@types/sharp": "^0.22.2",
         "@types/shelljs": "^0.8.5",
         "@types/socket.io": "^2.1.2",
@@ -211,6 +212,7 @@
         "readline": "^1.3.0",
         "request": "^2.88.0",
         "request-promise": "^4.2.4",
+        "rimraf": "^3.0.0",
         "serializr": "^1.5.1",
         "sharp": "^0.22.1",
         "shelljs": "^0.8.3",
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index dea057b93..a2f4d23c7 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -377,7 +377,6 @@ export namespace Docs {
                 let extension = path.extname(target);
                 target = `${target.substring(0, target.length - extension.length)}_o${extension}`;
             }
-            // if (target !== "http://www.cs.brown.edu/") {
             requestImageSize(target)
                 .then((size: any) => {
                     let aspect = size.height / size.width;
diff --git a/src/client/util/ClientDiagnostics.ts b/src/client/util/ClientDiagnostics.ts
index 24f196252..7eef935fd 100644
--- a/src/client/util/ClientDiagnostics.ts
+++ b/src/client/util/ClientDiagnostics.ts
@@ -1,9 +1,6 @@
-import { observable, runInAction } from "mobx";
-import { MainView } from "../views/MainView";
-
 export namespace ClientDiagnostics {
 
-    export function start() {
+    export async function start() {
 
         let serverPolls = 0;
         const serverHandle = setInterval(async () => {
@@ -17,14 +14,16 @@ export namespace ClientDiagnostics {
 
 
         let executed = false;
-        const solrHandle = setInterval(async () => {
+        const handle = async () => {
             const response = await fetch("/solrHeartbeat");
             if (!(await response.json()).running) {
                 !executed && alert("Looks like SOLR is not running on your machine.");
                 executed = true;
                 clearInterval(solrHandle);
             }
-        }, 1000 * 15);
+        };
+        await handle();
+        const solrHandle = setInterval(handle, 1000 * 15);
 
     }
 
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index f0880f193..16ae50685 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -22,18 +22,10 @@ import "./DirectoryImportBox.scss";
 import { Networking } from "../../Network";
 import { BatchedArray } from "array-batcher";
 import * as path from 'path';
-import { DashUploadUtils } from "../../../server/DashUploadUtils";
-import { SharedMediaTypes } from "../../../server/SharedMediaTypes";
+import { AcceptibleMedia } from "../../../server/SharedMediaTypes";
 
 const unsupported = ["text/html", "text/plain"];
 
-interface ImageUploadResponse {
-    name: string;
-    path: string;
-    type: string;
-    exif: any;
-}
-
 @observer
 export default class DirectoryImportBox extends React.Component<FieldViewProps> {
     private selector = React.createRef<HTMLInputElement>();
@@ -98,7 +90,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
             let file = files.item(i);
             if (file && !unsupported.includes(file.type)) {
                 const ext = path.extname(file.name).toLowerCase();
-                if (SharedMediaTypes.imageFormats.includes(ext)) {
+                if (AcceptibleMedia.imageFormats.includes(ext)) {
                     validated.push(file);
                 }
             }
@@ -114,7 +106,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
 
         runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`);
 
-        const uploads = await BatchedArray.from(validated, { batchSize: 15 }).batchedMapAsync<ImageUploadResponse>(async (batch, collector) => {
+        const uploads = await BatchedArray.from(validated, { batchSize: 15 }).batchedMapAsync<any>(async (batch, collector) => {
             const formData = new FormData();
 
             batch.forEach(file => {
@@ -127,16 +119,17 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
             runInAction(() => this.completed += batch.length);
         });
 
+        const size = "_o";
         await Promise.all(uploads.map(async upload => {
             const type = upload.type;
-            const path = Utils.prepend(upload.path);
+            const path = Utils.prepend(upload.clientAccessPath);
             const options = {
                 nativeWidth: 300,
                 width: 300,
                 title: upload.name
             };
             const document = await Docs.Get.DocumentFromType(type, path, options);
-            const { data, error } = upload.exif;
+            const { data, error } = upload.exifData;
             if (document) {
                 Doc.GetProto(document).exif = error || Docs.Get.DocumentHierarchyFromJson(data);
                 docs.push(document);
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index dec4a24e4..9e699978f 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -10,7 +10,7 @@ import { ClientDiagnostics } from "../util/ClientDiagnostics";
 AssignAllExtensions();
 
 (async () => {
-    ClientDiagnostics.start();
+    await ClientDiagnostics.start();
     const info = await CurrentUserUtils.loadCurrentUser();
     DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email);
     await Docs.Prototypes.initialize();
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 1c3ff37ee..a1bd1527e 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -22,6 +22,7 @@ import React = require("react");
 var path = require('path');
 import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
 import { ImageUtils } from "../../util/Import & Export/ImageUtils";
+import { Networking } from "../../Network";
 
 export interface CollectionViewProps extends FieldViewProps {
     addDocument: (document: Doc) => boolean;
@@ -271,28 +272,25 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                     let file = item.getAsFile();
                     let formData = new FormData();
 
-                    if (file) {
-                        formData.append('file', file);
+                    if (!file || !file.type) {
+                        continue;
                     }
-                    let dropFileName = file ? file.name : "-empty-";
 
-                    let prom = fetch(Utils.prepend("/upload"), {
-                        method: 'POST',
-                        body: formData
-                    }).then(async (res: Response) => {
-                        (await res.json()).map(action((file: any) => {
+                    formData.append('file', file);
+                    let dropFileName = file ? file.name : "-empty-";
+                    promises.push(Networking.PostFormDataToServer("/upload", formData).then(results => {
+                        results.map(action((file: any) => {
                             let full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName };
-                            let pathname = Utils.prepend(file.path);
+                            let pathname = Utils.prepend(file.clientAccessPath);
                             Docs.Get.DocumentFromType(type, pathname, full).then(doc => {
                                 doc && (Doc.GetProto(doc).fileUpload = path.basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""));
                                 doc && this.props.addDocument(doc);
                             });
                         }));
-                    });
-                    promises.push(prom);
+                    }));
                 }
             }
-            if (text) {
+            if (text && !text.includes("https://")) {
                 this.props.addDocument(Docs.Create.TextDocument({ ...options, documentText: "@@@" + text, width: 400, height: 315 }));
                 return;
             }
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index b737ce221..c075a4f99 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -125,7 +125,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
         !this.props.Document.lockedTransform && (this.props.Document.lockedTransform = true);
         // change the address to be the file address of the PNG version of each page
         // file address of the pdf
-        this._coverPath = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${this.props.url.substring("files/".length, this.props.url.length - ".pdf".length)}-${(this.Document.curPage || 1)}.PNG`)));
+        const path = Utils.prepend(`/thumbnail${this.props.url.substring("files/pdfs/".length, this.props.url.length - ".pdf".length)}-${(this.Document.curPage || 1)}.png`);
+        this._coverPath = JSON.parse(await rp.get(path));
         runInAction(() => this._showWaiting = this._showCover = true);
         this.props.startupLive && this.setupPdfJsViewer();
         this._searchReactionDisposer = reaction(() => this.Document.search_string, searchString => {
diff --git a/src/server/ActionUtilities.ts b/src/server/ActionUtilities.ts
index 7f493dd70..c9fc86fea 100644
--- a/src/server/ActionUtilities.ts
+++ b/src/server/ActionUtilities.ts
@@ -2,6 +2,7 @@ import * as fs from 'fs';
 import { ExecOptions } from 'shelljs';
 import { exec } from 'child_process';
 import * as path from 'path';
+import * as rimraf from "rimraf";
 
 export const command_line = (command: string, fromDirectory?: string) => {
     return new Promise<string>((resolve, reject) => {
@@ -68,4 +69,18 @@ export function msToTime(duration: number) {
     let secondsS = (seconds < 10) ? "0" + seconds : seconds;
 
     return hoursS + ":" + minutesS + ":" + secondsS + "." + milliseconds;
-}
\ No newline at end of file
+}
+
+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 async function Prune(rootDirectory: string): Promise<boolean> {
+    const error = await new Promise<Error>(resolve => rimraf(rootDirectory, resolve));
+    return error === null;
+}
+
+export const Destroy = (mediaPath: string) => new Promise<boolean>(resolve => fs.unlink(mediaPath, error => resolve(error === null)));
\ No newline at end of file
diff --git a/src/server/ApiManagers/DownloadManager.ts b/src/server/ApiManagers/DownloadManager.ts
index fc6ba0d22..5bad46eda 100644
--- a/src/server/ApiManagers/DownloadManager.ts
+++ b/src/server/ApiManagers/DownloadManager.ts
@@ -5,7 +5,7 @@ import * as Archiver from 'archiver';
 import * as express from 'express';
 import { Database } from "../database";
 import * as path from "path";
-import { DashUploadUtils } from "../DashUploadUtils";
+import { DashUploadUtils, SizeSuffix } from "../DashUploadUtils";
 import { publicDirectory } from "..";
 
 export type Hierarchy = { [id: string]: string | Hierarchy };
@@ -254,7 +254,7 @@ async function writeHierarchyRecursive(file: Archiver.Archiver, hierarchy: Hiera
                 // and dropped in the browser and thus hosted remotely) so we upload it
                 // to our server and point the zip file to it, so it can bundle up the bytes
                 const information = await DashUploadUtils.UploadImage(result);
-                path = information.mediaPaths[0];
+                path = information.serverAccessPaths[SizeSuffix.Original];
             }
             // write the file specified by the path to the directory in the
             // zip file given by the prefix.
diff --git a/src/server/ApiManagers/GooglePhotosManager.ts b/src/server/ApiManagers/GooglePhotosManager.ts
index c7af69375..5a709688b 100644
--- a/src/server/ApiManagers/GooglePhotosManager.ts
+++ b/src/server/ApiManagers/GooglePhotosManager.ts
@@ -86,10 +86,10 @@ export default class GooglePhotosManager extends ApiManager {
                 const contents: { mediaItems: MediaItem[] } = req.body;
                 let failed = 0;
                 if (contents) {
-                    const completed: Opt<DashUploadUtils.UploadInformation>[] = [];
+                    const completed: Opt<DashUploadUtils.ImageUploadInformation>[] = [];
                     for (let item of contents.mediaItems) {
                         const { contentSize, ...attributes } = await DashUploadUtils.InspectImage(item.baseUrl);
-                        const found: Opt<DashUploadUtils.UploadInformation> = await Database.Auxiliary.QueryUploadHistory(contentSize!);
+                        const found: Opt<DashUploadUtils.ImageUploadInformation> = 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) {
diff --git a/src/server/ApiManagers/PDFManager.ts b/src/server/ApiManagers/PDFManager.ts
index 632b4965a..4bd750aaf 100644
--- a/src/server/ApiManagers/PDFManager.ts
+++ b/src/server/ApiManagers/PDFManager.ts
@@ -2,12 +2,12 @@ import ApiManager, { Registration } from "./ApiManager";
 import { Method } from "../RouteManager";
 import RouteSubscriber from "../RouteSubscriber";
 import { exists, createReadStream, createWriteStream } from "fs";
-import { filesDirectory } from "..";
 import * as Pdfjs from 'pdfjs-dist';
 import { createCanvas } from "canvas";
 const probe = require("probe-image-size");
 import * as express from "express";
 import * as path from "path";
+import { Directory, serverPathToFile, clientPathToFile } from "./UploadManager";
 
 export default class PDFManager extends ApiManager {
 
@@ -21,21 +21,27 @@ export default class PDFManager extends ApiManager {
                 let noExt = filename.substring(0, filename.length - ".png".length);
                 let pagenumber = parseInt(noExt.split('-')[1]);
                 return new Promise<void>(resolve => {
-                    exists(filesDirectory + filename, (exists: boolean) => {
-                        console.log(`${filesDirectory + filename} ${exists ? "exists" : "does not exist"}`);
+                    const path = serverPathToFile(Directory.pdf_thumbnails, filename);
+                    exists(path, (exists: boolean) => {
+                        console.log(`${path} ${exists ? "exists" : "does not exist"}`);
                         if (exists) {
-                            let input = createReadStream(filesDirectory + filename);
-                            probe(input, (err: any, result: any) => {
+                            let input = createReadStream(path);
+                            probe(input, (err: any, { width, height }: any) => {
                                 if (err) {
                                     console.log(err);
                                     console.log(`error on ${filename}`);
                                     return;
                                 }
-                                res.send({ path: "/files/" + filename, width: result.width, height: result.height });
+                                res.send({
+                                    path: clientPathToFile(Directory.pdf_thumbnails, filename),
+                                    width,
+                                    height
+                                });
                             });
                         }
                         else {
-                            LoadPage(filesDirectory + filename.substring(0, filename.length - noExt.split('-')[1].length - ".PNG".length - 1) + ".pdf", pagenumber, res);
+                            const name = filename.substring(0, filename.length - noExt.split('-')[1].length - ".PNG".length - 1) + ".pdf";
+                            LoadPage(serverPathToFile(Directory.pdfs, name), pagenumber, res);
                         }
                         resolve();
                     });
@@ -55,8 +61,8 @@ export default class PDFManager extends ApiManager {
                         let canvasAndContext = factory.create(viewport.width, viewport.height);
                         let renderContext = {
                             canvasContext: canvasAndContext.context,
-                            viewport: viewport,
-                            canvasFactory: factory
+                            canvasFactory: factory,
+                            viewport
                         };
                         console.log("read " + pageNumber);
 
@@ -64,13 +70,17 @@ export default class PDFManager extends ApiManager {
                             .then(() => {
                                 console.log("saving " + pageNumber);
                                 let stream = canvasAndContext.canvas.createPNGStream();
-                                let pngFile = `${file.substring(0, file.length - ".pdf".length)}-${pageNumber}.PNG`;
+                                let filenames = path.basename(file).split(".");
+                                const pngFile = serverPathToFile(Directory.pdf_thumbnails, `${filenames[0]}-${pageNumber}.png`);
                                 let out = createWriteStream(pngFile);
                                 stream.pipe(out);
                                 out.on("finish", () => {
                                     console.log(`Success! Saved to ${pngFile}`);
-                                    let name = path.basename(pngFile);
-                                    res.send({ path: "/files/" + name, width: viewport.width, height: viewport.height });
+                                    res.send({
+                                        path: pngFile,
+                                        width: viewport.width,
+                                        height: viewport.height
+                                    });
                                 });
                             }, (reason: string) => {
                                 console.error(reason + ` ${pageNumber}`);
diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts
index 1c801715a..d3f8995b0 100644
--- a/src/server/ApiManagers/SearchManager.ts
+++ b/src/server/ApiManagers/SearchManager.ts
@@ -3,7 +3,7 @@ import { Method } from "../RouteManager";
 import { Search } from "../Search";
 var findInFiles = require('find-in-files');
 import * as path from 'path';
-import { filesDirectory } from "..";
+import { pathToDirectory, Directory } from "./UploadManager";
 
 export default class SearchManager extends ApiManager {
 
@@ -18,7 +18,7 @@ export default class SearchManager extends ApiManager {
                     res.send([]);
                     return;
                 }
-                let results = await findInFiles.find({ 'term': q, 'flags': 'ig' }, filesDirectory + "text", ".txt$");
+                let results = await findInFiles.find({ 'term': q, 'flags': 'ig' }, pathToDirectory(Directory.text), ".txt$");
                 let resObj: { ids: string[], numFound: number, lines: string[] } = { ids: [], numFound: 0, lines: [] };
                 for (var result in results) {
                     resObj.ids.push(path.basename(result, ".txt").replace(/upload_/, ""));
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 2a9faacd8..2f76871a6 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -7,14 +7,33 @@ import { extname, basename, dirname } from 'path';
 import { createReadStream, createWriteStream, unlink, readFileSync } from "fs";
 import { publicDirectory, filesDirectory } from "..";
 import { Database } from "../database";
-import { DashUploadUtils } from "../DashUploadUtils";
-import { Opt } from "../../new_fields/Doc";
-import { ParsedPDF } from "../PdfTypes";
-const pdf = require('pdf-parse');
+import { DashUploadUtils, SizeSuffix } from "../DashUploadUtils";
 import * as sharp from 'sharp';
-import { SharedMediaTypes } from "../SharedMediaTypes";
+import { AcceptibleMedia } from "../SharedMediaTypes";
+import { normalize } from "path";
 const imageDataUri = require('image-data-uri');
 
+export enum Directory {
+    parsed_files = "parsed_files",
+    images = "images",
+    videos = "videos",
+    pdfs = "pdfs",
+    text = "text",
+    pdf_thumbnails = "pdf_thumbnails"
+}
+
+export function serverPathToFile(directory: Directory, filename: string) {
+    return normalize(`${filesDirectory}/${directory}/${filename}`);
+}
+
+export function pathToDirectory(directory: Directory) {
+    return normalize(`${filesDirectory}/${directory}`);
+}
+
+export function clientPathToFile(directory: Directory, filename: string) {
+    return `/files/${directory}/${filename}`;
+}
+
 export default class UploadManager extends ApiManager {
 
     protected initialize(register: Registration): void {
@@ -129,13 +148,14 @@ export default class UploadManager extends ApiManager {
             subscription: "/upload",
             onValidation: async ({ req, res }) => {
                 let form = new formidable.IncomingForm();
-                form.uploadDir = filesDirectory;
+                form.uploadDir = pathToDirectory(Directory.parsed_files);
                 form.keepExtensions = true;
                 return new Promise<void>(resolve => {
                     form.parse(req, async (_err, _fields, files) => {
                         let results: any[] = [];
                         for (const key in files) {
-                            results.push(DashUploadUtils.upload(files[key]));
+                            const result = await DashUploadUtils.upload(files[key]);
+                            result && results.push(result);
                         }
                         _success(res, results);
                         resolve();
@@ -150,8 +170,8 @@ export default class UploadManager extends ApiManager {
             onValidation: async ({ req, res }) => {
                 const { source } = req.body;
                 if (typeof source === "string") {
-                    const uploadInformation = await DashUploadUtils.UploadImage(source);
-                    return res.send(await DashUploadUtils.InspectImage(uploadInformation.mediaPaths[0]));
+                    const { serverAccessPaths } = await DashUploadUtils.UploadImage(source);
+                    return res.send(await DashUploadUtils.InspectImage(serverAccessPaths[SizeSuffix.Original]));
                 }
                 res.send({});
             }
@@ -167,9 +187,9 @@ export default class UploadManager extends ApiManager {
                     res.status(401).send("incorrect parameters specified");
                     return;
                 }
-                return imageDataUri.outputFile(uri, filesDirectory + filename).then((savedName: string) => {
+                return imageDataUri.outputFile(uri, serverPathToFile(Directory.images, filename)).then((savedName: string) => {
                     const ext = extname(savedName).toLowerCase();
-                    const { pngs, jpgs } = SharedMediaTypes;
+                    const { pngs, jpgs } = AcceptibleMedia;
                     let resizers = [
                         { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
                         { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" },
@@ -189,10 +209,11 @@ export default class UploadManager extends ApiManager {
                     }
                     if (isImage) {
                         resizers.forEach(resizer => {
-                            createReadStream(savedName).pipe(resizer.resizer).pipe(createWriteStream(filesDirectory + filename + resizer.suffix + ext));
+                            const path = serverPathToFile(Directory.images, filename + resizer.suffix + ext);
+                            createReadStream(savedName).pipe(resizer.resizer).pipe(createWriteStream(path));
                         });
                     }
-                    res.send("/files/" + filename + ext);
+                    res.send(clientPathToFile(Directory.images, filename + ext));
                 });
             }
         });
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 839aada4b..0a670ec01 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -5,18 +5,32 @@ import * as sharp from 'sharp';
 import request = require('request-promise');
 import { ExifData, ExifImage } from 'exif';
 import { Opt } from '../new_fields/Doc';
-import { SharedMediaTypes } from './SharedMediaTypes';
+import { AcceptibleMedia } from './SharedMediaTypes';
 import { filesDirectory } from '.';
 import { File } from 'formidable';
-import { extname, basename } from "path";
+import { basename } from "path";
+import { ConsoleColors, createIfNotExists } from './ActionUtilities';
+import { ParsedPDF } from "../server/PdfTypes";
+const parse = require('pdf-parse');
+import { Directory, serverPathToFile, clientPathToFile } from './ApiManagers/UploadManager';
 
-const uploadDirectory = path.join(__dirname, './public/files/');
+export enum SizeSuffix {
+    Small = "_s",
+    Medium = "_m",
+    Large = "_l",
+    Original = "_o"
+}
 
 export namespace DashUploadUtils {
 
+    function InjectSize(filename: string, size: SizeSuffix) {
+        const extension = path.extname(filename).toLowerCase();
+        return filename.substring(0, filename.length - extension.length) + size + extension;
+    }
+
     export interface Size {
         width: number;
-        suffix: string;
+        suffix: SizeSuffix;
     }
 
     export interface ImageFileResponse {
@@ -27,215 +41,221 @@ export namespace DashUploadUtils {
     }
 
     export const Sizes: { [size: string]: Size } = {
-        SMALL: { width: 100, suffix: "_s" },
-        MEDIUM: { width: 400, suffix: "_m" },
-        LARGE: { width: 900, suffix: "_l" },
+        SMALL: { width: 100, suffix: SizeSuffix.Small },
+        MEDIUM: { width: 400, suffix: SizeSuffix.Medium },
+        LARGE: { width: 900, suffix: SizeSuffix.Large },
     };
 
     export function validateExtension(url: string) {
-        return SharedMediaTypes.imageFormats.includes(path.extname(url).toLowerCase());
+        return AcceptibleMedia.imageFormats.includes(path.extname(url).toLowerCase());
     }
 
     const size = "content-length";
     const type = "content-type";
 
-    export interface UploadInformation {
-        mediaPaths: string[];
-        fileNames: { [key: string]: string };
+    export interface ImageUploadInformation {
+        clientAccessPath: string;
+        serverAccessPaths: { [key: string]: string };
         exifData: EnrichedExifData;
         contentSize?: number;
         contentType?: string;
     }
 
-    export function upload(file: File): any {
+    export async function upload(file: File): Promise<any> {
         const { type, path, name } = file;
-        const filename = basename(path);
-        const extension = extname(path).toLowerCase();
-        if (extension === ".pdf") {
-
-        } else if  {
-            let partition: Opt<string>;
-            if(imageFormats.includes(extension)) {
-            partition = DashUploadUtils.Partitions.images;
-        } else if (videoFormats.includes(extension)) {
-            partition = DashUploadUtils.Partitions.videos;
-        }
-        let uploadInformation: Opt<DashUploadUtils.UploadInformation>;
-        if (partition) {
-            uploadInformation = await DashUploadUtils.UploadImage(`${filesDirectory}/${partition}/${filename}`, filename);
-        } else {
-            console.log(`Unable to accommodate, and ignored, the following file upload: ${filename}`);
+        const { imageFormats, videoFormats, applicationFormats } = AcceptibleMedia;
+        const types = type.split("/");
+
+        const category = types[0];
+        const format = `.${types[1]}`;
+
+        switch (category) {
+            case "image":
+                if (imageFormats.includes(format)) {
+                    const { clientAccessPath } = await UploadImage(path, basename(path), format);
+                    return { clientAccessPath, name, type };
+                }
+            case "video":
+                if (videoFormats.includes(format)) {
+                    return MoveParsedFile(path, Directory.videos);
+                }
+            case "application":
+                if (applicationFormats.includes(format)) {
+                    return UploadPdf(path);
+                }
         }
+        console.log(ConsoleColors.Red, `Ignoring unsupported file ${name} with upload type (${type}).`);
+        return { clientAccessPath: undefined };
     }
-    const exif = uploadInformation ? uploadInformation.exifData : undefined;
-    results.push({ name, type, path: `/files/${filename}`, exif });
 
-}
+    async function UploadPdf(absolutePath: string) {
+        let dataBuffer = fs.readFileSync(absolutePath);
+        const result: ParsedPDF = await parse(dataBuffer);
+        const parsedName = basename(absolutePath);
+        await new Promise<void>((resolve, reject) => {
+            const textFilename = `${parsedName.substring(0, parsedName.length - 4)}.txt`;
+            const writeStream = fs.createWriteStream(serverPathToFile(Directory.text, textFilename));
+            writeStream.write(result.text, error => error ? reject(error) : resolve());
+        });
+        return MoveParsedFile(absolutePath, Directory.pdfs);
+    }
 
-const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${sanitizeExtension(url)}`;
-const sanitize = (filename: string) => filename.replace(/\s+/g, "_");
-const sanitizeExtension = (source: string) => {
-    let extension = path.extname(source);
-    extension = extension.toLowerCase();
-    extension = extension.split("?")[0];
-    return extension;
-};
-
-/**
- * Uploads an image specified by the @param source to Dash's /public/files/
- * directory, and returns information generated during that upload 
- * 
- * @param {string} source is either the absolute path of an already uploaded image or
- * the url of a remote image
- * @param {string} filename dictates what to call the image. If not specified,
- * the name {@param prefix}_upload_{GUID}
- * @param {string} prefix is a string prepended to the generated image name in the
- * event that @param filename is not specified
- * 
- * @returns {UploadInformation} This method returns
- * 1) the paths to the uploaded images (plural due to resizing)
- * 2) the file name of each of the resized images
- * 3) the size of the image, in bytes (4432130)
- * 4) the content type of the image, i.e. image/(jpeg | png | ...)
- */
-export const UploadImage = async (source: string, filename?: string, prefix: string = ""): Promise<UploadInformation> => {
-    const metadata = await InspectImage(source);
-    return UploadInspectedImage(metadata, filename, prefix);
-};
-
-export interface InspectionResults {
-    isLocal: boolean;
-    stream: any;
-    normalizedUrl: string;
-    exifData: EnrichedExifData;
-    contentSize?: number;
-    contentType?: string;
-}
+    const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${sanitizeExtension(url)}`;
+    const sanitizeExtension = (source: string) => {
+        let extension = path.extname(source);
+        extension = extension.toLowerCase();
+        extension = extension.split("?")[0];
+        return extension;
+    };
 
-export interface EnrichedExifData {
-    data: ExifData;
-    error?: string;
-}
+    /**
+     * Uploads an image specified by the @param source to Dash's /public/files/
+     * directory, and returns information generated during that upload 
+     * 
+     * @param {string} source is either the absolute path of an already uploaded image or
+     * the url of a remote image
+     * @param {string} filename dictates what to call the image. If not specified,
+     * the name {@param prefix}_upload_{GUID}
+     * @param {string} prefix is a string prepended to the generated image name in the
+     * event that @param filename is not specified
+     * 
+     * @returns {ImageUploadInformation} This method returns
+     * 1) the paths to the uploaded images (plural due to resizing)
+     * 2) the file name of each of the resized images
+     * 3) the size of the image, in bytes (4432130)
+     * 4) the content type of the image, i.e. image/(jpeg | png | ...)
+     */
+    export const UploadImage = async (source: string, filename?: string, format?: string, prefix: string = ""): Promise<ImageUploadInformation> => {
+        const metadata = await InspectImage(source);
+        return UploadInspectedImage(metadata, filename, format, prefix);
+    };
 
-export enum Partitions {
-    pdf_text = "pdf_text",
-    images = "images",
-    videos = "videos"
-}
+    export interface InspectionResults {
+        isLocal: boolean;
+        stream: any;
+        normalizedUrl: string;
+        exifData: EnrichedExifData;
+        contentSize?: number;
+        contentType?: string;
+    }
 
-export async function buildFilePartitions() {
-    const pending = Object.keys(Partitions).map(sub => createIfNotExists(filesDirectory + sub));
-    return Promise.all(pending);
-}
+    export interface EnrichedExifData {
+        data: ExifData;
+        error?: string;
+    }
 
-/**
- * Based on the url's classification as local or remote, gleans
- * as much information as possible about the specified image
- * 
- * @param source is the path or url to the image in question
- */
-export const InspectImage = async (source: string): Promise<InspectionResults> => {
-    const { isLocal, stream, normalized: normalizedUrl } = classify(source);
-    const exifData = await parseExifData(source);
-    const results = {
-        exifData,
-        isLocal,
-        stream,
-        normalizedUrl
-    };
-    // stop here if local, since request.head() can't handle local paths, only urls on the web
-    if (isLocal) {
-        return results;
+    export async function buildFileDirectories() {
+        const pending = Object.keys(Directory).map(sub => createIfNotExists(`${filesDirectory}/${sub}`));
+        return Promise.all(pending);
     }
-    const metadata = (await new Promise<any>((resolve, reject) => {
-        request.head(source, async (error, res) => {
-            if (error) {
-                return reject(error);
-            }
-            resolve(res);
-        });
-    })).headers;
-    return {
-        contentSize: parseInt(metadata[size]),
-        contentType: metadata[type],
-        ...results
-    };
-};
-
-export const UploadInspectedImage = async (metadata: InspectionResults, filename?: string, prefix = ""): Promise<UploadInformation> => {
-    const { isLocal, stream, normalizedUrl, contentSize, contentType, exifData } = metadata;
-    const resolved = filename ? sanitize(filename) : generate(prefix, normalizedUrl);
-    const extension = sanitizeExtension(normalizedUrl || resolved);
-    let information: UploadInformation = {
-        mediaPaths: [],
-        fileNames: { clean: resolved },
-        exifData,
-        contentSize,
-        contentType,
-    };
-    const { pngs, jpgs } = SharedMediaTypes;
-    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());
-        }
-        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(normalizedUrl).pipe(resizer.resizer).pipe(fs.createWriteStream(mediaPath))
-                    .on('close', resolve)
-                    .on('error', reject);
-            });
+
+    /**
+     * Based on the url's classification as local or remote, gleans
+     * as much information as possible about the specified image
+     * 
+     * @param source is the path or url to the image in question
+     */
+    export const InspectImage = async (source: string): Promise<InspectionResults> => {
+        const { isLocal, stream, normalized: normalizedUrl } = classify(source);
+        const exifData = await parseExifData(source);
+        const results = {
+            exifData,
+            isLocal,
+            stream,
+            normalizedUrl
+        };
+        // stop here if local, since request.head() can't handle local paths, only urls on the web
+        if (isLocal) {
+            return results;
         }
-        if (!isLocal) {
-            await new Promise<void>(resolve => {
-                stream(normalizedUrl).pipe(fs.createWriteStream(uploadDirectory + resolved)).on('close', resolve);
+        const metadata = (await new Promise<any>((resolve, reject) => {
+            request.head(source, async (error, res) => {
+                if (error) {
+                    return reject(error);
+                }
+                resolve(res);
             });
-        }
-        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
+        })).headers;
+        return {
+            contentSize: parseInt(metadata[size]),
+            contentType: metadata[type],
+            ...results
+        };
     };
-};
-
-const parseExifData = async (source: string): Promise<EnrichedExifData> => {
-    return new Promise<EnrichedExifData>(resolve => {
-        new ExifImage(source, (error, data) => {
-            let reason: Opt<string> = undefined;
-            if (error) {
-                reason = (error as any).code;
+
+    export async function MoveParsedFile(absolutePath: string, destination: Directory): Promise<{ clientAccessPath: Opt<string> }> {
+        return new Promise<{ clientAccessPath: Opt<string> }>(resolve => {
+            const filename = basename(absolutePath);
+            const destinationPath = serverPathToFile(destination, filename);
+            fs.rename(absolutePath, destinationPath, error => {
+                resolve({ clientAccessPath: error ? undefined : clientPathToFile(destination, filename) });
+            });
+        });
+    }
+
+    export const UploadInspectedImage = async (metadata: InspectionResults, filename?: string, format?: string, prefix = ""): Promise<ImageUploadInformation> => {
+        const { isLocal, stream, normalizedUrl, contentSize, contentType, exifData } = metadata;
+        const resolved = filename || generate(prefix, normalizedUrl);
+        const extension = format || sanitizeExtension(normalizedUrl || resolved);
+        let information: ImageUploadInformation = {
+            clientAccessPath: clientPathToFile(Directory.images, resolved),
+            serverAccessPaths: {},
+            exifData,
+            contentSize,
+            contentType,
+        };
+        const { pngs, jpgs } = AcceptibleMedia;
+        return new Promise<ImageUploadInformation>(async (resolve, reject) => {
+            const resizers = [
+                { resizer: sharp().rotate(), suffix: SizeSuffix.Original },
+                ...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());
+            }
+            for (let { resizer, suffix } of resizers) {
+                let mediaPath: string;
+                await new Promise<void>(resolve => {
+                    const filename = InjectSize(resolved, suffix);
+                    information.serverAccessPaths[suffix] = serverPathToFile(Directory.images, filename);
+                    stream(normalizedUrl).pipe(resizer).pipe(fs.createWriteStream(serverPathToFile(Directory.images, filename)))
+                        .on('close', resolve)
+                        .on('error', reject);
+                });
             }
-            resolve({ data, error: reason });
+            if (isLocal) {
+                await new Promise<boolean>(resolve => {
+                    fs.unlink(normalizedUrl, error => resolve(error === null));
+                });
+            }
+            resolve(information);
         });
-    });
-};
+    };
 
-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)));
-};
+    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 Destroy = (mediaPath: string) => new Promise<boolean>(resolve => fs.unlink(mediaPath, error => resolve(error === null)));
+    const parseExifData = async (source: string): Promise<EnrichedExifData> => {
+        return new Promise<EnrichedExifData>(resolve => {
+            new ExifImage(source, (error, data) => {
+                let reason: Opt<string> = undefined;
+                if (error) {
+                    reason = (error as any).code;
+                }
+                resolve({ data, error: reason });
+            });
+        });
+    };
 
 }
\ No newline at end of file
diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts
index 3d3234125..8d0f441f0 100644
--- a/src/server/SharedMediaTypes.ts
+++ b/src/server/SharedMediaTypes.ts
@@ -1,9 +1,8 @@
-export namespace SharedMediaTypes {
-
+export namespace AcceptibleMedia {
     export const gifs = [".gif"];
     export const pngs = [".png"];
     export const jpgs = [".jpg", ".jpeg"];
     export const imageFormats = [...pngs, ...jpgs, ...gifs];
     export const videoFormats = [".mov", ".mp4"];
-
+    export const applicationFormats = [".pdf"];
 }
\ No newline at end of file
diff --git a/src/server/database.ts b/src/server/database.ts
index b81fc03a4..db81245c1 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -293,7 +293,7 @@ export namespace Database {
         };
 
         export const QueryUploadHistory = async (contentSize: number) => {
-            return SanitizedSingletonQuery<DashUploadUtils.UploadInformation>({ contentSize }, AuxiliaryCollections.GooglePhotosUploadHistory);
+            return SanitizedSingletonQuery<DashUploadUtils.ImageUploadInformation>({ contentSize }, AuxiliaryCollections.GooglePhotosUploadHistory);
         };
 
         export namespace GoogleAuthenticationToken {
@@ -322,7 +322,7 @@ export namespace Database {
 
         }
 
-        export const LogUpload = async (information: DashUploadUtils.UploadInformation) => {
+        export const LogUpload = async (information: DashUploadUtils.ImageUploadInformation) => {
             const bundle = {
                 _id: Utils.GenerateDeterministicGuid(String(information.contentSize!)),
                 ...information
diff --git a/src/server/index.ts b/src/server/index.ts
index d02a6005e..d77923710 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -24,7 +24,7 @@ import GooglePhotosManager from "./ApiManagers/GooglePhotosManager";
 import DiagnosticManager from "./ApiManagers/DiagnosticManager";
 
 export const publicDirectory = path.resolve(__dirname, "public");
-export const filesDirectory = path.resolve(publicDirectory, "files") + "/";
+export const filesDirectory = path.resolve(publicDirectory, "files");
 
 /**
  * These are the functions run before the server starts
@@ -34,7 +34,7 @@ export const filesDirectory = path.resolve(publicDirectory, "files") + "/";
 async function preliminaryFunctions() {
     await GoogleCredentialsLoader.loadCredentials();
     GoogleApiServerUtils.processProjectCredentials();
-    await DashUploadUtils.buildFilePartitions();
+    await DashUploadUtils.buildFileDirectories();
     await log_execution({
         startMessage: "attempting to initialize mongodb connection",
         endMessage: "connection outcome determined",
-- 
cgit v1.2.3-70-g09d2


From db6d3f77b1b429c1942019b79c44e378eb8b1ee4 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Wed, 27 Nov 2019 04:25:55 -0500
Subject: fixed google photos upload by appending size suffix

---
 src/client/util/Import & Export/DirectoryImportBox.tsx | 10 ++++------
 src/server/ApiManagers/GooglePhotosManager.ts          |  4 ++--
 src/server/DashUploadUtils.ts                          | 14 +++++++-------
 3 files changed, 13 insertions(+), 15 deletions(-)

(limited to 'src/client/util/Import & Export/DirectoryImportBox.tsx')

diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 16ae50685..b5e806a97 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -119,17 +119,15 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
             runInAction(() => this.completed += batch.length);
         });
 
-        const size = "_o";
-        await Promise.all(uploads.map(async upload => {
-            const type = upload.type;
-            const path = Utils.prepend(upload.clientAccessPath);
+        await Promise.all(uploads.map(async ({ name, type, clientAccessPath, exifData }) => {
+            const path = Utils.prepend(clientAccessPath);
             const options = {
                 nativeWidth: 300,
                 width: 300,
-                title: upload.name
+                title: name
             };
             const document = await Docs.Get.DocumentFromType(type, path, options);
-            const { data, error } = upload.exifData;
+            const { data, error } = exifData;
             if (document) {
                 Doc.GetProto(document).exif = error || Docs.Get.DocumentHierarchyFromJson(data);
                 docs.push(document);
diff --git a/src/server/ApiManagers/GooglePhotosManager.ts b/src/server/ApiManagers/GooglePhotosManager.ts
index 5a709688b..4a0c0b936 100644
--- a/src/server/ApiManagers/GooglePhotosManager.ts
+++ b/src/server/ApiManagers/GooglePhotosManager.ts
@@ -5,7 +5,7 @@ import { GoogleApiServerUtils } from "../apis/google/GoogleApiServerUtils";
 import { BatchedArray, TimeUnit } from "array-batcher";
 import { GooglePhotosUploadUtils } from "../apis/google/GooglePhotosUploadUtils";
 import { Opt } from "../../new_fields/Doc";
-import { DashUploadUtils } from "../DashUploadUtils";
+import { DashUploadUtils, InjectSize, SizeSuffix } from "../DashUploadUtils";
 import { Database } from "../database";
 
 const authenticationError = "Unable to authenticate Google credentials before uploading to Google Photos!";
@@ -55,7 +55,7 @@ export default class GooglePhotosManager extends ApiManager {
                         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);
+                            const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(token, InjectSize(url, SizeSuffix.Original)).catch(fail);
                             if (!uploadToken) {
                                 fail(`${path.extname(url)} is not an accepted extension`);
                             } else {
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 0a670ec01..81cd2d602 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -21,12 +21,12 @@ export enum SizeSuffix {
     Original = "_o"
 }
 
-export namespace DashUploadUtils {
+export function InjectSize(filename: string, size: SizeSuffix) {
+    const extension = path.extname(filename).toLowerCase();
+    return filename.substring(0, filename.length - extension.length) + size + extension;
+}
 
-    function InjectSize(filename: string, size: SizeSuffix) {
-        const extension = path.extname(filename).toLowerCase();
-        return filename.substring(0, filename.length - extension.length) + size + extension;
-    }
+export namespace DashUploadUtils {
 
     export interface Size {
         width: number;
@@ -72,8 +72,8 @@ export namespace DashUploadUtils {
         switch (category) {
             case "image":
                 if (imageFormats.includes(format)) {
-                    const { clientAccessPath } = await UploadImage(path, basename(path), format);
-                    return { clientAccessPath, name, type };
+                    const results = await UploadImage(path, basename(path), format);
+                    return { ...results, name, type };
                 }
             case "video":
                 if (videoFormats.includes(format)) {
-- 
cgit v1.2.3-70-g09d2


From 77ee66de66a411f79bbbc036d379d09be38d172f Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Mon, 2 Dec 2019 12:12:58 -0500
Subject: further cleanup

---
 src/Utils.ts                                       |   2 -
 src/client/util/ClientDiagnostics.ts               |  18 +--
 .../util/Import & Export/DirectoryImportBox.tsx    |   3 +-
 src/client/views/MainView.tsx                      |   2 +-
 src/client/views/collections/CollectionSubView.tsx |   6 +-
 src/server/ApiManagers/DeleteManager.ts            |   1 -
 src/server/ApiManagers/GeneralGoogleManager.ts     |  15 +--
 src/server/ApiManagers/PDFManager.ts               | 134 ++++++++++-----------
 src/server/ApiManagers/UploadManager.ts            |  43 ++++---
 src/server/DashUploadUtils.ts                      |  18 ++-
 src/server/RouteManager.ts                         |  14 +++
 .../authentication/models/current_user_utils.ts    |   3 +-
 src/server/credentials/test.json                   |  14 ---
 13 files changed, 125 insertions(+), 148 deletions(-)
 delete mode 100644 src/server/credentials/test.json

(limited to 'src/client/util/Import & Export/DirectoryImportBox.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index b60e9e023..2543743a4 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -2,8 +2,6 @@ import v4 = require('uuid/v4');
 import v5 = require("uuid/v5");
 import { Socket } from 'socket.io';
 import { Message } from './server/Message';
-import { EventEmitter } from 'events';
-import { ConsoleColors } from './server/ActionUtilities';
 
 export namespace Utils {
 
diff --git a/src/client/util/ClientDiagnostics.ts b/src/client/util/ClientDiagnostics.ts
index 7eef935fd..0a213aa1c 100644
--- a/src/client/util/ClientDiagnostics.ts
+++ b/src/client/util/ClientDiagnostics.ts
@@ -12,18 +12,22 @@ export namespace ClientDiagnostics {
             serverPolls--;
         }, 1000 * 15);
 
-
         let executed = false;
-        const handle = async () => {
+        let solrHandle: NodeJS.Timeout | undefined;
+        const handler = async () => {
             const response = await fetch("/solrHeartbeat");
             if (!(await response.json()).running) {
-                !executed && alert("Looks like SOLR is not running on your machine.");
-                executed = true;
-                clearInterval(solrHandle);
+                if (!executed) {
+                    alert("Looks like SOLR is not running on your machine.");
+                    executed = true;
+                    solrHandle && clearInterval(solrHandle);
+                }
             }
         };
-        await handle();
-        const solrHandle = setInterval(handle, 1000 * 15);
+        await handler();
+        if (!executed) {
+            solrHandle = setInterval(handler, 1000 * 15);
+        }
 
     }
 
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index b5e806a97..104d9e099 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -106,7 +106,8 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
 
         runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`);
 
-        const uploads = await BatchedArray.from(validated, { batchSize: 15 }).batchedMapAsync<any>(async (batch, collector) => {
+        const batched = BatchedArray.from(validated, { batchSize: 15 });
+        const uploads = await batched.batchedMapAsync<any>(async (batch, collector) => {
             const formData = new FormData();
 
             batch.forEach(file => {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 5231075a1..85dfd8be2 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -4,7 +4,7 @@ import {
     faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone, faCompressArrowsAlt
 } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, configure, observable, reaction, runInAction, autorun } from 'mobx';
+import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import "normalize.css";
 import * as React from 'react';
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index a1bd1527e..a1ae77fef 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -6,7 +6,7 @@ import { Id } from "../../../new_fields/FieldSymbols";
 import { List } from "../../../new_fields/List";
 import { listSpec } from "../../../new_fields/Schema";
 import { ScriptField } from "../../../new_fields/ScriptField";
-import { Cast, StrCast } from "../../../new_fields/Types";
+import { Cast } from "../../../new_fields/Types";
 import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
 import { Utils } from "../../../Utils";
 import { DocServer } from "../../DocServer";
@@ -279,9 +279,9 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                     formData.append('file', file);
                     let dropFileName = file ? file.name : "-empty-";
                     promises.push(Networking.PostFormDataToServer("/upload", formData).then(results => {
-                        results.map(action((file: any) => {
+                        results.map(action(({ clientAccessPath }: any) => {
                             let full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName };
-                            let pathname = Utils.prepend(file.clientAccessPath);
+                            let pathname = Utils.prepend(clientAccessPath);
                             Docs.Get.DocumentFromType(type, pathname, full).then(doc => {
                                 doc && (Doc.GetProto(doc).fileUpload = path.basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""));
                                 doc && this.props.addDocument(doc);
diff --git a/src/server/ApiManagers/DeleteManager.ts b/src/server/ApiManagers/DeleteManager.ts
index f58a28ce5..71818c673 100644
--- a/src/server/ApiManagers/DeleteManager.ts
+++ b/src/server/ApiManagers/DeleteManager.ts
@@ -56,7 +56,6 @@ export default class DeleteManager extends ApiManager {
             }
         });
 
-
     }
 
 }
diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts
index 171912185..629684e0c 100644
--- a/src/server/ApiManagers/GeneralGoogleManager.ts
+++ b/src/server/ApiManagers/GeneralGoogleManager.ts
@@ -20,8 +20,7 @@ export default class GeneralGoogleManager extends ApiManager {
             method: Method.GET,
             subscription: "/readGoogleAccessToken",
             onValidation: async ({ user, res }) => {
-                const userId = user.id;
-                const token = await GoogleApiServerUtils.retrieveAccessToken(userId);
+                const token = await GoogleApiServerUtils.retrieveAccessToken(user.id);
                 if (!token) {
                     return res.send(GoogleApiServerUtils.generateAuthenticationUrl());
                 }
@@ -37,18 +36,6 @@ export default class GeneralGoogleManager extends ApiManager {
             }
         });
 
-        register({
-            method: Method.GET,
-            subscription: "/deleteWithGoogleCredentials",
-            onValidation: async ({ res, isRelease }) => {
-                if (isRelease) {
-                    return _permission_denied(res, deletionPermissionError);
-                }
-                await Database.Auxiliary.GoogleAuthenticationToken.DeleteAll();
-                res.redirect("/delete");
-            }
-        });
-
         register({
             method: Method.POST,
             subscription: new RouteSubscriber("/googleDocs").add("sector", "action"),
diff --git a/src/server/ApiManagers/PDFManager.ts b/src/server/ApiManagers/PDFManager.ts
index 4bd750aaf..151b48dd9 100644
--- a/src/server/ApiManagers/PDFManager.ts
+++ b/src/server/ApiManagers/PDFManager.ts
@@ -4,10 +4,11 @@ import RouteSubscriber from "../RouteSubscriber";
 import { exists, createReadStream, createWriteStream } from "fs";
 import * as Pdfjs from 'pdfjs-dist';
 import { createCanvas } from "canvas";
-const probe = require("probe-image-size");
+const imageSize = require("probe-image-size");
 import * as express from "express";
 import * as path from "path";
 import { Directory, serverPathToFile, clientPathToFile } from "./UploadManager";
+import { ConsoleColors } from "../ActionUtilities";
 
 export default class PDFManager extends ApiManager {
 
@@ -16,84 +17,77 @@ export default class PDFManager extends ApiManager {
         register({
             method: Method.GET,
             subscription: new RouteSubscriber("thumbnail").add("filename"),
-            onValidation: ({ req, res }) => {
-                let filename = req.params.filename;
-                let noExt = filename.substring(0, filename.length - ".png".length);
-                let pagenumber = parseInt(noExt.split('-')[1]);
-                return new Promise<void>(resolve => {
-                    const path = serverPathToFile(Directory.pdf_thumbnails, filename);
-                    exists(path, (exists: boolean) => {
-                        console.log(`${path} ${exists ? "exists" : "does not exist"}`);
-                        if (exists) {
-                            let input = createReadStream(path);
-                            probe(input, (err: any, { width, height }: any) => {
-                                if (err) {
-                                    console.log(err);
-                                    console.log(`error on ${filename}`);
-                                    return;
-                                }
-                                res.send({
-                                    path: clientPathToFile(Directory.pdf_thumbnails, filename),
-                                    width,
-                                    height
-                                });
-                            });
-                        }
-                        else {
-                            const name = filename.substring(0, filename.length - noExt.split('-')[1].length - ".PNG".length - 1) + ".pdf";
-                            LoadPage(serverPathToFile(Directory.pdfs, name), pagenumber, res);
-                        }
-                        resolve();
-                    });
-                });
-            }
+            onValidation: ({ req, res }) => getOrCreateThumbnail(req.params.filename, res)
         });
 
-        function LoadPage(file: string, pageNumber: number, res: express.Response) {
-            console.log(file);
-            Pdfjs.getDocument(file).promise
-                .then((pdf: Pdfjs.PDFDocumentProxy) => {
-                    let factory = new NodeCanvasFactory();
-                    console.log(pageNumber);
-                    pdf.getPage(pageNumber).then((page: Pdfjs.PDFPageProxy) => {
-                        console.log("reading " + page);
-                        let viewport = page.getViewport(1 as any);
-                        let canvasAndContext = factory.create(viewport.width, viewport.height);
-                        let renderContext = {
-                            canvasContext: canvasAndContext.context,
-                            canvasFactory: factory,
-                            viewport
-                        };
-                        console.log("read " + pageNumber);
+    }
 
-                        page.render(renderContext).promise
-                            .then(() => {
-                                console.log("saving " + pageNumber);
-                                let stream = canvasAndContext.canvas.createPNGStream();
-                                let filenames = path.basename(file).split(".");
-                                const pngFile = serverPathToFile(Directory.pdf_thumbnails, `${filenames[0]}-${pageNumber}.png`);
-                                let out = createWriteStream(pngFile);
-                                stream.pipe(out);
-                                out.on("finish", () => {
-                                    console.log(`Success! Saved to ${pngFile}`);
-                                    res.send({
-                                        path: pngFile,
-                                        width: viewport.width,
-                                        height: viewport.height
-                                    });
-                                });
-                            }, (reason: string) => {
-                                console.error(reason + ` ${pageNumber}`);
-                            });
+}
+
+function getOrCreateThumbnail(thumbnailName: string, res: express.Response) {
+    const noExtension = thumbnailName.substring(0, thumbnailName.length - ".png".length);
+    const pageString = noExtension.split('-')[1];
+    const pageNumber = parseInt(pageString);
+    return new Promise<void>(resolve => {
+        const path = serverPathToFile(Directory.pdf_thumbnails, thumbnailName);
+        exists(path, (exists: boolean) => {
+            if (exists) {
+                let existingThumbnail = createReadStream(path);
+                imageSize(existingThumbnail, (err: any, { width, height }: any) => {
+                    if (err) {
+                        console.log(ConsoleColors.Red, `In PDF thumbnail response, unable to determine dimensions of ${thumbnailName}:`);
+                        console.log(err);
+                        return;
+                    }
+                    res.send({
+                        path: clientPathToFile(Directory.pdf_thumbnails, thumbnailName),
+                        width,
+                        height
                     });
                 });
-        }
-
-    }
+            } else {
+                const offset = thumbnailName.length - pageString.length - 5;
+                const name = thumbnailName.substring(0, offset) + ".pdf";
+                const path = serverPathToFile(Directory.pdfs, name);
+                CreateThumbnail(path, pageNumber, res);
+            }
+            resolve();
+        });
+    });
+}
 
+async function CreateThumbnail(file: string, pageNumber: number, res: express.Response) {
+    const documentProxy = await Pdfjs.getDocument(file).promise;
+    const factory = new NodeCanvasFactory();
+    const page = await documentProxy.getPage(pageNumber);
+    const viewport = page.getViewport(1 as any);
+    const { canvas, context } = factory.create(viewport.width, viewport.height);
+    const renderContext = {
+        canvasContext: context,
+        canvasFactory: factory,
+        viewport
+    };
+    await page.render(renderContext).promise;
+    const pngStream = canvas.createPNGStream();
+    const filenames = path.basename(file).split(".");
+    const pngFile = serverPathToFile(Directory.pdf_thumbnails, `${filenames[0]}-${pageNumber}.png`);
+    const out = createWriteStream(pngFile);
+    pngStream.pipe(out);
+    out.on("finish", () => {
+        res.send({
+            path: pngFile,
+            width: viewport.width,
+            height: viewport.height
+        });
+    });
+    out.on("error", error => {
+        console.log(ConsoleColors.Red, `In PDF thumbnail creation, encountered the following error when piping ${pngFile}:`);
+        console.log(error);
+    });
 }
 
 class NodeCanvasFactory {
+
     create = (width: number, height: number) => {
         var canvas = createCanvas(width, height);
         var context = canvas.getContext('2d');
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 2f76871a6..80ae0ad61 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -38,6 +38,27 @@ export default class UploadManager extends ApiManager {
 
     protected initialize(register: Registration): void {
 
+        register({
+            method: Method.POST,
+            subscription: "/upload",
+            onValidation: async ({ req, res }) => {
+                let form = new formidable.IncomingForm();
+                form.uploadDir = pathToDirectory(Directory.parsed_files);
+                form.keepExtensions = true;
+                return new Promise<void>(resolve => {
+                    form.parse(req, async (_err, _fields, files) => {
+                        let results: any[] = [];
+                        for (const key in files) {
+                            const result = await DashUploadUtils.upload(files[key]);
+                            result && results.push(result);
+                        }
+                        _success(res, results);
+                        resolve();
+                    });
+                });
+            }
+        });
+
         register({
             method: Method.POST,
             subscription: "/uploadDoc",
@@ -142,28 +163,6 @@ export default class UploadManager extends ApiManager {
             }
         });
 
-
-        register({
-            method: Method.POST,
-            subscription: "/upload",
-            onValidation: async ({ req, res }) => {
-                let form = new formidable.IncomingForm();
-                form.uploadDir = pathToDirectory(Directory.parsed_files);
-                form.keepExtensions = true;
-                return new Promise<void>(resolve => {
-                    form.parse(req, async (_err, _fields, files) => {
-                        let results: any[] = [];
-                        for (const key in files) {
-                            const result = await DashUploadUtils.upload(files[key]);
-                            result && results.push(result);
-                        }
-                        _success(res, results);
-                        resolve();
-                    });
-                });
-            }
-        });
-
         register({
             method: Method.POST,
             subscription: "/inspectImage",
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index c831eb072..9ccc72e35 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -85,7 +85,8 @@ export namespace DashUploadUtils {
                     return UploadPdf(path);
                 }
         }
-        console.log(ConsoleColors.Red, `Ignoring unsupported file ${name} with upload type (${type}).`);
+
+        console.log(ConsoleColors.Red, `Ignoring unsupported file (${name}) with upload type (${type}).`);
         return { clientAccessPath: undefined };
     }
 
@@ -169,17 +170,12 @@ export namespace DashUploadUtils {
         if (isLocal) {
             return results;
         }
-        const metadata = (await new Promise<any>((resolve, reject) => {
-            request.head(source, async (error, res) => {
-                if (error) {
-                    return reject(error);
-                }
-                resolve(res);
-            });
-        })).headers;
+        const { headers } = (await new Promise<any>((resolve, reject) => {
+            request.head(source, (error, res) => error ? reject(error) : resolve(res));
+        }));
         return {
-            contentSize: parseInt(metadata[size]),
-            contentType: metadata[type],
+            contentSize: parseInt(headers[size]),
+            contentType: headers[type],
             ...results
         };
     };
diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts
index 3a20d5af5..7c49485f1 100644
--- a/src/server/RouteManager.ts
+++ b/src/server/RouteManager.ts
@@ -26,6 +26,8 @@ export interface RouteInitializer {
     onError?: OnError;
 }
 
+const registered = new Map<string, Set<Method>>();
+
 export default class RouteManager {
     private server: express.Express;
     private _isRelease: boolean;
@@ -89,6 +91,18 @@ export default class RouteManager {
             } else {
                 route = subscriber.build;
             }
+            const existing = registered.get(route);
+            if (existing) {
+                if (existing.has(method)) {
+                    console.log(ConsoleColors.Red, `\nDuplicate registration error: already registered ${route} with Method[${method}]`);
+                    console.log('Please remove duplicate registrations before continuing...\n');
+                    process.exit(0);
+                }
+            } else {
+                const specific = new Set<Method>();
+                specific.add(method);
+                registered.set(route, specific);
+            }
             switch (method) {
                 case Method.GET:
                     this.server.get(route, supervised);
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 052aa54a6..ac4462f78 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -1,4 +1,4 @@
-import { action, computed, observable, reaction, runInAction } from "mobx";
+import { action, computed, observable, reaction } from "mobx";
 import * as rp from 'request-promise';
 import { DocServer } from "../../../client/DocServer";
 import { Docs } from "../../../client/documents/Documents";
@@ -11,7 +11,6 @@ import { listSpec } from "../../../new_fields/Schema";
 import { ScriptField, ComputedField } from "../../../new_fields/ScriptField";
 import { Cast, PromiseValue } from "../../../new_fields/Types";
 import { Utils } from "../../../Utils";
-import { ButtonBox } from "../../../client/views/nodes/ButtonBox";
 import { nullAudio } from "../../../new_fields/URLField";
 import { DragManager } from "../../../client/util/DragManager";
 import { InkingControl } from "../../../client/views/InkingControl";
diff --git a/src/server/credentials/test.json b/src/server/credentials/test.json
deleted file mode 100644
index 0a032cc2d..000000000
--- a/src/server/credentials/test.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-    "installed": {
-        "client_id": "343179513178-ud6tvmh275r2fq93u9eesrnc66t6akh9.apps.googleusercontent.com",
-        "project_id": "quickstart-1565056383187",
-        "auth_uri": "https://accounts.google.com/o/oauth2/auth",
-        "token_uri": "https://oauth2.googleapis.com/token",
-        "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
-        "client_secret": "w8KIFSc0MQpmUYHed4qEzn8b",
-        "redirect_uris": [
-            "urn:ietf:wg:oauth:2.0:oob",
-            "http://localhost"
-        ]
-    }
-}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2

