feat: implement Files page with persistent storage and version tracking

Three-panel file manager (nav, list, details) modeled after Stirling-PDF.
- Backend: user_files table, /api/v1/files/* CRUD routes, file storage
  helpers, thumbnail generation via Sharp, recursive CTE version chains
- Frontend: FilesNav, FileList, FileDetails, FileUploadArea components,
  Zustand store, mobile layout with bottom sheet
- Integration: tool-factory auto-saves results as new versions when
  fileId is provided, "Open File" loads file into tool processing flow
- Search, bulk select/delete/download, version badges, tool chain tags
This commit is contained in:
Siddharth Kumar Sah
2026-03-26 01:10:51 +08:00
parent 926b52d330
commit 62fbb5484c
24 changed files with 771 additions and 470 deletions
+9 -18
View File
@@ -1,15 +1,15 @@
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import type { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
import { z } from "zod";
import { eq } from "drizzle-orm";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import sharp from "sharp";
import { createWorkspace } from "../lib/workspace.js";
import type { z } from "zod";
import { db, schema } from "../db/index.js";
import { autoOrient } from "../lib/auto-orient.js";
import { validateImageBuffer } from "../lib/file-validation.js";
import { sanitizeFilename } from "../lib/filename.js";
import { autoOrient } from "../lib/auto-orient.js";
import { db, schema } from "../db/index.js";
import { createWorkspace } from "../lib/workspace.js";
export interface ToolRouteConfig<T> {
/** Unique tool identifier, used as the URL path segment. */
@@ -54,10 +54,7 @@ export function getToolConfig(toolId: string): ToolRouteConfig<any> | undefined
* - Error handling
* - Response formatting
*/
export function createToolRoute<T>(
app: FastifyInstance,
config: ToolRouteConfig<T>,
): void {
export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig<T>): void {
// Register in the tool registry for batch processing
toolRegistry.set(config.toolId, config);
@@ -101,17 +98,13 @@ export function createToolRoute<T>(
// Require a file
if (!fileBuffer || fileBuffer.length === 0) {
return reply
.status(400)
.send({ error: "No image file provided" });
return reply.status(400).send({ error: "No image file provided" });
}
// Validate the uploaded image
const validation = await validateImageBuffer(fileBuffer);
if (!validation.valid) {
return reply
.status(400)
.send({ error: `Invalid image: ${validation.reason}` });
return reply.status(400).send({ error: `Invalid image: ${validation.reason}` });
}
// Parse and validate settings
@@ -162,9 +155,7 @@ export function createToolRoute<T>(
.get();
if (parent) {
const newVersion = parent.version + 1;
const parentChain: string[] = parent.toolChain
? JSON.parse(parent.toolChain)
: [];
const parentChain: string[] = parent.toolChain ? JSON.parse(parent.toolChain) : [];
const newToolChain = [...parentChain, config.toolId];
const storedName = await saveFile(result.buffer, result.filename);
// Get image dimensions from the processed output