feat(docker): add Docker support with configuration files and entrypoint scripts

This commit is contained in:
Yusuf İpek
2026-04-09 15:54:03 +03:00
parent b1b1715578
commit e97d60cf36
13 changed files with 658 additions and 31 deletions
+101 -14
View File
@@ -1,20 +1,109 @@
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import {
CreateBucketCommand,
HeadBucketCommand,
PutObjectCommand,
S3Client,
} from '@aws-sdk/client-s3';
const R2_ACCOUNT_ID = process.env.R2_ACCOUNT_ID!;
const R2_ACCESS_KEY_ID = process.env.R2_ACCESS_KEY_ID!;
const R2_SECRET_ACCESS_KEY = process.env.R2_SECRET_ACCESS_KEY!;
const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME!;
const R2_ACCOUNT_ID = process.env.R2_ACCOUNT_ID;
const R2_ACCESS_KEY_ID = process.env.R2_ACCESS_KEY_ID;
const R2_SECRET_ACCESS_KEY = process.env.R2_SECRET_ACCESS_KEY;
const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME ?? '';
const R2_ENDPOINT = process.env.R2_ENDPOINT;
const R2_PUBLIC_BASE_URL = process.env.R2_PUBLIC_BASE_URL;
export const r2Client = new S3Client({
region: 'auto',
endpoint: R2_ENDPOINT || `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: R2_ACCESS_KEY_ID,
secretAccessKey: R2_SECRET_ACCESS_KEY,
let cachedR2Client: S3Client | null = null;
function trimTrailingSlashes(value: string): string {
return value.replace(/\/+$/, '');
}
function requireStorageValue(name: string, value: string | undefined): string {
if (!value) {
throw new Error(`Missing ${name} for S3-compatible storage`);
}
return value;
}
function getR2Endpoint(): string {
if (R2_ENDPOINT) {
return trimTrailingSlashes(R2_ENDPOINT);
}
if (!R2_ACCOUNT_ID) {
throw new Error('Missing R2_ENDPOINT or R2_ACCOUNT_ID for S3-compatible storage');
}
return `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com`;
}
function getOrCreateR2Client(): S3Client {
if (cachedR2Client) {
return cachedR2Client;
}
cachedR2Client = new S3Client({
region: 'auto',
endpoint: getR2Endpoint(),
forcePathStyle: Boolean(R2_ENDPOINT),
credentials: {
accessKeyId: requireStorageValue('R2_ACCESS_KEY_ID', R2_ACCESS_KEY_ID),
secretAccessKey: requireStorageValue('R2_SECRET_ACCESS_KEY', R2_SECRET_ACCESS_KEY),
},
});
return cachedR2Client;
}
export const r2Client = new Proxy({} as S3Client, {
get(_target, prop, receiver) {
if (prop === 'destroy') {
return () => {
if (!cachedR2Client) return;
cachedR2Client.destroy();
cachedR2Client = null;
};
}
const client = getOrCreateR2Client();
const value = Reflect.get(client, prop, receiver);
return typeof value === 'function' ? value.bind(client) : value;
},
});
export function getR2PublicObjectUrl(key: string): string {
const sanitizedKey = key.replace(/^\/+/, '');
if (R2_PUBLIC_BASE_URL) {
return `${trimTrailingSlashes(R2_PUBLIC_BASE_URL)}/${sanitizedKey}`;
}
if (R2_ENDPOINT) {
return `${trimTrailingSlashes(R2_ENDPOINT)}/${R2_BUCKET_NAME}/${sanitizedKey}`;
}
if (!R2_ACCOUNT_ID) {
throw new Error('Missing R2_PUBLIC_BASE_URL or R2_ACCOUNT_ID for public object URLs');
}
return `https://${R2_BUCKET_NAME}.${R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${sanitizedKey}`;
}
export async function ensureR2BucketExists(): Promise<void> {
try {
await r2Client.send(new HeadBucketCommand({ Bucket: R2_BUCKET_NAME }));
return;
} catch (error) {
const statusCode = (error as { $metadata?: { httpStatusCode?: number } })?.$metadata?.httpStatusCode;
if (statusCode && statusCode !== 404 && statusCode !== 301 && statusCode !== 403) {
throw error;
}
}
await r2Client.send(new CreateBucketCommand({ Bucket: R2_BUCKET_NAME }));
}
export async function uploadAudio(
buffer: Buffer,
filename: string,
@@ -34,9 +123,7 @@ export async function uploadAudio(
})
);
// R2 public URL — uses the R2.dev subdomain or custom domain
// For development, we use the R2.dev auto-generated URL
return `https://${R2_BUCKET_NAME}.${R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${key}`;
return getR2PublicObjectUrl(key);
}
export { R2_BUCKET_NAME };