feat: replace Cloudinary with MinIO for all media storage

- Create server/services/minio.ts: S3-compatible service (same interface as
  cloudinaryService) using @aws-sdk/client-s3 and MINIO_ENDPOINT env var
- Replace all cloudinaryService imports with minioService across routes.ts,
  routes/reels.ts, routes/remotion.ts, services/media-purge.ts
- Replace Cloudinary logo URL pattern (res.cloudinary.com/...) with
  buildMinioUrl() helper throughout all server routes
- GET /api/cloudinary/config now returns a logoUrl field (full MinIO URL)
  so clients never need to build the URL themselves
- Update settings pages (desktop + mobile): labels changed to MinIO
  (Bucket Name, Access Key, Secret Key)
- Update image-editor.tsx: remove Cloudinary URL transformation, use logoUrl
- Update media-utils.ts: simplify getVideoThumbnailUrl (no URL transforms)
- Add MINIO_ENDPOINT and MINIO_PUBLIC_URL to docker-compose.yml + .env.example
- DB table reuse: cloudinary_config.cloud_name = bucket, api_key = access key,
  api_secret = secret key — no migration needed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Sonnet 4.6 committed 2026-03-24 21:05:37 +01:00
1 parent b0e11cb8ab
commit e39fa1026d
13 files changed
+1832 -118

No files matched your search

+3 -3
View File
@@ -8,7 +8,7 @@ import { storage } from '../storage';
import { freeSoundService, type MusicTrack } from '../services/freesound';
import { ffmpegService } from '../services/ffmpeg';
import { facebookService } from '../services/facebook';
import { cloudinaryService } from '../services/cloudinary';
import { minioService as cloudinaryService, buildMinioUrl } from '../services/minio';
import { openRouterService } from '../services/openrouter';
import { db } from '../db';
import { cloudinaryConfig } from '@shared/schema';
@@ -343,7 +343,7 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
try {
const config = await storage.getCloudinaryConfig();
if (config && config.logoPublicId) {
watermarkUrl = `https://res.cloudinary.com/${config.cloudName}/image/upload/${config.logoPublicId}`;
watermarkUrl = buildMinioUrl(config.cloudName, config.logoPublicId);
}
} catch (e) {
console.error('Error fetching watermark configuration', e);
@@ -556,7 +556,7 @@ async function processReelBackground(
try {
const config = await storage.getCloudinaryConfig();
if (config && config.logoPublicId) {
watermarkUrl = `https://res.cloudinary.com/${config.cloudName}/image/upload/${config.logoPublicId}`;
watermarkUrl = buildMinioUrl(config.cloudName, config.logoPublicId);
}
} catch (e) {
console.error('Error fetching watermark configuration', e);
+2 -2
View File
@@ -6,7 +6,7 @@ import { bundle } from "@remotion/bundler";
import { renderMedia, selectComposition } from "@remotion/renderer";
import { ffmpegService } from "../services/ffmpeg";
import { storage as dbStorage } from "../storage";
import { cloudinaryService } from "../services/cloudinary";
import { minioService as cloudinaryService, buildMinioUrl } from "../services/minio";
import { facebookService } from "../services/facebook";
import * as musicMetadata from "music-metadata";
@@ -128,7 +128,7 @@ remotionRouter.post("/render", upload.fields([{ name: "images", maxCount: 4 }, {
try {
const cloudinaryConfig = await dbStorage.getCloudinaryConfig();
if (cloudinaryConfig?.cloudName && cloudinaryConfig?.logoPublicId) {
logoUrl = `https://res.cloudinary.com/${cloudinaryConfig.cloudName}/image/upload/${cloudinaryConfig.logoPublicId}`;
logoUrl = buildMinioUrl(cloudinaryConfig.cloudName, cloudinaryConfig.logoPublicId);
console.log("🏢 Logo URL:", logoUrl);
}
} catch (e) {