d079d76151
- Snapshots direkt im Datasets-Tab ladbar (lazy, per Pool) - Tabelle: Name, Created, Used, Referenced, Clones - Kontextmenü (⋮) mit Clone, Rename, Roll Back, Destroy Snapshot - Backend: /api/snapshots/clone + /api/snapshots/rename Endpoints Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
507 lines
15 KiB
TypeScript
507 lines
15 KiB
TypeScript
import axios, { AxiosInstance } from "axios"
|
|
|
|
export interface Pool {
|
|
name: string
|
|
size: number
|
|
alloc: number
|
|
free: number
|
|
fragmentation: string
|
|
capacity: string
|
|
health: "ONLINE" | "DEGRADED" | "FAULTED" | "OFFLINE" | "UNAVAIL"
|
|
}
|
|
|
|
export interface Vdev {
|
|
name: string
|
|
state: string
|
|
read: number
|
|
write: number
|
|
cksum: number
|
|
children?: Vdev[]
|
|
}
|
|
|
|
export interface PoolStatus extends Pool {
|
|
state?: string
|
|
scan?: string
|
|
errors?: string
|
|
vdevs: Vdev[]
|
|
}
|
|
|
|
export interface Dataset {
|
|
name: string
|
|
type: "filesystem" | "volume" | "snapshot"
|
|
used: number
|
|
avail: number
|
|
refer: number
|
|
mountpoint?: string
|
|
compression?: string
|
|
quota?: number
|
|
reservation?: number
|
|
}
|
|
|
|
export interface DatasetProperties {
|
|
compression?: string
|
|
quota?: number
|
|
reservation?: number
|
|
}
|
|
|
|
export interface Snapshot {
|
|
name: string
|
|
dataset?: string
|
|
created: number
|
|
used: number
|
|
referenced: number
|
|
creation_datetime?: string
|
|
}
|
|
|
|
export interface SambaShare {
|
|
name: string
|
|
path: string
|
|
comment?: string
|
|
read_only?: boolean
|
|
guest_ok?: boolean
|
|
valid_users?: string
|
|
}
|
|
|
|
export interface NfsShare {
|
|
path: string
|
|
clients: string
|
|
options?: string
|
|
}
|
|
|
|
export interface SystemInfo {
|
|
hostname: string
|
|
uptime: number
|
|
memory_total: number
|
|
memory_used: number
|
|
memory_available: number
|
|
cpu_count: number
|
|
cpu_model: string
|
|
os: string
|
|
}
|
|
|
|
export interface SystemStatus {
|
|
status: string
|
|
zfs_available: boolean
|
|
version: string
|
|
}
|
|
|
|
export interface SystemUser {
|
|
username: string
|
|
uid: number
|
|
gid: number
|
|
home: string
|
|
shell: string
|
|
gecos?: string
|
|
locked?: boolean
|
|
groups: string[]
|
|
}
|
|
|
|
export interface SystemGroup {
|
|
groupname: string
|
|
gid: number
|
|
members: string[]
|
|
}
|
|
|
|
export interface LoginEntry {
|
|
user: string
|
|
terminal: string
|
|
host: string
|
|
login_time: string
|
|
logout_time?: string
|
|
duration?: string
|
|
}
|
|
|
|
export class ZFSManagerAPI {
|
|
private client: AxiosInstance
|
|
private token: string | null = null
|
|
|
|
constructor(baseURL: string = process.env.NEXT_PUBLIC_API_URL || "") {
|
|
this.client = axios.create({
|
|
baseURL,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
})
|
|
|
|
if (typeof window !== "undefined") {
|
|
this.token = localStorage.getItem("access_token")
|
|
if (this.token) {
|
|
this.setAuthHeader()
|
|
}
|
|
}
|
|
|
|
this.client.interceptors.response.use(
|
|
(response) => response,
|
|
(error) => {
|
|
// Don't redirect on login endpoint itself
|
|
const isLoginEndpoint = error.config?.url?.includes("/api/auth/login")
|
|
|
|
if (error.response?.status === 401 && !isLoginEndpoint) {
|
|
localStorage.removeItem("access_token")
|
|
this.token = null
|
|
if (typeof window !== "undefined") {
|
|
window.location.href = "/login"
|
|
}
|
|
}
|
|
return Promise.reject(error)
|
|
}
|
|
)
|
|
}
|
|
|
|
private setAuthHeader() {
|
|
if (this.token) {
|
|
this.client.defaults.headers.common["Authorization"] = `Bearer ${this.token}`
|
|
}
|
|
}
|
|
|
|
// Auth
|
|
async login(username: string, password: string): Promise<{ access_token: string; token_type: string }> {
|
|
// Clear any existing token before login
|
|
localStorage.removeItem("access_token")
|
|
this.token = null
|
|
this.client.defaults.headers.common["Authorization"] = ""
|
|
|
|
try {
|
|
const response = await this.client.post("/api/auth/login", { username, password })
|
|
this.token = response.data.access_token
|
|
this.setAuthHeader()
|
|
if (this.token) {
|
|
localStorage.setItem("access_token", this.token)
|
|
}
|
|
return response.data
|
|
} catch (error: any) {
|
|
// Clear any invalid token on login failure
|
|
localStorage.removeItem("access_token")
|
|
this.token = null
|
|
this.client.defaults.headers.common["Authorization"] = ""
|
|
|
|
// Provide better error message
|
|
const message = error?.response?.data?.detail || error?.message || "Login failed"
|
|
const err = new Error(message)
|
|
throw err
|
|
}
|
|
}
|
|
|
|
async logout() {
|
|
this.token = null
|
|
this.client.defaults.headers.common["Authorization"] = ""
|
|
localStorage.removeItem("access_token")
|
|
}
|
|
|
|
async verifyToken(): Promise<{ valid: boolean; username: string }> {
|
|
try {
|
|
const response = await this.client.post("/api/auth/verify")
|
|
return response.data
|
|
} catch {
|
|
return { valid: false, username: "" }
|
|
}
|
|
}
|
|
|
|
// System Status (no auth required)
|
|
async getSystemStatus(): Promise<SystemStatus> {
|
|
try {
|
|
const response = await this.client.get("/api/status")
|
|
return response.data
|
|
} catch {
|
|
return { status: "unknown", zfs_available: false, version: "" }
|
|
}
|
|
}
|
|
|
|
// Pools
|
|
async getPools(): Promise<Pool[]> {
|
|
const response = await this.client.get("/api/pools/")
|
|
return response.data
|
|
}
|
|
|
|
async getPoolStatus(name: string): Promise<PoolStatus> {
|
|
const response = await this.client.get(`/api/pools/${name}`)
|
|
return response.data
|
|
}
|
|
|
|
async startScrub(poolName: string): Promise<{ status: string }> {
|
|
const response = await this.client.post(`/api/pools/${poolName}/scrub`)
|
|
return response.data
|
|
}
|
|
|
|
// Datasets
|
|
async getDatasets(pool: string = "tank"): Promise<Dataset[]> {
|
|
const response = await this.client.get("/api/datasets/", { params: { pool } })
|
|
return response.data
|
|
}
|
|
|
|
async createDataset(name: string, properties?: Record<string, string>): Promise<{ status: string }> {
|
|
const response = await this.client.post("/api/datasets/", { name, properties })
|
|
return response.data
|
|
}
|
|
|
|
async updateDatasetProperties(name: string, props: DatasetProperties): Promise<{ status: string }> {
|
|
const response = await this.client.patch(`/api/datasets/${name}`, props)
|
|
return response.data
|
|
}
|
|
|
|
async deleteDataset(name: string, recursive = false): Promise<{ status: string }> {
|
|
const response = await this.client.delete(`/api/datasets/${name}`, { params: { recursive } })
|
|
return response.data
|
|
}
|
|
|
|
// Snapshots
|
|
async getSnapshots(dataset?: string, limit = 50): Promise<Snapshot[]> {
|
|
const response = await this.client.get("/api/snapshots/", {
|
|
params: { ...(dataset ? { dataset } : {}), limit },
|
|
})
|
|
return response.data
|
|
}
|
|
|
|
async createSnapshot(dataset: string, name?: string): Promise<{ status: string }> {
|
|
const response = await this.client.post("/api/snapshots/", { dataset, name })
|
|
return response.data
|
|
}
|
|
|
|
async deleteSnapshot(name: string): Promise<{ status: string }> {
|
|
const response = await this.client.delete(`/api/snapshots/${name}`)
|
|
return response.data
|
|
}
|
|
|
|
async rollbackSnapshot(snapshot: string): Promise<{ status: string }> {
|
|
const response = await this.client.post("/api/snapshots/rollback", { snapshot })
|
|
return response.data
|
|
}
|
|
|
|
async cloneSnapshot(snapshot: string, target: string): Promise<{ status: string }> {
|
|
const response = await this.client.post("/api/snapshots/clone", { snapshot, target })
|
|
return response.data
|
|
}
|
|
|
|
async renameSnapshot(snapshot: string, new_name: string): Promise<{ status: string }> {
|
|
const response = await this.client.post("/api/snapshots/rename", { snapshot, new_name })
|
|
return response.data
|
|
}
|
|
|
|
// Shares — Samba
|
|
async getSambaShares(): Promise<SambaShare[]> {
|
|
const response = await this.client.get("/api/shares/samba")
|
|
return response.data.shares ?? response.data
|
|
}
|
|
|
|
async createSambaShare(share: SambaShare): Promise<{ status: string }> {
|
|
const response = await this.client.post("/api/shares/samba", share)
|
|
return response.data
|
|
}
|
|
|
|
async updateSambaShare(oldName: string, share: SambaShare): Promise<{ status: string }> {
|
|
const response = await this.client.put(`/api/shares/samba/${oldName}`, share)
|
|
return response.data
|
|
}
|
|
|
|
async deleteSambaShare(name: string): Promise<{ status: string }> {
|
|
const response = await this.client.delete(`/api/shares/samba/${name}`)
|
|
return response.data
|
|
}
|
|
|
|
async getSambaConfig(): Promise<{ parameters: Array<{ key: string; value: string }> }> {
|
|
const response = await this.client.get("/api/shares/samba/config")
|
|
return response.data
|
|
}
|
|
|
|
async setSambaConfig(parameters: { [key: string]: string }): Promise<{ status: string }> {
|
|
const response = await this.client.put("/api/shares/samba/config", { parameters })
|
|
return response.data
|
|
}
|
|
|
|
// Shares — NFS
|
|
async getNfsShares(): Promise<NfsShare[]> {
|
|
const response = await this.client.get("/api/shares/nfs")
|
|
return response.data.shares ?? response.data
|
|
}
|
|
|
|
async createNfsShare(share: NfsShare): Promise<{ status: string }> {
|
|
const response = await this.client.post("/api/shares/nfs", share)
|
|
return response.data
|
|
}
|
|
|
|
async deleteNfsShare(path: string): Promise<{ status: string }> {
|
|
const response = await this.client.delete("/api/shares/nfs", { params: { path } })
|
|
return response.data
|
|
}
|
|
|
|
// Shares Configuration
|
|
async getSambaGlobalConfig(): Promise<{ [key: string]: any }> {
|
|
const response = await this.client.get("/api/shares/samba/config")
|
|
return response.data
|
|
}
|
|
|
|
async setSambaGlobalConfig(config: string): Promise<{ status: string }> {
|
|
const response = await this.client.put("/api/shares/samba/config", { config })
|
|
return response.data
|
|
}
|
|
|
|
async getNfsGlobalConfig(): Promise<{ exports: string; path?: string }> {
|
|
const response = await this.client.get("/api/shares/nfs/config")
|
|
return response.data
|
|
}
|
|
|
|
async setNfsGlobalConfig(config: string): Promise<{ status: string }> {
|
|
const response = await this.client.put("/api/shares/nfs/config", { config })
|
|
return response.data
|
|
}
|
|
|
|
// System
|
|
async getSystemInfo(): Promise<SystemInfo> {
|
|
const response = await this.client.get("/api/system/info")
|
|
return response.data
|
|
}
|
|
|
|
async getMemory(): Promise<{ total: number; used: number; available: number; swap_total: number; swap_used: number }> {
|
|
const response = await this.client.get("/api/system/memory")
|
|
return response.data
|
|
}
|
|
|
|
async getCpuInfo(): Promise<{ count: number; percent?: number; load_average: number[] }> {
|
|
const response = await this.client.get("/api/system/cpu")
|
|
return response.data
|
|
}
|
|
|
|
async getUptime(): Promise<{ uptime_seconds: number; uptime_string: string }> {
|
|
const response = await this.client.get("/api/system/uptime")
|
|
return response.data
|
|
}
|
|
|
|
async getNetwork(): Promise<{ interfaces: any[] }> {
|
|
const response = await this.client.get("/api/system/network")
|
|
return response.data
|
|
}
|
|
|
|
async getNetworkTraffic(): Promise<{ interfaces: any[] }> {
|
|
const response = await this.client.get("/api/system/network/traffic")
|
|
return response.data
|
|
}
|
|
|
|
async getDiskIO(): Promise<{ disks: any[] }> {
|
|
const response = await this.client.get("/api/system/diskio")
|
|
return response.data
|
|
}
|
|
|
|
async getServices(): Promise<{ services: any[] }> {
|
|
const response = await this.client.get("/api/system/services")
|
|
return response.data
|
|
}
|
|
|
|
async getUnits(): Promise<{
|
|
services: any[]
|
|
targets: any[]
|
|
sockets: any[]
|
|
timers: any[]
|
|
paths: any[]
|
|
}> {
|
|
const response = await this.client.get("/api/system/units")
|
|
return response.data
|
|
}
|
|
|
|
async getSystemLogs(limit: number = 20): Promise<{ logs: string[] }> {
|
|
const response = await this.client.get(`/api/system/logs?limit=${limit}`)
|
|
return response.data
|
|
}
|
|
|
|
async getHealth(): Promise<{ status: string; version: string }> {
|
|
const response = await this.client.get("/health")
|
|
return response.data
|
|
}
|
|
|
|
// Identities - Users
|
|
async getUsers(): Promise<SystemUser[]> {
|
|
const response = await this.client.get("/api/identities/users")
|
|
return response.data.users ?? []
|
|
}
|
|
|
|
async createUser(username: string, home_dir?: string, shell?: string, gecos?: string): Promise<{ status: string }> {
|
|
const response = await this.client.post("/api/identities/users", { username, home_dir, shell, gecos })
|
|
return response.data
|
|
}
|
|
|
|
async deleteUser(username: string, remove_home: boolean = true): Promise<{ status: string }> {
|
|
const response = await this.client.delete(`/api/identities/users/${username}`, { params: { remove_home } })
|
|
return response.data
|
|
}
|
|
|
|
async changePassword(username: string, password: string): Promise<{ status: string }> {
|
|
const response = await this.client.post(`/api/identities/users/${username}/password`, { password })
|
|
return response.data
|
|
}
|
|
|
|
async changeShell(username: string, shell: string): Promise<{ status: string }> {
|
|
const response = await this.client.post(`/api/identities/users/${username}/shell`, { shell })
|
|
return response.data
|
|
}
|
|
|
|
async lockUser(username: string): Promise<{ status: string }> {
|
|
const response = await this.client.post(`/api/identities/users/${username}/lock`)
|
|
return response.data
|
|
}
|
|
|
|
async unlockUser(username: string): Promise<{ status: string }> {
|
|
const response = await this.client.post(`/api/identities/users/${username}/unlock`)
|
|
return response.data
|
|
}
|
|
|
|
async setSambaPassword(username: string, password: string): Promise<{ status: string }> {
|
|
const response = await this.client.post(`/api/identities/users/${username}/samba-password`, { password })
|
|
return response.data
|
|
}
|
|
|
|
// Identities - Groups
|
|
async getGroups(): Promise<SystemGroup[]> {
|
|
const response = await this.client.get("/api/identities/groups")
|
|
return response.data.groups ?? []
|
|
}
|
|
|
|
async createGroup(groupname: string): Promise<{ status: string }> {
|
|
const response = await this.client.post("/api/identities/groups", { groupname })
|
|
return response.data
|
|
}
|
|
|
|
async deleteGroup(groupname: string): Promise<{ status: string }> {
|
|
const response = await this.client.delete(`/api/identities/groups/${groupname}`)
|
|
return response.data
|
|
}
|
|
|
|
async addUserToGroup(username: string, groupname: string): Promise<{ status: string }> {
|
|
const response = await this.client.post(`/api/identities/users/${username}/groups`, { groupname })
|
|
return response.data
|
|
}
|
|
|
|
async removeUserFromGroup(username: string, groupname: string): Promise<{ status: string }> {
|
|
const response = await this.client.delete(`/api/identities/users/${username}/groups/${groupname}`)
|
|
return response.data
|
|
}
|
|
|
|
// Identities - Samba Users
|
|
async getSambaUsers(): Promise<SystemUser[]> {
|
|
const response = await this.client.get("/api/identities/samba-users")
|
|
return response.data.users ?? []
|
|
}
|
|
|
|
// Identities - Login History
|
|
async getLoginHistory(limit: number = 50): Promise<LoginEntry[]> {
|
|
const response = await this.client.get("/api/identities/login-history", { params: { limit } })
|
|
return response.data.logins ?? []
|
|
}
|
|
|
|
// Navigator - Copy, Move, Search
|
|
async copyFile(src: string, dst: string, overwrite: boolean = false): Promise<{ status: string }> {
|
|
const response = await this.client.post("/api/navigator/copy", { src, dst, overwrite })
|
|
return response.data
|
|
}
|
|
|
|
async moveFile(src: string, dst: string, overwrite: boolean = false): Promise<{ status: string }> {
|
|
const response = await this.client.post("/api/navigator/move", { src, dst, overwrite })
|
|
return response.data
|
|
}
|
|
|
|
async searchFiles(q: string, path: string = "", limit: number = 50): Promise<any[]> {
|
|
const response = await this.client.get("/api/navigator/search", { params: { q, path, limit } })
|
|
return response.data.results ?? []
|
|
}
|
|
}
|
|
|
|
export const api = new ZFSManagerAPI()
|