import { Request, Response } from "express"; import { fileTypeFromFile } from "file-type"; import { FileTypeResult } from "file-type/core"; import fs from "fs-extra"; import path from "path"; import GenerateUrl from "../util/MediaUrlGen.js"; import GenerateThumbnail from "../util/generateThumbnail.js"; import MediaFile from "../util/interfaces/MediaFile.js"; import ListableChecker from "../util/listableChecker.js"; import { PathToRoBillsFolder } from "../util/pathGenerators.js"; import { FolderPaths } from "../util/serverInit.js"; /** @description Bills will use the hierarchy of PDFs stored under the Job first, and then the Bills folder. */ export async function BillsListMedia(req: Request, res: Response) { const jobid: string = (req.body.jobid || "").trim(); const invoice_number: string = (req.body.invoice_number || "").trim(); await fs.ensureDir(PathToRoBillsFolder(jobid)); try { if (req.files) { const uploadedFiles = await processUploadedBillFiles(req.files as Express.Multer.File[], jobid); if (!res.headersSent) res.json(uploadedFiles); } else { const existingFiles = await processExistingBillFiles(jobid, invoice_number); if (!res.headersSent) res.json(existingFiles); } } catch (error) { // Optionally add logger here if you use one if (!res.headersSent) res.status(500).json(error); } } async function processUploadedBillFiles(files: Express.Multer.File[], jobid: string): Promise { const processFile = async (file: Express.Multer.File): Promise => { const relativeFilePath: string = path.join(PathToRoBillsFolder(jobid), file.filename); try { const relativeThumbPath: string = await GenerateThumbnail(relativeFilePath); const type: FileTypeResult | undefined = await Promise.race([ fileTypeFromFile(relativeFilePath), new Promise((resolve) => setTimeout(() => resolve(undefined), 5000)) ]); return { type, size: file.size, src: GenerateUrl([ FolderPaths.StaticPath, FolderPaths.JobsFolder, jobid, FolderPaths.BillsSubDir, file.filename ]), thumbnail: GenerateUrl([ FolderPaths.StaticPath, FolderPaths.JobsFolder, jobid, FolderPaths.BillsSubDir, relativeThumbPath ]), thumbnailHeight: 250, thumbnailWidth: 250, filename: file.filename, name: file.filename, path: relativeFilePath, thumbnailPath: relativeThumbPath }; } catch (error) { // Return basic info if thumbnail/type fails return { type: undefined, size: file.size, src: GenerateUrl([ FolderPaths.StaticPath, FolderPaths.JobsFolder, jobid, FolderPaths.BillsSubDir, file.filename ]), thumbnail: GenerateUrl([FolderPaths.StaticPath, "assets", "file.svg"]), thumbnailHeight: 250, thumbnailWidth: 250, filename: file.filename, name: file.filename, path: relativeFilePath, thumbnailPath: "" }; } }; return (await Promise.all(files.map(processFile))).filter((r): r is MediaFile => r !== null); } async function processExistingBillFiles(jobid: string, invoice_number: string): Promise { let filesList: fs.Dirent[] = ( await fs.readdir(PathToRoBillsFolder(jobid), { withFileTypes: true }) ).filter( (f) => f.isFile() && !/(^|\/)\.[^\/\.]/g.test(f.name) && (invoice_number !== "" ? f.name.toLowerCase().includes(invoice_number.toLowerCase()) : true) && ListableChecker(f) ); const processFile = async (file: fs.Dirent): Promise => { const relativeFilePath: string = path.join(PathToRoBillsFolder(jobid), file.name); try { if (!(await fs.pathExists(relativeFilePath))) { return null; } const fileStats = await Promise.race([ fs.stat(relativeFilePath), new Promise((_, reject) => setTimeout(() => reject(new Error("File stat timeout")), 5000)) ]); const relativeThumbPath: string = await GenerateThumbnail(relativeFilePath); const type: FileTypeResult | undefined = await Promise.race([ fileTypeFromFile(relativeFilePath), new Promise((resolve) => setTimeout(() => resolve(undefined), 5000)) ]); return { type, size: fileStats.size, src: GenerateUrl([FolderPaths.StaticPath, FolderPaths.JobsFolder, jobid, FolderPaths.BillsSubDir, file.name]), thumbnail: GenerateUrl([ FolderPaths.StaticPath, FolderPaths.JobsFolder, jobid, FolderPaths.BillsSubDir, relativeThumbPath ]), thumbnailHeight: 250, thumbnailWidth: 250, filename: file.name, name: file.name, path: relativeFilePath, thumbnailPath: relativeThumbPath }; } catch (error) { try { const fileStats = await fs.stat(relativeFilePath); return { type: undefined, size: fileStats.size, src: GenerateUrl([FolderPaths.StaticPath, FolderPaths.JobsFolder, jobid, FolderPaths.BillsSubDir, file.name]), thumbnail: GenerateUrl([FolderPaths.StaticPath, "assets", "file.svg"]), thumbnailHeight: 250, thumbnailWidth: 250, filename: file.name, name: file.name, path: relativeFilePath, thumbnailPath: "" }; } catch { return null; } } }; return (await Promise.all(filesList.map(processFile))).filter((r): r is MediaFile => r !== null); }