Compare commits

...

3 Commits

Author SHA1 Message Date
Oleg Proskurin b0e6304e07 fix: urls and storage structure 2025-12-26 01:10:14 +07:00
Oleg Proskurin 7e04fcbbb0 fix: storage structure 2025-12-25 23:54:18 +07:00
Oleg Proskurin 0a42a32817 feat: use CDN urls 2025-12-25 23:35:52 +07:00
7 changed files with 77 additions and 14 deletions

View File

@ -422,6 +422,8 @@ cdnRouter.get(
const generation = await genService.create({ const generation = await genService.create({
projectId: project.id, projectId: project.id,
apiKeyId: null as unknown as string, // System generation for live URLs apiKeyId: null as unknown as string, // System generation for live URLs
organizationSlug: orgSlug,
projectSlug: projectSlug,
prompt, prompt,
aspectRatio: (aspectRatio as string) || GENERATION_LIMITS.DEFAULT_ASPECT_RATIO, aspectRatio: (aspectRatio as string) || GENERATION_LIMITS.DEFAULT_ASPECT_RATIO,
autoEnhance: normalizedAutoEnhance, autoEnhance: normalizedAutoEnhance,

View File

@ -40,11 +40,11 @@ uploadRouter.post(
} }
// Extract org/project slugs from validated API key // Extract org/project slugs from validated API key
const orgId = req.apiKey?.organizationSlug || 'default'; const orgSlug = req.apiKey?.organizationSlug || process.env['DEFAULT_ORG_SLUG'] || 'default';
const projectId = req.apiKey?.projectSlug!; // Guaranteed by requireProjectKey middleware const projectSlug = req.apiKey?.projectSlug || process.env['DEFAULT_PROJECT_SLUG'] || 'main'; // Guaranteed by requireProjectKey middleware
console.log( console.log(
`[${timestamp}] [${requestId}] Starting file upload for org:${orgId}, project:${projectId}`, `[${timestamp}] [${requestId}] Starting file upload for org:${orgSlug}, project:${projectSlug}`,
); );
const file = req.file; const file = req.file;
@ -59,8 +59,8 @@ uploadRouter.post(
); );
const uploadResult = await storageService.uploadFile( const uploadResult = await storageService.uploadFile(
orgId, orgSlug,
projectId, projectSlug,
'uploads', 'uploads',
file.originalname, file.originalname,
file.buffer, file.buffer,

View File

@ -114,10 +114,14 @@ generationsRouter.post(
const projectId = req.apiKey.projectId; const projectId = req.apiKey.projectId;
const apiKeyId = req.apiKey.id; const apiKeyId = req.apiKey.id;
const organizationSlug = req.apiKey.organizationSlug || process.env['DEFAULT_ORG_SLUG'] || 'default';
const projectSlug = req.apiKey.projectSlug || process.env['DEFAULT_PROJECT_SLUG'] || 'main';
const generation = await service.create({ const generation = await service.create({
projectId, projectId,
apiKeyId, apiKeyId,
organizationSlug,
projectSlug,
prompt, prompt,
referenceImages, referenceImages,
aspectRatio, aspectRatio,

View File

@ -65,6 +65,8 @@ liveRouter.get(
const projectId = req.apiKey.projectId; const projectId = req.apiKey.projectId;
const apiKeyId = req.apiKey.id; const apiKeyId = req.apiKey.id;
const organizationSlug = req.apiKey.organizationSlug || process.env['DEFAULT_ORG_SLUG'] || 'default';
const projectSlug = req.apiKey.projectSlug || process.env['DEFAULT_PROJECT_SLUG'] || 'main';
try { try {
// Compute prompt hash for cache lookup // Compute prompt hash for cache lookup
@ -122,6 +124,8 @@ liveRouter.get(
const generation = await genService.create({ const generation = await genService.create({
projectId, projectId,
apiKeyId, apiKeyId,
organizationSlug,
projectSlug,
prompt, prompt,
aspectRatio: (aspectRatio as string) || GENERATION_LIMITS.DEFAULT_ASPECT_RATIO, aspectRatio: (aspectRatio as string) || GENERATION_LIMITS.DEFAULT_ASPECT_RATIO,
requestId: req.requestId, requestId: req.requestId,

View File

@ -268,6 +268,13 @@ export class MinioStorageService implements StorageService {
await this.client.removeObject(this.bucketName, filePath); await this.client.removeObject(this.bucketName, filePath);
} }
/**
* Get public URL for file access
* Returns CDN URL if MINIO_PUBLIC_URL is configured (production),
* otherwise falls back to API endpoint URL (development)
*
* @returns {string} URL for accessing the file
*/
getPublicUrl( getPublicUrl(
orgId: string, orgId: string,
projectId: string, projectId: string,
@ -275,9 +282,21 @@ export class MinioStorageService implements StorageService {
filename: string, filename: string,
): string { ): string {
this.validateFilePath(orgId, projectId, category, filename); this.validateFilePath(orgId, projectId, category, filename);
// Production-ready: Return API URL for presigned URL access
// If MINIO_PUBLIC_URL is configured, use direct CDN access
// This provides better performance and reduces API server load
if (this.publicUrl && process.env['USE_DIRECT_CDN'] !== 'false') {
const filePath = this.getFilePath(orgId, projectId, category, filename);
const cdnUrl = `${this.publicUrl}/${this.bucketName}/${filePath}`;
console.log(`[MinIO] Using CDN URL: ${cdnUrl}`);
return cdnUrl;
}
// Fallback to API URL for local development or when CDN is disabled
const apiBaseUrl = process.env['API_BASE_URL'] || 'http://localhost:3000'; const apiBaseUrl = process.env['API_BASE_URL'] || 'http://localhost:3000';
return `${apiBaseUrl}/api/images/${orgId}/${projectId}/${category}/${filename}`; const apiUrl = `${apiBaseUrl}/api/images/${orgId}/${projectId}/${category}/${filename}`;
console.log(`[MinIO] Using API URL: ${apiUrl}`);
return apiUrl;
} }
async getPresignedUploadUrl( async getPresignedUploadUrl(

View File

@ -1,7 +1,7 @@
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { eq, desc, count, and, isNull, inArray } from 'drizzle-orm'; import { eq, desc, count, and, isNull, inArray } from 'drizzle-orm';
import { db } from '@/db'; import { db } from '@/db';
import { generations, flows, images } from '@banatie/database'; import { generations, flows, images, projects } from '@banatie/database';
import type { import type {
Generation, Generation,
NewGeneration, NewGeneration,
@ -20,6 +20,8 @@ import type { ReferenceImage } from '@/types/api';
export interface CreateGenerationParams { export interface CreateGenerationParams {
projectId: string; projectId: string;
apiKeyId: string; apiKeyId: string;
organizationSlug: string; // For storage paths (orgSlug/projectSlug/category/file)
projectSlug: string; // For storage paths
prompt: string; prompt: string;
referenceImages?: string[] | undefined; // Aliases to resolve referenceImages?: string[] | undefined; // Aliases to resolve
aspectRatio?: string | undefined; aspectRatio?: string | undefined;
@ -151,8 +153,8 @@ export class GenerationService {
filename: `gen_${generation.id}`, filename: `gen_${generation.id}`,
referenceImages: referenceImageBuffers, referenceImages: referenceImageBuffers,
aspectRatio: params.aspectRatio || GENERATION_LIMITS.DEFAULT_ASPECT_RATIO, aspectRatio: params.aspectRatio || GENERATION_LIMITS.DEFAULT_ASPECT_RATIO,
orgId: 'default', orgId: params.organizationSlug, // Use slug for storage path
projectId: params.projectId, projectId: params.projectSlug, // Use slug for storage path
meta: params.meta || {}, meta: params.meta || {},
}); });
@ -377,6 +379,27 @@ export class GenerationService {
} }
} }
/**
* Get organization and project slugs for storage paths
*/
private async getSlugs(projectId: string): Promise<{ orgSlug: string; projectSlug: string }> {
const project = await db.query.projects.findFirst({
where: eq(projects.id, projectId),
with: {
organization: true,
},
});
if (!project) {
throw new Error('Project not found');
}
return {
orgSlug: project.organization.slug,
projectSlug: project.slug,
};
}
private async updateStatus( private async updateStatus(
id: string, id: string,
status: 'pending' | 'processing' | 'success' | 'failed', status: 'pending' | 'processing' | 'success' | 'failed',
@ -491,14 +514,17 @@ export class GenerationService {
// Update status to processing // Update status to processing
await this.updateStatus(id, 'processing'); await this.updateStatus(id, 'processing');
// Get slugs for storage paths
const { orgSlug, projectSlug } = await this.getSlugs(generation.projectId);
// Use EXACT same parameters as original (no overrides) // Use EXACT same parameters as original (no overrides)
const genResult = await this.imageGenService.generateImage({ const genResult = await this.imageGenService.generateImage({
prompt: generation.prompt, prompt: generation.prompt,
filename: `gen_${id}`, filename: `gen_${id}`,
referenceImages: [], // TODO: Re-resolve referenced images if needed referenceImages: [], // TODO: Re-resolve referenced images if needed
aspectRatio: generation.aspectRatio || GENERATION_LIMITS.DEFAULT_ASPECT_RATIO, aspectRatio: generation.aspectRatio || GENERATION_LIMITS.DEFAULT_ASPECT_RATIO,
orgId: 'default', orgId: orgSlug,
projectId: generation.projectId, projectId: projectSlug,
meta: generation.meta as Record<string, unknown> || {}, meta: generation.meta as Record<string, unknown> || {},
}); });
@ -605,14 +631,17 @@ export class GenerationService {
const promptToUse = updates.prompt || generation.prompt; const promptToUse = updates.prompt || generation.prompt;
const aspectRatioToUse = updates.aspectRatio || generation.aspectRatio || GENERATION_LIMITS.DEFAULT_ASPECT_RATIO; const aspectRatioToUse = updates.aspectRatio || generation.aspectRatio || GENERATION_LIMITS.DEFAULT_ASPECT_RATIO;
// Get slugs for storage paths
const { orgSlug, projectSlug } = await this.getSlugs(generation.projectId);
// Regenerate image // Regenerate image
const genResult = await this.imageGenService.generateImage({ const genResult = await this.imageGenService.generateImage({
prompt: promptToUse, prompt: promptToUse,
filename: `gen_${id}`, filename: `gen_${id}`,
referenceImages: [], referenceImages: [],
aspectRatio: aspectRatioToUse, aspectRatio: aspectRatioToUse,
orgId: 'default', orgId: orgSlug,
projectId: generation.projectId, projectId: projectSlug,
meta: updates.meta || generation.meta || {}, meta: updates.meta || generation.meta || {},
}); });

View File

@ -34,6 +34,11 @@ STORAGE_TYPE=minio
# Public URL for CDN access (used in API responses) # Public URL for CDN access (used in API responses)
MINIO_PUBLIC_URL=https://cdn.banatie.app MINIO_PUBLIC_URL=https://cdn.banatie.app
# Use direct CDN URLs instead of API proxy (recommended for production)
# Set to 'false' to force API URLs even when MINIO_PUBLIC_URL is configured
# Default: true (CDN enabled when MINIO_PUBLIC_URL is present)
USE_DIRECT_CDN=true
# ---------------------------------------- # ----------------------------------------
# API Configuration # API Configuration
# ---------------------------------------- # ----------------------------------------