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
197 lines
5.5 KiB
TypeScript
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 };
|