diff options
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/DashUploadUtils.ts | 39 | ||||
| -rw-r--r-- | src/server/RouteStore.ts | 5 | ||||
| -rw-r--r-- | src/server/apis/google/GoogleApiServerUtils.ts | 122 | ||||
| -rw-r--r-- | src/server/apis/google/GooglePhotosUploadUtils.ts | 4 | ||||
| -rw-r--r-- | src/server/authentication/config/passport.ts | 10 | ||||
| -rw-r--r-- | src/server/authentication/models/current_user_utils.ts | 86 | ||||
| -rw-r--r-- | src/server/database.ts | 12 | ||||
| -rw-r--r-- | src/server/index.ts | 123 |
8 files changed, 313 insertions, 88 deletions
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 4230e9b17..46d897339 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -3,6 +3,8 @@ import { Utils } from '../Utils'; import * as path from 'path'; import * as sharp from 'sharp'; import request = require('request-promise'); +import { ExifData, ExifImage } from 'exif'; +import { Opt } from '../new_fields/Doc'; const uploadDirectory = path.join(__dirname, './public/files/'); @@ -22,7 +24,7 @@ export namespace DashUploadUtils { const gifs = [".gif"]; const pngs = [".png"]; const jpgs = [".jpg", ".jpeg"]; - const imageFormats = [...pngs, ...jpgs, ...gifs]; + export const imageFormats = [...pngs, ...jpgs, ...gifs]; const videoFormats = [".mov", ".mp4"]; const size = "content-length"; @@ -31,12 +33,19 @@ export namespace DashUploadUtils { export interface UploadInformation { mediaPaths: string[]; fileNames: { [key: string]: string }; + exifData: EnrichedExifData; contentSize?: number; contentType?: string; } - const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`; + const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${sanitizeExtension(url)}`; const sanitize = (filename: string) => filename.replace(/\s+/g, "_"); + const sanitizeExtension = (source: string) => { + let extension = path.extname(source); + extension = extension.toLowerCase(); + extension = extension.split("?")[0]; + return extension; + }; /** * Uploads an image specified by the @param source to Dash's /public/files/ @@ -64,10 +73,16 @@ export namespace DashUploadUtils { isLocal: boolean; stream: any; normalizedUrl: string; + exifData: EnrichedExifData; contentSize?: number; contentType?: string; } + export interface EnrichedExifData { + data: ExifData; + error?: string; + } + /** * Based on the url's classification as local or remote, gleans * as much information as possible about the specified image @@ -76,7 +91,9 @@ export namespace DashUploadUtils { */ export const InspectImage = async (source: string): Promise<InspectionResults> => { const { isLocal, stream, normalized: normalizedUrl } = classify(source); + const exifData = await parseExifData(source); const results = { + exifData, isLocal, stream, normalizedUrl @@ -101,13 +118,13 @@ export namespace DashUploadUtils { }; export const UploadInspectedImage = async (metadata: InspectionResults, filename?: string, prefix = ""): Promise<UploadInformation> => { - const { isLocal, stream, normalizedUrl, contentSize, contentType } = metadata; + const { isLocal, stream, normalizedUrl, contentSize, contentType, exifData } = metadata; const resolved = filename ? sanitize(filename) : generate(prefix, normalizedUrl); - let extension = path.extname(normalizedUrl) || path.extname(resolved); - extension && (extension = extension.toLowerCase()); + const extension = sanitizeExtension(normalizedUrl || resolved); let information: UploadInformation = { mediaPaths: [], fileNames: { clean: resolved }, + exifData, contentSize, contentType, }; @@ -159,6 +176,18 @@ export namespace DashUploadUtils { }; }; + const parseExifData = async (source: string): Promise<EnrichedExifData> => { + return new Promise<EnrichedExifData>(resolve => { + new ExifImage(source, (error, data) => { + let reason: Opt<string> = undefined; + if (error) { + reason = (error as any).code; + } + resolve({ data, error: reason }); + }); + }); + }; + export const createIfNotExists = async (path: string) => { if (await new Promise<boolean>(resolve => fs.exists(path, resolve))) { return true; diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts index ee9cd8a0e..7426ffb39 100644 --- a/src/server/RouteStore.ts +++ b/src/server/RouteStore.ts @@ -13,6 +13,8 @@ export enum RouteStore { upload = "/upload", dataUriToImage = "/uploadURI", images = "/images", + inspectImage = "/inspectImage", + imageHierarchyExport = "/imageHierarchyExport", // USER AND WORKSPACES getCurrUser = "/getCurrentUser", @@ -32,7 +34,8 @@ export enum RouteStore { // APIS cognitiveServices = "/cognitiveservices", googleDocs = "/googleDocs", - googlePhotosAccessToken = "/googlePhotosAccessToken", + readGoogleAccessToken = "/readGoogleAccessToken", + writeGoogleAccessToken = "/writeGoogleAccessToken", googlePhotosMediaUpload = "/googlePhotosMediaUpload", googlePhotosMediaDownload = "/googlePhotosMediaDownload", googleDocsGet = "/googleDocsGet" diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index c899c2ef2..5714c9928 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -25,7 +25,8 @@ export namespace GoogleApiServerUtils { 'drive.file', 'photoslibrary', 'photoslibrary.appendonly', - 'photoslibrary.sharing' + 'photoslibrary.sharing', + 'userinfo.profile' ]; export const parseBuffer = (data: Buffer) => JSON.parse(data.toString()); @@ -75,6 +76,82 @@ export namespace GoogleApiServerUtils { }); }; + const RetrieveOAuthClient = async (information: CredentialInformation) => { + return new Promise<OAuth2Client>((resolve, reject) => { + readFile(information.credentialsPath, async (err, credentials) => { + if (err) { + reject(err); + return console.log('Error loading client secret file:', err); + } + const { client_secret, client_id, redirect_uris } = parseBuffer(credentials).installed; + resolve(new google.auth.OAuth2(client_id, client_secret, redirect_uris[0])); + }); + }); + }; + + export const GenerateAuthenticationUrl = async (information: CredentialInformation) => { + const client = await RetrieveOAuthClient(information); + return client.generateAuthUrl({ + access_type: 'offline', + scope: SCOPES.map(relative => prefix + relative), + }); + }; + + export interface GoogleAuthenticationResult { + access_token: string; + avatar: string; + name: string; + } + export const ProcessClientSideCode = async (information: CredentialInformation, authenticationCode: string): Promise<GoogleAuthenticationResult> => { + const oAuth2Client = await RetrieveOAuthClient(information); + return new Promise<GoogleAuthenticationResult>((resolve, reject) => { + oAuth2Client.getToken(authenticationCode, async (err, token) => { + if (err || !token) { + reject(err); + return console.error('Error retrieving access token', err); + } + oAuth2Client.setCredentials(token); + const enriched = injectUserInfo(token); + await Database.Auxiliary.GoogleAuthenticationToken.Write(information.userId, enriched); + const { given_name, picture } = enriched.userInfo; + resolve({ + access_token: enriched.access_token!, + avatar: picture, + name: given_name + }); + }); + }); + }; + + /** + * It's pretty cool: the credentials id_token is split into thirds by periods. + * The middle third contains a base64-encoded JSON string with all the + * user info contained in the interface below. So, we isolate that middle third, + * base64 decode with atob and parse the JSON. + * @param credentials the client credentials returned from OAuth after the user + * has executed the authentication routine + */ + const injectUserInfo = (credentials: Credentials): EnrichedCredentials => { + const userInfo = JSON.parse(atob(credentials.id_token!.split(".")[1])); + return { ...credentials, userInfo }; + }; + + export type EnrichedCredentials = Credentials & { userInfo: UserInfo }; + export interface UserInfo { + at_hash: string; + aud: string; + azp: string; + exp: number; + family_name: string; + given_name: string; + iat: number; + iss: string; + locale: string; + name: string; + picture: string; + sub: string; + } + export const RetrieveCredentials = (information: CredentialInformation) => { return new Promise<TokenResult>((resolve, reject) => { readFile(information.credentialsPath, async (err, credentials) => { @@ -107,17 +184,13 @@ export namespace GoogleApiServerUtils { return new Promise<TokenResult>((resolve, reject) => { // Attempting to authorize user (${userId}) Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId).then(token => { - if (!token) { - // No token registered, so awaiting input from user - return getNewToken(oAuth2Client, userId).then(resolve, reject); - } - if (token.expiry_date! < new Date().getTime()) { + if (token!.expiry_date! < new Date().getTime()) { // Token has expired, so submitting a request for a refreshed access token - return refreshToken(token, client_id, client_secret, oAuth2Client, userId).then(resolve, reject); + return refreshToken(token!, client_id, client_secret, oAuth2Client, userId).then(resolve, reject); } // Authentication successful! - oAuth2Client.setCredentials(token); - resolve({ token, client: oAuth2Client }); + oAuth2Client.setCredentials(token!); + resolve({ token: token!, client: oAuth2Client }); }); }); } @@ -145,35 +218,4 @@ export namespace GoogleApiServerUtils { }); }; - /** - * Get and store new token after prompting for user authorization, and then - * execute the given callback with the authorized OAuth2 client. - * @param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for. - * @param {getEventsCallback} callback The callback for the authorized client. - */ - function getNewToken(oAuth2Client: OAuth2Client, userId: string) { - return new Promise<TokenResult>((resolve, reject) => { - const authUrl = oAuth2Client.generateAuthUrl({ - access_type: 'offline', - scope: SCOPES.map(relative => prefix + relative), - }); - console.log('Authorize this app by visiting this url:', authUrl); - const rl = createInterface({ - input: process.stdin, - output: process.stdout, - }); - rl.question('Enter the code from that page here: ', (code) => { - rl.close(); - oAuth2Client.getToken(code, async (err, token) => { - if (err || !token) { - reject(err); - return console.error('Error retrieving access token', err); - } - oAuth2Client.setCredentials(token); - await Database.Auxiliary.GoogleAuthenticationToken.Write(userId, token); - resolve({ token, client: oAuth2Client }); - }); - }); - }); - } }
\ No newline at end of file diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index 16c4f6c3a..4a67e57cc 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -4,6 +4,7 @@ import * as path from 'path'; import { MediaItemCreationResult } from './SharedTypes'; import { NewMediaItem } from "../../index"; import { BatchedArray, TimeUnit } from 'array-batcher'; +import { DashUploadUtils } from '../../DashUploadUtils'; export namespace GooglePhotosUploadUtils { @@ -32,6 +33,9 @@ export namespace GooglePhotosUploadUtils { }; export const DispatchGooglePhotosUpload = async (url: string) => { + if (!DashUploadUtils.imageFormats.includes(path.extname(url))) { + return undefined; + } const body = await request(url, { encoding: null }); const parameters = { method: 'POST', diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts index 8b20d29b1..8915a4abf 100644 --- a/src/server/authentication/config/passport.ts +++ b/src/server/authentication/config/passport.ts @@ -41,11 +41,9 @@ export let isAuthenticated = (req: Request, res: Response, next: NextFunction) = export let isAuthorized = (req: Request, res: Response, next: NextFunction) => { const provider = req.path.split("/").slice(-1)[0]; - if (req.user) { - if (_.find((req.user as any).tokens, { kind: provider })) { - next(); - } else { - res.redirect(`/auth/${provider}`); - } + if (_.find((req.user as any).tokens, { kind: provider })) { + next(); + } else { + res.redirect(`/auth/${provider}`); } };
\ No newline at end of file diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index b2509a4f1..3858907ba 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -12,6 +12,7 @@ import { listSpec } from "../../../new_fields/Schema"; import { Cast, StrCast, PromiseValue } from "../../../new_fields/Types"; import { Utils } from "../../../Utils"; import { RouteStore } from "../../RouteStore"; +import { ScriptField } from "../../../new_fields/ScriptField"; export class CurrentUserUtils { private static curr_id: string; @@ -29,7 +30,6 @@ export class CurrentUserUtils { private static createUserDocument(id: string): Doc { let doc = new Doc(id, true); doc.viewType = CollectionViewType.Tree; - doc.dropAction = "alias"; doc.layout = CollectionView.LayoutString(); doc.title = Doc.CurrentUserEmail; this.updateUserDocument(doc); @@ -37,7 +37,9 @@ export class CurrentUserUtils { doc.gridGap = 5; doc.xMargin = 5; doc.yMargin = 5; + doc.height = 42; doc.boxShadow = "0 0"; + doc.convertToButtons = true; // for CollectionLinearView used as the docButton layout doc.optionalRightCollection = Docs.Create.StackingDocument([], { title: "New mobile uploads" }); return doc; } @@ -46,7 +48,7 @@ export class CurrentUserUtils { // setup workspaces library item if (doc.workspaces === undefined) { - const workspaces = Docs.Create.TreeDocument([], { title: "Workspaces".toUpperCase(), height: 100 }); + const workspaces = Docs.Create.TreeDocument([], { title: "WORKSPACES", height: 100 }); workspaces.boxShadow = "0 0"; doc.workspaces = workspaces; } @@ -98,21 +100,70 @@ export class CurrentUserUtils { doc.curPresentation = curPresentation; } - if (doc.sidebar === undefined) { - const sidebar = Docs.Create.StackingDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { title: "Sidebar" }); - sidebar.forceActive = true; - sidebar.lockedPosition = true; - sidebar.gridGap = 5; - sidebar.xMargin = 5; - sidebar.yMargin = 5; - sidebar.boxShadow = "1 1 3"; - doc.sidebar = sidebar; - } - PromiseValue(Cast(doc.sidebar, Doc)).then(sidebar => { - if (sidebar) { - sidebar.backgroundColor = "lightgrey"; + if (doc.Library === undefined) { + let Search = Docs.Create.ButtonDocument({ width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Search" }); + let Library = Docs.Create.ButtonDocument({ width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Library" }); + let Create = Docs.Create.ButtonDocument({ width: 35, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Create" }); + if (doc.sidebarContainer === undefined) { + doc.sidebarContainer = new Doc(); + (doc.sidebarContainer as Doc).chromeStatus = "disabled"; } - }) + + const library = Docs.Create.TreeDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { title: "Library" }); + library.forceActive = true; + library.lockedPosition = true; + library.gridGap = 5; + library.xMargin = 5; + library.yMargin = 5; + library.dropAction = "alias"; + Library.targetContainer = doc.sidebarContainer; + Library.library = library; + Library.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.library"); + + const searchBox = Docs.Create.QueryDocument({ title: "search stack" }); + searchBox.ignoreClick = true; + Search.searchBox = searchBox; + Search.targetContainer = doc.sidebarContainer; + Search.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.searchBox"); + + let createCollection = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Collection", icon: "folder" }); + createCollection.onDragStart = ScriptField.MakeFunction('Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })'); + let createWebPage = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Web Page", icon: "globe-asia" }); + createWebPage.onDragStart = ScriptField.MakeFunction('Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })'); + let createCatImage = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Image", icon: "cat" }); + createCatImage.onDragStart = ScriptField.MakeFunction('Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { width: 200, title: "an image of a cat" })'); + let createButton = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Button", icon: "bolt" }); + createButton.onDragStart = ScriptField.MakeFunction('Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })'); + let createPresentation = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Presentation", icon: "tv" }); + createPresentation.onDragStart = ScriptField.MakeFunction('Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" })'); + let createFolderImport = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Import Folder", icon: "cloud-upload-alt" }); + createFolderImport.onDragStart = ScriptField.MakeFunction('Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })'); + const dragCreators = Docs.Create.MasonryDocument([createCollection, createWebPage, createCatImage, createButton, createPresentation, createFolderImport], { width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons" }); + const color = Docs.Create.ColorDocument({ title: "color picker", width: 400 }); + color.dropAction = "alias"; + color.ignoreClick = true; + color.removeDropProperties = new List<string>(["dropAction", "ignoreClick"]); + const creators = Docs.Create.StackingDocument([dragCreators, color], { width: 500, height: 800, chromeStatus: "disabled", title: "creator stack" }); + Create.targetContainer = doc.sidebarContainer; + Create.creators = creators; + Create.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.creators"); + + const libraryButtons = Docs.Create.StackingDocument([Search, Library, Create], { width: 500, height: 80, chromeStatus: "disabled", title: "library stack" }); + libraryButtons.sectionFilter = "title"; + libraryButtons.boxShadow = "0 0"; + libraryButtons.ignoreClick = true; + libraryButtons.hideHeadings = true; + libraryButtons.backgroundColor = "lightgrey"; + + doc.libraryButtons = libraryButtons; + doc.Library = Library; + doc.Create = Create; + doc.Search = Search; + } + PromiseValue(Cast(doc.libraryButtons, Doc)).then(libraryButtons => { }); + PromiseValue(Cast(doc.Library, Doc)).then(library => library && library.library && library.targetContainer && (library.onClick as ScriptField).script.run({ this: library })); + PromiseValue(Cast(doc.Create, Doc)).then(async create => create && create.creators && create.targetContainer); + PromiseValue(Cast(doc.Search, Doc)).then(async search => search && search.searchBox && search.targetContainer); if (doc.overlays === undefined) { const overlays = Docs.Create.FreeformDocument([], { title: "Overlays" }); @@ -124,8 +175,7 @@ export class CurrentUserUtils { PromiseValue(Cast(doc.overlays, Doc)).then(overlays => overlays && Doc.AddDocToList(overlays, "data", doc.linkFollowBox = Docs.Create.LinkFollowBoxDocument({ x: 250, y: 20, width: 500, height: 370, title: "Link Follower" }))); } - StrCast(doc.title).indexOf("@") !== -1 && (doc.title = (StrCast(doc.title).split("@")[0] + "'s Library").toUpperCase()); - StrCast(doc.title).indexOf("'s Library") !== -1 && (doc.title = StrCast(doc.title).toUpperCase()); + doc.title = "DOCUMENTS"; doc.backgroundColor = "#eeeeee"; doc.width = 100; doc.preventTreeViewOpen = true; diff --git a/src/server/database.ts b/src/server/database.ts index d2375ebd9..db86b472d 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -4,6 +4,7 @@ import { Opt } from '../new_fields/Doc'; import { Utils, emptyFunction } from '../Utils'; import { DashUploadUtils } from './DashUploadUtils'; import { Credentials } from 'google-auth-library'; +import { GoogleApiServerUtils } from './apis/google/GoogleApiServerUtils'; export namespace Database { @@ -131,7 +132,7 @@ export namespace Database { } } - public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = Database.DocumentsCollection) { + public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = "newDocuments") { if (this.db) { this.db.collection(collectionName).findOne({ _id: id }, (err, result) => { if (result) { @@ -165,7 +166,7 @@ export namespace Database { } } - public async visit(ids: string[], fn: (result: any) => string[], collectionName = "newDocuments"): Promise<void> { + public async visit(ids: string[], fn: (result: any) => string[] | Promise<string[]>, collectionName = "newDocuments"): Promise<void> { if (this.db) { const visited = new Set<string>(); while (ids.length) { @@ -179,10 +180,9 @@ export namespace Database { for (const doc of docs) { const id = doc.id; visited.add(id); - ids.push(...fn(doc)); + ids.push(...(await fn(doc))); } } - } else { return new Promise(res => { this.onConnect.push(() => { @@ -260,8 +260,8 @@ export namespace Database { return SanitizedSingletonQuery<StoredCredentials>({ userId }, GoogleAuthentication, removeId); }; - export const Write = async (userId: string, token: any) => { - return Instance.insert({ userId, canAccess: [], ...token }, GoogleAuthentication); + export const Write = async (userId: string, enrichedCredentials: GoogleApiServerUtils.EnrichedCredentials) => { + return Instance.insert({ userId, canAccess: [], ...enrichedCredentials }, GoogleAuthentication); }; export const Update = async (userId: string, access_token: string, expiry_date: number) => { diff --git a/src/server/index.ts b/src/server/index.ts index 690836fff..010a851bc 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -53,6 +53,8 @@ import { DashUploadUtils } from './DashUploadUtils'; import { BatchedArray, TimeUnit } from 'array-batcher'; import { ParsedPDF } from "./PdfTypes"; import { reject } from 'bluebird'; +import { ExifData } from 'exif'; +import { Result } from '../client/northstar/model/idea/idea'; const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest)); let youtubeApiKey: string; @@ -211,7 +213,6 @@ const solrURL = "http://localhost:8983/solr/#/dash"; app.get("/textsearch", async (req, res) => { let q = req.query.q; - console.log("TEXTSEARCH " + q); if (q === undefined) { res.send([]); return; @@ -319,6 +320,76 @@ app.get("/serializeDoc/:docId", async (req, res) => { res.send({ docs, files: Array.from(files) }); }); +export type Hierarchy = { [id: string]: string | Hierarchy }; +export type ZipMutator = (file: Archiver.Archiver) => void | Promise<void>; + +app.get(`${RouteStore.imageHierarchyExport}/:docId`, 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<void> => { + const zip = Archiver('zip'); + zip.pipe(res); + await mutator(zip); + return zip.finalize(); +}; + +const targetedVisitorRecursive = async (seedId: string, hierarchy: Hierarchy): Promise<void> => { + 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<void> => { + 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}`); + } + } +}; + app.get("/downloadId/:docId", async (req, res) => { res.set('Content-disposition', `attachment;`); res.set('Content-Type', "application/zip"); @@ -590,10 +661,11 @@ const uploadDirectory = __dirname + "/public/files/"; const pdfDirectory = uploadDirectory + "text"; DashUploadUtils.createIfNotExists(pdfDirectory); -interface FileResponse { +interface ImageFileResponse { name: string; path: string; type: string; + exif: Opt<DashUploadUtils.EnrichedExifData>; } // SETTERS @@ -604,10 +676,11 @@ app.post( form.uploadDir = uploadDirectory; form.keepExtensions = true; form.parse(req, async (_err, _fields, files) => { - let results: FileResponse[] = []; + let results: ImageFileResponse[] = []; for (const key in files) { const { type, path: location, name } = files[key]; const filename = path.basename(location); + let uploadInformation: Opt<DashUploadUtils.UploadInformation>; if (filename.endsWith(".pdf")) { let dataBuffer = fs.readFileSync(uploadDirectory + filename); const result: ParsedPDF = await pdf(dataBuffer); @@ -622,9 +695,10 @@ app.post( }); }); } else { - await DashUploadUtils.UploadImage(uploadDirectory + filename, filename).catch(() => console.log(`Unable to process ${filename}`)); + uploadInformation = await DashUploadUtils.UploadImage(uploadDirectory + filename, filename); } - results.push({ name, type, path: `/files/${filename}` }); + const exif = uploadInformation ? uploadInformation.exifData : undefined; + results.push({ name, type, path: `/files/${filename}`, exif }); } _success(res, results); @@ -632,6 +706,15 @@ app.post( } ); +app.post(RouteStore.inspectImage, 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({}); +}); + addSecureRoute( Method.POST, (user, res, req) => { @@ -862,7 +945,21 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => { }); }); -app.get(RouteStore.googlePhotosAccessToken, (req, res) => GoogleApiServerUtils.RetrieveAccessToken({ credentialsPath, userId: req.header("userId")! }).then(token => res.send(token))); +app.get(RouteStore.readGoogleAccessToken, async (req, res) => { + const userId = req.header("userId")!; + const token = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId); + const information = { credentialsPath, userId }; + if (!token) { + return res.send(await GoogleApiServerUtils.GenerateAuthenticationUrl(information)); + } + GoogleApiServerUtils.RetrieveAccessToken(information).then(token => res.send(token)); +}); + +app.post(RouteStore.writeGoogleAccessToken, async (req, res) => { + const userId = req.header("userId")!; + const information = { credentialsPath, userId }; + res.send(await GoogleApiServerUtils.ProcessClientSideCode(information, req.body.authenticationCode)); +}); const tokenError = "Unable to successfully upload bytes for all images!"; const mediaError = "Unable to convert all uploaded bytes to media items!"; @@ -885,16 +982,17 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { await GooglePhotosUploadUtils.initialize({ credentialsPath, userId }); - let failed = 0; + let failed: number[] = []; const newMediaItems = await BatchedArray.from<GooglePhotosUploadUtils.MediaInput>(media, { batchSize: 25 }).batchedMapPatientInterval( { magnitude: 100, unit: TimeUnit.Milliseconds }, async (batch: GooglePhotosUploadUtils.MediaInput[]) => { const newMediaItems: NewMediaItem[] = []; - for (let element of batch) { + for (let index = 0; index < batch.length; index++) { + const element = batch[index]; const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url); if (!uploadToken) { - failed++; + failed.push(index); } else { newMediaItems.push({ description: element.description, @@ -906,12 +1004,13 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { } ); - if (failed) { - return _error(res, tokenError); + const failedCount = failed.length; + if (failedCount) { + console.log(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`); } GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then( - result => _success(res, result.newMediaItemResults), + result => _success(res, { results: result.newMediaItemResults, failed }), error => _error(res, mediaError, error) ); }); |
