feat(files): add save-as-new vs overwrite choice for library file edits (#564)

Editing a file from the library used to silently supersede it: the worker auto-saved every result as a new version and the leaf-only listing hid the original, which read as a destructive overwrite. Tool pages now show a per-edit choice for library-sourced files. The default saves the result as an independent new file and keeps the original; picking overwrite keeps the old superseding-version behavior.

The client sends a saveMode multipart field next to fileId, validated with a 400 on unknown values, and autoSaveToLibrary branches on it. Every hand-written route that honors fileId parses the field the same way as the factory. The review panel shows where an auto-saved result went instead of offering a second, duplicate save. Tools whose route or submitter ignores fileId keep the selector hidden via a shared unsupported-tools set, and the choice resets to the non-destructive default whenever a new file is staged.

Closes #495
This commit is contained in:
SnapOtter
2026-07-18 11:36:08 +08:00
committed by GitHub
parent e113684ddb
commit a23158d968
63 changed files with 1721 additions and 19 deletions
+12 -5
View File
@@ -11,6 +11,7 @@ import { mkdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { extname, join } from "node:path";
import { probeMedia } from "@snapotter/media-engine";
import type { LibrarySaveMode } from "@snapotter/shared";
import { eq } from "drizzle-orm";
import sharp from "sharp";
import { db, schema } from "../db/index.js";
@@ -173,6 +174,8 @@ export async function generatePreview(
export interface AutoSaveOpts {
fileId?: string;
/** "overwrite" supersedes the original with a linked version; "new" (default) keeps it. */
saveMode?: LibrarySaveMode;
userId: string | null;
buffer: Buffer;
outName: string;
@@ -182,8 +185,12 @@ export interface AutoSaveOpts {
/**
* Auto-save a processed output to the persistent user file library when a
* fileId is provided. Creates a new version linked to the parent file with
* the tool appended to the toolChain.
* fileId is provided. The tool is appended to the parent's toolChain either
* way; saveMode decides the linkage (issue #495):
* - "new" (default): independent root row (version 1, no parentId), so the
* original stays visible in the leaf-only library list.
* - "overwrite": new version linked to the parent (version + 1, parentId),
* which supersedes the original in the list.
*
* Returns the new file ID on success, undefined when no fileId or on error.
*/
@@ -201,7 +208,7 @@ export async function autoSaveToLibrary(opts: AutoSaveOpts): Promise<string | un
// another user's file via a known fileId.
if (parent.userId !== opts.userId) return undefined;
const newVersion = parent.version + 1;
const overwrite = opts.saveMode === "overwrite";
const parentChain: string[] = parent.toolChain ?? [];
const newToolChain = [...parentChain, opts.toolId];
const storedName = await saveFile(opts.buffer, opts.outName);
@@ -245,8 +252,8 @@ export async function autoSaveToLibrary(opts: AutoSaveOpts): Promise<string | un
size: opts.buffer.length,
width,
height,
version: newVersion,
parentId: opts.fileId,
version: overwrite ? parent.version + 1 : 1,
parentId: overwrite ? opts.fileId : null,
toolChain: newToolChain,
});
return newId;
+19 -1
View File
@@ -1,7 +1,11 @@
/**
* Shared types and naming helpers for the BullMQ job system.
* Shared types and naming helpers for the BullMQ job system, plus the
* saveMode multipart-field validation shared by tool-factory and the
* hand-written tool routes.
*/
import { LIBRARY_SAVE_MODES, type LibrarySaveMode } from "@snapotter/shared";
/** The five processing pools that partition work by resource profile. */
export const POOLS = ["image", "media", "ai", "docs", "system"] as const;
export type Pool = (typeof POOLS)[number];
@@ -32,6 +36,8 @@ export interface ToolJobData {
*/
dbSettings?: Record<string, unknown>;
fileId?: string;
/** How to save the result to the library when fileId is set; defaults to "new". */
saveMode?: LibrarySaveMode;
clientJobId?: string;
kind:
| "tool"
@@ -50,6 +56,18 @@ export interface ToolJobData {
_otel?: { traceparent: string; tracestate?: string };
}
/** Error message for the { error } 400 response when a multipart saveMode field has an unknown value. */
export const INVALID_SAVE_MODE_ERROR = 'Invalid saveMode (expected "new" or "overwrite")';
/**
* Validate a client-supplied multipart saveMode field.
* Returns undefined when the field was absent, null when the value is invalid.
*/
export function parseSaveModeField(raw: string | null): LibrarySaveMode | undefined | null {
if (raw === null) return undefined;
return (LIBRARY_SAVE_MODES as readonly string[]).includes(raw) ? (raw as LibrarySaveMode) : null;
}
/** Result returned by a completed BullMQ job. */
export interface ToolJobResult {
outputRefs: string[];
+6 -4
View File
@@ -417,12 +417,14 @@ async function processToolJob(job: Job<ToolJobData>): Promise<ToolJobResult> {
// Generate preview for non-browser-previewable formats
const previewRef = await generatePreview(resultBuffer, resultContentType, jobId, inputBuffer);
// Auto-save a new version when the input came from the user's library
// (data.fileId is set by tool-factory when the upload referenced a
// library file). Without a fileId this is a no-op, so tool-first uploads
// are not auto-saved.
// Auto-save when the input came from the user's library (data.fileId is
// set by the route when the upload referenced a library file). saveMode
// picks between an independent new file (default) and a superseding
// version. Without a fileId this is a no-op, so tool-first uploads are
// not auto-saved.
const savedFileId = await autoSaveToLibrary({
fileId: data.fileId,
saveMode: data.saveMode,
userId: data.userId,
buffer: resultBuffer,
outName,
+11
View File
@@ -15,6 +15,7 @@ import type { z } from "zod";
import { env } from "../config.js";
import { db, schema } from "../db/index.js";
import { enqueueToolJob, waitForJob } from "../jobs/enqueue.js";
import { INVALID_SAVE_MODE_ERROR, parseSaveModeField } from "../jobs/types.js";
import { formatZodErrors, friendlyError, stripInternalPaths } from "../lib/errors.js";
import { getFirstMissingBundleForTool, isToolInstalled } from "../lib/feature-status.js";
import { getObjectBuffer, putObject } from "../lib/object-storage.js";
@@ -246,6 +247,7 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
let filename = "file";
let settingsRaw: string | null = null;
let fileId: string | null = null;
let saveModeRaw: string | null = null;
let clientJobId: string | null = null;
let fileCount = 0;
const received: ReceivedUpload[] = [];
@@ -283,6 +285,9 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
if (part.fieldname === "fileId") {
fileId = part.value as string;
}
if (part.fieldname === "saveMode") {
saveModeRaw = part.value as string;
}
if (part.fieldname === "clientJobId") {
const raw = part.value as string;
if (typeof raw === "string" && raw.length > 0 && raw.length <= 128) {
@@ -304,6 +309,11 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
});
}
const saveMode = parseSaveModeField(saveModeRaw);
if (saveMode === null) {
return reply.status(400).send({ error: INVALID_SAVE_MODE_ERROR });
}
// Require at least one file
if (received.length === 0) {
return reply.status(400).send({ error: "No file provided" });
@@ -522,6 +532,7 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
settings,
dbSettings,
fileId: fileId ?? undefined,
saveMode,
clientJobId: clientJobId ?? undefined,
kind: "tool",
analyticsDistinctId: request.headers["x-posthog-distinct-id"] as string | undefined,
@@ -9,6 +9,7 @@ import sharp from "sharp";
import { z } from "zod";
import { registerAiJobHandler } from "../../jobs/ai-handlers.js";
import { enqueueToolJob } from "../../jobs/enqueue.js";
import { INVALID_SAVE_MODE_ERROR, parseSaveModeField } from "../../jobs/types.js";
import { autoOrient } from "../../lib/auto-orient.js";
import { formatZodErrors, stripInternalPaths } from "../../lib/errors.js";
import { isToolInstalled } from "../../lib/feature-status.js";
@@ -153,6 +154,7 @@ export function registerAiCanvasExpand(app: FastifyInstance) {
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let saveModeRaw: string | null = null;
let inputKey: string | null = null;
try {
@@ -171,6 +173,8 @@ export function registerAiCanvasExpand(app: FastifyInstance) {
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
} else if (part.fieldname === "saveMode") {
saveModeRaw = part.value as string;
}
}
} catch (err) {
@@ -180,6 +184,11 @@ export function registerAiCanvasExpand(app: FastifyInstance) {
});
}
const saveMode = parseSaveModeField(saveModeRaw);
if (saveMode === null) {
return reply.status(400).send({ error: INVALID_SAVE_MODE_ERROR });
}
if (!inputKey) {
return reply.status(400).send({ error: "No image file provided" });
}
@@ -252,6 +261,7 @@ export function registerAiCanvasExpand(app: FastifyInstance) {
settings,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
saveMode,
kind: "ai-tool",
});
@@ -8,6 +8,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod";
import { registerAiJobHandler } from "../../jobs/ai-handlers.js";
import { enqueueToolJob } from "../../jobs/enqueue.js";
import { INVALID_SAVE_MODE_ERROR, parseSaveModeField } from "../../jobs/types.js";
import { formatZodErrors, stripInternalPaths } from "../../lib/errors.js";
import { isToolInstalled } from "../../lib/feature-status.js";
import { type TranscriptSegment, toSrt, toVtt } from "../../lib/subtitle-format.js";
@@ -112,6 +113,7 @@ export function registerAutoSubtitles(app: FastifyInstance) {
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let saveModeRaw: string | null = null;
let inputKey: string | null = null;
try {
@@ -130,6 +132,8 @@ export function registerAutoSubtitles(app: FastifyInstance) {
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
} else if (part.fieldname === "saveMode") {
saveModeRaw = part.value as string;
}
}
} catch (err) {
@@ -139,6 +143,11 @@ export function registerAutoSubtitles(app: FastifyInstance) {
});
}
const saveMode = parseSaveModeField(saveModeRaw);
if (saveMode === null) {
return reply.status(400).send({ error: INVALID_SAVE_MODE_ERROR });
}
if (!inputKey) {
return reply.status(400).send({ error: "No video file provided" });
}
@@ -168,6 +177,7 @@ export function registerAutoSubtitles(app: FastifyInstance) {
settings,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
saveMode,
kind: "ai-tool",
});
@@ -6,6 +6,7 @@ import sharp from "sharp";
import { z } from "zod";
import { registerAiJobHandler } from "../../jobs/ai-handlers.js";
import { enqueueToolJob } from "../../jobs/enqueue.js";
import { INVALID_SAVE_MODE_ERROR, parseSaveModeField } from "../../jobs/types.js";
import { autoOrient } from "../../lib/auto-orient.js";
import { compositeOnColor, createGradientBackground } from "../../lib/bg-effects.js";
import { formatZodErrors, stripInternalPaths } from "../../lib/errors.js";
@@ -132,6 +133,7 @@ export function registerBackgroundReplace(app: FastifyInstance) {
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let saveModeRaw: string | null = null;
let inputKey: string | null = null;
try {
@@ -150,6 +152,8 @@ export function registerBackgroundReplace(app: FastifyInstance) {
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
} else if (part.fieldname === "saveMode") {
saveModeRaw = part.value as string;
}
}
} catch (err) {
@@ -159,6 +163,11 @@ export function registerBackgroundReplace(app: FastifyInstance) {
});
}
const saveMode = parseSaveModeField(saveModeRaw);
if (saveMode === null) {
return reply.status(400).send({ error: INVALID_SAVE_MODE_ERROR });
}
if (!inputKey) {
return reply.status(400).send({ error: "No image file provided" });
}
@@ -227,6 +236,7 @@ export function registerBackgroundReplace(app: FastifyInstance) {
settings,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
saveMode,
kind: "ai-tool",
});
@@ -6,6 +6,7 @@ import sharp from "sharp";
import { z } from "zod";
import { registerAiJobHandler } from "../../jobs/ai-handlers.js";
import { enqueueToolJob } from "../../jobs/enqueue.js";
import { INVALID_SAVE_MODE_ERROR, parseSaveModeField } from "../../jobs/types.js";
import { autoOrient } from "../../lib/auto-orient.js";
import { blurBackground } from "../../lib/bg-effects.js";
import { formatZodErrors, stripInternalPaths } from "../../lib/errors.js";
@@ -102,6 +103,7 @@ export function registerBlurBackground(app: FastifyInstance) {
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let saveModeRaw: string | null = null;
let inputKey: string | null = null;
try {
@@ -120,6 +122,8 @@ export function registerBlurBackground(app: FastifyInstance) {
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
} else if (part.fieldname === "saveMode") {
saveModeRaw = part.value as string;
}
}
} catch (err) {
@@ -129,6 +133,11 @@ export function registerBlurBackground(app: FastifyInstance) {
});
}
const saveMode = parseSaveModeField(saveModeRaw);
if (saveMode === null) {
return reply.status(400).send({ error: INVALID_SAVE_MODE_ERROR });
}
if (!inputKey) {
return reply.status(400).send({ error: "No image file provided" });
}
@@ -197,6 +206,7 @@ export function registerBlurBackground(app: FastifyInstance) {
settings,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
saveMode,
kind: "ai-tool",
});
+10
View File
@@ -9,6 +9,7 @@ import sharp from "sharp";
import { z } from "zod";
import { registerAiJobHandler } from "../../jobs/ai-handlers.js";
import { enqueueToolJob } from "../../jobs/enqueue.js";
import { INVALID_SAVE_MODE_ERROR, parseSaveModeField } from "../../jobs/types.js";
import { autoOrient } from "../../lib/auto-orient.js";
import { formatZodErrors, stripInternalPaths } from "../../lib/errors.js";
import { isToolInstalled } from "../../lib/feature-status.js";
@@ -88,6 +89,7 @@ export function registerBlurFaces(app: FastifyInstance) {
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let saveModeRaw: string | null = null;
let inputKey: string | null = null;
try {
@@ -106,6 +108,8 @@ export function registerBlurFaces(app: FastifyInstance) {
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
} else if (part.fieldname === "saveMode") {
saveModeRaw = part.value as string;
}
}
} catch (err) {
@@ -115,6 +119,11 @@ export function registerBlurFaces(app: FastifyInstance) {
});
}
const saveMode = parseSaveModeField(saveModeRaw);
if (saveMode === null) {
return reply.status(400).send({ error: INVALID_SAVE_MODE_ERROR });
}
if (!inputKey) {
return reply.status(400).send({ error: "No image file provided" });
}
@@ -174,6 +183,7 @@ export function registerBlurFaces(app: FastifyInstance) {
settings,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
saveMode,
kind: "ai-tool",
});
+10
View File
@@ -9,6 +9,7 @@ import sharp from "sharp";
import { z } from "zod";
import { registerAiJobHandler } from "../../jobs/ai-handlers.js";
import { enqueueToolJob } from "../../jobs/enqueue.js";
import { INVALID_SAVE_MODE_ERROR, parseSaveModeField } from "../../jobs/types.js";
import { autoOrient } from "../../lib/auto-orient.js";
import { formatZodErrors, stripInternalPaths } from "../../lib/errors.js";
import { isToolInstalled } from "../../lib/feature-status.js";
@@ -87,6 +88,7 @@ export function registerColorize(app: FastifyInstance) {
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let saveModeRaw: string | null = null;
let inputKey: string | null = null;
try {
@@ -105,6 +107,8 @@ export function registerColorize(app: FastifyInstance) {
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
} else if (part.fieldname === "saveMode") {
saveModeRaw = part.value as string;
}
}
} catch (err) {
@@ -114,6 +118,11 @@ export function registerColorize(app: FastifyInstance) {
});
}
const saveMode = parseSaveModeField(saveModeRaw);
if (saveMode === null) {
return reply.status(400).send({ error: INVALID_SAVE_MODE_ERROR });
}
if (!inputKey) {
return reply.status(400).send({ error: "No image file provided" });
}
@@ -173,6 +182,7 @@ export function registerColorize(app: FastifyInstance) {
settings,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
saveMode,
kind: "ai-tool",
});
@@ -8,6 +8,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod";
import { registerAiJobHandler } from "../../jobs/ai-handlers.js";
import { enqueueToolJob } from "../../jobs/enqueue.js";
import { INVALID_SAVE_MODE_ERROR, parseSaveModeField } from "../../jobs/types.js";
import { autoOrient } from "../../lib/auto-orient.js";
import { formatZodErrors, stripInternalPaths } from "../../lib/errors.js";
import { getFirstMissingBundleForTool, isToolInstalled } from "../../lib/feature-status.js";
@@ -82,6 +83,7 @@ export function registerEnhanceFaces(app: FastifyInstance) {
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let saveModeRaw: string | null = null;
let inputKey: string | null = null;
try {
@@ -100,6 +102,8 @@ export function registerEnhanceFaces(app: FastifyInstance) {
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
} else if (part.fieldname === "saveMode") {
saveModeRaw = part.value as string;
}
}
} catch (err) {
@@ -109,6 +113,11 @@ export function registerEnhanceFaces(app: FastifyInstance) {
});
}
const saveMode = parseSaveModeField(saveModeRaw);
if (saveMode === null) {
return reply.status(400).send({ error: INVALID_SAVE_MODE_ERROR });
}
if (!inputKey) {
return reply.status(400).send({ error: "No image file provided" });
}
@@ -168,6 +177,7 @@ export function registerEnhanceFaces(app: FastifyInstance) {
settings,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
saveMode,
kind: "ai-tool",
});
+10
View File
@@ -5,6 +5,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import sharp from "sharp";
import { z } from "zod";
import { enqueueToolJob } from "../../jobs/enqueue.js";
import { INVALID_SAVE_MODE_ERROR, parseSaveModeField } from "../../jobs/types.js";
import { autoOrient } from "../../lib/auto-orient.js";
import { stripInternalPaths } from "../../lib/errors.js";
import { isToolInstalled } from "../../lib/feature-status.js";
@@ -56,6 +57,7 @@ export function registerEraseObject(app: FastifyInstance) {
let filename = "image";
let clientJobId: string | null = null;
let fileId: string | null = null;
let saveModeRaw: string | null = null;
let format = "png";
let quality = 95;
let imageKey: string | null = null;
@@ -80,6 +82,8 @@ export function registerEraseObject(app: FastifyInstance) {
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
} else if (part.fieldname === "saveMode") {
saveModeRaw = part.value as string;
} else if (part.fieldname === "format") {
format = (part.value as string) || "png";
} else if (part.fieldname === "quality") {
@@ -93,6 +97,11 @@ export function registerEraseObject(app: FastifyInstance) {
});
}
const saveMode = parseSaveModeField(saveModeRaw);
if (saveMode === null) {
return reply.status(400).send({ error: INVALID_SAVE_MODE_ERROR });
}
if (!imageKey) {
return reply.status(400).send({ error: "No image file provided" });
}
@@ -170,6 +179,7 @@ export function registerEraseObject(app: FastifyInstance) {
settings: { format, quality },
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
saveMode,
kind: "ai-tool",
});
@@ -8,6 +8,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod";
import { registerAiJobHandler } from "../../jobs/ai-handlers.js";
import { enqueueToolJob } from "../../jobs/enqueue.js";
import { INVALID_SAVE_MODE_ERROR, parseSaveModeField } from "../../jobs/types.js";
import { autoOrient } from "../../lib/auto-orient.js";
import { formatZodErrors, stripInternalPaths } from "../../lib/errors.js";
import { isToolInstalled } from "../../lib/feature-status.js";
@@ -92,6 +93,7 @@ export function registerNoiseRemoval(app: FastifyInstance) {
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let saveModeRaw: string | null = null;
let inputKey: string | null = null;
try {
@@ -110,6 +112,8 @@ export function registerNoiseRemoval(app: FastifyInstance) {
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
} else if (part.fieldname === "saveMode") {
saveModeRaw = part.value as string;
}
}
} catch (err) {
@@ -119,6 +123,11 @@ export function registerNoiseRemoval(app: FastifyInstance) {
});
}
const saveMode = parseSaveModeField(saveModeRaw);
if (saveMode === null) {
return reply.status(400).send({ error: INVALID_SAVE_MODE_ERROR });
}
if (!inputKey) {
return reply.status(400).send({ error: "No image file provided" });
}
@@ -178,6 +187,7 @@ export function registerNoiseRemoval(app: FastifyInstance) {
settings: parsed,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
saveMode,
kind: "ai-tool",
});
+11
View File
@@ -7,6 +7,7 @@ import { z } from "zod";
import { env } from "../../config.js";
import { registerAiPathJobHandler } from "../../jobs/ai-handlers.js";
import { enqueueToolJob } from "../../jobs/enqueue.js";
import { INVALID_SAVE_MODE_ERROR, parseSaveModeField } from "../../jobs/types.js";
import { formatZodErrors, stripInternalPaths } from "../../lib/errors.js";
import { copyObjectToFile, deleteObject } from "../../lib/object-storage.js";
import { resolveOcrIngressSettings } from "../../lib/ocr-capability.js";
@@ -123,6 +124,7 @@ export function registerOcrPdf(app: FastifyInstance) {
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let saveModeRaw: string | null = null;
let inputKey: string | null = null;
const ingressAbort = new AbortController();
const abortIngress = () => ingressAbort.abort();
@@ -150,6 +152,8 @@ export function registerOcrPdf(app: FastifyInstance) {
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
} else if (part.fieldname === "saveMode") {
saveModeRaw = part.value as string;
}
}
} catch (err) {
@@ -161,6 +165,12 @@ export function registerOcrPdf(app: FastifyInstance) {
});
}
const saveMode = parseSaveModeField(saveModeRaw);
if (saveMode === null) {
if (inputKey) await deleteObject(inputKey).catch(() => {});
return reply.status(400).send({ error: INVALID_SAVE_MODE_ERROR });
}
if (!inputKey) {
return reply.status(400).send({ error: "No PDF file provided" });
}
@@ -250,6 +260,7 @@ export function registerOcrPdf(app: FastifyInstance) {
settings: { ...normalizedSettings, quality },
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
saveMode,
kind: "ai-tool",
});
} catch (err) {
+11
View File
@@ -11,6 +11,7 @@ import { z } from "zod";
import { env } from "../../config.js";
import { registerAiJobHandler } from "../../jobs/ai-handlers.js";
import { enqueueToolJob } from "../../jobs/enqueue.js";
import { INVALID_SAVE_MODE_ERROR, parseSaveModeField } from "../../jobs/types.js";
import { formatZodErrors, stripInternalPaths } from "../../lib/errors.js";
import { deleteObject } from "../../lib/object-storage.js";
import { resolveOcrIngressSettings } from "../../lib/ocr-capability.js";
@@ -134,6 +135,7 @@ export function registerOcr(app: FastifyInstance) {
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let saveModeRaw: string | null = null;
let inputKey: string | null = null;
try {
@@ -155,6 +157,8 @@ export function registerOcr(app: FastifyInstance) {
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
} else if (part.fieldname === "saveMode") {
saveModeRaw = part.value as string;
}
}
} catch (err) {
@@ -166,6 +170,12 @@ export function registerOcr(app: FastifyInstance) {
});
}
const saveMode = parseSaveModeField(saveModeRaw);
if (saveMode === null) {
if (inputKey) await deleteObject(inputKey).catch(() => {});
return reply.status(400).send({ error: INVALID_SAVE_MODE_ERROR });
}
if (!inputKey) {
return reply.status(400).send({ error: "No image file provided" });
}
@@ -223,6 +233,7 @@ export function registerOcr(app: FastifyInstance) {
settings: { ...normalizedSettings, quality },
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
saveMode,
kind: "ai-tool",
analyticsDistinctId: request.headers["x-posthog-distinct-id"] as string | undefined,
});
@@ -8,6 +8,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod";
import { registerAiJobHandler } from "../../jobs/ai-handlers.js";
import { enqueueToolJob } from "../../jobs/enqueue.js";
import { INVALID_SAVE_MODE_ERROR, parseSaveModeField } from "../../jobs/types.js";
import { autoOrient } from "../../lib/auto-orient.js";
import { formatZodErrors, stripInternalPaths } from "../../lib/errors.js";
import { isToolInstalled } from "../../lib/feature-status.js";
@@ -80,6 +81,7 @@ export function registerRedEyeRemoval(app: FastifyInstance) {
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let saveModeRaw: string | null = null;
let inputKey: string | null = null;
try {
@@ -98,6 +100,8 @@ export function registerRedEyeRemoval(app: FastifyInstance) {
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
} else if (part.fieldname === "saveMode") {
saveModeRaw = part.value as string;
}
}
} catch (err) {
@@ -107,6 +111,11 @@ export function registerRedEyeRemoval(app: FastifyInstance) {
});
}
const saveMode = parseSaveModeField(saveModeRaw);
if (saveMode === null) {
return reply.status(400).send({ error: INVALID_SAVE_MODE_ERROR });
}
if (!inputKey) {
return reply.status(400).send({ error: "No image file provided" });
}
@@ -166,6 +175,7 @@ export function registerRedEyeRemoval(app: FastifyInstance) {
settings,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
saveMode,
kind: "ai-tool",
});
@@ -8,6 +8,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod";
import { registerAiJobHandler } from "../../jobs/ai-handlers.js";
import { enqueueToolJob } from "../../jobs/enqueue.js";
import { INVALID_SAVE_MODE_ERROR, parseSaveModeField } from "../../jobs/types.js";
import { autoOrient } from "../../lib/auto-orient.js";
import {
applyEffects,
@@ -114,6 +115,7 @@ export function registerRemoveBackground(app: FastifyInstance) {
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let saveModeRaw: string | null = null;
let inputKey: string | null = null;
try {
@@ -132,6 +134,8 @@ export function registerRemoveBackground(app: FastifyInstance) {
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
} else if (part.fieldname === "saveMode") {
saveModeRaw = part.value as string;
}
}
} catch (err) {
@@ -141,6 +145,11 @@ export function registerRemoveBackground(app: FastifyInstance) {
});
}
const saveMode = parseSaveModeField(saveModeRaw);
if (saveMode === null) {
return reply.status(400).send({ error: INVALID_SAVE_MODE_ERROR });
}
if (!inputKey) {
return reply.status(400).send({ error: "No image file provided" });
}
@@ -218,6 +227,7 @@ export function registerRemoveBackground(app: FastifyInstance) {
settings,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
saveMode,
kind: "ai-tool",
});
@@ -10,6 +10,7 @@ import { z } from "zod";
import { env } from "../../config.js";
import { registerAiJobHandler } from "../../jobs/ai-handlers.js";
import { enqueueToolJob } from "../../jobs/enqueue.js";
import { INVALID_SAVE_MODE_ERROR, parseSaveModeField } from "../../jobs/types.js";
import { detectAnimation } from "../../lib/animation-detect.js";
import { formatZodErrors, stripInternalPaths } from "../../lib/errors.js";
import { isToolInstalled } from "../../lib/feature-status.js";
@@ -132,6 +133,7 @@ export function registerRemoveGifBackground(app: FastifyInstance) {
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let saveModeRaw: string | null = null;
let inputKey: string | null = null;
let bgBuffer: Buffer | null = null;
let bgName = "background";
@@ -157,6 +159,8 @@ export function registerRemoveGifBackground(app: FastifyInstance) {
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
} else if (part.fieldname === "saveMode") {
saveModeRaw = part.value as string;
}
}
} catch (err) {
@@ -166,6 +170,11 @@ export function registerRemoveGifBackground(app: FastifyInstance) {
});
}
const saveMode = parseSaveModeField(saveModeRaw);
if (saveMode === null) {
return reply.status(400).send({ error: INVALID_SAVE_MODE_ERROR });
}
if (!inputKey) {
return reply.status(400).send({ error: "No image file provided" });
}
@@ -247,6 +256,7 @@ export function registerRemoveGifBackground(app: FastifyInstance) {
dbSettings: settings, // keep the internal bgImageKey out of the audit row
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
saveMode,
kind: "ai-tool",
});
@@ -9,6 +9,7 @@ import sharp from "sharp";
import { z } from "zod";
import { registerAiJobHandler } from "../../jobs/ai-handlers.js";
import { enqueueToolJob } from "../../jobs/enqueue.js";
import { INVALID_SAVE_MODE_ERROR, parseSaveModeField } from "../../jobs/types.js";
import { autoOrient } from "../../lib/auto-orient.js";
import { formatZodErrors, stripInternalPaths } from "../../lib/errors.js";
import { isToolInstalled } from "../../lib/feature-status.js";
@@ -105,6 +106,7 @@ export function registerRestorePhoto(app: FastifyInstance) {
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let saveModeRaw: string | null = null;
let inputKey: string | null = null;
try {
@@ -123,6 +125,8 @@ export function registerRestorePhoto(app: FastifyInstance) {
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
} else if (part.fieldname === "saveMode") {
saveModeRaw = part.value as string;
}
}
} catch (err) {
@@ -132,6 +136,11 @@ export function registerRestorePhoto(app: FastifyInstance) {
});
}
const saveMode = parseSaveModeField(saveModeRaw);
if (saveMode === null) {
return reply.status(400).send({ error: INVALID_SAVE_MODE_ERROR });
}
if (!inputKey) {
return reply.status(400).send({ error: "No image file provided" });
}
@@ -198,6 +207,7 @@ export function registerRestorePhoto(app: FastifyInstance) {
settings,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
saveMode,
kind: "ai-tool",
});
+10
View File
@@ -8,6 +8,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod";
import { registerAiJobHandler } from "../../jobs/ai-handlers.js";
import { enqueueToolJob, waitForJob } from "../../jobs/enqueue.js";
import { INVALID_SAVE_MODE_ERROR, parseSaveModeField } from "../../jobs/types.js";
import { stripInternalPaths } from "../../lib/errors.js";
import { validateImageBuffer } from "../../lib/file-validation.js";
import { getObjectBuffer } from "../../lib/object-storage.js";
@@ -55,6 +56,7 @@ export function registerSignPdf(app: FastifyInstance) {
let filename = "document.pdf";
let clientJobId: string | null = null;
let fileId: string | null = null;
let saveModeRaw: string | null = null;
let placementsRaw: string | null = null;
const sigParts: Array<{ index: number; key: string }> = [];
@@ -81,6 +83,8 @@ export function registerSignPdf(app: FastifyInstance) {
if (/^[0-9a-f-]{36}$/i.test(raw)) clientJobId = raw;
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
} else if (part.fieldname === "saveMode") {
saveModeRaw = part.value as string;
}
}
} catch (err) {
@@ -90,6 +94,11 @@ export function registerSignPdf(app: FastifyInstance) {
});
}
const saveMode = parseSaveModeField(saveModeRaw);
if (saveMode === null) {
return reply.status(400).send({ error: INVALID_SAVE_MODE_ERROR });
}
if (!pdfKey) return reply.status(400).send({ error: "No PDF file provided" });
if (!placementsRaw) return reply.status(400).send({ error: "No placements provided" });
@@ -142,6 +151,7 @@ export function registerSignPdf(app: FastifyInstance) {
settings: { placements: parsed.placements },
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
saveMode,
kind: "ai-tool",
});
@@ -7,6 +7,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod";
import { registerAiJobHandler } from "../../jobs/ai-handlers.js";
import { enqueueToolJob } from "../../jobs/enqueue.js";
import { INVALID_SAVE_MODE_ERROR, parseSaveModeField } from "../../jobs/types.js";
import { formatZodErrors, stripInternalPaths } from "../../lib/errors.js";
import { isToolInstalled } from "../../lib/feature-status.js";
import { type TranscriptSegment, toSrt, toVtt } from "../../lib/subtitle-format.js";
@@ -100,6 +101,7 @@ export function registerTranscribeAudio(app: FastifyInstance) {
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let saveModeRaw: string | null = null;
let inputKey: string | null = null;
try {
@@ -118,6 +120,8 @@ export function registerTranscribeAudio(app: FastifyInstance) {
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
} else if (part.fieldname === "saveMode") {
saveModeRaw = part.value as string;
}
}
} catch (err) {
@@ -127,6 +131,11 @@ export function registerTranscribeAudio(app: FastifyInstance) {
});
}
const saveMode = parseSaveModeField(saveModeRaw);
if (saveMode === null) {
return reply.status(400).send({ error: INVALID_SAVE_MODE_ERROR });
}
if (!inputKey) {
return reply.status(400).send({ error: "No audio file provided" });
}
@@ -156,6 +165,7 @@ export function registerTranscribeAudio(app: FastifyInstance) {
settings,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
saveMode,
kind: "ai-tool",
});
@@ -9,6 +9,7 @@ import sharp from "sharp";
import { z } from "zod";
import { registerAiJobHandler } from "../../jobs/ai-handlers.js";
import { enqueueToolJob } from "../../jobs/enqueue.js";
import { INVALID_SAVE_MODE_ERROR, parseSaveModeField } from "../../jobs/types.js";
import { autoOrient } from "../../lib/auto-orient.js";
import { formatZodErrors, stripInternalPaths } from "../../lib/errors.js";
import { isToolInstalled } from "../../lib/feature-status.js";
@@ -173,6 +174,7 @@ export function registerTransparencyFixer(app: FastifyInstance) {
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let saveModeRaw: string | null = null;
let inputKey: string | null = null;
try {
@@ -191,6 +193,8 @@ export function registerTransparencyFixer(app: FastifyInstance) {
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
} else if (part.fieldname === "saveMode") {
saveModeRaw = part.value as string;
}
}
} catch (err) {
@@ -200,6 +204,11 @@ export function registerTransparencyFixer(app: FastifyInstance) {
});
}
const saveMode = parseSaveModeField(saveModeRaw);
if (saveMode === null) {
return reply.status(400).send({ error: INVALID_SAVE_MODE_ERROR });
}
if (!inputKey) {
return reply.status(400).send({ error: "No image file provided" });
}
@@ -263,6 +272,7 @@ export function registerTransparencyFixer(app: FastifyInstance) {
settings,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
saveMode,
kind: "ai-tool",
});
+10
View File
@@ -9,6 +9,7 @@ import sharp from "sharp";
import { z } from "zod";
import { registerAiJobHandler } from "../../jobs/ai-handlers.js";
import { enqueueToolJob } from "../../jobs/enqueue.js";
import { INVALID_SAVE_MODE_ERROR, parseSaveModeField } from "../../jobs/types.js";
import { autoOrient } from "../../lib/auto-orient.js";
import { formatZodErrors, stripInternalPaths } from "../../lib/errors.js";
import { isToolInstalled } from "../../lib/feature-status.js";
@@ -137,6 +138,7 @@ export function registerUpscale(app: FastifyInstance) {
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let saveModeRaw: string | null = null;
let inputKey: string | null = null;
try {
@@ -155,6 +157,8 @@ export function registerUpscale(app: FastifyInstance) {
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
} else if (part.fieldname === "saveMode") {
saveModeRaw = part.value as string;
}
}
} catch (err) {
@@ -164,6 +168,11 @@ export function registerUpscale(app: FastifyInstance) {
});
}
const saveMode = parseSaveModeField(saveModeRaw);
if (saveMode === null) {
return reply.status(400).send({ error: INVALID_SAVE_MODE_ERROR });
}
if (!inputKey) {
return reply.status(400).send({ error: "No image file provided" });
}
@@ -225,6 +234,7 @@ export function registerUpscale(app: FastifyInstance) {
settings,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
saveMode,
kind: "ai-tool",
});