From ec5c878fb4c5f7e03fdd214c0841cf2ebf983e8c Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Tue, 11 Feb 2020 15:48:06 -0500
Subject: refactored image upload data response format, google docs working for
 specific interactions

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

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

diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 071015193..d04f56e57 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -120,8 +120,8 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
             runInAction(() => this.completed += batch.length);
         });
 
-        await Promise.all(uploads.map(async ({ name, type, clientAccessPath, exifData }) => {
-            const path = Utils.prepend(clientAccessPath);
+        await Promise.all(uploads.map(async ({ name, type, accessPaths, exifData }) => {
+            const path = Utils.prepend(accessPaths.agnostic.client);
             const document = await Docs.Get.DocumentFromType(type, path, { _width: 300, title: name });
             const { data, error } = exifData;
             if (document) {
-- 
cgit v1.2.3-70-g09d2


From f33ad290f1de3a01f2c4536f03f040e09771f82e Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sun, 16 Feb 2020 00:50:23 -0500
Subject: improved file upload api

---
 src/client/Network.ts                              |  14 ++-
 .../util/Import & Export/DirectoryImportBox.tsx    |  17 ++--
 src/client/views/collections/CollectionSubView.tsx |  56 +++++------
 src/new_fields/Doc.ts                              |   6 +-
 src/scraping/buxton/final/BuxtonImporter.ts        |   7 +-
 src/server/ApiManagers/GooglePhotosManager.ts      |   5 +-
 src/server/ApiManagers/UploadManager.ts            |   8 +-
 src/server/DashUploadUtils.ts                      | 107 +++++++--------------
 src/server/SharedMediaTypes.ts                     |  39 ++++++++
 src/server/database.ts                             |   6 +-
 10 files changed, 137 insertions(+), 128 deletions(-)

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

diff --git a/src/client/Network.ts b/src/client/Network.ts
index ccf60f199..6982ecf19 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -1,5 +1,6 @@
 import { Utils } from "../Utils";
 import requestPromise = require('request-promise');
+import { Upload } from "../server/SharedMediaTypes";
 
 export namespace Networking {
 
@@ -17,12 +18,21 @@ export namespace Networking {
         return requestPromise.post(options);
     }
 
-    export async function PostFormDataToServer(relativeRoute: string, formData: FormData) {
+    export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> {
+        const formData = new FormData();
+        if (Array.isArray(files)) {
+            if (!files.length) {
+                return [];
+            }
+            files.forEach(file => formData.append(Utils.GenerateGuid(), file));
+        } else {
+            formData.append(Utils.GenerateGuid(), files);
+        }
         const parameters = {
             method: 'POST',
             body: formData
         };
-        const response = await fetch(relativeRoute, parameters);
+        const response = await fetch("/uploadFormData", parameters);
         return response.json();
     }
 
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index d04f56e57..3d8bcbab7 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -22,7 +22,7 @@ import "./DirectoryImportBox.scss";
 import { Networking } from "../../Network";
 import { BatchedArray } from "array-batcher";
 import * as path from 'path';
-import { AcceptibleMedia } from "../../../server/SharedMediaTypes";
+import { AcceptibleMedia, Upload } from "../../../server/SharedMediaTypes";
 
 const unsupported = ["text/html", "text/plain"];
 
@@ -107,20 +107,21 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
         runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`);
 
         const batched = BatchedArray.from(validated, { batchSize: 15 });
-        const uploads = await batched.batchedMapAsync<any>(async (batch, collector) => {
-            const formData = new FormData();
-
+        const uploads = await batched.batchedMapAsync<Upload.FileResponse<Upload.ImageInformation>>(async (batch, collector) => {
             batch.forEach(file => {
                 sizes.push(file.size);
                 modifiedDates.push(file.lastModified);
-                formData.append(Utils.GenerateGuid(), file);
             });
-
-            collector.push(...(await Networking.PostFormDataToServer("/uploadFormData", formData)));
+            collector.push(...(await Networking.UploadFilesToServer<Upload.ImageInformation>(batch)));
             runInAction(() => this.completed += batch.length);
         });
 
-        await Promise.all(uploads.map(async ({ name, type, accessPaths, exifData }) => {
+        await Promise.all(uploads.map(async response => {
+            const { source: { type }, result } = response;
+            if (result instanceof Error) {
+                return;
+            }
+            const { accessPaths, exifData } = result;
             const path = Utils.prepend(accessPaths.agnostic.client);
             const document = await Docs.Get.DocumentFromType(type, path, { _width: 300, title: name });
             const { data, error } = exifData;
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 0963e1ea6..e0b2d524b 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,4 +1,4 @@
-import { action, computed, IReactionDisposer, reaction, trace } from "mobx";
+import { action, computed, IReactionDisposer, reaction } from "mobx";
 import * as rp from 'request-promise';
 import CursorField from "../../../new_fields/CursorField";
 import { Doc, DocListCast, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc";
@@ -25,6 +25,7 @@ import { ImageUtils } from "../../util/Import & Export/ImageUtils";
 import { Networking } from "../../Network";
 import { GestureUtils } from "../../../pen-gestures/GestureUtils";
 import { InteractionUtils } from "../../util/InteractionUtils";
+import { Upload } from "../../../server/SharedMediaTypes";
 
 export interface CollectionViewProps extends FieldViewProps {
     addDocument: (document: Doc) => boolean;
@@ -288,6 +289,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
             }
             const { items } = e.dataTransfer;
             const { length } = items;
+            const files: File[] = [];
             if (length) {
                 const batch = UndoManager.StartBatch("collection view drop");
                 const promises: Promise<void>[] = [];
@@ -307,41 +309,31 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                             });
                         promises.push(prom);
                     }
-                    const type = item.type;
                     if (item.kind === "file") {
                         const file = item.getAsFile();
-                        const formData = new FormData();
-
-                        if (!file || !file.type) {
-                            continue;
-                        }
-
-                        formData.append('file', file);
-                        const dropFileName = file ? file.name : "-empty-";
-                        promises.push(Networking.PostFormDataToServer("/uploadFormData", formData).then(results => {
-                            results.map(action((result: any) => {
-                                const { accessPaths, nativeWidth, nativeHeight, contentSize } = result;
-                                if (Object.keys(accessPaths).length) {
-                                    const full = { ...options, _width: 300, title: dropFileName };
-                                    const pathname = Utils.prepend(accessPaths.agnostic.client);
-                                    Docs.Get.DocumentFromType(type, pathname, full).then(doc => {
-                                        if (doc) {
-                                            const proto = Doc.GetProto(doc);
-                                            proto.fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, "");
-                                            nativeWidth && (proto["data-nativeWidth"] = nativeWidth);
-                                            nativeHeight && (proto["data-nativeHeight"] = nativeHeight);
-                                            contentSize && (proto.contentSize = contentSize);
-                                            this.props?.addDocument(doc);
-                                        }
-                                    });
-                                } else {
-                                    alert("Upload failed...");
-                                }
-                            }));
-                        }));
+                        file && file.type && files.push(file);
                     }
                 }
-
+                (await Networking.UploadFilesToServer(files)).forEach(({ source: { name, type }, result }) => {
+                    if (result instanceof Error) {
+                        alert(`Upload failed: ${result.message}`);
+                        return;
+                    }
+                    const full = { ...options, _width: 300, title: name };
+                    const pathname = Utils.prepend(result.accessPaths.agnostic.client);
+                    Docs.Get.DocumentFromType(type, pathname, full).then(doc => {
+                        if (doc) {
+                            const proto = Doc.GetProto(doc);
+                            proto.fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, "");
+                            if (Upload.isImageInformation(result)) {
+                                proto["data-nativeWidth"] = result.nativeWidth;
+                                proto["data-nativeHeight"] = result.nativeHeight;
+                                proto.contentSize = result.contentSize;
+                            }
+                            this.props?.addDocument(doc);
+                        }
+                    });
+                });
                 if (promises.length) {
                     Promise.all(promises).finally(() => { completed && completed(); batch.end(); });
                 } else {
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 55c0660c0..a722f552e 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -112,10 +112,10 @@ export class Doc extends RefField {
             // getPrototypeOf: (target) => Cast(target[SelfProxy].proto, Doc) || null, // TODO this might be able to replace the proto logic in getter
             has: (target, key) => key in target.__fields,
             ownKeys: target => {
-                let obj = {} as any;
+                const obj = {} as any;
                 Object.assign(obj, target.___fields);
                 runInAction(() => obj.__LAYOUT__ = target.__LAYOUT__);
-                return Object.keys(obj)
+                return Object.keys(obj);
             },
             getOwnPropertyDescriptor: (target, prop) => {
                 if (prop.toString() === "__LAYOUT__") {
@@ -864,7 +864,7 @@ Scripting.addGlobal(function redo() { return UndoManager.Redo(); });
 Scripting.addGlobal(function curPresentationItem() {
     const curPres = Doc.UserDoc().curPresentation as Doc;
     return curPres && DocListCast(curPres[Doc.LayoutFieldKey(curPres)])[NumCast(curPres._itemIndex)];
-})
+});
 Scripting.addGlobal(function selectDoc(doc: any) { Doc.UserDoc().SelectedDocs = new List([doc]); });
 Scripting.addGlobal(function selectedDocs(container: Doc, excludeCollections: boolean, prevValue: any) {
     const docs = DocListCast(Doc.UserDoc().SelectedDocs).filter(d => !Doc.AreProtosEqual(d, container) && !d.annotationOn && d.type !== DocumentType.DOCUMENT && d.type !== DocumentType.KVP && (!excludeCollections || !Cast(d.data, listSpec(Doc), null)));
diff --git a/src/scraping/buxton/final/BuxtonImporter.ts b/src/scraping/buxton/final/BuxtonImporter.ts
index 47d6bbe83..8041343fd 100644
--- a/src/scraping/buxton/final/BuxtonImporter.ts
+++ b/src/scraping/buxton/final/BuxtonImporter.ts
@@ -8,6 +8,7 @@ const StreamZip = require('node-stream-zip');
 const createImageSizeStream = require("image-size-stream");
 import { parseXml } from "libxmljs";
 import { strictEqual } from "assert";
+import { Readable, PassThrough } from "stream";
 
 interface DocumentContents {
     body: string;
@@ -293,15 +294,15 @@ async function writeImages(zip: any): Promise<ImageData[]> {
 
     const imageUrls: ImageData[] = [];
     for (const mediaPath of imageEntries) {
-        const streamImage = () => new Promise<any>((resolve, reject) => {
+        const streamImage = () => new Promise<Readable>((resolve, reject) => {
             zip.stream(mediaPath, (error: any, stream: any) => error ? reject(error) : resolve(stream));
         });
 
         const { width, height, type } = await new Promise<Dimensions>(async resolve => {
-            const sizeStream = createImageSizeStream().on('size', (dimensions: Dimensions) => {
+            const sizeStream = (createImageSizeStream() as PassThrough).on('size', (dimensions: Dimensions) => {
                 readStream.destroy();
                 resolve(dimensions);
-            });
+            }).on("error", () => readStream.destroy());
             const readStream = await streamImage();
             readStream.pipe(sizeStream);
         });
diff --git a/src/server/ApiManagers/GooglePhotosManager.ts b/src/server/ApiManagers/GooglePhotosManager.ts
index 04b724f4b..25c54ee2e 100644
--- a/src/server/ApiManagers/GooglePhotosManager.ts
+++ b/src/server/ApiManagers/GooglePhotosManager.ts
@@ -8,6 +8,7 @@ import { Opt } from "../../new_fields/Doc";
 import { DashUploadUtils, InjectSize, SizeSuffix } from "../DashUploadUtils";
 import { Database } from "../database";
 import { red } from "colors";
+import { Upload } from "../SharedMediaTypes";
 
 const prefix = "google_photos_";
 const remoteUploadError = "None of the preliminary uploads to Google's servers was successful.";
@@ -139,7 +140,7 @@ export default class GooglePhotosManager extends ApiManager {
                     return;
                 }
                 let failed = 0;
-                const completed: Opt<DashUploadUtils.ImageUploadInformation>[] = [];
+                const completed: Opt<Upload.ImageInformation>[] = [];
                 for (const { baseUrl } of mediaItems) {
                     // start by getting the content size of the remote image
                     const results = await DashUploadUtils.InspectImage(baseUrl);
@@ -151,7 +152,7 @@ export default class GooglePhotosManager extends ApiManager {
                     const { contentSize, ...attributes } = results;
                     // check to see if we have uploaded a Google user content image *specifically via this route* already
                     // that has this exact content size
-                    const found: Opt<DashUploadUtils.ImageUploadInformation> = await Database.Auxiliary.QueryUploadHistory(contentSize);
+                    const found: Opt<Upload.ImageInformation> = await Database.Auxiliary.QueryUploadHistory(contentSize);
                     if (!found) {
                         // if we haven't, then upload it locally to Dash's server
                         const upload = await DashUploadUtils.UploadInspectedImage({ contentSize, ...attributes }, undefined, prefix, false).catch(error => _error(res, downloadError, error));
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 4d09528f4..8f2a5ea3e 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -4,12 +4,12 @@ import * as formidable from 'formidable';
 import v4 = require('uuid/v4');
 const AdmZip = require('adm-zip');
 import { extname, basename, dirname } from 'path';
-import { createReadStream, createWriteStream, unlink, readFileSync } from "fs";
+import { createReadStream, createWriteStream, unlink } from "fs";
 import { publicDirectory, filesDirectory } from "..";
 import { Database } from "../database";
-import { DashUploadUtils, SizeSuffix } from "../DashUploadUtils";
+import { DashUploadUtils } from "../DashUploadUtils";
 import * as sharp from 'sharp';
-import { AcceptibleMedia } from "../SharedMediaTypes";
+import { AcceptibleMedia, Upload } from "../SharedMediaTypes";
 import { normalize } from "path";
 const imageDataUri = require('image-data-uri');
 
@@ -47,7 +47,7 @@ export default class UploadManager extends ApiManager {
                 form.keepExtensions = true;
                 return new Promise<void>(resolve => {
                     form.parse(req, async (_err, _fields, files) => {
-                        const results: any[] = [];
+                        const results: Upload.FileResponse[] = [];
                         for (const key in files) {
                             const result = await DashUploadUtils.upload(files[key]);
                             result && results.push(result);
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 913ddc1c3..b66651ef2 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -3,9 +3,9 @@ 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 { ExifImage } from 'exif';
 import { Opt } from '../new_fields/Doc';
-import { AcceptibleMedia } from './SharedMediaTypes';
+import { AcceptibleMedia, Upload } from './SharedMediaTypes';
 import { filesDirectory } from '.';
 import { File } from 'formidable';
 import { basename } from "path";
@@ -14,7 +14,7 @@ import { ParsedPDF } from "../server/PdfTypes";
 const parse = require('pdf-parse');
 import { Directory, serverPathToFile, clientPathToFile, pathToDirectory } from './ApiManagers/UploadManager';
 import { red } from 'colors';
-import { Writable } from 'stream';
+import { Stream } from 'stream';
 const requestImageSize = require("../client/util/request-image-size");
 
 export enum SizeSuffix {
@@ -40,13 +40,6 @@ export namespace DashUploadUtils {
         suffix: SizeSuffix;
     }
 
-    export interface ImageFileResponse {
-        name: string;
-        path: string;
-        type: string;
-        exif: Opt<DashUploadUtils.EnrichedExifData>;
-    }
-
     export const Sizes: { [size: string]: Size } = {
         SMALL: { width: 100, suffix: SizeSuffix.Small },
         MEDIUM: { width: 400, suffix: SizeSuffix.Medium },
@@ -60,20 +53,9 @@ export namespace DashUploadUtils {
     const size = "content-length";
     const type = "content-type";
 
-    export interface ImageUploadInformation {
-        accessPaths: AccessPathInfo;
-        exifData: EnrichedExifData;
-        contentSize?: number;
-        contentType?: string;
-    }
-
-    export interface AccessPathInfo {
-        [suffix: string]: { client: string, server: string };
-    }
-
     const { imageFormats, videoFormats, applicationFormats } = AcceptibleMedia;
 
-    export async function upload(file: File): Promise<any> {
+    export async function upload(file: File): Promise<Upload.FileResponse> {
         const { type, path, name } = file;
         const types = type.split("/");
 
@@ -83,33 +65,33 @@ export namespace DashUploadUtils {
         switch (category) {
             case "image":
                 if (imageFormats.includes(format)) {
-                    const results = await UploadImage(path, basename(path));
-                    return { ...results, name, type };
+                    const result = await UploadImage(path, basename(path));
+                    return { source: file, result };
                 }
             case "video":
                 if (videoFormats.includes(format)) {
-                    return MoveParsedFile(path, Directory.videos);
+                    return MoveParsedFile(file, Directory.videos);
                 }
             case "application":
                 if (applicationFormats.includes(format)) {
-                    return UploadPdf(path);
+                    return UploadPdf(file);
                 }
         }
 
         console.log(red(`Ignoring unsupported file (${name}) with upload type (${type}).`));
-        return { accessPaths: {} };
+        return { source: file, result: new Error(`Could not upload unsupported file (${name}) with upload type (${type}).`) };
     }
 
-    async function UploadPdf(absolutePath: string) {
-        const dataBuffer = readFileSync(absolutePath);
+    async function UploadPdf(file: File) {
+        const { path, name } = file;
+        const dataBuffer = readFileSync(path);
         const result: ParsedPDF = await parse(dataBuffer);
-        const parsedName = basename(absolutePath);
         await new Promise<void>((resolve, reject) => {
-            const textFilename = `${parsedName.substring(0, parsedName.length - 4)}.txt`;
+            const textFilename = `${name.substring(0, name.length - 4)}.txt`;
             const writeStream = createWriteStream(serverPathToFile(Directory.text, textFilename));
             writeStream.write(result.text, error => error ? reject(error) : resolve());
         });
-        return MoveParsedFile(absolutePath, Directory.pdfs);
+        return MoveParsedFile(file, Directory.pdfs);
     }
 
     /**
@@ -123,13 +105,13 @@ export namespace DashUploadUtils {
      * @param {string} prefix is a string prepended to the generated image name in the
      * event that @param filename is not specified
      * 
-     * @returns {ImageUploadInformation} This method returns
+     * @returns {ImageUploadInformation | Error} This method returns
      * 1) the paths to the uploaded images (plural due to resizing)
-     * 2) the file name of each of the resized images
+     * 2) the exif data embedded in the image, or the error explaining why exif couldn't be parsed
      * 3) the size of the image, in bytes (4432130)
      * 4) the content type of the image, i.e. image/(jpeg | png | ...)
      */
-    export const UploadImage = async (source: string, filename?: string, prefix: string = ""): Promise<ImageUploadInformation | Error> => {
+    export const UploadImage = async (source: string, filename?: string, prefix: string = ""): Promise<Upload.ImageInformation | Error> => {
         const metadata = await InspectImage(source);
         if (metadata instanceof Error) {
             return metadata;
@@ -137,22 +119,6 @@ export namespace DashUploadUtils {
         return UploadInspectedImage(metadata, filename || metadata.filename, prefix);
     };
 
-    export interface InspectionResults {
-        source: string;
-        requestable: string;
-        exifData: EnrichedExifData;
-        contentSize: number;
-        contentType: string;
-        nativeWidth: number;
-        nativeHeight: number;
-        filename?: string;
-    }
-
-    export interface EnrichedExifData {
-        data: ExifData;
-        error?: string;
-    }
-
     export async function buildFileDirectories() {
         const pending = Object.keys(Directory).map(sub => createIfNotExists(`${filesDirectory}/${sub}`));
         return Promise.all(pending);
@@ -175,7 +141,7 @@ export namespace DashUploadUtils {
      * 
      * @param source is the path or url to the image in question
      */
-    export const InspectImage = async (source: string): Promise<InspectionResults | Error> => {
+    export const InspectImage = async (source: string): Promise<Upload.InspectionResults | Error> => {
         let rawMatches: RegExpExecArray | null;
         let filename: string | undefined;
         if ((rawMatches = /^data:image\/([a-z]+);base64,(.*)/.exec(source)) !== null) {
@@ -216,14 +182,17 @@ export namespace DashUploadUtils {
         };
     };
 
-    export async function MoveParsedFile(absolutePath: string, destination: Directory): Promise<Opt<{ accessPaths: AccessPathInfo }>> {
+    export async function MoveParsedFile(file: File, destination: Directory): Promise<Upload.FileResponse> {
+        const { name, path: sourcePath } = file;
         return new Promise(resolve => {
-            const filename = basename(absolutePath);
-            const destinationPath = serverPathToFile(destination, filename);
-            rename(absolutePath, destinationPath, error => {
-                resolve(error ? undefined : {
-                    accessPaths: {
-                        agnostic: getAccessPaths(destination, filename)
+            const destinationPath = serverPathToFile(destination, name);
+            rename(sourcePath, destinationPath, error => {
+                resolve({
+                    source: file,
+                    result: error ? error : {
+                        accessPaths: {
+                            agnostic: getAccessPaths(destination, name)
+                        }
                     }
                 });
             });
@@ -237,16 +206,16 @@ export namespace DashUploadUtils {
         };
     }
 
-    export const UploadInspectedImage = async (metadata: InspectionResults, filename?: string, prefix = "", cleanUp = true): Promise<ImageUploadInformation> => {
+    export const UploadInspectedImage = async (metadata: Upload.InspectionResults, filename?: string, prefix = "", cleanUp = true): Promise<Upload.ImageInformation> => {
         const { requestable, source, ...remaining } = metadata;
         const extension = `.${remaining.contentType.split("/")[1].toLowerCase()}`;
         const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}${extension}`;
         const { images } = Directory;
-        const information: ImageUploadInformation = {
+        const information: Upload.ImageInformation = {
             accessPaths: {
                 agnostic: getAccessPaths(images, resolved)
             },
-            ...remaining
+            ...metadata
         };
         const outputPath = pathToDirectory(Directory.images);
         const writtenFiles = await outputResizedImages(() => request(requestable), outputPath, resolved, extension);
@@ -259,9 +228,9 @@ export namespace DashUploadUtils {
         return information;
     };
 
-    const parseExifData = async (source: string): Promise<EnrichedExifData> => {
+    const parseExifData = async (source: string): Promise<Upload.EnrichedExifData> => {
         const image = await request.get(source, { encoding: null });
-        return new Promise<EnrichedExifData>(resolve => {
+        return new Promise(resolve => {
             new ExifImage({ image }, (error, data) => {
                 let reason: Opt<string> = undefined;
                 if (error) {
@@ -279,18 +248,14 @@ export namespace DashUploadUtils {
         force: true
     };
 
-    export interface ReadStreamLike {
-        pipe: (dest: Writable) => Writable;
-    }
-
-    export async function outputResizedImages(readStreamSource: () => ReadStreamLike | Promise<ReadStreamLike>, outputPath: string, fileName: string, ext: string) {
+    export async function outputResizedImages(readStreamSource: () => Stream | Promise<Stream>, outputPath: string, fileName: string, ext: string) {
         const writtenFiles: { [suffix: string]: string } = {};
         for (const { resizer, suffix } of resizers(ext)) {
             const resolved = writtenFiles[suffix] = InjectSize(fileName, suffix);
             await new Promise<void>(async (resolve, reject) => {
                 const writeStream = createWriteStream(path.resolve(outputPath, resolved));
-                let readStream: ReadStreamLike;
                 const source = readStreamSource();
+                let readStream: Stream;
                 if (source instanceof Promise) {
                     readStream = await source;
                 } else {
@@ -320,7 +285,7 @@ export namespace DashUploadUtils {
                     initial = initial.webp();
                 } else if (tiffs.includes(ext)) {
                     initial = initial.tiff();
-                } else {
+                } else if (ext === ".gif") {
                     initial = undefined;
                 }
                 return {
diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts
index 274b4f01e..185e787cc 100644
--- a/src/server/SharedMediaTypes.ts
+++ b/src/server/SharedMediaTypes.ts
@@ -1,3 +1,6 @@
+import { ExifData } from 'exif';
+import { File } from 'formidable';
+
 export namespace AcceptibleMedia {
     export const gifs = [".gif"];
     export const pngs = [".png"];
@@ -7,4 +10,40 @@ export namespace AcceptibleMedia {
     export const imageFormats = [...pngs, ...jpgs, ...gifs, ...webps, ...tiffs];
     export const videoFormats = [".mov", ".mp4"];
     export const applicationFormats = [".pdf"];
+}
+
+export namespace Upload {
+
+    export function isImageInformation(uploadResponse: Upload.FileInformation): uploadResponse is Upload.ImageInformation {
+        return "nativeWidth" in uploadResponse;
+    }
+
+    export interface FileInformation {
+        accessPaths: AccessPathInfo;
+    }
+
+    export type FileResponse<T extends FileInformation = FileInformation> = { source: File, result: T | Error };
+
+    export type ImageInformation = FileInformation & InspectionResults;
+
+    export interface AccessPathInfo {
+        [suffix: string]: { client: string, server: string };
+    }
+
+    export interface InspectionResults {
+        source: string;
+        requestable: string;
+        exifData: EnrichedExifData;
+        contentSize: number;
+        contentType: string;
+        nativeWidth: number;
+        nativeHeight: number;
+        filename?: string;
+    }
+
+    export interface EnrichedExifData {
+        data: ExifData;
+        error?: string;
+    }
+
 }
\ No newline at end of file
diff --git a/src/server/database.ts b/src/server/database.ts
index 83ce865c6..055f04c49 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -2,12 +2,12 @@ import * as mongodb from 'mongodb';
 import { Transferable } from './Message';
 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';
 import { IDatabase } from './IDatabase';
 import { MemoryDatabase } from './MemoryDatabase';
 import * as mongoose from 'mongoose';
+import { Upload } from './SharedMediaTypes';
 
 export namespace Database {
 
@@ -297,7 +297,7 @@ export namespace Database {
         };
 
         export const QueryUploadHistory = async (contentSize: number) => {
-            return SanitizedSingletonQuery<DashUploadUtils.ImageUploadInformation>({ contentSize }, AuxiliaryCollections.GooglePhotosUploadHistory);
+            return SanitizedSingletonQuery<Upload.ImageInformation>({ contentSize }, AuxiliaryCollections.GooglePhotosUploadHistory);
         };
 
         export namespace GoogleAuthenticationToken {
@@ -326,7 +326,7 @@ export namespace Database {
 
         }
 
-        export const LogUpload = async (information: DashUploadUtils.ImageUploadInformation) => {
+        export const LogUpload = async (information: Upload.ImageInformation) => {
             const bundle = {
                 _id: Utils.GenerateDeterministicGuid(String(information.contentSize!)),
                 ...information
-- 
cgit v1.2.3-70-g09d2

