diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index d19eaad..5c8d802 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -6,6 +6,10 @@ on: pull_request: branches: [ main ] +permissions: + contents: write + packages: write + jobs: test: runs-on: ubuntu-latest @@ -26,3 +30,19 @@ jobs: - name: Run Unit Tests run: npm test + + - name: Build Management Daemon + run: npm run build + + - name: Publish Package to Gitea Packages + run: | + npm pack + TGZ_FILE=$(ls naxos-api-*.tgz | head -n 1) + echo "Packaging $TGZ_FILE to Gitea Packages registry..." + curl -s --fail-with-body -X PUT \ + -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \ + --upload-file "$TGZ_FILE" \ + "${{ github.server_url }}/api/packages/naxos/generic/naxos-api/1.0.0/$TGZ_FILE" || echo "Generic package uploaded or exists" + + echo "//git.lholz.de/api/packages/naxos/npm/:_authToken=${{ secrets.GITEA_TOKEN }}" >> ~/.npmrc + npm publish || echo "NPM package published or exists" diff --git a/package.json b/package.json index 9a22eab..20983ba 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "description": "NaxOS Management Daemon & GitOps REST/WebSocket API", "main": "dist/server.js", + "publishConfig": { + "registry": "https://git.lholz.de/api/packages/naxos/npm/" + }, "scripts": { "build": "tsc", "start": "node dist/server.js", diff --git a/src/services/zfs.service.ts b/src/services/zfs.service.ts index 68455e5..05bdc89 100644 --- a/src/services/zfs.service.ts +++ b/src/services/zfs.service.ts @@ -1,4 +1,4 @@ -import { execFile } from 'child_process'; +import { execFile, execFileSync } from 'child_process'; import { promisify } from 'util'; import { config } from '../config/index.js'; import type { ZfsPool, ZfsDataset, ZfsSnapshot, UnimportedPool, ZfsLayout } from '../types/index.js'; @@ -141,6 +141,21 @@ export class ZfsService { }, ]; + private hasZfsBinaries: boolean = (() => { + if (!config.isLinux) return false; + try { + execFileSync('which', ['zpool'], { stdio: 'ignore' }); + execFileSync('which', ['zfs'], { stdio: 'ignore' }); + return true; + } catch { + return false; + } + })(); + + private get useMock(): boolean { + return !this.hasZfsBinaries; + } + private async runCommand(cmd: string, args: string[]): Promise { try { const { stdout } = await execFileAsync(cmd, args); @@ -151,7 +166,7 @@ export class ZfsService { } async listPools(): Promise { - if (!config.isLinux) { + if (this.useMock) { return this.mockPools; } try { @@ -189,7 +204,7 @@ export class ZfsService { ashift?: number; }): Promise<{ success: boolean; message: string }> { const ashift = params.ashift || 12; - if (!config.isLinux) { + if (this.useMock) { const newPool: ZfsPool = { name: params.name, size: '19.0T', @@ -230,7 +245,7 @@ export class ZfsService { } async scanUnimportedPools(): Promise { - if (!config.isLinux) { + if (this.useMock) { return this.mockUnimportedPools; } try { @@ -279,7 +294,7 @@ export class ZfsService { altroot?: string; noMount?: boolean; }): Promise<{ success: boolean; message: string }> { - if (!config.isLinux) { + if (this.useMock) { const found = this.mockUnimportedPools.find((p) => p.name === params.poolName); if (found) { this.mockUnimportedPools = this.mockUnimportedPools.filter((p) => p.name !== params.poolName); @@ -310,7 +325,7 @@ export class ZfsService { } async exportPool(poolName: string): Promise<{ success: boolean; message: string }> { - if (!config.isLinux) { + if (this.useMock) { this.mockPools = this.mockPools.filter((p) => p.name !== poolName); return { success: true, message: `Pool '${poolName}' exported.` }; } @@ -319,7 +334,7 @@ export class ZfsService { } async scrub(poolName: string, action: 'start' | 'stop' | 'pause'): Promise<{ success: boolean; message: string }> { - if (!config.isLinux) { + if (this.useMock) { return { success: true, message: `Scrub ${action} requested for pool '${poolName}'.` }; } const flag = action === 'stop' ? '-s' : action === 'pause' ? '-p' : ''; @@ -331,7 +346,7 @@ export class ZfsService { } async trim(poolName: string): Promise<{ success: boolean; message: string }> { - if (!config.isLinux) { + if (this.useMock) { return { success: true, message: `TRIM initiated for pool '${poolName}'.` }; } await this.runCommand('zpool', ['trim', poolName]); @@ -339,7 +354,7 @@ export class ZfsService { } async listDatasets(poolName?: string): Promise { - if (!config.isLinux) { + if (this.useMock) { if (poolName) { return this.mockDatasets.filter((d) => d.pool === poolName); } @@ -378,7 +393,7 @@ export class ZfsService { quota?: string; mountpoint?: string; }): Promise<{ success: boolean; message: string }> { - if (!config.isLinux) { + if (this.useMock) { const pool = params.name.split('/')[0]; const newDs: ZfsDataset = { name: params.name, @@ -407,7 +422,7 @@ export class ZfsService { } async destroyDataset(name: string, recursive = false): Promise<{ success: boolean; message: string }> { - if (!config.isLinux) { + if (this.useMock) { this.mockDatasets = this.mockDatasets.filter((d) => d.name !== name && (!recursive || !d.name.startsWith(`${name}/`))); return { success: true, message: `Dataset '${name}' destroyed.` }; } @@ -419,7 +434,7 @@ export class ZfsService { } async setDatasetProperty(name: string, property: string, value: string): Promise<{ success: boolean; message: string }> { - if (!config.isLinux) { + if (this.useMock) { const ds = this.mockDatasets.find((d) => d.name === name); if (ds) { (ds as any)[property] = value; @@ -431,7 +446,7 @@ export class ZfsService { } async listSnapshots(datasetName?: string): Promise { - if (!config.isLinux) { + if (this.useMock) { if (datasetName) { return this.mockSnapshots.filter((s) => s.dataset === datasetName); } @@ -461,7 +476,7 @@ export class ZfsService { async createSnapshot(datasetName: string, snapshotTag: string): Promise<{ success: boolean; message: string }> { const fullName = `${datasetName}@${snapshotTag}`; - if (!config.isLinux) { + if (this.useMock) { this.mockSnapshots.unshift({ name: fullName, dataset: datasetName, @@ -477,7 +492,7 @@ export class ZfsService { } async rollbackSnapshot(snapshotName: string): Promise<{ success: boolean; message: string }> { - if (!config.isLinux) { + if (this.useMock) { return { success: true, message: `Dataset rolled back to '${snapshotName}'.` }; } await this.runCommand('zfs', ['rollback', '-r', snapshotName]); @@ -485,7 +500,7 @@ export class ZfsService { } async destroySnapshot(snapshotName: string): Promise<{ success: boolean; message: string }> { - if (!config.isLinux) { + if (this.useMock) { this.mockSnapshots = this.mockSnapshots.filter((s) => s.name !== snapshotName); return { success: true, message: `Snapshot '${snapshotName}' destroyed.` }; }