From cee55b4a1b13909d55708eee6c364206ae7c0d4f Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 22 Oct 2019 19:39:19 -0400 Subject: api managers and web socket initial refactoring --- src/server/Websocket/Websocket.ts | 213 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 src/server/Websocket/Websocket.ts (limited to 'src/server/Websocket/Websocket.ts') diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts new file mode 100644 index 000000000..2461dd8d5 --- /dev/null +++ b/src/server/Websocket/Websocket.ts @@ -0,0 +1,213 @@ +import { Utils } from "../../Utils"; +import { MessageStore, Transferable, Types, Diff, YoutubeQueryInput, YoutubeQueryTypes } from "../Message"; +import { Client } from "../Client"; +import { Socket } from "socket.io"; +import { Database } from "../database"; +import { Search } from "../Search"; +import io from 'socket.io'; +import YoutubeApi from "../apis/youtube/youtubeApiSample"; +import { youtubeApiKey } from ".."; + +export namespace WebSocket { + + interface Map { + [key: string]: Client; + } + let clients: Map = {}; + + export const socketMap = new Map(); + export const timeMap: { [id: string]: number } = {}; + + export function initialize(serverPort: number, isRelease: boolean) { + const endpoint = io(); + endpoint.listen(serverPort); + console.log(`listening on port ${serverPort}`); + + endpoint.on("connection", function (socket: Socket) { + socket.use((_packet, next) => { + let id = socketMap.get(socket); + if (id) { + timeMap[id] = Date.now(); + } + next(); + }); + + Utils.Emit(socket, MessageStore.Foo, "handshooken"); + + Utils.AddServerHandler(socket, MessageStore.Bar, guid => barReceived(socket, guid)); + Utils.AddServerHandler(socket, MessageStore.SetField, (args) => setField(socket, args)); + Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField); + Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields); + if (isRelease) { + Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteFields); + } + + Utils.AddServerHandler(socket, MessageStore.CreateField, CreateField); + Utils.AddServerHandlerCallback(socket, MessageStore.YoutubeApiQuery, HandleYoutubeQuery); + Utils.AddServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff)); + Utils.AddServerHandler(socket, MessageStore.DeleteField, id => DeleteField(socket, id)); + Utils.AddServerHandler(socket, MessageStore.DeleteFields, ids => DeleteFields(socket, ids)); + Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField); + Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields); + }); + + } + + function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any[]) => void]) { + switch (query.type) { + case YoutubeQueryTypes.Channels: + YoutubeApi.authorizedGetChannel(youtubeApiKey); + break; + case YoutubeQueryTypes.SearchVideo: + YoutubeApi.authorizedGetVideos(youtubeApiKey, query.userInput, callback); + case YoutubeQueryTypes.VideoDetails: + YoutubeApi.authorizedGetVideoDetails(youtubeApiKey, query.videoIds, callback); + } + } + + export async function deleteFields() { + await Database.Instance.deleteAll(); + await Search.Instance.clear(); + await Database.Instance.deleteAll('newDocuments'); + } + + export async function deleteAll() { + await Database.Instance.deleteAll(); + await Database.Instance.deleteAll('newDocuments'); + await Database.Instance.deleteAll('sessions'); + await Database.Instance.deleteAll('users'); + await Search.Instance.clear(); + } + + function barReceived(socket: SocketIO.Socket, guid: string) { + clients[guid] = new Client(guid.toString()); + console.log(`User ${guid} has connected`); + socketMap.set(socket, guid); + } + + function getField([id, callback]: [string, (result?: Transferable) => void]) { + Database.Instance.getDocument(id, (result?: Transferable) => + callback(result ? result : undefined)); + } + + function getFields([ids, callback]: [string[], (result: Transferable[]) => void]) { + Database.Instance.getDocuments(ids, callback); + } + + function setField(socket: Socket, newValue: Transferable) { + Database.Instance.update(newValue.id, newValue, () => + socket.broadcast.emit(MessageStore.SetField.Message, newValue)); + if (newValue.type === Types.Text) { + Search.Instance.updateDocument({ id: newValue.id, data: (newValue as any).data }); + console.log("set field"); + console.log("checking in"); + } + } + + function GetRefField([id, callback]: [string, (result?: Transferable) => void]) { + Database.Instance.getDocument(id, callback, "newDocuments"); + } + + function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => void]) { + Database.Instance.getDocuments(ids, callback, "newDocuments"); + } + + const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { + "number": "_n", + "string": "_t", + "boolean": "_b", + "image": ["_t", "url"], + "video": ["_t", "url"], + "pdf": ["_t", "url"], + "audio": ["_t", "url"], + "web": ["_t", "url"], + "date": ["_d", value => new Date(value.date).toISOString()], + "proxy": ["_i", "fieldId"], + "list": ["_l", list => { + const results = []; + for (const value of list.fields) { + const term = ToSearchTerm(value); + if (term) { + results.push(term.value); + } + } + return results.length ? results : null; + }] + }; + + function ToSearchTerm(val: any): { suffix: string, value: any } | undefined { + if (val === null || val === undefined) { + return; + } + const type = val.__type || typeof val; + let suffix = suffixMap[type]; + if (!suffix) { + return; + } + + if (Array.isArray(suffix)) { + const accessor = suffix[1]; + if (typeof accessor === "function") { + val = accessor(val); + } else { + val = val[accessor]; + } + suffix = suffix[0]; + } + + return { suffix, value: val }; + } + + function getSuffix(value: string | [string, any]): string { + return typeof value === "string" ? value : value[0]; + } + + function UpdateField(socket: Socket, diff: Diff) { + Database.Instance.update(diff.id, diff.diff, + () => socket.broadcast.emit(MessageStore.UpdateField.Message, diff), false, "newDocuments"); + const docfield = diff.diff.$set; + if (!docfield) { + return; + } + const update: any = { id: diff.id }; + let dynfield = false; + for (let key in docfield) { + if (!key.startsWith("fields.")) continue; + dynfield = true; + let val = docfield[key]; + key = key.substring(7); + Object.values(suffixMap).forEach(suf => update[key + getSuffix(suf)] = { set: null }); + let term = ToSearchTerm(val); + if (term !== undefined) { + let { suffix, value } = term; + update[key + suffix] = { set: value }; + } + } + if (dynfield) { + Search.Instance.updateDocument(update); + } + } + + function DeleteField(socket: Socket, id: string) { + Database.Instance.delete({ _id: id }, "newDocuments").then(() => { + socket.broadcast.emit(MessageStore.DeleteField.Message, id); + }); + + Search.Instance.deleteDocuments([id]); + } + + function DeleteFields(socket: Socket, ids: string[]) { + Database.Instance.delete({ _id: { $in: ids } }, "newDocuments").then(() => { + socket.broadcast.emit(MessageStore.DeleteFields.Message, ids); + }); + + Search.Instance.deleteDocuments(ids); + + } + + function CreateField(newValue: any) { + Database.Instance.insert(newValue, "newDocuments"); + } + +} + -- cgit v1.2.3-70-g09d2 From e6bd33867cc7f7185575666255369f55cacb9856 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sat, 26 Oct 2019 18:28:38 -0400 Subject: restructured route registration and added preliminary comments for exporter --- src/server/ApiManagers/ApiManager.ts | 10 ++- src/server/ApiManagers/ExportManager.ts | 133 ++++++++++++++++++++++++++++++++ src/server/ApiManagers/SearchManager.ts | 10 +-- src/server/ApiManagers/UserManager.ts | 10 ++- src/server/ApiManagers/UtilManager.ts | 12 +-- src/server/RouteManager.ts | 15 ++-- src/server/Websocket/Websocket.ts | 2 +- src/server/index.ts | 96 ++++------------------- 8 files changed, 179 insertions(+), 109 deletions(-) create mode 100644 src/server/ApiManagers/ExportManager.ts (limited to 'src/server/Websocket/Websocket.ts') diff --git a/src/server/ApiManagers/ApiManager.ts b/src/server/ApiManagers/ApiManager.ts index 264c78a17..9fd726060 100644 --- a/src/server/ApiManagers/ApiManager.ts +++ b/src/server/ApiManagers/ApiManager.ts @@ -1,7 +1,11 @@ -import RouteManager from "../RouteManager"; +import RouteManager, { RouteInitializer } from "../RouteManager"; -export default abstract class ApiManager { +export type Registration = (initializer: RouteInitializer) => void; - public abstract register(router: RouteManager): void; +export default abstract class ApiManager { + protected abstract initialize(register: Registration): void; + public register(router: RouteManager) { + this.initialize(router.addSupervisedRoute); + } } \ No newline at end of file diff --git a/src/server/ApiManagers/ExportManager.ts b/src/server/ApiManagers/ExportManager.ts new file mode 100644 index 000000000..261acbbe0 --- /dev/null +++ b/src/server/ApiManagers/ExportManager.ts @@ -0,0 +1,133 @@ +import ApiManager, { Registration } from "./ApiManager"; +import RouteManager, { 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"; +import * as path from "path"; +import { DashUploadUtils } from "../DashUploadUtils"; + +export type Hierarchy = { [id: string]: string | Hierarchy }; +export type ZipMutator = (file: Archiver.Archiver) => void | Promise; +export interface DocumentElements { + data: string | any[]; + title: string; +} + +export default class ExportManager extends ApiManager { + + protected initialize(register: Registration): void { + + register({ + method: Method.GET, + subscription: new RouteSubscriber(RouteStore.imageHierarchyExport).add('docId'), + onValidation: async ({ req, res }) => { + const id = req.params.docId; + const hierarchy: Hierarchy = {}; + await buildHierarchyRecursive(id, hierarchy); + BuildAndDispatchZip(res, zip => writeHierarchyRecursive(zip, hierarchy)); + } + }); + } + +} + +/** + * This utility function factors out the process + * of creating a zip file and sending it back to the client + * by piping it into a response. + * + * Learn more about piping and readable / writable streams here! + * https://www.freecodecamp.org/news/node-js-streams-everything-you-need-to-know-c9141306be93/ + * + * @param res the writable stream response object that will transfer the generated zip file + * @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 { + const zip = Archiver('zip'); + zip.pipe(res); + await mutator(zip); + zip.finalize(); +} + +/** + * This function starts with a single document id as a seed, + * typically that of a collection, and then descends the entire tree + * of image or collection documents that are reachable from that seed. + * @param seedId the id of the root of the subtree we're trying to capture, interesting only if it's a collection + * @param hierarchy the data structure we're going to use to record the nesting of the collections and images as we descend + */ + +/* +Below is an example of the JSON hierarchy built from two images contained inside a collection titled 'a nested collection', +following the general recursive structure shown immediately below +{ + "parent folder name":{ + "first child's fild name":"first child's url" + ... + "nth child's fild name":"nth child's url" + } +} +{ + "a nested collection (865c4734-c036-4d67-a588-c71bb43d1440)":{ + "an image of a cat (ace99ffd-8ed8-4026-a5d5-a353fff57bdd).jpg":"https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", + "1*SGJw31T5Q9Zfsk24l2yirg.gif (9321cc9b-9b3e-4cb6-b99c-b7e667340f05).gif":"https://cdn-media-1.freecodecamp.org/images/1*SGJw31T5Q9Zfsk24l2yirg.gif" + } +} +*/ + +async function buildHierarchyRecursive(seedId: string, hierarchy: Hierarchy): Promise { + const { title, data } = await getData(seedId); + const label = `${title} (${seedId})`; + // is the document a collection? + if (Array.isArray(data)) { + // recurse over all documents in the collection. + const local: Hierarchy = {}; // create a child hierarchy for this level, which will get passed in as the parent of the recursive call + hierarchy[label] = local; // store it at the index in the parent, so we'll end up with a map of maps of maps + await Promise.all(data.map(proxy => buildHierarchyRecursive(proxy.fieldId, local))); + } else { + // now, data can only be a string, namely the url of the image + const filename = label + path.extname(data); // this is the file name under which the output image will be stored + hierarchy[filename] = data; + } +} + +async function getData(seedId: string): Promise { + return new Promise((resolve, reject) => { + Database.Instance.getDocument(seedId, async (result: any) => { + const { data, proto, title } = result.fields; + if (data) { + if (data.url) { + resolve({ data: data.url, title }); + } else if (data.fields) { + resolve({ data: data.fields, title }); + } else { + reject(); + } + } + if (proto) { + getData(proto.fieldId).then(resolve, reject); + } + }); + }); +} + +async function writeHierarchyRecursive(file: Archiver.Archiver, hierarchy: Hierarchy, prefix = "Dash Export"): Promise { + for (const key of Object.keys(hierarchy)) { + const result = hierarchy[key]; + if (typeof result === "string") { + let path: string; + let matches: RegExpExecArray | null; + if ((matches = /\:1050\/files\/(upload\_[\da-z]{32}.*)/g.exec(result)) !== null) { + path = `${__dirname}/public/files/${matches[1]}`; + } else { + const information = await DashUploadUtils.UploadImage(result); + path = information.mediaPaths[0]; + } + file.file(path, { name: key, prefix }); + } else { + await writeHierarchyRecursive(file, result, `${prefix}/${key}`); + } + } +} \ No newline at end of file diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts index 15b87204c..1c4b805e5 100644 --- a/src/server/ApiManagers/SearchManager.ts +++ b/src/server/ApiManagers/SearchManager.ts @@ -1,5 +1,5 @@ -import ApiManager from "./ApiManager"; -import RouteManager, { Method } from "../RouteManager"; +import ApiManager, { Registration } from "./ApiManager"; +import { Method } from "../RouteManager"; import { Search } from "../Search"; var findInFiles = require('find-in-files'); import * as path from 'path'; @@ -7,9 +7,9 @@ import { uploadDirectory } from ".."; export default class SearchManager extends ApiManager { - public register(router: RouteManager): void { + protected initialize(register: Registration): void { - router.addSupervisedRoute({ + register({ method: Method.GET, subscription: "/textsearch", onValidation: async ({ req, res }) => { @@ -29,7 +29,7 @@ export default class SearchManager extends ApiManager { } }); - router.addSupervisedRoute({ + register({ method: Method.GET, subscription: "/search", onValidation: async ({ req, res }) => { diff --git a/src/server/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts index bb8837dc6..dd1e50133 100644 --- a/src/server/ApiManagers/UserManager.ts +++ b/src/server/ApiManagers/UserManager.ts @@ -1,11 +1,12 @@ -import ApiManager from "./ApiManager"; -import RouteManager, { Method } from "../RouteManager"; +import ApiManager, { Registration } from "./ApiManager"; +import { Method } from "../RouteManager"; import { WebSocket } from "../Websocket/Websocket"; export default class UserManager extends ApiManager { - public register(router: RouteManager): void { - router.addSupervisedRoute({ + protected initialize(register: Registration): void { + + register({ method: Method.GET, subscription: "/whosOnline", onValidation: ({ res }) => { @@ -22,6 +23,7 @@ export default class UserManager extends ApiManager { res.send(users); } }); + } private msToTime(duration: number) { diff --git a/src/server/ApiManagers/UtilManager.ts b/src/server/ApiManagers/UtilManager.ts index 79b904e8a..a3f802b20 100644 --- a/src/server/ApiManagers/UtilManager.ts +++ b/src/server/ApiManagers/UtilManager.ts @@ -1,13 +1,13 @@ -import ApiManager from "./ApiManager"; -import RouteManager, { Method } from "../RouteManager"; +import ApiManager, { Registration } from "./ApiManager"; +import { Method } from "../RouteManager"; import { exec } from 'child_process'; import { command_line } from "../ActionUtilities"; export default class UtilManager extends ApiManager { - public register(router: RouteManager): void { + protected initialize(register: Registration): void { - router.addSupervisedRoute({ + register({ method: Method.GET, subscription: "/pull", onValidation: ({ res }) => { @@ -21,7 +21,7 @@ export default class UtilManager extends ApiManager { } }); - router.addSupervisedRoute({ + register({ method: Method.GET, subscription: "/buxton", onValidation: ({ res }) => { @@ -35,7 +35,7 @@ export default class UtilManager extends ApiManager { }, }); - router.addSupervisedRoute({ + register({ method: Method.GET, subscription: "/version", onValidation: ({ res }) => { diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts index b3864e89c..ef083a88a 100644 --- a/src/server/RouteManager.ts +++ b/src/server/RouteManager.ts @@ -2,7 +2,6 @@ import RouteSubscriber from "./RouteSubscriber"; import { RouteStore } from "./RouteStore"; import { DashUserModel } from "./authentication/models/user_model"; import * as express from 'express'; -import { Opt } from "../new_fields/Doc"; export enum Method { GET, @@ -41,15 +40,10 @@ export default class RouteManager { } /** - * Please invoke this function when adding a new route to Dash's server. - * It ensures that any requests leading to or containing user-sensitive information - * does not execute unless Passport authentication detects a user logged in. - * @param method whether or not the request is a GET or a POST - * @param handler the action to invoke, recieving a DashUserModel and, as expected, the Express.Request and Express.Response - * @param onRejection an optional callback invoked on return if no user is found to be logged in - * @param subscribers the forward slash prepended path names (reference and add to RouteStore.ts) that will all invoke the given @param handler + * + * @param initializer */ - addSupervisedRoute(initializer: RouteInitializer) { + addSupervisedRoute = (initializer: RouteInitializer): void => { const { method, subscription, onValidation, onUnauthenticated, onError } = initializer; const isRelease = this._isRelease; let supervised = async (req: express.Request, res: express.Response) => { @@ -72,6 +66,9 @@ export default class RouteManager { req.session!.target = target; if (onUnauthenticated) { await tryExecute(onUnauthenticated, core); + if (!res.headersSent) { + res.redirect(RouteStore.login); + } } else { res.redirect(RouteStore.login); } diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts index 2461dd8d5..cd2813d99 100644 --- a/src/server/Websocket/Websocket.ts +++ b/src/server/Websocket/Websocket.ts @@ -4,7 +4,7 @@ import { Client } from "../Client"; import { Socket } from "socket.io"; import { Database } from "../database"; import { Search } from "../Search"; -import io from 'socket.io'; +import * as io from 'socket.io'; import YoutubeApi from "../apis/youtube/youtubeApiSample"; import { youtubeApiKey } from ".."; diff --git a/src/server/index.ts b/src/server/index.ts index 93f4238bc..384800f23 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -33,12 +33,11 @@ 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 type Hierarchy = { [id: string]: string | Hierarchy }; -export type ZipMutator = (file: Archiver.Archiver) => void | Promise; - export interface NewMediaItem { description: string; simpleMediaItem: { @@ -72,9 +71,13 @@ async function PreliminaryFunctions() { } function routeSetter(router: RouteManager) { - new UtilManager().register(router); - new SearchManager().register(router); - new UserManager().register(router); + const managers: ApiManager[] = [ + new UtilManager(), + new SearchManager(), + new UserManager(), + new ExportManager() + ]; + managers.forEach(manager => manager.register(router)); WebSocket.initialize(serverPort, router.isRelease); @@ -152,77 +155,6 @@ function routeSetter(router: RouteManager) { } }); - router.addSupervisedRoute({ - method: Method.GET, - subscription: new RouteSubscriber(RouteStore.imageHierarchyExport).add('docId'), - onValidation: async ({ req, res }) => { - const id = req.params.docId; - const hierarchy: Hierarchy = {}; - await targetedVisitorRecursive(id, hierarchy); - BuildAndDispatchZip(res, async zip => { - await hierarchyTraverserRecursive(zip, hierarchy); - }); - } - }); - - const BuildAndDispatchZip = async (res: Response, mutator: ZipMutator): Promise => { - const zip = Archiver('zip'); - zip.pipe(res); - await mutator(zip); - return zip.finalize(); - }; - - const targetedVisitorRecursive = async (seedId: string, hierarchy: Hierarchy): Promise => { - const local: Hierarchy = {}; - const { title, data } = await getData(seedId); - const label = `${title} (${seedId})`; - if (Array.isArray(data)) { - hierarchy[label] = local; - await Promise.all(data.map(proxy => targetedVisitorRecursive(proxy.fieldId, local))); - } else { - hierarchy[label + path.extname(data)] = data; - } - }; - - const getData = async (seedId: string): Promise<{ data: string | any[], title: string }> => { - return new Promise<{ data: string | any[], title: string }>((resolve, reject) => { - Database.Instance.getDocument(seedId, async (result: any) => { - const { data, proto, title } = result.fields; - if (data) { - if (data.url) { - resolve({ data: data.url, title }); - } else if (data.fields) { - resolve({ data: data.fields, title }); - } else { - reject(); - } - } - if (proto) { - getData(proto.fieldId).then(resolve, reject); - } - }); - }); - }; - - const hierarchyTraverserRecursive = async (file: Archiver.Archiver, hierarchy: Hierarchy, prefix = "Dash Export"): Promise => { - for (const key of Object.keys(hierarchy)) { - const result = hierarchy[key]; - if (typeof result === "string") { - let path: string; - let matches: RegExpExecArray | null; - if ((matches = /\:1050\/files\/(upload\_[\da-z]{32}.*)/g.exec(result)) !== null) { - path = `${__dirname}/public/files/${matches[1]}`; - } else { - const information = await DashUploadUtils.UploadImage(result); - path = information.mediaPaths[0]; - } - file.file(path, { name: key, prefix }); - } else { - await hierarchyTraverserRecursive(file, result, `${prefix}/${key}`); - } - } - }; - router.addSupervisedRoute({ method: Method.GET, subscription: new RouteSubscriber("/downloadId").add("docId"), @@ -600,22 +532,24 @@ function routeSetter(router: RouteManager) { router.addSupervisedRoute({ method: Method.GET, subscription: RouteStore.delete, - onValidation: ({ res, isRelease }) => { + onValidation: async ({ res, isRelease }) => { if (isRelease) { return _permission_denied(res, deletionPermissionError); } - WebSocket.deleteFields().then(() => res.redirect(RouteStore.home)); + await WebSocket.deleteFields(); + res.redirect(RouteStore.home); } }); router.addSupervisedRoute({ method: Method.GET, subscription: RouteStore.deleteAll, - onValidation: ({ res, isRelease }) => { + onValidation: async ({ res, isRelease }) => { if (isRelease) { return _permission_denied(res, deletionPermissionError); } - WebSocket.deleteAll().then(() => res.redirect(RouteStore.home)); + await WebSocket.deleteAll(); + res.redirect(RouteStore.home); } }); -- cgit v1.2.3-70-g09d2 From 0b72a27ead9d1e933ae349b8a3e9e9b8702664d1 Mon Sep 17 00:00:00 2001 From: Sam Wilkins 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/server/Websocket/Websocket.ts') 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 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; @@ -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(); + 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 { + 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 { 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 { +/** + * 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 { return new Promise((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 { } 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 { - 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(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(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(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; + if (filename.endsWith(".pdf")) { + let dataBuffer = readFileSync(filesDirectory + filename); + const result: ParsedPDF = await pdf(dataBuffer); + await new Promise((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; + } + 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(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(); 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 { - 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(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(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(); - 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(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(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([ ["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; - } - - 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(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; - if (filename.endsWith(".pdf")) { - let dataBuffer = fs.readFileSync(uploadDirectory + filename); - const result: ParsedPDF = await pdf(dataBuffer); - await new Promise(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([ ["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(media, { batchSize: 25 }); - const newMediaItems = await batched.batchedMapPatientInterval( + const newMediaItems = await batched.batchedMapPatientInterval( { 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 9a72d65baaa293fa84592de9bc43d555b5cae319 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 19 Nov 2019 19:52:56 -0500 Subject: logging --- src/Utils.ts | 5 +++++ src/server/ActionUtilities.ts | 31 +++++++++++++++++++++++++++---- src/server/Initialization.ts | 8 +++++++- src/server/Websocket/Websocket.ts | 12 ++++++------ src/server/index.ts | 12 ++++++++++-- 5 files changed, 55 insertions(+), 13 deletions(-) (limited to 'src/server/Websocket/Websocket.ts') diff --git a/src/Utils.ts b/src/Utils.ts index 13d1be767..891a3826b 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -2,6 +2,7 @@ import v4 = require('uuid/v4'); import v5 = require("uuid/v5"); import { Socket } from 'socket.io'; import { Message } from './server/Message'; +import { EventEmitter } from 'events'; export namespace Utils { @@ -238,6 +239,10 @@ export namespace Utils { handler([arg, loggingCallback('S sending', fn, message.Name)]); }); } + + export function InjectLogger() { + + } } export function OmitKeys(obj: any, keys: string[], addKeyFunc?: (dup: any) => void): { omit: any, extract: any } { diff --git a/src/server/ActionUtilities.ts b/src/server/ActionUtilities.ts index b418772a6..5e88ea460 100644 --- a/src/server/ActionUtilities.ts +++ b/src/server/ActionUtilities.ts @@ -27,8 +27,31 @@ export const write_text_file = (relativePath: string, contents: any) => { }); }; -export async function log_execution(startMessage: string, endMessage: string, contents: () => void | Promise) { - console.log('\x1b[36m%s\x1b[0m', `${startMessage}...`); - await contents(); - console.log(endMessage); +export interface LogData { + startMessage: string; + endMessage: string; + action: () => void | Promise; +} + +export async function log_execution({ startMessage, endMessage, action }: LogData) { + const color = `\x1b[${30 + Math.ceil(Math.random() * 6)}m%s\x1b[0m`; + console.log(color, `${startMessage}...`); + await action(); + console.log(color, endMessage); +} + +export enum ConsoleColors { + Black = `\x1b[30m%s\x1b[0m`, + Red = `\x1b[31m%s\x1b[0m`, + Green = `\x1b[32m%s\x1b[0m`, + Yellow = `\x1b[33m%s\x1b[0m`, + Blue = `\x1b[34m%s\x1b[0m`, + Magenta = `\x1b[35m%s\x1b[0m`, + Cyan = `\x1b[36m%s\x1b[0m`, + White = `\x1b[37m%s\x1b[0m` +} + +export function logPort(listener: string, port: number) { + process.stdout.write(`${listener} listening on port `); + console.log(ConsoleColors.Yellow, port); } \ No newline at end of file diff --git a/src/server/Initialization.ts b/src/server/Initialization.ts index 6ac0787c5..357d7837e 100644 --- a/src/server/Initialization.ts +++ b/src/server/Initialization.ts @@ -19,6 +19,7 @@ import * as fs from 'fs'; import * as request from 'request'; import RouteSubscriber from './RouteSubscriber'; import { publicDirectory } from '.'; +import { ConsoleColors, logPort } from './ActionUtilities'; /* RouteSetter is a wrapper around the server that prevents the server from being exposed. */ @@ -35,6 +36,11 @@ export default async function InitializeServer(options: InitializationOptions) { server.use(express.static(publicDirectory)); server.use("/images", express.static(publicDirectory)); + server.use("*", (req, _res, next) => { + console.log(ConsoleColors.Cyan, req.originalUrl, req.user.id); + next(); + }); + server.use(wdm(compiler, { publicPath: config.output.publicPath })); server.use(whm(compiler)); @@ -44,7 +50,7 @@ export default async function InitializeServer(options: InitializationOptions) { const isRelease = determineEnvironment(); //vs. dev mode routeSetter(new RouteManager(server, isRelease)); - server.listen(listenAtPort, () => console.log(`server started at http://localhost:${listenAtPort}`)); + server.listen(listenAtPort, () => logPort("server", listenAtPort)); return isRelease; } diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts index f6a6c8718..810b3410c 100644 --- a/src/server/Websocket/Websocket.ts +++ b/src/server/Websocket/Websocket.ts @@ -6,9 +6,9 @@ import { Database } from "../database"; import { Search } from "../Search"; import * as io from 'socket.io'; import YoutubeApi from "../apis/youtube/youtubeApiSample"; -import { readFile } from "fs"; -import { Credentials } from "google-auth-library"; import { GoogleCredentialsLoader } from "../credentials/CredentialsLoader"; +import { ConsoleColors, logPort } from "../ActionUtilities"; +import { EventEmitter } from "events"; export namespace WebSocket { @@ -28,10 +28,10 @@ export namespace WebSocket { async function preliminaryFunctions() { } - export function initialize(serverPort: number, isRelease: boolean) { + export function initialize(socketPort: number, isRelease: boolean) { const endpoint = io(); - endpoint.listen(serverPort); - console.log(`listening on port ${serverPort}`); + endpoint.listen(socketPort); + logPort("websocket", socketPort); endpoint.on("connection", function (socket: Socket) { socket.use((_packet, next) => { @@ -92,7 +92,7 @@ export namespace WebSocket { function barReceived(socket: SocketIO.Socket, guid: string) { clients[guid] = new Client(guid.toString()); - console.log(`User ${guid} has connected`); + console.log(ConsoleColors.Green, `User ${guid} has connected`); socketMap.set(socket, guid); } diff --git a/src/server/index.ts b/src/server/index.ts index 01ebf0ac1..618940c1a 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -43,7 +43,11 @@ async function preliminaryFunctions() { // 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(); + await log_execution({ + startMessage: "attempting to initialize mongodb connection", + endMessage: "connection outcome determined", + action: Database.tryInitializeConnection + }); } /** @@ -106,6 +110,10 @@ function routeSetter(router: RouteManager) { } (async function start() { - await log_execution("starting execution of preliminary functions", "completed preliminary functions", preliminaryFunctions); + await log_execution({ + startMessage: "starting execution of preliminary functions", + endMessage: "completed preliminary functions", + action: preliminaryFunctions + }); await initializeServer({ listenAtPort: 1050, routeSetter }); })(); -- cgit v1.2.3-70-g09d2 From cc96b05f8695f84c547438fed7300db0ac6d4266 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 19 Nov 2019 19:53:39 -0500 Subject: missed --- src/Utils.ts | 2 +- src/server/Websocket/Websocket.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'src/server/Websocket/Websocket.ts') diff --git a/src/Utils.ts b/src/Utils.ts index 891a3826b..2c9e50808 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -240,7 +240,7 @@ export namespace Utils { }); } - export function InjectLogger() { + export function InjectLogger(color: string) { } } diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts index 810b3410c..9f0959e65 100644 --- a/src/server/Websocket/Websocket.ts +++ b/src/server/Websocket/Websocket.ts @@ -59,6 +59,8 @@ export namespace WebSocket { Utils.AddServerHandler(socket, MessageStore.DeleteFields, ids => DeleteFields(socket, ids)); Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField); Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields); + + Utils.InjectLogger(ConsoleColors.Magenta); }); } -- cgit v1.2.3-70-g09d2 From 23e72d25506a1cb87b15a34dbf7d85b61bfe063b Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 19 Nov 2019 20:05:12 -0500 Subject: inject logger, not working --- src/Utils.ts | 10 ++++++++-- src/server/Websocket/Websocket.ts | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) (limited to 'src/server/Websocket/Websocket.ts') diff --git a/src/Utils.ts b/src/Utils.ts index 2c9e50808..1e69cfbfe 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -3,6 +3,7 @@ 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 { @@ -240,8 +241,13 @@ export namespace Utils { }); } - export function InjectLogger(color: string) { - + export function InjectLogger(socket: Socket, color: string) { + const on = socket.on; + socket.on = function (event: string | symbol, listener: (...args: any[]) => void) { + console.log(color, event); + on(event, listener); + return socket; + }; } } diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts index 9f0959e65..308b32988 100644 --- a/src/server/Websocket/Websocket.ts +++ b/src/server/Websocket/Websocket.ts @@ -60,7 +60,7 @@ export namespace WebSocket { Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField); Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields); - Utils.InjectLogger(ConsoleColors.Magenta); + Utils.InjectLogger(socket, ConsoleColors.Yellow); }); } -- cgit v1.2.3-70-g09d2 From 6ddb23d48a8c97cbe12f841791825567c3a48e69 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 19 Nov 2019 22:17:34 -0500 Subject: removed call --- src/server/Websocket/Websocket.ts | 2 -- 1 file changed, 2 deletions(-) (limited to 'src/server/Websocket/Websocket.ts') diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts index 308b32988..810b3410c 100644 --- a/src/server/Websocket/Websocket.ts +++ b/src/server/Websocket/Websocket.ts @@ -59,8 +59,6 @@ export namespace WebSocket { Utils.AddServerHandler(socket, MessageStore.DeleteFields, ids => DeleteFields(socket, ids)); Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField); Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields); - - Utils.InjectLogger(socket, ConsoleColors.Yellow); }); } -- cgit v1.2.3-70-g09d2 From 1722788bf7d672043b1d51026b553426459bbd22 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 20 Nov 2019 19:17:48 -0500 Subject: small --- src/Utils.ts | 9 --------- src/server/Initialization.ts | 2 +- src/server/Websocket/Websocket.ts | 9 +++------ 3 files changed, 4 insertions(+), 16 deletions(-) (limited to 'src/server/Websocket/Websocket.ts') diff --git a/src/Utils.ts b/src/Utils.ts index 1e69cfbfe..fc12c1e14 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -240,15 +240,6 @@ export namespace Utils { handler([arg, loggingCallback('S sending', fn, message.Name)]); }); } - - export function InjectLogger(socket: Socket, color: string) { - const on = socket.on; - socket.on = function (event: string | symbol, listener: (...args: any[]) => void) { - console.log(color, event); - on(event, listener); - return socket; - }; - } } export function OmitKeys(obj: any, keys: string[], addKeyFunc?: (dup: any) => void): { omit: any, extract: any } { diff --git a/src/server/Initialization.ts b/src/server/Initialization.ts index 357d7837e..08b476822 100644 --- a/src/server/Initialization.ts +++ b/src/server/Initialization.ts @@ -37,7 +37,7 @@ export default async function InitializeServer(options: InitializationOptions) { server.use("/images", express.static(publicDirectory)); server.use("*", (req, _res, next) => { - console.log(ConsoleColors.Cyan, req.originalUrl, req.user.id); + console.log(ConsoleColors.Cyan, req.originalUrl, req.user.email); next(); }); diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts index 810b3410c..74a6b4263 100644 --- a/src/server/Websocket/Websocket.ts +++ b/src/server/Websocket/Websocket.ts @@ -8,7 +8,6 @@ import * as io from 'socket.io'; import YoutubeApi from "../apis/youtube/youtubeApiSample"; import { GoogleCredentialsLoader } from "../credentials/CredentialsLoader"; import { ConsoleColors, logPort } from "../ActionUtilities"; -import { EventEmitter } from "events"; export namespace WebSocket { @@ -30,9 +29,6 @@ export namespace WebSocket { export function initialize(socketPort: number, isRelease: boolean) { const endpoint = io(); - endpoint.listen(socketPort); - logPort("websocket", socketPort); - endpoint.on("connection", function (socket: Socket) { socket.use((_packet, next) => { let id = socketMap.get(socket); @@ -60,7 +56,8 @@ export namespace WebSocket { Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField); Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields); }); - + endpoint.listen(socketPort); + logPort("websocket", socketPort); } function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any[]) => void]) { @@ -92,7 +89,7 @@ export namespace WebSocket { function barReceived(socket: SocketIO.Socket, guid: string) { clients[guid] = new Client(guid.toString()); - console.log(ConsoleColors.Green, `User ${guid} has connected`); + console.log(ConsoleColors.Green, `user ${guid} has connected to the web socket`); socketMap.set(socket, guid); } -- cgit v1.2.3-70-g09d2 From b831be86743e329cce441b3d7ae2aa5321e7fb9c Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sat, 23 Nov 2019 17:09:13 -0500 Subject: improved user activity log --- src/server/ActionUtilities.ts | 13 ++++ src/server/ApiManagers/UserManager.ts | 46 ++++++------ src/server/Initialization.ts | 8 ++- src/server/Websocket/Websocket.ts | 22 +++--- .../authentication/controllers/user_controller.ts | 3 - views/login.pug | 2 - views/stylesheets/authentication.css | 81 ++++++++++++++++++++++ views/user_activity.pug | 19 +++++ 8 files changed, 155 insertions(+), 39 deletions(-) create mode 100644 views/user_activity.pug (limited to 'src/server/Websocket/Websocket.ts') diff --git a/src/server/ActionUtilities.ts b/src/server/ActionUtilities.ts index a5f33833d..7f493dd70 100644 --- a/src/server/ActionUtilities.ts +++ b/src/server/ActionUtilities.ts @@ -55,4 +55,17 @@ export enum ConsoleColors { export function logPort(listener: string, port: number) { process.stdout.write(`${listener} listening on port `); console.log(ConsoleColors.Yellow, port); +} + +export 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); + + 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/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts index 51a434fcf..4ee5a2b85 100644 --- a/src/server/ApiManagers/UserManager.ts +++ b/src/server/ApiManagers/UserManager.ts @@ -1,7 +1,14 @@ import ApiManager, { Registration } from "./ApiManager"; import { Method } from "../RouteManager"; -import { WebSocket } from "../Websocket/Websocket"; import { Database } from "../database"; +import { msToTime } from "../ActionUtilities"; + +export const timeMap: { [id: string]: number } = {}; +interface ActivityUnit { + user: string; + duration: number; +} + export default class UserManager extends ApiManager { @@ -32,35 +39,34 @@ export default class UserManager extends ApiManager { register({ method: Method.GET, - subscription: "/whosOnline", + subscription: "/activity", onValidation: ({ res }) => { - let users: any = { active: {}, inactive: {} }; const now = Date.now(); - const { timeMap } = WebSocket; + const activeTimes: ActivityUnit[] = []; + const inactiveTimes: ActivityUnit[] = []; + for (const user in timeMap) { const time = timeMap[user]; - const key = ((now - time) / 1000) < (60 * 5) ? "active" : "inactive"; - users[key][user] = `Last active ${msToTime(now - time)} ago`; + const duration = now - time; + const target = (duration / 1000) < (60 * 5) ? activeTimes : inactiveTimes; + target.push({ user, duration }); } - res.send(users); + const process = (target: { user: string, duration: number }[]) => { + const comparator = (first: ActivityUnit, second: ActivityUnit) => first.duration - second.duration; + const sorted = target.sort(comparator); + return sorted.map(({ user, duration }) => `${user} (last active ${msToTime(duration)} ago)`); + }; + + res.render("user_activity.pug", { + title: "User Activity", + active: process(activeTimes), + inactive: process(inactiveTimes) + }); } }); } -} - -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); - - 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/Initialization.ts b/src/server/Initialization.ts index 08b476822..7fad5556d 100644 --- a/src/server/Initialization.ts +++ b/src/server/Initialization.ts @@ -20,6 +20,8 @@ import * as request from 'request'; import RouteSubscriber from './RouteSubscriber'; import { publicDirectory } from '.'; import { ConsoleColors, logPort } from './ActionUtilities'; +import { WebSocket } from './Websocket/Websocket'; +import { timeMap } from './ApiManagers/UserManager'; /* RouteSetter is a wrapper around the server that prevents the server from being exposed. */ @@ -37,7 +39,11 @@ export default async function InitializeServer(options: InitializationOptions) { server.use("/images", express.static(publicDirectory)); server.use("*", (req, _res, next) => { - console.log(ConsoleColors.Cyan, req.originalUrl, req.user.email); + const userEmail = req.user?.email; + console.log(ConsoleColors.Cyan, req.originalUrl, userEmail ?? ""); + if (userEmail) { + timeMap[userEmail] = Date.now(); + } next(); }); diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts index 74a6b4263..fbf71f707 100644 --- a/src/server/Websocket/Websocket.ts +++ b/src/server/Websocket/Websocket.ts @@ -8,16 +8,12 @@ import * as io from 'socket.io'; import YoutubeApi from "../apis/youtube/youtubeApiSample"; import { GoogleCredentialsLoader } from "../credentials/CredentialsLoader"; import { ConsoleColors, logPort } from "../ActionUtilities"; +import { timeMap } from "../ApiManagers/UserManager"; export namespace WebSocket { - interface Map { - [key: string]: Client; - } - let clients: Map = {}; - + let clients: { [key: string]: Client } = {}; export const socketMap = new Map(); - export const timeMap: { [id: string]: number } = {}; export async function start(serverPort: number, isRelease: boolean) { await preliminaryFunctions(); @@ -31,9 +27,9 @@ export namespace WebSocket { const endpoint = io(); endpoint.on("connection", function (socket: Socket) { socket.use((_packet, next) => { - let id = socketMap.get(socket); - if (id) { - timeMap[id] = Date.now(); + let userEmail = socketMap.get(socket); + if (userEmail) { + timeMap[userEmail] = Date.now(); } next(); }); @@ -87,10 +83,10 @@ export namespace WebSocket { await Search.Instance.clear(); } - function barReceived(socket: SocketIO.Socket, guid: string) { - clients[guid] = new Client(guid.toString()); - console.log(ConsoleColors.Green, `user ${guid} has connected to the web socket`); - socketMap.set(socket, guid); + function barReceived(socket: SocketIO.Socket, userEmail: string) { + clients[userEmail] = new Client(userEmail.toString()); + console.log(ConsoleColors.Green, `user ${userEmail} has connected to the web socket`); + socketMap.set(socket, userEmail); } function getField([id, callback]: [string, (result?: Transferable) => void]) { diff --git a/src/server/authentication/controllers/user_controller.ts b/src/server/authentication/controllers/user_controller.ts index b2b9d33f6..517353479 100644 --- a/src/server/authentication/controllers/user_controller.ts +++ b/src/server/authentication/controllers/user_controller.ts @@ -3,10 +3,7 @@ import { Request, Response, NextFunction } from "express"; import * as passport from "passport"; import { IVerifyOptions } from "passport-local"; import "../config/passport"; -import * as request from "express-validator"; import flash = require("express-flash"); -import * as session from "express-session"; -import * as pug from 'pug'; import * as async from 'async'; import * as nodemailer from 'nodemailer'; import c = require("crypto"); diff --git a/views/login.pug b/views/login.pug index 9bc40a495..26da5e29e 100644 --- a/views/login.pug +++ b/views/login.pug @@ -14,11 +14,9 @@ block content .inner.login h3.auth_header Log In .form-group - //- label.col-sm-3.control-label(for='email', id='email_label') Email .col-sm-7 input.form-control(type='email', name='email', id='email', placeholder='Email', autofocus, required) .form-group - //- label.col-sm-3.control-label(for='password') Password .col-sm-7 input.form-control(type='password', name='password', id='password', placeholder='Password', required) .form-group diff --git a/views/stylesheets/authentication.css b/views/stylesheets/authentication.css index 36bb880af..ff1f4aace 100644 --- a/views/stylesheets/authentication.css +++ b/views/stylesheets/authentication.css @@ -139,4 +139,85 @@ body { padding-right: 10px; font-family: Arial, Helvetica, sans-serif; font-size: 16px; +} + +.outermost, .online-container { + display: flex; + flex-direction: row; + height: 98vh; + justify-content: center; +} + +.online-container { + background: white; + display: flex; + flex-direction: row; + height: 80%; + width: 80%; + align-self: center; + justify-content: center; + border-radius: 8px; + box-shadow: 10px 10px 10px #00000099; +} + +.partition { + width: 50%; + display: flex; + flex-direction: column; + border: 1px solid black; +} + +.inner-activity { + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; + border-top: 2px solid black; + background: white; + padding: 20px; + overflow: scroll; +} + +ol { + align-self: center; +} + +li { + font-family: Arial, Helvetica, sans-serif; + border: 1px solid black; + padding: 10px; + border-radius: 5px; + margin-bottom: 5px; +} + +.duration { + font-style: italic; +} + +span.user-type { + align-self: center; + font-family: Arial, Helvetica, sans-serif; + font-weight: bold; + font-size: 20px; + margin: 50px; +} + +#active-partition { + background: green; + border-top-left-radius: 8px; + border-bottom-left-radius: 8px; +} + +#active-inner { + border-bottom-left-radius: 8px; +} + +#inactive-partition { + background: red; + border-top-right-radius: 8px; + border-bottom-right-radius: 8px; +} + +#inactive-inner { + border-bottom-right-radius: 8px; } \ No newline at end of file diff --git a/views/user_activity.pug b/views/user_activity.pug new file mode 100644 index 000000000..68e42140d --- /dev/null +++ b/views/user_activity.pug @@ -0,0 +1,19 @@ +extends ./layout + +block content + style + include ./stylesheets/authentication.css + .outermost + .online-container + .partition(id="active-partition") + span.user-type Active Users + .inner-activity(id="active-inner") + ol + each val in active + li= val + .partition(id="inactive-partition") + span.user-type Inactive Users + .inner-activity(id="inactive-inner") + ol + each val in inactive + li= val \ No newline at end of file -- cgit v1.2.3-70-g09d2