Files
Socialflow/server/services/minio.ts
T
MichaelandClaude Sonnet 4.6 b353fb6ded fix: use relative URLs for local media storage to fix CSP blocking
Media files were stored with absolute http://localhost:5555/... URLs,
which got blocked by the Content Security Policy when the app runs at
https://socialflow.fnancy.fr. Now stores relative /uploads/... paths
and resolves to absolute URLs only when needed by external APIs
(Facebook, FFmpeg internal service).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 08:30:07 +01:00

108 lines
3.6 KiB
TypeScript

import fs from 'fs';
import path from 'path';
const UPLOADS_BASE = path.resolve(process.cwd(), 'uploads');
function ensureDir(dir: string): void {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
/**
* Builds the public URL for a locally stored file.
* Returns a relative path so it works on any domain/protocol.
* objectKey is the relative path from uploads/ (e.g. "media/123-file.jpg").
*/
export function buildMinioUrl(_bucketName: string, objectKey: string, _publicUrlOverride?: string | null): string {
return `/uploads/${objectKey}`;
}
/**
* Resolves a possibly-relative URL to an absolute URL for use by external
* services (Facebook API, Instagram API) that must download the file.
* Uses APP_URL env var (public-facing domain).
*/
export function resolvePublicUrl(url: string): string {
if (!url || !url.startsWith('/')) return url;
const base = (process.env.APP_URL || 'http://localhost:5555').replace(/\/$/, '');
return `${base}${url}`;
}
/**
* Resolves a possibly-relative URL to an absolute URL for use by internal
* Docker services (FFmpeg, etc.) that communicate via internal Docker network.
* Uses INTERNAL_APP_URL env var.
*/
export function resolveInternalUrl(url: string): string {
if (!url || !url.startsWith('/')) return url;
const base = (process.env.INTERNAL_APP_URL || process.env.APP_URL || 'http://localhost:5555').replace(/\/$/, '');
return `${base}${url}`;
}
class LocalStorageService {
async uploadMedia(
file: Buffer | string,
fileName: string,
_userId: string,
_mimeType: string
): Promise<{
publicId: string;
originalUrl: string;
facebookFeedUrl: string | null;
instagramFeedUrl: string | null;
instagramStoryUrl: string | null;
}> {
const dir = path.join(UPLOADS_BASE, 'media');
ensureDir(dir);
const safeName = path.basename(fileName).replace(/[^a-zA-Z0-9._-]/g, '-');
const objectKey = `media/${Date.now()}-${safeName}`;
const filePath = path.join(UPLOADS_BASE, objectKey);
const body: Buffer = typeof file === 'string' ? fs.readFileSync(file) : file;
fs.writeFileSync(filePath, body);
const url = buildMinioUrl('', objectKey);
return { publicId: objectKey, originalUrl: url, facebookFeedUrl: url, instagramFeedUrl: url, instagramStoryUrl: url };
}
async deleteMedia(publicId: string, _userId: string, _mediaType: 'image' | 'video'): Promise<void> {
const filePath = path.join(UPLOADS_BASE, publicId);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
}
async uploadStoryImageWithText(buffer: Buffer, originalFileName: string): Promise<string> {
const dir = path.join(UPLOADS_BASE, 'stories');
ensureDir(dir);
const safeName = path.basename(originalFileName).replace(/[^a-zA-Z0-9._-]/g, '-');
const objectKey = `stories/story-${Date.now()}-${safeName}`;
fs.writeFileSync(path.join(UPLOADS_BASE, objectKey), buffer);
return buildMinioUrl('', objectKey);
}
async uploadLogo(buffer: Buffer, fileName: string): Promise<{ publicId: string; url: string }> {
const dir = path.join(UPLOADS_BASE, 'logos');
ensureDir(dir);
const ext = path.extname(fileName) || '.png';
const objectKey = `logos/logo-${Date.now()}${ext}`;
fs.writeFileSync(path.join(UPLOADS_BASE, objectKey), buffer);
return { publicId: objectKey, url: buildMinioUrl('', objectKey) };
}
async deleteLogo(publicId: string): Promise<void> {
const filePath = path.join(UPLOADS_BASE, publicId);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
}
}
export const minioService = new LocalStorageService();