aboutsummaryrefslogtreecommitdiff
path: root/src/server/DashUploadUtils.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/DashUploadUtils.ts')
-rw-r--r--src/server/DashUploadUtils.ts248
1 files changed, 153 insertions, 95 deletions
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index cb7104757..2af816df8 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -1,11 +1,11 @@
-import { unlinkSync, createWriteStream, readFileSync, rename } from 'fs';
+import { unlinkSync, createWriteStream, readFileSync, rename, writeFile } from 'fs';
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,6 +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 { Stream } from 'stream';
const requestImageSize = require("../client/util/request-image-size");
export enum SizeSuffix {
@@ -39,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 },
@@ -59,56 +53,62 @@ export namespace DashUploadUtils {
const size = "content-length";
const type = "content-type";
- export interface ImageUploadInformation {
- clientAccessPath: string;
- serverAccessPaths: { [key: string]: string };
- exifData: EnrichedExifData;
- contentSize?: number;
- contentType?: string;
- }
-
- const { imageFormats, videoFormats, applicationFormats } = AcceptibleMedia;
+ const { imageFormats, videoFormats, applicationFormats, audioFormats } = 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("/");
const category = types[0];
- const format = `.${types[1]}`;
+ let format = `.${types[1]}`;
switch (category) {
case "image":
if (imageFormats.includes(format)) {
- const results = await UploadImage(path, basename(path), format);
- 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);
+ }
+ case "audio":
+ const components = format.split(";");
+ if (components.length > 1) {
+ format = components[0];
+ }
+ if (audioFormats.includes(format)) {
+ return UploadAudio(file, format);
}
}
console.log(red(`Ignoring unsupported file (${name}) with upload type (${type}).`));
- return { clientAccessPath: undefined };
+ 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: sourcePath } = file;
+ const dataBuffer = readFileSync(sourcePath);
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 name = path.basename(sourcePath);
+ 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);
}
- const generate = (prefix: string, extension: string) => `${prefix}upload_${Utils.GenerateGuid()}.${extension}`;
+ const manualSuffixes = [".webm"];
+
+ async function UploadAudio(file: File, format: string) {
+ const suffix = manualSuffixes.includes(format) ? format : undefined;
+ return MoveParsedFile(file, Directory.audio, suffix);
+ }
/**
* Uploads an image specified by the @param source to Dash's /public/files/
@@ -121,32 +121,20 @@ 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, format?: string, prefix: string = ""): Promise<ImageUploadInformation> => {
+ export const UploadImage = async (source: string, filename?: string, prefix: string = ""): Promise<Upload.ImageInformation | Error> => {
const metadata = await InspectImage(source);
- return UploadInspectedImage(metadata, filename, format, prefix);
+ if (metadata instanceof Error) {
+ return metadata;
+ }
+ return UploadInspectedImage(metadata, filename || metadata.filename, prefix);
};
- export interface InspectionResults {
- source: string;
- requestable: string;
- exifData: EnrichedExifData;
- contentSize: number;
- contentType: string;
- nativeWidth: number;
- nativeHeight: number;
- }
-
- 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);
@@ -158,13 +146,31 @@ export namespace DashUploadUtils {
type: string;
}
+ export interface ImageResizer {
+ resizer?: sharp.Sharp;
+ suffix: SizeSuffix;
+ }
+
/**
* Based on the url's classification as local or remote, gleans
* as much information as possible about the specified image
*
* @param source is the path or url to the image in question
*/
- export const InspectImage = async (source: string): Promise<InspectionResults> => {
+ 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) {
+ const [ext, data] = rawMatches.slice(1, 3);
+ const resolved = filename = `upload_${Utils.GenerateGuid()}.${ext}`;
+ const error = await new Promise<Error | null>(resolve => {
+ writeFile(serverPathToFile(Directory.images, resolved), data, "base64", resolve);
+ });
+ if (error !== null) {
+ return error;
+ }
+ source = `http://localhost:1050${clientPathToFile(Directory.images, resolved)}`;
+ }
let resolvedUrl: string;
const matches = isLocal().exec(source);
if (matches === null) {
@@ -187,62 +193,62 @@ export namespace DashUploadUtils {
contentType: headers[type],
nativeWidth,
nativeHeight,
+ filename,
...results
};
};
- export async function MoveParsedFile(absolutePath: string, destination: Directory): Promise<{ clientAccessPath: Opt<string> }> {
- return new Promise<{ clientAccessPath: Opt<string> }>(resolve => {
- const filename = basename(absolutePath);
- const destinationPath = serverPathToFile(destination, filename);
- rename(absolutePath, destinationPath, error => {
- resolve({ clientAccessPath: error ? undefined : clientPathToFile(destination, filename) });
+ export async function MoveParsedFile(file: File, destination: Directory, suffix: string | undefined = undefined): Promise<Upload.FileResponse> {
+ const { path: sourcePath } = file;
+ let name = path.basename(sourcePath);
+ suffix && (name += suffix);
+ return new Promise(resolve => {
+ const destinationPath = serverPathToFile(destination, name);
+ rename(sourcePath, destinationPath, error => {
+ resolve({
+ source: file,
+ result: error ? error : {
+ accessPaths: {
+ agnostic: getAccessPaths(destination, name)
+ }
+
+ }
+ }
+ );
});
});
}
- export const UploadInspectedImage = async (metadata: InspectionResults, filename?: string, format?: string, prefix = ""): Promise<ImageUploadInformation> => {
+ function getAccessPaths(directory: Directory, fileName: string) {
+ return {
+ client: clientPathToFile(directory, fileName),
+ server: serverPathToFile(directory, fileName)
+ };
+ }
+
+ export const UploadInspectedImage = async (metadata: Upload.InspectionResults, filename?: string, prefix = "", cleanUp = true): Promise<Upload.ImageInformation> => {
const { requestable, source, ...remaining } = metadata;
- const extension = remaining.contentType.toLowerCase().split("/")[1]; //format || sanitizeExtension(requestable || resolved);
- const resolved = filename || generate(prefix, extension);
- const information: ImageUploadInformation = {
- clientAccessPath: clientPathToFile(Directory.images, resolved),
- serverAccessPaths: {},
- ...remaining
+ const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}.${remaining.contentType.split("/")[1].toLowerCase()}`;
+ const { images } = Directory;
+ const information: Upload.ImageInformation = {
+ accessPaths: {
+ agnostic: getAccessPaths(images, resolved)
+ },
+ ...metadata
};
- const { pngs, jpgs } = AcceptibleMedia;
- return new Promise<ImageUploadInformation>(async (resolve, reject) => {
- const resizers = [
- { resizer: sharp().rotate(), suffix: SizeSuffix.Original },
- ...Object.values(Sizes).map(size => ({
- resizer: sharp().resize(size.width, undefined, { withoutEnlargement: true }).rotate(),
- suffix: size.suffix
- }))
- ];
- if (pngs.includes(extension)) {
- resizers.forEach(element => element.resizer = element.resizer.png());
- } else if (jpgs.includes(extension)) {
- resizers.forEach(element => element.resizer = element.resizer.jpeg());
- }
- for (const { resizer, suffix } of resizers) {
- await new Promise<void>(resolve => {
- const filename = InjectSize(resolved, suffix);
- information.serverAccessPaths[suffix] = serverPathToFile(Directory.images, filename);
- request(requestable).pipe(resizer).pipe(createWriteStream(serverPathToFile(Directory.images, filename)))
- .on('close', resolve)
- .on('error', reject);
- });
- }
- if (isLocal().test(source)) {
- unlinkSync(source);
- }
- resolve(information);
- });
+ const writtenFiles = await outputResizedImages(() => request(requestable), resolved, pathToDirectory(Directory.images));
+ for (const suffix of Object.keys(writtenFiles)) {
+ information.accessPaths[suffix] = getAccessPaths(images, writtenFiles[suffix]);
+ }
+ if (isLocal().test(source) && cleanUp) {
+ unlinkSync(source);
+ }
+ 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) {
@@ -253,4 +259,56 @@ export namespace DashUploadUtils {
});
};
+ const { pngs, jpgs, webps, tiffs } = AcceptibleMedia;
+ const pngOptions = {
+ compressionLevel: 9,
+ adaptiveFiltering: true,
+ force: true
+ };
+
+ export async function outputResizedImages(streamProvider: () => Stream | Promise<Stream>, outputFileName: string, outputDirectory: string) {
+ const writtenFiles: { [suffix: string]: string } = {};
+ for (const { resizer, suffix } of resizers(path.extname(outputFileName))) {
+ const outputPath = path.resolve(outputDirectory, writtenFiles[suffix] = InjectSize(outputFileName, suffix));
+ await new Promise<void>(async (resolve, reject) => {
+ const source = streamProvider();
+ let readStream: Stream;
+ if (source instanceof Promise) {
+ readStream = await source;
+ } else {
+ readStream = source;
+ }
+ if (resizer) {
+ readStream = readStream.pipe(resizer.withMetadata());
+ }
+ readStream.pipe(createWriteStream(outputPath)).on("close", resolve).on("error", reject);
+ });
+ }
+ return writtenFiles;
+ }
+
+ function resizers(ext: string): DashUploadUtils.ImageResizer[] {
+ return [
+ { suffix: SizeSuffix.Original },
+ ...Object.values(DashUploadUtils.Sizes).map(({ suffix, width }) => {
+ let initial: sharp.Sharp | undefined = sharp().resize(width, undefined, { withoutEnlargement: true });
+ if (pngs.includes(ext)) {
+ initial = initial.png(pngOptions);
+ } else if (jpgs.includes(ext)) {
+ initial = initial.jpeg();
+ } else if (webps.includes(ext)) {
+ initial = initial.webp();
+ } else if (tiffs.includes(ext)) {
+ initial = initial.tiff();
+ } else if (ext === ".gif") {
+ initial = undefined;
+ }
+ return {
+ resizer: initial,
+ suffix
+ };
+ })
+ ];
+ }
+
} \ No newline at end of file