export interface FileBrowserItem { name: string path: string size: number modified: string isDir: boolean type: string extension: string } interface FileBrowserListResponse { items: FileBrowserItem[] numDirs: number numFiles: number sorting: { by: string; asc: boolean } } class FileBrowserClient { private token: string | null = null private baseUrl: string constructor() { this.baseUrl = `${window.location.origin}/app/filebrowser` } get isAuthenticated(): boolean { return this.token !== null } async login(username = 'admin', password = 'admin'): Promise { try { const res = await fetch(`${this.baseUrl}/api/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }), }) if (!res.ok) return false const text = await res.text() // FileBrowser returns the JWT as a plain string (possibly quoted) this.token = text.replace(/^"|"$/g, '') return true } catch { return false } } private headers(): Record { const h: Record = {} if (this.token) h['X-Auth'] = this.token return h } async listDirectory(path: string): Promise { const safePath = path.startsWith('/') ? path : `/${path}` const res = await fetch(`${this.baseUrl}/api/resources${safePath}`, { headers: this.headers(), }) if (!res.ok) throw new Error(`Failed to list directory: ${res.status}`) const data: FileBrowserListResponse = await res.json() return (data.items || []).map((item) => ({ ...item, extension: item.name.includes('.') ? item.name.split('.').pop()!.toLowerCase() : '', })) } downloadUrl(path: string): string { const safePath = path.startsWith('/') ? path : `/${path}` // Token is passed as query param for direct downloads (img src, audio src, etc.) return `${this.baseUrl}/api/raw${safePath}?auth=${this.token}` } async upload(dirPath: string, file: File): Promise { const safePath = dirPath.endsWith('/') ? dirPath : `${dirPath}/` const encodedName = encodeURIComponent(file.name) const res = await fetch( `${this.baseUrl}/api/resources${safePath}${encodedName}?override=true`, { method: 'POST', headers: this.headers(), body: file, }, ) if (!res.ok) { const text = await res.text().catch(() => '') throw new Error(`Upload failed (${res.status}): ${text}`) } } async createFolder(parentPath: string, name: string): Promise { const safePath = parentPath.endsWith('/') ? parentPath : `${parentPath}/` const res = await fetch(`${this.baseUrl}/api/resources${safePath}${name}/`, { method: 'POST', headers: this.headers(), }) if (!res.ok) throw new Error(`Create folder failed: ${res.status}`) } async deleteItem(path: string): Promise { const safePath = path.startsWith('/') ? path : `/${path}` const res = await fetch(`${this.baseUrl}/api/resources${safePath}`, { method: 'DELETE', headers: this.headers(), }) if (!res.ok) throw new Error(`Delete failed: ${res.status}`) } async getUsage(): Promise<{ totalSize: number; folderCount: number; fileCount: number }> { if (!this.isAuthenticated) { const ok = await this.login() if (!ok) return { totalSize: 0, folderCount: 0, fileCount: 0 } } const res = await fetch(`${this.baseUrl}/api/resources/`, { headers: this.headers(), }) if (!res.ok) return { totalSize: 0, folderCount: 0, fileCount: 0 } const data: FileBrowserListResponse = await res.json() const items = data.items || [] const folderCount = items.filter(i => i.isDir).length const fileCount = items.filter(i => !i.isDir).length const totalSize = items.reduce((sum, i) => sum + (i.size || 0), 0) return { totalSize, folderCount, fileCount } } async rename(oldPath: string, newName: string): Promise { const safePath = oldPath.startsWith('/') ? oldPath : `/${oldPath}` const dir = safePath.substring(0, safePath.lastIndexOf('/') + 1) const res = await fetch(`${this.baseUrl}/api/resources${safePath}`, { method: 'PATCH', headers: { ...this.headers(), 'Content-Type': 'application/json', }, body: JSON.stringify({ destination: `${dir}${newName}` }), }) if (!res.ok) throw new Error(`Rename failed: ${res.status}`) } } export const fileBrowserClient = new FileBrowserClient()