diff --git a/docker-compose.yml b/docker-compose.yml index d258d64..407af57 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -65,6 +65,7 @@ services: MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} MINIO_BROWSER_REDIRECT_URL: http://localhost:9001 MINIO_SERVER_URL: http://localhost:9000 + MINIO_DOMAIN: localhost # CRITICAL: SNMD command for full S3 compatibility command: server /data{1...4} --console-address ":9001" healthcheck: diff --git a/docs/api/api.rest b/docs/api/api.rest index 715801b..8e1fa5c 100644 --- a/docs/api/api.rest +++ b/docs/api/api.rest @@ -1,5 +1,11 @@ @base = http://localhost:3000 + +### Health + +GET {{base}}/health + + ### Info GET {{base}}/api/info @@ -30,15 +36,8 @@ POST {{base}}/api/text-to-image Content-Type: application/json { - "prompt": "банановый стимпанк. много стимпанк машин и меаханизмов посвященных бананм и работающих на бананах. банановая феерия", - "filename": "banatie-party", - "autoEnhance": true, - "enhancementOptions": { - "imageStyle": "photorealistic", - "aspectRatio": "landscape", - "mood": "peaceful", - "lighting": "golden hour" - } + "prompt": "A majestic eagle soaring over snow-capped mountains", + "filename": "test-eagle" } diff --git a/src/routes/images.ts b/src/routes/images.ts index 97ed862..c4bcfce 100644 --- a/src/routes/images.ts +++ b/src/routes/images.ts @@ -24,54 +24,37 @@ imagesRouter.get( const storageService = StorageFactory.getInstance(); try { - // Method 1: Redirect to presigned URL (24 hour expiry) - const presignedUrl = await storageService.getPresignedDownloadUrl( + // Stream the file directly through our API (more reliable than presigned URL redirects) + const fileBuffer = await storageService.downloadFile( orgId, projectId, category as 'uploads' | 'generated' | 'references', - filename, - 24 * 60 * 60 // 24 hours + filename ); - // Redirect to the presigned URL - return res.redirect(302, presignedUrl); + // Determine content type from filename + const ext = filename.toLowerCase().split('.').pop(); + const contentType = { + 'png': 'image/png', + 'jpg': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'gif': 'image/gif', + 'webp': 'image/webp', + 'svg': 'image/svg+xml' + }[ext || ''] || 'application/octet-stream'; + + res.setHeader('Content-Type', contentType); + res.setHeader('Cache-Control', 'public, max-age=86400'); // 24 hours + res.setHeader('Content-Length', fileBuffer.length); + + return res.send(fileBuffer); } catch (error) { - console.error('Failed to generate presigned URL:', error); - - try { - // Method 2: Fallback - Stream the file directly through our API - const fileBuffer = await storageService.downloadFile( - orgId, - projectId, - category as 'uploads' | 'generated' | 'references', - filename - ); - - // Determine content type from filename - const ext = filename.toLowerCase().split('.').pop(); - const contentType = { - 'png': 'image/png', - 'jpg': 'image/jpeg', - 'jpeg': 'image/jpeg', - 'gif': 'image/gif', - 'webp': 'image/webp', - 'svg': 'image/svg+xml' - }[ext || ''] || 'application/octet-stream'; - - res.setHeader('Content-Type', contentType); - res.setHeader('Cache-Control', 'public, max-age=86400'); // 24 hours - res.setHeader('Content-Length', fileBuffer.length); - - return res.send(fileBuffer); - - } catch (streamError) { - console.error('Failed to stream file:', streamError); - return res.status(404).json({ - success: false, - message: 'File not found' - }); - } + console.error('Failed to stream file:', error); + return res.status(404).json({ + success: false, + message: 'File not found' + }); } }) ); diff --git a/src/services/MinioStorageService.ts b/src/services/MinioStorageService.ts index 97622da..07b925d 100644 --- a/src/services/MinioStorageService.ts +++ b/src/services/MinioStorageService.ts @@ -181,7 +181,20 @@ export class MinioStorageService implements StorageService { expirySeconds: number = 86400 // 24 hours default ): Promise { const filePath = this.getFilePath(orgId, projectId, category, filename); - return await this.client.presignedGetObject(this.bucketName, filePath, expirySeconds); + const presignedUrl = await this.client.presignedGetObject(this.bucketName, filePath, expirySeconds); + + // Replace internal Docker hostname with public URL if configured + if (this.publicUrl) { + const clientEndpoint = this.client.host + (this.client.port ? `:${this.client.port}` : ''); + const publicEndpoint = this.publicUrl.replace(/^https?:\/\//, ''); + + return presignedUrl.replace( + `${this.client.protocol}//${clientEndpoint}`, + this.publicUrl + ); + } + + return presignedUrl; } async listProjectFiles(