337 lines
10 KiB
TypeScript
337 lines
10 KiB
TypeScript
export interface SambaShare {
|
|
id: number;
|
|
name: string;
|
|
path: string;
|
|
comment: string;
|
|
read_only: boolean;
|
|
guest_ok: boolean;
|
|
valid_users: string[];
|
|
valid_groups: string[];
|
|
invalid_users: string[];
|
|
}
|
|
|
|
export interface NFSClient {
|
|
host: string;
|
|
read_only: boolean;
|
|
async: boolean;
|
|
root_squash: boolean;
|
|
subtree_check: boolean;
|
|
advanced: NFSAdvanced;
|
|
}
|
|
|
|
export interface NFSAdvanced {
|
|
all_squash: boolean;
|
|
secure: boolean;
|
|
wdelay: boolean;
|
|
hide: boolean;
|
|
crossmnt: boolean;
|
|
[key: string]: boolean;
|
|
}
|
|
|
|
export interface NFSExport {
|
|
id: number;
|
|
path: string;
|
|
clients: NFSClient[];
|
|
read_only: boolean;
|
|
async: boolean;
|
|
root_squash: boolean;
|
|
subtree_check: boolean;
|
|
fsid: number;
|
|
advanced: string;
|
|
}
|
|
|
|
export interface User {
|
|
id: number;
|
|
username: string;
|
|
groups: string[];
|
|
smb_enabled: boolean;
|
|
disabled: boolean;
|
|
}
|
|
|
|
export interface DirtyModule {
|
|
module: string;
|
|
marked_at: string;
|
|
}
|
|
|
|
export interface ApplyLogEntry {
|
|
id: number;
|
|
module: string;
|
|
message: string;
|
|
success: boolean;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface DiskUsage {
|
|
path: string;
|
|
total_bytes: number;
|
|
free_bytes: number;
|
|
used_bytes: number;
|
|
used_percent: number;
|
|
sources: ("manual" | "samba" | "nfs")[];
|
|
used_by?: string[];
|
|
available: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
export interface WatchedMount {
|
|
id: number;
|
|
path: string;
|
|
}
|
|
|
|
export interface ServiceStatus {
|
|
name: string;
|
|
active: boolean;
|
|
state: string;
|
|
}
|
|
|
|
export interface SystemStatus {
|
|
disks: DiskUsage[];
|
|
services: ServiceStatus[];
|
|
}
|
|
|
|
export interface VersionInfo {
|
|
version: string;
|
|
commit: string;
|
|
}
|
|
|
|
export interface FileEntry {
|
|
name: string;
|
|
path: string;
|
|
is_dir: boolean;
|
|
size: number;
|
|
mode: string;
|
|
mode_num: number;
|
|
mod_time: number;
|
|
}
|
|
|
|
export interface FileInfo {
|
|
path: string;
|
|
name: string;
|
|
is_dir: boolean;
|
|
size: number;
|
|
mode: string;
|
|
mode_num: number;
|
|
mod_time: number;
|
|
uid: number;
|
|
gid: number;
|
|
total_bytes?: number;
|
|
free_bytes?: number;
|
|
}
|
|
|
|
export interface DirList {
|
|
entries: FileEntry[];
|
|
path: string;
|
|
total: number;
|
|
page: number;
|
|
limit: number;
|
|
has_more: boolean;
|
|
}
|
|
|
|
export interface SearchHit {
|
|
name: string;
|
|
path: string;
|
|
is_dir: boolean;
|
|
size: number;
|
|
}
|
|
|
|
export interface FileCapabilities {
|
|
chmod: boolean;
|
|
chown: boolean;
|
|
}
|
|
|
|
export interface FilePreview {
|
|
type: "text" | "image" | "binary";
|
|
content?: string;
|
|
mime: string;
|
|
size: number;
|
|
}
|
|
|
|
export type JobKind =
|
|
| "mergerfs_preview"
|
|
| "mergerfs_move"
|
|
| "snapraid_diff"
|
|
| "snapraid_sync"
|
|
| "snapraid_scrub"
|
|
| "snapraid_check";
|
|
|
|
export interface StorageCapabilities {
|
|
rsync: boolean;
|
|
mergerfs: boolean;
|
|
mergerfs_mounted: boolean;
|
|
snapraid: boolean;
|
|
}
|
|
|
|
export interface StorageConfig {
|
|
id: number;
|
|
mover_source: string;
|
|
mover_dest: string;
|
|
mover_clean_macos: boolean;
|
|
mover_remove_source: boolean;
|
|
mover_inplace: boolean;
|
|
mover_rsync_options: string;
|
|
mover_warning_threshold: number;
|
|
snapraid_conf: string;
|
|
snapraid_data_dirs: string;
|
|
snapraid_parity_dir: string;
|
|
snapraid_scrub_plan: number;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface PathStat {
|
|
path: string;
|
|
available: boolean;
|
|
total_bytes: number;
|
|
used_bytes: number;
|
|
free_bytes: number;
|
|
used_percent: number;
|
|
error?: string;
|
|
}
|
|
|
|
export interface StorageDiskUsage {
|
|
source: PathStat;
|
|
}
|
|
|
|
export interface StorageJob {
|
|
id: number;
|
|
kind: JobKind;
|
|
status: "queued" | "running" | "success" | "failed" | "cancelled" | "interrupted";
|
|
args_json: string;
|
|
pid: number;
|
|
started_at: string | null;
|
|
finished_at: string | null;
|
|
exit_code: number;
|
|
output: string;
|
|
error: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface StorageEvent {
|
|
type: "line" | "status" | "end" | "error";
|
|
line?: string;
|
|
status?: string;
|
|
exit_code?: number;
|
|
message?: string;
|
|
}
|
|
|
|
export class ApiError extends Error {
|
|
status: number;
|
|
constructor(status: number, message: string) {
|
|
super(message);
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
const isFormData = body instanceof FormData;
|
|
const res = await fetch(`/api${path}`, {
|
|
method,
|
|
headers: isFormData ? undefined : body ? { "Content-Type": "application/json" } : undefined,
|
|
body: isFormData ? body : body ? JSON.stringify(body) : undefined,
|
|
});
|
|
if (res.status === 204) {
|
|
return undefined as T;
|
|
}
|
|
const text = await res.text();
|
|
if (!text) {
|
|
throw new ApiError(res.status, "empty response");
|
|
}
|
|
const data = JSON.parse(text);
|
|
if (!res.ok) {
|
|
throw new ApiError(res.status, (data && data.error) || res.statusText);
|
|
}
|
|
return data as T;
|
|
}
|
|
|
|
export const api = {
|
|
// auth
|
|
status: () => request<{ authenticated: boolean; username: string }>("GET", "/auth/status"),
|
|
login: (username: string, password: string) =>
|
|
request<{ username: string }>("POST", "/auth/login", { username, password }),
|
|
logout: () => request<{ ok: boolean }>("POST", "/auth/logout"),
|
|
changePassword: (oldPassword: string, newPassword: string) =>
|
|
request<{ ok: boolean }>("PUT", "/auth/password", { old_password: oldPassword, new_password: newPassword }),
|
|
|
|
// dirty / apply
|
|
dirty: () => request<{ modules: DirtyModule[] | null }>("GET", "/dirty"),
|
|
apply: () => request<{ results: { module: string; applied: boolean; error?: string }[] }>("POST", "/apply"),
|
|
applyLog: () => request<{ entries: ApplyLogEntry[] }>("GET", "/apply/log"),
|
|
systemStatus: () => request<SystemStatus>("GET", "/system/status"),
|
|
version: () => request<VersionInfo>("GET", "/version"),
|
|
|
|
// samba
|
|
listShares: () => request<{ shares: SambaShare[] | null }>("GET", "/samba/shares/"),
|
|
createShare: (s: Partial<SambaShare>) => request<SambaShare>("POST", "/samba/shares/", s),
|
|
updateShare: (id: number, s: Partial<SambaShare>) => request<SambaShare>("PUT", `/samba/shares/${id}/`, s),
|
|
deleteShare: (id: number) => request<void>("DELETE", `/samba/shares/${id}/`),
|
|
|
|
// nfs
|
|
listExports: () => request<{ exports: NFSExport[] | null }>("GET", "/nfs/exports/"),
|
|
createExport: (e: Partial<NFSExport>) => request<NFSExport>("POST", "/nfs/exports/", e),
|
|
updateExport: (id: number, e: Partial<NFSExport>) => request<NFSExport>("PUT", `/nfs/exports/${id}/`, e),
|
|
deleteExport: (id: number) => request<void>("DELETE", `/nfs/exports/${id}/`),
|
|
|
|
// users
|
|
listUsers: () => request<{ users: User[] | null }>("GET", "/users/"),
|
|
createUser: (u: Partial<User> & { password?: string }) => request<User>("POST", "/users/", u),
|
|
updateUser: (id: number, u: Partial<User> & { password?: string }) => request<User>("PUT", `/users/${id}/`, u),
|
|
deleteUser: (id: number) => request<void>("DELETE", `/users/${id}/`),
|
|
|
|
// watched mounts
|
|
listWatchedMounts: () => request<{ mounts: WatchedMount[] }>("GET", "/system/watched-mounts"),
|
|
createWatchedMount: (path: string) => request<WatchedMount>("POST", "/system/watched-mounts", { path }),
|
|
deleteWatchedMount: (id: number) => request<void>("DELETE", `/system/watched-mounts/${id}`),
|
|
|
|
// import
|
|
importSamba: () => request<{ imported: number; message?: string }>("POST", "/import/samba"),
|
|
importNfs: () => request<{ imported: number; message?: string }>("POST", "/import/nfs"),
|
|
importUsers: () => request<{ imported: number; message?: string }>("POST", "/import/users"),
|
|
|
|
// files
|
|
fileCapabilities: () => request<FileCapabilities>("GET", "/files/capabilities"),
|
|
fileRoots: () => request<{ roots: string[]; unrestricted: boolean }>("GET", "/files/roots"),
|
|
listFiles: (path: string, page = 1, limit = 200) =>
|
|
request<DirList>("GET", `/files/?path=${encodeURIComponent(path)}&page=${page}&limit=${limit}`),
|
|
fileInfo: (path: string) => request<FileInfo>("GET", `/files/info?path=${encodeURIComponent(path)}`),
|
|
mkdirFile: (path: string) => request<void>("POST", "/files/mkdir", { path }),
|
|
renameFile: (path: string, newName: string) =>
|
|
request<void>("POST", "/files/rename", { path, newName }),
|
|
chmodFile: (path: string, mode: string) =>
|
|
request<void>("POST", "/files/chmod", { path, mode }),
|
|
chownFile: (path: string, uid: number, gid: number) =>
|
|
request<void>("POST", "/files/chown", { path, uid, gid }),
|
|
deleteFile: (path: string) => request<void>("DELETE", `/files/?path=${encodeURIComponent(path)}`),
|
|
uploadFile: (dir: string, file: File) => {
|
|
const fd = new FormData();
|
|
fd.append("file", file);
|
|
return request<{ path: string }>("POST", `/files/upload?path=${encodeURIComponent(dir)}`, fd);
|
|
},
|
|
filePreview: (path: string) =>
|
|
request<FilePreview>("GET", `/files/preview?path=${encodeURIComponent(path)}`),
|
|
searchFiles: (path: string, q: string, limit = 100) =>
|
|
request<{ results: SearchHit[] }>(`GET`, `/files/search?path=${encodeURIComponent(path)}&q=${encodeURIComponent(q)}&limit=${limit}`),
|
|
fileDownloadUrl: (path: string) => `/api/files/download?path=${encodeURIComponent(path)}`,
|
|
|
|
// storage
|
|
storageCapabilities: () => request<StorageCapabilities>("GET", "/storage/capabilities"),
|
|
getStorageConfig: () => request<StorageConfig>("GET", "/storage/config"),
|
|
updateStorageConfig: (c: Partial<StorageConfig>) => request<StorageConfig>("PUT", "/storage/config", c),
|
|
storageDiskUsage: () => request<StorageDiskUsage>("GET", "/storage/disk-usage"),
|
|
listStorageJobs: (limit = 50) => request<{ jobs: StorageJob[] }>("GET", `/storage/jobs?limit=${limit}`),
|
|
getStorageJob: (id: number) => request<StorageJob>("GET", `/storage/jobs/${id}`),
|
|
startStorageJob: (kind: JobKind, args?: Record<string, unknown>) =>
|
|
request<StorageJob>("POST", "/storage/jobs", { kind, args }),
|
|
cancelStorageJob: (id: number) => request<{ ok: boolean }>("POST", `/storage/jobs/${id}/cancel`),
|
|
storageJobStreamUrl: (id: number) => `/api/storage/jobs/${id}/stream`,
|
|
getStorageConfFile: () => fetch(`/api/storage/conf-file`).then(r => {
|
|
if (!r.ok) throw new Error(`HTTP ${r.status}: ${r.statusText}`);
|
|
return r.text();
|
|
}),
|
|
};
|
|
|
|
export function formatBytes(bytes: number): string {
|
|
if (bytes === 0) return "0 B";
|
|
const units = ["B", "KB", "MB", "GB", "TB", "PB"];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
|
|
}
|