IO-1998 LMS Zip Download.

This commit is contained in:
Patrick Fic
2022-07-20 14:17:03 -07:00
parent 37349c7481
commit 3d0791f633
7 changed files with 138 additions and 9 deletions

77
jobs/jobsDownloadMedia.ts Normal file
View File

@@ -0,0 +1,77 @@
import { Request, Response } from "express";
import fs from "fs-extra";
import multer from "multer";
import path from "path";
import { logger } from "../server";
import GenerateThumbnail from "../util/generateThumbnail";
import generateUniqueFilename from "../util/generateUniqueFilename";
import { ConvertHeicFiles } from "../util/heicConverter";
import { PathToRoFolder } from "../util/pathGenerators";
import { JobsListMedia } from "./jobsListMedia";
import JSZip from "jszip";
import ListableChecker from "../util/listableChecker";
import { JobRelativeFilePath } from "../util/serverInit";
//param: files: string[] | array of filenames.
export async function jobsDownloadMedia(req: Request, res: Response) {
const jobid: string = (req.body.jobid || "").trim();
try {
//Do we need all files or just some files?
const files: string[] = req.body.files || [];
const zip: JSZip = new JSZip();
await fs.ensureDir(PathToRoFolder(jobid));
logger.debug(`Generating batch download for Job ID ${jobid}`, files);
//Prepare the zip file.
const filesList: fs.Dirent[] = (
await fs.readdir(PathToRoFolder(jobid), {
withFileTypes: true,
})
).filter((f) => f.isFile() && ListableChecker(f));
if (files.length === 0) {
//Get everything.
await Promise.all(
filesList.map(async (file) => {
//Do something async
const fileOnDisk: Buffer = await fs.readFile(
JobRelativeFilePath(jobid, file.name)
);
zip.file(path.parse(path.basename(file.name)).base, fileOnDisk);
})
);
} else {
//Get the files that are in the list and see which are requested.
await Promise.all(
filesList.map(async (file) => {
if (files.includes(path.parse(path.basename(file.name)).base)) {
// File is in the set of requested files.
const fileOnDisk: Buffer = await fs.readFile(
JobRelativeFilePath(jobid, file.name)
);
zip.file(path.parse(path.basename(file.name)).base, fileOnDisk);
}
})
);
}
//Send it as a response to download it automatically.
// res.setHeader("Content-disposition", "attachment; filename=" + filename);
zip
.generateNodeStream({
type: "nodebuffer",
streamFiles: true,
//encodeFileName: (filename) => `${jobid}.zip`,
})
.pipe(res);
} catch (error) {
logger.error("Error downloading job media.", {
jobid,
error: (error as Error).message,
});
res.status(500).json((error as Error).message);
}
}

View File

@@ -7,7 +7,7 @@ import MediaFile from "../util/interfaces/MediaFile";
import ListableChecker from "../util/listableChecker"; import ListableChecker from "../util/listableChecker";
import GenerateUrl from "../util/MediaUrlGen"; import GenerateUrl from "../util/MediaUrlGen";
import { PathToRoFolder } from "../util/pathGenerators"; import { PathToRoFolder } from "../util/pathGenerators";
import { FolderPaths } from "../util/serverInit"; import { FolderPaths, JobRelativeFilePath } from "../util/serverInit";
export async function JobsListMedia(req: Request, res: Response) { export async function JobsListMedia(req: Request, res: Response) {
const jobid: string = (req.body.jobid || "").trim(); const jobid: string = (req.body.jobid || "").trim();
@@ -19,8 +19,13 @@ export async function JobsListMedia(req: Request, res: Response) {
//We just uploaded files, we're going to send only those back. //We just uploaded files, we're going to send only those back.
ret = await Promise.all( ret = await Promise.all(
(req.files as Express.Multer.File[]).map(async (file) => { (req.files as Express.Multer.File[]).map(async (file) => {
const relativeFilePath: string = JobRelativeFilePath(
jobid,
file.filename
);
const relativeThumbPath: string = await GenerateThumbnail( const relativeThumbPath: string = await GenerateThumbnail(
path.join(FolderPaths.Jobs, jobid, file.filename) relativeFilePath
); );
return { return {
src: GenerateUrl([ src: GenerateUrl([
@@ -38,6 +43,7 @@ export async function JobsListMedia(req: Request, res: Response) {
thumbnailHeight: 250, thumbnailHeight: 250,
thumbnailWidth: 250, thumbnailWidth: 250,
filename: file.filename, filename: file.filename,
relativeFilePath,
}; };
}) })
); );
@@ -46,15 +52,17 @@ export async function JobsListMedia(req: Request, res: Response) {
await fs.readdir(PathToRoFolder(jobid), { await fs.readdir(PathToRoFolder(jobid), {
withFileTypes: true, withFileTypes: true,
}) })
).filter( ).filter((f) => f.isFile() && ListableChecker(f));
(f) =>
f.isFile() && !/(^|\/)\.[^\/\.]/g.test(f.name) && ListableChecker(f)
);
ret = await Promise.all( ret = await Promise.all(
filesList.map(async (file) => { filesList.map(async (file) => {
const relativeFilePath: string = JobRelativeFilePath(
jobid,
file.name
);
const relativeThumbPath: string = await GenerateThumbnail( const relativeThumbPath: string = await GenerateThumbnail(
path.join(FolderPaths.Jobs, jobid, file.name) relativeFilePath
); );
return { return {
src: GenerateUrl([ src: GenerateUrl([
@@ -72,6 +80,7 @@ export async function JobsListMedia(req: Request, res: Response) {
thumbnailHeight: 250, thumbnailHeight: 250,
thumbnailWidth: 250, thumbnailWidth: 250,
filename: file.name, filename: file.name,
relativeFilePath,
}; };
}) })
); );

View File

@@ -24,6 +24,7 @@
"heic-convert": "^1.2.4", "heic-convert": "^1.2.4",
"helmet": "^5.0.2", "helmet": "^5.0.2",
"image-thumbnail": "^1.0.14", "image-thumbnail": "^1.0.14",
"jszip": "^3.10.0",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "^1.4.4", "multer": "^1.4.4",
"response-time": "^2.3.2", "response-time": "^2.3.2",

View File

@@ -21,6 +21,7 @@ import {
BillsUploadMedia, BillsUploadMedia,
} from "./bills/billsUploadMedia"; } from "./bills/billsUploadMedia";
import ValidateImsToken from "./util/validateToken"; import ValidateImsToken from "./util/validateToken";
import { jobsDownloadMedia } from "./jobs/jobsDownloadMedia";
dotenv.config({ dotenv.config({
path: resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`), path: resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`),
@@ -129,6 +130,12 @@ app.post(
JobRequestValidator, JobRequestValidator,
jobsUploadMedia jobsUploadMedia
); );
app.post(
"/jobs/download",
ValidateImsToken,
JobRequestValidator,
jobsDownloadMedia
);
app.post( app.post(
"/jobs/move", //JobRequestValidator, "/jobs/move", //JobRequestValidator,
ValidateImsToken, ValidateImsToken,

View File

@@ -2,7 +2,7 @@ import fs from "fs-extra";
function ListableChecker(file: fs.Dirent) { function ListableChecker(file: fs.Dirent) {
if (file.name === "Thumbs.db") return false; if (file.name === "Thumbs.db") return false;
return true; if (!/(^|\/)\.[^\/\.]/g.test(file.name)) return true;
} }
export default ListableChecker; export default ListableChecker;

View File

@@ -24,6 +24,9 @@ export const FolderPaths = {
VendorsFolder, VendorsFolder,
}; };
export function JobRelativeFilePath(jobid: string, filename: string) {
return path.join(FolderPaths.Jobs, jobid, filename);
}
export default function InitServer() { export default function InitServer() {
logger.info(`Ensuring Root media path exists: ${FolderPaths.Root}`); logger.info(`Ensuring Root media path exists: ${FolderPaths.Root}`);
fs.ensureDirSync(FolderPaths.Root); fs.ensureDirSync(FolderPaths.Root);

View File

@@ -1132,6 +1132,11 @@ image-thumbnail@^1.0.14:
sharp "^0.28" sharp "^0.28"
validator "^13.0.0" validator "^13.0.0"
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
import-lazy@^2.1.0: import-lazy@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz" resolved "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz"
@@ -1282,6 +1287,16 @@ jsonfile@^6.0.1:
optionalDependencies: optionalDependencies:
graceful-fs "^4.1.6" graceful-fs "^4.1.6"
jszip@^3.10.0:
version "3.10.0"
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.0.tgz#faf3db2b4b8515425e34effcdbb086750a346061"
integrity sha512-LDfVtOLtOxb9RXkYOwPyNBTQDL4eUbqahtoY6x07GiDJHwSYvn8sHHIw8wINImV3MqbMNve2gSuM1DDqEKk09Q==
dependencies:
lie "~3.3.0"
pako "~1.0.2"
readable-stream "~2.3.6"
setimmediate "^1.0.5"
keyv@^3.0.0: keyv@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz" resolved "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz"
@@ -1306,6 +1321,13 @@ libheif-js@^1.10.0:
resolved "https://registry.yarnpkg.com/libheif-js/-/libheif-js-1.12.0.tgz#9ad1ed16a8e6412b4d3d83565d285465a00e7305" resolved "https://registry.yarnpkg.com/libheif-js/-/libheif-js-1.12.0.tgz#9ad1ed16a8e6412b4d3d83565d285465a00e7305"
integrity sha512-hDs6xQ7028VOwAFwEtM0Q+B2x2NW69Jb2MhQFUbk3rUrHzz4qo5mqS8VrqNgYnSc8TiUGnR691LnO4uIfEE23w== integrity sha512-hDs6xQ7028VOwAFwEtM0Q+B2x2NW69Jb2MhQFUbk3rUrHzz4qo5mqS8VrqNgYnSc8TiUGnR691LnO4uIfEE23w==
lie@~3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
dependencies:
immediate "~3.0.5"
logform@^2.3.2, logform@^2.4.0: logform@^2.3.2, logform@^2.4.0:
version "2.4.0" version "2.4.0"
resolved "https://registry.npmjs.org/logform/-/logform-2.4.0.tgz" resolved "https://registry.npmjs.org/logform/-/logform-2.4.0.tgz"
@@ -1598,6 +1620,11 @@ package-json@^6.3.0:
registry-url "^5.0.0" registry-url "^5.0.0"
semver "^6.2.0" semver "^6.2.0"
pako@~1.0.2:
version "1.0.11"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
parse-cache-control@^1.0.1: parse-cache-control@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz" resolved "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz"
@@ -1744,7 +1771,7 @@ readable-stream@1.1.x:
isarray "0.0.1" isarray "0.0.1"
string_decoder "~0.10.x" string_decoder "~0.10.x"
readable-stream@^2.0.6, readable-stream@^2.2.2: readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@~2.3.6:
version "2.3.7" version "2.3.7"
resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz"
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
@@ -1887,6 +1914,11 @@ set-blocking@~2.0.0:
resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz"
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==
setprototypeof@1.2.0: setprototypeof@1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz"