Files
ProxmoxVE-Local/src/server/services/localScripts.ts
2026-03-29 09:49:52 +02:00

184 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readFile, readdir, writeFile, mkdir } from 'fs/promises';
import { join } from 'path';
import type { Script, ScriptCard } from '~/types/script';
import { scriptDownloaderService } from './scriptDownloader.js';
export class LocalScriptsService {
private scriptsDirectory: string;
constructor() {
this.scriptsDirectory = join(process.cwd(), 'scripts', 'json');
}
async getJsonFiles(): Promise<string[]> {
try {
const files = await readdir(this.scriptsDirectory);
return files.filter(file => file.endsWith('.json'));
} catch {
return [];
}
}
async getScriptContent(filename: string): Promise<Script> {
try {
const filePath = join(this.scriptsDirectory, filename);
const content = await readFile(filePath, 'utf-8');
return JSON.parse(content) as Script;
} catch (error) {
console.error(`Error reading script file ${filename}:`, error);
throw new Error(`Failed to read script: ${filename}`);
}
}
/** Returns all scripts from PocketBase (community scripts) merged with any local JSON user scripts. */
async getAllScripts(): Promise<Script[]> {
try {
const { getAllScripts: pbGetAll } = await import('./pbScripts');
const pbScripts = await pbGetAll();
const communityScripts: Script[] = pbScripts.map(pb => ({
name: pb.name,
slug: pb.slug,
description: pb.description,
logo: pb.logo ?? null,
type: pb.type as Script['type'],
updateable: pb.updateable,
privileged: pb.privileged,
interface_port: pb.port ?? null,
website: pb.website ?? null,
documentation: pb.documentation ?? null,
config_path: pb.config_path ?? null,
date_created: pb.script_created ?? '',
default_credentials: { username: pb.default_user ?? null, password: pb.default_passwd ?? null },
is_dev: pb.is_dev,
is_disabled: pb.is_disabled,
is_deleted: pb.is_deleted,
has_arm: pb.has_arm,
categories: pb.categories.map(c => c.name),
install_methods: pb.install_methods_json.map(m => ({
type: m.type,
resources: m.resources,
config_path: m.config_path,
script: scriptDownloaderService.deriveScriptPath(pb.type, m.type, pb.slug) ?? undefined,
})),
notes: pb.notes_json.map(n => ({ text: n.text, type: n.type })),
}));
// Merge local user JSON scripts (only those not already in PocketBase)
try {
const jsonFiles = await this.getJsonFiles();
const slugsSeen = new Set(communityScripts.map(s => s.slug));
for (const filename of jsonFiles) {
try {
const script = await this.getScriptContent(filename);
if (!slugsSeen.has(script.slug)) {
communityScripts.push(script);
slugsSeen.add(script.slug);
}
} catch { /* skip bad files */ }
}
} catch { /* local JSON folder absent fine */ }
return communityScripts;
} catch (error) {
console.error('Error fetching scripts from PocketBase, falling back to local JSON:', error);
try {
const jsonFiles = await this.getJsonFiles();
const scripts: Script[] = [];
for (const filename of jsonFiles) {
try { scripts.push(await this.getScriptContent(filename)); } catch { /* skip */ }
}
return scripts;
} catch {
return [];
}
}
}
async getScriptCards(): Promise<ScriptCard[]> {
const scripts = await this.getAllScripts();
return scripts.map(script => ({
name: script.name,
slug: script.slug,
description: script.description,
logo: script.logo,
type: script.type,
updateable: script.updateable,
website: script.website ?? null,
}));
}
/** Fetches a script by slug, preferring PocketBase then local JSON. */
async getScriptBySlug(slug: string): Promise<Script | null> {
try {
const { getScriptBySlug: pbGetBySlug } = await import('./pbScripts');
const pb = await pbGetBySlug(slug);
if (pb) {
return {
name: pb.name,
slug: pb.slug,
description: pb.description,
logo: pb.logo ?? null,
type: pb.type as Script['type'],
updateable: pb.updateable,
privileged: pb.privileged,
interface_port: pb.port ?? null,
website: pb.website ?? null,
documentation: pb.documentation ?? null,
config_path: pb.config_path ?? null,
date_created: pb.script_created ?? '',
default_credentials: { username: pb.default_user ?? null, password: pb.default_passwd ?? null },
is_dev: pb.is_dev,
is_disabled: pb.is_disabled,
is_deleted: pb.is_deleted,
has_arm: pb.has_arm,
categories: pb.categories.map(c => c.name),
install_methods: pb.install_methods_json.map(m => ({
type: m.type,
resources: m.resources,
config_path: m.config_path,
script: scriptDownloaderService.deriveScriptPath(pb.type, m.type, pb.slug) ?? undefined,
})),
notes: pb.notes_json.map(n => ({ text: n.text, type: n.type })),
};
}
} catch (error) {
console.warn(`PocketBase lookup failed for slug "${slug}", trying local JSON:`, error);
}
// Fallback: local JSON user script
try {
const filePath = join(this.scriptsDirectory, `${slug}.json`);
const content = await readFile(filePath, 'utf-8');
return JSON.parse(content) as Script;
} catch {
return null;
}
}
async getMetadata(): Promise<unknown> {
try {
const { getMetadata } = await import('./pbScripts');
return await getMetadata();
} catch {
return {};
}
}
async saveScriptsFromGitHub(scripts: Script[]): Promise<void> {
try {
await mkdir(this.scriptsDirectory, { recursive: true });
for (const script of scripts) {
const filename = `${script.slug}.json`;
const filePath = join(this.scriptsDirectory, filename);
await writeFile(filePath, JSON.stringify(script, null, 2), 'utf-8');
}
} catch (error) {
console.error('Error saving scripts from GitHub:', error);
throw new Error('Failed to save scripts from GitHub');
}
}
}
// Singleton instance
export const localScriptsService = new LocalScriptsService();