Skip to content
Merged
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ node_modules
/priv/domain_blacklist.txt
/priv/plts
/priv/db
/priv/puppeteer
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ algora-*.tar

# Ignore assets that are produced by build tools.
/priv/static/assets/
/priv/puppeteer

# Ignore digested assets cache.
/priv/static/cache_manifest.json
Expand Down
18 changes: 14 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,17 @@
ARG ELIXIR_VERSION=1.18.1
ARG OTP_VERSION=27.2
ARG DEBIAN_VERSION=bookworm-20241223-slim
ARG NODE_VERSION=23-bookworm-slim

ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
ARG NODE_IMAGE="node:${NODE_VERSION}"

FROM ${NODE_IMAGE} as node_stage

ENV PUPPETEER_CACHE_DIR="/app/puppeteer"

RUN npx --yes puppeteer browsers install

FROM ${BUILDER_IMAGE} as builder

Expand Down Expand Up @@ -52,7 +59,7 @@ COPY lib lib

COPY assets assets

COPY --from=node:23-bookworm-slim /usr/local/bin /usr/local/bin
COPY --from=node_stage /usr/local/bin /usr/local/bin

# compile assets
RUN mix assets.deploy
Expand All @@ -77,9 +84,9 @@ RUN apt-get update -y && \
# TODO: remove after migration
RUN apt-get update -y && apt-get install -y postgresql-client

COPY --from=node:23-bookworm-slim /usr/local/bin /usr/local/bin
COPY --from=node_stage /usr/local/bin /usr/local/bin

RUN npm install -g @algora/[email protected]
RUN apt-get update -y && apt-get install -y ca-certificates fonts-liberation libasound2 libatk-bridge2.0-0 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 lsb-release wget xdg-utils

# Set the locale
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
Expand All @@ -94,9 +101,12 @@ RUN chown nobody /app
# set runner ENV
ENV MIX_ENV="prod"

# Only copy the final release from the build stage
# Copy the final release from the build stage
COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/algora ./

# Copy the puppeteer cache from the node stage
COPY --from=node_stage --chown=nobody:root /app/puppeteer ./puppeteer

USER nobody

# If using an environment that doesn't automatically reap zombie processes, it is
Expand Down
20 changes: 20 additions & 0 deletions assets/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@ let optsServer = {
],
};

let optsPuppeteer = {
entryPoints: ["js/puppeteer-img.js"],
platform: "node",
bundle: true,
minify: true,
target: "node19.6.1",
conditions: [],
outdir: "../priv/puppeteer",
logLevel: "info",
sourcemap: watch ? "inline" : false,
tsconfig: "./tsconfig.json",
plugins: [],
};

if (watch) {
esbuild
.context(optsClient)
Expand All @@ -56,7 +70,13 @@ if (watch) {
.context(optsServer)
.then((ctx) => ctx.watch())
.catch((_error) => process.exit(1));

esbuild
.context(optsPuppeteer)
.then((ctx) => ctx.watch())
.catch((_error) => process.exit(1));
} else {
esbuild.build(optsClient);
esbuild.build(optsServer);
esbuild.build(optsPuppeteer);
}
6 changes: 5 additions & 1 deletion assets/js/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,11 @@ topbar.config({
barColors: { 0: "rgba(5, 150, 105, 1)" },
shadowColor: "rgba(0, 0, 0, .3)",
});
window.addEventListener("phx:page-loading-start", (info) => topbar.show(300));
window.addEventListener("phx:page-loading-start", (info) => {
if (!window.location.search.includes("screenshot")) {
topbar.show(300);
}
});
window.addEventListener("phx:page-loading-stop", (info) => topbar.hide());

// Accessible routing
Expand Down
126 changes: 126 additions & 0 deletions assets/js/puppeteer-img.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import puppeteer from "puppeteer";

function parseArgs() {
const args = process.argv.slice(2);
const options = {
type: "png",
path: null,
width: "800",
height: "600",
scaleFactor: "1",
x: null,
y: null,
clipWidth: null,
clipHeight: null,
};

for (let i = 0; i < args.length; i++) {
let arg = args[i];
let value = null;

[arg, value] = arg.split("=");

switch (arg) {
case "-t":
case "--type":
options.type = value;
break;
case "-p":
case "--path":
options.path = value;
break;
case "-w":
case "--width":
options.width = value;
break;
case "-h":
case "--height":
options.height = value;
break;
case "-s":
case "--scale-factor":
options.scaleFactor = value;
break;
case "-x":
case "--x":
options.x = value;
break;
case "-y":
case "--y":
options.y = value;
break;
case "--clip-width":
options.clipWidth = value;
break;
case "--clip-height":
options.clipHeight = value;
break;
}
}

// URL is the first non-option argument
options.url = args.find((arg) => !arg.startsWith("-"));
return options;
}

function _validateInteger(value) {
const parsed = parseInt(value);
if (value && !parsed) {
console.error("Number values must be valid integer");
return null;
}
return parsed;
}

(async () => {
const options = parseArgs();
let screenshotOptions = {};
let viewportOptions = {};

if (!options.url) {
console.error("URL required");
return;
}

viewportOptions.width = _validateInteger(options.width) || 800;
viewportOptions.height = _validateInteger(options.height) || 600;
viewportOptions.deviceScaleFactor =
_validateInteger(options.scaleFactor) || 1;
screenshotOptions.type = ["jpeg", "png"].includes(options.type)
? options.type
: "png";
screenshotOptions.path = options.path || `./image.${screenshotOptions.type}`;

const clipParams = {
x: options.x,
y: options.y,
width: options.clipWidth,
height: options.clipHeight,
};
const hasClipParams = Object.values(clipParams).every((val) => val !== null);

if (hasClipParams) {
screenshotOptions.clip = {};
for (const [key, value] of Object.entries(clipParams)) {
screenshotOptions.clip[key] = _validateInteger(value);
}
}

puppeteer
.launch({
devtools: false,
args: ["--no-sandbox", "--disable-setuid-sandbox", "--single-process"],
ignoreHTTPSErrors: true,
})
.then(async function (browser) {
const page = await browser.newPage();
await page.setViewport(viewportOptions);
await page.goto(options.url, { waitUntil: ["networkidle2"] });
await page.focus("body");
await page.screenshot(screenshotOptions);

await page.close();
await browser.close();
process.stdout.write(screenshotOptions.path);
});
})();
1 change: 1 addition & 0 deletions assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"esbuild": "^0.24.0",
"esbuild-plugin-import-glob": "^0.1.1",
"esbuild-svelte": "^0.9.0",
"puppeteer": "^24.4.0",
"svelte": "^4.2.19",
"svelte-preprocess": "^6.0.3",
"tailwindcss-animate": "^1.0.7",
Expand Down
Loading