mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: add FEATURE_NOT_INSTALLED guards to API tool routes
Return 501 with structured error when an AI tool's feature bundle is not installed, preventing Python ImportError crashes. Guards added to tool-factory, batch, pipeline (both validation loops), and restore-photo custom route.
This commit is contained in:
@@ -8,12 +8,14 @@
|
|||||||
* Returns a ZIP file containing all processed images.
|
* Returns a ZIP file containing all processed images.
|
||||||
*/
|
*/
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
|
import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared";
|
||||||
import archiver from "archiver";
|
import archiver from "archiver";
|
||||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||||
import PQueue from "p-queue";
|
import PQueue from "p-queue";
|
||||||
import { env } from "../config.js";
|
import { env } from "../config.js";
|
||||||
import { autoOrient } from "../lib/auto-orient.js";
|
import { autoOrient } from "../lib/auto-orient.js";
|
||||||
import { formatZodErrors } from "../lib/errors.js";
|
import { formatZodErrors } from "../lib/errors.js";
|
||||||
|
import { isToolInstalled } from "../lib/feature-status.js";
|
||||||
import { validateImageBuffer } from "../lib/file-validation.js";
|
import { validateImageBuffer } from "../lib/file-validation.js";
|
||||||
import { sanitizeFilename } from "../lib/filename.js";
|
import { sanitizeFilename } from "../lib/filename.js";
|
||||||
import { decodeHeic } from "../lib/heic-converter.js";
|
import { decodeHeic } from "../lib/heic-converter.js";
|
||||||
@@ -37,6 +39,18 @@ export async function registerBatchRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
return reply.status(404).send({ error: `Tool "${toolId}" not found` });
|
return reply.status(404).send({ error: `Tool "${toolId}" not found` });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Guard: check if the tool's AI feature bundle is installed
|
||||||
|
if (!isToolInstalled(toolId)) {
|
||||||
|
const bundle = getBundleForTool(toolId);
|
||||||
|
return reply.status(501).send({
|
||||||
|
error: "Feature not installed",
|
||||||
|
code: "FEATURE_NOT_INSTALLED",
|
||||||
|
feature: TOOL_BUNDLE_MAP[toolId],
|
||||||
|
featureName: bundle?.name ?? toolId,
|
||||||
|
estimatedSize: bundle?.estimatedSize ?? "unknown",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Parse multipart: collect all files and the settings field
|
// Parse multipart: collect all files and the settings field
|
||||||
const files: ParsedFile[] = [];
|
const files: ParsedFile[] = [];
|
||||||
let settingsRaw: string | null = null;
|
let settingsRaw: string | null = null;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { writeFile } from "node:fs/promises";
|
import { writeFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared";
|
||||||
import archiver from "archiver";
|
import archiver from "archiver";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||||
@@ -18,6 +19,7 @@ import { env } from "../config.js";
|
|||||||
import { db, schema } from "../db/index.js";
|
import { db, schema } from "../db/index.js";
|
||||||
import { autoOrient } from "../lib/auto-orient.js";
|
import { autoOrient } from "../lib/auto-orient.js";
|
||||||
import { formatZodErrors } from "../lib/errors.js";
|
import { formatZodErrors } from "../lib/errors.js";
|
||||||
|
import { isToolInstalled } from "../lib/feature-status.js";
|
||||||
import { validateImageBuffer } from "../lib/file-validation.js";
|
import { validateImageBuffer } from "../lib/file-validation.js";
|
||||||
import { sanitizeFilename } from "../lib/filename.js";
|
import { sanitizeFilename } from "../lib/filename.js";
|
||||||
import { decodeHeic } from "../lib/heic-converter.js";
|
import { decodeHeic } from "../lib/heic-converter.js";
|
||||||
@@ -156,6 +158,17 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Guard: check if the tool's AI feature bundle is installed
|
||||||
|
if (!isToolInstalled(resolvedToolId)) {
|
||||||
|
const bundle = getBundleForTool(resolvedToolId);
|
||||||
|
return reply.status(501).send({
|
||||||
|
error: `Step ${i + 1} (${step.toolId}): Feature "${bundle?.name}" is not installed`,
|
||||||
|
code: "FEATURE_NOT_INSTALLED",
|
||||||
|
feature: TOOL_BUNDLE_MAP[resolvedToolId],
|
||||||
|
featureName: bundle?.name ?? resolvedToolId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Validate the settings for this tool
|
// Validate the settings for this tool
|
||||||
const settingsResult = toolConfig.settingsSchema.safeParse(step.settings);
|
const settingsResult = toolConfig.settingsSchema.safeParse(step.settings);
|
||||||
if (!settingsResult.success) {
|
if (!settingsResult.success) {
|
||||||
@@ -447,6 +460,17 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Guard: check if the tool's AI feature bundle is installed
|
||||||
|
if (!isToolInstalled(step.toolId)) {
|
||||||
|
const bundle = getBundleForTool(step.toolId);
|
||||||
|
return reply.status(501).send({
|
||||||
|
error: `Step ${i + 1} (${step.toolId}): Feature "${bundle?.name}" is not installed`,
|
||||||
|
code: "FEATURE_NOT_INSTALLED",
|
||||||
|
feature: TOOL_BUNDLE_MAP[step.toolId],
|
||||||
|
featureName: bundle?.name ?? step.toolId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const settingsResult = toolConfig.settingsSchema.safeParse(step.settings);
|
const settingsResult = toolConfig.settingsSchema.safeParse(step.settings);
|
||||||
if (!settingsResult.success) {
|
if (!settingsResult.success) {
|
||||||
return reply.status(400).send({
|
return reply.status(400).send({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { writeFile } from "node:fs/promises";
|
import { writeFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
import { getBundleForTool, TOOL_BUNDLE_MAP } from "@ashim/shared";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
@@ -8,6 +9,7 @@ import type { z } from "zod";
|
|||||||
import { db, schema } from "../db/index.js";
|
import { db, schema } from "../db/index.js";
|
||||||
import { autoOrient } from "../lib/auto-orient.js";
|
import { autoOrient } from "../lib/auto-orient.js";
|
||||||
import { formatZodErrors } from "../lib/errors.js";
|
import { formatZodErrors } from "../lib/errors.js";
|
||||||
|
import { isToolInstalled } from "../lib/feature-status.js";
|
||||||
import { validateImageBuffer } from "../lib/file-validation.js";
|
import { validateImageBuffer } from "../lib/file-validation.js";
|
||||||
import { sanitizeFilename } from "../lib/filename.js";
|
import { sanitizeFilename } from "../lib/filename.js";
|
||||||
import { decodeHeic } from "../lib/heic-converter.js";
|
import { decodeHeic } from "../lib/heic-converter.js";
|
||||||
@@ -194,6 +196,19 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
|||||||
return reply.status(400).send({ error: "Settings must be valid JSON" });
|
return reply.status(400).send({ error: "Settings must be valid JSON" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Guard: check if the tool's AI feature bundle is installed
|
||||||
|
const bundleId = TOOL_BUNDLE_MAP[config.toolId];
|
||||||
|
if (bundleId && !isToolInstalled(config.toolId)) {
|
||||||
|
const bundle = getBundleForTool(config.toolId);
|
||||||
|
return reply.status(501).send({
|
||||||
|
error: "Feature not installed",
|
||||||
|
code: "FEATURE_NOT_INSTALLED",
|
||||||
|
feature: bundleId,
|
||||||
|
featureName: bundle?.name ?? bundleId,
|
||||||
|
estimatedSize: bundle?.estimatedSize ?? "unknown",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Process the image (worker thread or main thread)
|
// Process the image (worker thread or main thread)
|
||||||
try {
|
try {
|
||||||
let result: { buffer: Buffer; filename: string; contentType: string };
|
let result: { buffer: Buffer; filename: string; contentType: string };
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import { randomUUID } from "node:crypto";
|
|||||||
import { writeFile } from "node:fs/promises";
|
import { writeFile } from "node:fs/promises";
|
||||||
import { basename, join } from "node:path";
|
import { basename, join } from "node:path";
|
||||||
import { restorePhoto } from "@ashim/ai";
|
import { restorePhoto } from "@ashim/ai";
|
||||||
|
import { getBundleForTool } from "@ashim/shared";
|
||||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { autoOrient } from "../../lib/auto-orient.js";
|
import { autoOrient } from "../../lib/auto-orient.js";
|
||||||
|
import { isToolInstalled } from "../../lib/feature-status.js";
|
||||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||||
import { resolveOutputFormat } from "../../lib/output-format.js";
|
import { resolveOutputFormat } from "../../lib/output-format.js";
|
||||||
@@ -67,6 +69,18 @@ export function registerRestorePhoto(app: FastifyInstance) {
|
|||||||
return reply.status(400).send({ error: `Invalid image: ${validation.reason}` });
|
return reply.status(400).send({ error: `Invalid image: ${validation.reason}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Guard: check if the photo restoration feature bundle is installed
|
||||||
|
if (!isToolInstalled("restore-photo")) {
|
||||||
|
const bundle = getBundleForTool("restore-photo");
|
||||||
|
return reply.status(501).send({
|
||||||
|
error: "Feature not installed",
|
||||||
|
code: "FEATURE_NOT_INSTALLED",
|
||||||
|
feature: "photo-restoration",
|
||||||
|
featureName: bundle?.name ?? "Photo Restoration",
|
||||||
|
estimatedSize: bundle?.estimatedSize ?? "unknown",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const settings = settingsSchema.parse(settingsRaw ? JSON.parse(settingsRaw) : {});
|
const settings = settingsSchema.parse(settingsRaw ? JSON.parse(settingsRaw) : {});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user