Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ RUN ARCH=$(uname -m) && \
COPY --from=install /temp/prod/node_modules node_modules
COPY --from=prerelease /app/public/ /app/public/
COPY --from=prerelease /app/dist /app/dist
COPY entrypoint.sh /app/entrypoint.sh

# COPY . .
RUN mkdir data
Expand All @@ -111,4 +112,4 @@ EXPOSE 3000/tcp
# used for calibre
ENV QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox"
ENV NODE_ENV=production
ENTRYPOINT [ "bun", "run", "dist/src/index.js" ]
ENTRYPOINT [ "/app/entrypoint.sh" ]
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ All are optional, JWT_SECRET is recommended to be set.
| ACCOUNT_REGISTRATION | false | Allow users to register accounts |
| HTTP_ALLOWED | false | Allow HTTP connections, only set this to true locally |
| ALLOW_UNAUTHENTICATED | false | Allow unauthenticated users to use the service, only set this to true locally |
| ALLOW_URL_UPLOAD | false | Allow uploading/converting files from URLs |
| AUTO_DELETE_EVERY_N_HOURS | 24 | Checks every n hours for files older then n hours and deletes them, set to 0 to disable |
| WEBROOT | | The address to the root path setting this to "/convert" will serve the website on "example.com/convert/" |
| BRANDING | ConvertX | Custom string that allows you to change the display name of the website in the header (max 26 characters) |
Expand All @@ -103,6 +104,8 @@ All are optional, JWT_SECRET is recommended to be set.
| LANGUAGE | en | Language to format date strings in, specified as a [BCP 47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag) |
| UNAUTHENTICATED_USER_SHARING | false | Shares conversion history between all unauthenticated users |
| MAX_CONVERT_PROCESS | 0 | Maximum number of concurrent conversion processes allowed. Set to 0 for unlimited. |
| MAGICK_MAX_WIDTH | unset (ImageMagick default) | (Docker only) ImageMagick maximum image width in pixels |
| MAGICK_MAX_HEIGHT | unset (ImageMagick default) | (Docker only) ImageMagick maximum image height in pixels |
| PORT | 3000 | Application listen port |

### Docker images
Expand Down
15 changes: 6 additions & 9 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/bash
set -e

if [ -n "$MAGICK_MAX_WIDTH" ]; then
if ! [[ "$MAGICK_MAX_WIDTH" =~ ^[0-9]+$ ]]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When MAGICK_MAX_WIDTH or MAGICK_MAX_HEIGHT is set to 0, this check accepts it despite requiring a positive integer, then writes a zero ImageMagick dimension limit. Reject zero values so invalid configuration fails at startup rather than making normal image conversions exceed the limit.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At entrypoint.sh, line 5:

<comment>When `MAGICK_MAX_WIDTH` or `MAGICK_MAX_HEIGHT` is set to `0`, this check accepts it despite requiring a positive integer, then writes a zero ImageMagick dimension limit. Reject zero values so invalid configuration fails at startup rather than making normal image conversions exceed the limit.</comment>

<file context>
@@ -1,20 +1,44 @@
 set -e
 
+if [ -n "$MAGICK_MAX_WIDTH" ]; then
+  if ! [[ "$MAGICK_MAX_WIDTH" =~ ^[0-9]+$ ]]; then
+    echo "Error: MAGICK_MAX_WIDTH must be a positive integer in pixels" >&2
+    exit 1
</file context>

echo "Error: MAGICK_MAX_WIDTH must be a positive integer in pixels" >&2
exit 1
fi
fi

if [ -n "$MAGICK_MAX_HEIGHT" ]; then
if ! [[ "$MAGICK_MAX_HEIGHT" =~ ^[0-9]+$ ]]; then
echo "Error: MAGICK_MAX_HEIGHT must be a positive integer in pixels" >&2
exit 1
fi
fi

if [ -n "$MAGICK_MAX_WIDTH" ] || [ -n "$MAGICK_MAX_HEIGHT" ]; then
POLICY_DIR="/etc/ImageMagick-7"
POLICY_FILE="$POLICY_DIR/policy.xml"

if [ -f "$POLICY_FILE" ] && grep -q "</policymap>" "$POLICY_FILE"; then
[ -n "$MAGICK_MAX_WIDTH" ] && sed -i '/<policy domain="resource" name="width"/d' "$POLICY_FILE"
[ -n "$MAGICK_MAX_HEIGHT" ] && sed -i '/<policy domain="resource" name="height"/d' "$POLICY_FILE"
if [ -n "$MAGICK_MAX_WIDTH" ]; then
sed -i "s|</policymap>| <policy domain=\"resource\" name=\"width\" value=\"$MAGICK_MAX_WIDTH\"/>\n</policymap>|" "$POLICY_FILE"
fi
if [ -n "$MAGICK_MAX_HEIGHT" ]; then
sed -i "s|</policymap>| <policy domain=\"resource\" name=\"height\" value=\"$MAGICK_MAX_HEIGHT\"/>\n</policymap>|" "$POLICY_FILE"
fi
else
mkdir -p "$POLICY_DIR"
echo "<policymap>" > "$POLICY_FILE"
if [ -n "$MAGICK_MAX_WIDTH" ]; then
echo " <policy domain=\"resource\" name=\"width\" value=\"$MAGICK_MAX_WIDTH\"/>" >> "$POLICY_FILE"
fi
if [ -n "$MAGICK_MAX_HEIGHT" ]; then
echo " <policy domain=\"resource\" name=\"height\" value=\"$MAGICK_MAX_HEIGHT\"/>" >> "$POLICY_FILE"
fi
echo "</policymap>" >> "$POLICY_FILE"
fi
fi

exec bun run dist/src/index.js
1 change: 1 addition & 0 deletions eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export default defineConfig(
"target",
"convert_to_target",
"job-details-toggle",
"share-btn",
],
},
],
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@elysiajs/static": "^1.4.10",
"@kitajs/html": "^4.2.13",
"elysia": "1.4.30",
"mime": "^4.1.0",
"sanitize-filename": "^1.6.4",
"tar": "^7.5.22"
},
Expand Down
52 changes: 52 additions & 0 deletions public/results.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,56 @@ const jobId = window.location.pathname.split("/").pop();
const main = document.querySelector("main");
let progressElem = document.querySelector("progress");

const supportsWebShare = navigator.share && navigator.canShare;

const setupShareButtons = () => {
if (!supportsWebShare) {
return;
}

document.querySelectorAll(".share-btn").forEach((btn) => {
if (btn.dataset.setupComplete) return;

const filename = btn.dataset.filename;
const mimeType = btn.dataset.mimeType;
const fileUrl = btn.dataset.downloadUrl;
const dummyFile = new File([], filename, { type: mimeType });

if (!navigator.canShare({ files: [dummyFile] })) {
return;
}
let cachedFile = null;
let isFetching = false;

btn.addEventListener("click", async (e) => {
e.preventDefault();
if (isFetching) return;
try {
if (!cachedFile) {
isFetching = true;
Comment on lines +31 to +32

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: After the first attempt caches the file, this branch skips isFetching = true, so rapid clicks can start concurrent navigator.share calls. Set isFetching before the cache check and leave finally to clear it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At public/results.js, line 31:

<comment>After the first attempt caches the file, this branch skips `isFetching = true`, so rapid clicks can start concurrent `navigator.share` calls. Set `isFetching` before the cache check and leave `finally` to clear it.</comment>

<file context>
@@ -21,17 +21,31 @@ const setupShareButtons = () => {
-        const blob = await response.blob();
-        const file = new File([blob], filename, { type: mimeType });
-        await navigator.share({ files: [file] });
+        if (!cachedFile) {
+          isFetching = true;
+          const response = await fetch(fileUrl);
</file context>
Suggested change
if (!cachedFile) {
isFetching = true;
isFetching = true;
if (!cachedFile) {

const response = await fetch(fileUrl);
if (!response.ok) {
throw new Error(`Failed to download file (${response.status})`);
}
const blob = await response.blob();
cachedFile = new File([blob], filename, { type: mimeType });
}
await navigator.share({ files: [cachedFile] });
} catch (err) {
if (err.name === "NotAllowedError" && cachedFile) {
alert("File is ready. Please tap the share button again to share.");
} else if (err.name !== "AbortError") {
console.error("Error sharing:", err);
}
} finally {
isFetching = false;
}
});
btn.style.display = "";
btn.dataset.setupComplete = true;
});
};

const refreshData = () => {
// console.log("Refreshing data...", progressElem.value, progressElem.max);
if (progressElem.value !== progressElem.max) {
Expand All @@ -12,6 +62,7 @@ const refreshData = () => {
.then((res) => res.text())
.then((html) => {
main.innerHTML = html;
setupShareButtons();
})
.catch((err) => console.log(err));

Expand All @@ -21,6 +72,7 @@ const refreshData = () => {
progressElem = document.querySelector("progress");
};

setupShareButtons();
refreshData();

window.downloadAll = function () {
Expand Down
Loading