From 0781116d47bfd28cf74ebb51e35e5fe1116af378 Mon Sep 17 00:00:00 2001 From: antonin <18756890+Antoninj@users.noreply.github.com> Date: Sat, 18 Jul 2026 13:07:33 +0200 Subject: [PATCH 1/3] Add configurable S3 storage prefix --- .../components/storages/s3/s3.form.tsx | 13 +++++++++++++ .../components/storages/s3/s3.schema.ts | 1 + .../channel/components/storages/s3/s3.ts | 19 ++++++++++++------- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/features/channel/components/storages/s3/s3.form.tsx b/src/features/channel/components/storages/s3/s3.form.tsx index edcaceca..39a28c1e 100644 --- a/src/features/channel/components/storages/s3/s3.form.tsx +++ b/src/features/channel/components/storages/s3/s3.form.tsx @@ -84,6 +84,19 @@ export const StorageS3Form = ({ form }: StorageS3FormProps) => { )} /> + ( + + Prefix + + + + + + )} + /> ""), z.literal("").transform(() => ""), diff --git a/src/features/channel/components/storages/s3/s3.ts b/src/features/channel/components/storages/s3/s3.ts index fc4672c7..c4c77ada 100644 --- a/src/features/channel/components/storages/s3/s3.ts +++ b/src/features/channel/components/storages/s3/s3.ts @@ -15,6 +15,7 @@ type S3Config = { accessKey: string; secretKey: string; bucketName: string; + prefix?: string; port?: number; ssl?: boolean; }; @@ -30,8 +31,12 @@ async function getS3Client(config: S3Config) { }); } -const BASE_DIR = ""; +function buildKey(config: S3Config, path: string) { + const prefix = config.prefix?.trim().replace(/^\/+|\/+$/g, ""); + const normalizedPath = path.replace(/^\/+/, ""); + return prefix ? `${prefix}/${normalizedPath}` : normalizedPath; +} async function ensureBucket(config: S3Config) { const client = await getS3Client(config); @@ -46,7 +51,7 @@ export async function uploadS3( const client = await getS3Client(config); await ensureBucket(config); - const key = `${BASE_DIR}${input.data.path}`; + const key = buildKey(config, input.data.path); const file = input.data.file; let uploadStream: Readable; @@ -73,7 +78,7 @@ export async function getS3( input: { data: StorageGetInput, metadata: StorageMetaData } ): Promise { const client = await getS3Client(config); - const key = `${BASE_DIR}${input.data.path}`; + const key = buildKey(config, input.data.path); try { await client.statObject(config.bucketName, key); @@ -102,7 +107,7 @@ export async function deleteS3(config: S3Config, input: { metadata?: StorageMetaData }): Promise { const client = await getS3Client(config); - const key = `${BASE_DIR}${input.data.path}`; + const key = buildKey(config, input.data.path); try { await client.removeObject(config.bucketName, key); @@ -121,7 +126,7 @@ export async function pingS3(config: S3Config): Promise { provider: "s3", response: "Bucket does not exist" }; - const key = `${BASE_DIR}ping.txt`; + const key = buildKey(config, "ping.txt"); await client.putObject(config.bucketName, key, Buffer.from("ping")); await client.getObject(config.bucketName, key); await client.removeObject(config.bucketName, key); @@ -150,8 +155,8 @@ export async function copyS3( const client = await getS3Client(config); await ensureBucket(config); - const sourceKey = `${BASE_DIR}${input.data.from}`; - const destinationKey = `${BASE_DIR}${input.data.to}`; + const sourceKey = buildKey(config, input.data.from); + const destinationKey = buildKey(config, input.data.to); try { await client.copyObject( From 4a7793260e80eafa72ee36d3d6f6660f6ca4e7e3 Mon Sep 17 00:00:00 2001 From: antonin <18756890+Antoninj@users.noreply.github.com> Date: Sat, 18 Jul 2026 13:07:33 +0200 Subject: [PATCH 2/3] feat: configure backup file prefix --- .env.example | 4 ++++ src/env.mjs | 3 +++ .../components/storages/s3/s3.form.tsx | 13 ------------- .../components/storages/s3/s3.schema.ts | 1 - .../channel/components/storages/s3/s3.ts | 19 +++++++------------ .../storages/utils/storages.helpers.ts | 7 ++++++- 6 files changed, 20 insertions(+), 27 deletions(-) diff --git a/.env.example b/.env.example index 41acc702..b666be12 100644 --- a/.env.example +++ b/.env.example @@ -73,6 +73,10 @@ AUTH_PASSKEY_ENABLED=true # Retention RETENTION_CRON="* * * * *" +# Optional prefix for backup files stored in configured storage channels. +# Defaults to "backups" when unset. +#BACKUP_FILE_PREFIX=backups + TRUSTED_DOMAINS="http://localhost:8887, http://localhost:3055, http://localhost:3056" #OPENAPI_ENABLED=true diff --git a/src/env.mjs b/src/env.mjs index 3ae8f6fa..9eb45fb5 100644 --- a/src/env.mjs +++ b/src/env.mjs @@ -66,6 +66,8 @@ export const env = createEnv({ STALE_BACKUP_THRESHOLD_HOURS: z.coerce.number().default(6), + BACKUP_FILE_PREFIX: z.string().optional(), + AUTH_OIDC_ID: z.string().optional().default("oidc"), AUTH_OIDC_TITLE: z.string().optional(), AUTH_OIDC_DESC: z.string().optional(), @@ -154,6 +156,7 @@ export const env = createEnv({ RETENTION_CRON: process.env.RETENTION_CRON, CLEANING_HEALTHCHECK_LOGS_CRON: process.env.CLEANING_HEALTHCHECK_LOGS_CRON, STALE_BACKUP_THRESHOLD_HOURS: process.env.STALE_BACKUP_THRESHOLD_HOURS, + BACKUP_FILE_PREFIX: process.env.BACKUP_FILE_PREFIX, AUTH_OIDC_ID: process.env.AUTH_OIDC_ID, AUTH_OIDC_TITLE: process.env.AUTH_OIDC_TITLE, diff --git a/src/features/channel/components/storages/s3/s3.form.tsx b/src/features/channel/components/storages/s3/s3.form.tsx index 39a28c1e..edcaceca 100644 --- a/src/features/channel/components/storages/s3/s3.form.tsx +++ b/src/features/channel/components/storages/s3/s3.form.tsx @@ -84,19 +84,6 @@ export const StorageS3Form = ({ form }: StorageS3FormProps) => { )} /> - ( - - Prefix - - - - - - )} - /> ""), z.literal("").transform(() => ""), diff --git a/src/features/channel/components/storages/s3/s3.ts b/src/features/channel/components/storages/s3/s3.ts index c4c77ada..fc4672c7 100644 --- a/src/features/channel/components/storages/s3/s3.ts +++ b/src/features/channel/components/storages/s3/s3.ts @@ -15,7 +15,6 @@ type S3Config = { accessKey: string; secretKey: string; bucketName: string; - prefix?: string; port?: number; ssl?: boolean; }; @@ -31,12 +30,8 @@ async function getS3Client(config: S3Config) { }); } -function buildKey(config: S3Config, path: string) { - const prefix = config.prefix?.trim().replace(/^\/+|\/+$/g, ""); - const normalizedPath = path.replace(/^\/+/, ""); +const BASE_DIR = ""; - return prefix ? `${prefix}/${normalizedPath}` : normalizedPath; -} async function ensureBucket(config: S3Config) { const client = await getS3Client(config); @@ -51,7 +46,7 @@ export async function uploadS3( const client = await getS3Client(config); await ensureBucket(config); - const key = buildKey(config, input.data.path); + const key = `${BASE_DIR}${input.data.path}`; const file = input.data.file; let uploadStream: Readable; @@ -78,7 +73,7 @@ export async function getS3( input: { data: StorageGetInput, metadata: StorageMetaData } ): Promise { const client = await getS3Client(config); - const key = buildKey(config, input.data.path); + const key = `${BASE_DIR}${input.data.path}`; try { await client.statObject(config.bucketName, key); @@ -107,7 +102,7 @@ export async function deleteS3(config: S3Config, input: { metadata?: StorageMetaData }): Promise { const client = await getS3Client(config); - const key = buildKey(config, input.data.path); + const key = `${BASE_DIR}${input.data.path}`; try { await client.removeObject(config.bucketName, key); @@ -126,7 +121,7 @@ export async function pingS3(config: S3Config): Promise { provider: "s3", response: "Bucket does not exist" }; - const key = buildKey(config, "ping.txt"); + const key = `${BASE_DIR}ping.txt`; await client.putObject(config.bucketName, key, Buffer.from("ping")); await client.getObject(config.bucketName, key); await client.removeObject(config.bucketName, key); @@ -155,8 +150,8 @@ export async function copyS3( const client = await getS3Client(config); await ensureBucket(config); - const sourceKey = buildKey(config, input.data.from); - const destinationKey = buildKey(config, input.data.to); + const sourceKey = `${BASE_DIR}${input.data.from}`; + const destinationKey = `${BASE_DIR}${input.data.to}`; try { await client.copyObject( diff --git a/src/features/storages/utils/storages.helpers.ts b/src/features/storages/utils/storages.helpers.ts index ec05586f..de0ef02b 100644 --- a/src/features/storages/utils/storages.helpers.ts +++ b/src/features/storages/utils/storages.helpers.ts @@ -14,11 +14,16 @@ import {db} from "@/db"; import {createHash} from "crypto"; import {getServerUrl} from "@/utils/get-server-url"; import path from "path"; +import {env} from "@/env"; function computeChecksum(buffer: Buffer): string { return createHash("sha256").update(buffer).digest("hex"); } +function getBackupFilePrefix(): string { + return env.BACKUP_FILE_PREFIX?.trim().replace(/^\/+|\/+$/g, "") || "backups"; +} + export async function storeBackupFiles( backup: Backup, database: DatabaseWith, @@ -55,7 +60,7 @@ export async function storeBackupFiles( return []; } - const path = `backups/${database.project?.slug}/${fileName}`; + const path = `${getBackupFilePrefix()}/${database.project?.slug}/${fileName}`; const size = file.length; const checksum = computeChecksum(file); From 5f4293a29cae73fde2ad082e154780d6c8ce1392 Mon Sep 17 00:00:00 2001 From: antonin <18756890+Antoninj@users.noreply.github.com> Date: Sat, 18 Jul 2026 13:19:40 +0200 Subject: [PATCH 3/3] feat: pass backup prefix to agents --- src/env.mjs | 5 +++++ .../agents/utils/status/storage-channels.helpers.ts | 6 ++++++ src/features/storages/utils/storages.helpers.ts | 6 +----- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/env.mjs b/src/env.mjs index 9eb45fb5..20ddabd1 100644 --- a/src/env.mjs +++ b/src/env.mjs @@ -5,6 +5,11 @@ import packageJson from "../package.json" with {type: "json"}; const {version} = packageJson; +export function getBackupFilePrefix() { + const separator = String.fromCharCode(47); + return env.BACKUP_FILE_PREFIX?.trim().split(separator).filter(Boolean).join(separator) || "backups"; +} + export const env = createEnv({ emptyStringAsUndefined: true, server: { diff --git a/src/features/agents/utils/status/storage-channels.helpers.ts b/src/features/agents/utils/status/storage-channels.helpers.ts index c30bd373..4e0e35ef 100644 --- a/src/features/agents/utils/status/storage-channels.helpers.ts +++ b/src/features/agents/utils/status/storage-channels.helpers.ts @@ -1,11 +1,13 @@ import * as drizzleDb from "@/db"; import {db} from "@/db"; import {eq} from "drizzle-orm"; +import {getBackupFilePrefix} from "@/env"; export type PingDatabaseStorageChannels = { id: string; config: any provider: string + prefix: string } export async function getDatabaseStorageChannels(databaseId: string): Promise { @@ -24,6 +26,8 @@ export async function getDatabaseStorageChannels(databaseId: string): Promise