mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
@@ -0,0 +1 @@
|
||||
ALTER TABLE `users` ADD `team` text NOT NULL DEFAULT 'Default';
|
||||
@@ -57,12 +57,8 @@
|
||||
"name": "api_keys_user_id_users_id_fk",
|
||||
"tableFrom": "api_keys",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -191,12 +187,8 @@
|
||||
"name": "sessions_user_id_users_id_fk",
|
||||
"tableFrom": "sessions",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -294,9 +286,7 @@
|
||||
"indexes": {
|
||||
"users_username_unique": {
|
||||
"name": "users_username_unique",
|
||||
"columns": [
|
||||
"username"
|
||||
],
|
||||
"columns": ["username"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
@@ -316,4 +306,4 @@
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,12 +57,8 @@
|
||||
"name": "api_keys_user_id_users_id_fk",
|
||||
"tableFrom": "api_keys",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -236,12 +232,8 @@
|
||||
"name": "sessions_user_id_users_id_fk",
|
||||
"tableFrom": "sessions",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -339,9 +331,7 @@
|
||||
"indexes": {
|
||||
"users_username_unique": {
|
||||
"name": "users_username_unique",
|
||||
"columns": [
|
||||
"username"
|
||||
],
|
||||
"columns": ["username"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
@@ -361,4 +351,4 @@
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,12 +64,8 @@
|
||||
"name": "api_keys_user_id_users_id_fk",
|
||||
"tableFrom": "api_keys",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -212,12 +208,8 @@
|
||||
"name": "pipelines_user_id_users_id_fk",
|
||||
"tableFrom": "pipelines",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -264,12 +256,8 @@
|
||||
"name": "sessions_user_id_users_id_fk",
|
||||
"tableFrom": "sessions",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -367,9 +355,7 @@
|
||||
"indexes": {
|
||||
"users_username_unique": {
|
||||
"name": "users_username_unique",
|
||||
"columns": [
|
||||
"username"
|
||||
],
|
||||
"columns": ["username"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
@@ -389,4 +375,4 @@
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "c1a2b3d4-e5f6-7890-abcd-ef1234567890",
|
||||
"prevId": "b73098db-4ef1-44e2-834f-9114a4da4e77",
|
||||
"tables": {
|
||||
"api_keys": {
|
||||
"name": "api_keys",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"key_hash": {
|
||||
"name": "key_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"key_prefix": {
|
||||
"name": "key_prefix",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'Default API Key'"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_used_at": {
|
||||
"name": "last_used_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"api_keys_user_id_users_id_fk": {
|
||||
"name": "api_keys_user_id_users_id_fk",
|
||||
"tableFrom": "api_keys",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"jobs": {
|
||||
"name": "jobs",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'queued'"
|
||||
},
|
||||
"progress": {
|
||||
"name": "progress",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"input_files": {
|
||||
"name": "input_files",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"output_path": {
|
||||
"name": "output_path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"settings": {
|
||||
"name": "settings",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"error": {
|
||||
"name": "error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"completed_at": {
|
||||
"name": "completed_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"pipelines": {
|
||||
"name": "pipelines",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"steps": {
|
||||
"name": "steps",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"pipelines_user_id_users_id_fk": {
|
||||
"name": "pipelines_user_id_users_id_fk",
|
||||
"tableFrom": "pipelines",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"sessions": {
|
||||
"name": "sessions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"sessions_user_id_users_id_fk": {
|
||||
"name": "sessions_user_id_users_id_fk",
|
||||
"tableFrom": "sessions",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"settings": {
|
||||
"name": "settings",
|
||||
"columns": {
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"users": {
|
||||
"name": "users",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password_hash": {
|
||||
"name": "password_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'user'"
|
||||
},
|
||||
"must_change_password": {
|
||||
"name": "must_change_password",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"team": {
|
||||
"name": "team",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'Default'"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_username_unique": {
|
||||
"name": "users_username_unique",
|
||||
"columns": ["username"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -64,12 +64,8 @@
|
||||
"name": "api_keys_user_id_users_id_fk",
|
||||
"tableFrom": "api_keys",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -212,12 +208,8 @@
|
||||
"name": "pipelines_user_id_users_id_fk",
|
||||
"tableFrom": "pipelines",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -264,12 +256,8 @@
|
||||
"name": "sessions_user_id_users_id_fk",
|
||||
"tableFrom": "sessions",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -404,12 +392,8 @@
|
||||
"name": "user_files_user_id_users_id_fk",
|
||||
"tableFrom": "user_files",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -484,9 +468,7 @@
|
||||
"indexes": {
|
||||
"users_username_unique": {
|
||||
"name": "users_username_unique",
|
||||
"columns": [
|
||||
"username"
|
||||
],
|
||||
"columns": ["username"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
@@ -506,4 +488,4 @@
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
import Database from "better-sqlite3";
|
||||
import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||
import { env } from "../config.js";
|
||||
import * as schema from "./schema.js";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
// Ensure data directory exists
|
||||
mkdirSync(dirname(env.DB_PATH), { recursive: true });
|
||||
@@ -17,4 +17,4 @@ sqlite.pragma("synchronous = NORMAL");
|
||||
sqlite.pragma("foreign_keys = ON");
|
||||
|
||||
export const db = drizzle(sqlite, { schema });
|
||||
export { schema };
|
||||
export { schema, sqlite };
|
||||
|
||||
+18
-12
@@ -1,22 +1,23 @@
|
||||
import Fastify from "fastify";
|
||||
import cors from "@fastify/cors";
|
||||
import rateLimit from "@fastify/rate-limit";
|
||||
import { env } from "./config.js";
|
||||
import { APP_VERSION } from "@stirling-image/shared";
|
||||
import Fastify from "fastify";
|
||||
import { env } from "./config.js";
|
||||
import { db, schema } from "./db/index.js";
|
||||
import { runMigrations } from "./db/migrate.js";
|
||||
import { ensureDefaultAdmin, authRoutes, authMiddleware } from "./plugins/auth.js";
|
||||
import { registerUpload } from "./plugins/upload.js";
|
||||
import { registerStatic } from "./plugins/static.js";
|
||||
import { startCleanupCron } from "./lib/cleanup.js";
|
||||
import { fileRoutes } from "./routes/files.js";
|
||||
import { registerToolRoutes } from "./routes/tools/index.js";
|
||||
import { authMiddleware, authRoutes, ensureDefaultAdmin } from "./plugins/auth.js";
|
||||
import { registerStatic } from "./plugins/static.js";
|
||||
import { registerUpload } from "./plugins/upload.js";
|
||||
import { apiKeyRoutes } from "./routes/api-keys.js";
|
||||
import { registerBatchRoutes } from "./routes/batch.js";
|
||||
import { fileRoutes } from "./routes/files.js";
|
||||
import { registerPipelineRoutes } from "./routes/pipeline.js";
|
||||
import { registerProgressRoutes } from "./routes/progress.js";
|
||||
import { apiKeyRoutes } from "./routes/api-keys.js";
|
||||
import { settingsRoutes } from "./routes/settings.js";
|
||||
import { teamsRoutes } from "./routes/teams.js";
|
||||
import { registerToolRoutes } from "./routes/tools/index.js";
|
||||
import { userFileRoutes } from "./routes/user-files.js";
|
||||
import { db, schema } from "./db/index.js";
|
||||
|
||||
// Run before anything else
|
||||
runMigrations();
|
||||
@@ -34,7 +35,7 @@ const app = Fastify({
|
||||
await app.register(cors, {
|
||||
origin: env.CORS_ORIGIN
|
||||
? env.CORS_ORIGIN.split(",").map((s) => s.trim())
|
||||
: process.env.NODE_ENV === "production" ? false : true,
|
||||
: process.env.NODE_ENV !== "production",
|
||||
});
|
||||
|
||||
// Security headers
|
||||
@@ -91,17 +92,22 @@ await apiKeyRoutes(app);
|
||||
// Settings routes
|
||||
await settingsRoutes(app);
|
||||
|
||||
// Teams routes
|
||||
await teamsRoutes(app);
|
||||
|
||||
// Health check
|
||||
app.get("/api/v1/health", async () => {
|
||||
let dbOk = false;
|
||||
try {
|
||||
db.select().from(schema.settings).limit(1).all();
|
||||
dbOk = true;
|
||||
} catch { /* db unreachable */ }
|
||||
} catch {
|
||||
/* db unreachable */
|
||||
}
|
||||
return {
|
||||
status: dbOk ? "healthy" : "degraded",
|
||||
version: APP_VERSION,
|
||||
uptime: process.uptime().toFixed(0) + "s",
|
||||
uptime: `${process.uptime().toFixed(0)}s`,
|
||||
storage: { mode: env.STORAGE_MODE, available: "N/A" },
|
||||
database: dbOk ? "ok" : "error",
|
||||
queue: { active: 0, pending: 0 },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mkdir, writeFile, unlink } from "node:fs/promises";
|
||||
import { join, extname } from "node:path";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { mkdir, unlink, writeFile } from "node:fs/promises";
|
||||
import { extname, join } from "node:path";
|
||||
import { env } from "../config.js";
|
||||
|
||||
let storageReady = false;
|
||||
|
||||
@@ -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
|
||||
|
||||
+172
-213
@@ -12,14 +12,14 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { extname } from "node:path";
|
||||
import type { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
|
||||
import { eq, desc, like, and, sql } from "drizzle-orm";
|
||||
import { and, desc, eq, like, sql } from "drizzle-orm";
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { db, schema, sqlite } from "../db/index.js";
|
||||
import { deleteStoredFile, getStoredFilePath, saveFile } from "../lib/file-storage.js";
|
||||
import { validateImageBuffer } from "../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../lib/filename.js";
|
||||
import { saveFile, deleteStoredFile, getStoredFilePath } from "../lib/file-storage.js";
|
||||
import { getAuthUser } from "../plugins/auth.js";
|
||||
import { db, schema } from "../db/index.js";
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -62,7 +62,7 @@ function serializeFile(row: typeof schema.userFiles.$inferSelect) {
|
||||
height: row.height,
|
||||
version: row.version,
|
||||
parentId: row.parentId,
|
||||
toolChain: row.toolChain ? JSON.parse(row.toolChain) : null,
|
||||
toolChain: row.toolChain ? JSON.parse(row.toolChain) : [],
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
};
|
||||
}
|
||||
@@ -144,76 +144,69 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
|
||||
* Multipart form with one or more image file parts.
|
||||
* Validates each (magic bytes + dimensions), stores to disk, creates DB record.
|
||||
*/
|
||||
app.post(
|
||||
"/api/v1/files/upload",
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const user = getAuthUser(request);
|
||||
const userId = user?.id ?? null;
|
||||
app.post("/api/v1/files/upload", async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const user = getAuthUser(request);
|
||||
const userId = user?.id ?? null;
|
||||
|
||||
const created: ReturnType<typeof serializeFile>[] = [];
|
||||
const created: ReturnType<typeof serializeFile>[] = [];
|
||||
|
||||
const parts = request.parts();
|
||||
const parts = request.parts();
|
||||
|
||||
for await (const part of parts) {
|
||||
if (part.type !== "file") continue;
|
||||
for await (const part of parts) {
|
||||
if (part.type !== "file") continue;
|
||||
|
||||
// Consume the stream into a buffer
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of part.file) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
const buffer = Buffer.concat(chunks);
|
||||
// Consume the stream into a buffer
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of part.file) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
const buffer = Buffer.concat(chunks);
|
||||
|
||||
if (buffer.length === 0) continue;
|
||||
if (buffer.length === 0) continue;
|
||||
|
||||
// Validate image
|
||||
const validation = await validateImageBuffer(buffer);
|
||||
if (!validation.valid) {
|
||||
return reply.status(400).send({
|
||||
error: `Invalid file "${part.filename}": ${validation.reason}`,
|
||||
});
|
||||
}
|
||||
|
||||
const safeName = sanitizeFilename(part.filename ?? "upload");
|
||||
const mimeType = formatToMime(validation.format);
|
||||
|
||||
// Persist to disk
|
||||
const storedName = await saveFile(buffer, safeName);
|
||||
|
||||
// Create DB record
|
||||
const id = randomUUID();
|
||||
db.insert(schema.userFiles)
|
||||
.values({
|
||||
id,
|
||||
userId,
|
||||
originalName: safeName,
|
||||
storedName,
|
||||
mimeType,
|
||||
size: buffer.length,
|
||||
width: validation.width,
|
||||
height: validation.height,
|
||||
version: 1,
|
||||
parentId: null,
|
||||
toolChain: null,
|
||||
})
|
||||
.run();
|
||||
|
||||
const row = db
|
||||
.select()
|
||||
.from(schema.userFiles)
|
||||
.where(eq(schema.userFiles.id, id))
|
||||
.get();
|
||||
|
||||
if (row) created.push(serializeFile(row));
|
||||
// Validate image
|
||||
const validation = await validateImageBuffer(buffer);
|
||||
if (!validation.valid) {
|
||||
return reply.status(400).send({
|
||||
error: `Invalid file "${part.filename}": ${validation.reason}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (created.length === 0) {
|
||||
return reply.status(400).send({ error: "No valid files uploaded" });
|
||||
}
|
||||
const safeName = sanitizeFilename(part.filename ?? "upload");
|
||||
const mimeType = formatToMime(validation.format);
|
||||
|
||||
return reply.status(201).send({ files: created });
|
||||
},
|
||||
);
|
||||
// Persist to disk
|
||||
const storedName = await saveFile(buffer, safeName);
|
||||
|
||||
// Create DB record
|
||||
const id = randomUUID();
|
||||
db.insert(schema.userFiles)
|
||||
.values({
|
||||
id,
|
||||
userId,
|
||||
originalName: safeName,
|
||||
storedName,
|
||||
mimeType,
|
||||
size: buffer.length,
|
||||
width: validation.width,
|
||||
height: validation.height,
|
||||
version: 1,
|
||||
parentId: null,
|
||||
toolChain: null,
|
||||
})
|
||||
.run();
|
||||
|
||||
const row = db.select().from(schema.userFiles).where(eq(schema.userFiles.id, id)).get();
|
||||
|
||||
if (row) created.push(serializeFile(row));
|
||||
}
|
||||
|
||||
if (created.length === 0) {
|
||||
return reply.status(400).send({ error: "No valid files uploaded" });
|
||||
}
|
||||
|
||||
return reply.status(201).send({ files: created });
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/v1/files/:id
|
||||
@@ -223,17 +216,10 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
|
||||
*/
|
||||
app.get(
|
||||
"/api/v1/files/:id",
|
||||
async (
|
||||
request: FastifyRequest<{ Params: { id: string } }>,
|
||||
reply: FastifyReply,
|
||||
) => {
|
||||
async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
|
||||
const { id } = request.params;
|
||||
|
||||
const file = db
|
||||
.select()
|
||||
.from(schema.userFiles)
|
||||
.where(eq(schema.userFiles.id, id))
|
||||
.get();
|
||||
const file = db.select().from(schema.userFiles).where(eq(schema.userFiles.id, id)).get();
|
||||
|
||||
if (!file) {
|
||||
return reply.status(404).send({ error: "File not found" });
|
||||
@@ -254,11 +240,11 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
const chainRows = db.all<ChainRow>(
|
||||
sql`
|
||||
const chainRows = sqlite
|
||||
.prepare(`
|
||||
WITH RECURSIVE
|
||||
ancestors(id, parent_id) AS (
|
||||
SELECT id, parent_id FROM user_files WHERE id = ${id}
|
||||
SELECT id, parent_id FROM user_files WHERE id = ?
|
||||
UNION ALL
|
||||
SELECT uf.id, uf.parent_id FROM user_files uf
|
||||
INNER JOIN ancestors a ON uf.id = a.parent_id
|
||||
@@ -277,8 +263,8 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
|
||||
INNER JOIN chain c ON child.parent_id = c.id
|
||||
)
|
||||
SELECT * FROM chain ORDER BY version ASC
|
||||
`,
|
||||
);
|
||||
`)
|
||||
.all(id) as ChainRow[];
|
||||
|
||||
const versions = chainRows.map((r) => ({
|
||||
id: r.id,
|
||||
@@ -289,7 +275,7 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
|
||||
height: r.height,
|
||||
version: r.version,
|
||||
parentId: r.parent_id,
|
||||
toolChain: r.tool_chain ? JSON.parse(r.tool_chain) : null,
|
||||
toolChain: r.tool_chain ? JSON.parse(r.tool_chain) : [],
|
||||
createdAt: new Date(r.created_at * 1000).toISOString(),
|
||||
}));
|
||||
|
||||
@@ -307,17 +293,10 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
|
||||
*/
|
||||
app.get(
|
||||
"/api/v1/files/:id/download",
|
||||
async (
|
||||
request: FastifyRequest<{ Params: { id: string } }>,
|
||||
reply: FastifyReply,
|
||||
) => {
|
||||
async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
|
||||
const { id } = request.params;
|
||||
|
||||
const file = db
|
||||
.select()
|
||||
.from(schema.userFiles)
|
||||
.where(eq(schema.userFiles.id, id))
|
||||
.get();
|
||||
const file = db.select().from(schema.userFiles).where(eq(schema.userFiles.id, id)).get();
|
||||
|
||||
if (!file) {
|
||||
return reply.status(404).send({ error: "File not found" });
|
||||
@@ -347,17 +326,10 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
|
||||
*/
|
||||
app.get(
|
||||
"/api/v1/files/:id/thumbnail",
|
||||
async (
|
||||
request: FastifyRequest<{ Params: { id: string } }>,
|
||||
reply: FastifyReply,
|
||||
) => {
|
||||
async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
|
||||
const { id } = request.params;
|
||||
|
||||
const file = db
|
||||
.select()
|
||||
.from(schema.userFiles)
|
||||
.where(eq(schema.userFiles.id, id))
|
||||
.get();
|
||||
const file = db.select().from(schema.userFiles).where(eq(schema.userFiles.id, id)).get();
|
||||
|
||||
if (!file) {
|
||||
return reply.status(404).send({ error: "File not found" });
|
||||
@@ -387,35 +359,35 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
|
||||
* Bulk delete. Body: { ids: string[] }
|
||||
* For each id, deletes the entire version chain (all ancestors and descendants).
|
||||
*/
|
||||
app.delete(
|
||||
"/api/v1/files",
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const body = request.body as { ids?: unknown } | null;
|
||||
app.delete("/api/v1/files", async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const body = request.body as { ids?: unknown } | null;
|
||||
|
||||
if (!Array.isArray(body?.ids) || body.ids.length === 0) {
|
||||
return reply.status(400).send({ error: "ids must be a non-empty array" });
|
||||
}
|
||||
if (!Array.isArray(body?.ids) || body.ids.length === 0) {
|
||||
return reply.status(400).send({ error: "ids must be a non-empty array" });
|
||||
}
|
||||
|
||||
const ids = body.ids.filter((id): id is string => typeof id === "string");
|
||||
if (ids.length === 0) {
|
||||
return reply.status(400).send({ error: "ids must contain string values" });
|
||||
}
|
||||
const ids = body.ids.filter((id): id is string => typeof id === "string");
|
||||
if (ids.length === 0) {
|
||||
return reply.status(400).send({ error: "ids must contain string values" });
|
||||
}
|
||||
|
||||
let deletedCount = 0;
|
||||
let deletedCount = 0;
|
||||
|
||||
interface DeleteChainRow { id: string; stored_name: string }
|
||||
interface DeleteChainRow {
|
||||
id: string;
|
||||
stored_name: string;
|
||||
}
|
||||
|
||||
for (const id of ids) {
|
||||
// Collect all files in the chain using a recursive CTE
|
||||
const chainRows = db.all<DeleteChainRow>(
|
||||
sql`
|
||||
for (const id of ids) {
|
||||
// Collect all files in the chain using a recursive CTE
|
||||
const chainRows = sqlite
|
||||
.prepare(`
|
||||
WITH RECURSIVE chain(id, stored_name) AS (
|
||||
-- Anchor: find the root of this chain
|
||||
SELECT f.id, f.stored_name
|
||||
FROM user_files f
|
||||
WHERE f.id = (
|
||||
WITH RECURSIVE ancestors(id, parent_id) AS (
|
||||
SELECT id, parent_id FROM user_files WHERE id = ${id}
|
||||
SELECT id, parent_id FROM user_files WHERE id = ?
|
||||
UNION ALL
|
||||
SELECT uf.id, uf.parent_id FROM user_files uf
|
||||
INNER JOIN ancestors a ON uf.id = a.parent_id
|
||||
@@ -423,27 +395,23 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
|
||||
SELECT id FROM ancestors WHERE parent_id IS NULL LIMIT 1
|
||||
)
|
||||
UNION ALL
|
||||
-- Recurse: all descendants
|
||||
SELECT child.id, child.stored_name
|
||||
FROM user_files child
|
||||
INNER JOIN chain c ON child.parent_id = c.id
|
||||
)
|
||||
SELECT id, stored_name FROM chain
|
||||
`,
|
||||
);
|
||||
`)
|
||||
.all(id) as DeleteChainRow[];
|
||||
|
||||
for (const row of chainRows) {
|
||||
await deleteStoredFile(row.stored_name);
|
||||
db.delete(schema.userFiles)
|
||||
.where(eq(schema.userFiles.id, row.id))
|
||||
.run();
|
||||
deletedCount++;
|
||||
}
|
||||
for (const row of chainRows) {
|
||||
await deleteStoredFile(row.stored_name);
|
||||
db.delete(schema.userFiles).where(eq(schema.userFiles.id, row.id)).run();
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return reply.send({ deleted: deletedCount });
|
||||
},
|
||||
);
|
||||
return reply.send({ deleted: deletedCount });
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/v1/files/save-result
|
||||
@@ -454,105 +422,96 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
|
||||
* parentId — id of the parent user file record
|
||||
* toolId — the tool that produced this result
|
||||
*/
|
||||
app.post(
|
||||
"/api/v1/files/save-result",
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const user = getAuthUser(request);
|
||||
const userId = user?.id ?? null;
|
||||
app.post("/api/v1/files/save-result", async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const user = getAuthUser(request);
|
||||
const userId = user?.id ?? null;
|
||||
|
||||
let fileBuffer: Buffer | null = null;
|
||||
let filename = "result";
|
||||
let parentId: string | null = null;
|
||||
let toolId: string | null = null;
|
||||
let fileBuffer: Buffer | null = null;
|
||||
let filename = "result";
|
||||
let parentId: string | null = null;
|
||||
let toolId: string | null = null;
|
||||
|
||||
const parts = request.parts();
|
||||
for await (const part of parts) {
|
||||
if (part.type === "file") {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of part.file) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
fileBuffer = Buffer.concat(chunks);
|
||||
filename = sanitizeFilename(part.filename ?? "result");
|
||||
} else if (part.fieldname === "parentId") {
|
||||
parentId = (part.value as string).trim() || null;
|
||||
} else if (part.fieldname === "toolId") {
|
||||
toolId = (part.value as string).trim() || null;
|
||||
const parts = request.parts();
|
||||
for await (const part of parts) {
|
||||
if (part.type === "file") {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of part.file) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
fileBuffer = Buffer.concat(chunks);
|
||||
filename = sanitizeFilename(part.filename ?? "result");
|
||||
} else if (part.fieldname === "parentId") {
|
||||
parentId = (part.value as string).trim() || null;
|
||||
} else if (part.fieldname === "toolId") {
|
||||
toolId = (part.value as string).trim() || null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileBuffer || fileBuffer.length === 0) {
|
||||
return reply.status(400).send({ error: "No file provided" });
|
||||
}
|
||||
if (!fileBuffer || fileBuffer.length === 0) {
|
||||
return reply.status(400).send({ error: "No file provided" });
|
||||
}
|
||||
|
||||
if (!parentId) {
|
||||
return reply.status(400).send({ error: "parentId is required" });
|
||||
}
|
||||
if (!parentId) {
|
||||
return reply.status(400).send({ error: "parentId is required" });
|
||||
}
|
||||
|
||||
// Validate the image
|
||||
const validation = await validateImageBuffer(fileBuffer);
|
||||
if (!validation.valid) {
|
||||
return reply.status(400).send({
|
||||
error: `Invalid file: ${validation.reason}`,
|
||||
});
|
||||
}
|
||||
// Validate the image
|
||||
const validation = await validateImageBuffer(fileBuffer);
|
||||
if (!validation.valid) {
|
||||
return reply.status(400).send({
|
||||
error: `Invalid file: ${validation.reason}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Look up the parent to compute the next version and carry forward the tool chain
|
||||
const parent = db
|
||||
.select()
|
||||
.from(schema.userFiles)
|
||||
.where(eq(schema.userFiles.id, parentId))
|
||||
.get();
|
||||
// Look up the parent to compute the next version and carry forward the tool chain
|
||||
const parent = db
|
||||
.select()
|
||||
.from(schema.userFiles)
|
||||
.where(eq(schema.userFiles.id, parentId))
|
||||
.get();
|
||||
|
||||
if (!parent) {
|
||||
return reply.status(404).send({ error: "Parent file not found" });
|
||||
}
|
||||
if (!parent) {
|
||||
return reply.status(404).send({ error: "Parent file not found" });
|
||||
}
|
||||
|
||||
const nextVersion = parent.version + 1;
|
||||
const nextVersion = parent.version + 1;
|
||||
|
||||
// Build the tool chain: append the new toolId to the parent's chain
|
||||
const existingChain: string[] = parent.toolChain
|
||||
? JSON.parse(parent.toolChain)
|
||||
: [];
|
||||
const newChain = toolId ? [...existingChain, toolId] : existingChain;
|
||||
// Build the tool chain: append the new toolId to the parent's chain
|
||||
const existingChain: string[] = parent.toolChain ? JSON.parse(parent.toolChain) : [];
|
||||
const newChain = toolId ? [...existingChain, toolId] : existingChain;
|
||||
|
||||
// Determine the original filename (preserve parent's name, update extension)
|
||||
const ext = extname(filename) || extname(parent.originalName);
|
||||
const baseName = parent.originalName.replace(/\.[^.]+$/, "");
|
||||
const resultName = `${baseName}${ext}`;
|
||||
// Determine the original filename (preserve parent's name, update extension)
|
||||
const ext = extname(filename) || extname(parent.originalName);
|
||||
const baseName = parent.originalName.replace(/\.[^.]+$/, "");
|
||||
const resultName = `${baseName}${ext}`;
|
||||
|
||||
const mimeType = formatToMime(validation.format) || extToMime(ext);
|
||||
const mimeType = formatToMime(validation.format) || extToMime(ext);
|
||||
|
||||
// Persist to disk
|
||||
const storedName = await saveFile(fileBuffer, resultName);
|
||||
// Persist to disk
|
||||
const storedName = await saveFile(fileBuffer, resultName);
|
||||
|
||||
// Create DB record
|
||||
const id = randomUUID();
|
||||
db.insert(schema.userFiles)
|
||||
.values({
|
||||
id,
|
||||
userId,
|
||||
originalName: resultName,
|
||||
storedName,
|
||||
mimeType,
|
||||
size: fileBuffer.length,
|
||||
width: validation.width,
|
||||
height: validation.height,
|
||||
version: nextVersion,
|
||||
parentId,
|
||||
toolChain: JSON.stringify(newChain),
|
||||
})
|
||||
.run();
|
||||
// Create DB record
|
||||
const id = randomUUID();
|
||||
db.insert(schema.userFiles)
|
||||
.values({
|
||||
id,
|
||||
userId,
|
||||
originalName: resultName,
|
||||
storedName,
|
||||
mimeType,
|
||||
size: fileBuffer.length,
|
||||
width: validation.width,
|
||||
height: validation.height,
|
||||
version: nextVersion,
|
||||
parentId,
|
||||
toolChain: JSON.stringify(newChain),
|
||||
})
|
||||
.run();
|
||||
|
||||
const row = db
|
||||
.select()
|
||||
.from(schema.userFiles)
|
||||
.where(eq(schema.userFiles.id, id))
|
||||
.get();
|
||||
const row = db.select().from(schema.userFiles).where(eq(schema.userFiles.id, id)).get();
|
||||
|
||||
return reply.status(201).send({ file: row ? serializeFile(row) : null });
|
||||
},
|
||||
);
|
||||
return reply.status(201).send({ file: row ? serializeFile(row) : null });
|
||||
});
|
||||
|
||||
app.log.info("User file routes registered");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user