chore: fix lint
This commit is contained in:
parent
d984891791
commit
17325a6e6d
87 changed files with 7624 additions and 7640 deletions
|
|
@ -1,67 +1,67 @@
|
|||
import { Html } from "@elysiajs/html";
|
||||
import Elysia, { t } from "elysia";
|
||||
import { getPossibleTargets } from "../converters/main";
|
||||
import { userService } from "./user";
|
||||
|
||||
export const chooseConverter = new Elysia().use(userService).post(
|
||||
"/conversions",
|
||||
({ body }) => {
|
||||
return (
|
||||
<>
|
||||
<article
|
||||
class={`
|
||||
convert_to_popup absolute z-2 m-0 hidden h-[50vh] max-h-[50vh] w-full flex-col
|
||||
overflow-x-hidden overflow-y-auto rounded bg-neutral-800
|
||||
sm:h-[30vh]
|
||||
`}
|
||||
>
|
||||
{Object.entries(getPossibleTargets(body.fileType)).map(([converter, targets]) => (
|
||||
<article
|
||||
class={`convert_to_group flex w-full flex-col border-b border-neutral-700 p-4`}
|
||||
data-converter={converter}
|
||||
>
|
||||
<header class="mb-2 w-full text-xl font-bold" safe>
|
||||
{converter}
|
||||
</header>
|
||||
<ul class="convert_to_target flex flex-row flex-wrap gap-1">
|
||||
{targets.map((target) => (
|
||||
<button
|
||||
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
|
||||
tabindex={0}
|
||||
class={`
|
||||
target rounded bg-neutral-700 p-1 text-base
|
||||
hover:bg-neutral-600
|
||||
`}
|
||||
data-value={`${target},${converter}`}
|
||||
data-target={target}
|
||||
data-converter={converter}
|
||||
type="button"
|
||||
safe
|
||||
>
|
||||
{target}
|
||||
</button>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</article>
|
||||
|
||||
<select name="convert_to" aria-label="Convert to" required hidden>
|
||||
<option selected disabled value="">
|
||||
Convert to
|
||||
</option>
|
||||
{Object.entries(getPossibleTargets(body.fileType)).map(([converter, targets]) => (
|
||||
<optgroup label={converter}>
|
||||
{targets.map((target) => (
|
||||
<option value={`${target},${converter}`} safe>
|
||||
{target}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
</>
|
||||
);
|
||||
},
|
||||
{ body: t.Object({ fileType: t.String() }) },
|
||||
);
|
||||
import { Html } from "@elysiajs/html";
|
||||
import Elysia, { t } from "elysia";
|
||||
import { getPossibleTargets } from "../converters/main";
|
||||
import { userService } from "./user";
|
||||
|
||||
export const chooseConverter = new Elysia().use(userService).post(
|
||||
"/conversions",
|
||||
({ body }) => {
|
||||
return (
|
||||
<>
|
||||
<article
|
||||
class={`
|
||||
convert_to_popup absolute z-2 m-0 hidden h-[50vh] max-h-[50vh] w-full flex-col
|
||||
overflow-x-hidden overflow-y-auto rounded bg-neutral-800
|
||||
sm:h-[30vh]
|
||||
`}
|
||||
>
|
||||
{Object.entries(getPossibleTargets(body.fileType)).map(([converter, targets]) => (
|
||||
<article
|
||||
class={`convert_to_group flex w-full flex-col border-b border-neutral-700 p-4`}
|
||||
data-converter={converter}
|
||||
>
|
||||
<header class="mb-2 w-full text-xl font-bold" safe>
|
||||
{converter}
|
||||
</header>
|
||||
<ul class="convert_to_target flex flex-row flex-wrap gap-1">
|
||||
{targets.map((target) => (
|
||||
<button
|
||||
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
|
||||
tabindex={0}
|
||||
class={`
|
||||
target rounded bg-neutral-700 p-1 text-base
|
||||
hover:bg-neutral-600
|
||||
`}
|
||||
data-value={`${target},${converter}`}
|
||||
data-target={target}
|
||||
data-converter={converter}
|
||||
type="button"
|
||||
safe
|
||||
>
|
||||
{target}
|
||||
</button>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</article>
|
||||
|
||||
<select name="convert_to" aria-label="Convert to" required hidden>
|
||||
<option selected disabled value="">
|
||||
Convert to
|
||||
</option>
|
||||
{Object.entries(getPossibleTargets(body.fileType)).map(([converter, targets]) => (
|
||||
<optgroup label={converter}>
|
||||
{targets.map((target) => (
|
||||
<option value={`${target},${converter}`} safe>
|
||||
{target}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
</>
|
||||
);
|
||||
},
|
||||
{ body: t.Object({ fileType: t.String() }) },
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,94 +1,94 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import { Elysia, t } from "elysia";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { outputDir, uploadsDir } from "..";
|
||||
import { handleConvert } from "../converters/main";
|
||||
import db from "../db/db";
|
||||
import { Jobs } from "../db/types";
|
||||
import { WEBROOT } from "../helpers/env";
|
||||
import { normalizeFiletype } from "../helpers/normalizeFiletype";
|
||||
import { userService } from "./user";
|
||||
|
||||
export const convert = new Elysia().use(userService).post(
|
||||
"/convert",
|
||||
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
if (!jobId?.value) {
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const existingJob = db
|
||||
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
|
||||
.as(Jobs)
|
||||
.get(jobId.value, user.id);
|
||||
|
||||
if (!existingJob) {
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
|
||||
const userOutputDir = `${outputDir}${user.id}/${jobId.value}/`;
|
||||
|
||||
// create the output directory
|
||||
try {
|
||||
await mkdir(userOutputDir, { recursive: true });
|
||||
} catch (error) {
|
||||
console.error(`Failed to create the output directory: ${userOutputDir}.`, error);
|
||||
}
|
||||
|
||||
const convertTo = normalizeFiletype(body.convert_to.split(",")[0] ?? "");
|
||||
const converterName = body.convert_to.split(",")[1];
|
||||
|
||||
if (!converterName) {
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const fileNames = JSON.parse(body.file_names) as string[];
|
||||
|
||||
for (let i = 0; i < fileNames.length; i++) {
|
||||
fileNames[i] = sanitize(fileNames[i] || "");
|
||||
}
|
||||
|
||||
if (!Array.isArray(fileNames) || fileNames.length === 0) {
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
db.query("UPDATE jobs SET num_files = ?1, status = 'pending' WHERE id = ?2").run(
|
||||
fileNames.length,
|
||||
jobId.value,
|
||||
);
|
||||
|
||||
// Start the conversion process in the background
|
||||
handleConvert(fileNames, userUploadsDir, userOutputDir, convertTo, converterName, jobId)
|
||||
.then(() => {
|
||||
// All conversions are done, update the job status to 'completed'
|
||||
if (jobId.value) {
|
||||
db.query("UPDATE jobs SET status = 'completed' WHERE id = ?1").run(jobId.value);
|
||||
}
|
||||
|
||||
// Delete all uploaded files in userUploadsDir
|
||||
// rmSync(userUploadsDir, { recursive: true, force: true });
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error in conversion process:", error);
|
||||
});
|
||||
|
||||
// Redirect the client immediately
|
||||
return redirect(`${WEBROOT}/results/${jobId.value}`, 302);
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
convert_to: t.String(),
|
||||
file_names: t.String(),
|
||||
}),
|
||||
auth: true,
|
||||
},
|
||||
);
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { Elysia, t } from "elysia";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { outputDir, uploadsDir } from "..";
|
||||
import { handleConvert } from "../converters/main";
|
||||
import db from "../db/db";
|
||||
import { Jobs } from "../db/types";
|
||||
import { WEBROOT } from "../helpers/env";
|
||||
import { normalizeFiletype } from "../helpers/normalizeFiletype";
|
||||
import { userService } from "./user";
|
||||
|
||||
export const convert = new Elysia().use(userService).post(
|
||||
"/convert",
|
||||
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
if (!jobId?.value) {
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const existingJob = db
|
||||
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
|
||||
.as(Jobs)
|
||||
.get(jobId.value, user.id);
|
||||
|
||||
if (!existingJob) {
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
|
||||
const userOutputDir = `${outputDir}${user.id}/${jobId.value}/`;
|
||||
|
||||
// create the output directory
|
||||
try {
|
||||
await mkdir(userOutputDir, { recursive: true });
|
||||
} catch (error) {
|
||||
console.error(`Failed to create the output directory: ${userOutputDir}.`, error);
|
||||
}
|
||||
|
||||
const convertTo = normalizeFiletype(body.convert_to.split(",")[0] ?? "");
|
||||
const converterName = body.convert_to.split(",")[1];
|
||||
|
||||
if (!converterName) {
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const fileNames = JSON.parse(body.file_names) as string[];
|
||||
|
||||
for (let i = 0; i < fileNames.length; i++) {
|
||||
fileNames[i] = sanitize(fileNames[i] || "");
|
||||
}
|
||||
|
||||
if (!Array.isArray(fileNames) || fileNames.length === 0) {
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
db.query("UPDATE jobs SET num_files = ?1, status = 'pending' WHERE id = ?2").run(
|
||||
fileNames.length,
|
||||
jobId.value,
|
||||
);
|
||||
|
||||
// Start the conversion process in the background
|
||||
handleConvert(fileNames, userUploadsDir, userOutputDir, convertTo, converterName, jobId)
|
||||
.then(() => {
|
||||
// All conversions are done, update the job status to 'completed'
|
||||
if (jobId.value) {
|
||||
db.query("UPDATE jobs SET status = 'completed' WHERE id = ?1").run(jobId.value);
|
||||
}
|
||||
|
||||
// Delete all uploaded files in userUploadsDir
|
||||
// rmSync(userUploadsDir, { recursive: true, force: true });
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error in conversion process:", error);
|
||||
});
|
||||
|
||||
// Redirect the client immediately
|
||||
return redirect(`${WEBROOT}/results/${jobId.value}`, 302);
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
convert_to: t.String(),
|
||||
file_names: t.String(),
|
||||
}),
|
||||
auth: true,
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,32 +1,32 @@
|
|||
import { unlink } from "node:fs/promises";
|
||||
import { Elysia, t } from "elysia";
|
||||
import { uploadsDir } from "..";
|
||||
import db from "../db/db";
|
||||
import { WEBROOT } from "../helpers/env";
|
||||
import { userService } from "./user";
|
||||
|
||||
export const deleteFile = new Elysia().use(userService).post(
|
||||
"/delete",
|
||||
async ({ body, redirect, cookie: { jobId }, user }) => {
|
||||
if (!jobId?.value) {
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const existingJob = await db
|
||||
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
|
||||
.get(jobId.value, user.id);
|
||||
|
||||
if (!existingJob) {
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
|
||||
|
||||
await unlink(`${userUploadsDir}${body.filename}`);
|
||||
|
||||
return {
|
||||
message: "File deleted successfully.",
|
||||
};
|
||||
},
|
||||
{ body: t.Object({ filename: t.String() }), auth: true },
|
||||
);
|
||||
import { unlink } from "node:fs/promises";
|
||||
import { Elysia, t } from "elysia";
|
||||
import { uploadsDir } from "..";
|
||||
import db from "../db/db";
|
||||
import { WEBROOT } from "../helpers/env";
|
||||
import { userService } from "./user";
|
||||
|
||||
export const deleteFile = new Elysia().use(userService).post(
|
||||
"/delete",
|
||||
async ({ body, redirect, cookie: { jobId }, user }) => {
|
||||
if (!jobId?.value) {
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const existingJob = await db
|
||||
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
|
||||
.get(jobId.value, user.id);
|
||||
|
||||
if (!existingJob) {
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
|
||||
|
||||
await unlink(`${userUploadsDir}${body.filename}`);
|
||||
|
||||
return {
|
||||
message: "File deleted successfully.",
|
||||
};
|
||||
},
|
||||
{ body: t.Object({ filename: t.String() }), auth: true },
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,61 +1,65 @@
|
|||
import path from "node:path";
|
||||
import { Elysia, t } from 'elysia'
|
||||
import sanitize from "sanitize-filename";
|
||||
import * as tar from "tar";
|
||||
import { outputDir } from "..";
|
||||
import db from "../db/db";
|
||||
import { WEBROOT } from "../helpers/env";
|
||||
import { userService } from "./user";
|
||||
|
||||
export const download = new Elysia()
|
||||
.use(userService)
|
||||
.get(
|
||||
"/download/:userId/:jobId/:fileName",
|
||||
async ({ params, redirect, user }) => {
|
||||
const job = await db
|
||||
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
|
||||
.get(user.id, params.jobId);
|
||||
|
||||
if (!job) {
|
||||
return redirect(`${WEBROOT}/results`, 302);
|
||||
}
|
||||
// parse from URL encoded string
|
||||
const userId = decodeURIComponent(params.userId);
|
||||
const jobId = decodeURIComponent(params.jobId);
|
||||
const fileName = sanitize(decodeURIComponent(params.fileName));
|
||||
|
||||
const filePath = `${outputDir}${userId}/${jobId}/${fileName}`;
|
||||
return Bun.file(filePath);
|
||||
},
|
||||
{
|
||||
auth: true,
|
||||
}
|
||||
)
|
||||
.get("/archive/:userId/:jobId", async ({ params, redirect, user }) => {
|
||||
const job = await db
|
||||
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
|
||||
.get(user.id, params.jobId);
|
||||
|
||||
if (!job) {
|
||||
return redirect(`${WEBROOT}/results`, 302);
|
||||
}
|
||||
|
||||
const userId = decodeURIComponent(params.userId);
|
||||
const jobId = decodeURIComponent(params.jobId);
|
||||
const outputPath = `${outputDir}${userId}/${jobId}`;
|
||||
const outputTar = path.join(outputPath, `converted_files_${jobId}.tar`);
|
||||
|
||||
await tar.create(
|
||||
{
|
||||
file: outputTar,
|
||||
cwd: outputPath,
|
||||
filter: (path) => {
|
||||
return !path.match(".*\\.tar");
|
||||
},
|
||||
},
|
||||
["."],
|
||||
);
|
||||
return Bun.file(outputTar);
|
||||
}, {
|
||||
auth: true,
|
||||
});
|
||||
import path from "node:path";
|
||||
import { Elysia } from "elysia";
|
||||
import sanitize from "sanitize-filename";
|
||||
import * as tar from "tar";
|
||||
import { outputDir } from "..";
|
||||
import db from "../db/db";
|
||||
import { WEBROOT } from "../helpers/env";
|
||||
import { userService } from "./user";
|
||||
|
||||
export const download = new Elysia()
|
||||
.use(userService)
|
||||
.get(
|
||||
"/download/:userId/:jobId/:fileName",
|
||||
async ({ params, redirect, user }) => {
|
||||
const job = await db
|
||||
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
|
||||
.get(user.id, params.jobId);
|
||||
|
||||
if (!job) {
|
||||
return redirect(`${WEBROOT}/results`, 302);
|
||||
}
|
||||
// parse from URL encoded string
|
||||
const userId = decodeURIComponent(params.userId);
|
||||
const jobId = decodeURIComponent(params.jobId);
|
||||
const fileName = sanitize(decodeURIComponent(params.fileName));
|
||||
|
||||
const filePath = `${outputDir}${userId}/${jobId}/${fileName}`;
|
||||
return Bun.file(filePath);
|
||||
},
|
||||
{
|
||||
auth: true,
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/archive/:userId/:jobId",
|
||||
async ({ params, redirect, user }) => {
|
||||
const job = await db
|
||||
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
|
||||
.get(user.id, params.jobId);
|
||||
|
||||
if (!job) {
|
||||
return redirect(`${WEBROOT}/results`, 302);
|
||||
}
|
||||
|
||||
const userId = decodeURIComponent(params.userId);
|
||||
const jobId = decodeURIComponent(params.jobId);
|
||||
const outputPath = `${outputDir}${userId}/${jobId}`;
|
||||
const outputTar = path.join(outputPath, `converted_files_${jobId}.tar`);
|
||||
|
||||
await tar.create(
|
||||
{
|
||||
file: outputTar,
|
||||
cwd: outputPath,
|
||||
filter: (path) => {
|
||||
return !path.match(".*\\.tar");
|
||||
},
|
||||
},
|
||||
["."],
|
||||
);
|
||||
return Bun.file(outputTar);
|
||||
},
|
||||
{
|
||||
auth: true,
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,213 +1,215 @@
|
|||
import { Html } from "@elysiajs/html";
|
||||
import { Elysia } from "elysia";
|
||||
import { BaseHtml } from "../components/base";
|
||||
import { Header } from "../components/header";
|
||||
import db from "../db/db";
|
||||
import { Filename, Jobs } from "../db/types";
|
||||
import { ALLOW_UNAUTHENTICATED, HIDE_HISTORY, LANGUAGE, WEBROOT } from "../helpers/env";
|
||||
import { userService } from "./user";
|
||||
|
||||
export const history = new Elysia()
|
||||
.use(userService)
|
||||
.get("/history", async ({ jwt, redirect, user }) => {
|
||||
if (HIDE_HISTORY) {
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
let userJobs = db.query("SELECT * FROM jobs WHERE user_id = ?").as(Jobs).all(user.id).reverse();
|
||||
|
||||
for (const job of userJobs) {
|
||||
const files = db.query("SELECT * FROM file_names WHERE job_id = ?").as(Filename).all(job.id);
|
||||
|
||||
job.finished_files = files.length;
|
||||
job.files_detailed = files;
|
||||
}
|
||||
|
||||
// Filter out jobs with no files
|
||||
userJobs = userJobs.filter((job) => job.num_files > 0);
|
||||
|
||||
return (
|
||||
<BaseHtml webroot={WEBROOT} title="ConvertX | Results">
|
||||
<>
|
||||
<Header
|
||||
webroot={WEBROOT}
|
||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||
hideHistory={HIDE_HISTORY}
|
||||
loggedIn
|
||||
/>
|
||||
<main
|
||||
class={`
|
||||
w-full flex-1 px-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
<article class="article">
|
||||
<h1 class="mb-4 text-xl">Results</h1>
|
||||
<table
|
||||
class={`
|
||||
w-full table-auto overflow-y-auto rounded bg-neutral-900 text-left
|
||||
[&_td]:p-4
|
||||
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
|
||||
`}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
<span class="sr-only">Expand details</span>
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Time
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Files
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
max-sm:hidden
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Files Done
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
View
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{userJobs.map((job) => (
|
||||
<>
|
||||
<tr id={`job-row-${job.id}`}>
|
||||
<td class="job-details-toggle cursor-pointer" data-job-id={job.id}>
|
||||
<svg
|
||||
id={`arrow-${job.id}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="inline-block h-4 w-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M8.25 4.5l7.5 7.5-7.5 7.5"
|
||||
/>
|
||||
</svg>
|
||||
</td>
|
||||
<td safe>{new Date(job.date_created).toLocaleTimeString(LANGUAGE)}</td>
|
||||
<td>{job.num_files}</td>
|
||||
<td class="max-sm:hidden">{job.finished_files}</td>
|
||||
<td safe>{job.status}</td>
|
||||
<td>
|
||||
<a
|
||||
class={`
|
||||
text-accent-500 underline
|
||||
hover:text-accent-400
|
||||
`}
|
||||
href={`${WEBROOT}/results/${job.id}`}
|
||||
>
|
||||
View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id={`details-${job.id}`} class="hidden">
|
||||
<td colspan="6">
|
||||
<div class="p-2 text-sm text-neutral-500">
|
||||
<div class="mb-1 font-semibold">Detailed File Information:</div>
|
||||
{job.files_detailed.map((file: Filename) => (
|
||||
<div class="flex items-center">
|
||||
<span class="w-5/12 truncate" title={file.file_name} safe>
|
||||
{file.file_name}
|
||||
</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class={`mx-2 inline-block h-4 w-4 text-neutral-500`}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span class="w-5/12 truncate" title={file.output_file_name} safe>
|
||||
{file.output_file_name}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
</main>
|
||||
<script>
|
||||
{`
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const toggles = document.querySelectorAll('.job-details-toggle');
|
||||
toggles.forEach(toggle => {
|
||||
toggle.addEventListener('click', function() {
|
||||
const jobId = this.dataset.jobId;
|
||||
const detailsRow = document.getElementById(\`details-\${jobId}\`);
|
||||
// The arrow SVG itself has the ID arrow-\${jobId}
|
||||
const arrow = document.getElementById(\`arrow-\${jobId}\`);
|
||||
|
||||
if (detailsRow && arrow) {
|
||||
detailsRow.classList.toggle("hidden");
|
||||
if (detailsRow.classList.contains("hidden")) {
|
||||
// Right-facing arrow (collapsed)
|
||||
arrow.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />';
|
||||
} else {
|
||||
// Down-facing arrow (expanded)
|
||||
arrow.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
`}
|
||||
</script>
|
||||
</>
|
||||
</BaseHtml>
|
||||
);
|
||||
}, {
|
||||
auth: true
|
||||
});
|
||||
import { Html } from "@elysiajs/html";
|
||||
import { Elysia } from "elysia";
|
||||
import { BaseHtml } from "../components/base";
|
||||
import { Header } from "../components/header";
|
||||
import db from "../db/db";
|
||||
import { Filename, Jobs } from "../db/types";
|
||||
import { ALLOW_UNAUTHENTICATED, HIDE_HISTORY, LANGUAGE, WEBROOT } from "../helpers/env";
|
||||
import { userService } from "./user";
|
||||
|
||||
export const history = new Elysia().use(userService).get(
|
||||
"/history",
|
||||
async ({ redirect, user }) => {
|
||||
if (HIDE_HISTORY) {
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
let userJobs = db.query("SELECT * FROM jobs WHERE user_id = ?").as(Jobs).all(user.id).reverse();
|
||||
|
||||
for (const job of userJobs) {
|
||||
const files = db.query("SELECT * FROM file_names WHERE job_id = ?").as(Filename).all(job.id);
|
||||
|
||||
job.finished_files = files.length;
|
||||
job.files_detailed = files;
|
||||
}
|
||||
|
||||
// Filter out jobs with no files
|
||||
userJobs = userJobs.filter((job) => job.num_files > 0);
|
||||
|
||||
return (
|
||||
<BaseHtml webroot={WEBROOT} title="ConvertX | Results">
|
||||
<>
|
||||
<Header
|
||||
webroot={WEBROOT}
|
||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||
hideHistory={HIDE_HISTORY}
|
||||
loggedIn
|
||||
/>
|
||||
<main
|
||||
class={`
|
||||
w-full flex-1 px-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
<article class="article">
|
||||
<h1 class="mb-4 text-xl">Results</h1>
|
||||
<table
|
||||
class={`
|
||||
w-full table-auto overflow-y-auto rounded bg-neutral-900 text-left
|
||||
[&_td]:p-4
|
||||
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
|
||||
`}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
<span class="sr-only">Expand details</span>
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Time
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Files
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
max-sm:hidden
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Files Done
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
View
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{userJobs.map((job) => (
|
||||
<>
|
||||
<tr id={`job-row-${job.id}`}>
|
||||
<td class="job-details-toggle cursor-pointer" data-job-id={job.id}>
|
||||
<svg
|
||||
id={`arrow-${job.id}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="inline-block h-4 w-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M8.25 4.5l7.5 7.5-7.5 7.5"
|
||||
/>
|
||||
</svg>
|
||||
</td>
|
||||
<td safe>{new Date(job.date_created).toLocaleTimeString(LANGUAGE)}</td>
|
||||
<td>{job.num_files}</td>
|
||||
<td class="max-sm:hidden">{job.finished_files}</td>
|
||||
<td safe>{job.status}</td>
|
||||
<td>
|
||||
<a
|
||||
class={`
|
||||
text-accent-500 underline
|
||||
hover:text-accent-400
|
||||
`}
|
||||
href={`${WEBROOT}/results/${job.id}`}
|
||||
>
|
||||
View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id={`details-${job.id}`} class="hidden">
|
||||
<td colspan="6">
|
||||
<div class="p-2 text-sm text-neutral-500">
|
||||
<div class="mb-1 font-semibold">Detailed File Information:</div>
|
||||
{job.files_detailed.map((file: Filename) => (
|
||||
<div class="flex items-center">
|
||||
<span class="w-5/12 truncate" title={file.file_name} safe>
|
||||
{file.file_name}
|
||||
</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class={`mx-2 inline-block h-4 w-4 text-neutral-500`}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span class="w-5/12 truncate" title={file.output_file_name} safe>
|
||||
{file.output_file_name}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
</main>
|
||||
<script>
|
||||
{`
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const toggles = document.querySelectorAll('.job-details-toggle');
|
||||
toggles.forEach(toggle => {
|
||||
toggle.addEventListener('click', function() {
|
||||
const jobId = this.dataset.jobId;
|
||||
const detailsRow = document.getElementById(\`details-\${jobId}\`);
|
||||
// The arrow SVG itself has the ID arrow-\${jobId}
|
||||
const arrow = document.getElementById(\`arrow-\${jobId}\`);
|
||||
|
||||
if (detailsRow && arrow) {
|
||||
detailsRow.classList.toggle("hidden");
|
||||
if (detailsRow.classList.contains("hidden")) {
|
||||
// Right-facing arrow (collapsed)
|
||||
arrow.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />';
|
||||
} else {
|
||||
// Down-facing arrow (expanded)
|
||||
arrow.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
`}
|
||||
</script>
|
||||
</>
|
||||
</BaseHtml>
|
||||
);
|
||||
},
|
||||
{
|
||||
auth: true,
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,73 +1,75 @@
|
|||
import { Html } from "@elysiajs/html";
|
||||
import Elysia from "elysia";
|
||||
import { BaseHtml } from "../components/base";
|
||||
import { Header } from "../components/header";
|
||||
import { getAllInputs, getAllTargets } from "../converters/main";
|
||||
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
|
||||
import { userService } from "./user";
|
||||
|
||||
export const listConverters = new Elysia()
|
||||
.use(userService)
|
||||
.get("/converters", async () => {
|
||||
return (
|
||||
<BaseHtml webroot={WEBROOT} title="ConvertX | Converters">
|
||||
<>
|
||||
<Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn />
|
||||
<main
|
||||
class={`
|
||||
w-full flex-1 px-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
<article class="article">
|
||||
<h1 class="mb-4 text-xl">Converters</h1>
|
||||
<table
|
||||
class={`
|
||||
w-full table-auto rounded bg-neutral-900 text-left
|
||||
[&_td]:p-4
|
||||
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
|
||||
[&_ul]:list-inside [&_ul]:list-disc
|
||||
`}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="mx-4 my-2">Converter</th>
|
||||
<th class="mx-4 my-2">From (Count)</th>
|
||||
<th class="mx-4 my-2">To (Count)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.entries(getAllTargets()).map(([converter, targets]) => {
|
||||
const inputs = getAllInputs(converter);
|
||||
return (
|
||||
<tr>
|
||||
<td safe>{converter}</td>
|
||||
<td>
|
||||
Count: {inputs.length}
|
||||
<ul>
|
||||
{inputs.map((input) => (
|
||||
<li safe>{input}</li>
|
||||
))}
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
Count: {targets.length}
|
||||
<ul>
|
||||
{targets.map((target) => (
|
||||
<li safe>{target}</li>
|
||||
))}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
</main>
|
||||
</>
|
||||
</BaseHtml>
|
||||
);
|
||||
}, {
|
||||
auth: true
|
||||
});
|
||||
import { Html } from "@elysiajs/html";
|
||||
import Elysia from "elysia";
|
||||
import { BaseHtml } from "../components/base";
|
||||
import { Header } from "../components/header";
|
||||
import { getAllInputs, getAllTargets } from "../converters/main";
|
||||
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
|
||||
import { userService } from "./user";
|
||||
|
||||
export const listConverters = new Elysia().use(userService).get(
|
||||
"/converters",
|
||||
async () => {
|
||||
return (
|
||||
<BaseHtml webroot={WEBROOT} title="ConvertX | Converters">
|
||||
<>
|
||||
<Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn />
|
||||
<main
|
||||
class={`
|
||||
w-full flex-1 px-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
<article class="article">
|
||||
<h1 class="mb-4 text-xl">Converters</h1>
|
||||
<table
|
||||
class={`
|
||||
w-full table-auto rounded bg-neutral-900 text-left
|
||||
[&_td]:p-4
|
||||
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
|
||||
[&_ul]:list-inside [&_ul]:list-disc
|
||||
`}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="mx-4 my-2">Converter</th>
|
||||
<th class="mx-4 my-2">From (Count)</th>
|
||||
<th class="mx-4 my-2">To (Count)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.entries(getAllTargets()).map(([converter, targets]) => {
|
||||
const inputs = getAllInputs(converter);
|
||||
return (
|
||||
<tr>
|
||||
<td safe>{converter}</td>
|
||||
<td>
|
||||
Count: {inputs.length}
|
||||
<ul>
|
||||
{inputs.map((input) => (
|
||||
<li safe>{input}</li>
|
||||
))}
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
Count: {targets.length}
|
||||
<ul>
|
||||
{targets.map((target) => (
|
||||
<li safe>{target}</li>
|
||||
))}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
</main>
|
||||
</>
|
||||
</BaseHtml>
|
||||
);
|
||||
},
|
||||
{
|
||||
auth: true,
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,207 +1,215 @@
|
|||
import { Html } from "@elysiajs/html";
|
||||
import { JWTPayloadSpec } from "@elysiajs/jwt";
|
||||
import { Elysia } from "elysia";
|
||||
import { BaseHtml } from "../components/base";
|
||||
import { Header } from "../components/header";
|
||||
import db from "../db/db";
|
||||
import { Filename, Jobs } from "../db/types";
|
||||
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
|
||||
import { userService } from "./user";
|
||||
|
||||
function ResultsArticle({
|
||||
user,
|
||||
job,
|
||||
files,
|
||||
outputPath,
|
||||
}: {
|
||||
user: {
|
||||
id: string;
|
||||
} & JWTPayloadSpec;
|
||||
job: Jobs;
|
||||
files: Filename[];
|
||||
outputPath: string;
|
||||
}) {
|
||||
return (
|
||||
<article class="article">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h1 class="text-xl">Results</h1>
|
||||
<div>
|
||||
<a
|
||||
style={files.length !== job.num_files ? "pointer-events: none;" : ""}
|
||||
href={`${WEBROOT}/archive/${user.id}/${job.id}`}
|
||||
download={`converted_files_${job.id}.tar`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="float-right w-40 btn-primary"
|
||||
{...(files.length !== job.num_files ? { disabled: true, "aria-busy": "true" } : "")}
|
||||
>
|
||||
{files.length === job.num_files ? "Download All" : "Converting..."}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<progress
|
||||
max={job.num_files}
|
||||
value={files.length}
|
||||
class={`
|
||||
mb-4 inline-block h-2 w-full appearance-none overflow-hidden rounded-full border-0
|
||||
bg-neutral-700 bg-none text-accent-500 accent-accent-500
|
||||
[&::-moz-progress-bar]:bg-accent-500 [&::-webkit-progress-value]:rounded-full
|
||||
[&::-webkit-progress-value]:[background:none]
|
||||
[&[value]::-webkit-progress-value]:bg-accent-500
|
||||
[&[value]::-webkit-progress-value]:transition-[inline-size]
|
||||
`}
|
||||
/>
|
||||
<table
|
||||
class={`
|
||||
w-full table-auto rounded bg-neutral-900 text-left
|
||||
[&_td]:p-4
|
||||
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
|
||||
`}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Converted File Name
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
View
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Download
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files.map((file) => (
|
||||
<tr>
|
||||
<td safe class="max-w-[20vw] truncate">
|
||||
{file.output_file_name}
|
||||
</td>
|
||||
<td safe>{file.status}</td>
|
||||
<td>
|
||||
<a
|
||||
class={`
|
||||
text-accent-500 underline
|
||||
hover:text-accent-400
|
||||
`}
|
||||
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
|
||||
>
|
||||
View
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
class={`
|
||||
text-accent-500 underline
|
||||
hover:text-accent-400
|
||||
`}
|
||||
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
|
||||
download={file.output_file_name}
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export const results = new Elysia()
|
||||
.use(userService)
|
||||
.get("/results/:jobId", async ({ params, jwt, set, redirect, cookie: { job_id }, user }) => {
|
||||
if (job_id?.value) {
|
||||
// Clear the job_id cookie since we are viewing the results
|
||||
job_id.remove();
|
||||
}
|
||||
|
||||
const job = db
|
||||
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
|
||||
.as(Jobs)
|
||||
.get(user.id, params.jobId);
|
||||
|
||||
if (!job) {
|
||||
set.status = 404;
|
||||
return {
|
||||
message: "Job not found.",
|
||||
};
|
||||
}
|
||||
|
||||
const outputPath = `${user.id}/${params.jobId}/`;
|
||||
|
||||
const files = db
|
||||
.query("SELECT * FROM file_names WHERE job_id = ?")
|
||||
.as(Filename)
|
||||
.all(params.jobId);
|
||||
|
||||
return (
|
||||
<BaseHtml webroot={WEBROOT} title="ConvertX | Result">
|
||||
<>
|
||||
<Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn />
|
||||
<main
|
||||
class={`
|
||||
w-full flex-1 px-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
<ResultsArticle user={user} job={job} files={files} outputPath={outputPath} />
|
||||
</main>
|
||||
<script src={`${WEBROOT}/results.js`} defer />
|
||||
</>
|
||||
</BaseHtml>
|
||||
);
|
||||
}, { auth: true })
|
||||
.post("/progress/:jobId", async ({ jwt, set, params, cookie: { job_id }, user }) => {
|
||||
if (job_id?.value) {
|
||||
// Clear the job_id cookie since we are viewing the results
|
||||
job_id.remove();
|
||||
}
|
||||
|
||||
const job = db
|
||||
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
|
||||
.as(Jobs)
|
||||
.get(user.id, params.jobId);
|
||||
|
||||
if (!job) {
|
||||
set.status = 404;
|
||||
return {
|
||||
message: "Job not found.",
|
||||
};
|
||||
}
|
||||
|
||||
const outputPath = `${user.id}/${params.jobId}/`;
|
||||
|
||||
const files = db
|
||||
.query("SELECT * FROM file_names WHERE job_id = ?")
|
||||
.as(Filename)
|
||||
.all(params.jobId);
|
||||
|
||||
return <ResultsArticle user={user} job={job} files={files} outputPath={outputPath} />;
|
||||
}, { auth: true });
|
||||
import { Html } from "@elysiajs/html";
|
||||
import { JWTPayloadSpec } from "@elysiajs/jwt";
|
||||
import { Elysia } from "elysia";
|
||||
import { BaseHtml } from "../components/base";
|
||||
import { Header } from "../components/header";
|
||||
import db from "../db/db";
|
||||
import { Filename, Jobs } from "../db/types";
|
||||
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
|
||||
import { userService } from "./user";
|
||||
|
||||
function ResultsArticle({
|
||||
user,
|
||||
job,
|
||||
files,
|
||||
outputPath,
|
||||
}: {
|
||||
user: {
|
||||
id: string;
|
||||
} & JWTPayloadSpec;
|
||||
job: Jobs;
|
||||
files: Filename[];
|
||||
outputPath: string;
|
||||
}) {
|
||||
return (
|
||||
<article class="article">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h1 class="text-xl">Results</h1>
|
||||
<div>
|
||||
<a
|
||||
style={files.length !== job.num_files ? "pointer-events: none;" : ""}
|
||||
href={`${WEBROOT}/archive/${user.id}/${job.id}`}
|
||||
download={`converted_files_${job.id}.tar`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="float-right w-40 btn-primary"
|
||||
{...(files.length !== job.num_files ? { disabled: true, "aria-busy": "true" } : "")}
|
||||
>
|
||||
{files.length === job.num_files ? "Download All" : "Converting..."}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<progress
|
||||
max={job.num_files}
|
||||
value={files.length}
|
||||
class={`
|
||||
mb-4 inline-block h-2 w-full appearance-none overflow-hidden rounded-full border-0
|
||||
bg-neutral-700 bg-none text-accent-500 accent-accent-500
|
||||
[&::-moz-progress-bar]:bg-accent-500 [&::-webkit-progress-value]:rounded-full
|
||||
[&::-webkit-progress-value]:[background:none]
|
||||
[&[value]::-webkit-progress-value]:bg-accent-500
|
||||
[&[value]::-webkit-progress-value]:transition-[inline-size]
|
||||
`}
|
||||
/>
|
||||
<table
|
||||
class={`
|
||||
w-full table-auto rounded bg-neutral-900 text-left
|
||||
[&_td]:p-4
|
||||
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
|
||||
`}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Converted File Name
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
View
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Download
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files.map((file) => (
|
||||
<tr>
|
||||
<td safe class="max-w-[20vw] truncate">
|
||||
{file.output_file_name}
|
||||
</td>
|
||||
<td safe>{file.status}</td>
|
||||
<td>
|
||||
<a
|
||||
class={`
|
||||
text-accent-500 underline
|
||||
hover:text-accent-400
|
||||
`}
|
||||
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
|
||||
>
|
||||
View
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
class={`
|
||||
text-accent-500 underline
|
||||
hover:text-accent-400
|
||||
`}
|
||||
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
|
||||
download={file.output_file_name}
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export const results = new Elysia()
|
||||
.use(userService)
|
||||
.get(
|
||||
"/results/:jobId",
|
||||
async ({ params, set, cookie: { job_id }, user }) => {
|
||||
if (job_id?.value) {
|
||||
// Clear the job_id cookie since we are viewing the results
|
||||
job_id.remove();
|
||||
}
|
||||
|
||||
const job = db
|
||||
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
|
||||
.as(Jobs)
|
||||
.get(user.id, params.jobId);
|
||||
|
||||
if (!job) {
|
||||
set.status = 404;
|
||||
return {
|
||||
message: "Job not found.",
|
||||
};
|
||||
}
|
||||
|
||||
const outputPath = `${user.id}/${params.jobId}/`;
|
||||
|
||||
const files = db
|
||||
.query("SELECT * FROM file_names WHERE job_id = ?")
|
||||
.as(Filename)
|
||||
.all(params.jobId);
|
||||
|
||||
return (
|
||||
<BaseHtml webroot={WEBROOT} title="ConvertX | Result">
|
||||
<>
|
||||
<Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn />
|
||||
<main
|
||||
class={`
|
||||
w-full flex-1 px-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
<ResultsArticle user={user} job={job} files={files} outputPath={outputPath} />
|
||||
</main>
|
||||
<script src={`${WEBROOT}/results.js`} defer />
|
||||
</>
|
||||
</BaseHtml>
|
||||
);
|
||||
},
|
||||
{ auth: true },
|
||||
)
|
||||
.post(
|
||||
"/progress/:jobId",
|
||||
async ({ set, params, cookie: { job_id }, user }) => {
|
||||
if (job_id?.value) {
|
||||
// Clear the job_id cookie since we are viewing the results
|
||||
job_id.remove();
|
||||
}
|
||||
|
||||
const job = db
|
||||
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
|
||||
.as(Jobs)
|
||||
.get(user.id, params.jobId);
|
||||
|
||||
if (!job) {
|
||||
set.status = 404;
|
||||
return {
|
||||
message: "Job not found.",
|
||||
};
|
||||
}
|
||||
|
||||
const outputPath = `${user.id}/${params.jobId}/`;
|
||||
|
||||
const files = db
|
||||
.query("SELECT * FROM file_names WHERE job_id = ?")
|
||||
.as(Filename)
|
||||
.all(params.jobId);
|
||||
|
||||
return <ResultsArticle user={user} job={job} files={files} outputPath={outputPath} />;
|
||||
},
|
||||
{ auth: true },
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,248 +1,250 @@
|
|||
import { randomInt } from "node:crypto";
|
||||
import { Html } from "@elysiajs/html";
|
||||
import { JWTPayloadSpec } from "@elysiajs/jwt";
|
||||
import { Elysia, t } from "elysia";
|
||||
import { BaseHtml } from "../components/base";
|
||||
import { Header } from "../components/header";
|
||||
import { getAllTargets } from "../converters/main";
|
||||
import db from "../db/db";
|
||||
import { User } from "../db/types";
|
||||
import {
|
||||
ACCOUNT_REGISTRATION,
|
||||
ALLOW_UNAUTHENTICATED,
|
||||
HIDE_HISTORY,
|
||||
HTTP_ALLOWED,
|
||||
UNAUTHENTICATED_USER_SHARING,
|
||||
WEBROOT,
|
||||
} from "../helpers/env";
|
||||
import { FIRST_RUN, userService } from "./user";
|
||||
|
||||
export const root = new Elysia()
|
||||
.use(userService)
|
||||
.get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => {
|
||||
if (!ALLOW_UNAUTHENTICATED) {
|
||||
if (FIRST_RUN) {
|
||||
return redirect(`${WEBROOT}/setup`, 302);
|
||||
}
|
||||
|
||||
if (!auth?.value) {
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
}
|
||||
|
||||
// validate jwt
|
||||
let user: ({ id: string } & JWTPayloadSpec) | false = false;
|
||||
if (ALLOW_UNAUTHENTICATED) {
|
||||
const newUserId = String(
|
||||
UNAUTHENTICATED_USER_SHARING
|
||||
? 0
|
||||
: randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)),
|
||||
);
|
||||
const accessToken = await jwt.sign({
|
||||
id: newUserId,
|
||||
});
|
||||
|
||||
user = { id: newUserId };
|
||||
if (!auth) {
|
||||
return {
|
||||
message: "No auth cookie, perhaps your browser is blocking cookies.",
|
||||
};
|
||||
}
|
||||
|
||||
// set cookie
|
||||
auth.set({
|
||||
value: accessToken,
|
||||
httpOnly: true,
|
||||
secure: !HTTP_ALLOWED,
|
||||
maxAge: 24 * 60 * 60,
|
||||
sameSite: "strict",
|
||||
});
|
||||
} else if (auth?.value) {
|
||||
user = await jwt.verify(auth.value);
|
||||
|
||||
if (
|
||||
user !== false &&
|
||||
user.id &&
|
||||
(Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED)
|
||||
) {
|
||||
// Make sure user exists in db
|
||||
const existingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id);
|
||||
|
||||
if (!existingUser) {
|
||||
if (auth?.value) {
|
||||
auth.remove();
|
||||
}
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
// create a new job
|
||||
db.query("INSERT INTO jobs (user_id, date_created) VALUES (?, ?)").run(
|
||||
user.id,
|
||||
new Date().toISOString(),
|
||||
);
|
||||
|
||||
const { id } = db
|
||||
.query("SELECT id FROM jobs WHERE user_id = ? ORDER BY id DESC")
|
||||
.get(user.id) as { id: number };
|
||||
|
||||
if (!jobId) {
|
||||
return { message: "Cookies should be enabled to use this app." };
|
||||
}
|
||||
|
||||
jobId.set({
|
||||
value: id,
|
||||
httpOnly: true,
|
||||
secure: !HTTP_ALLOWED,
|
||||
maxAge: 24 * 60 * 60,
|
||||
sameSite: "strict",
|
||||
});
|
||||
|
||||
console.log("jobId set to:", id);
|
||||
|
||||
return (
|
||||
<BaseHtml webroot={WEBROOT}>
|
||||
<>
|
||||
<Header
|
||||
webroot={WEBROOT}
|
||||
accountRegistration={ACCOUNT_REGISTRATION}
|
||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||
hideHistory={HIDE_HISTORY}
|
||||
loggedIn
|
||||
/>
|
||||
<main
|
||||
class={`
|
||||
w-full flex-1 px-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
<article class="article">
|
||||
<h1 class="mb-4 text-xl">Convert</h1>
|
||||
<div class="mb-4 scrollbar-thin max-h-[50vh] overflow-y-auto">
|
||||
<table
|
||||
id="file-list"
|
||||
class={`
|
||||
w-full table-auto rounded bg-neutral-900
|
||||
[&_td]:p-4 [&_td]:first:max-w-[30vw] [&_td]:first:truncate
|
||||
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
id="dropzone"
|
||||
class={`
|
||||
relative flex h-48 w-full items-center justify-center rounded border border-dashed
|
||||
border-neutral-700 transition-all
|
||||
hover:border-neutral-600
|
||||
[&.dragover]:border-4 [&.dragover]:border-neutral-500
|
||||
`}
|
||||
>
|
||||
<span>
|
||||
<b>Choose a file</b> or drag it here
|
||||
</span>
|
||||
<input
|
||||
type="file"
|
||||
name="file"
|
||||
multiple
|
||||
class="absolute inset-0 size-full cursor-pointer opacity-0"
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
<form
|
||||
method="post"
|
||||
action={`${WEBROOT}/convert`}
|
||||
class="relative mx-auto mb-[35vh] w-full max-w-4xl"
|
||||
>
|
||||
<input type="hidden" name="file_names" id="file_names" />
|
||||
<article class="article w-full">
|
||||
<input
|
||||
type="search"
|
||||
name="convert_to_search"
|
||||
placeholder="Search for conversions"
|
||||
autocomplete="off"
|
||||
class="w-full rounded-sm bg-neutral-800 p-4"
|
||||
/>
|
||||
<div class="select_container relative">
|
||||
<article
|
||||
class={`
|
||||
convert_to_popup absolute z-2 m-0 hidden h-[30vh] max-h-[50vh] w-full flex-col
|
||||
overflow-x-hidden overflow-y-auto rounded bg-neutral-800
|
||||
sm:h-[30vh]
|
||||
`}
|
||||
>
|
||||
{Object.entries(getAllTargets()).map(([converter, targets]) => (
|
||||
<article
|
||||
class={`
|
||||
convert_to_group flex w-full flex-col border-b border-neutral-700 p-4
|
||||
`}
|
||||
data-converter={converter}
|
||||
>
|
||||
<header class="mb-2 w-full text-xl font-bold" safe>
|
||||
{converter}
|
||||
</header>
|
||||
<ul class={`convert_to_target flex flex-row flex-wrap gap-1`}>
|
||||
{targets.map((target) => (
|
||||
<button
|
||||
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
|
||||
tabindex={0}
|
||||
class={`
|
||||
target rounded bg-neutral-700 p-1 text-base
|
||||
hover:bg-neutral-600
|
||||
`}
|
||||
data-value={`${target},${converter}`}
|
||||
data-target={target}
|
||||
data-converter={converter}
|
||||
type="button"
|
||||
safe
|
||||
>
|
||||
{target}
|
||||
</button>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</article>
|
||||
|
||||
{/* Hidden element which determines the format to convert the file too and the converter to use */}
|
||||
<select name="convert_to" aria-label="Convert to" required hidden>
|
||||
<option selected disabled value="">
|
||||
Convert to
|
||||
</option>
|
||||
{Object.entries(getAllTargets()).map(([converter, targets]) => (
|
||||
<optgroup label={converter}>
|
||||
{targets.map((target) => (
|
||||
<option value={`${target},${converter}`} safe>
|
||||
{target}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</article>
|
||||
<input
|
||||
class={`
|
||||
w-full btn-primary opacity-100
|
||||
disabled:cursor-not-allowed disabled:opacity-50
|
||||
`}
|
||||
type="submit"
|
||||
value="Convert"
|
||||
disabled
|
||||
/>
|
||||
</form>
|
||||
</main>
|
||||
<script src="script.js" defer />
|
||||
</>
|
||||
</BaseHtml>
|
||||
);
|
||||
}, {
|
||||
cookie: t.Cookie({
|
||||
auth: t.Optional(t.String()),
|
||||
jobId: t.Optional(t.String()),
|
||||
})
|
||||
});
|
||||
import { randomInt } from "node:crypto";
|
||||
import { Html } from "@elysiajs/html";
|
||||
import { JWTPayloadSpec } from "@elysiajs/jwt";
|
||||
import { Elysia, t } from "elysia";
|
||||
import { BaseHtml } from "../components/base";
|
||||
import { Header } from "../components/header";
|
||||
import { getAllTargets } from "../converters/main";
|
||||
import db from "../db/db";
|
||||
import { User } from "../db/types";
|
||||
import {
|
||||
ACCOUNT_REGISTRATION,
|
||||
ALLOW_UNAUTHENTICATED,
|
||||
HIDE_HISTORY,
|
||||
HTTP_ALLOWED,
|
||||
UNAUTHENTICATED_USER_SHARING,
|
||||
WEBROOT,
|
||||
} from "../helpers/env";
|
||||
import { FIRST_RUN, userService } from "./user";
|
||||
|
||||
export const root = new Elysia().use(userService).get(
|
||||
"/",
|
||||
async ({ jwt, redirect, cookie: { auth, jobId } }) => {
|
||||
if (!ALLOW_UNAUTHENTICATED) {
|
||||
if (FIRST_RUN) {
|
||||
return redirect(`${WEBROOT}/setup`, 302);
|
||||
}
|
||||
|
||||
if (!auth?.value) {
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
}
|
||||
|
||||
// validate jwt
|
||||
let user: ({ id: string } & JWTPayloadSpec) | false = false;
|
||||
if (ALLOW_UNAUTHENTICATED) {
|
||||
const newUserId = String(
|
||||
UNAUTHENTICATED_USER_SHARING
|
||||
? 0
|
||||
: randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)),
|
||||
);
|
||||
const accessToken = await jwt.sign({
|
||||
id: newUserId,
|
||||
});
|
||||
|
||||
user = { id: newUserId };
|
||||
if (!auth) {
|
||||
return {
|
||||
message: "No auth cookie, perhaps your browser is blocking cookies.",
|
||||
};
|
||||
}
|
||||
|
||||
// set cookie
|
||||
auth.set({
|
||||
value: accessToken,
|
||||
httpOnly: true,
|
||||
secure: !HTTP_ALLOWED,
|
||||
maxAge: 24 * 60 * 60,
|
||||
sameSite: "strict",
|
||||
});
|
||||
} else if (auth?.value) {
|
||||
user = await jwt.verify(auth.value);
|
||||
|
||||
if (
|
||||
user !== false &&
|
||||
user.id &&
|
||||
(Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED)
|
||||
) {
|
||||
// Make sure user exists in db
|
||||
const existingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id);
|
||||
|
||||
if (!existingUser) {
|
||||
if (auth?.value) {
|
||||
auth.remove();
|
||||
}
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
// create a new job
|
||||
db.query("INSERT INTO jobs (user_id, date_created) VALUES (?, ?)").run(
|
||||
user.id,
|
||||
new Date().toISOString(),
|
||||
);
|
||||
|
||||
const { id } = db
|
||||
.query("SELECT id FROM jobs WHERE user_id = ? ORDER BY id DESC")
|
||||
.get(user.id) as { id: number };
|
||||
|
||||
if (!jobId) {
|
||||
return { message: "Cookies should be enabled to use this app." };
|
||||
}
|
||||
|
||||
jobId.set({
|
||||
value: id,
|
||||
httpOnly: true,
|
||||
secure: !HTTP_ALLOWED,
|
||||
maxAge: 24 * 60 * 60,
|
||||
sameSite: "strict",
|
||||
});
|
||||
|
||||
console.log("jobId set to:", id);
|
||||
|
||||
return (
|
||||
<BaseHtml webroot={WEBROOT}>
|
||||
<>
|
||||
<Header
|
||||
webroot={WEBROOT}
|
||||
accountRegistration={ACCOUNT_REGISTRATION}
|
||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||
hideHistory={HIDE_HISTORY}
|
||||
loggedIn
|
||||
/>
|
||||
<main
|
||||
class={`
|
||||
w-full flex-1 px-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
<article class="article">
|
||||
<h1 class="mb-4 text-xl">Convert</h1>
|
||||
<div class="mb-4 scrollbar-thin max-h-[50vh] overflow-y-auto">
|
||||
<table
|
||||
id="file-list"
|
||||
class={`
|
||||
w-full table-auto rounded bg-neutral-900
|
||||
[&_td]:p-4 [&_td]:first:max-w-[30vw] [&_td]:first:truncate
|
||||
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
id="dropzone"
|
||||
class={`
|
||||
relative flex h-48 w-full items-center justify-center rounded border border-dashed
|
||||
border-neutral-700 transition-all
|
||||
hover:border-neutral-600
|
||||
[&.dragover]:border-4 [&.dragover]:border-neutral-500
|
||||
`}
|
||||
>
|
||||
<span>
|
||||
<b>Choose a file</b> or drag it here
|
||||
</span>
|
||||
<input
|
||||
type="file"
|
||||
name="file"
|
||||
multiple
|
||||
class="absolute inset-0 size-full cursor-pointer opacity-0"
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
<form
|
||||
method="post"
|
||||
action={`${WEBROOT}/convert`}
|
||||
class="relative mx-auto mb-[35vh] w-full max-w-4xl"
|
||||
>
|
||||
<input type="hidden" name="file_names" id="file_names" />
|
||||
<article class="article w-full">
|
||||
<input
|
||||
type="search"
|
||||
name="convert_to_search"
|
||||
placeholder="Search for conversions"
|
||||
autocomplete="off"
|
||||
class="w-full rounded-sm bg-neutral-800 p-4"
|
||||
/>
|
||||
<div class="select_container relative">
|
||||
<article
|
||||
class={`
|
||||
convert_to_popup absolute z-2 m-0 hidden h-[30vh] max-h-[50vh] w-full flex-col
|
||||
overflow-x-hidden overflow-y-auto rounded bg-neutral-800
|
||||
sm:h-[30vh]
|
||||
`}
|
||||
>
|
||||
{Object.entries(getAllTargets()).map(([converter, targets]) => (
|
||||
<article
|
||||
class={`
|
||||
convert_to_group flex w-full flex-col border-b border-neutral-700 p-4
|
||||
`}
|
||||
data-converter={converter}
|
||||
>
|
||||
<header class="mb-2 w-full text-xl font-bold" safe>
|
||||
{converter}
|
||||
</header>
|
||||
<ul class={`convert_to_target flex flex-row flex-wrap gap-1`}>
|
||||
{targets.map((target) => (
|
||||
<button
|
||||
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
|
||||
tabindex={0}
|
||||
class={`
|
||||
target rounded bg-neutral-700 p-1 text-base
|
||||
hover:bg-neutral-600
|
||||
`}
|
||||
data-value={`${target},${converter}`}
|
||||
data-target={target}
|
||||
data-converter={converter}
|
||||
type="button"
|
||||
safe
|
||||
>
|
||||
{target}
|
||||
</button>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</article>
|
||||
|
||||
{/* Hidden element which determines the format to convert the file too and the converter to use */}
|
||||
<select name="convert_to" aria-label="Convert to" required hidden>
|
||||
<option selected disabled value="">
|
||||
Convert to
|
||||
</option>
|
||||
{Object.entries(getAllTargets()).map(([converter, targets]) => (
|
||||
<optgroup label={converter}>
|
||||
{targets.map((target) => (
|
||||
<option value={`${target},${converter}`} safe>
|
||||
{target}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</article>
|
||||
<input
|
||||
class={`
|
||||
w-full btn-primary opacity-100
|
||||
disabled:cursor-not-allowed disabled:opacity-50
|
||||
`}
|
||||
type="submit"
|
||||
value="Convert"
|
||||
disabled
|
||||
/>
|
||||
</form>
|
||||
</main>
|
||||
<script src="script.js" defer />
|
||||
</>
|
||||
</BaseHtml>
|
||||
);
|
||||
},
|
||||
{
|
||||
cookie: t.Cookie({
|
||||
auth: t.Optional(t.String()),
|
||||
jobId: t.Optional(t.String()),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,39 +1,39 @@
|
|||
import { Elysia, t } from "elysia";
|
||||
import db from "../db/db";
|
||||
import { WEBROOT } from "../helpers/env";
|
||||
import { uploadsDir } from "../index";
|
||||
import { userService } from "./user";
|
||||
|
||||
export const upload = new Elysia().use(userService).post(
|
||||
"/upload",
|
||||
async ({ body, redirect, user, cookie: { jobId } }) => {
|
||||
if (!jobId?.value) {
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const existingJob = await db
|
||||
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
|
||||
.get(jobId.value, user.id);
|
||||
|
||||
if (!existingJob) {
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
|
||||
|
||||
if (body?.file) {
|
||||
if (Array.isArray(body.file)) {
|
||||
for (const file of body.file) {
|
||||
await Bun.write(`${userUploadsDir}${file.name}`, file);
|
||||
}
|
||||
} else {
|
||||
await Bun.write(`${userUploadsDir}${body.file["name"]}`, body.file);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: "Files uploaded successfully.",
|
||||
};
|
||||
},
|
||||
{ body: t.Object({ file: t.Files() }), auth: true },
|
||||
);
|
||||
import { Elysia, t } from "elysia";
|
||||
import db from "../db/db";
|
||||
import { WEBROOT } from "../helpers/env";
|
||||
import { uploadsDir } from "../index";
|
||||
import { userService } from "./user";
|
||||
|
||||
export const upload = new Elysia().use(userService).post(
|
||||
"/upload",
|
||||
async ({ body, redirect, user, cookie: { jobId } }) => {
|
||||
if (!jobId?.value) {
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const existingJob = await db
|
||||
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
|
||||
.get(jobId.value, user.id);
|
||||
|
||||
if (!existingJob) {
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
|
||||
|
||||
if (body?.file) {
|
||||
if (Array.isArray(body.file)) {
|
||||
for (const file of body.file) {
|
||||
await Bun.write(`${userUploadsDir}${file.name}`, file);
|
||||
}
|
||||
} else {
|
||||
await Bun.write(`${userUploadsDir}${body.file["name"]}`, body.file);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: "Files uploaded successfully.",
|
||||
};
|
||||
},
|
||||
{ body: t.Object({ file: t.Files() }), auth: true },
|
||||
);
|
||||
|
|
|
|||
1041
src/pages/user.tsx
1041
src/pages/user.tsx
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue