added ffmpeg

This commit is contained in:
C4illin
2024-05-21 15:34:38 +02:00
parent e55b8abaa0
commit 4aeeaa5060
8 changed files with 911 additions and 113 deletions

View File

@@ -15,29 +15,26 @@ RUN mkdir -p /temp/prod
COPY package.json bun.lockb /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production
# install pandoc
RUN apt-get update && apt-get install -y pandoc
# copy node_modules from temp directory
# then copy all (non-ignored) project files into the image
FROM base AS prerelease
COPY --from=install /temp/dev/node_modules node_modules
COPY . .
# FROM base AS prerelease
# COPY --from=install /temp/dev/node_modules node_modules
# COPY . .
# [optional] tests & build
ENV NODE_ENV=production
# # [optional] tests & build
# ENV NODE_ENV=production
# RUN bun test
# RUN bun run build
# copy production dependencies and source code into final image
FROM base AS release
COPY --from=install /temp/prod/node_modules node_modules
COPY --from=prerelease /app/src/index.tsx /app/src/
COPY --from=prerelease /app/package.json .
# COPY --from=prerelease /app/src/index.tsx /app/src/
# COPY --from=prerelease /app/package.json .
COPY . .
# copy pandoc
COPY --from=install /usr/bin/pandoc /usr/bin/pandoc
# install additional dependencies
RUN apt-get update && apt-get install -y pandoc texlive-latex-recommended ffmpeg
# run the app
USER bun

673
src/converters/ffmpeg.ts Normal file
View File

@@ -0,0 +1,673 @@
import { exec } from "node:child_process";
// This could be done dynamically by running `ffmpeg -formats` and parsing the output
export const properties = {
from: {
video: [
"012v",
"4xm",
"8bps",
"aasc",
"agm",
"aic",
"alias_pix",
"amv",
"anm",
"ansi",
"apng",
"arbc",
"argo",
"asv1",
"asv2",
"aura",
"aura2",
"av1",
"avrn",
"avrp",
"avs",
"avui",
"ayuv",
"bethsoftvid",
"bfi",
"binkvideo",
"bintext",
"bitpacked",
"bmp",
"bmv_video",
"brender_pix",
"c93",
"cavs",
"cdgraphics",
"cdtoons",
"cdxl",
"cfhd",
"cinepak",
"clearvideo",
"cljr",
"cllc",
"cmv",
"cpia",
"cri",
"cscd",
"cyuv",
"dds",
"dfa",
"dirac",
"dnxhd",
"dpx",
"dsicinvideo",
"dvvideo",
"dxa",
"dxtory",
"dxv",
"escape124",
"escape130",
"exr",
"ffv1",
"ffvhuff",
"fic",
"fits",
"flashsv",
"flashsv2",
"flic",
"flv1",
"fmvc",
"fraps",
"frwu",
"g2m",
"gdv",
"gif",
"h261",
"h263",
"h263i",
"h263p",
"h264",
"hap",
"hevc",
"hnm4video",
"hq_hqa",
"hqx",
"huffyuv",
"hymt",
"idcin",
"idf",
"iff_ilbm",
"imm4",
"imm5",
"indeo2",
"indeo3",
"indeo4",
"indeo5",
"interplayvideo",
"ipu",
"jpeg2000",
"jpegls",
"jv",
"kgv1",
"kmvc",
"lagarith",
"loco",
"lscr",
"m101",
"mad",
"magicyuv",
"mdec",
"mimic",
"mjpeg",
"mjpegb",
"mmvideo",
"mobiclip",
"motionpixels",
"mpeg1video",
"mpeg2video",
"mpeg4",
"msa1",
"mscc",
"msmpeg4v1",
"msmpeg4v2",
"msmpeg4v3",
"msp2",
"msrle",
"mss1",
"mss2",
"msvideo1",
"mszh",
"mts2",
"mv30",
"mvc1",
"mvc2",
"mvdv",
"mvha",
"mwsc",
"mxpeg",
"notchlc",
"nuv",
"paf_video",
"pam",
"pbm",
"pcx",
"pfm",
"pgm",
"pgmyuv",
"pgx",
"photocd",
"pictor",
"pixlet",
"png",
"ppm",
"prores",
"prosumer",
"psd",
"ptx",
"qdraw",
"qpeg",
"qtrle",
"r10k",
"r210",
"rasc",
"rawvideo",
"rl2",
"roq",
"rpza",
"rscc",
"rv10",
"rv20",
"rv30",
"rv40",
"sanm",
"scpr",
"screenpresso",
"sga",
"sgi",
"sgirle",
"sheervideo",
"simbiosis_imx",
"smackvideo",
"smc",
"smvjpeg",
"snow",
"sp5x",
"speedhq",
"srgc",
"sunrast",
"svg",
"svq1",
"svq3",
"targa",
"targa_y216",
"tdsc",
"tgq",
"tgv",
"theora",
"thp",
"tiertexseqvideo",
"tiff",
"tmv",
"tqi",
"truemotion1",
"truemotion2",
"truemotion2rt",
"tscc",
"tscc2",
"txd",
"ulti",
"utvideo",
"v210",
"v210x",
"v308",
"v408",
"v410",
"vb",
"vble",
"vc1",
"vc1image",
"vcr1",
"vixl",
"vmdvideo",
"vmnc",
"vp3",
"vp4",
"vp5",
"vp6",
"vp6a",
"vp6f",
"vp7",
"vp8",
"vp9",
"wcmv",
"webp",
"wmv1",
"wmv2",
"wmv3",
"wmv3image",
"wnv1",
"wrapped_avframe",
"ws_vqa",
"xan_wc3",
"xan_wc4",
"xbin",
"xbm",
"xface",
"xpm",
"xwd",
"y41p",
"ylc",
"yop",
"yuv4",
"zerocodec",
"zlib",
"zmbv",
],
audio: [
"8svx_exp",
"8svx_fib",
"aac",
"aac_latm",
"ac3",
"acelp.kelvin",
"adpcm_4xm",
"adpcm_adx",
"adpcm_afc",
"adpcm_agm",
"adpcm_aica",
"adpcm_argo",
"adpcm_ct",
"adpcm_dtk",
"adpcm_ea",
"adpcm_ea_maxis_xa",
"adpcm_ea_r1",
"adpcm_ea_r2",
"adpcm_ea_r3",
"adpcm_ea_xas",
"adpcm_g722",
"adpcm_g726",
"adpcm_g726le",
"adpcm_ima_alp",
"adpcm_ima_amv",
"adpcm_ima_apc",
"adpcm_ima_apm",
"adpcm_ima_cunning",
"adpcm_ima_dat4",
"adpcm_ima_dk3",
"adpcm_ima_dk4",
"adpcm_ima_ea_eacs",
"adpcm_ima_ea_sead",
"adpcm_ima_iss",
"adpcm_ima_moflex",
"adpcm_ima_mtf",
"adpcm_ima_oki",
"adpcm_ima_qt",
"adpcm_ima_rad",
"adpcm_ima_smjpeg",
"adpcm_ima_ssi",
"adpcm_ima_wav",
"adpcm_ima_ws",
"adpcm_ms",
"adpcm_mtaf",
"adpcm_psx",
"adpcm_sbpro_2",
"adpcm_sbpro_3",
"adpcm_sbpro_4",
"adpcm_swf",
"adpcm_thp",
"adpcm_thp_le",
"adpcm_vima",
"adpcm_xa",
"adpcm_yamaha",
"adpcm_zork",
"alac",
"amr_nb",
"amr_wb",
"ape",
"aptx",
"aptx_hd",
"atrac1",
"atrac3",
"atrac3al",
"atrac3p",
"atrac3pal",
"atrac9",
"avc",
"binkaudio_dct",
"binkaudio_rdft",
"bmv_audio",
"codec2",
"comfortnoise",
"cook",
"derf_dpcm",
"dolby_e",
"dsd_lsbf",
"dsd_lsbf_planar",
"dsd_msbf",
"dsd_msbf_planar",
"dsicinaudio",
"dss_sp",
"dst",
"dts",
"dvaudio",
"eac3",
"evrc",
"fastaudio",
"flac",
"g723_1",
"g729",
"gremlin_dpcm",
"gsm",
"gsm_ms",
"hca",
"hcom",
"iac",
"ilbc",
"imc",
"interplay_dpcm",
"interplayacm",
"mace3",
"mace6",
"metasound",
"mlp",
"mp1",
"mp2",
"mp3",
"mp3adu",
"mp3on4",
"mp4als",
"musepack7",
"musepack8",
"nellymoser",
"opus",
"paf_audio",
"pcm_alaw",
"pcm_bluray",
"pcm_dvd",
"pcm_f16le",
"pcm_f24le",
"pcm_f32be",
"pcm_f32le",
"pcm_f64be",
"pcm_f64le",
"pcm_lxf",
"pcm_mulaw",
"pcm_s16be",
"pcm_s16be_planar",
"pcm_s16le",
"pcm_s16le_planar",
"pcm_s24be",
"pcm_s24daud",
"pcm_s24le",
"pcm_s24le_planar",
"pcm_s32be",
"pcm_s32le",
"pcm_s32le_planar",
"pcm_s64be",
"pcm_s64le",
"pcm_s8",
"pcm_s8_planar",
"pcm_sga",
"pcm_u16be",
"pcm_u16le",
"pcm_u24be",
"pcm_u24le",
"pcm_u32be",
"pcm_u32le",
"pcm_u8",
"pcm_vidc",
"qcelp",
"qdm2",
"qdmc",
"ra_144",
"ra_288",
"ralf",
"roq_dpcm",
"s302m",
"sbc",
"sdx2_dpcm",
"shorten",
"sipr",
"siren",
"smackaudio",
"sol_dpcm",
"sonic",
"speex",
"tak",
"truehd",
"truespeech",
"tta",
"twinvq",
"vmdaudio",
"vorbis",
"wavesynth",
"wavpack",
"westwood_snd1",
"wmalossless",
"wmapro",
"wmav1",
"wmav2",
"wmavoice",
"xan_dpcm",
"xma1",
"xma2",
],
subtitles: [
"ass",
"dvb_subtitle",
"dvb_teletext",
"dvd_subtitle",
"eia_608",
"hdmv_pgs_subtitle",
"jacosub",
"microdvd",
"mov_text",
"mpl2",
"pjs",
"realtext",
"sami",
"stl",
"subrip",
"subviewer",
"subviewer1",
"text",
"vplayer",
"webvtt",
"xsub",
],
},
to: {
video: [
"a64_multi",
"a64_multi5",
"ljpeg",
"alias_pix",
"amv",
"apng",
"asv1",
"asv2",
"av1",
"avrp",
"avui",
"ayuv",
"bmp",
"cfhd",
"cinepak",
"cljr",
"dirac",
"dnxhd",
"dpx",
"dvvideo",
"exr",
"ffv1",
"ffvhuff",
"fits",
"flashsv",
"flashsv2",
"flv1",
"gif",
"h261",
"h263",
"h263p",
"h264",
"hap",
"hevc",
"huffyuv",
"jpeg2000",
"jpegls",
"magicyuv",
"mjpeg",
"mpeg1video",
"mpeg2video",
"mpeg4",
"msmpeg4v2",
"msmpeg4v3",
"msvideo1",
"pam",
"pbm",
"pcx",
"pfm",
"pgm",
"pgmyuv",
"png",
"ppm",
"prores",
"qtrle",
"r10k",
"r210",
"rawvideo",
"roq",
"rpza",
"rv10",
"rv20",
"sgi",
"snow",
"speedhq",
"sunrast",
"svq1",
"targa",
"theora",
"tiff",
"utvideo",
"v210",
"v308",
"v408",
"v410",
"vp8",
"vp9",
"webp",
"wmv1",
"wmv2",
"wrapped_avframe",
"xbm",
"xface",
"xwd",
"y41p",
"yuv4",
"zlib",
"zmbv",
],
audio: [
"aac",
"ac3",
"adpcm_adx",
"adpcm_argo",
"adpcm_g722",
"adpcm_g726",
"adpcm_g726le",
"adpcm_ima_alp",
"adpcm_ima_amv",
"adpcm_ima_apm",
"adpcm_ima_qt",
"adpcm_ima_ssi",
"adpcm_ima_wav",
"adpcm_ms",
"adpcm_swf",
"adpcm_yamaha",
"alac",
"aptx",
"aptx_hd",
"codec2",
"comfortnoise",
"dts",
"eac3",
"flac",
"g723_1",
"gsm",
"gsm_ms",
"mlp",
"mp2",
"mp3",
"nellymoser",
"opus",
"pcm_alaw",
"pcm_dvd",
"pcm_f32be",
"pcm_f32le",
"pcm_f64be",
"pcm_f64le",
"pcm_mulaw",
"pcm_s16be",
"pcm_s16be_planar",
"pcm_s16le",
"pcm_s16le_planar",
"pcm_s24be",
"pcm_s24daud",
"pcm_s24le",
"pcm_s24le_planar",
"pcm_s32be",
"pcm_s32le",
"pcm_s32le_planar",
"pcm_s64be",
"pcm_s64le",
"pcm_s8",
"pcm_s8_planar",
"pcm_u16be",
"pcm_u16le",
"pcm_u24be",
"pcm_u24le",
"pcm_u32be",
"pcm_u32le",
"pcm_u8",
"pcm_vidc",
"ra_144",
"roq_dpcm",
"s302m",
"sbc",
"sonic",
"speex",
"truehd",
"tta",
"vorbis",
"wavpack",
"wmav1",
"wmav2",
],
subtitles: [
"ass",
"dvb_subtitle",
"dvd_subtitle",
"mov_text",
"subrip",
"text",
"webvtt",
"xsub",
],
},
};
export async function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
options?: any,
) {
return exec(
`ffmpeg -f "${fileType}" -i "${filePath}" -f "${convertTo}" "${targetPath}"`,
(error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return;
}
console.log(`stdout: ${stdout}`);
console.error(`stderr: ${stderr}`);
},
);
}

View File

@@ -8,78 +8,164 @@ import {
convert as convertPandoc,
} from "./pandoc";
import {
properties as propertiesFfmpeg,
convert as convertFfmpeg,
} from "./ffmpeg";
const properties: {
[key: string]: {
properties: {
from: string[] | { [key: string]: string[] };
to: string[] | { [key: string]: string[] };
options?: {
[key: string]: {
[key: string]: {
description: string;
type: string;
default: number;
};
};
};
};
converter: (
filePath: string,
fileType: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
convertTo: any,
targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
options?: any,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
) => any;
};
} = {
sharp: {
properties: propertiesImage,
converter: convertImage,
},
pandoc: {
properties: propertiesPandoc,
converter: convertPandoc,
},
ffmpeg: {
properties: propertiesFfmpeg,
converter: convertFfmpeg,
},
};
import { normalizeFiletype } from "../helpers/normalizeFiletype";
export async function mainConverter(
inputFilePath: string,
fileType: string,
convertTo: string,
fileTypeOriginal: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
convertTo: any,
targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
options?: any,
converter?: string,
) {
// Check if the fileType and convertTo are supported by the sharp converter
if (
propertiesImage.from.includes(fileType) &&
propertiesImage.to.includes(convertTo)
) {
// Use the sharp converter
try {
await convertImage(
inputFilePath,
fileType,
convertTo,
targetPath,
options,
);
console.log(
`Converted ${inputFilePath} from ${fileType} to ${convertTo} successfully.`,
);
} catch (error) {
console.error(
`Failed to convert ${inputFilePath} from ${fileType} to ${convertTo}.`,
error,
);
const fileType = normalizeFiletype(fileTypeOriginal);
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
let converterFunc: any;
if (converter) {
converterFunc = properties[converter];
} else {
// Iterate over each converter in properties
for (const converterName in properties) {
const converterObj = properties[converterName];
if (!converterObj) {
break;
}
// if converter properties.from is an object loop thorugh the keys otherwise use the array
// for example ffmpeg is an object eg from: {video: ["mp4", "webm"], audio: ["mp3"]}
if (Array.isArray(converterObj.properties.from) && Array.isArray(converterObj.properties.to)) {
if (
converterObj.properties.from.includes(fileType) &&
converterObj.properties.to.includes(convertTo)
) {
converterFunc = converterObj.converter;
break;
}
} else {
for (const key in converterObj.properties.from) {
if (
converterObj.properties.from[key].includes(fileType) &&
converterObj.properties.to[key].includes(convertTo)
) {
converterFunc = converterObj.converter;
break;
}
}
}
}
}
// Check if the fileType and convertTo are supported by the pandoc converter
else if (
propertiesPandoc.from.includes(fileType) &&
propertiesPandoc.to.includes(convertTo)
) {
// Use the pandoc converter
try {
await convertPandoc(
inputFilePath,
fileType,
convertTo,
targetPath,
options,
);
console.log(
`Converted ${inputFilePath} from ${fileType} to ${convertTo} successfully.`,
);
} catch (error) {
console.error(
`Failed to convert ${inputFilePath} from ${fileType} to ${convertTo}.`,
error,
);
}
} else {
if (!converterFunc) {
console.log(
`Neither the sharp nor pandoc converter support converting from ${fileType} to ${convertTo}.`,
`No available converter supports converting from ${fileType} to ${convertTo}.`,
);
return;
}
try {
await converterFunc(
inputFilePath,
fileType,
convertTo,
targetPath,
options,
);
console.log(
`Converted ${inputFilePath} from ${fileType} to ${convertTo} successfully using ${converter}.`,
);
} catch (error) {
console.error(
`Failed to convert ${inputFilePath} from ${fileType} to ${convertTo} using ${converter}.`,
error,
);
}
}
const possibleConversions: { [key: string]: string[] } = {};
for (const from of propertiesImage.from) {
possibleConversions[from] = propertiesImage.to;
for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties;
if (!converterProperties) {
continue;
}
if (Array.isArray(converterProperties.from)) {
for (const extension of converterProperties.from) {
possibleConversions[extension] = converterProperties.to;
}
} else {
for (const key in converterProperties.from) {
if (!converterProperties.from[key] || !converterProperties.to[key]) {
continue;
}
for (const extension of converterProperties.from[key]) {
possibleConversions[extension] = converterProperties.to[key];
}
}
}
}
for (const from of propertiesPandoc.from) {
possibleConversions[from] = propertiesPandoc.to;
}
// // save all possible conversions to a file
// import fs from "fs";
// import path from "path";
// import { FormatEnum } from "sharp";
// fs.writeFileSync(
// path.join(__dirname, ".", "possibleConversions.json"),
// JSON.stringify(possibleConversions),
// );
export const getPossibleConversions = (from: string): string[] => {
const fromClean = normalizeFiletype(from);
@@ -87,6 +173,20 @@ export const getPossibleConversions = (from: string): string[] => {
return possibleConversions[fromClean] || ([] as string[]);
};
let allTargets: string[] = [];
for (const converterName in properties) {
const converterProperties = properties[converterName].properties;
if (Array.isArray(converterProperties.from)) {
allTargets = allTargets.concat(converterProperties.to);
} else {
for (const key in converterProperties.to) {
allTargets = allTargets.concat(converterProperties.to[key]);
}
}
}
export const getAllTargets = () => {
return [...propertiesImage.to, ...propertiesPandoc.to];
return allTargets;
};

View File

@@ -115,15 +115,23 @@ export const properties = {
],
};
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
options?: any,
) {
return exec(
`pandoc "${filePath}" -f ${fileType} -t ${convertTo} -o "${targetPath}"`,
(error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return;
}
console.log(`stdout: ${stdout}`);
console.error(`stderr: ${stderr}`);
},
);
}

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,5 @@
import sharp from "sharp";
import type { FormatEnum } from "sharp";
// declare possible conversions
export const properties = {
@@ -11,11 +12,18 @@ export const properties = {
type: "number",
default: 1,
},
}
}
}
},
},
};
export async function convert(filePath: string, fileType: string, convertTo: string, targetPath: string, options?: any) {
export async function convert(
filePath: string,
fileType: string,
convertTo: keyof FormatEnum,
targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
options?: any,
) {
if (fileType === "svg") {
const scale = options.scale || 1;
const metadata = await sharp(filePath).metadata();
@@ -24,14 +32,14 @@ export async function convert(filePath: string, fileType: string, convertTo: str
throw new Error("Could not get metadata from image");
}
const newWidth = Math.round(metadata.width * scale)
const newHeight = Math.round(metadata.height * scale)
const newWidth = Math.round(metadata.width * scale);
const newHeight = Math.round(metadata.height * scale);
return await sharp(filePath)
.resize(newWidth, newHeight)
.toFormat(convertTo)
.toFile(targetPath);
.resize(newWidth, newHeight)
.toFormat(convertTo)
.toFile(targetPath);
}
return await sharp(filePath).toFormat(convertTo).toFile(targetPath);
}
}

View File

@@ -130,7 +130,7 @@ const app = new Elysia()
})
.post(
"/register",
async function handler({ body, set, jwt, cookie: { auth } }) {
async ({ body, set, jwt, cookie: { auth } }) => {
const existingUser = await db
.query("SELECT * FROM users WHERE email = ?")
.get(body.email);
@@ -177,6 +177,7 @@ const app = new Elysia()
Location: "/",
};
},
{ body: t.Object({ email: t.String(), password: t.String() }) },
)
.get("/login", async ({ jwt, redirect, cookie: { auth } }) => {
console.log("login handler");
@@ -276,17 +277,15 @@ const app = new Elysia()
sameSite: "strict",
});
// redirect to home
set.status = 302;
set.headers = {
Location: "/",
};
redirect("/");
},
{ body: t.Object({ email: t.String(), password: t.String() }) },
)
.get("/logout", ({ redirect, cookie: { auth } }) => {
if (auth?.value) {
auth.remove();
}
return redirect("/login");
})
.post("/logout", ({ redirect, cookie: { auth } }) => {
@@ -518,8 +517,9 @@ const app = new Elysia()
return redirect(`/results/${jobId.value}`);
},
)
.get("/history", async ({ jwt, redirect, cookie: { auth } }) => {
.get("/hist", async ({ body, jwt, redirect, cookie: { auth } }) => {
console.log("results page");
if (!auth?.value) {
console.log("no auth value");
return redirect("/login");
@@ -533,14 +533,7 @@ const app = new Elysia()
const userJobs = db
.query("SELECT * FROM jobs WHERE user_id = ?")
.all(user.id) as {
id: number;
user_id: number;
date_created: string;
status: string;
num_files: number;
finished_files: number;
}[];
.all(user.id) as IJobs[];
for (const job of userJobs) {
const files = db
@@ -631,6 +624,7 @@ const app = new Elysia()
<button
type="button"
style={{ width: "10rem", float: "right" }}
onclick="downloadAll()"
>
Download All
</button>
@@ -671,6 +665,7 @@ const app = new Elysia()
</table>
</article>
</main>
<script src="/downloadAll.js" defer />
</BaseHtml>
);
},
@@ -703,31 +698,34 @@ const app = new Elysia()
return Bun.file(filePath);
},
)
.get("/zip/:userId/:jobId", async ({ params, jwt, redirect, cookie: { auth } }) => {
// TODO: Implement zip download
if (!auth?.value) {
return redirect("/login");
}
.get(
"/zip/:userId/:jobId",
async ({ params, jwt, redirect, cookie: { auth } }) => {
// TODO: Implement zip download
if (!auth?.value) {
return redirect("/login");
}
const user = await jwt.verify(auth.value);
if (!user) {
return redirect("/login");
}
const user = await jwt.verify(auth.value);
if (!user) {
return redirect("/login");
}
const job = await db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.get(user.id, params.jobId);
const job = await db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.get(user.id, params.jobId);
if (!job) {
return redirect("/results");
}
if (!job) {
return redirect("/results");
}
const userId = decodeURIComponent(params.userId);
const jobId = decodeURIComponent(params.jobId);
const outputPath = `${outputDir}${userId}/${jobId}/`;
const userId = decodeURIComponent(params.userId);
const jobId = decodeURIComponent(params.jobId);
const outputPath = `${outputDir}${userId}/${jobId}/`;
// return Bun.zip(outputPath);
})
// return Bun.zip(outputPath);
},
)
.onError(({ code, error, request }) => {
// log.error(` ${request.method} ${request.url}`, code, error);
console.error(error);

13
src/public/downloadAll.js Normal file
View File

@@ -0,0 +1,13 @@
window.downloadAll = function () {
// Get all download links
const downloadLinks = document.querySelectorAll("a[download]");
// Trigger download for each link
downloadLinks.forEach((link, index) => {
// We add a delay for each download to prevent them from starting at the same time
setTimeout(() => {
const event = new MouseEvent("click");
link.dispatchEvent(event);
}, index * 100);
});
};