feat: support large file uploads up to 4GB (disk storage + streaming)

This commit is contained in:
Michael committed 2026-01-23 16:27:27 +01:00
1 parent 427c0b290e
commit 891b374500
4 files changed
+71 -25

No files matched your search

+1 -1
View File
@@ -237,7 +237,7 @@ export default function NewReel() {
'video/x-msvideo': ['.avi'], 'video/x-msvideo': ['.avi'],
'video/*': [], // Fallback 'video/*': [], // Fallback
}, },
maxSize: 500 * 1024 * 1024, // 500MB pour supporter les vidéos 4K maxSize: 4 * 1024 * 1024 * 1024, // 4GB Limit
noClick: true, noClick: true,
noKeyboard: true, noKeyboard: true,
}); });
+34 -6
View File
@@ -1,5 +1,7 @@
import type { Express, Request, Response, NextFunction } from "express"; import type { Express, Request, Response, NextFunction } from "express";
import { createServer, type Server } from "http"; import { createServer, type Server } from "http";
import fs from "fs";
import os from "os";
import { storage } from "./storage"; import { storage } from "./storage";
import { db } from "./db"; import { db } from "./db";
import multer from "multer"; import multer from "multer";
@@ -20,14 +22,25 @@ const ALLOWED_MIME_TYPES = [
]; ];
// Configuration multer avec validation de taille et type // Configuration multer avec validation de taille et type
// Configuration multer avec validation de taille (4GB) et stockage disque temporaire
const upload = multer({ const upload = multer({
storage: multer.memoryStorage(), storage: multer.diskStorage({
destination: os.tmpdir(),
filename: (req, file, cb) => {
// Nettoyer le nom de fichier pour éviter les problèmes d'encodage
const safeName = file.originalname.replace(/[^a-zA-Z0-9.]/g, '_');
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, uniqueSuffix + '-' + safeName);
}
}),
limits: { limits: {
fileSize: 50 * 1024 * 1024, // 50MB max fileSize: 4 * 1024 * 1024 * 1024, // 4GB max
files: 10 // 10 fichiers max files: 10 // 10 fichiers max
}, },
fileFilter: (req, file, cb) => { fileFilter: (req, file, cb) => {
if (ALLOWED_MIME_TYPES.includes(file.mimetype)) { // Accepter plus de formats vidéo si nécessaire (ex: mkv, avi) pour le transcodage
// Mais on garde les limites actuelles pour l'instant
if (ALLOWED_MIME_TYPES.includes(file.mimetype) || file.mimetype === 'application/octet-stream') { // iOS envoie parfois octet-stream
cb(null, true); cb(null, true);
} else { } else {
cb(new Error(`Type de fichier non autorisé: ${file.mimetype}`)); cb(new Error(`Type de fichier non autorisé: ${file.mimetype}`));
@@ -498,6 +511,8 @@ export async function registerRoutes(app: Express): Promise<Server> {
return res.status(400).json({ error: "No file uploaded" }); return res.status(400).json({ error: "No file uploaded" });
} }
console.log(`📂 Processing upload: ${req.file.originalname} (${(req.file.size / 1024 / 1024).toFixed(2)} MB)`);
const user = req.user as User; const user = req.user as User;
const userId = user.id; const userId = user.id;
@@ -505,22 +520,29 @@ export async function registerRoutes(app: Express): Promise<Server> {
const cloudinaryConfig = await storage.getAnyCloudinaryConfig(); const cloudinaryConfig = await storage.getAnyCloudinaryConfig();
if (!cloudinaryConfig) { if (!cloudinaryConfig) {
// Nettoyer le fichier temporaire
await fs.promises.unlink(req.file.path).catch(console.error);
return res.status(400).json({ return res.status(400).json({
error: "Cloudinary not configured. Please ask an administrator to configure Cloudinary in Settings first." error: "Cloudinary not configured. Please ask an administrator to configure Cloudinary in Settings first."
}); });
} }
// Upload to Cloudinary (service will use shared config internally) // Upload to Cloudinary using file path (streamed from disk)
// req.file.path est disponible avec DiskStorage
const uploadResult = await cloudinaryService.uploadMedia( const uploadResult = await cloudinaryService.uploadMedia(
req.file.buffer, req.file.path,
req.file.originalname, req.file.originalname,
userId, userId,
req.file.mimetype req.file.mimetype
); );
// Clean up temp file immediately after upload to free disk space
await fs.promises.unlink(req.file.path).catch(err => console.error("Failed to cleanup temp file:", err));
const mediaItem = await storage.createMedia({ const mediaItem = await storage.createMedia({
userId, userId,
type: req.file.mimetype.startsWith("video/") ? "video" : "image", // Détection basique basée sur le mimetype ou le résultat Cloudinary
type: req.file.mimetype.startsWith("video/") || req.file.originalname.match(/\.(mp4|mov|avi|mkv)$/i) ? "video" : "image",
cloudinaryPublicId: uploadResult.publicId, cloudinaryPublicId: uploadResult.publicId,
originalUrl: uploadResult.originalUrl, originalUrl: uploadResult.originalUrl,
facebookFeedUrl: uploadResult.facebookFeedUrl, facebookFeedUrl: uploadResult.facebookFeedUrl,
@@ -533,6 +555,12 @@ export async function registerRoutes(app: Express): Promise<Server> {
res.json(mediaItem); res.json(mediaItem);
} catch (error) { } catch (error) {
console.error("Error uploading media:", error); console.error("Error uploading media:", error);
// Attempt cleanup on error
if (req.file && req.file.path) {
await fs.promises.unlink(req.file.path).catch(() => { });
}
const errorMessage = error instanceof Error ? error.message : "Failed to upload media"; const errorMessage = error instanceof Error ? error.message : "Failed to upload media";
res.status(500).json({ error: errorMessage }); res.status(500).json({ error: errorMessage });
} }
+30 -18
View File
@@ -3,7 +3,7 @@ import { storage } from '../storage';
class CloudinaryService { class CloudinaryService {
async uploadMedia( async uploadMedia(
buffer: Buffer, file: Buffer | string,
fileName: string, fileName: string,
userId: string, userId: string,
mimeType: string mimeType: string
@@ -32,30 +32,42 @@ class CloudinaryService {
const isVideo = mimeType.startsWith('video/'); const isVideo = mimeType.startsWith('video/');
// Define eager transformations for images (generated during upload) // Define eager transformations for images (generated during upload)
// Note: Eager transformations are not applied to videos in this flow to avoid timeouts or huge processing
const eagerTransformations = !isVideo ? [ const eagerTransformations = !isVideo ? [
{ width: 1080, height: 1080, crop: 'fill', gravity: 'center' }, // Facebook Feed (square, no borders) { width: 1080, height: 1080, crop: 'fill', gravity: 'center' }, // Facebook Feed (square, no borders)
{ width: 1080, height: 1080, crop: 'pad', gravity: 'center', background: 'auto' }, // Instagram Feed { width: 1080, height: 1080, crop: 'pad', gravity: 'center', background: 'auto' }, // Instagram Feed
{ width: 1080, height: 1920, crop: 'pad', gravity: 'center', background: 'auto' } // Instagram Story { width: 1080, height: 1920, crop: 'pad', gravity: 'center', background: 'auto' } // Instagram Story
] : undefined; ] : undefined;
// Upload to Cloudinary with eager transformations const uploadOptions = {
const uploadResult = await new Promise<any>((resolve, reject) => { folder: 'social-flow',
const uploadStream = cloudinary.uploader.upload_stream( public_id: `${Date.now()}-${fileName.replace(/\.[^/.]+$/, '')}`,
{ resource_type: isVideo ? 'video' : 'image',
folder: 'social-flow', eager: eagerTransformations,
public_id: `${Date.now()}-${fileName.replace(/\.[^/.]+$/, '')}`, eager_async: false, // Generate transformations immediately
resource_type: isVideo ? 'video' : 'image', };
eager: eagerTransformations,
eager_async: false, // Generate transformations immediately
},
(error, result) => {
if (error) reject(error);
else resolve(result);
}
);
uploadStream.end(buffer); let uploadResult;
});
if (typeof file === 'string') {
// Upload from file path (streamed by SDK)
uploadResult = await cloudinary.uploader.upload(file, {
...uploadOptions,
resource_type: isVideo ? 'video' : 'image', // Type assertion for SDK
} as any);
} else {
// Upload from buffer (memory)
uploadResult = await new Promise<any>((resolve, reject) => {
const uploadStream = cloudinary.uploader.upload_stream(
uploadOptions as any,
(error, result) => {
if (error) reject(error);
else resolve(result);
}
);
uploadStream.end(file);
});
}
const publicId = uploadResult.public_id; const publicId = uploadResult.public_id;
const originalUrl = uploadResult.secure_url; const originalUrl = uploadResult.secure_url;
+6
View File
@@ -97,6 +97,12 @@ Ajout d'une fonctionnalité complète de création de Reels Facebook permettant
- [x] Afficher les étapes (Upload, Audio, Stabilisation, Encodage) - [x] Afficher les étapes (Upload, Audio, Stabilisation, Encodage)
- [x] Estimer le temps restant selon les options activées (Stabilisation = +30s) - [x] Estimer le temps restant selon les options activées (Stabilisation = +30s)
### Phase 9: Maintenance & Bug Fixes 🛠️
- [x] **Fix API 500** (`server/routes/reels.ts`) : Variable `stabilize` manquante corrigée.
- [x] **Fix iOS Upload** (`client/src/pages/new-reel.tsx`) : Ajout support `.mov` et augmentation limite taille (500MB).
- [ ] **Support Gros Fichiers (4GB)** : Passage à DiskStorage et streaming Cloudinary.
## Progress Log ## Progress Log
- **22 Jan 2026** - Analyse complète et PRD créé - **22 Jan 2026** - Analyse complète et PRD créé