aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Wilkins <35748010+samwilkins333@users.noreply.github.com>2019-10-09 16:59:11 -0400
committerGitHub <noreply@github.com>2019-10-09 16:59:11 -0400
commit062a938dae7e78b59aaf9a706d2b656cc4474ac6 (patch)
treedbbbfe1238a4babd3638132ea48f7e2332e7c540
parent0890639c166df6558c3a5c0fd3d427157b3549de (diff)
parentae9584b6a0b588e0b854a7c5b56723e1a71d47d3 (diff)
Merge pull request #293 from browngraphicslab/exif
Exif
-rw-r--r--package.json2
-rw-r--r--src/client/documents/Documents.ts2
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx12
-rw-r--r--src/client/util/Import & Export/ImageUtils.ts31
-rw-r--r--src/client/views/collections/CollectionSubView.tsx2
-rw-r--r--src/client/views/collections/CollectionView.tsx2
-rw-r--r--src/server/DashUploadUtils.ts37
-rw-r--r--src/server/RouteStore.ts2
-rw-r--r--src/server/database.ts7
-rw-r--r--src/server/index.ts92
10 files changed, 173 insertions, 16 deletions
diff --git a/package.json b/package.json
index 9d12f51b0..6e96f94d2 100644
--- a/package.json
+++ b/package.json
@@ -65,6 +65,7 @@
"@types/cookie-session": "^2.0.36",
"@types/d3-format": "^1.3.1",
"@types/dotenv": "^6.1.1",
+ "@types/exif": "^0.6.0",
"@types/express": "^4.16.1",
"@types/express-flash": "0.0.0",
"@types/express-session": "^1.15.12",
@@ -132,6 +133,7 @@
"crypto-browserify": "^3.11.0",
"d3-format": "^1.3.2",
"dotenv": "^8.0.0",
+ "exif": "^0.6.0",
"express": "^4.16.4",
"express-flash": "0.0.2",
"express-session": "^1.15.6",
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 34e146f0e..9d1a6ed3e 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -256,7 +256,7 @@ export namespace Docs {
let title = prototypeId.toUpperCase().replace(upper, `_${upper}`);
// synthesize the default options, the type and title from computed values and
// whatever options pertain to this specific prototype
- let options = { title: title, type: type, baseProto: true, ...defaultOptions, ...(template.options || {}) };
+ let options = { title, type, baseProto: true, ...defaultOptions, ...(template.options || {}) };
let primary = layout.view.LayoutString(layout.ext);
let collectionView = layout.collectionView;
if (collectionView) {
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index d3f81b992..d74b51993 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx
@@ -22,13 +22,15 @@ import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
import "./DirectoryImportBox.scss";
import { Identified } from "../../Network";
import { BatchedArray } from "array-batcher";
+import { ExifData } from "exif";
const unsupported = ["text/html", "text/plain"];
-interface FileResponse {
+interface ImageUploadResponse {
name: string;
path: string;
type: string;
+ exif: any;
}
@observer
@@ -117,7 +119,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
const responses = await Identified.PostFormDataToServer(RouteStore.upload, formData);
runInAction(() => this.completed += batch.length);
- return responses as FileResponse[];
+ return responses as ImageUploadResponse[];
});
await Promise.all(uploads.map(async upload => {
@@ -129,7 +131,11 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
title: upload.name
};
const document = await Docs.Get.DocumentFromType(type, path, options);
- document && docs.push(document);
+ const { data, error } = upload.exif;
+ if (document) {
+ Doc.GetProto(document).exif = error || Docs.Get.DocumentHierarchyFromJson(data);
+ docs.push(document);
+ }
}));
for (let i = 0; i < docs.length; i++) {
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
new file mode 100644
index 000000000..c9abf38fa
--- /dev/null
+++ b/src/client/util/Import & Export/ImageUtils.ts
@@ -0,0 +1,31 @@
+import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
+import { ImageField } from "../../../new_fields/URLField";
+import { Cast, StrCast } from "../../../new_fields/Types";
+import { RouteStore } from "../../../server/RouteStore";
+import { Docs } from "../../documents/Documents";
+import { Identified } from "../../Network";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { Utils } from "../../../Utils";
+
+export namespace ImageUtils {
+
+ export const ExtractExif = async (document: Doc): Promise<boolean> => {
+ const field = Cast(document.data, ImageField);
+ if (!field) {
+ return false;
+ }
+ const source = field.url.href;
+ const response = await Identified.PostToServer(RouteStore.inspectImage, { source });
+ const { error, data } = response.exifData;
+ document.exif = error || Docs.Get.DocumentHierarchyFromJson(data);
+ return data !== undefined;
+ };
+
+ export const ExportHierarchyToFileSystem = async (collection: Doc): Promise<void> => {
+ const a = document.createElement("a");
+ a.href = Utils.prepend(`${RouteStore.imageHierarchyExport}/${collection[Id]}`);
+ a.download = `Dash Export [${StrCast(collection.title)}].zip`;
+ a.click();
+ };
+
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 28d1eb384..2a536a0f4 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -24,6 +24,7 @@ import { CollectionView } from "./CollectionView";
import React = require("react");
var path = require('path');
import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
+import { ImageUtils } from "../../util/Import & Export/ImageUtils";
export interface CollectionViewProps extends FieldViewProps {
addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
@@ -194,6 +195,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
if (img) {
let split = img.split("src=\"")[1].split("\"")[0];
let doc = Docs.Create.ImageDocument(split, { ...options, width: 300 });
+ ImageUtils.ExtractExif(doc);
this.props.addDocument(doc, false);
return;
} else {
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 534246326..893763840 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -18,6 +18,7 @@ import { CollectionSchemaView } from "./CollectionSchemaView";
import { CollectionStackingView } from './CollectionStackingView';
import { CollectionTreeView } from "./CollectionTreeView";
import { CollectionViewBaseChrome } from './CollectionViewChromes';
+import { ImageUtils } from '../../util/Import & Export/ImageUtils';
export const COLLECTION_BORDER_WIDTH = 2;
library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy);
@@ -123,6 +124,7 @@ export class CollectionView extends React.Component<FieldViewProps> {
let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : [];
layoutItems.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" });
!existing && ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "hand-point-right" });
+ ContextMenu.Instance.addItem({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) });
}
}
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 57b46714a..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/');
@@ -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 391d9dc0c..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",
diff --git a/src/server/database.ts b/src/server/database.ts
index d2375ebd9..990441d5a 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -131,7 +131,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 +165,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 +179,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(() => {
diff --git a/src/server/index.ts b/src/server/index.ts
index dd44a0ce8..62938b9c7 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;
@@ -319,6 +321,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 +662,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 +677,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 +696,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 +707,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) => {