Integrate Cloudinary for cloud-based image and video storage

Implement Cloudinary integration for uploading, storing, and transforming media files, including a new settings section for user configuration and API endpoints for media management.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/p0c5kxJ
This commit is contained in:
michaelschal committed 2025-10-01 14:14:59 +00:00
1 parent 66e20a83e7
commit bee6f53f23
6 files changed
+363 -35

No files matched your search

+104 -16
View File
@@ -3,16 +3,12 @@ import { createServer, type Server } from "http";
import { storage } from "./storage";
import multer from "multer";
import { openRouterService } from "./services/openrouter";
import { imageProcessor } from "./services/imageProcessor";
import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema } from "@shared/schema";
import express from "express";
import path from "path";
import { cloudinaryService } from "./services/cloudinary";
import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema, insertCloudinaryConfigSchema } from "@shared/schema";
const upload = multer({ storage: multer.memoryStorage() });
export async function registerRoutes(app: Express): Promise<Server> {
// Serve uploaded files
app.use("/uploads", express.static(path.join(process.cwd(), "uploads")));
// Stats endpoint
app.get("/api/stats", async (req, res) => {
@@ -69,21 +65,32 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
const userId = "demo-user"; // In real app, get from session/auth
// Check if Cloudinary is configured
const cloudinaryConfig = await storage.getCloudinaryConfig(userId);
const originalUrl = await imageProcessor.saveOriginalFile(req.file.buffer, req.file.originalname);
let processedUrls = null;
if (req.file.mimetype.startsWith("image/")) {
processedUrls = await imageProcessor.processImage(req.file.buffer, req.file.originalname);
if (!cloudinaryConfig) {
return res.status(400).json({
error: "Cloudinary not configured. Please configure Cloudinary in Settings first."
});
}
// Upload to Cloudinary
const uploadResult = await cloudinaryService.uploadMedia(
req.file.buffer,
req.file.originalname,
userId,
req.file.mimetype
);
const mediaItem = await storage.createMedia({
userId,
type: req.file.mimetype.startsWith("video/") ? "video" : "image",
originalUrl,
facebookFeedUrl: processedUrls?.facebookFeed || null,
instagramFeedUrl: processedUrls?.instagramFeed || null,
instagramStoryUrl: processedUrls?.instagramStory || null,
cloudinaryPublicId: uploadResult.publicId,
originalUrl: uploadResult.originalUrl,
facebookFeedUrl: uploadResult.facebookFeedUrl,
instagramFeedUrl: uploadResult.instagramFeedUrl,
instagramStoryUrl: uploadResult.instagramStoryUrl,
fileName: req.file.originalname,
fileSize: req.file.size,
});
@@ -91,7 +98,8 @@ export async function registerRoutes(app: Express): Promise<Server> {
res.json(mediaItem);
} catch (error) {
console.error("Error uploading media:", error);
res.status(500).json({ error: "Failed to upload media" });
const errorMessage = error instanceof Error ? error.message : "Failed to upload media";
res.status(500).json({ error: errorMessage });
}
});
@@ -107,6 +115,39 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
// Delete media
app.delete("/api/media/:id", async (req, res) => {
try {
const userId = "demo-user"; // In real app, get from session/auth
const mediaId = req.params.id;
// Get media to find cloudinary public ID
const allMedia = await storage.getMedia(userId);
const mediaToDelete = allMedia.find(m => m.id === mediaId);
if (!mediaToDelete) {
return res.status(404).json({ error: "Media not found" });
}
// Delete from Cloudinary
if (mediaToDelete.cloudinaryPublicId) {
await cloudinaryService.deleteMedia(
mediaToDelete.cloudinaryPublicId,
userId,
mediaToDelete.type
);
}
// Delete from database
await storage.deleteMedia(mediaId);
res.json({ success: true });
} catch (error) {
console.error("Error deleting media:", error);
res.status(500).json({ error: "Failed to delete media" });
}
});
// Posts
app.get("/api/posts", async (req, res) => {
try {
@@ -195,6 +236,53 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
// Cloudinary Config
app.get("/api/cloudinary/config", async (req, res) => {
try {
const userId = "demo-user"; // In real app, get from session/auth
const config = await storage.getCloudinaryConfig(userId);
if (!config) {
return res.json(null);
}
// Don't send the API secret to the client
const { apiSecret, ...safeConfig } = config;
res.json(safeConfig);
} catch (error) {
console.error("Error fetching Cloudinary config:", error);
res.status(500).json({ error: "Failed to fetch Cloudinary config" });
}
});
app.post("/api/cloudinary/config", async (req, res) => {
try {
const userId = "demo-user"; // In real app, get from session/auth
const configData = insertCloudinaryConfigSchema.parse({
...req.body,
userId,
});
// Check if config already exists
const existingConfig = await storage.getCloudinaryConfig(userId);
let config;
if (existingConfig) {
config = await storage.updateCloudinaryConfig(userId, configData);
} else {
config = await storage.createCloudinaryConfig(configData);
}
// Don't send the API secret back
const { apiSecret, ...safeConfig } = config;
res.json(safeConfig);
} catch (error) {
console.error("Error saving Cloudinary config:", error);
res.status(500).json({ error: "Failed to save Cloudinary config" });
}
});
const httpServer = createServer(app);
return httpServer;
+106
View File
@@ -0,0 +1,106 @@
import { v2 as cloudinary } from 'cloudinary';
import { storage } from '../storage';
class CloudinaryService {
async uploadMedia(
buffer: Buffer,
fileName: string,
userId: string,
mimeType: string
): Promise<{
publicId: string;
originalUrl: string;
facebookFeedUrl: string | null;
instagramFeedUrl: string | null;
instagramStoryUrl: string | null;
}> {
// Get user's Cloudinary config
const config = await storage.getCloudinaryConfig(userId);
if (!config) {
throw new Error('Cloudinary configuration not found. Please configure Cloudinary in Settings.');
}
// Configure Cloudinary with user's credentials
cloudinary.config({
cloud_name: config.cloudName,
api_key: config.apiKey,
api_secret: config.apiSecret,
secure: true,
});
const isVideo = mimeType.startsWith('video/');
// Upload to Cloudinary
const uploadResult = await new Promise<any>((resolve, reject) => {
const uploadStream = cloudinary.uploader.upload_stream(
{
folder: 'social-flow',
public_id: `${Date.now()}-${fileName.replace(/\.[^/.]+$/, '')}`,
resource_type: isVideo ? 'video' : 'image',
},
(error, result) => {
if (error) reject(error);
else resolve(result);
}
);
uploadStream.end(buffer);
});
const publicId = uploadResult.public_id;
const originalUrl = uploadResult.secure_url;
// Generate transformation URLs only for images
let facebookFeedUrl = null;
let instagramFeedUrl = null;
let instagramStoryUrl = null;
if (!isVideo) {
facebookFeedUrl = cloudinary.url(publicId, {
transformation: [
{ width: 1200, height: 630, crop: 'fill', gravity: 'auto' }
]
});
instagramFeedUrl = cloudinary.url(publicId, {
transformation: [
{ width: 1080, height: 1080, crop: 'fill', gravity: 'auto' }
]
});
instagramStoryUrl = cloudinary.url(publicId, {
transformation: [
{ width: 1080, height: 1920, crop: 'fill', gravity: 'auto' }
]
});
}
return {
publicId,
originalUrl,
facebookFeedUrl,
instagramFeedUrl,
instagramStoryUrl,
};
}
async deleteMedia(publicId: string, userId: string, mediaType: 'image' | 'video'): Promise<void> {
const config = await storage.getCloudinaryConfig(userId);
if (!config) {
throw new Error('Cloudinary configuration not found');
}
cloudinary.config({
cloud_name: config.cloudName,
api_key: config.apiKey,
api_secret: config.apiSecret,
secure: true,
});
await cloudinary.uploader.destroy(publicId, {
resource_type: mediaType,
});
}
}
export const cloudinaryService = new CloudinaryService();
+27
View File
@@ -5,6 +5,7 @@ import {
posts,
scheduledPosts,
aiGenerations,
cloudinaryConfig,
type User,
type InsertUser,
type SocialPage,
@@ -17,6 +18,8 @@ import {
type InsertScheduledPost,
type AiGeneration,
type InsertAiGeneration,
type CloudinaryConfig,
type InsertCloudinaryConfig,
} from "@shared/schema";
import { db } from "./db";
import { eq, and, gte, lte, desc, isNull } from "drizzle-orm";
@@ -58,6 +61,11 @@ export interface IStorage {
// AI Generations
getAiGenerations(userId: string): Promise<AiGeneration[]>;
createAiGeneration(generation: InsertAiGeneration): Promise<AiGeneration>;
// Cloudinary Config
getCloudinaryConfig(userId: string): Promise<CloudinaryConfig | undefined>;
createCloudinaryConfig(config: InsertCloudinaryConfig): Promise<CloudinaryConfig>;
updateCloudinaryConfig(userId: string, config: Partial<InsertCloudinaryConfig>): Promise<CloudinaryConfig>;
}
export class DatabaseStorage implements IStorage {
@@ -208,6 +216,25 @@ export class DatabaseStorage implements IStorage {
const [newGeneration] = await db.insert(aiGenerations).values(generation).returning();
return newGeneration;
}
// Cloudinary Config
async getCloudinaryConfig(userId: string): Promise<CloudinaryConfig | undefined> {
const [config] = await db.select().from(cloudinaryConfig).where(eq(cloudinaryConfig.userId, userId));
return config || undefined;
}
async createCloudinaryConfig(config: InsertCloudinaryConfig): Promise<CloudinaryConfig> {
const [newConfig] = await db.insert(cloudinaryConfig).values(config).returning();
return newConfig;
}
async updateCloudinaryConfig(userId: string, config: Partial<InsertCloudinaryConfig>): Promise<CloudinaryConfig> {
const [updated] = await db.update(cloudinaryConfig)
.set({ ...config, updatedAt: new Date() })
.where(eq(cloudinaryConfig.userId, userId))
.returning();
return updated;
}
}
export const storage = new DatabaseStorage();