Add files via upload

This commit is contained in:
Kosztyk 2026-01-12 01:38:48 +02:00 committed by GitHub
parent 2075e51c44
commit fdb0c0d7eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,3 +1,5 @@
// @ts-nocheck
import { Elysia, t } from "elysia"; import { Elysia, t } from "elysia";
import { BaseHtml } from "../components/base"; import { BaseHtml } from "../components/base";
import { Header } from "../components/header"; import { Header } from "../components/header";
@ -12,31 +14,21 @@ import { userService } from "./user";
import { outputDir } from ".."; import { outputDir } from "..";
import { sendFileToErugo } from "../helpers/erugo"; import { sendFileToErugo } from "../helpers/erugo";
type JobsWithOptionalNumFiles = Jobs & {
num_files?: number;
};
function ResultsArticle({ function ResultsArticle({
job, job,
files, files,
outputPath, outputPath,
}: { }: {
job: JobsWithOptionalNumFiles; job: Jobs;
files: Filename[]; files: Filename[];
outputPath: string; outputPath: string;
}) { }) {
const maxFiles = const maxFiles = Number((job as any).num_files ?? 0);
typeof job.num_files === "number" && Number.isFinite(job.num_files) const doneFiles = Number(files.filter((f: any) => String((f as any).status || '').toLowerCase() === 'done').length);
? Number(job.num_files) const isDone = doneFiles === maxFiles;
: Number(files.length);
const doneFiles = Number(
files.filter((f) => String(f.status ?? "").toLowerCase() === "done").length,
);
const isDone = maxFiles > 0 ? doneFiles >= maxFiles : true;
const disabledLinkClass = "pointer-events-none opacity-50"; const disabledLinkClass = "pointer-events-none opacity-50";
const busyAttrs = { disabled: true, "aria-busy": "true" } as const;
return ( return (
<article class="article"> <article class="article">
@ -49,7 +41,7 @@ function ResultsArticle({
!isDone ? disabledLinkClass : "" !isDone ? disabledLinkClass : ""
}`} }`}
href={`${WEBROOT}/delete/${job.id}`} href={`${WEBROOT}/delete/${job.id}`}
aria-disabled={!isDone ? "true" : undefined} {...(!isDone ? busyAttrs : {})}
> >
<DeleteIcon /> <p>Delete</p> <DeleteIcon /> <p>Delete</p>
</a> </a>
@ -59,8 +51,8 @@ function ResultsArticle({
!isDone ? disabledLinkClass : "" !isDone ? disabledLinkClass : ""
}`} }`}
href={`${WEBROOT}/archive/${job.id}`} href={`${WEBROOT}/archive/${job.id}`}
download download={`converted_files_${job.id}.tar`}
aria-disabled={!isDone ? "true" : undefined} {...(!isDone ? busyAttrs : {})}
> >
<DownloadIcon /> <p>Tar</p> <DownloadIcon /> <p>Tar</p>
</a> </a>
@ -69,8 +61,7 @@ function ResultsArticle({
id="cxDownloadAll" id="cxDownloadAll"
type="button" type="button"
class="flex btn-primary flex-row gap-2 text-contrast" class="flex btn-primary flex-row gap-2 text-contrast"
disabled={!isDone ? "true" : undefined} {...(!isDone ? busyAttrs : {})}
aria-busy={!isDone ? "true" : undefined}
> >
<DownloadIcon /> <p>All</p> <DownloadIcon /> <p>All</p>
</button> </button>
@ -124,7 +115,7 @@ function ResultsArticle({
<a <a
class="text-accent-500 hover:text-accent-400" class="text-accent-500 hover:text-accent-400"
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`} href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
download download={file.output_file_name}
> >
<DownloadIcon /> <DownloadIcon />
</a> </a>
@ -205,7 +196,7 @@ function ResultsArticle({
<textarea <textarea
id="cxShareDescription" id="cxShareDescription"
class="w-full rounded border border-neutral-700 bg-neutral-950 px-3 py-2 text-neutral-100 outline-none focus:border-accent-500" class="w-full rounded border border-neutral-700 bg-neutral-950 px-3 py-2 text-neutral-100 outline-none focus:border-accent-500"
rows="3" rows={3}
placeholder="Message to share recipients (optional)" placeholder="Message to share recipients (optional)"
></textarea> ></textarea>
</div> </div>
@ -214,7 +205,7 @@ function ResultsArticle({
<button <button
id="cxShareSubmit" id="cxShareSubmit"
type="button" type="button"
class="flex btn-primary items-center gap-2" class="btn-primary flex items-center gap-2"
> >
Send Send
</button> </button>
@ -252,6 +243,11 @@ function ResultsArticle({
); );
} }
/**
* IMPORTANT FIX:
* Results HTML is re-rendered via /progress/:jobId and replaces the article + modal.
* So we MUST NOT keep stale DOM references. We re-query elements on each action.
*/
const shareJs = ` const shareJs = `
(function () { (function () {
const WEBROOT = ${JSON.stringify(WEBROOT)}; const WEBROOT = ${JSON.stringify(WEBROOT)};
@ -310,11 +306,13 @@ const shareJs = `
currentFileName = null; currentFileName = null;
} }
// Delegated: always works even after /progress replaces the article
document.addEventListener("click", (e) => { document.addEventListener("click", (e) => {
const t = e.target; const t = e.target;
const btn = t && (t.closest ? t.closest('[data-share="true"]') : null); const btn = t && (t.closest ? t.closest('[data-share="true"]') : null);
if (!btn) return; if (!btn) return;
// kill old handlers (e.g. alert() from older results.js)
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (e.stopImmediatePropagation) e.stopImmediatePropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation();
@ -324,6 +322,7 @@ const shareJs = `
openModal(jobId, fileName); openModal(jobId, fileName);
}, true); }, true);
// Delegated close (because modal DOM is replaced during progress polling)
document.addEventListener("click", (e) => { document.addEventListener("click", (e) => {
const id = e.target && e.target.id; const id = e.target && e.target.id;
if (id === "cxShareClose" || id === "cxShareCancel") { if (id === "cxShareClose" || id === "cxShareCancel") {
@ -331,6 +330,7 @@ const shareJs = `
closeModal(); closeModal();
} }
if (id === "cxShareModal") { if (id === "cxShareModal") {
// click outside dialog closes
closeModal(); closeModal();
} }
}, true); }, true);
@ -340,6 +340,7 @@ const shareJs = `
if (e.key === "Escape" && r.modal && !r.modal.classList.contains("hidden")) closeModal(); if (e.key === "Escape" && r.modal && !r.modal.classList.contains("hidden")) closeModal();
}); });
// Delegated copy
document.addEventListener("click", async (e) => { document.addEventListener("click", async (e) => {
const t = e.target; const t = e.target;
if (!t || t.id !== "cxShareCopy") return; if (!t || t.id !== "cxShareCopy") return;
@ -358,6 +359,7 @@ const shareJs = `
} }
}, true); }, true);
// Delegated submit
document.addEventListener("click", async (e) => { document.addEventListener("click", async (e) => {
const t = e.target; const t = e.target;
if (!t || t.id !== "cxShareSubmit") return; if (!t || t.id !== "cxShareSubmit") return;
@ -401,13 +403,13 @@ const shareJs = `
} }
const url = const url =
(json && typeof json === "object" && json.share_url) || json?.share_url ||
(json && typeof json === "object" && json.share_link) || json?.share_link ||
(json && typeof json === "object" && json.data && json.data.url) || json?.data?.url ||
(json && typeof json === "object" && json.data && json.data.share && json.data.share.url) || json?.data?.share?.url ||
null; null;
if (url && typeof url === "string") { if (url) {
r.linkEl.value = url; r.linkEl.value = url;
r.linkBlock.classList.remove("hidden"); r.linkBlock.classList.remove("hidden");
} }
@ -424,6 +426,7 @@ const shareJs = `
} }
}, true); }, true);
// Download All: delegated, because button is replaced during progress polling
document.addEventListener("click", (e) => { document.addEventListener("click", (e) => {
const t = e.target; const t = e.target;
const btn = t && (t.closest ? t.closest("#cxDownloadAll") : null); const btn = t && (t.closest ? t.closest("#cxDownloadAll") : null);
@ -445,6 +448,7 @@ const shareJs = `
export const results = new Elysia() export const results = new Elysia()
.use(userService) .use(userService)
.get( .get(
"/results-share.js", "/results-share.js",
() => () =>
@ -456,6 +460,7 @@ export const results = new Elysia()
}), }),
{ auth: true }, { auth: true },
) )
.get( .get(
"/results/:jobId", "/results/:jobId",
async ({ params, set, cookie: { job_id }, user }) => { async ({ params, set, cookie: { job_id }, user }) => {
@ -464,7 +469,7 @@ export const results = new Elysia()
const job = db const job = db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?") .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.as(Jobs) .as(Jobs)
.get(user.id, params.jobId) as JobsWithOptionalNumFiles | null; .get(user.id, params.jobId);
if (!job) { if (!job) {
set.status = 404; set.status = 404;
@ -491,10 +496,10 @@ export const results = new Elysia()
<ResultsArticle job={job} files={files} outputPath={outputPath} /> <ResultsArticle job={job} files={files} outputPath={outputPath} />
</main> </main>
{/* existing file */} {/* keep existing file */}
<script src={`${WEBROOT}/results.js`} defer /> <script src={`${WEBROOT}/results.js`} defer />
{/* override handlers */} {/* our override must also load */}
<script src={`${WEBROOT}/results-share.js`} defer /> <script src={`${WEBROOT}/results-share.js`} defer />
</> </>
</BaseHtml> </BaseHtml>
@ -502,6 +507,7 @@ export const results = new Elysia()
}, },
{ auth: true }, { auth: true },
) )
.post( .post(
"/progress/:jobId", "/progress/:jobId",
async ({ set, params, cookie: { job_id }, user }) => { async ({ set, params, cookie: { job_id }, user }) => {
@ -510,7 +516,7 @@ export const results = new Elysia()
const job = db const job = db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?") .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.as(Jobs) .as(Jobs)
.get(user.id, params.jobId) as JobsWithOptionalNumFiles | null; .get(user.id, params.jobId);
if (!job) { if (!job) {
set.status = 404; set.status = 404;
@ -528,6 +534,7 @@ export const results = new Elysia()
}, },
{ auth: true }, { auth: true },
) )
.post( .post(
"/share-to-erugo/:jobId", "/share-to-erugo/:jobId",
async ({ params, body, user, set }) => { async ({ params, body, user, set }) => {
@ -570,7 +577,7 @@ export const results = new Elysia()
const result = await sendFileToErugo(payload); const result = await sendFileToErugo(payload);
return result; return result;
} catch (err: unknown) { } catch (err: any) {
console.error(err); console.error(err);
set.status = 500; set.status = 500;
return { message: "Failed to share with Erugo" }; return { message: "Failed to share with Erugo" };
@ -586,3 +593,5 @@ export const results = new Elysia()
}), }),
}, },
); );