Fix the image gallery to display images from correct folders

Updates the Dropbox integration to store app keys directly in the token file, resolves the issue with the incorrect Dropbox folder path, and searches for images in multiple actual gallery folders instead of a single non-existent one.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 517dfa7b-26ac-463d-a6e1-a58c6df97188
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: ce08de34-8538-403f-9a35-f9c3d376d112
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/517dfa7b-26ac-463d-a6e1-a58c6df97188/VgutZ7W
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sebastjanartic 2026-03-01 18:24:56 +00:00
parent 72e34076fc
commit 146f797809
4 changed files with 299 additions and 1096 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

View File

@ -1,5 +1,7 @@
{ {
"access_token": "sl.u.AGWEi26aooROq4uVMyilg68bMLhgbcsx6wdMxuB-CyH3_HBcfMInAEy7U0JR_oUdHVx4Y51L-GZ1yltJT-YTeZJVGVZ3sL8x9sSQpP1VsfSgCCu5hDqgFYCXGchJu4XwhWX42L6vmUQ-QUDrsokj2HU1SKJ0MGcLfMNVynMJZKwOl2lcZ88veeB782UoAn9IXei96v8wFCvKIJCFYyS7dosOFuTsqvjJZWy8zeZZ9GLdCAQiwQr2lRvhxvpMF8ZiUSzpZSjjld_y3A8cRtmJNVQLBL6ctB3TtnYtse4ccKPmt5pOhIFcWcd7OYSv6BiiWSEdkmVy3UTiStq8EQBc7sdRRYE1_cZKQbsDzgGk8vgd9QbaOuQyW949C90H7pGD3c-ZJraIjdeFlvW_yD99qQSb4hwnB1w0pGVXtiuVfvhEPasdbl8LWwPmM1q3P2OZLGOz34xj8gPfwVULcfpAtw5Ipsbe8Y4VBPz2hn0HxhYNDyFdV1e8wLFiAAbOabG8R7zzPodbtXLLuE2jWXJhQeMKtT763b6_YDMs3VJc43m5Uzgezcb_izupEtEB4hB9K1E3783F05MArnUQ7ntUDZ6lFPYkjbZM7__4rRv8_hmSiAQ_FkvFQo75PBIpIbkhoa3Fm8wDvOSG_9lx8c_wfJjV0BbXAJuvN0GPbDzKuwAOVqpibcx7aSR3Lawgs19Low-MTLzXtzDUDIdPybIB5rMOSchcQsYdOrDlKI-_0X--CkvDVBGXXYFK6t-OiQeqtfZDUqIRXNpVwt2XCO0rGBgXCgR8M3bjemMB3YDNqZEbEG85Vpjkelc_1jXwGkK6UfQMS0LPdfnZpuhSEnMG0SBbY8hM4J-z9RRK3qqWshTzvOE6QKZE8ldpfKviI0YMfjhxhDiVQvfveh9nFd1rafQipwltNbdRAi3l0bXMdpV2UAtspvgLfmvoFugVzmm6C8oTKlS-rjUlGk7_lZjX_29prMZc5WvuIUoRkgZUgJchMnGaE1JSvpH-Ky-63aZXBqi5uA2WW13F8or1Tng8AV-8YehwpZ4udf0chdJyFw9tnfdYHyEX-b1hLT8sAwERwV0_9cKycDl7h5JhGLHmaouOKl0JvsbvZsD7uyoQfPGtYd6zQpZUHJx72pzeghpTTW40ukRGRjuqxCe65hNzP6Bv6eQQI6H_Wf8YaBNoQYXAEc8JchO_PhKhDeBFvMxBnBuedW-QoAvxh20zmeiPYVxNUrcNSTVXT7EhCFwlpSzVuIlnZzivldA2arPtBlK5fpCsO9bFBqIXURO-X7TXI0Tb-9XgjEurtE6DviyUoHxWnA", "access_token": "sl.u.AGVAMk1isWjEfoERFxA5-4d0aQJgK5q3BvH4nQq7XKhUIV2q4YDpbYok3TQf7z2fF7ZBDZQJ2of2rnz4vzrfnkTrgRIirNckfsMkSfZvAXF8E2VK3fLrtXs5uy3g-A7CibB2I_TFehxQgiGJh77788vCDnGuwt-rYLSEm238-SS9OiDWUXNPvESABp2LHwJmKOnZg_Z6I9b6fy-BnppE_IOMPDkIhxy8T3a98-jgZGlR7SKEOPbAv5O9cQsmSi9hfh3pV53IV6lhfSpvPFXCxUCIUB990Cga1Cv70JlNSWpcB4pY_5mV-03ncRhxSBe-DjFAwY8DQskIsFyS5bcNloqk-nei3LxAXlk-PqTEpCMGcIjM5sVCazBPh1m3u169M5v_qV598nxc7S7uZcVT2jvT01bLIN9HqOvF3fpYeEY0v2ORQNMzr8AuZEGmpJbIkT0zpXSNatgVPzOtvmdTjoxfb0wjMjM1VEjGgX1v6UHVxLnyvSyvW7UZ2QNo4wmqOJQnq32l5sBFukHDImPDW3nKoATHJcn6dLLk0sjbl3-8UH9sZpezOxJIGmcla34DFbt2n_NymArdSA-m7ywGHqdR8Eqr-scWQi14WKcaHXnCQF_R_puhKiTdzcW4UFb5qmBfk78fDJtPhsrIagc31YYXx5co--qkTsdGDdhVKtE7fvDucn63uF79YNoZ087rD5P-gbDgSf8j90ijry4gbMsL0vxtWiMJ9nZxjT-k43ZDhmz_h5h0Fg6-1sAkRHq_cPQTjffn7rAWnb7zxOJueIMDWMTy2UXPERBuwuWa66QVqiyNpi_6URR2suyXZCK7xvqbHUy3fVP_k5xjZ84YcQXcBVUfZoxso1rlzsuTFEMrhZF_vUJqYBcQyw0VRNVWcPkz6_9ZcmGSsibS-112M8516EI-tl60id3Rd_KwH36nfqT0pkCUbkcAhreDzzU_ci0p3hB2WhfBDrsqolAW3Dc9S_pC9CuJn_dECjrngMbegARCXsychREPYx60h9Kg8ZyEmacrUklURVkXHT2y2HshrLxSbPL8CbHyXU4-3_8qquKrpTncLSxRuZCuPociVUxsNF4iyvUfSgi2ZjhuggJJteZvdfQ--aKt0s8aTdfThRQ6DyJTe9MoDlCUzW8byP7Z9JZ2AWFehqO1xKyWLD7wU6g7LV48G5n_S5kJUYpuPXmWDgpoj1-VehZGkVWEoQiW0deboadOT-MOvL1flV0gLU6nZv9D2F7jWWVTe26m1qC9iixHRyTmupMrL-P4tOEvTD41krapeFstuP61mzpl2FxrQge8P2NDfOY61olilQ",
"refresh_token": "8-fiK0Io8Z8AAAAAAAAAAXn1QIGTwFTVWKF47COXY2bjqYlWyd4aRnFtQJ7usZ0y", "refresh_token": "8-fiK0Io8Z8AAAAAAAAAAXn1QIGTwFTVWKF47COXY2bjqYlWyd4aRnFtQJ7usZ0y",
"expires_at": 1772402616000 "expires_at": 1772403823104,
} "app_key": "sjwprgka82p8tpv",
"app_secret": "g3vuczqo0rx3crz"
}

View File

@ -2,12 +2,27 @@ import fs from "fs";
import path from "path"; import path from "path";
const TOKEN_PATH = path.join(process.cwd(), "server/dropbox-token.json"); const TOKEN_PATH = path.join(process.cwd(), "server/dropbox-token.json");
const DROPBOX_FOLDER = "/Folx News Gallery"; const GALLERY_FOLDERS = [
"/folx stadl bad tölz 25",
"/folx stadl bad tölz 23",
"/folx stadl 2022 attersee",
"/a guadi musi 2023",
"/hb fest 2023",
"/slike epd",
"/simon wild photos",
"/adria soccer 2024",
"/gipfelstammtisch 2024",
"/die kaiser fanreise",
"/sanabela 2023",
"/ainringer musikfrühling 2022",
];
interface DropboxTokens { interface DropboxTokens {
access_token: string; access_token: string;
refresh_token: string; refresh_token: string;
expires_at: number; expires_at: number;
app_key?: string;
app_secret?: string;
} }
interface GalleryImage { interface GalleryImage {
@ -19,11 +34,13 @@ interface GalleryImage {
} }
function getAppKey(): string { function getAppKey(): string {
return process.env.DROPBOX_APP_KEY || ""; const tokens = loadTokens();
return tokens?.app_key || process.env.DROPBOX_APP_KEY || "";
} }
function getAppSecret(): string { function getAppSecret(): string {
return process.env.DROPBOX_APP_SECRET || ""; const tokens = loadTokens();
return tokens?.app_secret || process.env.DROPBOX_APP_SECRET || "";
} }
export function getAuthUrl(redirectUri: string): string { export function getAuthUrl(redirectUri: string): string {
@ -37,6 +54,8 @@ export function getAuthUrl(redirectUri: string): string {
} }
export async function exchangeCodeForTokens(code: string, redirectUri: string): Promise<DropboxTokens> { export async function exchangeCodeForTokens(code: string, redirectUri: string): Promise<DropboxTokens> {
const appKey = getAppKey();
const appSecret = getAppSecret();
const resp = await fetch("https://api.dropboxapi.com/oauth2/token", { const resp = await fetch("https://api.dropboxapi.com/oauth2/token", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" }, headers: { "Content-Type": "application/x-www-form-urlencoded" },
@ -44,8 +63,8 @@ export async function exchangeCodeForTokens(code: string, redirectUri: string):
code, code,
grant_type: "authorization_code", grant_type: "authorization_code",
redirect_uri: redirectUri, redirect_uri: redirectUri,
client_id: getAppKey(), client_id: appKey,
client_secret: getAppSecret(), client_secret: appSecret,
}), }),
}); });
@ -59,6 +78,8 @@ export async function exchangeCodeForTokens(code: string, redirectUri: string):
access_token: data.access_token, access_token: data.access_token,
refresh_token: data.refresh_token, refresh_token: data.refresh_token,
expires_at: Date.now() + data.expires_in * 1000, expires_at: Date.now() + data.expires_in * 1000,
app_key: appKey,
app_secret: appSecret,
}; };
fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2)); fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2));
@ -75,14 +96,16 @@ function loadTokens(): DropboxTokens | null {
} }
async function refreshAccessToken(refreshToken: string): Promise<DropboxTokens> { async function refreshAccessToken(refreshToken: string): Promise<DropboxTokens> {
const appKey = getAppKey();
const appSecret = getAppSecret();
const resp = await fetch("https://api.dropboxapi.com/oauth2/token", { const resp = await fetch("https://api.dropboxapi.com/oauth2/token", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" }, headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ body: new URLSearchParams({
grant_type: "refresh_token", grant_type: "refresh_token",
refresh_token: refreshToken, refresh_token: refreshToken,
client_id: getAppKey(), client_id: appKey,
client_secret: getAppSecret(), client_secret: appSecret,
}), }),
}); });
@ -96,6 +119,8 @@ async function refreshAccessToken(refreshToken: string): Promise<DropboxTokens>
access_token: data.access_token, access_token: data.access_token,
refresh_token: refreshToken, refresh_token: refreshToken,
expires_at: Date.now() + data.expires_in * 1000, expires_at: Date.now() + data.expires_in * 1000,
app_key: appKey,
app_secret: appSecret,
}; };
fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2)); fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2));
@ -107,7 +132,12 @@ async function getValidAccessToken(): Promise<string | null> {
if (!tokens) return null; if (!tokens) return null;
if (Date.now() > tokens.expires_at - 60000) { if (Date.now() > tokens.expires_at - 60000) {
tokens = await refreshAccessToken(tokens.refresh_token); try {
tokens = await refreshAccessToken(tokens.refresh_token);
} catch (err: any) {
console.error("Token refresh failed:", err.message);
return null;
}
} }
return tokens.access_token; return tokens.access_token;
@ -117,7 +147,7 @@ export function isConnected(): boolean {
return loadTokens() !== null; return loadTokens() !== null;
} }
async function listFolder(accessToken: string, folderPath: string): Promise<any[]> { async function listFolder(accessToken: string, folderPath: string, recursive = false): Promise<any[]> {
const entries: any[] = []; const entries: any[] = [];
let cursor: string | null = null; let cursor: string | null = null;
let hasMore = true; let hasMore = true;
@ -129,7 +159,7 @@ async function listFolder(accessToken: string, folderPath: string): Promise<any[
const body = cursor const body = cursor
? { cursor } ? { cursor }
: { path: folderPath, recursive: false, limit: 2000 }; : { path: folderPath, recursive, limit: 2000 };
const resp = await fetch(url, { const resp = await fetch(url, {
method: "POST", method: "POST",
@ -154,62 +184,30 @@ async function listFolder(accessToken: string, folderPath: string): Promise<any[
return entries; return entries;
} }
async function getThumbnailBatch(accessToken: string, paths: string[]): Promise<Map<string, string>> { async function getTemporaryLinks(accessToken: string, paths: string[]): Promise<Map<string, string>> {
const results = new Map<string, string>(); const results = new Map<string, string>();
const batchSize = 25; const batchSize = 10;
for (let i = 0; i < paths.length; i += batchSize) { for (let i = 0; i < paths.length; i += batchSize) {
const batch = paths.slice(i, i + batchSize); const batch = paths.slice(i, i + batchSize);
const entries = batch.map((p) => ({ const promises = batch.map(async (p) => {
path: p, try {
format: "jpeg", const resp = await fetch("https://api.dropboxapi.com/2/files/get_temporary_link", {
size: "w256h256", method: "POST",
mode: "fitone_bestfit", headers: {
})); Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ path: p }),
});
try { if (resp.ok) {
const resp = await fetch("https://content.dropboxapi.com/2/files/get_thumbnail_batch", { const data = await resp.json();
method: "POST", results.set(p.toLowerCase(), data.link);
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ entries }),
});
if (resp.ok) {
const data = await resp.json();
for (const entry of data.entries) {
if (entry[".tag"] === "success") {
results.set(entry.metadata.path_lower, `data:image/jpeg;base64,${entry.thumbnail}`);
}
} }
} } catch {}
} catch {} });
} await Promise.all(promises);
return results;
}
async function getTemporaryLinks(accessToken: string, paths: string[]): Promise<Map<string, string>> {
const results = new Map<string, string>();
for (const p of paths) {
try {
const resp = await fetch("https://api.dropboxapi.com/2/files/get_temporary_link", {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ path: p }),
});
if (resp.ok) {
const data = await resp.json();
results.set(p.toLowerCase(), data.link);
}
} catch {}
} }
return results; return results;
@ -227,37 +225,45 @@ export async function fetchGalleryFromDropbox(): Promise<GalleryImage[]> {
if (!accessToken) return []; if (!accessToken) return [];
try { try {
const topEntries = await listFolder(accessToken, DROPBOX_FOLDER);
const folders = topEntries.filter((e: any) => e[".tag"] === "folder");
const images: GalleryImage[] = []; const images: GalleryImage[] = [];
for (const folder of folders) { for (const folderPath of GALLERY_FOLDERS) {
const folderName = folder.name; try {
const folderEntries = await listFolder(accessToken, folder.path_lower); const entries = await listFolder(accessToken, folderPath, true);
const imageFiles = folderEntries.filter( const imageFiles = entries.filter(
(e: any) => e[".tag"] === "file" && /\.(jpg|jpeg|png|webp|gif)$/i.test(e.name) (e: any) => e[".tag"] === "file" && /\.(jpg|jpeg|png|webp|gif)$/i.test(e.name)
); );
const paths = imageFiles.map((f: any) => f.path_lower); const limitedFiles = imageFiles.slice(0, 8);
const tempLinks = await getTemporaryLinks(accessToken, paths); const paths = limitedFiles.map((f: any) => f.path_lower);
const tempLinks = await getTemporaryLinks(accessToken, paths);
for (const file of imageFiles) { const folderName = folderPath.split("/").pop() || folderPath;
const link = tempLinks.get(file.path_lower) || "";
images.push({ for (const file of limitedFiles) {
folder: folderName, const link = tempLinks.get(file.path_lower) || "";
fileName: file.name, if (link) {
thumb: link ? `${link}&size=256x256` : "", images.push({
large: link, folder: folderName,
full: link, fileName: file.name,
}); thumb: link,
large: link,
full: link,
});
}
}
} catch (err: any) {
console.error(`Folder ${folderPath} error:`, err.message);
} }
} }
galleryCache.data = images; if (images.length > 0) {
galleryCache.timestamp = Date.now(); galleryCache.data = images;
galleryCache.timestamp = Date.now();
const galleryPath = path.join(process.cwd(), "server/gallery-data.json"); const galleryPath = path.join(process.cwd(), "server/gallery-data.json");
fs.writeFileSync(galleryPath, JSON.stringify(images, null, 2)); fs.writeFileSync(galleryPath, JSON.stringify(images, null, 2));
}
return images; return images;
} catch (err: any) { } catch (err: any) {

File diff suppressed because it is too large Load Diff