diff options
Diffstat (limited to 'src/server/DashUploadUtils.ts')
-rw-r--r-- | src/server/DashUploadUtils.ts | 248 |
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 |