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",
"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";
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 {
access_token: string;
refresh_token: string;
expires_at: number;
app_key?: string;
app_secret?: string;
}
interface GalleryImage {
@ -19,11 +34,13 @@ interface GalleryImage {
}
function getAppKey(): string {
return process.env.DROPBOX_APP_KEY || "";
const tokens = loadTokens();
return tokens?.app_key || process.env.DROPBOX_APP_KEY || "";
}
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 {
@ -37,6 +54,8 @@ export function getAuthUrl(redirectUri: string): string {
}
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", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
@ -44,8 +63,8 @@ export async function exchangeCodeForTokens(code: string, redirectUri: string):
code,
grant_type: "authorization_code",
redirect_uri: redirectUri,
client_id: getAppKey(),
client_secret: getAppSecret(),
client_id: appKey,
client_secret: appSecret,
}),
});
@ -59,6 +78,8 @@ export async function exchangeCodeForTokens(code: string, redirectUri: string):
access_token: data.access_token,
refresh_token: data.refresh_token,
expires_at: Date.now() + data.expires_in * 1000,
app_key: appKey,
app_secret: appSecret,
};
fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2));
@ -75,14 +96,16 @@ function loadTokens(): DropboxTokens | null {
}
async function refreshAccessToken(refreshToken: string): Promise<DropboxTokens> {
const appKey = getAppKey();
const appSecret = getAppSecret();
const resp = await fetch("https://api.dropboxapi.com/oauth2/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: getAppKey(),
client_secret: getAppSecret(),
client_id: appKey,
client_secret: appSecret,
}),
});
@ -96,6 +119,8 @@ async function refreshAccessToken(refreshToken: string): Promise<DropboxTokens>
access_token: data.access_token,
refresh_token: refreshToken,
expires_at: Date.now() + data.expires_in * 1000,
app_key: appKey,
app_secret: appSecret,
};
fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2));
@ -107,7 +132,12 @@ async function getValidAccessToken(): Promise<string | null> {
if (!tokens) return null;
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;
@ -117,7 +147,7 @@ export function isConnected(): boolean {
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[] = [];
let cursor: string | null = null;
let hasMore = true;
@ -129,7 +159,7 @@ async function listFolder(accessToken: string, folderPath: string): Promise<any[
const body = cursor
? { cursor }
: { path: folderPath, recursive: false, limit: 2000 };
: { path: folderPath, recursive, limit: 2000 };
const resp = await fetch(url, {
method: "POST",
@ -154,62 +184,30 @@ async function listFolder(accessToken: string, folderPath: string): Promise<any[
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 batchSize = 25;
const batchSize = 10;
for (let i = 0; i < paths.length; i += batchSize) {
const batch = paths.slice(i, i + batchSize);
const entries = batch.map((p) => ({
path: p,
format: "jpeg",
size: "w256h256",
mode: "fitone_bestfit",
}));
const promises = batch.map(async (p) => {
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 }),
});
try {
const resp = await fetch("https://content.dropboxapi.com/2/files/get_thumbnail_batch", {
method: "POST",
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}`);
}
if (resp.ok) {
const data = await resp.json();
results.set(p.toLowerCase(), data.link);
}
}
} catch {}
}
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 {}
} catch {}
});
await Promise.all(promises);
}
return results;
@ -227,37 +225,45 @@ export async function fetchGalleryFromDropbox(): Promise<GalleryImage[]> {
if (!accessToken) return [];
try {
const topEntries = await listFolder(accessToken, DROPBOX_FOLDER);
const folders = topEntries.filter((e: any) => e[".tag"] === "folder");
const images: GalleryImage[] = [];
for (const folder of folders) {
const folderName = folder.name;
const folderEntries = await listFolder(accessToken, folder.path_lower);
const imageFiles = folderEntries.filter(
(e: any) => e[".tag"] === "file" && /\.(jpg|jpeg|png|webp|gif)$/i.test(e.name)
);
for (const folderPath of GALLERY_FOLDERS) {
try {
const entries = await listFolder(accessToken, folderPath, true);
const imageFiles = entries.filter(
(e: any) => e[".tag"] === "file" && /\.(jpg|jpeg|png|webp|gif)$/i.test(e.name)
);
const paths = imageFiles.map((f: any) => f.path_lower);
const tempLinks = await getTemporaryLinks(accessToken, paths);
const limitedFiles = imageFiles.slice(0, 8);
const paths = limitedFiles.map((f: any) => f.path_lower);
const tempLinks = await getTemporaryLinks(accessToken, paths);
for (const file of imageFiles) {
const link = tempLinks.get(file.path_lower) || "";
images.push({
folder: folderName,
fileName: file.name,
thumb: link ? `${link}&size=256x256` : "",
large: link,
full: link,
});
const folderName = folderPath.split("/").pop() || folderPath;
for (const file of limitedFiles) {
const link = tempLinks.get(file.path_lower) || "";
if (link) {
images.push({
folder: folderName,
fileName: file.name,
thumb: link,
large: link,
full: link,
});
}
}
} catch (err: any) {
console.error(`Folder ${folderPath} error:`, err.message);
}
}
galleryCache.data = images;
galleryCache.timestamp = Date.now();
if (images.length > 0) {
galleryCache.data = images;
galleryCache.timestamp = Date.now();
const galleryPath = path.join(process.cwd(), "server/gallery-data.json");
fs.writeFileSync(galleryPath, JSON.stringify(images, null, 2));
const galleryPath = path.join(process.cwd(), "server/gallery-data.json");
fs.writeFileSync(galleryPath, JSON.stringify(images, null, 2));
}
return images;
} catch (err: any) {

File diff suppressed because it is too large Load Diff