Files tab: browser with breadcrumbs/sort/filter, text editor, image preview, streamed upload with progress, download, mkdir/touch/rename/move/chmod/delete, open-in-terminal; root toggle via sudo-reexecuted 'hamlab fs' helper
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
38d4ee09ae
commit
63b020d556
@@ -76,7 +76,40 @@ export interface SmartInfo { device: string; available: boolean; supported: bool
|
||||
export interface ZFSSnapshot { name: string; used: number; created: number }
|
||||
export interface ZFSInfo { available: boolean; message?: string; pools: { name: string; size: number; alloc: number; free: number; frag: number; cap: number; health: string; dedup: string; scan?: string; errors?: string; config?: string }[]; datasets: { name: string; type: string; used: number; avail: number; refer: number; used_by_snapshots: number; mount?: string; compressratio: string; snapshots: number }[] }
|
||||
|
||||
export interface FileEntry { name: string; type: 'dir' | 'file' | 'link' | 'other'; size: number; mode: string; perm: string; mtime: number; user: string; group: string; target?: string; broken?: boolean }
|
||||
export interface FileInfo extends FileEntry { path: string; text: boolean }
|
||||
|
||||
const q = (path: string, sudo: boolean, extra = '') => `path=${encodeURIComponent(path)}${sudo ? '&sudo=1' : ''}${extra}`
|
||||
|
||||
export const api = {
|
||||
files: (path: string, sudo: boolean) => req<{ path: string; parent?: string; entries: FileEntry[] }>('GET', `/api/files?${q(path, sudo)}`),
|
||||
fileStat: (path: string, sudo: boolean) => req<FileInfo>('GET', `/api/files/stat?${q(path, sudo)}`),
|
||||
fileRead: async (path: string, sudo: boolean): Promise<string> => {
|
||||
const r = await fetch(`/api/files/read?${q(path, sudo, '&max=2097152')}`, { credentials: 'same-origin' })
|
||||
if (r.status === 401) throw new Unauthenticated()
|
||||
if (!r.ok) { let m = r.statusText; try { m = (await r.json()).error ?? m } catch { /* */ } throw new Error(m) }
|
||||
return r.text()
|
||||
},
|
||||
fileReadURL: (path: string, sudo: boolean) => `/api/files/read?${q(path, sudo)}`,
|
||||
fileDownloadURL: (path: string, sudo: boolean) => `/api/files/download?${q(path, sudo)}`,
|
||||
fileWrite: async (path: string, text: string, sudo: boolean) => {
|
||||
const r = await fetch(`/api/files/write?${q(path, sudo)}`, { method: 'PUT', credentials: 'same-origin', body: text, headers: { 'Content-Type': 'application/octet-stream' } })
|
||||
if (r.status === 401) throw new Unauthenticated()
|
||||
if (!r.ok) { let m = r.statusText; try { m = (await r.json()).error ?? m } catch { /* */ } throw new Error(m) }
|
||||
},
|
||||
fileUpload: (dir: string, file: File, sudo: boolean, onProgress: (pct: number) => void) => new Promise<void>((resolve, reject) => {
|
||||
const x = new XMLHttpRequest()
|
||||
x.open('PUT', `/api/files/upload?${q(dir, sudo)}&name=${encodeURIComponent(file.name)}`)
|
||||
x.upload.onprogress = (e) => { if (e.lengthComputable) onProgress(Math.round((e.loaded / e.total) * 100)) }
|
||||
x.onload = () => { if (x.status >= 200 && x.status < 300) resolve(); else { let m = x.statusText; try { m = JSON.parse(x.responseText).error ?? m } catch { /* */ } reject(new Error(m)) } }
|
||||
x.onerror = () => reject(new Error('network error'))
|
||||
x.send(file)
|
||||
}),
|
||||
fileMkdir: (path: string, sudo: boolean) => req<void>('POST', `/api/files/mkdir${sudo ? '?sudo=1' : ''}`, { path }),
|
||||
fileTouch: (path: string, sudo: boolean) => req<void>('POST', `/api/files/touch${sudo ? '?sudo=1' : ''}`, { path }),
|
||||
fileRename: (path: string, to: string, sudo: boolean) => req<void>('POST', `/api/files/rename${sudo ? '?sudo=1' : ''}`, { path, to }),
|
||||
fileDelete: (path: string, recursive: boolean, sudo: boolean) => req<void>('POST', `/api/files/delete${sudo ? '?sudo=1' : ''}`, { path, recursive }),
|
||||
fileChmod: (path: string, mode: string, sudo: boolean) => req<void>('POST', `/api/files/chmod${sudo ? '?sudo=1' : ''}`, { path, mode }),
|
||||
storage: () => req<{ devices: StorageDevice[]; zfs: ZFSInfo; smartctl: boolean }>('GET', '/api/storage'),
|
||||
smart: (dev: string, force = false) => req<SmartInfo>('GET', `/api/storage/smart/${dev}${force ? '?force=1' : ''}`),
|
||||
smartTest: (dev: string, kind: 'short' | 'long') => req<void>('POST', `/api/storage/smart/${dev}/test/${kind}`),
|
||||
|
||||
Reference in New Issue
Block a user