Files
ProxmoxVE-Local/src/server/services/scriptDownloader.js
CanbiZ cc276ddff3 Improve script downloader and auto-sync services
Added detailed JSDoc comments and type annotations to ScriptDownloaderService for better maintainability and clarity. Refactored initialization logic to always set scriptsDirectory and repoUrl. Enhanced autoSyncService to specify cron job timezone and options. Updated PrismaClient import path for compatibility with generated client structure.
2025-11-28 13:10:20 +01:00

859 lines
33 KiB
JavaScript

// Real JavaScript implementation for script downloading
import { join } from 'path';
import { writeFile, mkdir, access, readFile, unlink } from 'fs/promises';
export class ScriptDownloaderService {
constructor() {
/** @type {string} */
this.scriptsDirectory = join(process.cwd(), 'scripts');
/** @type {string} */
this.repoUrl = process.env.REPO_URL || 'https://github.com/community-scripts/ProxmoxVE';
}
initializeConfig() {
// Re-initialize if needed (for environment changes)
this.scriptsDirectory = join(process.cwd(), 'scripts');
this.repoUrl = process.env.REPO_URL || 'https://github.com/community-scripts/ProxmoxVE';
}
/**
* Validates that a directory path doesn't contain nested directories with the same name
* (e.g., prevents ct/ct or install/install)
* @param {string} dirPath - The directory path to validate
* @returns {boolean}
*/
validateDirectoryPath(dirPath) {
const normalizedPath = dirPath.replace(/\\/g, '/');
const parts = normalizedPath.split('/');
// Check for consecutive duplicate directory names
for (let i = 0; i < parts.length - 1; i++) {
if (parts[i] === parts[i + 1] && parts[i] !== '') {
throw new Error(`Invalid directory path: nested directory detected (${parts[i]}/${parts[i + 1]}) in path: ${dirPath}`);
}
}
return true;
}
/**
* Validates that finalTargetDir doesn't contain nested directory names like ct/ct or install/install
* @param {string} targetDir - The base target directory
* @param {string} finalTargetDir - The final target directory to validate
* @returns {string}
*/
validateTargetDir(targetDir, finalTargetDir) {
// Check if finalTargetDir contains nested directory names
const normalized = finalTargetDir.replace(/\\/g, '/');
const parts = normalized.split('/');
// Check for consecutive duplicate directory names
for (let i = 0; i < parts.length - 1; i++) {
if (parts[i] === parts[i + 1]) {
console.warn(`[Path Validation] Detected nested directory pattern "${parts[i]}/${parts[i + 1]}" in finalTargetDir: ${finalTargetDir}. Using base directory "${targetDir}" instead.`);
return targetDir; // Return the base directory instead
}
}
return finalTargetDir;
}
/**
* Ensure a directory exists, creating it if necessary
* @param {string} dirPath - The directory path to ensure exists
* @returns {Promise<void>}
*/
async ensureDirectoryExists(dirPath) {
// Validate the directory path to prevent nested directories with the same name
this.validateDirectoryPath(dirPath);
try {
console.log(`[Directory Creation] Ensuring directory exists: ${dirPath}`);
await mkdir(dirPath, { recursive: true });
console.log(`[Directory Creation] Directory created/verified: ${dirPath}`);
} catch (/** @type {any} */ error) {
if (error.code !== 'EEXIST') {
console.error(`[Directory Creation] Error creating directory ${dirPath}:`, error.message);
throw error;
}
// Directory already exists, which is fine
console.log(`[Directory Creation] Directory already exists: ${dirPath}`);
}
}
/**
* Extract repository path from GitHub URL
* @param {string} repoUrl - The GitHub repository URL
* @returns {string}
*/
extractRepoPath(repoUrl) {
const match = /github\.com\/([^\/]+)\/([^\/]+)/.exec(repoUrl);
if (!match) {
throw new Error(`Invalid GitHub repository URL: ${repoUrl}`);
}
return `${match[1]}/${match[2]}`;
}
/**
* Download a file from GitHub
* @param {string} repoUrl - The GitHub repository URL
* @param {string} filePath - The file path within the repository
* @param {string} [branch] - The branch to download from
* @returns {Promise<string>}
*/
async downloadFileFromGitHub(repoUrl, filePath, branch = 'main') {
this.initializeConfig();
if (!repoUrl) {
throw new Error('Repository URL is not set');
}
const repoPath = this.extractRepoPath(repoUrl);
const url = `https://raw.githubusercontent.com/${repoPath}/${branch}/${filePath}`;
/** @type {Record<string, string>} */
const headers = {
'User-Agent': 'PVEScripts-Local/1.0',
};
// Add GitHub token authentication if available
if (process.env.GITHUB_TOKEN) {
headers.Authorization = `token ${process.env.GITHUB_TOKEN}`;
}
console.log(`Downloading from GitHub: ${url}`);
const response = await fetch(url, { headers });
if (!response.ok) {
throw new Error(`Failed to download ${filePath} from ${repoUrl}: ${response.status} ${response.statusText}`);
}
return response.text();
}
/**
* Get repository URL for a script
* @param {import('~/types/script').Script} script - The script object
* @returns {string}
*/
getRepoUrlForScript(script) {
// Use repository_url from script if available, otherwise fallback to env or default
if (script.repository_url) {
return script.repository_url;
}
this.initializeConfig();
return this.repoUrl;
}
/**
* Modify script content to use local paths
* @param {string} content - The script content
* @returns {string}
*/
modifyScriptContent(content) {
// Replace the build.func source line
const oldPattern = /source <\(curl -fsSL https:\/\/raw\.githubusercontent\.com\/community-scripts\/ProxmoxVE\/main\/misc\/build\.func\)/g;
const newPattern = 'SCRIPT_DIR="$(dirname "$0")" \nsource "$SCRIPT_DIR/../core/build.func"';
return content.replace(oldPattern, newPattern);
}
/**
* Load a script by downloading its files
* @param {import('~/types/script').Script} script - The script to load
* @returns {Promise<{success: boolean, message: string, files: string[], error?: string}>}
*/
async loadScript(script) {
this.initializeConfig();
try {
/** @type {string[]} */
const files = [];
const repoUrl = this.getRepoUrlForScript(script);
const branch = process.env.REPO_BRANCH || 'main';
console.log(`Loading script "${script.name}" (${script.slug}) from repository: ${repoUrl}`);
// Ensure directories exist
await this.ensureDirectoryExists(join(this.scriptsDirectory, 'ct'));
await this.ensureDirectoryExists(join(this.scriptsDirectory, 'install'));
await this.ensureDirectoryExists(join(this.scriptsDirectory, 'tools'));
await this.ensureDirectoryExists(join(this.scriptsDirectory, 'vm'));
if (script.install_methods?.length) {
for (const method of script.install_methods) {
if (method.script) {
const scriptPath = method.script;
const fileName = scriptPath.split('/').pop();
if (fileName) {
// Download from GitHub using the script's repository URL
console.log(`Downloading script file: ${scriptPath} from ${repoUrl}`);
const content = await this.downloadFileFromGitHub(repoUrl, scriptPath, branch);
// Determine target directory based on script path
let targetDir;
let finalTargetDir;
let filePath;
if (scriptPath.startsWith('ct/')) {
targetDir = 'ct';
finalTargetDir = targetDir;
// Validate and sanitize finalTargetDir
finalTargetDir = this.validateTargetDir(targetDir, finalTargetDir);
// Modify the content for CT scripts
const modifiedContent = this.modifyScriptContent(content);
filePath = join(this.scriptsDirectory, targetDir, fileName);
await writeFile(filePath, modifiedContent, 'utf-8');
} else if (scriptPath.startsWith('tools/')) {
targetDir = 'tools';
// Preserve subdirectory structure for tools scripts
const subPath = scriptPath.replace('tools/', '');
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
// Validate and sanitize finalTargetDir
finalTargetDir = this.validateTargetDir(targetDir, finalTargetDir);
// Ensure the subdirectory exists
await this.ensureDirectoryExists(join(this.scriptsDirectory, finalTargetDir));
filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
await writeFile(filePath, content, 'utf-8');
} else if (scriptPath.startsWith('vm/')) {
targetDir = 'vm';
// Preserve subdirectory structure for VM scripts
const subPath = scriptPath.replace('vm/', '');
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
// Validate and sanitize finalTargetDir
finalTargetDir = this.validateTargetDir(targetDir, finalTargetDir);
// Ensure the subdirectory exists
await this.ensureDirectoryExists(join(this.scriptsDirectory, finalTargetDir));
filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
await writeFile(filePath, content, 'utf-8');
} else {
// Handle other script types (fallback to ct directory)
targetDir = 'ct';
finalTargetDir = targetDir;
// Validate and sanitize finalTargetDir
finalTargetDir = this.validateTargetDir(targetDir, finalTargetDir);
const modifiedContent = this.modifyScriptContent(content);
filePath = join(this.scriptsDirectory, targetDir, fileName);
await writeFile(filePath, modifiedContent, 'utf-8');
}
files.push(`${finalTargetDir}/${fileName}`);
console.log(`Successfully downloaded: ${finalTargetDir}/${fileName}`);
}
}
}
}
// Only download install script for CT scripts
const hasCtScript = script.install_methods?.some(method => method.script?.startsWith('ct/'));
if (hasCtScript) {
const installScriptName = `${script.slug}-install.sh`;
try {
console.log(`Downloading install script: install/${installScriptName} from ${repoUrl}`);
const installContent = await this.downloadFileFromGitHub(repoUrl, `install/${installScriptName}`, branch);
const localInstallPath = join(this.scriptsDirectory, 'install', installScriptName);
await writeFile(localInstallPath, installContent, 'utf-8');
files.push(`install/${installScriptName}`);
console.log(`Successfully downloaded: install/${installScriptName}`);
} catch (error) {
// Install script might not exist, that's okay
console.log(`Install script not found: install/${installScriptName}`);
}
}
// Download alpine install script if alpine variant exists (only for CT scripts)
const hasAlpineCtVariant = script.install_methods?.some(
method => method.type === 'alpine' && method.script?.startsWith('ct/')
);
console.log(`[${script.slug}] Checking for alpine variant:`, {
hasAlpineCtVariant,
installMethods: script.install_methods?.map(m => ({ type: m.type, script: m.script }))
});
if (hasAlpineCtVariant) {
const alpineInstallScriptName = `alpine-${script.slug}-install.sh`;
try {
console.log(`[${script.slug}] Downloading alpine install script: install/${alpineInstallScriptName} from ${repoUrl}`);
const alpineInstallContent = await this.downloadFileFromGitHub(repoUrl, `install/${alpineInstallScriptName}`, branch);
const localAlpineInstallPath = join(this.scriptsDirectory, 'install', alpineInstallScriptName);
await writeFile(localAlpineInstallPath, alpineInstallContent, 'utf-8');
files.push(`install/${alpineInstallScriptName}`);
console.log(`[${script.slug}] Successfully downloaded: install/${alpineInstallScriptName}`);
} catch (error) {
// Alpine install script might not exist, that's okay
console.error(`[${script.slug}] Alpine install script not found or error: install/${alpineInstallScriptName}`, error);
if (error instanceof Error) {
console.error(`[${script.slug}] Error details:`, error.message, error.stack);
}
}
} else {
console.log(`[${script.slug}] No alpine CT variant found, skipping alpine install script download`);
}
return {
success: true,
message: `Successfully loaded ${files.length} script(s) for ${script.name}`,
files
};
} catch (error) {
console.error('Error loading script:', error);
return {
success: false,
message: error instanceof Error ? error.message : 'Failed to load script',
files: []
};
}
}
/**
* Check if a script is downloaded
* @param {import('~/types/script').Script} script - The script to check
* @returns {Promise<boolean>}
*/
async isScriptDownloaded(script) {
if (!script.install_methods?.length) return false;
// Check if ALL script files are downloaded
for (const method of script.install_methods) {
if (method.script) {
const scriptPath = method.script;
const fileName = scriptPath.split('/').pop();
if (fileName) {
// Determine target directory based on script path
let targetDir;
let finalTargetDir;
let filePath;
if (scriptPath.startsWith('ct/')) {
targetDir = 'ct';
finalTargetDir = targetDir;
filePath = join(this.scriptsDirectory, targetDir, fileName);
} else if (scriptPath.startsWith('tools/')) {
targetDir = 'tools';
const subPath = scriptPath.replace('tools/', '');
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
} else if (scriptPath.startsWith('vm/')) {
targetDir = 'vm';
const subPath = scriptPath.replace('vm/', '');
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
} else {
targetDir = 'ct';
finalTargetDir = targetDir;
filePath = join(this.scriptsDirectory, targetDir, fileName);
}
try {
await import('fs/promises').then(fs => fs.readFile(filePath, 'utf8'));
// File exists, continue checking other methods
} catch {
// File doesn't exist, script is not fully downloaded
return false;
}
}
}
}
// All files exist, script is downloaded
return true;
}
/**
* Check which script files exist locally
* @param {import('~/types/script').Script} script - The script to check
* @returns {Promise<{ctExists: boolean, installExists: boolean, files: string[]}>}
*/
async checkScriptExists(script) {
this.initializeConfig();
const files = [];
let ctExists = false;
let installExists = false;
try {
// Check scripts based on their install methods
if (script.install_methods?.length) {
for (const method of script.install_methods) {
if (method.script) {
const scriptPath = method.script;
const fileName = scriptPath.split('/').pop();
if (fileName) {
let targetDir;
let finalTargetDir;
let filePath;
if (scriptPath.startsWith('ct/')) {
targetDir = 'ct';
finalTargetDir = targetDir;
filePath = join(this.scriptsDirectory, targetDir, fileName);
} else if (scriptPath.startsWith('tools/')) {
targetDir = 'tools';
// Preserve subdirectory structure for tools scripts
const subPath = scriptPath.replace('tools/', '');
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
} else if (scriptPath.startsWith('vm/')) {
targetDir = 'vm';
// Preserve subdirectory structure for VM scripts
const subPath = scriptPath.replace('vm/', '');
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
} else {
targetDir = 'ct'; // Default fallback
finalTargetDir = targetDir;
filePath = join(this.scriptsDirectory, targetDir, fileName);
}
try {
await access(filePath);
files.push(`${finalTargetDir}/${fileName}`);
// Set ctExists for all script types (CT, tools, vm) for UI consistency
if (scriptPath.startsWith('ct/') || scriptPath.startsWith('tools/') || scriptPath.startsWith('vm/')) {
ctExists = true;
}
} catch {
// File doesn't exist
}
}
}
}
}
// Check for install script for CT scripts
const hasCtScript = script.install_methods?.some(method => method.script?.startsWith('ct/'));
if (hasCtScript) {
const installScriptName = `${script.slug}-install.sh`;
const installPath = join(this.scriptsDirectory, 'install', installScriptName);
try {
await access(installPath);
files.push(`install/${installScriptName}`);
installExists = true;
} catch {
// Install script doesn't exist
}
}
// Check alpine install script if alpine variant exists (only for CT scripts)
const hasAlpineCtVariant = script.install_methods?.some(
method => method.type === 'alpine' && method.script?.startsWith('ct/')
);
if (hasAlpineCtVariant) {
const alpineInstallScriptName = `alpine-${script.slug}-install.sh`;
const alpineInstallPath = join(this.scriptsDirectory, 'install', alpineInstallScriptName);
try {
await access(alpineInstallPath);
files.push(`install/${alpineInstallScriptName}`);
installExists = true; // Mark as exists if alpine install script exists
} catch {
// File doesn't exist
}
}
return { ctExists, installExists, files };
} catch (error) {
console.error('Error checking script existence:', error);
return { ctExists: false, installExists: false, files: [] };
}
}
/**
* Delete a script's local files
* @param {import('~/types/script').Script} script - The script to delete
* @returns {Promise<{success: boolean, message: string, deletedFiles: string[]}>}
*/
async deleteScript(script) {
this.initializeConfig();
const deletedFiles = [];
try {
// Get the list of files that exist for this script
const fileCheck = await this.checkScriptExists(script);
if (fileCheck.files.length === 0) {
return {
success: false,
message: 'No script files found to delete',
deletedFiles: []
};
}
// Delete all files
for (const filePath of fileCheck.files) {
try {
const fullPath = join(this.scriptsDirectory, filePath);
await unlink(fullPath);
deletedFiles.push(filePath);
} catch (error) {
// Log error but continue deleting other files
console.error(`Error deleting file ${filePath}:`, error);
}
}
if (deletedFiles.length === 0) {
return {
success: false,
message: 'Failed to delete any script files',
deletedFiles: []
};
}
return {
success: true,
message: `Successfully deleted ${deletedFiles.length} file(s) for ${script.name}`,
deletedFiles
};
} catch (error) {
console.error('Error deleting script:', error);
return {
success: false,
message: error instanceof Error ? error.message : 'Failed to delete script',
deletedFiles
};
}
}
/**
* Compare local script content with remote
* @param {import('~/types/script').Script} script - The script to compare
* @returns {Promise<{hasDifferences: boolean, differences: string[], error?: string}>}
*/
async compareScriptContent(script) {
this.initializeConfig();
/** @type {string[]} */
const differences = [];
let hasDifferences = false;
const repoUrl = this.getRepoUrlForScript(script);
const branch = process.env.REPO_BRANCH || 'main';
try {
// First check if any local files exist
const localFilesExist = await this.checkScriptExists(script);
if (!localFilesExist.ctExists && !localFilesExist.installExists) {
// No local files exist, so no comparison needed
return { hasDifferences: false, differences: [] };
}
// If we have local files, proceed with comparison
// Use Promise.all to run comparisons in parallel
const comparisonPromises = [];
// Compare scripts only if they exist locally
if (localFilesExist.ctExists && script.install_methods?.length) {
for (const method of script.install_methods) {
if (method.script) {
const scriptPath = method.script;
const fileName = scriptPath.split('/').pop();
if (fileName) {
let targetDir;
let finalTargetDir;
if (scriptPath.startsWith('ct/')) {
targetDir = 'ct';
finalTargetDir = targetDir;
} else if (scriptPath.startsWith('tools/')) {
targetDir = 'tools';
// Preserve subdirectory structure for tools scripts
const subPath = scriptPath.replace('tools/', '');
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
} else if (scriptPath.startsWith('vm/')) {
targetDir = 'vm';
// Preserve subdirectory structure for VM scripts
const subPath = scriptPath.replace('vm/', '');
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
} else {
continue; // Skip unknown script types
}
comparisonPromises.push(
this.compareSingleFile(script, scriptPath, `${finalTargetDir}/${fileName}`)
.then(result => {
if (result.error) {
console.error(`[Comparison] Error comparing ${result.filePath}: ${result.error}`);
}
if (result.hasDifferences) {
hasDifferences = true;
differences.push(result.filePath);
}
})
.catch((error) => {
console.error(`[Comparison] Promise error for ${scriptPath}:`, error);
})
);
}
}
}
}
// Compare install script only if it exists locally
if (localFilesExist.installExists) {
const installScriptName = `${script.slug}-install.sh`;
const installScriptPath = `install/${installScriptName}`;
comparisonPromises.push(
this.compareSingleFile(script, installScriptPath, installScriptPath)
.then(result => {
if (result.error) {
console.error(`[Comparison] Error comparing ${result.filePath}: ${result.error}`);
}
if (result.hasDifferences) {
hasDifferences = true;
differences.push(result.filePath);
}
})
.catch((error) => {
console.error(`[Comparison] Promise error for ${installScriptPath}:`, error);
})
);
}
// Compare alpine install script if alpine variant exists (only for CT scripts)
const hasAlpineCtVariant = script.install_methods?.some(
method => method.type === 'alpine' && method.script?.startsWith('ct/')
);
if (hasAlpineCtVariant) {
const alpineInstallScriptName = `alpine-${script.slug}-install.sh`;
const alpineInstallScriptPath = `install/${alpineInstallScriptName}`;
const localAlpineInstallPath = join(this.scriptsDirectory, alpineInstallScriptPath);
// Check if alpine install script exists locally
try {
await access(localAlpineInstallPath);
comparisonPromises.push(
this.compareSingleFile(script, alpineInstallScriptPath, alpineInstallScriptPath)
.then(result => {
if (result.error) {
console.error(`[Comparison] Error comparing ${result.filePath}: ${result.error}`);
}
if (result.hasDifferences) {
hasDifferences = true;
differences.push(result.filePath);
}
})
.catch((error) => {
console.error(`[Comparison] Promise error for ${alpineInstallScriptPath}:`, error);
})
);
} catch {
// Alpine install script doesn't exist locally, skip comparison
}
}
// Wait for all comparisons to complete
await Promise.all(comparisonPromises);
console.log(`[Comparison] Completed comparison for ${script.slug}: hasDifferences=${hasDifferences}, differences=${differences.length}`);
return { hasDifferences, differences };
} catch (/** @type {any} */ error) {
console.error(`[Comparison] Error comparing script content for ${script.slug}:`, error);
return { hasDifferences: false, differences: [], error: error.message };
}
}
/**
* Compare a single file with remote
* @param {import('~/types/script').Script} script - The script object
* @param {string} remotePath - The remote file path
* @param {string} filePath - The local file path
* @returns {Promise<{hasDifferences: boolean, filePath: string, error?: string}>}
*/
async compareSingleFile(script, remotePath, filePath) {
try {
const localPath = join(this.scriptsDirectory, filePath);
const repoUrl = this.getRepoUrlForScript(script);
const branch = process.env.REPO_BRANCH || 'main';
console.log(`[Comparison] Comparing ${filePath} from ${repoUrl} (branch: ${branch})`);
// Read local content
const localContent = await readFile(localPath, 'utf-8');
console.log(`[Comparison] Local file size: ${localContent.length} bytes`);
// Download remote content from the script's repository
const remoteContent = await this.downloadFileFromGitHub(repoUrl, remotePath, branch);
console.log(`[Comparison] Remote file size: ${remoteContent.length} bytes`);
// Apply modification only for CT scripts, not for other script types
let modifiedRemoteContent;
if (remotePath.startsWith('ct/')) {
modifiedRemoteContent = this.modifyScriptContent(remoteContent);
console.log(`[Comparison] Applied CT script modifications`);
} else {
modifiedRemoteContent = remoteContent; // Don't modify tools or vm scripts
}
// Compare content
const hasDifferences = localContent !== modifiedRemoteContent;
if (hasDifferences) {
console.log(`[Comparison] Differences found in ${filePath}`);
} else {
console.log(`[Comparison] No differences in ${filePath}`);
}
return { hasDifferences, filePath };
} catch (/** @type {any} */ error) {
console.error(`[Comparison] Error comparing file ${filePath}:`, error.message);
// Return error information so it can be handled upstream
return { hasDifferences: false, filePath, error: error.message };
}
}
/**
* Get diff between local and remote script
* @param {import('~/types/script').Script} script - The script object
* @param {string} filePath - The file path to diff
* @returns {Promise<{diff: string|null, localContent: string|null, remoteContent: string|null}>}
*/
async getScriptDiff(script, filePath) {
this.initializeConfig();
try {
const repoUrl = this.getRepoUrlForScript(script);
const branch = process.env.REPO_BRANCH || 'main';
let localContent = null;
let remoteContent = null;
if (filePath.startsWith('ct/')) {
// Handle CT script
const fileName = filePath.split('/').pop();
if (fileName) {
const localPath = join(this.scriptsDirectory, 'ct', fileName);
try {
localContent = await readFile(localPath, 'utf-8');
} catch {
// Error reading local CT script
}
try {
// Find the corresponding script path in install_methods
const method = script.install_methods?.find(m => m.script === filePath);
if (method?.script) {
const downloadedContent = await this.downloadFileFromGitHub(repoUrl, method.script, branch);
remoteContent = this.modifyScriptContent(downloadedContent);
}
} catch {
// Error downloading remote CT script
}
}
} else if (filePath.startsWith('install/')) {
// Handle install script
const localPath = join(this.scriptsDirectory, filePath);
try {
localContent = await readFile(localPath, 'utf-8');
} catch {
// Error reading local install script
}
try {
remoteContent = await this.downloadFileFromGitHub(repoUrl, filePath, branch);
} catch {
// Error downloading remote install script
}
}
if (!localContent || !remoteContent) {
return { diff: null, localContent, remoteContent };
}
// Generate diff using a simple line-by-line comparison
const diff = this.generateDiff(localContent, remoteContent);
return { diff, localContent, remoteContent };
} catch (error) {
console.error('Error getting script diff:', error);
return { diff: null, localContent: null, remoteContent: null };
}
}
/**
* Generate a simple line-by-line diff
* @param {string} localContent - The local file content
* @param {string} remoteContent - The remote file content
* @returns {string}
*/
generateDiff(localContent, remoteContent) {
const localLines = localContent.split('\n');
const remoteLines = remoteContent.split('\n');
let diff = '';
let i = 0;
let j = 0;
while (i < localLines.length || j < remoteLines.length) {
const localLine = localLines[i];
const remoteLine = remoteLines[j];
if (i >= localLines.length) {
// Only remote lines left
diff += `+${j + 1}: ${remoteLine}\n`;
j++;
} else if (j >= remoteLines.length) {
// Only local lines left
diff += `-${i + 1}: ${localLine}\n`;
i++;
} else if (localLine === remoteLine) {
// Lines are the same
diff += ` ${i + 1}: ${localLine}\n`;
i++;
j++;
} else {
// Lines are different - find the best match
let found = false;
for (let k = j + 1; k < Math.min(j + 10, remoteLines.length); k++) {
if (localLine === remoteLines[k]) {
// Found match in remote, local line was removed
for (let l = j; l < k; l++) {
diff += `+${l + 1}: ${remoteLines[l]}\n`;
}
diff += ` ${i + 1}: ${localLine}\n`;
i++;
j = k + 1;
found = true;
break;
}
}
if (!found) {
for (let k = i + 1; k < Math.min(i + 10, localLines.length); k++) {
if (remoteLine === localLines[k]) {
// Found match in local, remote line was added
diff += `-${i + 1}: ${localLine}\n`;
for (let l = i + 1; l < k; l++) {
diff += `-${l + 1}: ${localLines[l]}\n`;
}
diff += `+${j + 1}: ${remoteLine}\n`;
i = k + 1;
j++;
found = true;
break;
}
}
}
if (!found) {
// No match found, lines are different
diff += `-${i + 1}: ${localLine}\n`;
diff += `+${j + 1}: ${remoteLine}\n`;
i++;
j++;
}
}
}
return diff;
}
}
export const scriptDownloaderService = new ScriptDownloaderService();