archy/neode-ui/src/api/filebrowser-client.ts

140 lines
4.5 KiB
TypeScript
Raw Normal View History

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<boolean> {
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<string, string> {
const h: Record<string, string> = {}
if (this.token) h['X-Auth'] = this.token
return h
}
async listDirectory(path: string): Promise<FileBrowserItem[]> {
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<void> {
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<void> {
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<void> {
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<void> {
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()