mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
chore: remove swagger deps, parallelize CI jobs
- Remove @fastify/swagger and @fastify/swagger-ui (API docs live on GitHub Pages) - Run typecheck, build, and docker CI jobs in parallel instead of sequentially
This commit is contained in:
@@ -1,10 +1,7 @@
|
||||
import Fastify from "fastify";
|
||||
import cors from "@fastify/cors";
|
||||
import rateLimit from "@fastify/rate-limit";
|
||||
import swagger from "@fastify/swagger";
|
||||
import swaggerUi from "@fastify/swagger-ui";
|
||||
import { env } from "./config.js";
|
||||
import { APP_VERSION } from "@stirling-image/shared";
|
||||
import { runMigrations } from "./db/migrate.js";
|
||||
import { ensureDefaultAdmin, authRoutes, authMiddleware } from "./plugins/auth.js";
|
||||
import { registerUpload } from "./plugins/upload.js";
|
||||
@@ -51,24 +48,6 @@ await app.register(rateLimit, {
|
||||
timeWindow: "1 minute",
|
||||
});
|
||||
|
||||
// Swagger / OpenAPI documentation (dev only)
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
await app.register(swagger, {
|
||||
openapi: {
|
||||
info: {
|
||||
title: "Stirling Image API",
|
||||
description: "API for Stirling Image — self-hosted image processing suite",
|
||||
version: APP_VERSION,
|
||||
},
|
||||
servers: [{ url: `http://localhost:1349` }],
|
||||
},
|
||||
});
|
||||
|
||||
await app.register(swaggerUi, {
|
||||
routePrefix: "/api/docs",
|
||||
});
|
||||
}
|
||||
|
||||
// Multipart upload support
|
||||
await registerUpload(app);
|
||||
|
||||
|
||||
@@ -117,17 +117,34 @@ export async function registerBatchRoutes(
|
||||
};
|
||||
updateJobProgress({ ...progress });
|
||||
|
||||
// Set up response headers for ZIP streaming
|
||||
// Tell Fastify we're taking over the response — without this,
|
||||
// Fastify's lifecycle hooks conflict with reply.raw.writeHead()
|
||||
// and can throw unhandled errors that crash the process.
|
||||
reply.hijack();
|
||||
|
||||
// Set up response headers for ZIP streaming.
|
||||
// X-File-Order must be URI-encoded because filenames can contain
|
||||
// spaces/special chars that are invalid in HTTP header values.
|
||||
reply.raw.writeHead(200, {
|
||||
"Content-Type": "application/zip",
|
||||
"Content-Disposition": `attachment; filename="batch-${toolId}-${jobId.slice(0, 8)}.zip"`,
|
||||
"Transfer-Encoding": "chunked",
|
||||
"X-Job-Id": jobId,
|
||||
"X-File-Order": files.map(f => f.filename).join(","),
|
||||
"X-File-Order": files.map(f => encodeURIComponent(f.filename)).join(","),
|
||||
});
|
||||
|
||||
// Create ZIP archive that pipes directly to the response
|
||||
const archive = archiver("zip", { zlib: { level: 5 } });
|
||||
|
||||
// Handle archive-level errors to prevent unhandled exceptions
|
||||
// that would crash the server process.
|
||||
archive.on("error", (err) => {
|
||||
request.log.error({ err }, "Archiver error during batch processing");
|
||||
if (!reply.raw.writableEnded) {
|
||||
reply.raw.end();
|
||||
}
|
||||
});
|
||||
|
||||
archive.pipe(reply.raw);
|
||||
|
||||
// Use p-queue for concurrency control
|
||||
@@ -154,50 +171,54 @@ export async function registerBatchRoutes(
|
||||
}
|
||||
|
||||
// Process all files through the queue
|
||||
const tasks = files.map((file) =>
|
||||
queue.add(async () => {
|
||||
progress.currentFile = file.filename;
|
||||
updateJobProgress({ ...progress });
|
||||
|
||||
// Validate the image
|
||||
const validation = await validateImageBuffer(file.buffer);
|
||||
if (!validation.valid) {
|
||||
progress.failedFiles++;
|
||||
progress.errors.push({
|
||||
filename: file.filename,
|
||||
error: `Invalid image: ${validation.reason}`,
|
||||
});
|
||||
progress.completedFiles++;
|
||||
try {
|
||||
const tasks = files.map((file) =>
|
||||
queue.add(async () => {
|
||||
progress.currentFile = file.filename;
|
||||
updateJobProgress({ ...progress });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await toolConfig.process(
|
||||
file.buffer,
|
||||
settings,
|
||||
file.filename,
|
||||
);
|
||||
// Validate the image
|
||||
const validation = await validateImageBuffer(file.buffer);
|
||||
if (!validation.valid) {
|
||||
progress.failedFiles++;
|
||||
progress.errors.push({
|
||||
filename: file.filename,
|
||||
error: `Invalid image: ${validation.reason}`,
|
||||
});
|
||||
progress.completedFiles++;
|
||||
updateJobProgress({ ...progress });
|
||||
return;
|
||||
}
|
||||
|
||||
const zipFilename = getUniqueName(result.filename);
|
||||
archive.append(result.buffer, { name: zipFilename });
|
||||
try {
|
||||
const result = await toolConfig.process(
|
||||
file.buffer,
|
||||
settings,
|
||||
file.filename,
|
||||
);
|
||||
|
||||
progress.completedFiles++;
|
||||
updateJobProgress({ ...progress });
|
||||
} catch (err) {
|
||||
progress.failedFiles++;
|
||||
progress.errors.push({
|
||||
filename: file.filename,
|
||||
error: err instanceof Error ? err.message : "Processing failed",
|
||||
});
|
||||
progress.completedFiles++;
|
||||
updateJobProgress({ ...progress });
|
||||
}
|
||||
}),
|
||||
);
|
||||
const zipFilename = getUniqueName(result.filename);
|
||||
archive.append(result.buffer, { name: zipFilename });
|
||||
|
||||
// Wait for all tasks to complete
|
||||
await Promise.all(tasks);
|
||||
progress.completedFiles++;
|
||||
updateJobProgress({ ...progress });
|
||||
} catch (err) {
|
||||
progress.failedFiles++;
|
||||
progress.errors.push({
|
||||
filename: file.filename,
|
||||
error: err instanceof Error ? err.message : "Processing failed",
|
||||
});
|
||||
progress.completedFiles++;
|
||||
updateJobProgress({ ...progress });
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// Wait for all tasks to complete
|
||||
await Promise.all(tasks);
|
||||
} catch (err) {
|
||||
request.log.error({ err }, "Unexpected error in batch queue");
|
||||
}
|
||||
|
||||
// Finalize progress
|
||||
progress.status =
|
||||
|
||||
@@ -86,6 +86,9 @@ export async function registerProgressRoutes(
|
||||
) => {
|
||||
const { jobId } = request.params;
|
||||
|
||||
// Take over the response from Fastify for SSE streaming
|
||||
reply.hijack();
|
||||
|
||||
// Send SSE headers via the raw Node response
|
||||
reply.raw.writeHead(200, {
|
||||
"Content-Type": "text/event-stream",
|
||||
|
||||
@@ -65,6 +65,7 @@ export function registerBulkRename(app: FastifyInstance) {
|
||||
try {
|
||||
const jobId = randomUUID();
|
||||
|
||||
reply.hijack();
|
||||
reply.raw.writeHead(200, {
|
||||
"Content-Type": "application/zip",
|
||||
"Content-Disposition": `attachment; filename="renamed-${jobId.slice(0, 8)}.zip"`,
|
||||
|
||||
@@ -43,6 +43,7 @@ export function registerFavicon(app: FastifyInstance) {
|
||||
try {
|
||||
const jobId = randomUUID();
|
||||
|
||||
reply.hijack();
|
||||
reply.raw.writeHead(200, {
|
||||
"Content-Type": "application/zip",
|
||||
"Content-Disposition": `attachment; filename="favicons-${jobId.slice(0, 8)}.zip"`,
|
||||
|
||||
@@ -70,6 +70,7 @@ export function registerSplit(app: FastifyInstance) {
|
||||
const jobId = randomUUID();
|
||||
|
||||
// Set up response headers for ZIP
|
||||
reply.hijack();
|
||||
reply.raw.writeHead(200, {
|
||||
"Content-Type": "application/zip",
|
||||
"Content-Disposition": `attachment; filename="split-${jobId.slice(0, 8)}.zip"`,
|
||||
|
||||
Reference in New Issue
Block a user