import { createClient } from '@supabase/supabase-js'; export const FB_SOURCE = 'srv'; export const PROJECT_DEFAULT_SUBFOLDERS = ['00 Project Files']; export const REQUEST_DEFAULT_SUBFOLDERS = ['Old Books', 'Working Files', 'Survey']; export function normalizePath(path) { const raw = String(path || '/').trim(); const parts = raw.split('/').filter(Boolean); const clean = []; for (const part of parts) { if (part === '.') continue; if (part === '..') throw new Error('Invalid path: path traversal not allowed'); clean.push(part); } return `/${clean.join('/')}`; } export function joinPath(...parts) { return normalizePath(parts.join('/')); } export function safeName(value, fallback = '') { const cleaned = String(value || '') .trim() .replace(/[\\/:*?"<>|#%{}^~[\]`]+/g, '-') .replace(/\s+/g, ' ') .replace(/^-+|-+$/g, ''); return cleaned || fallback; } export function getConfig() { const url = String(process.env.FILEBROWSER_URL || '').trim().replace(/\/+$/, ''); return { url, token: process.env.FILEBROWSER_TOKEN || '', clientRoot: normalizePath(process.env.FILEBROWSER_CLIENT_ROOT || '/fourgebranding/Clients'), subsRoot: normalizePath(process.env.FILEBROWSER_SUBS_ROOT || '/fourgebranding/team'), configured: Boolean(url), }; } export function getToken(config) { const token = String(config.token || '').trim(); if (!token) throw new Error('FILEBROWSER_TOKEN not configured'); return token; } export async function fbFetch(config, method, endpoint, { params = {}, headers = {}, body } = {}) { const qs = new URLSearchParams({ source: FB_SOURCE, ...params }).toString(); const url = `${config.url}${endpoint}?${qs}`; const token = getToken(config); const response = await fetch(url, { method, headers: { Authorization: `Bearer ${token}`, ...headers }, body, }); if (!response.ok) { const text = await response.text().catch(() => ''); const error = new Error(text || `FileBrowser ${response.status}`); error.status = response.status; throw error; } const text = await response.text(); try { return text ? JSON.parse(text) : null; } catch { return text; } } function isAlreadyExistsError(error) { const message = String(error?.message || '').toLowerCase(); return error?.status === 409 || message.includes('exist') || message.includes('conflict'); } // Creates a single directory whose parent is already known to exist. // Much cheaper than ensureDirectory, which re-walks the path from the root. export async function createDirectory(config, fullPath) { try { await fbFetch(config, 'POST', '/api/resources', { params: { path: normalizePath(fullPath), isDir: 'true' }, }); } catch (error) { if (!isAlreadyExistsError(error)) throw error; } } // Runs tasks with bounded concurrency, preserving input order in the result. export async function pooled(items, limit, worker) { const results = new Array(items.length); let cursor = 0; const runners = Array.from({ length: Math.min(limit, items.length) }, async () => { while (cursor < items.length) { const index = cursor; cursor += 1; results[index] = await worker(items[index], index); } }); await Promise.all(runners); return results; } export async function ensureDirectory(config, fullPath) { const parts = normalizePath(fullPath).split('/').filter(Boolean); let current = '/'; for (const part of parts) { current = joinPath(current, part); try { await fbFetch(config, 'POST', '/api/resources', { params: { path: current, isDir: 'true' }, }); } catch (error) { if (!isAlreadyExistsError(error)) throw error; } } } // Returns directory names at `path`, or null when the path itself does not exist. export async function listDirectories(config, path) { let payload; try { payload = await fbFetch(config, 'GET', '/api/resources', { params: { path: normalizePath(path) } }); } catch (error) { if (error?.status === 404) return null; throw error; } // This FileBrowser returns child directories under `folders`; older builds use a mixed `items` array. if (Array.isArray(payload?.folders)) return payload.folders.map(folder => folder.name); const items = payload?.items || []; return items.filter(item => item.type === 'directory' || item.isDir).map(item => item.name); } export function createAdminClient() { const supabaseUrl = process.env.VITE_SUPABASE_URL || process.env.SUPABASE_URL; const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY; if (!supabaseUrl || !serviceKey) throw new Error('Supabase admin env not configured'); return createClient(supabaseUrl, serviceKey, { auth: { persistSession: false, autoRefreshToken: false }, }); }