add graphicsmagick
This commit is contained in:
parent
3e2338a7c5
commit
4cf06d9406
9 changed files with 706 additions and 87 deletions
14
Dockerfile
14
Dockerfile
|
|
@ -1,5 +1,3 @@
|
||||||
# use the official Bun image
|
|
||||||
# see all versions at https://hub.docker.com/r/oven/bun/tags
|
|
||||||
FROM oven/bun:1-debian as base
|
FROM oven/bun:1-debian as base
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
@ -28,14 +26,20 @@ RUN cd /temp/prod && bun install --frozen-lockfile --production
|
||||||
|
|
||||||
# copy production dependencies and source code into final image
|
# copy production dependencies and source code into final image
|
||||||
FROM base AS release
|
FROM base AS release
|
||||||
|
# install additional dependencies
|
||||||
|
RUN rm -rf /var/lib/apt/lists/partial && apt-get update -o Acquire::CompressionTypes::Order::=gz \
|
||||||
|
&& apt-get install -y \
|
||||||
|
pandoc \
|
||||||
|
texlive-latex-recommended \
|
||||||
|
ffmpeg \
|
||||||
|
graphicsmagick \
|
||||||
|
ghostscript
|
||||||
|
|
||||||
COPY --from=install /temp/prod/node_modules node_modules
|
COPY --from=install /temp/prod/node_modules node_modules
|
||||||
# COPY --from=prerelease /app/src/index.tsx /app/src/
|
# COPY --from=prerelease /app/src/index.tsx /app/src/
|
||||||
# COPY --from=prerelease /app/package.json .
|
# COPY --from=prerelease /app/package.json .
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# install additional dependencies
|
|
||||||
RUN apt-get update && apt-get install -y pandoc texlive-latex-recommended ffmpeg
|
|
||||||
|
|
||||||
# run the app
|
# run the app
|
||||||
USER bun
|
USER bun
|
||||||
EXPOSE 3000/tcp
|
EXPOSE 3000/tcp
|
||||||
|
|
|
||||||
41
README.md
Normal file
41
README.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# ConvertX
|
||||||
|
|
||||||
|
A self-hosted online file converter. Supports 708 different formats.
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Convert files to different formats
|
||||||
|
- Password protection
|
||||||
|
- Multiple accounts
|
||||||
|
|
||||||
|
|
||||||
|
## Converters supported
|
||||||
|
|
||||||
|
| Converter | Use case | Converts from | Converts to |
|
||||||
|
|----------------|---------------|---------------|-------------|
|
||||||
|
| Sharp | Images (fast) | 7 | 6 |
|
||||||
|
| Pandoc | Documents | 43 | 65 |
|
||||||
|
| GraphicsMagick | Images | 166 | 133 |
|
||||||
|
| FFmpeg | Video | 461 | 170 |
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
```yml
|
||||||
|
# docker-compose.yml
|
||||||
|
services:
|
||||||
|
convertx:
|
||||||
|
image: ghcr.io/c4illin/convertx:master
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment: # Defaults are listed below
|
||||||
|
- ACCOUNT_REGISTRATION=false # true or false
|
||||||
|
volumes:
|
||||||
|
- /path/you/want:/app/data
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- or
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run ghcr.io/c4illin/convertx:master -p 3000:3000 -e ACCOUNT_REGISTRATION=false -v /path/you/want:/app/data
|
||||||
|
``` -->
|
||||||
|
|
||||||
|
Then visit `http://localhost:3000` in your browser and create your account.
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
"name": "convertx-frontend",
|
"name": "convertx-frontend",
|
||||||
"version": "1.0.50",
|
"version": "1.0.50",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"dev": "bun run --watch src/index.tsx",
|
||||||
"dev": "bun run --hot src/index.tsx"
|
"hot": "bun run --hot src/index.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elysiajs/cookie": "^0.8.0",
|
"@elysiajs/cookie": "^0.8.0",
|
||||||
|
|
|
||||||
|
|
@ -659,16 +659,34 @@ export async function convert(
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||||
options?: any,
|
options?: any,
|
||||||
) {
|
) {
|
||||||
|
let command = "ffmpeg";
|
||||||
|
|
||||||
return exec(
|
const notWorking = ["bmp"];
|
||||||
`ffmpeg -f "${fileType}" -i "${filePath}" -f "${convertTo}" "${targetPath}"`,
|
|
||||||
(error, stdout, stderr) => {
|
if (!(fileType in notWorking)) {
|
||||||
|
command += ` -f "${fileType}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
command += ` -i "${filePath}"`;
|
||||||
|
|
||||||
|
if (!(convertTo in notWorking)) {
|
||||||
|
command += ` -f "${convertTo}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
command += " ${targetPath}";
|
||||||
|
|
||||||
|
return exec(command, (error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(`exec error: ${error}`);
|
console.error(`exec error: ${error}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (stdout) {
|
||||||
console.log(`stdout: ${stdout}`);
|
console.log(`stdout: ${stdout}`);
|
||||||
console.error(`stderr: ${stderr}`);
|
}
|
||||||
},
|
|
||||||
);
|
if (stderr) {
|
||||||
|
console.error(`stderr: ${stderr}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
335
src/converters/graphicsmagick.ts
Normal file
335
src/converters/graphicsmagick.ts
Normal file
|
|
@ -0,0 +1,335 @@
|
||||||
|
import { exec } from "node:child_process";
|
||||||
|
|
||||||
|
export const properties = {
|
||||||
|
from: {
|
||||||
|
image: [
|
||||||
|
"3fr",
|
||||||
|
"8bim",
|
||||||
|
"8bimtext",
|
||||||
|
"8bimwtext",
|
||||||
|
"app1",
|
||||||
|
"app1jpeg",
|
||||||
|
"art",
|
||||||
|
"arw",
|
||||||
|
"avs",
|
||||||
|
"b",
|
||||||
|
"bie",
|
||||||
|
"bigtiff",
|
||||||
|
"bmp",
|
||||||
|
"c",
|
||||||
|
"cals",
|
||||||
|
"caption",
|
||||||
|
"cin",
|
||||||
|
"cmyk",
|
||||||
|
"cmyka",
|
||||||
|
"cr2",
|
||||||
|
"crw",
|
||||||
|
"cur",
|
||||||
|
"cut",
|
||||||
|
"dcm",
|
||||||
|
"dcr",
|
||||||
|
"dcx",
|
||||||
|
"dng",
|
||||||
|
"dpx",
|
||||||
|
"epdf",
|
||||||
|
"epi",
|
||||||
|
"eps",
|
||||||
|
"epsf",
|
||||||
|
"epsi",
|
||||||
|
"ept",
|
||||||
|
"ept2",
|
||||||
|
"ept3",
|
||||||
|
"erf",
|
||||||
|
"exif",
|
||||||
|
"fax",
|
||||||
|
"file",
|
||||||
|
"fits",
|
||||||
|
"fractal",
|
||||||
|
"ftp",
|
||||||
|
"g",
|
||||||
|
"gif",
|
||||||
|
"gif87",
|
||||||
|
"gradient",
|
||||||
|
"gray",
|
||||||
|
"graya",
|
||||||
|
"heic",
|
||||||
|
"heif",
|
||||||
|
"hrz",
|
||||||
|
"http",
|
||||||
|
"icb",
|
||||||
|
"icc",
|
||||||
|
"icm",
|
||||||
|
"ico",
|
||||||
|
"icon",
|
||||||
|
"identity",
|
||||||
|
"image",
|
||||||
|
"iptc",
|
||||||
|
"iptctext",
|
||||||
|
"iptcwtext",
|
||||||
|
"jbg",
|
||||||
|
"jbig",
|
||||||
|
"jng",
|
||||||
|
"jnx",
|
||||||
|
"jpeg",
|
||||||
|
"jpg",
|
||||||
|
"k",
|
||||||
|
"k25",
|
||||||
|
"kdc",
|
||||||
|
"label",
|
||||||
|
"m",
|
||||||
|
"mac",
|
||||||
|
"map",
|
||||||
|
"mat",
|
||||||
|
"mef",
|
||||||
|
"miff",
|
||||||
|
"mng",
|
||||||
|
"mono",
|
||||||
|
"mpc",
|
||||||
|
"mrw",
|
||||||
|
"msl",
|
||||||
|
"mtv",
|
||||||
|
"mvg",
|
||||||
|
"nef",
|
||||||
|
"null",
|
||||||
|
"o",
|
||||||
|
"orf",
|
||||||
|
"otb",
|
||||||
|
"p7",
|
||||||
|
"pal",
|
||||||
|
"palm",
|
||||||
|
"pam",
|
||||||
|
"pbm",
|
||||||
|
"pcd",
|
||||||
|
"pcds",
|
||||||
|
"pct",
|
||||||
|
"pcx",
|
||||||
|
"pdb",
|
||||||
|
"pdf",
|
||||||
|
"pef",
|
||||||
|
"pfa",
|
||||||
|
"pfb",
|
||||||
|
"pgm",
|
||||||
|
"picon",
|
||||||
|
"pict",
|
||||||
|
"pix",
|
||||||
|
"plasma",
|
||||||
|
"png",
|
||||||
|
"png00",
|
||||||
|
"png24",
|
||||||
|
"png32",
|
||||||
|
"png48",
|
||||||
|
"png64",
|
||||||
|
"png8",
|
||||||
|
"pnm",
|
||||||
|
"ppm",
|
||||||
|
"ps",
|
||||||
|
"ptif",
|
||||||
|
"pwp",
|
||||||
|
"r",
|
||||||
|
"raf",
|
||||||
|
"ras",
|
||||||
|
"rgb",
|
||||||
|
"rgba",
|
||||||
|
"rla",
|
||||||
|
"rle",
|
||||||
|
"sct",
|
||||||
|
"sfw",
|
||||||
|
"sgi",
|
||||||
|
"sr2",
|
||||||
|
"srf",
|
||||||
|
"stegano",
|
||||||
|
"sun",
|
||||||
|
"svg",
|
||||||
|
"svgz",
|
||||||
|
"text",
|
||||||
|
"tga",
|
||||||
|
"tiff",
|
||||||
|
"tile",
|
||||||
|
"tim",
|
||||||
|
"topol",
|
||||||
|
"ttf",
|
||||||
|
"txt",
|
||||||
|
"uyvy",
|
||||||
|
"vda",
|
||||||
|
"vicar",
|
||||||
|
"vid",
|
||||||
|
"viff",
|
||||||
|
"vst",
|
||||||
|
"wbmp",
|
||||||
|
"webp",
|
||||||
|
"wmf",
|
||||||
|
"wpg",
|
||||||
|
"x3f",
|
||||||
|
"xbm",
|
||||||
|
"xc",
|
||||||
|
"xcf",
|
||||||
|
"xmp",
|
||||||
|
"xpm",
|
||||||
|
"xv",
|
||||||
|
"xwd",
|
||||||
|
"y",
|
||||||
|
"yuv",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
image: [
|
||||||
|
"8bim",
|
||||||
|
"8bimtext",
|
||||||
|
"8bimwtext",
|
||||||
|
"app1",
|
||||||
|
"app1jpeg",
|
||||||
|
"art",
|
||||||
|
"avs",
|
||||||
|
"b",
|
||||||
|
"bie",
|
||||||
|
"bigtiff",
|
||||||
|
"bmp",
|
||||||
|
"bmp2",
|
||||||
|
"bmp3",
|
||||||
|
"brf",
|
||||||
|
"c",
|
||||||
|
"cals",
|
||||||
|
"cin",
|
||||||
|
"cmyk",
|
||||||
|
"cmyka",
|
||||||
|
"dcx",
|
||||||
|
"dpx",
|
||||||
|
"epdf",
|
||||||
|
"epi",
|
||||||
|
"eps",
|
||||||
|
"eps2",
|
||||||
|
"eps3",
|
||||||
|
"epsf",
|
||||||
|
"epsi",
|
||||||
|
"ept",
|
||||||
|
"ept2",
|
||||||
|
"ept3",
|
||||||
|
"exif",
|
||||||
|
"fax",
|
||||||
|
"fits",
|
||||||
|
"g",
|
||||||
|
"gif",
|
||||||
|
"gif87",
|
||||||
|
"gray",
|
||||||
|
"graya",
|
||||||
|
"histogram",
|
||||||
|
"html",
|
||||||
|
"icb",
|
||||||
|
"icc",
|
||||||
|
"icm",
|
||||||
|
"info",
|
||||||
|
"iptc",
|
||||||
|
"iptctext",
|
||||||
|
"iptcwtext",
|
||||||
|
"isobrl",
|
||||||
|
"isobrl6",
|
||||||
|
"jbg",
|
||||||
|
"jbig",
|
||||||
|
"jng",
|
||||||
|
"jpeg",
|
||||||
|
"jpg",
|
||||||
|
"k",
|
||||||
|
"m",
|
||||||
|
"m2v",
|
||||||
|
"map",
|
||||||
|
"mat",
|
||||||
|
"matte",
|
||||||
|
"miff",
|
||||||
|
"mng",
|
||||||
|
"mono",
|
||||||
|
"mpc",
|
||||||
|
"mpeg",
|
||||||
|
"mpg",
|
||||||
|
"msl",
|
||||||
|
"mtv",
|
||||||
|
"mvg",
|
||||||
|
"null",
|
||||||
|
"o",
|
||||||
|
"otb",
|
||||||
|
"p7",
|
||||||
|
"pal",
|
||||||
|
"pam",
|
||||||
|
"pbm",
|
||||||
|
"pcd",
|
||||||
|
"pcds",
|
||||||
|
"pcl",
|
||||||
|
"pct",
|
||||||
|
"pcx",
|
||||||
|
"pdb",
|
||||||
|
"pdf",
|
||||||
|
"pgm",
|
||||||
|
"picon",
|
||||||
|
"pict",
|
||||||
|
"png",
|
||||||
|
"png00",
|
||||||
|
"png24",
|
||||||
|
"png32",
|
||||||
|
"png48",
|
||||||
|
"png64",
|
||||||
|
"png8",
|
||||||
|
"pnm",
|
||||||
|
"ppm",
|
||||||
|
"preview",
|
||||||
|
"ps",
|
||||||
|
"ps2",
|
||||||
|
"ps3",
|
||||||
|
"ptif",
|
||||||
|
"r",
|
||||||
|
"ras",
|
||||||
|
"rgb",
|
||||||
|
"rgba",
|
||||||
|
"sgi",
|
||||||
|
"shtml",
|
||||||
|
"sun",
|
||||||
|
"text",
|
||||||
|
"tga",
|
||||||
|
"tiff",
|
||||||
|
"txt",
|
||||||
|
"ubrl",
|
||||||
|
"ubrl6",
|
||||||
|
"uil",
|
||||||
|
"uyvy",
|
||||||
|
"vda",
|
||||||
|
"vicar",
|
||||||
|
"vid",
|
||||||
|
"viff",
|
||||||
|
"vst",
|
||||||
|
"wbmp",
|
||||||
|
"webp",
|
||||||
|
"x",
|
||||||
|
"xbm",
|
||||||
|
"xmp",
|
||||||
|
"xpm",
|
||||||
|
"xv",
|
||||||
|
"xwd",
|
||||||
|
"y",
|
||||||
|
"yuv",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function convert(
|
||||||
|
filePath: string,
|
||||||
|
fileType: string,
|
||||||
|
convertTo: string,
|
||||||
|
targetPath: string,
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||||
|
options?: any,
|
||||||
|
) {
|
||||||
|
return exec(
|
||||||
|
`gm convert "${filePath}" "${targetPath}"`,
|
||||||
|
(error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
console.error(`exec error: ${error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (stdout) {
|
||||||
|
console.log(`stdout: ${stdout}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
console.error(`stderr: ${stderr}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -9,10 +9,15 @@ import {
|
||||||
} from "./pandoc";
|
} from "./pandoc";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
properties as propertiesFfmpeg,
|
properties as propertiesFFmpeg,
|
||||||
convert as convertFfmpeg,
|
convert as convertFFmpeg,
|
||||||
} from "./ffmpeg";
|
} from "./ffmpeg";
|
||||||
|
|
||||||
|
import {
|
||||||
|
properties as propertiesGraphicsmagick,
|
||||||
|
convert as convertGraphicsmagick,
|
||||||
|
} from "./graphicsmagick";
|
||||||
|
|
||||||
const properties: {
|
const properties: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
properties: {
|
properties: {
|
||||||
|
|
@ -48,9 +53,13 @@ const properties: {
|
||||||
properties: propertiesPandoc,
|
properties: propertiesPandoc,
|
||||||
converter: convertPandoc,
|
converter: convertPandoc,
|
||||||
},
|
},
|
||||||
|
graphicsmagick: {
|
||||||
|
properties: propertiesGraphicsmagick,
|
||||||
|
converter: convertGraphicsmagick,
|
||||||
|
},
|
||||||
ffmpeg: {
|
ffmpeg: {
|
||||||
properties: propertiesFfmpeg,
|
properties: propertiesFFmpeg,
|
||||||
converter: convertFfmpeg,
|
converter: convertFFmpeg,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -86,6 +95,7 @@ export async function mainConverter(
|
||||||
|
|
||||||
for (const key in converterObj.properties.from) {
|
for (const key in converterObj.properties.from) {
|
||||||
if (
|
if (
|
||||||
|
// HOW??
|
||||||
converterObj.properties.from[key].includes(fileType) &&
|
converterObj.properties.from[key].includes(fileType) &&
|
||||||
converterObj.properties.to[key].includes(convertTo)
|
converterObj.properties.to[key].includes(convertTo)
|
||||||
) {
|
) {
|
||||||
|
|
@ -122,7 +132,7 @@ export async function mainConverter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const possibleConversions: { [key: string]: { [key: string]: string[] } } = {};
|
const possibleTargets: { [key: string]: { [key: string]: string[] } } = {};
|
||||||
|
|
||||||
for (const converterName in properties) {
|
for (const converterName in properties) {
|
||||||
const converterProperties = properties[converterName]?.properties;
|
const converterProperties = properties[converterName]?.properties;
|
||||||
|
|
@ -137,30 +147,44 @@ for (const converterName in properties) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const extension of converterProperties.from[key] ?? []) {
|
for (const extension of converterProperties.from[key] ?? []) {
|
||||||
if (!possibleConversions[extension]) {
|
if (!possibleTargets[extension]) {
|
||||||
possibleConversions[extension] = {};
|
possibleTargets[extension] = {};
|
||||||
}
|
}
|
||||||
possibleConversions[extension][converterName] =
|
|
||||||
|
possibleTargets[extension][converterName] =
|
||||||
converterProperties.to[key] || [];
|
converterProperties.to[key] || [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// // save all possible conversions to a file
|
export const getPossibleTargets = (
|
||||||
// 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,
|
from: string,
|
||||||
): { [key: string]: string[] } => {
|
): { [key: string]: string[] } => {
|
||||||
const fromClean = normalizeFiletype(from);
|
const fromClean = normalizeFiletype(from);
|
||||||
|
|
||||||
return possibleConversions[fromClean] || {};
|
return possibleTargets[fromClean] || {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const possibleInputs: string[] = [];
|
||||||
|
for (const converterName in properties) {
|
||||||
|
const converterProperties = properties[converterName]?.properties;
|
||||||
|
|
||||||
|
if (!converterProperties) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in converterProperties.from) {
|
||||||
|
for (const extension of converterProperties.from[key] ?? []) {
|
||||||
|
if (!possibleInputs.includes(extension)) {
|
||||||
|
possibleInputs.push(extension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
possibleInputs.sort();
|
||||||
|
|
||||||
|
export const getPossibleInputs = () => {
|
||||||
|
return possibleInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const allTargets: { [key: string]: string[] } = {};
|
const allTargets: { [key: string]: string[] } = {};
|
||||||
|
|
@ -184,3 +208,50 @@ for (const converterName in properties) {
|
||||||
export const getAllTargets = () => {
|
export const getAllTargets = () => {
|
||||||
return allTargets;
|
return allTargets;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const allInputs: { [key: string]: string[] } = {};
|
||||||
|
for (const converterName in properties) {
|
||||||
|
const converterProperties = properties[converterName]?.properties;
|
||||||
|
|
||||||
|
if (!converterProperties) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in converterProperties.from) {
|
||||||
|
if (allInputs[converterName]) {
|
||||||
|
allInputs[converterName].push(...converterProperties.from[key]);
|
||||||
|
} else {
|
||||||
|
allInputs[converterName] = converterProperties.from[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAllInputs = (converter: string) => {
|
||||||
|
return allInputs[converter] || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// // count the number of unique formats
|
||||||
|
// const uniqueFormats = new Set();
|
||||||
|
|
||||||
|
// for (const converterName in properties) {
|
||||||
|
// const converterProperties = properties[converterName]?.properties;
|
||||||
|
|
||||||
|
// if (!converterProperties) {
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for (const key in converterProperties.from) {
|
||||||
|
// for (const extension of converterProperties.from[key] ?? []) {
|
||||||
|
// uniqueFormats.add(extension);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for (const key in converterProperties.to) {
|
||||||
|
// for (const extension of converterProperties.to[key] ?? []) {
|
||||||
|
// uniqueFormats.add(extension);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // print the number of unique Inputs and Outputs
|
||||||
|
// console.log(`Unique Formats: ${uniqueFormats.size}`);
|
||||||
File diff suppressed because one or more lines are too long
190
src/index.tsx
190
src/index.tsx
|
|
@ -10,8 +10,10 @@ import { BaseHtml } from "./components/base";
|
||||||
import { Header } from "./components/header";
|
import { Header } from "./components/header";
|
||||||
import {
|
import {
|
||||||
mainConverter,
|
mainConverter,
|
||||||
getPossibleConversions,
|
getPossibleTargets,
|
||||||
|
getPossibleInputs,
|
||||||
getAllTargets,
|
getAllTargets,
|
||||||
|
getAllInputs,
|
||||||
} from "./converters/main";
|
} from "./converters/main";
|
||||||
import { normalizeFiletype } from "./helpers/normalizeFiletype";
|
import { normalizeFiletype } from "./helpers/normalizeFiletype";
|
||||||
|
|
||||||
|
|
@ -19,7 +21,7 @@ const db = new Database("./data/mydb.sqlite", { create: true });
|
||||||
const uploadsDir = "./data/uploads/";
|
const uploadsDir = "./data/uploads/";
|
||||||
const outputDir = "./data/output/";
|
const outputDir = "./data/output/";
|
||||||
|
|
||||||
const accountRegistration =
|
const ACCOUNT_REGISTRATION =
|
||||||
process.env.ACCOUNT_REGISTRATION === "true" || false;
|
process.env.ACCOUNT_REGISTRATION === "true" || false;
|
||||||
|
|
||||||
// fileNames: fileNames,
|
// fileNames: fileNames,
|
||||||
|
|
@ -50,6 +52,8 @@ CREATE TABLE IF NOT EXISTS jobs (
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
);`);
|
);`);
|
||||||
|
|
||||||
|
let FIRST_RUN = db.query("SELECT * FROM users").get() === null || false;
|
||||||
|
|
||||||
interface IUser {
|
interface IUser {
|
||||||
id: number;
|
id: number;
|
||||||
email: string;
|
email: string;
|
||||||
|
|
@ -94,7 +98,54 @@ const app = new Elysia()
|
||||||
prefix: "/",
|
prefix: "/",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.get("/register", () => {
|
.get("/setup", ({ redirect }) => {
|
||||||
|
if (!FIRST_RUN) {
|
||||||
|
return redirect("/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseHtml title="ConvertX | Setup">
|
||||||
|
<main class="container">
|
||||||
|
<h1>Welcome to ConvertX</h1>
|
||||||
|
<article>
|
||||||
|
<header>Create your account</header>
|
||||||
|
<form method="post" action="/register">
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
Email/Username
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
placeholder="Email"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Password
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
placeholder="Password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<input type="submit" value="Create account" />
|
||||||
|
</form>
|
||||||
|
<footer>
|
||||||
|
Report any issues on{" "}
|
||||||
|
<a href="https://github.com/C4illin/ConvertX">GitHub</a>.
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
</BaseHtml>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.get("/register", ({ redirect }) => {
|
||||||
|
if (!ACCOUNT_REGISTRATION) {
|
||||||
|
return redirect("/login");
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseHtml title="ConvertX | Register">
|
<BaseHtml title="ConvertX | Register">
|
||||||
<Header />
|
<Header />
|
||||||
|
|
@ -130,7 +181,15 @@ const app = new Elysia()
|
||||||
})
|
})
|
||||||
.post(
|
.post(
|
||||||
"/register",
|
"/register",
|
||||||
async ({ body, set, jwt, cookie: { auth } }) => {
|
async ({ body, set, redirect, jwt, cookie: { auth } }) => {
|
||||||
|
if (!ACCOUNT_REGISTRATION && !FIRST_RUN) {
|
||||||
|
return redirect("/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FIRST_RUN) {
|
||||||
|
FIRST_RUN = false;
|
||||||
|
}
|
||||||
|
|
||||||
const existingUser = await db
|
const existingUser = await db
|
||||||
.query("SELECT * FROM users WHERE email = ?")
|
.query("SELECT * FROM users WHERE email = ?")
|
||||||
.get(body.email);
|
.get(body.email);
|
||||||
|
|
@ -171,20 +230,18 @@ const app = new Elysia()
|
||||||
sameSite: "strict",
|
sameSite: "strict",
|
||||||
});
|
});
|
||||||
|
|
||||||
// redirect to home
|
redirect("/");
|
||||||
set.status = 302;
|
|
||||||
set.headers = {
|
|
||||||
Location: "/",
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
{ body: t.Object({ email: t.String(), password: t.String() }) },
|
{ body: t.Object({ email: t.String(), password: t.String() }) },
|
||||||
)
|
)
|
||||||
.get("/login", async ({ jwt, redirect, cookie: { auth } }) => {
|
.get("/login", async ({ jwt, redirect, cookie: { auth } }) => {
|
||||||
console.log("login handler");
|
if (FIRST_RUN) {
|
||||||
|
return redirect("/setup");
|
||||||
|
}
|
||||||
|
|
||||||
// if already logged in, redirect to home
|
// if already logged in, redirect to home
|
||||||
if (auth?.value) {
|
if (auth?.value) {
|
||||||
const user = await jwt.verify(auth.value);
|
const user = await jwt.verify(auth.value);
|
||||||
console.log(user);
|
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
return redirect("/");
|
return redirect("/");
|
||||||
|
|
@ -296,6 +353,10 @@ const app = new Elysia()
|
||||||
return redirect("/login");
|
return redirect("/login");
|
||||||
})
|
})
|
||||||
.get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => {
|
.get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => {
|
||||||
|
if (FIRST_RUN) {
|
||||||
|
return redirect("/setup");
|
||||||
|
}
|
||||||
|
|
||||||
if (!auth?.value) {
|
if (!auth?.value) {
|
||||||
return redirect("/login");
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
@ -347,8 +408,20 @@ const app = new Elysia()
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<article>
|
<article>
|
||||||
<h1>Convert</h1>
|
<h1>Convert</h1>
|
||||||
<table id="file-list" />
|
<div style={{ maxHeight: "50vh", overflowY: "auto" }}>
|
||||||
|
<table id="file-list" class="striped" />
|
||||||
|
</div>
|
||||||
<input type="file" name="file" multiple />
|
<input type="file" name="file" multiple />
|
||||||
|
{/* <label for="convert_from">Convert from</label> */}
|
||||||
|
<select name="convert_from" aria-label="Convert from" required>
|
||||||
|
<option selected disabled value="">
|
||||||
|
Convert from
|
||||||
|
</option>
|
||||||
|
{getPossibleInputs().map((input) => (
|
||||||
|
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
|
||||||
|
<option>{input}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
</article>
|
</article>
|
||||||
<form method="post" action="/convert">
|
<form method="post" action="/convert">
|
||||||
<input type="hidden" name="file_names" id="file_names" />
|
<input type="hidden" name="file_names" id="file_names" />
|
||||||
|
|
@ -378,13 +451,13 @@ const app = new Elysia()
|
||||||
.post(
|
.post(
|
||||||
"/conversions",
|
"/conversions",
|
||||||
({ body }) => {
|
({ body }) => {
|
||||||
console.log(body);
|
|
||||||
return (
|
return (
|
||||||
<select name="convert_to" aria-label="Convert to" required>
|
<select name="convert_to" aria-label="Convert to" required>
|
||||||
<option selected disabled value="">
|
<option selected disabled value="">
|
||||||
Convert to
|
Convert to
|
||||||
</option>
|
</option>
|
||||||
{Object.entries(getPossibleConversions(body.fileType)).map(([converter, targets]) => (
|
{Object.entries(getPossibleTargets(body.fileType)).map(
|
||||||
|
([converter, targets]) => (
|
||||||
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
|
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
|
||||||
<optgroup label={converter}>
|
<optgroup label={converter}>
|
||||||
{targets.map((target) => (
|
{targets.map((target) => (
|
||||||
|
|
@ -392,7 +465,8 @@ const app = new Elysia()
|
||||||
<option value={`${target},${converter}`}>{target}</option>
|
<option value={`${target},${converter}`}>{target}</option>
|
||||||
))}
|
))}
|
||||||
</optgroup>
|
</optgroup>
|
||||||
))}
|
),
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -513,7 +587,9 @@ const app = new Elysia()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const convertTo = normalizeFiletype(body.convert_to.split(",")[0] as string);
|
const convertTo = normalizeFiletype(
|
||||||
|
body.convert_to.split(",")[0] as string,
|
||||||
|
);
|
||||||
const converterName = body.convert_to.split(",")[1];
|
const converterName = body.convert_to.split(",")[1];
|
||||||
const fileNames = JSON.parse(body.file_names) as string[];
|
const fileNames = JSON.parse(body.file_names) as string[];
|
||||||
|
|
||||||
|
|
@ -532,16 +608,25 @@ const app = new Elysia()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Start the conversion process in the background
|
// Start the conversion process in the background
|
||||||
Promise.all(fileNames.map(async (fileName) => {
|
Promise.all(
|
||||||
|
fileNames.map(async (fileName) => {
|
||||||
const filePath = `${userUploadsDir}${fileName}`;
|
const filePath = `${userUploadsDir}${fileName}`;
|
||||||
const fileTypeOrig = fileName.split(".").pop() as string;
|
const fileTypeOrig = fileName.split(".").pop() as string;
|
||||||
const fileType = normalizeFiletype(fileTypeOrig);
|
const fileType = normalizeFiletype(fileTypeOrig);
|
||||||
const newFileName = fileName.replace(fileTypeOrig, convertTo);
|
const newFileName = fileName.replace(fileTypeOrig, convertTo);
|
||||||
const targetPath = `${userOutputDir}${newFileName}`;
|
const targetPath = `${userOutputDir}${newFileName}`;
|
||||||
|
|
||||||
await mainConverter(filePath, fileType, convertTo, targetPath, {}, converterName);
|
await mainConverter(
|
||||||
|
filePath,
|
||||||
|
fileType,
|
||||||
|
convertTo,
|
||||||
|
targetPath,
|
||||||
|
{},
|
||||||
|
converterName,
|
||||||
|
);
|
||||||
query.run(jobId.value, fileName, newFileName);
|
query.run(jobId.value, fileName, newFileName);
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// All conversions are done, update the job status to 'completed'
|
// All conversions are done, update the job status to 'completed'
|
||||||
db.run(
|
db.run(
|
||||||
|
|
@ -550,7 +635,7 @@ const app = new Elysia()
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Error in conversion process:', error);
|
console.error("Error in conversion process:", error);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Redirect the client immediately
|
// Redirect the client immediately
|
||||||
|
|
@ -564,20 +649,16 @@ const app = new Elysia()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.get("/test", async ({ jwt, redirect, cookie: { auth } }) => {
|
.get("/test", async ({ jwt, redirect, cookie: { auth } }) => {
|
||||||
console.log("results page");
|
|
||||||
|
|
||||||
if (!auth?.value) {
|
if (!auth?.value) {
|
||||||
console.log("no auth value");
|
|
||||||
return redirect("/login");
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
const user = await jwt.verify(auth.value);
|
const user = await jwt.verify(auth.value);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
console.log("no user");
|
|
||||||
return redirect("/login");
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
const userJobs = db
|
let userJobs = db
|
||||||
.query("SELECT * FROM jobs WHERE user_id = ?")
|
.query("SELECT * FROM jobs WHERE user_id = ?")
|
||||||
.all(user.id) as IJobs[];
|
.all(user.id) as IJobs[];
|
||||||
|
|
||||||
|
|
@ -589,6 +670,9 @@ const app = new Elysia()
|
||||||
job.finished_files = files.length;
|
job.finished_files = files.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// filter out jobs with no files
|
||||||
|
userJobs = userJobs.filter((job) => job.num_files > 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseHtml title="ConvertX | Results">
|
<BaseHtml title="ConvertX | Results">
|
||||||
<Header loggedIn />
|
<Header loggedIn />
|
||||||
|
|
@ -741,6 +825,62 @@ const app = new Elysia()
|
||||||
return Bun.file(filePath);
|
return Bun.file(filePath);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.get("/converters", async ({ params, jwt, redirect, cookie: { auth } }) => {
|
||||||
|
if (!auth?.value) {
|
||||||
|
return redirect("/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await jwt.verify(auth.value);
|
||||||
|
if (!user) {
|
||||||
|
return redirect("/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseHtml title="ConvertX | Converters">
|
||||||
|
<Header loggedIn />
|
||||||
|
<main class="container">
|
||||||
|
<article>
|
||||||
|
<h1>Converters</h1>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Converter</th>
|
||||||
|
<th>From (Count)</th>
|
||||||
|
<th>To (Count)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Object.entries(getAllTargets()).map(([converter, targets]) => {
|
||||||
|
const inputs = getAllInputs(converter);
|
||||||
|
return (
|
||||||
|
<tr key={converter}>
|
||||||
|
<td>{converter}</td>
|
||||||
|
<td>
|
||||||
|
Count: {inputs.length}
|
||||||
|
<ul>
|
||||||
|
{inputs.map((input, index) => (
|
||||||
|
<li key={index}>{input}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Count: {targets.length}
|
||||||
|
<ul>
|
||||||
|
{targets.map((target, index) => (
|
||||||
|
<li key={index}>{target}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
</BaseHtml>
|
||||||
|
);
|
||||||
|
})
|
||||||
.get(
|
.get(
|
||||||
"/zip/:userId/:jobId",
|
"/zip/:userId/:jobId",
|
||||||
async ({ params, jwt, redirect, cookie: { auth } }) => {
|
async ({ params, jwt, redirect, cookie: { auth } }) => {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ let fileType;
|
||||||
|
|
||||||
const selectElem = document.querySelector("select[name='convert_to']");
|
const selectElem = document.querySelector("select[name='convert_to']");
|
||||||
|
|
||||||
|
const convertFromSelect = document.querySelector("select[name='convert_from']");
|
||||||
|
|
||||||
// Add a 'change' event listener to the file input element
|
// Add a 'change' event listener to the file input element
|
||||||
fileInput.addEventListener("change", (e) => {
|
fileInput.addEventListener("change", (e) => {
|
||||||
console.log(e.target.files);
|
console.log(e.target.files);
|
||||||
|
|
@ -20,8 +22,8 @@ fileInput.addEventListener("change", (e) => {
|
||||||
const row = document.createElement("tr");
|
const row = document.createElement("tr");
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td>${file.name}</td>
|
<td>${file.name}</td>
|
||||||
<td>${(file.size / 1024 / 1024).toFixed(2)} MB</td>
|
<td>${(file.size / 1024).toFixed(2)} kB</td>
|
||||||
<td><button class="secondary" onclick="deleteRow(this)">x</button></td>
|
<td><a class="secondary" onclick="deleteRow(this)" style="cursor: pointer">Remove</a></td>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (!fileType) {
|
if (!fileType) {
|
||||||
|
|
@ -30,6 +32,15 @@ fileInput.addEventListener("change", (e) => {
|
||||||
fileInput.setAttribute("accept", `.${fileType}`);
|
fileInput.setAttribute("accept", `.${fileType}`);
|
||||||
setTitle();
|
setTitle();
|
||||||
|
|
||||||
|
// choose the option that matches the file type
|
||||||
|
for (const option of convertFromSelect.children) {
|
||||||
|
console.log(option.value);
|
||||||
|
if (option.value === fileType) {
|
||||||
|
option.selected = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fetch("/conversions", {
|
fetch("/conversions", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ fileType: fileType }),
|
body: JSON.stringify({ fileType: fileType }),
|
||||||
|
|
@ -57,7 +68,7 @@ fileInput.addEventListener("change", (e) => {
|
||||||
|
|
||||||
const setTitle = () => {
|
const setTitle = () => {
|
||||||
const title = document.querySelector("h1");
|
const title = document.querySelector("h1");
|
||||||
title.textContent = `Convert ${fileType ? `.${fileType}` : "___"}`;
|
title.textContent = `Convert ${fileType ? `.${fileType}` : ""}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add a onclick for the delete button
|
// Add a onclick for the delete button
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue