feat: make all hardcoded limits configurable via env vars

- bodyLimit: conditional on MAX_UPLOAD_SIZE_MB (0 = 1GB practical max)
- rate limiting: disabled when RATE_LIMIT_PER_MIN=0
- shutdown timeout: 8s → 30s
- upload plugin: no fileSize/files cap when env=0
- session duration: configurable via SESSION_DURATION_HOURS (default 168h)
- login attempts: configurable via LOGIN_ATTEMPT_LIMIT
- batch/pipeline/svg-to-raster: skip guard when MAX_BATCH_SIZE=0
- pipeline steps: configurable via MAX_PIPELINE_STEPS (0 = unlimited)
- user-files: remove 200 hard cap
- stitch canvas: configurable via MAX_CANVAS_PIXELS (0 = unlimited)
- PDF pages: configurable via MAX_PDF_PAGES (0 = unlimited)
- SVG size: configurable via MAX_SVG_SIZE_MB (0 = unlimited)
- logo size: configurable via MAX_LOGO_SIZE_KB (default 2048)
- worker threads: auto-detect via resolveWorkerThreads (0 = auto)
- megapixels: skip validation when MAX_MEGAPIXELS=0
- seam carving: remove 1200px dimension cap
- concurrency: auto-detect via resolveConcurrency (0 = auto)
This commit is contained in:
ashim-hq
2026-04-20 21:50:17 +08:00
parent be254f9ca6
commit 6746989aa1
14 changed files with 57 additions and 81 deletions
+8 -40
View File
@@ -47,17 +47,11 @@ async function findCaire(): Promise<string> {
);
}
/** Max pixels on the longest edge before downscaling for caire. */
const MAX_CAIRE_DIMENSION = 1200;
/**
* Content-aware resize using caire (Go seam carving engine).
* Supports both shrinking and enlarging via seam removal/insertion.
*
* Large images (>1200px longest edge) are downscaled first because
* seam carving is O(width * height * seams) and becomes impractical
* on high-resolution inputs. JPEG intermediate is used because Go's
* JPEG decoder is significantly faster than PNG for large images.
* Processes at native resolution -- JPEG intermediate is used because
* Go's JPEG decoder is significantly faster than PNG for large images.
*/
export async function seamCarve(
inputBuffer: Buffer,
@@ -66,35 +60,18 @@ export async function seamCarve(
): Promise<SeamCarveResult> {
const cairePath = await findCaire();
const id = randomUUID();
// Use JPEG for input (fast decode in Go) and PNG for output (lossless)
const inputPath = join(outputDir, `caire-in-${id}.jpg`);
const outputPath = join(outputDir, `caire-out-${id}.png`);
try {
// Downscale large images and convert to JPEG for fast caire processing
const meta = await sharp(inputBuffer).metadata();
const origWidth = meta.width ?? 0;
const origHeight = meta.height ?? 0;
const longest = Math.max(origWidth, origHeight);
const width = meta.width ?? 0;
const height = meta.height ?? 0;
let width = origWidth;
let height = origHeight;
if (longest > MAX_CAIRE_DIMENSION) {
const scale = MAX_CAIRE_DIMENSION / longest;
width = Math.round(origWidth * scale);
height = Math.round(origHeight * scale);
}
// Always output JPEG for caire input (Go decodes JPEG 3-5x faster than PNG)
const processBuffer = await sharp(inputBuffer)
.resize(width, height, { fit: "fill" })
.jpeg({ quality: 95 })
.toBuffer();
const processBuffer = await sharp(inputBuffer).jpeg({ quality: 95 }).toBuffer();
await writeFile(inputPath, processBuffer);
// Build caire arguments
const args = ["-in", inputPath, "-out", outputPath, "-preview=false"];
if (options.square) {
@@ -102,19 +79,10 @@ export async function seamCarve(
args.push("-square", "-width", String(shortest), "-height", String(shortest));
} else {
if (options.width) {
// Scale user-specified dimensions proportionally if image was downscaled
const targetW =
longest > MAX_CAIRE_DIMENSION
? Math.round(options.width * (MAX_CAIRE_DIMENSION / longest))
: options.width;
args.push("-width", String(targetW));
args.push("-width", String(options.width));
}
if (options.height) {
const targetH =
longest > MAX_CAIRE_DIMENSION
? Math.round(options.height * (MAX_CAIRE_DIMENSION / longest))
: options.height;
args.push("-height", String(targetH));
args.push("-height", String(options.height));
}
}
@@ -122,7 +90,7 @@ export async function seamCarve(
if (options.blurRadius !== undefined) args.push("-blur", String(options.blurRadius));
if (options.sobelThreshold !== undefined) args.push("-sobel", String(options.sobelThreshold));
const megapixels = (origWidth * origHeight) / 1_000_000;
const megapixels = (width * height) / 1_000_000;
const timeoutMs = Math.max(120_000, megapixels * 10 * 1000);
await execFileAsync(cairePath, args, { timeout: timeoutMs });