Published your App
Replit-Commit-Author: Deployment Replit-Commit-Session-Id: 23852c00-4779-460a-9e0c-d09fee4b6c92 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 2b7650db-b0d4-4415-b9bb-cc11e719c2b7 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/23852c00-4779-460a-9e0c-d09fee4b6c92/ncMMRQ9 Replit-Commit-Deployment-Build-Id: 55db64a6-94a6-4850-b7bf-6b5712a636c3 Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
8cbb08f182
commit
a23ae0bdb7
10
server/cloudinary-gallery-map.json
Normal file
10
server/cloudinary-gallery-map.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"DSC06227.jpg": "folx-gallery/DSC06227",
|
||||
"Ulli Bastian (2).jpg": "folx-gallery/Ulli_Bastian__2_",
|
||||
"DSC08185.jpg": "folx-gallery/DSC08185",
|
||||
"DSC08429.jpg": "folx-gallery/DSC08429",
|
||||
"Julia Buchner 2.jpg": "folx-gallery/Julia_Buchner_2",
|
||||
"Julia Buchner 1 .jpg": "folx-gallery/Julia_Buchner_1_",
|
||||
"John Prisco 1.jpg": "folx-gallery/John_Prisco_1",
|
||||
"Mosaik 4.jpg": "folx-gallery/Mosaik_4"
|
||||
}
|
||||
94
server/cloudinary.ts
Normal file
94
server/cloudinary.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { v2 as cloudinary } from "cloudinary";
|
||||
|
||||
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";
|
||||
|
||||
export interface CloudinaryGalleryImage {
|
||||
publicId: string;
|
||||
fileName: string;
|
||||
artist: string;
|
||||
thumb: string;
|
||||
large: string;
|
||||
mobile: string;
|
||||
full: string;
|
||||
}
|
||||
|
||||
function buildUrl(publicId: string, transforms: string): string {
|
||||
return cloudinary.url(publicId, {
|
||||
transformation: transforms,
|
||||
secure: true,
|
||||
});
|
||||
}
|
||||
|
||||
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"),
|
||||
};
|
||||
}
|
||||
|
||||
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 result = await cloudinary.uploader.upload(imageUrl, {
|
||||
public_id: publicId,
|
||||
overwrite: false,
|
||||
resource_type: "image",
|
||||
});
|
||||
return result.public_id;
|
||||
} catch (err: any) {
|
||||
console.error(`Cloudinary upload failed for ${fileName}:`, err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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 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 };
|
||||
@ -1,8 +1,10 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { uploadToCloudinary, generateImageUrls } from "./cloudinary";
|
||||
|
||||
const TOKEN_PATH = path.join(process.cwd(), "server/dropbox-token.json");
|
||||
const GALLERY_ROOT = "/family room/photos/oddaje/izvajalci selekcija";
|
||||
const CLOUDINARY_MAP_PATH = path.join(process.cwd(), "server/cloudinary-gallery-map.json");
|
||||
|
||||
interface DropboxTokens {
|
||||
access_token: string;
|
||||
@ -22,6 +24,23 @@ interface GalleryImage {
|
||||
artist: string;
|
||||
}
|
||||
|
||||
interface CloudinaryMap {
|
||||
[fileName: string]: string;
|
||||
}
|
||||
|
||||
function loadCloudinaryMap(): CloudinaryMap {
|
||||
try {
|
||||
if (fs.existsSync(CLOUDINARY_MAP_PATH)) {
|
||||
return JSON.parse(fs.readFileSync(CLOUDINARY_MAP_PATH, "utf-8"));
|
||||
}
|
||||
} catch {}
|
||||
return {};
|
||||
}
|
||||
|
||||
function saveCloudinaryMap(map: CloudinaryMap): void {
|
||||
fs.writeFileSync(CLOUDINARY_MAP_PATH, JSON.stringify(map, null, 2));
|
||||
}
|
||||
|
||||
function extractArtistFromFileName(fileName: string): string {
|
||||
if (/^DSC\d/i.test(fileName) || /^IMG[\s_]\d/i.test(fileName)) return "";
|
||||
const withoutExt = fileName.replace(/\.\w+$/, "");
|
||||
@ -233,6 +252,28 @@ export async function fetchGalleryFromDropbox(): Promise<GalleryImage[]> {
|
||||
return galleryCache.data;
|
||||
}
|
||||
|
||||
const cloudinaryMap = loadCloudinaryMap();
|
||||
const hasCloudinaryImages = Object.keys(cloudinaryMap).length > 0;
|
||||
|
||||
if (hasCloudinaryImages) {
|
||||
const images: GalleryImage[] = [];
|
||||
for (const [fileName, publicId] of Object.entries(cloudinaryMap)) {
|
||||
const urls = generateImageUrls(publicId);
|
||||
images.push({
|
||||
folder: "Foto All",
|
||||
fileName,
|
||||
thumb: urls.thumb,
|
||||
large: urls.large,
|
||||
mobile: urls.mobile,
|
||||
full: urls.full,
|
||||
artist: extractArtistFromFileName(fileName),
|
||||
});
|
||||
}
|
||||
galleryCache.data = images;
|
||||
galleryCache.timestamp = Date.now();
|
||||
return images;
|
||||
}
|
||||
|
||||
const accessToken = await getValidAccessToken();
|
||||
if (!accessToken) return [];
|
||||
|
||||
@ -305,3 +346,55 @@ export async function fetchGalleryFromDropbox(): Promise<GalleryImage[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function migrateGalleryToCloudinary(): Promise<{ uploaded: number; skipped: number; failed: number }> {
|
||||
const accessToken = await getValidAccessToken();
|
||||
if (!accessToken) throw new Error("Dropbox not connected");
|
||||
|
||||
const originalEntries = await listFolder(accessToken, ORIGINAL_FOLDER.toLowerCase());
|
||||
const imgFilter = (e: any) => e[".tag"] === "file" && /\.(jpg|jpeg|png|webp|gif)$/i.test(e.name);
|
||||
const originalFiles = originalEntries.filter(imgFilter);
|
||||
|
||||
const cloudinaryMap = loadCloudinaryMap();
|
||||
let uploaded = 0;
|
||||
let skipped = 0;
|
||||
let failed = 0;
|
||||
|
||||
const batchSize = 5;
|
||||
for (let i = 0; i < originalFiles.length; i += batchSize) {
|
||||
const batch = originalFiles.slice(i, i + batchSize);
|
||||
const links = await getTemporaryLinks(accessToken, batch.map((f: any) => f.path_lower));
|
||||
|
||||
const uploadPromises = batch.map(async (file: any) => {
|
||||
const fileName = file.name;
|
||||
|
||||
if (cloudinaryMap[fileName]) {
|
||||
skipped++;
|
||||
return;
|
||||
}
|
||||
|
||||
const link = links.get(file.path_lower);
|
||||
if (!link) {
|
||||
failed++;
|
||||
return;
|
||||
}
|
||||
|
||||
const publicId = await uploadToCloudinary(link, fileName);
|
||||
if (publicId) {
|
||||
cloudinaryMap[fileName] = publicId;
|
||||
uploaded++;
|
||||
console.log(`[cloudinary] Uploaded: ${fileName} -> ${publicId}`);
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(uploadPromises);
|
||||
saveCloudinaryMap(cloudinaryMap);
|
||||
}
|
||||
|
||||
galleryCache.data = [];
|
||||
galleryCache.timestamp = 0;
|
||||
|
||||
return { uploaded, skipped, failed };
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,7 @@ import { seedDatabase } from "./seed";
|
||||
import { generateDailyHoroscopes, getHoroscopesForToday, getOrGenerateHoroscope } from "./horoscope-generator";
|
||||
import { analyzeAllArticleImages, getCachedFocalPoints } from "./focal-point";
|
||||
import { optimizeImage } from "./image-optimizer";
|
||||
import { getAuthUrl, exchangeCodeForTokens, isConnected, fetchGalleryFromDropbox, getValidAccessToken } from "./dropbox";
|
||||
import { getAuthUrl, exchangeCodeForTokens, isConnected, fetchGalleryFromDropbox, getValidAccessToken, migrateGalleryToCloudinary } from "./dropbox";
|
||||
import multer from "multer";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
@ -353,7 +353,16 @@ export async function registerRoutes(
|
||||
}
|
||||
});
|
||||
|
||||
// Gallery API - serves optimized photos from Dropbox
|
||||
app.post("/api/gallery/migrate-to-cloudinary", async (_req, res) => {
|
||||
try {
|
||||
const result = await migrateGalleryToCloudinary();
|
||||
res.json(result);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Gallery API - serves optimized photos from Dropbox/Cloudinary
|
||||
app.get("/api/gallery", async (req, res) => {
|
||||
try {
|
||||
let data: any[] = [];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user