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
This commit is contained in:
parent
6279a35ab2
commit
77fdd872f6
267
server/dropbox.ts
Normal file
267
server/dropbox.ts
Normal file
@ -0,0 +1,267 @@
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
@ -6,6 +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 } from "./dropbox";
|
||||
import multer from "multer";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
@ -195,12 +196,58 @@ export async function registerRoutes(
|
||||
}
|
||||
});
|
||||
|
||||
// Gallery API - serves shuffled photos from Dropbox
|
||||
app.get("/api/gallery", (_req, res) => {
|
||||
// Dropbox OAuth
|
||||
app.get("/api/dropbox/auth", (_req, res) => {
|
||||
const host = _req.headers.host || "news-folx-tv.replit.app";
|
||||
const protocol = _req.headers["x-forwarded-proto"] || "https";
|
||||
const redirectUri = `${protocol}://${host}/api/dropbox/callback`;
|
||||
const url = getAuthUrl(redirectUri);
|
||||
res.redirect(url);
|
||||
});
|
||||
|
||||
app.get("/api/dropbox/callback", async (req, res) => {
|
||||
const code = req.query.code as string;
|
||||
if (!code) {
|
||||
return res.status(400).send("Manjka avtorizacijska koda.");
|
||||
}
|
||||
try {
|
||||
const galleryPath = path.join(process.cwd(), "server/gallery-data.json");
|
||||
const data = JSON.parse(fs.readFileSync(galleryPath, "utf-8"));
|
||||
// Shuffle using Fisher-Yates
|
||||
const host = req.headers.host || "news-folx-tv.replit.app";
|
||||
const protocol = req.headers["x-forwarded-proto"] || "https";
|
||||
const redirectUri = `${protocol}://${host}/api/dropbox/callback`;
|
||||
await exchangeCodeForTokens(code, redirectUri);
|
||||
res.send("<html><body style='background:#111;color:#fff;font-family:sans-serif;text-align:center;padding:60px'><h1>Dropbox povezan!</h1><p>Lahko zaprete to stran.</p></body></html>");
|
||||
} catch (err: any) {
|
||||
res.status(500).send(`Napaka: ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/dropbox/status", (_req, res) => {
|
||||
res.json({ connected: isConnected() });
|
||||
});
|
||||
|
||||
app.post("/api/dropbox/refresh-gallery", async (_req, res) => {
|
||||
try {
|
||||
const images = await fetchGalleryFromDropbox();
|
||||
res.json({ count: images.length });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Gallery API - serves shuffled photos from Dropbox
|
||||
app.get("/api/gallery", async (_req, res) => {
|
||||
try {
|
||||
let data: any[] = [];
|
||||
|
||||
if (isConnected()) {
|
||||
data = await fetchGalleryFromDropbox();
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
const galleryPath = path.join(process.cwd(), "server/gallery-data.json");
|
||||
data = JSON.parse(fs.readFileSync(galleryPath, "utf-8"));
|
||||
}
|
||||
|
||||
const shuffled = [...data];
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user