main
Some checks failed
Backend Deploy 🚀 / build-and-deploy (push) Has been cancelled

This commit is contained in:
Harun CAN
2026-03-29 12:43:49 +03:00
parent 829413f05d
commit 85c35c73e8
41 changed files with 6127 additions and 36 deletions

View File

@@ -0,0 +1,204 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as path from 'path';
import * as fs from 'fs/promises';
import * as crypto from 'crypto';
/**
* Storage Service — Medya dosyalarının yönetimi
*
* Strateji: file-organizer skill'inden elde edilen bilgilerle tasarlandı
* - Geliştirme ortamı: Lokal dosya sistemi (/data/media/)
* - Üretim ortamı: Cloudflare R2 / AWS S3
*
* Dosya yapısı:
* /data/media/
* ├── {projectId}/
* │ ├── scenes/
* │ │ ├── scene-001-video.mp4
* │ │ ├── scene-001-audio.mp3
* │ │ └── scene-002-video.mp4
* │ ├── audio/
* │ │ ├── narration.mp3
* │ │ └── music.mp3
* │ ├── output/
* │ │ ├── final-video.mp4
* │ │ └── thumbnail.jpg
* │ └── subtitles/
* │ └── captions.srt
* └── temp/ (otomatik temizlenir)
*/
export interface UploadResult {
key: string;
url: string;
bucket: string;
sizeBytes: number;
mimeType: string;
}
export interface StorageConfig {
provider: 'local' | 's3' | 'r2';
basePath: string;
bucket: string;
cdnUrl?: string;
}
@Injectable()
export class StorageService {
private readonly logger = new Logger(StorageService.name);
private readonly config: StorageConfig;
constructor(private readonly configService: ConfigService) {
const provider = this.configService.get<string>('STORAGE_PROVIDER', 'local');
this.config = {
provider: provider as StorageConfig['provider'],
basePath: this.configService.get<string>('STORAGE_LOCAL_PATH', './data/media'),
bucket: this.configService.get<string>('STORAGE_BUCKET', 'contentgen-media'),
cdnUrl: this.configService.get<string>('STORAGE_CDN_URL'),
};
this.logger.log(`📦 Storage provider: ${this.config.provider}`);
}
/**
* Sahne videosu için anahtar oluştur
*/
getSceneVideoKey(projectId: string, sceneOrder: number): string {
return `${projectId}/scenes/scene-${String(sceneOrder).padStart(3, '0')}-video.mp4`;
}
/**
* Sahne ses kaydı için anahtar oluştur
*/
getSceneAudioKey(projectId: string, sceneOrder: number): string {
return `${projectId}/audio/scene-${String(sceneOrder).padStart(3, '0')}-narration.mp3`;
}
/**
* Final video için anahtar oluştur
*/
getFinalVideoKey(projectId: string): string {
const hash = crypto.randomBytes(4).toString('hex');
return `${projectId}/output/final-${hash}.mp4`;
}
/**
* Thumbnail için anahtar oluştur
*/
getThumbnailKey(projectId: string): string {
return `${projectId}/output/thumbnail.jpg`;
}
/**
* Altyazı dosyası için anahtar oluştur
*/
getSubtitleKey(projectId: string): string {
return `${projectId}/subtitles/captions.srt`;
}
/**
* Müzik dosyası için anahtar oluştur
*/
getMusicKey(projectId: string): string {
return `${projectId}/audio/background-music.mp3`;
}
/**
* Dosya yükle
*/
async upload(key: string, data: Buffer, mimeType: string): Promise<UploadResult> {
if (this.config.provider === 'local') {
return this.uploadLocal(key, data, mimeType);
}
// S3/R2 desteği sonra eklenecek
return this.uploadLocal(key, data, mimeType);
}
/**
* Dosya indir
*/
async download(key: string): Promise<Buffer> {
if (this.config.provider === 'local') {
return this.downloadLocal(key);
}
return this.downloadLocal(key);
}
/**
* Dosya sil
*/
async delete(key: string): Promise<void> {
if (this.config.provider === 'local') {
return this.deleteLocal(key);
}
return this.deleteLocal(key);
}
/**
* Proje dosyalarını temizle
*/
async cleanupProject(projectId: string): Promise<void> {
const projectDir = path.join(this.config.basePath, projectId);
try {
await fs.rm(projectDir, { recursive: true, force: true });
this.logger.log(`🗑️ Proje dosyaları silindi: ${projectId}`);
} catch (error) {
this.logger.warn(`Proje temizleme hatası: ${projectId}${error}`);
}
}
/**
* Dosyanın public URL'ini oluştur
*/
getPublicUrl(key: string): string {
if (this.config.cdnUrl) {
return `${this.config.cdnUrl}/${key}`;
}
if (this.config.provider === 'local') {
return `/media/${key}`;
}
return `https://${this.config.bucket}.r2.dev/${key}`;
}
// ── Private: Lokal dosya sistemi ──────────────────────────────────
private async uploadLocal(key: string, data: Buffer, mimeType: string): Promise<UploadResult> {
const filePath = path.join(this.config.basePath, key);
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(filePath, data);
this.logger.debug(`📥 Dosya yüklendi: ${key} (${data.length} bytes)`);
return {
key,
url: this.getPublicUrl(key),
bucket: this.config.bucket,
sizeBytes: data.length,
mimeType,
};
}
private async downloadLocal(key: string): Promise<Buffer> {
const filePath = path.join(this.config.basePath, key);
return fs.readFile(filePath);
}
private async deleteLocal(key: string): Promise<void> {
const filePath = path.join(this.config.basePath, key);
try {
await fs.unlink(filePath);
} catch {
// Dosya bulunamadı — sessizce geç
}
}
}