mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: add enterprise licensing and S3 storage backend
Add the enterprise package with Ed25519 license key validation and feature gating. Enterprise code lives in the public repo under a proprietary license (Cal.com/PostHog model), protected legally, not by code hiding. Implement S3-compatible storage backend as the first enterprise feature. The file-storage module now delegates to either local filesystem or S3 based on STORAGE_MODE env var. Works with AWS S3, Cloudflare R2, DigitalOcean Spaces, MinIO, and any S3-compatible provider. Workspace files remain local (ephemeral processing). New env vars: STORAGE_MODE, S3_BUCKET, S3_REGION, S3_ENDPOINT, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE, S3_PREFIX, SNAPOTTER_LICENSE_KEY. Tested against MinIO: 10 S3 integration tests + 82 existing tests pass with zero regressions.
This commit is contained in:
@@ -10,8 +10,6 @@
|
||||
* POST /api/v1/files/save-result — Save a tool processing result (new version)
|
||||
*/
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { extname } from "node:path";
|
||||
import { and, desc, eq, like, sql } from "drizzle-orm";
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
@@ -24,9 +22,10 @@ import {
|
||||
deleteStoredFile,
|
||||
deleteThumbnail,
|
||||
getCachedThumbnail,
|
||||
getStoredFilePath,
|
||||
readStoredFile,
|
||||
saveFile,
|
||||
saveThumbnail,
|
||||
streamStoredFile,
|
||||
} from "../lib/file-storage.js";
|
||||
import { validateImageBuffer } from "../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../lib/filename.js";
|
||||
@@ -375,14 +374,12 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
|
||||
return reply.status(404).send({ error: "File not found" });
|
||||
}
|
||||
|
||||
const filePath = getStoredFilePath(file.storedName);
|
||||
|
||||
const stream = createReadStream(filePath);
|
||||
stream.on("error", () => {
|
||||
if (!reply.raw.headersSent) {
|
||||
reply.status(404).send({ error: "File not found on disk" });
|
||||
}
|
||||
});
|
||||
let stream;
|
||||
try {
|
||||
stream = await streamStoredFile(file.storedName);
|
||||
} catch {
|
||||
return reply.status(404).send({ error: "File not found in storage" });
|
||||
}
|
||||
|
||||
return reply
|
||||
.header("Content-Type", file.mimeType)
|
||||
@@ -422,10 +419,8 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
|
||||
.send(cached);
|
||||
}
|
||||
|
||||
const filePath = getStoredFilePath(file.storedName);
|
||||
|
||||
try {
|
||||
const rawBuffer = await readFile(filePath);
|
||||
const rawBuffer = await readStoredFile(file.storedName);
|
||||
const validation = await validateImageBuffer(rawBuffer, file.originalName);
|
||||
let decoded: Buffer<ArrayBuffer> = Buffer.from(rawBuffer);
|
||||
if (validation.valid && validation.format === "heif") {
|
||||
|
||||
Reference in New Issue
Block a user