diff options
author | Sam Wilkins <35748010+samwilkins333@users.noreply.github.com> | 2019-10-09 16:59:11 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-10-09 16:59:11 -0400 |
commit | 062a938dae7e78b59aaf9a706d2b656cc4474ac6 (patch) | |
tree | dbbbfe1238a4babd3638132ea48f7e2332e7c540 /src | |
parent | 0890639c166df6558c3a5c0fd3d427157b3549de (diff) | |
parent | ae9584b6a0b588e0b854a7c5b56723e1a71d47d3 (diff) |
Merge pull request #293 from browngraphicslab/exif
Exif
Diffstat (limited to 'src')
-rw-r--r-- | src/client/documents/Documents.ts | 2 | ||||
-rw-r--r-- | src/client/util/Import & Export/DirectoryImportBox.tsx | 12 | ||||
-rw-r--r-- | src/client/util/Import & Export/ImageUtils.ts | 31 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSubView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/collections/CollectionView.tsx | 2 | ||||
-rw-r--r-- | src/server/DashUploadUtils.ts | 37 | ||||
-rw-r--r-- | src/server/RouteStore.ts | 2 | ||||
-rw-r--r-- | src/server/database.ts | 7 | ||||
-rw-r--r-- | src/server/index.ts | 92 |
9 files changed, 171 insertions, 16 deletions
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) => { |