import fssync from 'fs'; import fs from 'fs/promises'; import path from 'path'; import { organizationAssets, fileUploads, type OrganizationAsset, type InsertOrganizationAsset, } from "@shared/schema"; import * as storage from "../storage/documents.storage"; import { uploadToS3, streamFromS3, isS3Enabled, S3_BUCKET } from "../utils/s3"; import { uploadsDir } from "../utils/upload"; import { db } from "../db"; import { eq, and } from "drizzle-orm"; export interface AssetListFilters { organizationId: number; type?: string; isActive?: boolean; } export interface MulterFile { path: string; originalname: string; mimetype: string; size: number; } const MIME_BY_EXT: Record = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml', }; function getMimeType(fileKeyOrName: string, fallback?: string): string { const ext = path.extname(fileKeyOrName).slice(1).toLowerCase(); return fallback || MIME_BY_EXT[ext] || 'image/png'; } export class AssetService { // ── CRUD ─────────────────────────────────────────────────────────────────── async list(filters: AssetListFilters): Promise { return storage.listOrganizationAssets(filters); } async getById(id: number, organizationId: number): Promise { return storage.getOrganizationAsset(id, organizationId); } async create(data: InsertOrganizationAsset & { file?: MulterFile }): Promise { let fileKey = data.fileKey; if (data.file) { const upload = await this.uploadAssetFile(data.file); fileKey = upload.key; } if (!fileKey) { throw new Error('Не указан файл для актива'); } return storage.createOrganizationAsset({ ...data, fileKey, }); } async update( id: number, organizationId: number, updates: Partial & { file?: MulterFile }, ): Promise { let fileKey = updates.fileKey; if (updates.file) { const existing = await storage.getOrganizationAsset(id, organizationId); if (existing?.fileKey) { await this.deleteAssetFile(existing.fileKey); } const upload = await this.uploadAssetFile(updates.file); fileKey = upload.key; } return storage.updateOrganizationAsset(id, organizationId, { ...updates, ...(fileKey ? { fileKey } : {}), }); } async delete(id: number, organizationId: number): Promise { const asset = await storage.getOrganizationAsset(id, organizationId); if (asset?.fileKey) { await this.deleteAssetFile(asset.fileKey); } await storage.deleteOrganizationAsset(id, organizationId); } // ── File handling ────────────────────────────────────────────────────────── private async uploadAssetFile(file: MulterFile): Promise<{ url: string; key: string }> { const buffer = await fs.readFile(file.path); const name = file.originalname; const mimeType = file.mimetype || getMimeType(name); if (isS3Enabled) { const result = await uploadToS3(buffer, name, mimeType); await fs.unlink(file.path).catch(() => {}); return result; } // Локальный режим: копируем файл из tmp в uploads/ const ext = path.extname(name); const key = `${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`; const dest = path.join(uploadsDir, key); await fs.copyFile(file.path, dest); await fs.unlink(file.path).catch(() => {}); return { url: `/api/files/${key}`, key }; } private async deleteAssetFile(fileKey: string): Promise { if (isS3Enabled) { const { deleteFromS3 } = await import("../utils/s3.js"); await deleteFromS3(fileKey); } else { const filePath = path.join(uploadsDir, path.basename(fileKey)); await fs.unlink(filePath).catch(() => {}); } } // ── Base64 loading ───────────────────────────────────────────────────────── async loadAsBase64(fileKey: string): Promise { const mimeType = getMimeType(fileKey); if (isS3Enabled) { const streamResult = await streamFromS3(fileKey); if (!streamResult?.body) return null; const chunks: Buffer[] = []; for await (const chunk of streamResult.body as any) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } const buffer = Buffer.concat(chunks); return `data:${mimeType};base64,${buffer.toString('base64')}`; } const filePath = path.join(uploadsDir, path.basename(fileKey)); try { const buffer = await fs.readFile(filePath); return `data:${mimeType};base64,${buffer.toString('base64')}`; } catch { return null; } } async loadByIdAsBase64(assetId: number, organizationId: number): Promise { const asset = await storage.getOrganizationAsset(assetId, organizationId); if (!asset?.fileKey) return null; return this.loadAsBase64(asset.fileKey); } async getAssetsForGeneration( placements: Array<{ assetId: number; position: string; width?: number; height?: number; opacity?: number }>, organizationId: number, ): Promise> { const result: Array<{ assetId: number; base64: string; position: string; width?: number; height?: number; opacity?: number }> = []; for (const placement of placements) { const base64 = await this.loadByIdAsBase64(placement.assetId, organizationId); if (!base64) continue; result.push({ assetId: placement.assetId, base64, position: placement.position, width: placement.width, height: placement.height, opacity: placement.opacity, }); } return result; } }