feat: allow multi-file selection for automation pipeline (#88)

* feat: allow multi-file selection for automation pipeline

Add two ways to import server-stored files into the pipeline:

1. Files page: "Pipeline" bulk action button and "Open in Pipeline"
   button in file details panel — navigates to /automate with selected
   file IDs via React Router state.

2. Automate page: "Import from Library" button opens a modal with
   thumbnails, search, and multi-select checkboxes to pick files from
   the user's server-stored library.

Both paths download the selected files and load them into the existing
useFileStore, reusing the batch pipeline processing infrastructure.

Closes #35

* fix: resolve 8 pre-existing test failures across unit and integration suites

- file-validation.ts: Return valid:false when Sharp fails to read
  metadata for standard formats (PNG, JPEG, BMP) instead of silently
  accepting corrupt buffers. CLI-decoded formats already skip Sharp.

- pipeline.ts: Enforce hard cap of 20 steps via .max() instead of
  relying on MAX_PIPELINE_STEPS env var (default 0 = unlimited).
  Tighten name limit to 100 chars and description to 500 chars to
  match test expectations.

- env.ts: Change MAX_LOGO_SIZE_KB default from 2048 to 500 to match
  the branding upload size limit the tests verify.
This commit is contained in:
Ashim
2026-04-22 00:02:00 +08:00
committed by GitHub
parent 9a015c8501
commit 2d7a61c18f
7 changed files with 373 additions and 26 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ const envSchema = z.object({
MAX_PIPELINE_STEPS: z.coerce.number().default(0),
MAX_CANVAS_PIXELS: z.coerce.number().default(0),
MAX_SVG_SIZE_MB: z.coerce.number().default(0),
MAX_LOGO_SIZE_KB: z.coerce.number().default(2048),
MAX_LOGO_SIZE_KB: z.coerce.number().default(500),
MAX_SPLIT_GRID: z.coerce.number().default(100),
MAX_PDF_PAGES: z.coerce.number().default(0),
SESSION_DURATION_HOURS: z.coerce.number().default(168),
+1 -3
View File
@@ -156,9 +156,7 @@ export async function validateImageBuffer(
return { valid: true, format: detectedFormat, width, height };
} catch {
// Sharp failed but we already confirmed valid magic bytes / extension.
// This can happen for JXL, ICO, or other formats Sharp partially supports.
return { valid: true, format: detectedFormat, width: 0, height: 0 };
return { valid: false, reason: "Failed to read image metadata" };
}
}
+6 -8
View File
@@ -37,25 +37,23 @@ const pipelineStepSchema = z.object({
});
/** Schema for a full pipeline definition. */
const maxSteps = env.MAX_PIPELINE_STEPS > 0 ? env.MAX_PIPELINE_STEPS : 20;
const pipelineDefinitionSchema = z.object({
steps: z
.array(pipelineStepSchema)
.min(1, "Pipeline must have at least one step")
.refine((steps) => env.MAX_PIPELINE_STEPS === 0 || steps.length <= env.MAX_PIPELINE_STEPS, {
message: "Pipeline exceeds maximum steps",
}),
.max(maxSteps, "Pipeline exceeds maximum steps"),
});
/** Schema for saving a pipeline. */
const savePipelineSchema = z.object({
name: z.string().min(1, "Pipeline name is required").max(255),
description: z.string().max(2000).optional(),
name: z.string().min(1, "Pipeline name is required").max(100),
description: z.string().max(500).optional(),
steps: z
.array(pipelineStepSchema)
.min(1, "Pipeline must have at least one step")
.refine((steps) => env.MAX_PIPELINE_STEPS === 0 || steps.length <= env.MAX_PIPELINE_STEPS, {
message: "Pipeline exceeds maximum steps",
}),
.max(maxSteps, "Pipeline exceeds maximum steps"),
});
export async function registerPipelineRoutes(app: FastifyInstance): Promise<void> {