folx-tv/server/cloudinary.ts
sebastjanartic f52f8c7ba0 Add functionality to delete images from the gallery and Cloudinary
Implement image deletion functionality, including a confirmation dialog and API endpoint for removing images from Cloudinary and updating the gallery map.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 23852c00-4779-460a-9e0c-d09fee4b6c92
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 37efc56d-aeff-47ff-8064-9aa64d8b204c
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/23852c00-4779-460a-9e0c-d09fee4b6c92/ncMMRQ9
Replit-Helium-Checkpoint-Created: true
2026-03-06 10:58:47 +00:00

197 lines
5.5 KiB
TypeScript

import { v2 as cloudinary } from "cloudinary";
import sharp from "sharp";
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
});
const GALLERY_FOLDER = "folx-gallery";
const MAX_UPLOAD_SIZE = 9 * 1024 * 1024;
export interface CloudinaryGalleryImage {
publicId: string;
fileName: string;
artist: string;
thumb: string;
large: string;
mobile: string;
full: string;
}
function buildUrl(publicId: string, transforms: string): string {
const cloudName = process.env.CLOUDINARY_CLOUD_NAME || "djqxt0pf3";
return `https://res.cloudinary.com/${cloudName}/image/upload/${transforms}/${publicId}`;
}
export function generateImageUrls(publicId: string): { thumb: string; large: string; mobile: string; full: string } {
return {
thumb: buildUrl(publicId, "c_fill,g_face:center,w_300,h_300,q_auto,f_auto"),
mobile: buildUrl(publicId, "c_fill,g_auto,w_600,h_338,q_auto,f_auto"),
large: buildUrl(publicId, "c_fit,w_1200,q_auto,f_auto"),
full: buildUrl(publicId, "c_fit,w_1800,q_auto,f_auto"),
};
}
async function downloadAndCompress(imageUrl: string): Promise<Buffer> {
const resp = await fetch(imageUrl);
if (!resp.ok) throw new Error(`Download failed: ${resp.status}`);
const arrayBuffer = await resp.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
if (buffer.length <= MAX_UPLOAD_SIZE) {
return buffer;
}
const compressed = await sharp(buffer)
.resize({ width: 4000, withoutEnlargement: true })
.jpeg({ quality: 85 })
.toBuffer();
if (compressed.length > MAX_UPLOAD_SIZE) {
return sharp(buffer)
.resize({ width: 3000, withoutEnlargement: true })
.jpeg({ quality: 75 })
.toBuffer();
}
return compressed;
}
export async function uploadToCloudinary(imageUrl: string, fileName: string): Promise<string | null> {
const publicId = `${GALLERY_FOLDER}/${fileName.replace(/\.\w+$/, "").replace(/[^a-zA-Z0-9_-]/g, "_")}`;
try {
const existing = await cloudinary.api.resource(publicId).catch(() => null);
if (existing) {
return publicId;
}
} catch {}
try {
const buffer = await downloadAndCompress(imageUrl);
return new Promise((resolve, reject) => {
const uploadStream = cloudinary.uploader.upload_stream(
{
public_id: publicId,
overwrite: false,
resource_type: "image",
},
(error, result) => {
if (error) reject(error);
else resolve(result!.public_id);
}
);
uploadStream.end(buffer);
});
} catch (err: any) {
console.error(`Cloudinary upload failed for ${fileName}:`, err.message);
return null;
}
}
export async function deleteFromCloudinary(publicId: string): Promise<boolean> {
try {
const result = await cloudinary.uploader.destroy(publicId, { resource_type: "image" });
return result.result === "ok";
} catch (err: any) {
console.error(`Cloudinary delete failed for ${publicId}:`, err.message);
return false;
}
}
export async function checkCloudinaryImage(publicId: string): Promise<boolean> {
try {
await cloudinary.api.resource(publicId);
return true;
} catch {
return false;
}
}
export async function listCloudinaryGallery(): Promise<string[]> {
try {
const result = await cloudinary.api.resources({
type: "upload",
prefix: GALLERY_FOLDER + "/",
max_results: 500,
resource_type: "image",
});
return result.resources.map((r: any) => r.public_id);
} catch (err: any) {
console.error("Cloudinary list failed:", err.message);
return [];
}
}
export interface CloudinaryResourceInfo {
publicId: string;
fileName: string;
artist: string;
width: number;
height: number;
format: string;
bytes: number;
createdAt: string;
thumb: string;
large: string;
mobile: string;
full: string;
}
export async function listCloudinaryGalleryDetailed(): Promise<CloudinaryResourceInfo[]> {
try {
let allResources: any[] = [];
let nextCursor: string | undefined;
do {
const result = await cloudinary.api.resources({
type: "upload",
prefix: GALLERY_FOLDER + "/",
max_results: 500,
resource_type: "image",
...(nextCursor ? { next_cursor: nextCursor } : {}),
});
allResources = allResources.concat(result.resources);
nextCursor = result.next_cursor;
} while (nextCursor);
return allResources.map((r: any) => {
const urls = generateImageUrls(r.public_id);
const rawName = r.public_id.replace(`${GALLERY_FOLDER}/`, "").replace(/_/g, " ");
let originalFileName = rawName;
const ext = r.format || "jpg";
originalFileName = rawName + "." + ext;
return {
publicId: r.public_id,
fileName: originalFileName,
artist: extractArtistFromPublicId(r.public_id),
width: r.width,
height: r.height,
format: r.format,
bytes: r.bytes,
createdAt: r.created_at,
...urls,
};
});
} catch (err: any) {
console.error("Cloudinary detailed list failed:", err.message);
return [];
}
}
export function extractArtistFromPublicId(publicId: string): string {
const name = publicId.replace(`${GALLERY_FOLDER}/`, "").replace(/_/g, " ");
if (/^DSC\d/i.test(name) || /^IMG[\s_]\d/i.test(name)) return "";
const cleaned = name
.replace(/\s*\(\d+\)\s*$/, "")
.replace(/\s*\d+\s*$/, "")
.replace(/\s*\(\d+\)\s*$/, "");
return cleaned.trim();
}
export { cloudinary };