aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSam Wilkins <samwilkins333@gmail.com>2019-11-09 16:18:23 -0500
committerSam Wilkins <samwilkins333@gmail.com>2019-11-09 16:18:23 -0500
commit0b72a27ead9d1e933ae349b8a3e9e9b8702664d1 (patch)
tree326f75721a1894a04e40e3bfc9f77d7433bb1c25 /src
parentc53d599f8ecffe173d8df06777721658f065674a (diff)
factored out all but google resources into managers
Diffstat (limited to 'src')
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx10
-rw-r--r--src/server/ApiManagers/DeleteManager.ts65
-rw-r--r--src/server/ApiManagers/ExportManager.ts157
-rw-r--r--src/server/ApiManagers/PDFManager.ts107
-rw-r--r--src/server/ApiManagers/SearchManager.ts4
-rw-r--r--src/server/ApiManagers/UploadManager.ts227
-rw-r--r--src/server/ApiManagers/UserManager.ts47
-rw-r--r--src/server/DashUploadUtils.ts17
-rw-r--r--src/server/SharedMediaTypes.ts9
-rw-r--r--src/server/Websocket/Websocket.ts19
-rw-r--r--src/server/apis/google/GoogleApiServerUtils.ts46
-rw-r--r--src/server/apis/google/GooglePhotosUploadUtils.ts19
-rw-r--r--src/server/credentials/CredentialsLoader.ts29
-rw-r--r--src/server/credentials/google_project_credentials.json14
-rw-r--r--src/server/credentials/test.json (renamed from src/server/credentials/google_docs_credentials.json)5
-rw-r--r--src/server/index.ts570
16 files changed, 760 insertions, 585 deletions
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,6 +1,8 @@
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 {
@@ -8,6 +10,29 @@ export default class UserManager extends ApiManager {
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",
onValidation: ({ res }) => {
let users: any = { active: {}, inactive: {} };
@@ -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
@@ -62,6 +61,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
*/
export type ApiResponse = Promise<GaxiosResponse>;
@@ -97,32 +113,6 @@ export namespace GoogleApiServerUtils {
}
/**
- * 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
* of needless duplicate clients that would arise from
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';
@@ -29,6 +28,22 @@ export namespace GooglePhotosUploadUtils {
}
/**
+ * 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
* the relative path in the caller.
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_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/google_docs_credentials.json b/src/server/credentials/test.json
index 955c5a3c1..0a032cc2d 100644
--- a/src/server/credentials/google_docs_credentials.json
+++ b/src/server/credentials/test.json
@@ -6,6 +6,9 @@
"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"]
+ "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 });
+})();