folx-tv/server/dropbox.ts
sebastjanartic 77fdd872f6 Add Dropbox integration for secure image storage and retrieval
Integrates Dropbox OAuth for authentication, adds API routes for authorization, token exchange, and fetching gallery images, and updates the gallery API to support fetching images directly from Dropbox.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 517dfa7b-26ac-463d-a6e1-a58c6df97188
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: e2398e24-3175-4cbf-91ff-003984c8d042
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/517dfa7b-26ac-463d-a6e1-a58c6df97188/ls5p9ni
Replit-Helium-Checkpoint-Created: true
2026-03-01 18:01:27 +00:00

268 lines
7.2 KiB
TypeScript

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";
interface DropboxTokens {
access_token: string;
refresh_token: string;
expires_at: number;
}
interface GalleryImage {
folder: string;
fileName: string;
thumb: string;
large: string;
full: string;
}
function getAppKey(): string {
return process.env.DROPBOX_APP_KEY || "";
}
function getAppSecret(): string {
return process.env.DROPBOX_APP_SECRET || "";
}
export function getAuthUrl(redirectUri: string): string {
const params = new URLSearchParams({
client_id: getAppKey(),
response_type: "code",
redirect_uri: redirectUri,
token_access_type: "offline",
});
return `https://www.dropbox.com/oauth2/authorize?${params.toString()}`;
}
export async function exchangeCodeForTokens(code: string, redirectUri: string): Promise<DropboxTokens> {
const resp = await fetch("https://api.dropboxapi.com/oauth2/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
code,
grant_type: "authorization_code",
redirect_uri: redirectUri,
client_id: getAppKey(),
client_secret: getAppSecret(),
}),
});
if (!resp.ok) {
const err = await resp.text();
throw new Error(`Token exchange failed: ${err}`);
}
const data = await resp.json();
const tokens: DropboxTokens = {
access_token: data.access_token,
refresh_token: data.refresh_token,
expires_at: Date.now() + data.expires_in * 1000,
};
fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2));
return tokens;
}
function loadTokens(): DropboxTokens | null {
try {
if (fs.existsSync(TOKEN_PATH)) {
return JSON.parse(fs.readFileSync(TOKEN_PATH, "utf-8"));
}
} catch {}
return null;
}
async function refreshAccessToken(refreshToken: string): Promise<DropboxTokens> {
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(),
}),
});
if (!resp.ok) {
const err = await resp.text();
throw new Error(`Token refresh failed: ${err}`);
}
const data = await resp.json();
const tokens: DropboxTokens = {
access_token: data.access_token,
refresh_token: refreshToken,
expires_at: Date.now() + data.expires_in * 1000,
};
fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2));
return tokens;
}
async function getValidAccessToken(): Promise<string | null> {
let tokens = loadTokens();
if (!tokens) return null;
if (Date.now() > tokens.expires_at - 60000) {
tokens = await refreshAccessToken(tokens.refresh_token);
}
return tokens.access_token;
}
export function isConnected(): boolean {
return loadTokens() !== null;
}
async function listFolder(accessToken: string, folderPath: string): Promise<any[]> {
const entries: any[] = [];
let cursor: string | null = null;
let hasMore = true;
while (hasMore) {
const url = cursor
? "https://api.dropboxapi.com/2/files/list_folder/continue"
: "https://api.dropboxapi.com/2/files/list_folder";
const body = cursor
? { cursor }
: { path: folderPath, recursive: false, limit: 2000 };
const resp = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!resp.ok) {
const err = await resp.text();
throw new Error(`List folder failed: ${err}`);
}
const data = await resp.json();
entries.push(...data.entries);
hasMore = data.has_more;
cursor = data.cursor;
}
return entries;
}
async function getThumbnailBatch(accessToken: string, paths: string[]): Promise<Map<string, string>> {
const results = new Map<string, string>();
const batchSize = 25;
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",
}));
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}`);
}
}
}
} 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 {}
}
return results;
}
const galleryCache: { data: GalleryImage[]; timestamp: number } = { data: [], timestamp: 0 };
const CACHE_DURATION = 30 * 60 * 1000;
export async function fetchGalleryFromDropbox(): Promise<GalleryImage[]> {
if (galleryCache.data.length > 0 && Date.now() - galleryCache.timestamp < CACHE_DURATION) {
return galleryCache.data;
}
const accessToken = await getValidAccessToken();
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)
);
const paths = imageFiles.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,
});
}
}
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));
return images;
} catch (err: any) {
console.error("Dropbox gallery fetch error:", err.message);
return [];
}
}