From 7f782d5a648bfbbc9950aec0ba6206862b6d31ab Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Sun, 13 Jul 2025 00:17:08 -0700 Subject: [PATCH] 1.0.14 --- Dockerfile | 51 +- bills/billRequestValidator.ts | 19 +- bills/billsListMedia.ts | 203 +++-- bills/billsUploadMedia.ts | 236 ++++- jobs/jobRequestValidator.ts | 15 +- jobs/jobsDeleteMedia.ts | 379 +++++++- jobs/jobsDownloadMedia.ts | 75 +- jobs/jobsListMedia.ts | 163 +++- jobs/jobsMoveMedia.ts | 388 ++++++-- jobs/jobsUploadMedia.ts | 204 ++++- package-lock.json | 1574 ++++++++++++++++++--------------- package.json | 36 +- redis-docker-compose.yml | 8 - server.ts | 73 +- util/generateThumbnail.ts | 207 ++++- util/heicConverter.ts | 291 +++--- util/interfaces/MediaFile.ts | 13 +- util/serverInit.ts | 18 +- util/validateToken.ts | 27 +- 19 files changed, 2564 insertions(+), 1416 deletions(-) delete mode 100644 redis-docker-compose.yml diff --git a/Dockerfile b/Dockerfile index fe15438..a50104c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,40 +3,7 @@ FROM node:22-alpine AS builder # Install build dependencies -RUN apk add --no-cache \ - bash wget build-base autoconf automake cmake libtool pkgconf \ - libjpeg-turbo-dev libpng-dev libwebp-dev tiff-dev libde265-dev \ - ruby ruby-dev - -# Replace source built libde265 and libheif with installed libraries in next release -# libheif-dev libde265-dev x265-dev - -# Build libde265 -WORKDIR /build/libde265 -RUN wget https://github.com/strukturag/libde265/archive/v1.0.15.tar.gz \ - && tar -xvf v1.0.15.tar.gz \ - && cd libde265-1.0.15 \ - && cmake . \ - && make \ - && make install - -# Build libheif -WORKDIR /build/libheif -RUN wget https://github.com/strukturag/libheif/archive/v1.19.7.tar.gz \ - && tar -xvf v1.19.7.tar.gz \ - && cd libheif-1.19.7 \ - && cmake --preset=release . \ - && make \ - && make install - -# Build ImageMagick -WORKDIR /build/imagemagick -RUN wget https://download.imagemagick.org/archive/releases/ImageMagick-7.1.1-44.tar.xz \ - && tar -xvf ImageMagick-7.1.1-44.tar.xz \ - && cd ImageMagick-7.1.1-44 \ - && ./configure --with-heic=yes --with-webp=yes \ - && make \ - && make install +RUN apk add --no-cache bash wget # Node.js application build stage WORKDIR /usr/src/app @@ -50,18 +17,12 @@ RUN npm run build # Final stage FROM node:22-alpine +# Enable community repository for additional packages +RUN echo "https://dl-cdn.alpinelinux.org/alpine/v$(grep -oE '[0-9]+\.[0-9]+' /etc/alpine-release)/community" >> /etc/apk/repositories +RUN apk update + # Install runtime dependencies only -RUN apk add --no-cache \ - bash redis ghostscript graphicsmagick imagemagick \ - libjpeg-turbo libpng libwebp tiff - -# Copy built libraries from builder -COPY --from=builder /usr/local/lib/ /usr/local/lib/ -COPY --from=builder /usr/local/bin/ /usr/local/bin/ -COPY --from=builder /usr/local/include/ /usr/local/include/ - -# Update library cache -RUN ldconfig /usr/local/lib +RUN apk add --no-cache bash redis ghostscript graphicsmagick imagemagick libjpeg-turbo libpng libwebp tiff libheif libde265 x265 ffmpeg RUN npm install -g pm2 diff --git a/bills/billRequestValidator.ts b/bills/billRequestValidator.ts index e5d1f2b..c80a422 100644 --- a/bills/billRequestValidator.ts +++ b/bills/billRequestValidator.ts @@ -1,11 +1,16 @@ import { NextFunction, Request, Response } from "express"; -export default function BillRequestValidator(req: Request, res: Response, next: NextFunction) { - const jobid: string = (req.body.jobid || "").trim(); - if (jobid === "") { - res.status(400).json({ error: "No RO Number has been specified." }); - return; - } else { +const validateBillRequest = (req: Request, res: Response, next: NextFunction) => { + try { + const jobid: string = (req.body.jobid || "").trim(); + if (!jobid) { + res.status(400).json({ error: "No RO Number has been specified." }); + return; + } next(); + } catch (error) { + res.status(500).json({ error: "Error validating job request.", details: (error as Error).message }); } -} +}; + +export default validateBillRequest; \ No newline at end of file diff --git a/bills/billsListMedia.ts b/bills/billsListMedia.ts index 052042b..65ff840 100644 --- a/bills/billsListMedia.ts +++ b/bills/billsListMedia.ts @@ -13,82 +13,151 @@ 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 vendorid: string = (req.body.vendorid || "").trim(); const invoice_number: string = (req.body.invoice_number || "").trim(); - let ret: MediaFile[]; - //Ensure all directories exist. await fs.ensureDir(PathToRoBillsFolder(jobid)); - if (req.files) { - ret = await Promise.all( - (req.files as Express.Multer.File[]).map(async (file) => { - const relativeFilePath: string = path.join(PathToRoBillsFolder(jobid), file.filename); + 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); + } +} - const relativeThumbPath: string = await GenerateThumbnail(relativeFilePath); - const type: FileTypeResult | undefined = await fileTypeFromFile(relativeFilePath); +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, - 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, - relativeFilePath - }; - }) - ); - } else { - 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) - ); - - ret = await Promise.all( - filesList.map(async (file) => { - const relativeFilePath: string = path.join(PathToRoBillsFolder(jobid), file.name); - - const relativeThumbPath: string = await GenerateThumbnail(relativeFilePath); - const type: FileTypeResult | undefined = await fileTypeFromFile(relativeFilePath); - const fileSize = await fs.stat(relativeFilePath); - return { - type, - size: fileSize.size, + type: undefined, + size: fileStats.size, src: GenerateUrl([FolderPaths.StaticPath, FolderPaths.JobsFolder, jobid, FolderPaths.BillsSubDir, file.name]), - thumbnail: GenerateUrl([ - FolderPaths.StaticPath, - FolderPaths.JobsFolder, - jobid, - FolderPaths.BillsSubDir, - relativeThumbPath - ]), + thumbnail: GenerateUrl([FolderPaths.StaticPath, "assets", "file.svg"]), thumbnailHeight: 250, thumbnailWidth: 250, filename: file.name, - relativeFilePath + name: file.name, + path: relativeFilePath, + thumbnailPath: "" }; - }) - ); - } + } catch { + return null; + } + } + }; - res.json(ret); + return (await Promise.all(filesList.map(processFile))).filter((r): r is MediaFile => r !== null); } diff --git a/bills/billsUploadMedia.ts b/bills/billsUploadMedia.ts index 84b1ac4..9eb6337 100644 --- a/bills/billsUploadMedia.ts +++ b/bills/billsUploadMedia.ts @@ -1,3 +1,4 @@ +import { Job, Queue, QueueEvents, Worker } from "bullmq"; import dotenv from "dotenv"; import { Request, Response } from "express"; import fs from "fs-extra"; @@ -6,7 +7,7 @@ import path, { resolve } from "path"; import { logger } from "../server.js"; import GenerateThumbnail from "../util/generateThumbnail.js"; import { generateUniqueBillFilename } from "../util/generateUniqueFilename.js"; -import { ConvertHeicFiles } from "../util/heicConverter.js"; +import { convertHeicFiles } from "../util/heicConverter.js"; import { PathToRoBillsFolder, PathToVendorBillsFile } from "../util/pathGenerators.js"; import { BillsListMedia } from "./billsListMedia.js"; @@ -14,72 +15,209 @@ dotenv.config({ path: resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) }); +const BILLS_UPLOAD_QUEUE_NAME = "billsUploadProcessingQueue"; + +const connectionOpts = { + host: "localhost", + port: 6379, + maxRetriesPerRequest: 3, + enableReadyCheck: true, + reconnectOnError: (err: Error) => err.message.includes("READONLY") +}; + +const billsUploadProcessingQueue = new Queue(BILLS_UPLOAD_QUEUE_NAME, { + connection: connectionOpts, + defaultJobOptions: { + removeOnComplete: true, + removeOnFail: true, + attempts: 3, + backoff: { type: "exponential", delay: 1000 } + } +}); + +const billsUploadQueueEvents = new QueueEvents(BILLS_UPLOAD_QUEUE_NAME, { + connection: connectionOpts +}); + +// Bills upload processing worker +const billsUploadWorker = new Worker( + BILLS_UPLOAD_QUEUE_NAME, + async ( + job: Job<{ + files: Express.Multer.File[]; + vendorid: string; + invoice_number: string; + skipThumbnails: boolean; + duplicateToVendor: boolean; + }> + ) => { + const { files, vendorid, invoice_number, skipThumbnails, duplicateToVendor } = job.data; + try { + logger.debug(`Processing bills upload with ${files.length} files`); + await job.updateProgress(10); + + // Convert HEIC files if any + await convertHeicFiles(files); + await job.updateProgress(30); + + if (!skipThumbnails) { + // Filter out HEIC files since they've been converted to JPEG already + const filesToThumbnail = files.filter((file) => { + const isHeic = + file.mimetype === "image/heic" || + file.mimetype === "image/heif" || + /\.heic$/i.test(file.filename) || + /\.heif$/i.test(file.filename); + return !isHeic; + }); + + logger.debug( + `Generating thumbnails for ${filesToThumbnail.length} bill files (excluding HEIC)`, + filesToThumbnail.map((f) => f.filename) + ); + + // Generate thumbnails + const thumbnailPromises = filesToThumbnail.map((file) => GenerateThumbnail(file.path)); + await Promise.all(thumbnailPromises); + await job.updateProgress(60); + } + + // Duplicate to vendor folder if enabled + if (duplicateToVendor && vendorid && invoice_number) { + await Promise.all( + files.map(async (file) => { + const target = path.join(PathToVendorBillsFile(vendorid), file.filename); + await fs.ensureDir(path.dirname(target)); + await fs.copyFile(file.path, target); + }) + ); + await job.updateProgress(90); + } + + logger.debug(`Bills upload processing completed`); + await job.updateProgress(100); + + return { success: true, filesProcessed: files.length }; + } catch (error) { + logger.error(`Error processing bills upload:`, error); + throw error; + } + }, + { + connection: connectionOpts, + concurrency: 2 // Allow 2 bills upload processing jobs to run concurrently + } +); + +// Event listeners for bills upload queue and worker +billsUploadProcessingQueue.on("waiting", (job) => { + logger.debug(`[BillsUploadQueue] Job waiting: ${job.data.invoice_number}`); +}); +billsUploadProcessingQueue.on("error", (error) => { + logger.error(`[BillsUploadQueue] Queue error:`, error); +}); + +billsUploadWorker.on("ready", () => { + logger.debug("[BillsUploadWorker] Worker ready"); +}); +billsUploadWorker.on("active", (job, prev) => { + logger.debug(`[BillsUploadWorker] Job ${job.id} active (previous: ${prev})`); +}); +billsUploadWorker.on("completed", async (job) => { + logger.debug(`[BillsUploadWorker] Job ${job.id} completed`); + await job.remove(); + logger.debug(`[BillsUploadWorker] Job ${job.id} removed from Redis`); +}); +billsUploadWorker.on("failed", (jobId, reason) => { + logger.error(`[BillsUploadWorker] Job ${jobId} failed: ${reason}`); +}); +billsUploadWorker.on("error", (error) => { + logger.error(`[BillsUploadWorker] Worker error:`, error); +}); +billsUploadWorker.on("stalled", (job) => { + logger.error(`[BillsUploadWorker] Worker stalled: ${job}`); +}); +billsUploadWorker.on("ioredis:close", () => { + logger.error("[BillsUploadWorker] Redis connection closed"); +}); + export const BillsMediaUploadMulter = multer({ storage: multer.diskStorage({ - destination: function (req, file, cb) { + destination(req, file, cb) { const jobid: string = (req.body.jobid || "").trim(); - const DestinationFolder: string = PathToRoBillsFolder(jobid); - fs.ensureDirSync(DestinationFolder); - cb(jobid === "" || jobid === null ? new Error("Job ID not specified.") : null, DestinationFolder); + if (!jobid) { + cb(new Error("Job ID not specified."), ""); + return; + } + const destinationFolder = PathToRoBillsFolder(jobid); + fs.ensureDir(destinationFolder) + .then(() => cb(null, destinationFolder)) + .catch((err) => cb(err as Error, "")); }, - filename: function (req, file, cb) { - logger.info("Uploading file: ", { - file: path.basename(file.originalname) - }); + filename(req, file, cb) { + logger.info("Uploading file:", { file: path.basename(file.originalname) }); const invoice_number: string = (req.body.invoice_number || "").trim(); - - cb( - invoice_number === "" || invoice_number === null ? new Error("Invoice number not specified.") : null, - generateUniqueBillFilename(file, invoice_number) - ); + if (!invoice_number) { + cb(new Error("Invoice number not specified."), ""); + return; + } + cb(null, generateUniqueBillFilename(file, invoice_number)); } }) }); export async function BillsUploadMedia(req: Request, res: Response) { try { - if (!req.files) { - res.send({ + const files = req.files as Express.Multer.File[] | undefined; + + if (!files || files.length === 0) { + logger.warning("Upload contained no files."); + res.status(400).json({ status: false, message: "No file uploaded" }); - } else { - await ConvertHeicFiles(req.files as Express.Multer.File[]); - - const thumbnailGenerationQueue: Promise[] = []; - - //for each file.path, generate the thumbnail. - (req.files as Express.Multer.File[]).forEach((file) => { - thumbnailGenerationQueue.push(GenerateThumbnail(file.path)); - }); - await Promise.all(thumbnailGenerationQueue); - //Check to see if it should be duplicated to the vendor. - if (process.env.DUPLICATE_BILL_TO_VENDOR) { - const copyQueue: Promise[] = []; - - //for each file.path, generate the thumbnail. - (req.files as Express.Multer.File[]).forEach((file) => { - const vendorid: string = (req.body.vendorid || "").trim(); - const invoice_number: string = (req.body.invoice_number || "").trim(); - - copyQueue.push( - (async () => { - const target: string = path.join(PathToVendorBillsFile(vendorid), file.filename); - await fs.ensureDir(path.dirname(target)); - await fs.copyFile(file.path, target); - })() - ); - //Copy Queue is not awaited - we don't care if it finishes before we serve up the thumbnails from the jobs directory. - }); - } - - BillsListMedia(req, res); + return; } + + const vendorid: string = (req.body.vendorid || "").trim(); + const invoice_number: string = (req.body.invoice_number || "").trim(); + const skipThumbnails = req.body.skip_thumbnail === "true"; + const duplicateToVendor = process.env.DUPLICATE_BILL_TO_VENDOR === "true"; + + // If skipping thumbnails, queue job without waiting and return immediately + if (skipThumbnails) { + await billsUploadProcessingQueue.add("processBillsUpload", { + files, + vendorid, + invoice_number, + skipThumbnails: true, + duplicateToVendor + }); + res.sendStatus(200); + return; + } + + // Add bills upload processing job to queue and wait for completion + logger.debug(`Adding bills upload processing job for ${files.length} files`); + const job = await billsUploadProcessingQueue.add("processBillsUpload", { + files, + vendorid, + invoice_number, + skipThumbnails: false, + duplicateToVendor + }); + + // Wait for the job to complete + await job.waitUntilFinished(billsUploadQueueEvents); + logger.debug(`Bills upload processing job completed`); + + // Delegate to BillsListMedia to respond with updated media list + await BillsListMedia(req, res); } catch (error) { logger.error("Error while uploading Bill Media", { files: req.files, - error + error: (error as Error).message }); - res.status(500).send(error); + res.status(500).json({ error: (error as Error).message }); } } diff --git a/jobs/jobRequestValidator.ts b/jobs/jobRequestValidator.ts index 6433afa..48538f0 100644 --- a/jobs/jobRequestValidator.ts +++ b/jobs/jobRequestValidator.ts @@ -1,11 +1,16 @@ import { NextFunction, Request, Response } from "express"; -const validateJobRequest: (req: Request, res: Response, next: NextFunction) => void = (req, res, next) => { - const jobId: string = (req.body.jobid || "").trim(); - if (jobId === "") { - return res.status(400).json({ error: "No RO Number has been specified." }); +const validateJobRequest = (req: Request, res: Response, next: NextFunction) => { + try { + const jobid: string = (req.body.jobid || "").trim(); + if (!jobid) { + res.status(400).json({ error: "No RO Number has been specified." }); + return; + } + next(); + } catch (error) { + res.status(500).json({ error: "Error validating job request.", details: (error as Error).message }); } - next(); }; export default validateJobRequest; diff --git a/jobs/jobsDeleteMedia.ts b/jobs/jobsDeleteMedia.ts index 93f60b3..a140cac 100644 --- a/jobs/jobsDeleteMedia.ts +++ b/jobs/jobsDeleteMedia.ts @@ -1,64 +1,351 @@ +import { Job, Queue, QueueEvents, Worker } from "bullmq"; import { Request, Response } from "express"; import fs from "fs-extra"; import path from "path"; import { logger } from "../server.js"; import MediaFile from "../util/interfaces/MediaFile.js"; import ListableChecker from "../util/listableChecker.js"; -import { PathToRoBillsFolder, PathToRoFolder } from "../util/pathGenerators.js"; +import { PathToRoBillsFolder, PathToRoFolder, PathToVendorBillsFile } from "../util/pathGenerators.js"; import { BillsRelativeFilePath, FolderPaths, JobRelativeFilePath } from "../util/serverInit.js"; +const DELETE_QUEUE_NAME = "deleteQueue"; + +const connectionOpts = { + host: "localhost", + port: 6379, + maxRetriesPerRequest: 3, + enableReadyCheck: true, + reconnectOnError: (err: Error) => err.message.includes("READONLY") +}; + +const deleteQueue = new Queue(DELETE_QUEUE_NAME, { + connection: connectionOpts, + defaultJobOptions: { + removeOnComplete: 10, + removeOnFail: 5, + attempts: 3, + backoff: { type: "exponential", delay: 2000 } + } +}); + +const deleteQueueEvents = new QueueEvents(DELETE_QUEUE_NAME, { + connection: connectionOpts +}); + +const deleteWorker = new Worker( + DELETE_QUEUE_NAME, + async (job: Job<{ jobid: string; files: string[] }>) => { + const { jobid, files } = job.data; + logger.debug(`[DeleteWorker] Starting delete operation for job ${jobid} with ${files.length} files`); + + try { + await job.updateProgress(5); + + const result = await processDeleteOperation(jobid, files, job); + + await job.updateProgress(100); + logger.debug(`[DeleteWorker] Completed delete operation for job ${jobid}`); + return result; + } catch (error) { + logger.error(`[DeleteWorker] Error deleting files for job ${jobid}:`, error); + throw error; + } + }, + { + connection: connectionOpts, + concurrency: 2 // Limit concurrent delete operations + } +); + +// Worker event listeners for logging +deleteWorker.on("ready", () => { + logger.debug("[DeleteWorker] Worker is ready"); +}); +deleteWorker.on("active", (job, prev) => { + logger.debug(`[DeleteWorker] Job ${job.id} active (previous: ${prev})`); +}); +deleteWorker.on("completed", async (job) => { + logger.debug(`[DeleteWorker] Job ${job.id} completed`); +}); +deleteWorker.on("failed", (job, err) => { + logger.error(`[DeleteWorker] Job ${job?.id} failed:`, err); +}); +deleteWorker.on("stalled", (jobId) => { + logger.error(`[DeleteWorker] Job stalled: ${jobId}`); +}); +deleteWorker.on("error", (err) => { + logger.error("[DeleteWorker] Worker error:", err); +}); + +// Queue event listeners +deleteQueue.on("waiting", (job) => { + logger.debug(`[DeleteQueue] Job waiting in queue: job ${job.data.jobid} - ${job.data.files.length} files`); +}); +deleteQueue.on("error", (err) => { + logger.error("[DeleteQueue] Queue error:", err); +}); + +async function processDeleteOperation( + jobid: string, + files: string[], + job?: Job +): Promise<{ deleted: number; failed: number }> { + await fs.ensureDir(PathToRoFolder(jobid)); + logger.debug("Deleting media for job: " + PathToRoFolder(jobid)); + + try { + // Setup lists for both file locations + async function readFilteredDir(dirPath: string): Promise { + const filtered: fs.Dirent[] = []; + try { + const dir = await fs.opendir(dirPath); + for await (const dirent of dir) { + if (dirent.isFile() && ListableChecker(dirent)) { + filtered.push(dirent); + } + } + } catch (err) { + logger.error(`Failed to read directory: ${dirPath}`, err); + } + return filtered; + } + + const jobFileList = await readFilteredDir(PathToRoFolder(jobid)); + const billFileList = await readFilteredDir(PathToRoBillsFolder(jobid)); + + if (job) await job.updateProgress(15); + + // Helper function for safe file deletion + const safeUnlink = async (filePath: string, logPrefix: string = "[DeleteWorker] ") => { + try { + // lstat first to ensure it's a file and the handle is safe to unlink + if (await fs.pathExists(filePath)) { + const stats = await fs.lstat(filePath); + if (stats.isFile()) { + await fs.unlink(filePath); + logger.debug(`${logPrefix}Deleted: ${filePath}`); + } + } + } catch (err) { + logger.warn(`${logPrefix}Failed to delete ${filePath}: ${err}`); + } + }; + + // Helper to delete a file and its thumbnails + const deleteFileWithThumbs = async (mediaFile: MediaFile, logPrefix: string) => { + + try { + await safeUnlink(mediaFile.path, logPrefix); + + // Delete thumbnails + const thumbDir = path.dirname(mediaFile.thumbnailPath); + const baseThumb = path.basename(mediaFile.thumbnailPath, path.extname(mediaFile.thumbnailPath)); + + logger.debug(`${logPrefix}Deleting thumbnails from: ${thumbDir}, baseThumb: ${baseThumb}`); + + for (const ext of [".jpg", ".png"]) { + const thumbPath = path.join(thumbDir, `${baseThumb}${ext}`); + await safeUnlink(thumbPath, logPrefix); + } + + // Delete ConvertedOriginal file if it exists + // The ConvertedOriginal folder contains the original files with the same filename but original extension + const convertedOriginalDir = path.join(path.dirname(mediaFile.path), FolderPaths.ConvertedOriginalSubDir); + + try { + if (await fs.pathExists(convertedOriginalDir)) { + const convertedOriginalFiles = await fs.readdir(convertedOriginalDir); + const currentFileName = path.basename(mediaFile.path, path.extname(mediaFile.path)); + + logger.debug(`Looking for ConvertedOriginal files with base name: ${currentFileName}`, { + convertedOriginalDir, + currentFileName, + availableFiles: convertedOriginalFiles + }); + + for (const file of convertedOriginalFiles) { + const fileBaseName = path.basename(file, path.extname(file)); + // Match files that have the same base name (same filename, potentially different extension) + if (fileBaseName === currentFileName) { + const convertedOriginalPath = path.join(convertedOriginalDir, file); + await safeUnlink(convertedOriginalPath, logPrefix); + logger.debug(`Found and deleted ConvertedOriginal file: ${convertedOriginalPath}`); + } + } + } + } catch (error) { + logger.warn(`Error checking/deleting ConvertedOriginal files for ${mediaFile.path}:`, error); + } + } catch (error) { + logger.error(`${logPrefix}Error in deleteFileWithThumbs for ${mediaFile.path}:`, error); + throw error; + } + }; + + // Convert to MediaFile objects for better type safety + const jobMediaFiles: MediaFile[] = jobFileList.map((file) => { + const thumbName = file.name.replace(/\.[^/.]+$/, ".jpg"); + const thumbPath = path.join(FolderPaths.Jobs, jobid, FolderPaths.ThumbsSubDir, thumbName); + const filePath = JobRelativeFilePath(jobid, file.name); + return { + name: file.name, + path: filePath, + thumbnailPath: thumbPath, + src: filePath, + thumbnail: thumbPath, + thumbnailHeight: 0, + thumbnailWidth: 0, + filename: file.name + }; + }); + + // Delete job files with proper error handling + const jobDeletions = jobMediaFiles + .filter((mediaFile) => files.includes(path.basename(mediaFile.filename))) + .map((mediaFile) => deleteFileWithThumbs(mediaFile, "[DeleteWorker] ")); + + // Prepare bill media files + const billMediaFiles: MediaFile[] = billFileList.map((file) => { + const thumbName = file.name.replace(/\.[^/.]+$/, ".jpg"); + const thumbPath = path.join(FolderPaths.Jobs, jobid, FolderPaths.BillsSubDir, FolderPaths.ThumbsSubDir, thumbName); + const filePath = BillsRelativeFilePath(jobid, file.name); + return { + name: file.name, + path: filePath, + thumbnailPath: thumbPath, + src: filePath, + thumbnail: thumbPath, + thumbnailHeight: 0, + thumbnailWidth: 0, + filename: file.name + }; + }); + + // Delete bill files using the helper function + const billDeletions = billMediaFiles + .filter((mediaFile) => files.includes(path.basename(mediaFile.filename))) + .map((mediaFile) => deleteFileWithThumbs(mediaFile, "[DeleteWorker] Bill: ")); + + // Delete vendor duplicates if DUPLICATE_BILL_TO_VENDOR is enabled + const vendorDeletions: Promise[] = []; + const duplicateToVendor = process.env.DUPLICATE_BILL_TO_VENDOR === "true"; + + if (duplicateToVendor) { + const billFilesToDelete = billMediaFiles + .filter((mediaFile) => files.includes(path.basename(mediaFile.filename))) + .map(mediaFile => path.basename(mediaFile.filename)); + + for (const billFile of billFilesToDelete) { + vendorDeletions.push( + (async () => { + try { + // Search for this file in all vendor directories + const vendorsDir = FolderPaths.Vendors; + if (await fs.pathExists(vendorsDir)) { + const vendors = await fs.readdir(vendorsDir, { withFileTypes: true }); + + for (const vendor of vendors) { + if (vendor.isDirectory()) { + const vendorFilePath = path.join(vendorsDir, vendor.name, billFile); + if (await fs.pathExists(vendorFilePath)) { + await safeUnlink(vendorFilePath, "[DeleteWorker] Vendor: "); + logger.debug(`[DeleteWorker] Deleted vendor file: ${vendorFilePath}`); + } + } + } + } + } catch (error) { + logger.warn(`[DeleteWorker] Failed to delete vendor copies for ${billFile}:`, error); + } + })() + ); + } + } + + if (job) await job.updateProgress(80); + + // Wait for all deletions to complete + const results = await Promise.allSettled([...jobDeletions, ...billDeletions, ...vendorDeletions]); + const failed = results.filter(r => r.status === 'rejected').length; + const deleted = results.filter(r => r.status === 'fulfilled').length; + + logger.debug(`[DeleteWorker] Delete operation completed: ${deleted} successful, ${failed} failed`); + + return { deleted, failed }; + } catch (error) { + logger.error("[DeleteWorker] Error in processDeleteOperation:", error); + throw error; + } +} + export async function JobsDeleteMedia(req: Request, res: Response) { const jobid: string = (req.body.jobid || "").trim(); const files: string[] = req.body.files || []; - await fs.ensureDir(PathToRoFolder(jobid)); - logger.debug("Deleteing media for job: " + PathToRoFolder(jobid)); - let ret: MediaFile[]; + try { - // Setup lists for both file locations - const jobFileList: fs.Dirent[] = ( - await fs.readdir(PathToRoFolder(jobid), { - withFileTypes: true - }) - ).filter((f) => f.isFile() && ListableChecker(f)); - const billFileList: fs.Dirent[] = ( - await fs.readdir(PathToRoBillsFolder(jobid), { - withFileTypes: true - }) - ).filter((f) => f.isFile() && ListableChecker(f)); + if (!files.length) { + res.status(400).json({ error: "files must be specified." }); + return; + } - // Check both list for the files that need to be deleted - await Promise.all( - jobFileList.map(async (file) => { - if (files.includes(path.parse(path.basename(file.name)).base)) { - // File is in the set of requested files. - await fs.remove(JobRelativeFilePath(jobid, file.name)); - await fs.remove( - path.join(FolderPaths.Jobs, jobid, FolderPaths.ThumbsSubDir, file.name.replace(/\.[^/.]+$/, ".png")) - ); - } - }) - ); - await Promise.all( - billFileList.map(async (file) => { - if (files.includes(path.parse(path.basename(file.name)).base)) { - // File is in the set of requested files. - await fs.remove(BillsRelativeFilePath(jobid, file.name)); - await fs.remove( - path.join( - FolderPaths.Jobs, - jobid, - FolderPaths.BillsSubDir, - FolderPaths.ThumbsSubDir, - file.name.replace(/\.[^/.]+$/, ".png") - ) - ); - } - }) - ); + // For small operations (1-5 files), process synchronously for immediate feedback + if (files.length <= 5) { + logger.debug("Processing small delete operation synchronously"); + await processDeleteOperation(jobid, files); + res.sendStatus(200); + return; + } + + // For larger operations, use BullMQ but still return success immediately + logger.debug(`[JobsDeleteMedia] Queuing delete operation for ${files.length} files`); + const job = await deleteQueue.add("deleteMedia", { jobid, files }); + + // Return success immediately (optimistic response) + res.sendStatus(200); + + // Process in background - if it fails, files will still be there on next refresh + job.waitUntilFinished(deleteQueueEvents) + .then(() => { + logger.debug(`[JobsDeleteMedia] Background delete completed for job ${job.id}`); + }) + .catch((error) => { + logger.error(`[JobsDeleteMedia] Background delete failed for job ${job.id}:`, error); + }); - if (!res.headersSent) res.sendStatus(200); } catch (error) { logger.error("Error deleting job media.", { jobid, error }); - if (!res.headersSent) res.status(500).json(error); + if (!res.headersSent) res.status(500).json({ error: "Failed to delete media", details: error }); + } +} + +/** + * Get the status of a delete operation job + */ +export async function JobsDeleteStatus(req: Request, res: Response) { + const { jobId } = req.params; + + try { + const job = await Job.fromId(deleteQueue, jobId); + + if (!job) { + res.status(404).json({ error: "Job not found" }); + return; + } + + const state = await job.getState(); + const progress = job.progress; + + res.json({ + jobId, + state, + progress, + data: job.data, + finishedOn: job.finishedOn, + processedOn: job.processedOn, + failedReason: job.failedReason + }); + } catch (error) { + logger.error("Error getting delete job status:", error); + res.status(500).json({ error: "Failed to get job status" }); } } diff --git a/jobs/jobsDownloadMedia.ts b/jobs/jobsDownloadMedia.ts index 874c7e9..9e2ea52 100644 --- a/jobs/jobsDownloadMedia.ts +++ b/jobs/jobsDownloadMedia.ts @@ -12,77 +12,60 @@ 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 jobFileList: fs.Dirent[] = ( - await fs.readdir(PathToRoFolder(jobid), { - withFileTypes: true - }) + await fs.readdir(PathToRoFolder(jobid), { withFileTypes: true }) ).filter((f) => f.isFile() && ListableChecker(f)); const billFileList: fs.Dirent[] = ( - await fs.readdir(PathToRoBillsFolder(jobid), { - withFileTypes: true - }) + await fs.readdir(PathToRoBillsFolder(jobid), { withFileTypes: true }) ).filter((f) => f.isFile() && ListableChecker(f)); - if (files.length === 0) { - //Get everything. + // Helper to add files to the zip + const addFilesToZip = async ( + fileList: fs.Dirent[], + relativePathFn: (jobid: string, filename: string) => string + ) => { await Promise.all( - jobFileList.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); - }) - ); - await Promise.all( - billFileList.map(async (file) => { - //Do something async - const fileOnDisk: Buffer = await fs.readFile(BillsRelativeFilePath(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( - jobFileList.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); + fileList.map(async (file) => { + const baseName = path.basename(file.name); + if (files.length === 0 || files.includes(baseName)) { + try { + const fileOnDisk: Buffer = await fs.readFile(relativePathFn(jobid, file.name)); + zip.file(baseName, fileOnDisk); + } catch (err) { + logger.warn(`Could not add file to zip: ${file.name}`, err); + } } }) ); - await Promise.all( - billFileList.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(BillsRelativeFilePath(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); + }; + + await addFilesToZip(jobFileList, JobRelativeFilePath); + await addFilesToZip(billFileList, BillsRelativeFilePath); + + // Set headers for download + res.setHeader("Content-Disposition", `attachment; filename="${jobid}.zip"`); + res.setHeader("Content-Type", "application/zip"); zip .generateNodeStream({ type: "nodebuffer", streamFiles: true - //encodeFileName: (filename) => `${jobid}.zip`, }) - .pipe(res); + .pipe(res) + .on("finish", () => { + logger.debug(`Zip stream finished for Job ID ${jobid}`); + }); } catch (error) { logger.error("Error downloading job media.", { jobid, error: (error as Error).message }); - res.status(500).json((error as Error).message); + if (!res.headersSent) res.status(500).json((error as Error).message); } } diff --git a/jobs/jobsListMedia.ts b/jobs/jobsListMedia.ts index 182f101..4dca971 100644 --- a/jobs/jobsListMedia.ts +++ b/jobs/jobsListMedia.ts @@ -14,56 +14,14 @@ export async function JobsListMedia(req: Request, res: Response) { const jobid: string = (req.body.jobid || "").trim(); await fs.ensureDir(PathToRoFolder(jobid)); logger.debug("Listing media for job: " + PathToRoFolder(jobid)); - let ret: MediaFile[]; + try { + let ret: MediaFile[]; + if (req.files) { - //We just uploaded files, we're going to send only those back. - ret = await Promise.all( - (req.files as Express.Multer.File[]).map(async (file) => { - const relativeFilePath: string = JobRelativeFilePath(jobid, file.filename); - - const relativeThumbPath: string = await GenerateThumbnail(relativeFilePath); - - const type: FileTypeResult | undefined = await fileTypeFromFile(relativeFilePath); - - return { - type, - size: file.size, - src: GenerateUrl([FolderPaths.StaticPath, FolderPaths.JobsFolder, jobid, file.filename]), - thumbnail: GenerateUrl([FolderPaths.StaticPath, FolderPaths.JobsFolder, jobid, relativeThumbPath]), - thumbnailHeight: 250, - thumbnailWidth: 250, - filename: file.filename, - relativeFilePath - }; - }) - ); + ret = await processUploadedFiles(req.files as Express.Multer.File[], jobid); } else { - const filesList: fs.Dirent[] = ( - await fs.readdir(PathToRoFolder(jobid), { - withFileTypes: true - }) - ).filter((f) => f.isFile() && ListableChecker(f)); - - ret = await Promise.all( - filesList.map(async (file) => { - const relativeFilePath: string = JobRelativeFilePath(jobid, file.name); - - const relativeThumbPath: string = await GenerateThumbnail(relativeFilePath); - const type: FileTypeResult | undefined = await fileTypeFromFile(relativeFilePath); - const fileSize = await fs.stat(relativeFilePath); - return { - type, - size: fileSize.size, - src: GenerateUrl([FolderPaths.StaticPath, FolderPaths.JobsFolder, jobid, file.name]), - thumbnail: GenerateUrl([FolderPaths.StaticPath, FolderPaths.JobsFolder, jobid, relativeThumbPath]), - thumbnailHeight: 250, - thumbnailWidth: 250, - filename: file.name, - relativeFilePath - }; - }) - ); + ret = await processExistingFiles(jobid); } if (!res.headersSent) res.json(ret); @@ -72,3 +30,114 @@ export async function JobsListMedia(req: Request, res: Response) { if (!res.headersSent) res.status(500).json(error); } } + +async function processUploadedFiles(files: Express.Multer.File[], jobid: string): Promise { + const processFile = async (file: Express.Multer.File): Promise => { + const relativeFilePath: string = JobRelativeFilePath(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, file.filename]), + thumbnail: GenerateUrl([FolderPaths.StaticPath, FolderPaths.JobsFolder, jobid, relativeThumbPath]), + thumbnailHeight: 250, + thumbnailWidth: 250, + filename: file.filename, + name: file.filename, + path: relativeFilePath, + thumbnailPath: relativeThumbPath + }; + } catch (error) { + logger.error(`Error processing uploaded file ${file.filename}:`, error); + return { + type: undefined, + size: file.size, + src: GenerateUrl([FolderPaths.StaticPath, FolderPaths.JobsFolder, jobid, 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 processExistingFiles(jobid: string): Promise { + const dirPath = PathToRoFolder(jobid); + const mediaFiles: MediaFile[] = []; + const dir = await fs.opendir(dirPath); + + for await (const dirent of dir) { + if (!dirent.isFile() || !ListableChecker(dirent)) continue; + + const file = dirent.name; + const relativeFilePath: string = JobRelativeFilePath(jobid, file); + + try { + if (!(await fs.pathExists(relativeFilePath))) { + logger.warn(`File no longer exists: ${relativeFilePath}`); + continue; + } + + 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)) + ]); + + mediaFiles.push({ + type, + size: fileStats.size, + src: GenerateUrl([FolderPaths.StaticPath, FolderPaths.JobsFolder, jobid, file]), + thumbnail: GenerateUrl([FolderPaths.StaticPath, FolderPaths.JobsFolder, jobid, relativeThumbPath]), + thumbnailHeight: 250, + thumbnailWidth: 250, + filename: file, + name: file, + path: relativeFilePath, + thumbnailPath: relativeThumbPath + }); + } catch (error) { + logger.error(`Error processing existing file ${file}:`, error); + + try { + const fileStats = await fs.stat(relativeFilePath); + mediaFiles.push({ + type: undefined, + size: fileStats.size, + src: GenerateUrl([FolderPaths.StaticPath, FolderPaths.JobsFolder, jobid, file]), + thumbnail: GenerateUrl([FolderPaths.StaticPath, "assets", "file.svg"]), + thumbnailHeight: 250, + thumbnailWidth: 250, + filename: file, + name: file, + path: relativeFilePath, + thumbnailPath: "" + }); + } catch (statError) { + logger.error(`Could not get stats for ${file}:`, statError); + } + } + } + + return mediaFiles; +} diff --git a/jobs/jobsMoveMedia.ts b/jobs/jobsMoveMedia.ts index 0994fae..7c06918 100644 --- a/jobs/jobsMoveMedia.ts +++ b/jobs/jobsMoveMedia.ts @@ -1,3 +1,4 @@ +import { Job, Queue, QueueEvents, Worker } from "bullmq"; import { Request, Response } from "express"; import fs from "fs-extra"; import path from "path"; @@ -7,90 +8,299 @@ import { PathToRoBillsFolder, PathToRoFolder } from "../util/pathGenerators.js"; import { FolderPaths } from "../util/serverInit.js"; import { JobsListMedia } from "./jobsListMedia.js"; +const MOVE_QUEUE_NAME = "moveQueue"; + +const connectionOpts = { + host: "localhost", + port: 6379, + maxRetriesPerRequest: 3, + enableReadyCheck: true, + reconnectOnError: (err: Error) => err.message.includes("READONLY") +}; + +const moveQueue = new Queue(MOVE_QUEUE_NAME, { + connection: connectionOpts, + defaultJobOptions: { + removeOnComplete: 10, + removeOnFail: 5, + attempts: 3, + backoff: { type: "exponential", delay: 2000 } + } +}); + +const moveQueueEvents = new QueueEvents(MOVE_QUEUE_NAME, { + connection: connectionOpts +}); + +const moveWorker = new Worker( + MOVE_QUEUE_NAME, + async (job: Job<{ jobid: string; from_jobid: string; files: string[] }>) => { + const { jobid, from_jobid, files } = job.data; + logger.debug(`[MoveWorker] Starting move operation from ${from_jobid} to ${jobid} for ${files.length} files`); + + try { + await job.updateProgress(5); + + const result = await processMoveOperation(jobid, from_jobid, files, job); + + await job.updateProgress(100); + logger.debug(`[MoveWorker] Completed move operation from ${from_jobid} to ${jobid}`); + return result; + } catch (error) { + logger.error(`[MoveWorker] Error moving files from ${from_jobid} to ${jobid}:`, error); + throw error; + } + }, + { + connection: connectionOpts, + concurrency: 2 // Limit concurrent move operations to avoid I/O overwhelm + } +); + +// Worker event listeners for logging +moveWorker.on("ready", () => { + logger.debug("[MoveWorker] Worker is ready"); +}); +moveWorker.on("active", (job, prev) => { + logger.debug(`[MoveWorker] Job ${job.id} active (previous: ${prev})`); +}); +moveWorker.on("completed", async (job) => { + logger.debug(`[MoveWorker] Job ${job.id} completed`); +}); +moveWorker.on("failed", (job, err) => { + logger.error(`[MoveWorker] Job ${job?.id} failed:`, err); +}); +moveWorker.on("stalled", (jobId) => { + logger.error(`[MoveWorker] Job stalled: ${jobId}`); +}); +moveWorker.on("error", (err) => { + logger.error("[MoveWorker] Worker error:", err); +}); + +// Queue event listeners +moveQueue.on("waiting", (job) => { + logger.debug(`[MoveQueue] Job waiting in queue: ${job.data.from_jobid} -> ${job.data.jobid}`); +}); +moveQueue.on("error", (err) => { + logger.error("[MoveQueue] Queue error:", err); +}); + +async function processMoveOperation( + jobid: string, + from_jobid: string, + files: string[], + job?: Job +): Promise<{ moved: number; failed: number }> { + try { + const jobFileList: string[] = []; + const jobDir = await fs.opendir(PathToRoFolder(from_jobid)); + for await (const dirent of jobDir) { + if (dirent.isFile() && ListableChecker(dirent)) jobFileList.push(dirent.name); + } + + const billFileList: string[] = []; + const billDir = await fs.opendir(PathToRoBillsFolder(from_jobid)); + for await (const dirent of billDir) { + if (dirent.isFile() && ListableChecker(dirent)) billFileList.push(dirent.name); + } + + await fs.ensureDir(PathToRoFolder(jobid)); + logger.debug("Moving job based media.", { jobid, from_jobid, files }); + + if (job) await job.updateProgress(15); + + const moveOps: Promise[] = []; + let processedFiles = 0; + const totalFiles = files.length; + + for (const file of files) { + if (jobFileList.includes(file)) { + // Move main file + moveOps.push( + fs + .move(path.join(FolderPaths.Jobs, from_jobid, file), path.join(FolderPaths.Jobs, jobid, file), { + overwrite: true + }) + .then(() => logger.debug(`[MoveWorker] Moved main file: ${file}`)) + .catch((err) => logger.warn(`[MoveWorker] Failed to move main file ${file}:`, err)) + ); + + // Move thumbnails + const baseThumb = file.replace(/\.[^/.]+$/, ""); + for (const ext of [".jpg", ".png"]) { + moveOps.push( + fs + .move( + path.join(FolderPaths.Jobs, from_jobid, FolderPaths.ThumbsSubDir, `${baseThumb}${ext}`), + path.join(FolderPaths.Jobs, jobid, FolderPaths.ThumbsSubDir, `${baseThumb}${ext}`), + { overwrite: true } + ) + .then(() => logger.debug(`[MoveWorker] Moved thumbnail: ${baseThumb}${ext}`)) + .catch(() => {}) // Thumbnails might not exist + ); + } + + // Move ConvertedOriginal file if it exists + const baseFileName = file.replace(/\.[^/.]+$/, ""); + const sourceConvertedDir = path.join(FolderPaths.Jobs, from_jobid, FolderPaths.ConvertedOriginalSubDir); + const targetConvertedDir = path.join(FolderPaths.Jobs, jobid, FolderPaths.ConvertedOriginalSubDir); + + moveOps.push( + (async () => { + try { + if (await fs.pathExists(sourceConvertedDir)) { + const convertedOriginalFiles = await fs.readdir(sourceConvertedDir); + for (const convertedFile of convertedOriginalFiles) { + const convertedFileBaseName = path.basename(convertedFile, path.extname(convertedFile)); + if (convertedFileBaseName === baseFileName) { + await fs.ensureDir(targetConvertedDir); + await fs.move( + path.join(sourceConvertedDir, convertedFile), + path.join(targetConvertedDir, convertedFile), + { overwrite: true } + ); + logger.debug(`[MoveWorker] Moved ConvertedOriginal: ${convertedFile}`); + } + } + } + } catch (error) { + logger.warn(`[MoveWorker] Failed to move ConvertedOriginal for ${file}:`, error); + } + })() + ); + } + + if (billFileList.includes(file)) { + // Move bill file + moveOps.push( + fs + .move( + path.join(FolderPaths.Jobs, from_jobid, FolderPaths.BillsSubDir, file), + path.join(FolderPaths.Jobs, jobid, FolderPaths.BillsSubDir, file), + { overwrite: true } + ) + .then(() => logger.debug(`[MoveWorker] Moved bill file: ${file}`)) + .catch((err) => logger.warn(`[MoveWorker] Failed to move bill file ${file}:`, err)) + ); + + // Move bill thumbnails + const baseThumb = file.replace(/\.[^/.]+$/, ""); + for (const ext of [".jpg", ".png"]) { + moveOps.push( + fs + .move( + path.join( + FolderPaths.Jobs, + from_jobid, + FolderPaths.BillsSubDir, + FolderPaths.ThumbsSubDir, + `${baseThumb}${ext}` + ), + path.join( + FolderPaths.Jobs, + jobid, + FolderPaths.BillsSubDir, + FolderPaths.ThumbsSubDir, + `${baseThumb}${ext}` + ), + { overwrite: true } + ) + .then(() => logger.debug(`[MoveWorker] Moved bill thumbnail: ${baseThumb}${ext}`)) + .catch(() => {}) // Thumbnails might not exist + ); + } + + // Move bill ConvertedOriginal file if it exists + const billBaseFileName = file.replace(/\.[^/.]+$/, ""); + const sourceBillConvertedDir = path.join(FolderPaths.Jobs, from_jobid, FolderPaths.BillsSubDir, FolderPaths.ConvertedOriginalSubDir); + const targetBillConvertedDir = path.join(FolderPaths.Jobs, jobid, FolderPaths.BillsSubDir, FolderPaths.ConvertedOriginalSubDir); + + moveOps.push( + (async () => { + try { + if (await fs.pathExists(sourceBillConvertedDir)) { + const convertedOriginalFiles = await fs.readdir(sourceBillConvertedDir); + for (const convertedFile of convertedOriginalFiles) { + const convertedFileBaseName = path.basename(convertedFile, path.extname(convertedFile)); + if (convertedFileBaseName === billBaseFileName) { + await fs.ensureDir(targetBillConvertedDir); + await fs.move( + path.join(sourceBillConvertedDir, convertedFile), + path.join(targetBillConvertedDir, convertedFile), + { overwrite: true } + ); + logger.debug(`[MoveWorker] Moved bill ConvertedOriginal: ${convertedFile}`); + } + } + } + } catch (error) { + logger.warn(`[MoveWorker] Failed to move bill ConvertedOriginal for ${file}:`, error); + } + })() + ); + } + + processedFiles++; + if (job) { + const progress = 15 + Math.round((processedFiles / totalFiles) * 70); + await job.updateProgress(progress); + } + } + + if (job) await job.updateProgress(90); + + const results = await Promise.allSettled(moveOps); + const failed = results.filter(r => r.status === 'rejected').length; + const moved = results.filter(r => r.status === 'fulfilled').length; + + logger.debug(`[MoveWorker] Move operation completed: ${moved} successful, ${failed} failed`); + + return { moved, failed }; + } catch (error) { + logger.error("[MoveWorker] Error in processMoveOperation:", error); + throw error; + } +} + export async function JobsMoveMedia(req: Request, res: Response) { const jobid: string = (req.body.jobid || "").trim(); const from_jobid: string = (req.body.from_jobid || "").trim(); - const files: string[] = req.body.files; //Just file names. + const files: string[] = req.body.files || []; try { - //Validate the request is valid and contains everything that it needs. - if (from_jobid === "") { - res.status(400).json({ error: "from_jobid must be specified. " }); + if (!from_jobid) { + res.status(400).json({ error: "from_jobid must be specified." }); return; } - if (files.length === 0) { - res.status(400).json({ error: "files must be specified. " }); + if (!files.length) { + res.status(400).json({ error: "files must be specified." }); return; } - // Setup lists for both file locations - const jobFileList: string[] = ( - await fs.readdir(PathToRoFolder(from_jobid), { - withFileTypes: true - }) - ) - .filter((f) => f.isFile() && ListableChecker(f)) - .map((dirent) => dirent.name); - const billFileList: string[] = ( - await fs.readdir(PathToRoBillsFolder(from_jobid), { - withFileTypes: true - }) - ) - .filter((f) => f.isFile() && ListableChecker(f)) - .map((dirent) => dirent.name); - - //Make sure the destination RO directory exists. - await fs.ensureDir(PathToRoFolder(jobid)); - logger.debug("Moving job based media.", { jobid, from_jobid, files }); - const movingQueue: Promise[] = []; - - files.forEach((file) => { - if (jobFileList.includes(file)) { - movingQueue.push( - fs.move(path.join(FolderPaths.Jobs, from_jobid, file), path.join(FolderPaths.Jobs, jobid, file)) - ); - - movingQueue.push( - fs.move( - path.join(FolderPaths.Jobs, from_jobid, FolderPaths.ThumbsSubDir, file.replace(/\.[^/.]+$/, ".png")), - path.join(FolderPaths.Jobs, jobid, FolderPaths.ThumbsSubDir, file.replace(/\.[^/.]+$/, ".png")) - ) - ); - } - if (billFileList.includes(file)) { - movingQueue.push( - fs.move( - path.join(FolderPaths.Jobs, from_jobid, FolderPaths.BillsSubDir, file), - path.join(FolderPaths.Jobs, jobid, FolderPaths.BillsSubDir, file) - ) - ); - - movingQueue.push( - fs.move( - path.join( - FolderPaths.Jobs, - from_jobid, - FolderPaths.BillsSubDir, - FolderPaths.ThumbsSubDir, - file.replace(/\.[^/.]+$/, ".png") - ), - path.join( - FolderPaths.Jobs, - jobid, - FolderPaths.BillsSubDir, - FolderPaths.ThumbsSubDir, - file.replace(/\.[^/.]+$/, ".png") - ) - ) - ); - } - }); - - //Use AllSettled as it allows for individual moves to fail. - //e.g. if the thumbnail does not exist. - await Promise.allSettled(movingQueue); + // For small operations (1-3 files), process synchronously for immediate feedback + if (files.length <= 3) { + logger.debug("Processing small move operation synchronously"); + await processMoveOperation(jobid, from_jobid, files); + JobsListMedia(req, res); + return; + } + // For larger operations, use BullMQ but still return updated file list + logger.debug(`[JobsMoveMedia] Queuing move operation for ${files.length} files`); + const job = await moveQueue.add("moveMedia", { jobid, from_jobid, files }); + + // Return the updated file list immediately (optimistic update) JobsListMedia(req, res); + + // Process in background - if it fails, files will be back on next refresh + job.waitUntilFinished(moveQueueEvents) + .then(() => { + logger.debug(`[JobsMoveMedia] Background move completed for job ${job.id}`); + }) + .catch((error) => { + logger.error(`[JobsMoveMedia] Background move failed for job ${job.id}:`, error); + }); + } catch (err) { logger.error("Error moving job media", { from_jobid, @@ -98,6 +308,38 @@ export async function JobsMoveMedia(req: Request, res: Response) { files, err }); - res.status(500).send(err); + res.status(500).json({ error: "Failed to queue move operation", details: err }); + } +} + +/** + * Get the status of a move operation job + */ +export async function JobsMoveStatus(req: Request, res: Response) { + const { jobId } = req.params; + + try { + const job = await Job.fromId(moveQueue, jobId); + + if (!job) { + res.status(404).json({ error: "Job not found" }); + return; + } + + const state = await job.getState(); + const progress = job.progress; + + res.json({ + jobId, + state, + progress, + data: job.data, + finishedOn: job.finishedOn, + processedOn: job.processedOn, + failedReason: job.failedReason + }); + } catch (error) { + logger.error("Error getting move job status:", error); + res.status(500).json({ error: "Failed to get job status" }); } } diff --git a/jobs/jobsUploadMedia.ts b/jobs/jobsUploadMedia.ts index 60166d8..0003a7e 100644 --- a/jobs/jobsUploadMedia.ts +++ b/jobs/jobsUploadMedia.ts @@ -1,3 +1,5 @@ +import { Job, Queue, QueueEvents, Worker } from "bullmq"; +import dotenv from "dotenv"; import { Request, Response } from "express"; import fs from "fs-extra"; import multer from "multer"; @@ -5,10 +7,151 @@ import path from "path"; import { logger } from "../server.js"; import GenerateThumbnail from "../util/generateThumbnail.js"; import generateUniqueFilename from "../util/generateUniqueFilename.js"; -import { ConvertHeicFiles } from "../util/heicConverter.js"; +import { convertHeicFiles } from "../util/heicConverter.js"; import { PathToRoFolder } from "../util/pathGenerators.js"; import { JobsListMedia } from "./jobsListMedia.js"; +dotenv.config({ + path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) +}); + +const UPLOAD_QUEUE_NAME = "uploadProcessingQueue"; + +const connectionOpts = { + host: "localhost", + port: 6379, + maxRetriesPerRequest: 3, + enableReadyCheck: true, + reconnectOnError: (err: Error) => err.message.includes("READONLY") +}; + +const uploadProcessingQueue = new Queue(UPLOAD_QUEUE_NAME, { + connection: connectionOpts, + defaultJobOptions: { + removeOnComplete: true, + removeOnFail: true, + attempts: 3, + backoff: { type: "exponential", delay: 1000 } + } +}); + +const uploadQueueEvents = new QueueEvents(UPLOAD_QUEUE_NAME, { + connection: connectionOpts +}); + +// Upload processing worker +const uploadWorker = new Worker( + UPLOAD_QUEUE_NAME, + async ( + job: Job<{ + files: Express.Multer.File[]; + jobid: string; + skipThumbnails: boolean; + }> + ) => { + const { files, jobid, skipThumbnails } = job.data; + try { + logger.debug(`Processing upload for job ${jobid} with ${files.length} files, skipThumbnails: ${skipThumbnails}`); + await job.updateProgress(10); + + // Convert HEIC files if any + await convertHeicFiles(files); + await job.updateProgress(40); + + if (!skipThumbnails) { + // Log file states after conversion for debugging + logger.debug("File states after HEIC conversion:", files.map(f => ({ + filename: f.filename, + mimetype: f.mimetype, + path: f.path + }))); + + // Check if converted files actually exist on disk + for (const file of files) { + const exists = await fs.pathExists(file.path); + logger.debug(`File existence check: ${file.filename} at ${file.path} - exists: ${exists}`); + } + + // After HEIC conversion, all files should be ready for thumbnail generation + // Only exclude files that are still HEIC (which shouldn't happen after conversion) + const filesToThumbnail = []; + for (const file of files) { + const isStillHeic = + file.mimetype === "image/heic" || + file.mimetype === "image/heif" || + /\.heic$/i.test(file.filename) || + /\.heif$/i.test(file.filename); + + logger.debug(`File ${file.filename} - mimetype: ${file.mimetype}, isStillHeic: ${isStillHeic}, including: ${!isStillHeic}`); + + if (!isStillHeic) { + filesToThumbnail.push(file); + } + } + + logger.debug( + `Generating thumbnails for ${filesToThumbnail.length} files (excluding HEIC)`, + filesToThumbnail.map((f) => f.filename) + ); + + // Generate thumbnails + logger.debug(`About to generate thumbnails for ${filesToThumbnail.length} files`); + const thumbnailPromises = filesToThumbnail.map((file, index) => { + logger.debug(`Starting thumbnail generation for file ${index + 1}/${filesToThumbnail.length}: ${file.filename} at ${file.path}`); + return GenerateThumbnail(file.path); + }); + const thumbnailResults = await Promise.all(thumbnailPromises); + logger.debug(`Thumbnail generation completed. Results:`, thumbnailResults); + await job.updateProgress(90); + } + + logger.debug(`Upload processing completed for job ${jobid}`); + await job.updateProgress(100); + + return { success: true, filesProcessed: files.length }; + } catch (error) { + logger.error(`Error processing upload for job ${jobid}:`, error); + throw error; + } + }, + { + connection: connectionOpts, + concurrency: 2 // Allow 2 upload processing jobs to run concurrently + } +); + +// Event listeners for upload queue and worker +uploadProcessingQueue.on("waiting", (job) => { + logger.debug(`[UploadQueue] Job waiting: ${job.data.jobid}`); +}); +uploadProcessingQueue.on("error", (error) => { + logger.error(`[UploadQueue] Queue error:`, error); +}); + +uploadWorker.on("ready", () => { + logger.debug("[UploadWorker] Worker ready"); +}); +uploadWorker.on("active", (job, prev) => { + logger.debug(`[UploadWorker] Job ${job.id} active (previous: ${prev})`); +}); +uploadWorker.on("completed", async (job) => { + logger.debug(`[UploadWorker] Job ${job.id} completed`); + await job.remove(); + logger.debug(`[UploadWorker] Job ${job.id} removed from Redis`); +}); +uploadWorker.on("failed", (jobId, reason) => { + logger.error(`[UploadWorker] Job ${jobId} failed: ${reason}`); +}); +uploadWorker.on("error", (error) => { + logger.error(`[UploadWorker] Worker error:`, error); +}); +uploadWorker.on("stalled", (job) => { + logger.error(`[UploadWorker] Worker stalled: ${job}`); +}); +uploadWorker.on("ioredis:close", () => { + logger.error("[UploadWorker] Redis connection closed"); +}); + export const JobMediaUploadMulter = multer({ storage: multer.diskStorage({ destination: function (req, file, cb) { @@ -30,37 +173,46 @@ export async function jobsUploadMedia(req: Request, res: Response) { const jobid: string = (req.body.jobid || "").trim(); try { - if (!req.files || (req.files as Express.Multer.File[]).length === 0) { + const files = req.files as Express.Multer.File[] | undefined; + if (!files || files.length === 0) { logger.warning("Upload contained no files."); res.status(400).send({ status: false, message: "No file uploaded" }); - } else { - //If we want to skip waiting for everything, just send it back that we're good. - if (req.body.skip_thumbnail) { - req.headers.skipReponse === "true"; - res.sendStatus(200); - } - - //Check if there's a heic in the file set. If so, modify the file set. - await ConvertHeicFiles(req.files as Express.Multer.File[]); - - logger.debug( - "Creating thumbnails for newly uploaded media", - (req.files as Express.Multer.File[]).map((f) => f.filename) - ); - const thumbnailGenerationQueue: Promise[] = []; - - //for each file.path, generate the thumbnail. - (req.files as Express.Multer.File[]).forEach((file) => { - thumbnailGenerationQueue.push(GenerateThumbnail(file.path)); - }); - - await Promise.all(thumbnailGenerationQueue); - - JobsListMedia(req, res); + return; } + + // Check if client wants to skip waiting for response but still do processing + if (req.body.skip_thumbnail) { + logger.debug("Client requested skip_thumbnail - responding immediately but processing in background"); + // Set header to indicate we're skipping the response wait + req.headers.skipResponse = "true"; + res.sendStatus(200); + + // Continue with background processing (conversion + thumbnails) + const job = await uploadProcessingQueue.add("processUpload", { + files, + jobid, + skipThumbnails: false // Always do thumbnails, just don't wait for completion + }); + return; + } + + // Add upload processing job to queue and wait for completion + logger.debug(`Adding upload processing job for ${files.length} files with thumbnails enabled`); + const job = await uploadProcessingQueue.add("processUpload", { + files, + jobid, + skipThumbnails: false + }); + + // Wait for the job to complete + await job.waitUntilFinished(uploadQueueEvents); + logger.debug(`Upload processing job completed for job ${jobid}`); + + // Return the updated media list + JobsListMedia(req, res); } catch (error) { logger.error("Error uploading job media.", { jobid, diff --git a/package-lock.json b/package-lock.json index adb0a9a..7ef0a4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,22 +9,22 @@ "version": "1.0.13", "license": "UNLICENSED", "dependencies": { - "@types/compression": "^1.7.5", - "axios": "^1.8.1", - "body-parser": "^1.20.3", - "bullmq": "^5.41.7", + "@types/compression": "^1.8.1", + "axios": "^1.10.0", + "body-parser": "^2.2.0", + "bullmq": "^5.56.4", "compression": "^1.8.0", "cors": "^2.8.5", - "dotenv": "16.4.7", - "express": "^4.21.2", - "file-type": "^20.4.0", + "dotenv": "17.2.0", + "express": "^5.1.0", + "file-type": "^21.0.0", "fs-extra": "^11.3.0", "gm": "^1.25.1", - "helmet": "^8.0.0", + "helmet": "^8.1.0", "image-thumbnail": "^1.0.17", "jszip": "^3.10.1", "morgan": "^1.10.0", - "multer": "^1.4.4", + "multer": "^2.0.1", "nocache": "^4.0.0", "response-time": "^2.3.3", "simple-thumbnail": "^1.6.5", @@ -32,20 +32,20 @@ "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { - "@types/cors": "^2.8.17", - "@types/express": "^5.0.0", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", "@types/fs-extra": "^11.0.4", "@types/gm": "^1.25.4", "@types/image-thumbnail": "^1.0.4", - "@types/morgan": "^1.9.9", - "@types/multer": "^1.4.12", - "@types/node": "^22.13.9", - "@types/response-time": "^2.3.8", - "nodemon": "^3.1.9", - "prettier": "^3.5.3", + "@types/morgan": "^1.9.10", + "@types/multer": "^2.0.0", + "@types/node": "^24.0.13", + "@types/response-time": "^2.3.9", + "nodemon": "^3.1.10", + "prettier": "^3.6.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "typescript": "^5.8.2" + "typescript": "^5.8.3" }, "engines": { "node": ">=18.0.0" @@ -543,17 +543,17 @@ ] }, "node_modules/@tokenizer/inflate": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.6.tgz", - "integrity": "sha512-SdR/i05U7Xhnsq36iyIq/ZiGGw4PKzw4ww3bOq80Pjj4wyXpqyTcgrgdDdGlcatnlvzNJx8CQw3hp6QZvkUwhA==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", "license": "MIT", "dependencies": { - "debug": "^4.3.7", + "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" }, "engines": { - "node": ">=16" + "node": ">=18" }, "funding": { "type": "github", @@ -561,9 +561,9 @@ } }, "node_modules/@tokenizer/inflate/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -580,7 +580,8 @@ "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" }, "node_modules/@tsconfig/node10": { "version": "1.0.8", @@ -621,12 +622,13 @@ } }, "node_modules/@types/compression": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.5.tgz", - "integrity": "sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==", "license": "MIT", "dependencies": { - "@types/express": "*" + "@types/express": "*", + "@types/node": "*" } }, "node_modules/@types/connect": { @@ -639,23 +641,23 @@ } }, "node_modules/@types/cors": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", - "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/express": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", - "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", - "@types/qs": "*", "@types/serve-static": "*" } }, @@ -715,30 +717,32 @@ "license": "MIT" }, "node_modules/@types/morgan": { - "version": "1.9.9", - "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.9.tgz", - "integrity": "sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==", + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/multer": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz", - "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", "dev": true, + "license": "MIT", "dependencies": { "@types/express": "*" } }, "node_modules/@types/node": { - "version": "22.13.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", - "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", + "version": "24.0.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz", + "integrity": "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==", "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~7.8.0" } }, "node_modules/@types/qs": { @@ -754,10 +758,11 @@ "license": "MIT" }, "node_modules/@types/response-time": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/@types/response-time/-/response-time-2.3.8.tgz", - "integrity": "sha512-7qGaNYvdxc0zRab8oHpYx7AW17qj+G0xuag1eCrw3M2VWPJQ/HyKaaghWygiaOUl0y9x7QGQwppDpqLJ5V9pzw==", + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@types/response-time/-/response-time-2.3.9.tgz", + "integrity": "sha512-w5i5/y/1N3hkSBru1dat7Pf/YzdFLAANbKR78i2VIPnKw1Ub2ZNXE/n3K4v1BBMIIbAccgxpGZT8lIuLW284Dw==", "dev": true, + "license": "MIT", "dependencies": { "@types/express": "*", "@types/node": "*" @@ -796,13 +801,34 @@ "license": "ISC" }, "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" }, "engines": { "node": ">= 0.6" @@ -858,12 +884,6 @@ "dev": true, "license": "MIT" }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", - "license": "MIT" - }, "node_modules/array-parallel": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/array-parallel/-/array-parallel-0.1.3.tgz", @@ -888,9 +908,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", - "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -928,27 +948,84 @@ } }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=18" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/body-parser/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/body-parser/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/body-parser/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" } }, "node_modules/brace-expansion": { @@ -981,9 +1058,9 @@ "license": "MIT" }, "node_modules/bullmq": { - "version": "5.41.7", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.7.tgz", - "integrity": "sha512-eZbKJSx15bflfzKRiR+dKeLTr/M/YKb4cIp73OdU79PEMHQ6aEFUtbG6R+f0KvLLznI/O01G581U2Eqli6S2ew==", + "version": "5.56.4", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.56.4.tgz", + "integrity": "sha512-5wSHd0oXs2jS6P+6tay/01Iz0cWRK8iYcscKtpS/GewEq0bJZwbkMZc77GJVOT9SP+UQuXA2y+pQTdCQJel7kQ==", "license": "MIT", "dependencies": { "cron-parser": "^4.9.0", @@ -996,35 +1073,16 @@ } }, "node_modules/busboy": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", - "integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", "dependencies": { - "dicer": "0.2.5", - "readable-stream": "1.1.x" + "streamsearch": "^1.1.0" }, "engines": { - "node": ">=0.8.0" + "node": ">=10.16.0" } }, - "node_modules/busboy/node_modules/readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/busboy/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "license": "MIT" - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1047,13 +1105,13 @@ } }, "node_modules/call-bound": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -1228,45 +1286,24 @@ "license": "MIT" }, "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", "engines": [ - "node >= 0.8" + "node >= 6.0" ], "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", - "readable-stream": "^2.2.2", + "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, - "node_modules/concat-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "license": "MIT" - }, - "node_modules/concat-stream/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -1313,10 +1350,13 @@ } }, "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", - "license": "MIT" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } }, "node_modules/core-util-is": { "version": "1.0.3", @@ -1411,16 +1451,6 @@ "node": ">= 0.8" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/detect-libc": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", @@ -1429,36 +1459,6 @@ "node": ">=8" } }, - "node_modules/dicer": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", - "integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==", - "dependencies": { - "readable-stream": "1.1.x", - "streamsearch": "0.1.2" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/dicer/node_modules/readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/dicer/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "license": "MIT" - }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -1470,9 +1470,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz", + "integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -1582,70 +1582,107 @@ } }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { - "node": ">= 0.10.0" + "node": ">= 18" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/express" } }, - "node_modules/express/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" + "node_modules/express/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true } - ], - "license": "MIT" + } + }, + "node_modules/express/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } }, "node_modules/fecha": { "version": "4.2.3", @@ -1668,18 +1705,18 @@ } }, "node_modules/file-type": { - "version": "20.4.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.0.tgz", - "integrity": "sha512-+NZeExsi4G6EWaMbSmvBeCoqsj9EqNvOj1o/0uPVPW4O51FSCmxFlNEp/PitsqBMCbax4cGoaYmnUK5FLTuG4g==", + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz", + "integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==", "license": "MIT", "dependencies": { - "@tokenizer/inflate": "^0.2.6", - "strtok3": "^10.2.0", + "@tokenizer/inflate": "^0.2.7", + "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sindresorhus/file-type?sponsor=1" @@ -1698,23 +1735,39 @@ } }, "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", "license": "MIT", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { "node": ">= 0.8" } }, + "node_modules/finalhandler/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/fn.name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", @@ -1763,12 +1816,12 @@ } }, "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/fs-extra": { @@ -1809,17 +1862,17 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", + "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "get-proto": "^1.0.0", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", @@ -1936,9 +1989,9 @@ } }, "node_modules/helmet": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.0.0.tgz", - "integrity": "sha512-VyusHLEIIO5mjQPUI1wpOAEu+wl6Q0998jzTxqUYGE45xCIcAxy3MsbEK/yyJUJ3ADeMoB6MornPH6GMWAf+Pw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -1961,11 +2014,12 @@ } }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" @@ -1988,7 +2042,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/ignore-by-default": { "version": "1.0.1", @@ -2140,6 +2195,12 @@ "node": ">=0.12.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -2152,12 +2213,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2297,35 +2352,17 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", + "engines": { + "node": ">=18" + }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -2452,28 +2489,27 @@ } }, "node_modules/multer": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz", - "integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.1.tgz", + "integrity": "sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", - "busboy": "^0.2.11", - "concat-stream": "^1.5.2", - "mkdirp": "^0.5.4", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", "object-assign": "^4.1.1", - "on-finished": "^2.3.0", - "type-is": "^1.6.4", - "xtend": "^4.0.0" + "type-is": "^1.6.18", + "xtend": "^4.0.2" }, "engines": { - "node": ">= 0.10.0" + "node": ">= 10.16.0" } }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -2509,9 +2545,9 @@ } }, "node_modules/nodemon": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", - "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", "dev": true, "license": "MIT", "dependencies": { @@ -2675,22 +2711,12 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/peek-readable": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-6.1.1.tgz", - "integrity": "sha512-7QmvgRKhxM0E2PGV4ocfROItVode+ELI27n4q+lpufZ+tRKBu/pBP8WOmw9HXn2ui/AUizqtvaVQhcJrOkRqYg==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" + "node": ">=16" } }, "node_modules/picomatch": { @@ -2707,9 +2733,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", "bin": { @@ -2754,12 +2780,12 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -2786,13 +2812,14 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", - "iconv-lite": "0.4.24", + "iconv-lite": "0.6.3", "unpipe": "1.0.0" }, "engines": { @@ -2859,6 +2886,39 @@ "node": ">= 0.8.0" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -2877,7 +2937,8 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" }, "node_modules/semver": { "version": "7.6.3", @@ -2891,51 +2952,78 @@ } }, "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "license": "MIT", "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 18" } }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "node_modules/send/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" } }, "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "license": "MIT", "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 18" } }, "node_modules/setimmediate": { @@ -3162,11 +3250,11 @@ "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" }, "node_modules/streamsearch": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", - "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", "engines": { - "node": ">=0.8.0" + "node": ">=10.0.0" } }, "node_modules/string_decoder": { @@ -3189,13 +3277,12 @@ } }, "node_modules/strtok3": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.2.1.tgz", - "integrity": "sha512-Q2dTnW3UXokAvXmXvrvMoUj/me3LyJI76HNHeuGMh2o0As/vzd7eHV3ncLOyvu928vQIDbE7Vf9ldEnC7cwy1w==", + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.1.tgz", + "integrity": "sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw==", "license": "MIT", "dependencies": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^6.1.1" + "@tokenizer/token": "^0.3.0" }, "engines": { "node": ">=18" @@ -3246,9 +3333,10 @@ } }, "node_modules/token-types": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", - "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.3.tgz", + "integrity": "sha512-IKJ6EzuPPWtKtEIEPpIdXv9j5j2LGJEYk0CKY2efgKoYKLBiZdh6iQkLVBow/CB3phyWAWCyk+bZeaimJn6uRQ==", + "license": "MIT", "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" @@ -3362,13 +3450,13 @@ "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3398,9 +3486,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", "license": "MIT" }, "node_modules/universalify": { @@ -3415,7 +3503,7 @@ "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -3427,15 +3515,6 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "license": "MIT" }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -3806,19 +3885,19 @@ "optional": true }, "@tokenizer/inflate": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.6.tgz", - "integrity": "sha512-SdR/i05U7Xhnsq36iyIq/ZiGGw4PKzw4ww3bOq80Pjj4wyXpqyTcgrgdDdGlcatnlvzNJx8CQw3hp6QZvkUwhA==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", "requires": { - "debug": "^4.3.7", + "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" }, "dependencies": { "debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "requires": { "ms": "^2.1.3" } @@ -3864,11 +3943,12 @@ } }, "@types/compression": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.5.tgz", - "integrity": "sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==", "requires": { - "@types/express": "*" + "@types/express": "*", + "@types/node": "*" } }, "@types/connect": { @@ -3880,22 +3960,21 @@ } }, "@types/cors": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", - "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", "dev": true, "requires": { "@types/node": "*" } }, "@types/express": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", - "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "requires": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", - "@types/qs": "*", "@types/serve-static": "*" } }, @@ -3953,29 +4032,29 @@ "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" }, "@types/morgan": { - "version": "1.9.9", - "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.9.tgz", - "integrity": "sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==", + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", "dev": true, "requires": { "@types/node": "*" } }, "@types/multer": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz", - "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", "dev": true, "requires": { "@types/express": "*" } }, "@types/node": { - "version": "22.13.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", - "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", + "version": "24.0.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz", + "integrity": "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==", "requires": { - "undici-types": "~6.20.0" + "undici-types": "~7.8.0" } }, "@types/qs": { @@ -3989,9 +4068,9 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, "@types/response-time": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/@types/response-time/-/response-time-2.3.8.tgz", - "integrity": "sha512-7qGaNYvdxc0zRab8oHpYx7AW17qj+G0xuag1eCrw3M2VWPJQ/HyKaaghWygiaOUl0y9x7QGQwppDpqLJ5V9pzw==", + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@types/response-time/-/response-time-2.3.9.tgz", + "integrity": "sha512-w5i5/y/1N3hkSBru1dat7Pf/YzdFLAANbKR78i2VIPnKw1Ub2ZNXE/n3K4v1BBMIIbAccgxpGZT8lIuLW284Dw==", "dev": true, "requires": { "@types/express": "*", @@ -4028,12 +4107,27 @@ "dev": true }, "accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "dependencies": { + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "requires": { + "mime-db": "^1.54.0" + } + } } }, "acorn": { @@ -4069,11 +4163,6 @@ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, "array-parallel": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/array-parallel/-/array-parallel-0.1.3.tgz", @@ -4095,9 +4184,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "axios": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", - "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", "requires": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -4125,22 +4214,57 @@ "dev": true }, "body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "requires": { + "ms": "^2.1.3" + } + }, + "media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" + }, + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "requires": { + "mime-db": "^1.54.0" + } + }, + "type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "requires": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + } + } } }, "brace-expansion": { @@ -4168,9 +4292,9 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "bullmq": { - "version": "5.41.7", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.7.tgz", - "integrity": "sha512-eZbKJSx15bflfzKRiR+dKeLTr/M/YKb4cIp73OdU79PEMHQ6aEFUtbG6R+f0KvLLznI/O01G581U2Eqli6S2ew==", + "version": "5.56.4", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.56.4.tgz", + "integrity": "sha512-5wSHd0oXs2jS6P+6tay/01Iz0cWRK8iYcscKtpS/GewEq0bJZwbkMZc77GJVOT9SP+UQuXA2y+pQTdCQJel7kQ==", "requires": { "cron-parser": "^4.9.0", "ioredis": "^5.4.1", @@ -4182,30 +4306,11 @@ } }, "busboy": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", - "integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", "requires": { - "dicer": "0.2.5", - "readable-stream": "1.1.x" - }, - "dependencies": { - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } + "streamsearch": "^1.1.0" } }, "bytes": { @@ -4223,12 +4328,12 @@ } }, "call-bound": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "requires": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" } }, "chokidar": { @@ -4348,41 +4453,20 @@ "dev": true }, "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", "requires": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", - "readable-stream": "^2.2.2", + "readable-stream": "^3.0.2", "typedarray": "^0.0.6" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - } } }, "content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", "requires": { "safe-buffer": "5.2.1" }, @@ -4405,9 +4489,9 @@ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==" }, "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==" }, "core-util-is": { "version": "1.0.3", @@ -4477,43 +4561,11 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" }, - "destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" - }, "detect-libc": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==" }, - "dicer": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", - "integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==", - "requires": { - "readable-stream": "1.1.x", - "streamsearch": "0.1.2" - }, - "dependencies": { - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -4521,9 +4573,9 @@ "dev": true }, "dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==" + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz", + "integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==" }, "dunder-proto": { "version": "1.0.1", @@ -4598,47 +4650,74 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "requires": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "dependencies": { - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + "debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "requires": { + "ms": "^2.1.3" + } + }, + "media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" + }, + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "requires": { + "mime-db": "^1.54.0" + } + }, + "type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "requires": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + } } } }, @@ -4661,12 +4740,12 @@ } }, "file-type": { - "version": "20.4.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.0.tgz", - "integrity": "sha512-+NZeExsi4G6EWaMbSmvBeCoqsj9EqNvOj1o/0uPVPW4O51FSCmxFlNEp/PitsqBMCbax4cGoaYmnUK5FLTuG4g==", + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz", + "integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==", "requires": { - "@tokenizer/inflate": "^0.2.6", - "strtok3": "^10.2.0", + "@tokenizer/inflate": "^0.2.7", + "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } @@ -4681,17 +4760,26 @@ } }, "finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", "requires": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "dependencies": { + "debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "requires": { + "ms": "^2.1.3" + } + } } }, "fn.name": { @@ -4720,9 +4808,9 @@ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" }, "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==" }, "fs-extra": { "version": "11.3.0", @@ -4747,16 +4835,16 @@ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "get-intrinsic": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "requires": { - "call-bind-apply-helpers": "^1.0.1", + "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "get-proto": "^1.0.0", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", @@ -4832,9 +4920,9 @@ } }, "helmet": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.0.0.tgz", - "integrity": "sha512-VyusHLEIIO5mjQPUI1wpOAEu+wl6Q0998jzTxqUYGE45xCIcAxy3MsbEK/yyJUJ3ADeMoB6MornPH6GMWAf+Pw==" + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==" }, "http-errors": { "version": "2.0.0", @@ -4849,11 +4937,11 @@ } }, "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "requires": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "ieee754": { @@ -4967,16 +5055,16 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, "is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5087,19 +5175,9 @@ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, "merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==" }, "mime-db": { "version": "1.52.0", @@ -5192,24 +5270,23 @@ } }, "multer": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz", - "integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.1.tgz", + "integrity": "sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==", "requires": { "append-field": "^1.0.0", - "busboy": "^0.2.11", - "concat-stream": "^1.5.2", - "mkdirp": "^0.5.4", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", "object-assign": "^4.1.1", - "on-finished": "^2.3.0", - "type-is": "^1.6.4", - "xtend": "^4.0.0" + "type-is": "^1.6.18", + "xtend": "^4.0.2" } }, "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==" }, "nocache": { "version": "4.0.0", @@ -5231,9 +5308,9 @@ } }, "nodemon": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", - "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", "dev": true, "requires": { "chokidar": "^3.5.2", @@ -5340,14 +5417,9 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" }, "path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" - }, - "peek-readable": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-6.1.1.tgz", - "integrity": "sha512-7QmvgRKhxM0E2PGV4ocfROItVode+ELI27n4q+lpufZ+tRKBu/pBP8WOmw9HXn2ui/AUizqtvaVQhcJrOkRqYg==" + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==" }, "picomatch": { "version": "2.3.1", @@ -5356,9 +5428,9 @@ "dev": true }, "prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true }, "process-nextick-args": { @@ -5387,11 +5459,11 @@ "dev": true }, "qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "requires": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" } }, "queue": { @@ -5408,13 +5480,13 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", "requires": { "bytes": "3.1.2", "http-errors": "2.0.0", - "iconv-lite": "0.4.24", + "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, @@ -5459,6 +5531,28 @@ "on-headers": "~1.0.1" } }, + "router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "requires": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "dependencies": { + "debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "requires": { + "ms": "^2.1.3" + } + } + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -5480,41 +5574,55 @@ "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==" }, "send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "requires": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" }, "dependencies": { - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + "debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "requires": { + "ms": "^2.1.3" + } + }, + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "requires": { + "mime-db": "^1.54.0" + } } } }, "serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "requires": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" } }, "setimmediate": { @@ -5678,9 +5786,9 @@ "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" }, "streamsearch": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", - "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" }, "string_decoder": { "version": "1.1.1", @@ -5697,12 +5805,11 @@ "dev": true }, "strtok3": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.2.1.tgz", - "integrity": "sha512-Q2dTnW3UXokAvXmXvrvMoUj/me3LyJI76HNHeuGMh2o0As/vzd7eHV3ncLOyvu928vQIDbE7Vf9ldEnC7cwy1w==", + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.1.tgz", + "integrity": "sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw==", "requires": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^6.1.1" + "@tokenizer/token": "^0.3.0" } }, "supports-color": { @@ -5734,9 +5841,9 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, "token-types": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", - "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.3.tgz", + "integrity": "sha512-IKJ6EzuPPWtKtEIEPpIdXv9j5j2LGJEYk0CKY2efgKoYKLBiZdh6iQkLVBow/CB3phyWAWCyk+bZeaimJn6uRQ==", "requires": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" @@ -5805,12 +5912,12 @@ "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true }, "uint8array-extras": { @@ -5825,9 +5932,9 @@ "dev": true }, "undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==" }, "universalify": { "version": "2.0.0", @@ -5837,18 +5944,13 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, "uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", diff --git a/package.json b/package.json index 051e602..e0bba3f 100644 --- a/package.json +++ b/package.json @@ -13,22 +13,22 @@ "makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss,ts}\"" }, "dependencies": { - "@types/compression": "^1.7.5", - "axios": "^1.8.1", - "body-parser": "^1.20.3", - "bullmq": "^5.41.7", + "@types/compression": "^1.8.1", + "axios": "^1.10.0", + "body-parser": "^2.2.0", + "bullmq": "^5.56.4", "compression": "^1.8.0", "cors": "^2.8.5", - "dotenv": "16.4.7", - "express": "^4.21.2", - "file-type": "^20.4.0", + "dotenv": "17.2.0", + "express": "^5.1.0", + "file-type": "^21.0.0", "fs-extra": "^11.3.0", "gm": "^1.25.1", - "helmet": "^8.0.0", + "helmet": "^8.1.0", "image-thumbnail": "^1.0.17", "jszip": "^3.10.1", "morgan": "^1.10.0", - "multer": "^1.4.4", + "multer": "^2.0.1", "nocache": "^4.0.0", "response-time": "^2.3.3", "simple-thumbnail": "^1.6.5", @@ -36,19 +36,19 @@ "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { - "@types/cors": "^2.8.17", - "@types/express": "^5.0.0", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", "@types/fs-extra": "^11.0.4", "@types/gm": "^1.25.4", "@types/image-thumbnail": "^1.0.4", - "@types/morgan": "^1.9.9", - "@types/multer": "^1.4.12", - "@types/node": "^22.13.9", - "@types/response-time": "^2.3.8", - "nodemon": "^3.1.9", - "prettier": "^3.5.3", + "@types/morgan": "^1.9.10", + "@types/multer": "^2.0.0", + "@types/node": "^24.0.13", + "@types/response-time": "^2.3.9", + "nodemon": "^3.1.10", + "prettier": "^3.6.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "typescript": "^5.8.2" + "typescript": "^5.8.3" } } diff --git a/redis-docker-compose.yml b/redis-docker-compose.yml deleted file mode 100644 index ea3ab77..0000000 --- a/redis-docker-compose.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: '3' - -services: - redis-server: - image: "redis:alpine" - command: redis-server - ports: - - "6379:6379" \ No newline at end of file diff --git a/server.ts b/server.ts index ed62142..2c4c933 100644 --- a/server.ts +++ b/server.ts @@ -26,6 +26,7 @@ dotenv.config({ path: resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) }); +// Logger setup const commonTransportConfig = { maxSize: "20m", maxFiles: 14, @@ -42,10 +43,6 @@ const baseFormat = winston.format.combine( winston.format.prettyPrint() ); -const consoleTransport = new winston.transports.Console({ - format: winston.format.combine(winston.format.colorize(), winston.format.timestamp(), winston.format.simple()) -}); - export const logger = winston.createLogger({ format: baseFormat, level: "http", @@ -54,15 +51,13 @@ export const logger = winston.createLogger({ new DailyRotateFile({ filename: path.join(FolderPaths.Root, "logs", "exceptions-%DATE%.log"), ...commonTransportConfig - }), - consoleTransport + }) ], rejectionHandlers: [ new DailyRotateFile({ filename: path.join(FolderPaths.Root, "logs", "rejections-%DATE%.log"), ...commonTransportConfig - }), - consoleTransport + }) ], transports: [ new DailyRotateFile({ @@ -78,55 +73,48 @@ export const logger = winston.createLogger({ new DailyRotateFile({ filename: path.join(FolderPaths.Root, "logs", "ALL-%DATE%.log"), ...commonTransportConfig + }), + new winston.transports.Console({ + format: winston.format.combine(winston.format.colorize(), winston.format.timestamp(), winston.format.simple()) }) ] }); -logger.add( - new winston.transports.Console({ - format: winston.format.combine(winston.format.colorize(), winston.format.timestamp(), winston.format.simple()) - }) -); - +// App init const app: Express = express(); const port = process.env.PORT; +app.disable("x-powered-by"); +app.set("trust proxy", 1); + +// Middleware order: security, logging, parsing, etc. app.set("etag", false); +app.use(helmet({ crossOriginResourcePolicy: { policy: "cross-origin" } })); +app.use(nocache()); +app.use(cors()); app.use(compression()); +app.use(responseTime()); + +app.use(bodyParser.json({ limit: "1000mb" })); +app.use(bodyParser.urlencoded({ limit: "1000mb", extended: true })); + app.use((req, res, next) => { res.setHeader("Connection", "keep-alive"); next(); }); -app.use(nocache()); -app.use(bodyParser.json({ limit: "1000mb" })); -app.use(bodyParser.urlencoded({ limit: "1000mb", extended: true })); -app.use(responseTime()); -app.use(cors()); - -const morganMiddleware = morgan( - "combined", //":method :url :status :res[content-length] - :response-time ms" - { - stream: { - write: (message) => logger.http(message.trim()) - } - } -); +const morganMiddleware = morgan("combined", { + stream: { write: (message) => logger.http(message.trim()) } +}); app.use(morganMiddleware); -app.use(helmet({ crossOriginResourcePolicy: { policy: "cross-origin" } })); + +// Job endpoints app.post("/jobs/list", ValidateImsToken, validateJobRequest, JobsListMedia); app.post("/jobs/upload", ValidateImsToken, JobMediaUploadMulter.array("file"), validateJobRequest, jobsUploadMedia); app.post("/jobs/download", ValidateImsToken, validateJobRequest, jobsDownloadMedia); -app.post( - "/jobs/move", //JobRequestValidator, - ValidateImsToken, - JobsMoveMedia -); -app.post( - "/jobs/delete", //JobRequestValidator, - ValidateImsToken, - JobsDeleteMedia -); +app.post("/jobs/move", ValidateImsToken, JobsMoveMedia); +app.post("/jobs/delete", ValidateImsToken, JobsDeleteMedia); +// Bill endpoints app.post("/bills/list", BillRequestValidator, BillsListMedia); app.post( "/bills/upload", @@ -136,16 +124,19 @@ app.post( BillsUploadMedia ); -app.get("/", ValidateImsToken, (req: express.Request, res: express.Response) => { +// Health and root +app.get("/", ValidateImsToken, (req, res) => { res.send("IMS running."); }); -app.get("/health", (req: express.Request, res: express.Response) => { +app.get("/health", (req, res) => { res.status(200).send("OK"); }); +// Static files InitServer(); app.use(FolderPaths.StaticPath, express.static(FolderPaths.Root, { etag: false, maxAge: 30 * 1000 })); app.use("/assets", express.static("./assets", { etag: false, maxAge: 30 * 1000 })); + app.listen(port, () => { logger.info(`ImEX Media Server is running at http://localhost:${port}`); }); diff --git a/util/generateThumbnail.ts b/util/generateThumbnail.ts index 6486769..32eac2e 100644 --- a/util/generateThumbnail.ts +++ b/util/generateThumbnail.ts @@ -1,7 +1,8 @@ +import { Job, Queue, QueueEvents, Worker } from "bullmq"; import { fileTypeFromFile } from "file-type"; import { FileTypeResult } from "file-type/core"; import fs from "fs-extra"; -import { access } from "fs/promises"; +import { access, FileHandle, open as fsOpen } from "fs/promises"; import gm from "gm"; import imageThumbnail from "image-thumbnail"; import path from "path"; @@ -9,75 +10,187 @@ import { logger } from "../server.js"; import { AssetPaths, FolderPaths } from "./serverInit.js"; //@ts-ignore import simpleThumb from "simple-thumbnail"; -//const ffmpeg = require("ffmpeg-static"); -/** @returns {string} Returns the relative path from the file to the thumbnail on the server. This must be converted to a URL. */ -export default async function GenerateThumbnail(file: string) { - // const type: core.FileTypeResult | undefined = await ft.fileTypeFromFile(file); - const type: FileTypeResult | undefined = await fileTypeFromFile(file); - let thumbPath: string = path.join( - path.dirname(file), - FolderPaths.ThumbsSubDir, - `${path.parse(path.basename(file)).name}.jpg` - ); +const QUEUE_NAME = "thumbnailQueue"; + +const connectionOpts = { + host: "localhost", + port: 6379, + maxRetriesPerRequest: 3, + enableReadyCheck: true, + reconnectOnError: (err: Error) => err.message.includes("READONLY") +}; + +const thumbnailQueue = new Queue(QUEUE_NAME, { + connection: connectionOpts, + defaultJobOptions: { + removeOnComplete: true, + removeOnFail: true, + attempts: 3, + backoff: { type: "exponential", delay: 1000 } + } +}); + +const thumbnailQueueEvents = new QueueEvents(QUEUE_NAME, { + connection: connectionOpts +}); + +const thumbnailWorker = new Worker( + QUEUE_NAME, + async (job: Job<{ file: string }>) => { + const { file } = job.data; + logger.debug(`[ThumbnailWorker] Starting thumbnail generation for ${file}`); + try { + await job.updateProgress(10); + + const result = await processThumbnail(file, job); + + await job.updateProgress(100); + logger.debug(`[ThumbnailWorker] Completed thumbnail generation for ${file}`); + return result; + } catch (error) { + logger.error(`[ThumbnailWorker] Error generating thumbnail for ${file}:`, error); + throw error; + } + }, + { + connection: connectionOpts, + concurrency: 1 + } +); + +// Worker event listeners for logging +thumbnailWorker.on("ready", () => { + logger.debug("[ThumbnailWorker] Worker is ready"); +}); +thumbnailWorker.on("active", (job, prev) => { + logger.debug(`[ThumbnailWorker] Job ${job.id} active (previous: ${prev})`); +}); +thumbnailWorker.on("completed", async (job) => { + logger.debug(`[ThumbnailWorker] Job ${job.id} completed`); + await job.remove(); + logger.debug(`[ThumbnailWorker] Job ${job.id} removed from Redis`); +}); +thumbnailWorker.on("failed", (job, err) => { + logger.error(`[ThumbnailWorker] Job ${job?.id} failed:`, err); +}); +thumbnailWorker.on("stalled", (jobId) => { + logger.error(`[ThumbnailWorker] Job stalled: ${jobId}`); +}); +thumbnailWorker.on("error", (err) => { + logger.error("[ThumbnailWorker] Worker error:", err); +}); +thumbnailWorker.on("ioredis:close", () => { + logger.error("[ThumbnailWorker] Redis connection closed"); +}); + +// Queue event listeners +thumbnailQueue.on("waiting", (job) => { + logger.debug(`[ThumbnailQueue] Job waiting in queue: ${job.data.file}`); +}); +thumbnailQueue.on("error", (err) => { + logger.error("[ThumbnailQueue] Queue error:", err); +}); + +async function processThumbnail(file: string, job?: Job): Promise { + let fileHandle: FileHandle | null = null; + let thumbPath = "unknown"; + try { - //Ensure the thumbs directory exists. + await access(file, fs.constants.R_OK); + + // Wait for stable file size to avoid incomplete thumbnails + const size1 = (await fs.stat(file)).size; + await new Promise((r) => setTimeout(r, 100)); + const size2 = (await fs.stat(file)).size; + if (size1 !== size2) { + throw new Error("File is still being written to, skipping thumbnail generation"); + } + + fileHandle = await fsOpen(file, "r"); + + const type: FileTypeResult | undefined = await fileTypeFromFile(file); + const baseName = path.parse(path.basename(file)).name; + + thumbPath = path.join(path.dirname(file), FolderPaths.ThumbsSubDir, `${baseName}.jpg`); await fs.ensureDir(path.dirname(thumbPath)); - //Check for existing thumbnail if (await fs.pathExists(thumbPath)) { - logger.debug("Using existing thumbnail:", thumbPath); + logger.debug(`[ThumbnailWorker] Using existing thumbnail: ${thumbPath}`); return path.relative(path.dirname(file), thumbPath); } - //Check to see if the file is an image, PDF, or video. - if (!type?.mime) { - throw new Error("Unknown file type"); - } + if (!type?.mime) throw new Error("Unknown file type"); - if (type?.mime === "application/pdf" || type?.mime === "image/heic" || type?.mime === "image/heif") { - await GeneratePdfThumbnail(file, thumbPath); - } else if (type?.mime.startsWith("video")) { - await simpleThumb(file, thumbPath, "250x?", { - // path: ffmpeg, - }); + await fileHandle.close(); + fileHandle = null; + + if (["application/pdf", "image/heic", "image/heif"].includes(type.mime)) { + logger.debug(`[ThumbnailWorker] Generating PDF/HEIC thumbnail for: ${file}`); + await generatePdfThumbnail(file, thumbPath); + } else if (type.mime.startsWith("video")) { + logger.debug(`[ThumbnailWorker] Generating video thumbnail for: ${file}`); + await simpleThumb(file, thumbPath, "250x?"); } else { - logger.debug("Thumbnail being created for : " + thumbPath); - //Ignoring typescript as the interface is lacking a parameter. - // @ts-ignore - const thumbnail = await imageThumbnail(file, { + logger.debug(`[ThumbnailWorker] Generating image thumbnail for: ${file}`); + const thumbnailBuffer = await imageThumbnail(file, { responseType: "buffer", height: 250, - width: 250, - failOnError: false + width: 250 }); - await fs.writeFile(thumbPath, thumbnail); + await fs.writeFile(thumbPath, thumbnailBuffer); } + return path.relative(path.dirname(file), thumbPath); - } catch (err) { - logger.error("Error when genenerating thumbnail:", { + } catch (error) { + logger.error("[ThumbnailWorker] Error generating thumbnail:", { thumbPath, - err, - message: (err as Error).message + error, + message: (error as Error).message }); return path.relative(path.dirname(file), AssetPaths.File); + } finally { + if (fileHandle) { + try { + await fileHandle.close(); + } catch (closeError) { + logger.error("[ThumbnailWorker] Error closing file handle:", closeError); + } + } } } -async function GeneratePdfThumbnail(file: string, thumbPath: string) { - const fileOnDisk: Buffer = await fs.readFile(file); - return new Promise((resolve, reject) => { - gm(fileOnDisk) - .selectFrame(0) +async function generatePdfThumbnail(file: string, thumbPath: string): Promise { + return new Promise((resolve, reject) => { + gm(file + "[0]") // first page only .setFormat("jpg") .resize(250, 250, "!") .quality(75) - .write(thumbPath, (error) => { - if (error) { - reject(error); - } else { - resolve(thumbPath); - } - }); + .write(thumbPath, (err) => (err ? reject(err) : resolve(thumbPath))); }); } + +/** + * Add a thumbnail generation job and wait for result. + * Returns the relative path to the thumbnail or default file icon on error. + */ +export default async function GenerateThumbnail(file: string): Promise { + const baseName = path.parse(path.basename(file)).name; + const thumbPath = path.join(path.dirname(file), FolderPaths.ThumbsSubDir, `${baseName}.jpg`); + + if (await fs.pathExists(thumbPath)) { + logger.debug(`[GenerateThumbnail] Returning existing thumbnail immediately for ${file}`); + return path.relative(path.dirname(file), thumbPath); + } + + try { + logger.debug(`[GenerateThumbnail] Adding job to queue for ${file}`); + const job = await thumbnailQueue.add("generate", { file }); + const result = await job.waitUntilFinished(thumbnailQueueEvents); + logger.debug(`[GenerateThumbnail] Job completed for ${file}`); + return result as string; + } catch (error) { + logger.error(`[GenerateThumbnail] Job failed for ${file}:`, error); + return path.relative(path.dirname(file), AssetPaths.File); + } +} diff --git a/util/heicConverter.ts b/util/heicConverter.ts index a5c81ef..2bd8fdf 100644 --- a/util/heicConverter.ts +++ b/util/heicConverter.ts @@ -1,4 +1,4 @@ -import { Job, Queue, Worker } from "bullmq"; +import { Job, Queue, QueueEvents, Worker } from "bullmq"; import dotenv from "dotenv"; import { fileTypeFromFile } from "file-type"; import { FileTypeResult } from "file-type/core"; @@ -10,60 +10,112 @@ import { logger } from "../server.js"; import { generateUniqueHeicFilename } from "./generateUniqueFilename.js"; import { FolderPaths } from "./serverInit.js"; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -const HeicQueue = new Queue("HEIC Queue", { - connection: { - host: "localhost", - port: 6379, - maxRetriesPerRequest: 3, - enableReadyCheck: true, - reconnectOnError: function (err) { - const targetError = "READONLY"; - return err.message.includes(targetError); - } - }, - defaultJobOptions: { - removeOnComplete: true, - removeOnFail: true, - attempts: 3, - backoff: { - type: "exponential", - delay: 1000 - } - } -}); -const cleanupINTERVAL = 1000 * 60 * 10; -setInterval(cleanupQueue, cleanupINTERVAL); - dotenv.config({ path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) }); -const imageMagick = gm.subClass({ imageMagick: true }); +const QUEUE_NAME = "heicQueue"; + +const connectionOpts = { + host: "localhost", + port: 6379, + maxRetriesPerRequest: 3, + enableReadyCheck: true, + reconnectOnError: (err: Error) => err.message.includes("READONLY") +}; + +const heicQueue = new Queue(QUEUE_NAME, { + connection: connectionOpts, + defaultJobOptions: { + removeOnComplete: true, + removeOnFail: true, + attempts: 3, + backoff: { type: "exponential", delay: 1000 } + } +}); + +// Re-added QueueEvents for waiting on job completion +const heicQueueEvents = new QueueEvents(QUEUE_NAME, { + connection: connectionOpts +}); + +const CLEANUP_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes +setInterval(cleanupQueue, CLEANUP_INTERVAL_MS); async function cleanupQueue() { - const ONE_HOUR = 1000 * 60 * 60; - const SIX_HOURS = ONE_HOUR * 6; try { - // Clean completed jobs older than 1 hour - await HeicQueue.clean(ONE_HOUR, 500, "completed"); + const ONE_HOUR = 60 * 60 * 1000; + const SIX_HOURS = 6 * ONE_HOUR; - // Clean failed jobs older than 24 hours - await HeicQueue.clean(SIX_HOURS, 500, "failed"); + await heicQueue.clean(ONE_HOUR, 500, "completed"); + await heicQueue.clean(SIX_HOURS, 500, "failed"); - // Get queue health - const jobCounts = await HeicQueue.getJobCounts(); - logger.log("debug", `Queue status: ${JSON.stringify(jobCounts)}`); + const counts = await heicQueue.getJobCounts(); + logger.debug(`HEIC Queue status: ${JSON.stringify(counts)}`); } catch (error) { - logger.log("error", `Queue cleanup error: ${error}`); + logger.error("HEIC Queue cleanup error:", error); } } -export async function ConvertHeicFiles(files: Express.Multer.File[]) { - const validFiles = await filterValidHeicFiles(files); +/** + * Filter files to include only valid HEIC images. + */ +async function filterHeicFiles(files: Express.Multer.File[]) { + const valid: Express.Multer.File[] = []; + for (const file of files) { + const type: FileTypeResult | undefined = await fileTypeFromFile(file.path); + if (type?.mime === "image/heic") valid.push(file); + } + return valid; +} - const jobs = validFiles.map((file) => ({ +/** + * Handle original file based on environment variable. + */ +async function handleOriginalFile(fileInfo: { path: string; destination: string; originalFilename: string }) { + try { + if (process.env.KEEP_CONVERTED_ORIGINALS) { + const destDir = path.join(fileInfo.destination, FolderPaths.ConvertedOriginalSubDir); + await fs.ensureDir(destDir); + await fs.move(fileInfo.path, path.join(destDir, fileInfo.originalFilename), { overwrite: true }); + } else { + await fs.unlink(fileInfo.path); + } + } catch (error) { + logger.error("Error handling original file:", error); + throw error; + } +} + +/** + * Convert HEIC to JPEG using GraphicsMagick stream. + */ +async function convertToJpeg(inputPath: string, outputPath: string): Promise { + return new Promise((resolve, reject) => { + const readStream = fs.createReadStream(inputPath); + const writeStream = fs.createWriteStream(outputPath); + + gm(readStream) + .setFormat("jpg") + .stream() + .pipe(writeStream) + .on("finish", () => resolve(outputPath)) + .on("error", reject); + }); +} + +/** + * Add HEIC files to the conversion queue and wait for job completion. + */ +export async function convertHeicFiles(files: Express.Multer.File[]) { + const heicFiles = await filterHeicFiles(files); + + if (heicFiles.length === 0) { + logger.debug("No HEIC files found to convert."); + return; + } + + const jobs = heicFiles.map((file) => ({ name: file.filename, data: { convertedFileName: generateUniqueHeicFilename(file), @@ -75,134 +127,99 @@ export async function ConvertHeicFiles(files: Express.Multer.File[]) { } })); - await HeicQueue.addBulk(jobs); - - const fileMap = new Map(files.map((file, index) => [file.filename, index])); - jobs.forEach((job) => { - const fileIndex = fileMap.get(job.data.fileInfo.originalFilename); - if (fileIndex !== undefined) { - files[fileIndex].filename = job.data.convertedFileName; - files[fileIndex].mimetype = "image/jpeg"; - } - }); -} - -async function filterValidHeicFiles(files: Express.Multer.File[]) { - const validFiles = []; - for (const file of files) { - const type: FileTypeResult | undefined = await fileTypeFromFile(file.path); - if (type?.mime === "image/heic") { - validFiles.push(file); + // Add jobs and wait for completion of each before proceeding + for (const jobData of jobs) { + try { + const job = await heicQueue.add(jobData.name, jobData.data); + await job.waitUntilFinished(heicQueueEvents); + logger.debug(`Job ${job.id} finished successfully.`); + } catch (error) { + logger.error(`Job for ${jobData.data.fileInfo.originalFilename} failed:`, error); + // Depending on your error handling strategy you might rethrow or continue } } - return validFiles; -} -async function handleOriginalFile(fileInfo: { path: string; destination: string; originalFilename: string }) { - try { - if (process.env.KEEP_CONVERTED_ORIGINALS) { - await fs.ensureDir(path.join(fileInfo.destination, FolderPaths.ConvertedOriginalSubDir)); - await fs.move( - fileInfo.path, - `${path.join(fileInfo.destination, FolderPaths.ConvertedOriginalSubDir)}/${fileInfo.originalFilename}` - ); - } else { - await fs.unlink(fileInfo.path); + // Update original files list with new names, mimetype, and path + const filenameToIndex = new Map(files.map((f, i) => [f.filename, i])); + for (const { data } of jobs) { + const idx = filenameToIndex.get(data.fileInfo.originalFilename); + if (idx !== undefined) { + const oldPath = files[idx].path; + files[idx].filename = data.convertedFileName; + files[idx].mimetype = "image/jpeg"; + files[idx].path = path.join(data.fileInfo.destination, data.convertedFileName); + logger.debug(`Updated file entry: ${data.fileInfo.originalFilename} -> ${data.convertedFileName}`, { + oldPath, + newPath: files[idx].path, + newMimetype: files[idx].mimetype + }); } - } catch (error) { - logger.log("error", `Error handling original file: ${error}`); - throw error; } } -async function ConvertToJpeg(file: string, newPath: string) { - // const fileOnDisk: Buffer = await fs.readFile(file); - - // return new Promise((resolve, reject) => { - // imageMagick(fileOnDisk) - // .setFormat("jpg") - // .write(newPath, (error) => { - // if (error) reject(error.message); - // resolve(newPath); - // }); - // }); - return new Promise((resolve, reject) => { - const readStream = fs.createReadStream(file); - const writeStream = fs.createWriteStream(newPath); - - imageMagick(readStream) - .setFormat("jpg") - .stream() - .pipe(writeStream) - .on("finish", () => resolve(newPath)) - .on("error", (error) => reject(error.message)); - }); -} - -const HeicWorker = new Worker( - "HEIC Queue", - async (job: Job) => { +// Worker processing HEIC conversion jobs +const heicWorker = new Worker( + QUEUE_NAME, + async ( + job: Job<{ + fileInfo: { path: string; destination: string; originalFilename: string }; + convertedFileName: string; + }> + ) => { const { fileInfo, convertedFileName } = job.data; try { - logger.log("debug", `Attempting to Convert ${fileInfo.originalFilename} image to JPEG from HEIC.`); + logger.debug(`Converting ${fileInfo.originalFilename} from HEIC to JPEG.`); await job.updateProgress(10); - await ConvertToJpeg(fileInfo.path, `${fileInfo.destination}/${convertedFileName}`); + + const outputPath = path.join(fileInfo.destination, convertedFileName); + await convertToJpeg(fileInfo.path, outputPath); + await job.updateProgress(50); await handleOriginalFile(fileInfo); - logger.log("debug", `Converted ${fileInfo.originalFilename} image to JPEG from HEIC.`); + + logger.debug(`Successfully converted ${fileInfo.originalFilename} to JPEG.`); await job.updateProgress(100); return true; } catch (error) { - logger.log( - "error", - `QUEUE ERROR: Error converting ${fileInfo.originalFilename} image to JPEG from HEIC. ${JSON.stringify(error)}` - ); + logger.error(`Error converting ${fileInfo.originalFilename}:`, error); throw error; } }, { - connection: { - host: "localhost", - port: 6379, - maxRetriesPerRequest: 3, - enableReadyCheck: true, - reconnectOnError: function (err) { - const targetError = "READONLY"; - return err.message.includes(targetError); - } - }, + connection: connectionOpts, concurrency: 1 } ); -HeicQueue.on("waiting", (job) => { - logger.log("debug", `[BULLMQ] Job is waiting in queue! ${job.data.convertedFileName}`); +// Event listeners for queue and worker +heicQueue.on("waiting", (job) => { + logger.debug(`[heicQueue] Job waiting in queue: ${job.data.convertedFileName}`); }); -HeicQueue.on("error", (error) => { - logger.log("error", `[BULLMQ] Queue Error! ${error}`); +heicQueue.on("error", (error) => { + logger.error(`[heicQueue] Queue error:`, error); }); -HeicWorker.on("ready", () => { - logger.log("debug", `[BULLMQ] Worker Ready`); +heicWorker.on("ready", () => { + logger.debug("[heicWorker] Worker ready"); }); -HeicWorker.on("active", (job, prev) => { - logger.log("debug", `[BULLMQ] Job ${job.id} is now active; previous status was ${prev}`); +heicWorker.on("active", (job, prev) => { + logger.debug(`[heicWorker] Job ${job.id} active (previous: ${prev})`); }); -HeicWorker.on("completed", async (job, returnvalue) => { - logger.log("debug", `[BULLMQ] ${job.id} has completed and returned ${returnvalue}`); +heicWorker.on("completed", async (job) => { + logger.debug(`[heicWorker] Job ${job.id} completed`); await job.remove(); - logger.log("debug", `Job ${job.id} removed from Redis`); + logger.debug(`Job ${job.id} removed from Redis`); }); -HeicWorker.on("failed", (jobId, failedReason) => { - logger.log("error", `[BULLMQ] ${jobId} has failed with reason ${failedReason}`); +heicWorker.on("failed", (jobId, reason) => { + logger.error(`[heicWorker] Job ${jobId} failed: ${reason}`); }); -HeicWorker.on("error", (error) => { - logger.log("error", `[BULLMQ] There was a queue error! ${error}`); +heicWorker.on("error", (error) => { + logger.error(`[heicWorker] Worker error:`, error); }); -HeicWorker.on("stalled", (error) => { - logger.log("error", `[BULLMQ] There was a worker stall! ${error}`); +heicWorker.on("stalled", (job) => { + logger.error(`[heicWorker] Worker stalled: ${job}`); }); -HeicWorker.on("ioredis:close", () => { - logger.log("error", `[BULLMQ] Redis connection closed!`); +heicWorker.on("ioredis:close", () => { + logger.error("[heicWorker] Redis connection closed"); }); diff --git a/util/interfaces/MediaFile.ts b/util/interfaces/MediaFile.ts index 09d7661..fbe65b0 100644 --- a/util/interfaces/MediaFile.ts +++ b/util/interfaces/MediaFile.ts @@ -1,11 +1,14 @@ -import internal from "stream"; +import { FileTypeResult } from "file-type/core"; -interface MediaFile { +export default interface MediaFile { + type?: FileTypeResult | undefined; + size?: number; src: string; + filename: string; + name: string; + path: string; + thumbnailPath: string; thumbnail: string; thumbnailHeight: number; thumbnailWidth: number; - filename: string; } - -export default MediaFile; diff --git a/util/serverInit.ts b/util/serverInit.ts index d6d64fb..bb1e0c0 100644 --- a/util/serverInit.ts +++ b/util/serverInit.ts @@ -8,17 +8,21 @@ dotenv.config({ path: resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) }); -const RootDirectory = process.env.MEDIA_PATH!.replace("~", os.homedir); +// Resolve root directory, supporting ~ for home +const RootDirectory = (process.env.MEDIA_PATH || "").replace("~", os.homedir); + +// Folder names const JobsFolder = "Jobs"; const VendorsFolder = "Vendors"; +// Folder paths object export const FolderPaths = { Root: RootDirectory, Jobs: path.join(RootDirectory, JobsFolder), Vendors: path.join(RootDirectory, VendorsFolder), - ThumbsSubDir: "/thumbs", - BillsSubDir: "/bills", - ConvertedOriginalSubDir: "/ConvertedOriginal", + ThumbsSubDir: "thumbs", + BillsSubDir: "bills", + ConvertedOriginalSubDir: "ConvertedOriginal", StaticPath: "/static", JobsFolder, VendorsFolder @@ -28,19 +32,25 @@ export const AssetPaths = { File: "/assets/file.png" }; +// Utility functions for relative file paths export function JobRelativeFilePath(jobid: string, filename: string) { return path.join(FolderPaths.Jobs, jobid, filename); } export function BillsRelativeFilePath(jobid: string, filename: string) { return path.join(FolderPaths.Jobs, jobid, FolderPaths.BillsSubDir, filename); } + +// Ensure all required directories exist at startup export default function InitServer() { logger.info(`Ensuring Root media path exists: ${FolderPaths.Root}`); ensureDirSync(FolderPaths.Root); + logger.info(`Ensuring Jobs media path exists: ${FolderPaths.Jobs}`); ensureDirSync(FolderPaths.Jobs); + logger.info(`Ensuring Vendors media path exists: ${FolderPaths.Vendors}`); ensureDirSync(FolderPaths.Vendors); + logger.info("Folder Paths", FolderPaths); logger.info("IMS Token set to: " + (process.env.IMS_TOKEN || "").trim()); } diff --git a/util/validateToken.ts b/util/validateToken.ts index c95a96e..867ba5d 100644 --- a/util/validateToken.ts +++ b/util/validateToken.ts @@ -1,23 +1,32 @@ import dotenv from "dotenv"; import { NextFunction, Request, Response } from "express"; import { resolve } from "path"; +import { logger } from "../server.js"; dotenv.config({ path: resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) }); export default function ValidateImsToken(req: Request, res: Response, next: NextFunction) { - const jobid: string = (req.body.jobid || "").trim(); + try { + const IMS_TOKEN: string = (process.env.IMS_TOKEN || "").trim(); - const IMS_TOKEN: string = (process.env.IMS_TOKEN || "").trim(); - - if (IMS_TOKEN === "") { - next(); - } else { - if (req.headers.ims_token !== IMS_TOKEN) { - res.sendStatus(401); - } else { + if (!IMS_TOKEN) { + logger.debug("IMS_TOKEN not set, skipping token validation."); next(); + return; } + + const token = req.headers.ims_token || req.headers["ims-token"] || req.headers["x-ims-token"]; + if (token !== IMS_TOKEN) { + logger.warn("Invalid IMS token provided.", { provided: token }); + res.sendStatus(401); + return; + } + + next(); + } catch (error) { + logger.error("Error validating IMS token.", { error: (error as Error).message }); + if (!res.headersSent) res.status(500).json({ error: "Error validating IMS token." }); } }