Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
351ba09f4e | ||
|
|
580986abfa | ||
|
|
e1d270d52c | ||
|
|
20dbcae42a | ||
|
|
8e8c724392 | ||
|
|
201b33ec84 | ||
|
|
6d2df9929c | ||
|
|
f33504baf5 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pve-scripts-local",
|
"name": "pve-scripts-local",
|
||||||
"version": "0.1.0",
|
"version": "0.5.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
39
server.js
39
server.js
@@ -3,6 +3,7 @@ import { parse } from 'url';
|
|||||||
import next from 'next';
|
import next from 'next';
|
||||||
import { WebSocketServer } from 'ws';
|
import { WebSocketServer } from 'ws';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
import { join, resolve } from 'path';
|
import { join, resolve } from 'path';
|
||||||
import stripAnsi from 'strip-ansi';
|
import stripAnsi from 'strip-ansi';
|
||||||
import { spawn as ptySpawn } from 'node-pty';
|
import { spawn as ptySpawn } from 'node-pty';
|
||||||
@@ -56,6 +57,8 @@ const handle = app.getRequestHandler();
|
|||||||
* @property {string} user
|
* @property {string} user
|
||||||
* @property {string} password
|
* @property {string} password
|
||||||
* @property {number} [id]
|
* @property {number} [id]
|
||||||
|
* @property {string} [auth_type]
|
||||||
|
* @property {string} [ssh_key_path]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -295,6 +298,20 @@ class ScriptExecutionHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve full server from DB when client sends server with id but no ssh_key_path (e.g. for Shell/Update over SSH).
|
||||||
|
* @param {ServerInfo|null} server - Server from WebSocket message
|
||||||
|
* @returns {Promise<ServerInfo|null>} Same server or full server from DB
|
||||||
|
*/
|
||||||
|
async resolveServerForSSH(server) {
|
||||||
|
if (!server?.id) return server;
|
||||||
|
if (server.auth_type === 'key' && (!server.ssh_key_path || !existsSync(server.ssh_key_path))) {
|
||||||
|
const full = await this.db.getServerById(server.id);
|
||||||
|
return /** @type {ServerInfo|null} */ (full ?? server);
|
||||||
|
}
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ExtendedWebSocket} ws
|
* @param {ExtendedWebSocket} ws
|
||||||
* @param {WebSocketMessage} message
|
* @param {WebSocketMessage} message
|
||||||
@@ -305,16 +322,21 @@ class ScriptExecutionHandler {
|
|||||||
switch (action) {
|
switch (action) {
|
||||||
case 'start':
|
case 'start':
|
||||||
if (scriptPath && executionId) {
|
if (scriptPath && executionId) {
|
||||||
|
let serverToUse = server;
|
||||||
|
if (serverToUse?.id) {
|
||||||
|
serverToUse = await this.resolveServerForSSH(serverToUse) ?? serverToUse;
|
||||||
|
}
|
||||||
|
const resolved = serverToUse ?? server;
|
||||||
if (isClone && containerId && storage && server && cloneCount && hostnames && containerType) {
|
if (isClone && containerId && storage && server && cloneCount && hostnames && containerType) {
|
||||||
await this.startSSHCloneExecution(ws, containerId, executionId, storage, server, containerType, cloneCount, hostnames);
|
await this.startSSHCloneExecution(ws, containerId, executionId, storage, /** @type {ServerInfo} */ (resolved), containerType, cloneCount, hostnames);
|
||||||
} else if (isBackup && containerId && storage) {
|
} else if (isBackup && containerId && storage) {
|
||||||
await this.startBackupExecution(ws, containerId, executionId, storage, mode, server);
|
await this.startBackupExecution(ws, containerId, executionId, storage, mode, resolved);
|
||||||
} else if (isUpdate && containerId) {
|
} else if (isUpdate && containerId) {
|
||||||
await this.startUpdateExecution(ws, containerId, executionId, mode, server, backupStorage);
|
await this.startUpdateExecution(ws, containerId, executionId, mode, resolved, backupStorage);
|
||||||
} else if (isShell && containerId) {
|
} else if (isShell && containerId) {
|
||||||
await this.startShellExecution(ws, containerId, executionId, mode, server, containerType);
|
await this.startShellExecution(ws, containerId, executionId, mode, resolved, containerType);
|
||||||
} else {
|
} else {
|
||||||
await this.startScriptExecution(ws, scriptPath, executionId, mode, server, envVars);
|
await this.startScriptExecution(ws, scriptPath, executionId, mode, resolved, envVars);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.sendMessage(ws, {
|
this.sendMessage(ws, {
|
||||||
@@ -1153,10 +1175,11 @@ class ScriptExecutionHandler {
|
|||||||
const hostname = hostnames[i];
|
const hostname = hostnames[i];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Read config file to get hostname/name
|
// Read config file to get hostname/name (node-specific path)
|
||||||
|
const nodeName = server.name;
|
||||||
const configPath = containerType === 'lxc'
|
const configPath = containerType === 'lxc'
|
||||||
? `/etc/pve/lxc/${nextId}.conf`
|
? `/etc/pve/nodes/${nodeName}/lxc/${nextId}.conf`
|
||||||
: `/etc/pve/qemu-server/${nextId}.conf`;
|
: `/etc/pve/nodes/${nodeName}/qemu-server/${nextId}.conf`;
|
||||||
|
|
||||||
let configContent = '';
|
let configContent = '';
|
||||||
await new Promise(/** @type {(resolve: (value?: void) => void) => void} */ ((resolve) => {
|
await new Promise(/** @type {(resolve: (value?: void) => void) => void} */ ((resolve) => {
|
||||||
|
|||||||
@@ -418,44 +418,46 @@ async function isVM(scriptId: number, containerId: string, serverId: number | nu
|
|||||||
return false; // Default to LXC if SSH fails
|
return false; // Default to LXC if SSH fails
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check both config file paths
|
// Node-specific paths (multi-node Proxmox: /etc/pve/nodes/NODENAME/...)
|
||||||
const vmConfigPath = `/etc/pve/qemu-server/${containerId}.conf`;
|
const nodeName = (server as Server).name;
|
||||||
const lxcConfigPath = `/etc/pve/lxc/${containerId}.conf`;
|
const vmConfigPathNode = `/etc/pve/nodes/${nodeName}/qemu-server/${containerId}.conf`;
|
||||||
|
const lxcConfigPathNode = `/etc/pve/nodes/${nodeName}/lxc/${containerId}.conf`;
|
||||||
|
// Fallback for single-node or when server.name is not the Proxmox node name
|
||||||
|
const vmConfigPathFallback = `/etc/pve/qemu-server/${containerId}.conf`;
|
||||||
|
const lxcConfigPathFallback = `/etc/pve/lxc/${containerId}.conf`;
|
||||||
|
|
||||||
// Check VM config file
|
const checkPathExists = (path: string): Promise<boolean> =>
|
||||||
let vmConfigExists = false;
|
new Promise<boolean>((resolve) => {
|
||||||
await new Promise<void>((resolve) => {
|
let exists = false;
|
||||||
void sshExecutionService.executeCommand(
|
void sshExecutionService.executeCommand(
|
||||||
server as Server,
|
server as Server,
|
||||||
`test -f "${vmConfigPath}" && echo "exists" || echo "not_exists"`,
|
`test -f "${path}" && echo "exists" || echo "not_exists"`,
|
||||||
(data: string) => {
|
(data: string) => {
|
||||||
if (data.includes('exists')) {
|
if (data.includes('exists')) exists = true;
|
||||||
vmConfigExists = true;
|
},
|
||||||
}
|
() => resolve(exists),
|
||||||
},
|
() => resolve(exists)
|
||||||
() => resolve(),
|
);
|
||||||
() => resolve()
|
});
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (vmConfigExists) {
|
// Prefer node-specific paths first
|
||||||
return true; // VM config file exists
|
const vmConfigExistsNode = await checkPathExists(vmConfigPathNode);
|
||||||
|
if (vmConfigExistsNode) {
|
||||||
|
return true; // VM config file exists on node
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check LXC config file (not needed for return value, but check for completeness)
|
const lxcConfigExistsNode = await checkPathExists(lxcConfigPathNode);
|
||||||
await new Promise<void>((resolve) => {
|
if (lxcConfigExistsNode) {
|
||||||
void sshExecutionService.executeCommand(
|
return false; // LXC config file exists on node
|
||||||
server as Server,
|
}
|
||||||
`test -f "${lxcConfigPath}" && echo "exists" || echo "not_exists"`,
|
|
||||||
(_data: string) => {
|
|
||||||
// Data handler not needed - just checking if file exists
|
|
||||||
},
|
|
||||||
() => resolve(),
|
|
||||||
() => resolve()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return false; // Always LXC since VM config doesn't exist
|
// Fallback: single-node or server.name not matching Proxmox node name
|
||||||
|
const vmConfigExistsFallback = await checkPathExists(vmConfigPathFallback);
|
||||||
|
if (vmConfigExistsFallback) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // LXC (or neither path exists)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error determining container type:', error);
|
console.error('Error determining container type:', error);
|
||||||
return false; // Default to LXC on error
|
return false; // Default to LXC on error
|
||||||
@@ -971,10 +973,11 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to check config file for community-script tag and extract hostname/name
|
// Helper function to check config file for community-script tag and extract hostname/name
|
||||||
|
const nodeName = (server as Server).name;
|
||||||
const checkConfigAndExtractInfo = async (id: string, isVM: boolean): Promise<any> => {
|
const checkConfigAndExtractInfo = async (id: string, isVM: boolean): Promise<any> => {
|
||||||
const configPath = isVM
|
const configPath = isVM
|
||||||
? `/etc/pve/qemu-server/${id}.conf`
|
? `/etc/pve/nodes/${nodeName}/qemu-server/${id}.conf`
|
||||||
: `/etc/pve/lxc/${id}.conf`;
|
: `/etc/pve/nodes/${nodeName}/lxc/${id}.conf`;
|
||||||
|
|
||||||
const readCommand = `cat "${configPath}" 2>/dev/null`;
|
const readCommand = `cat "${configPath}" 2>/dev/null`;
|
||||||
|
|
||||||
@@ -1318,10 +1321,10 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
|
|
||||||
// Check if ID exists in either pct list (containers) or qm list (VMs)
|
// Check if ID exists in either pct list (containers) or qm list (VMs)
|
||||||
if (!existingIds.has(containerId)) {
|
if (!existingIds.has(containerId)) {
|
||||||
// Also verify config file doesn't exist as a double-check
|
// Also verify config file doesn't exist as a double-check (node-specific paths)
|
||||||
// Check both container and VM config paths
|
const nodeName = (server as Server).name;
|
||||||
const checkContainerCommand = `test -f "/etc/pve/lxc/${containerId}.conf" && echo "exists" || echo "not_found"`;
|
const checkContainerCommand = `test -f "/etc/pve/nodes/${nodeName}/lxc/${containerId}.conf" && echo "exists" || echo "not_found"`;
|
||||||
const checkVMCommand = `test -f "/etc/pve/qemu-server/${containerId}.conf" && echo "exists" || echo "not_found"`;
|
const checkVMCommand = `test -f "/etc/pve/nodes/${nodeName}/qemu-server/${containerId}.conf" && echo "exists" || echo "not_found"`;
|
||||||
|
|
||||||
const configExists = await new Promise<boolean>((resolve) => {
|
const configExists = await new Promise<boolean>((resolve) => {
|
||||||
let combinedOutput = '';
|
let combinedOutput = '';
|
||||||
@@ -2237,8 +2240,9 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read config file
|
// Read config file (node-specific path)
|
||||||
const configPath = `/etc/pve/lxc/${script.container_id}.conf`;
|
const nodeName = (server as Server).name;
|
||||||
|
const configPath = `/etc/pve/nodes/${nodeName}/lxc/${script.container_id}.conf`;
|
||||||
const readCommand = `cat "${configPath}" 2>/dev/null`;
|
const readCommand = `cat "${configPath}" 2>/dev/null`;
|
||||||
let rawConfig = '';
|
let rawConfig = '';
|
||||||
|
|
||||||
@@ -2368,8 +2372,9 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write config file using heredoc for safe escaping
|
// Write config file using heredoc for safe escaping (node-specific path)
|
||||||
const configPath = `/etc/pve/lxc/${script.container_id}.conf`;
|
const nodeName = (server as Server).name;
|
||||||
|
const configPath = `/etc/pve/nodes/${nodeName}/lxc/${script.container_id}.conf`;
|
||||||
const writeCommand = `cat > "${configPath}" << 'EOFCONFIG'
|
const writeCommand = `cat > "${configPath}" << 'EOFCONFIG'
|
||||||
${rawConfig}
|
${rawConfig}
|
||||||
EOFCONFIG`;
|
EOFCONFIG`;
|
||||||
@@ -2777,9 +2782,10 @@ EOFCONFIG`;
|
|||||||
const { getSSHExecutionService } = await import('~/server/ssh-execution-service');
|
const { getSSHExecutionService } = await import('~/server/ssh-execution-service');
|
||||||
const sshExecutionService = getSSHExecutionService();
|
const sshExecutionService = getSSHExecutionService();
|
||||||
|
|
||||||
|
const nodeName = (server as Server).name;
|
||||||
const configPath = input.containerType === 'lxc'
|
const configPath = input.containerType === 'lxc'
|
||||||
? `/etc/pve/lxc/${input.containerId}.conf`
|
? `/etc/pve/nodes/${nodeName}/lxc/${input.containerId}.conf`
|
||||||
: `/etc/pve/qemu-server/${input.containerId}.conf`;
|
: `/etc/pve/nodes/${nodeName}/qemu-server/${input.containerId}.conf`;
|
||||||
|
|
||||||
let configContent = '';
|
let configContent = '';
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
@@ -3171,10 +3177,11 @@ EOFCONFIG`;
|
|||||||
const { getSSHExecutionService } = await import('~/server/ssh-execution-service');
|
const { getSSHExecutionService } = await import('~/server/ssh-execution-service');
|
||||||
const sshExecutionService = getSSHExecutionService();
|
const sshExecutionService = getSSHExecutionService();
|
||||||
|
|
||||||
// Read config file to get hostname/name
|
// Read config file to get hostname/name (node-specific path)
|
||||||
|
const nodeName = (server as Server).name;
|
||||||
const configPath = input.containerType === 'lxc'
|
const configPath = input.containerType === 'lxc'
|
||||||
? `/etc/pve/lxc/${input.containerId}.conf`
|
? `/etc/pve/nodes/${nodeName}/lxc/${input.containerId}.conf`
|
||||||
: `/etc/pve/qemu-server/${input.containerId}.conf`;
|
: `/etc/pve/nodes/${nodeName}/qemu-server/${input.containerId}.conf`;
|
||||||
|
|
||||||
let configContent = '';
|
let configContent = '';
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// JavaScript wrapper for githubJsonService (for use with node server.js)
|
// JavaScript wrapper for githubJsonService (for use with node server.js)
|
||||||
import { writeFile, mkdir, readdir, readFile } from 'fs/promises';
|
import { writeFile, mkdir, readdir, readFile, unlink } from 'fs/promises';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { repositoryService } from './repositoryService.js';
|
import { repositoryService } from './repositoryService.js';
|
||||||
import { listDirectory, downloadRawFile } from '../lib/gitProvider/index.js';
|
import { listDirectory, downloadRawFile } from '../lib/gitProvider/index.js';
|
||||||
@@ -163,25 +163,42 @@ class GitHubJsonService {
|
|||||||
const localFiles = await this.getLocalJsonFiles();
|
const localFiles = await this.getLocalJsonFiles();
|
||||||
console.log(`Found ${localFiles.length} local JSON files`);
|
console.log(`Found ${localFiles.length} local JSON files`);
|
||||||
|
|
||||||
|
// Delete local JSON files that belong to this repo but are no longer in the remote
|
||||||
|
const remoteFilenames = new Set(githubFiles.map((f) => f.name));
|
||||||
|
const deletedFiles = await this.deleteLocalFilesRemovedFromRepo(repoUrl, remoteFilenames);
|
||||||
|
if (deletedFiles.length > 0) {
|
||||||
|
console.log(`Removed ${deletedFiles.length} obsolete JSON file(s) no longer in ${repoUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
const filesToSync = await this.findFilesToSyncForRepo(repoUrl, githubFiles, localFiles);
|
const filesToSync = await this.findFilesToSyncForRepo(repoUrl, githubFiles, localFiles);
|
||||||
console.log(`Found ${filesToSync.length} files that need syncing from ${repoUrl}`);
|
console.log(`Found ${filesToSync.length} files that need syncing from ${repoUrl}`);
|
||||||
|
|
||||||
if (filesToSync.length === 0) {
|
if (filesToSync.length === 0) {
|
||||||
|
const msg =
|
||||||
|
deletedFiles.length > 0
|
||||||
|
? `All JSON files are up to date for repository: ${repoUrl}. Removed ${deletedFiles.length} obsolete file(s).`
|
||||||
|
: `All JSON files are up to date for repository: ${repoUrl}`;
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `All JSON files are up to date for repository: ${repoUrl}`,
|
message: msg,
|
||||||
count: 0,
|
count: 0,
|
||||||
syncedFiles: []
|
syncedFiles: [],
|
||||||
|
deletedFiles
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncedFiles = await this.syncSpecificFiles(repoUrl, filesToSync);
|
const syncedFiles = await this.syncSpecificFiles(repoUrl, filesToSync);
|
||||||
|
|
||||||
|
const msg =
|
||||||
|
deletedFiles.length > 0
|
||||||
|
? `Successfully synced ${syncedFiles.length} JSON files from ${repoUrl}, removed ${deletedFiles.length} obsolete file(s).`
|
||||||
|
: `Successfully synced ${syncedFiles.length} JSON files from ${repoUrl}`;
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Successfully synced ${syncedFiles.length} JSON files from ${repoUrl}`,
|
message: msg,
|
||||||
count: syncedFiles.length,
|
count: syncedFiles.length,
|
||||||
syncedFiles
|
syncedFiles,
|
||||||
|
deletedFiles
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`JSON sync failed for ${repoUrl}:`, error);
|
console.error(`JSON sync failed for ${repoUrl}:`, error);
|
||||||
@@ -189,7 +206,8 @@ class GitHubJsonService {
|
|||||||
success: false,
|
success: false,
|
||||||
message: `Failed to sync JSON files from ${repoUrl}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
message: `Failed to sync JSON files from ${repoUrl}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
count: 0,
|
count: 0,
|
||||||
syncedFiles: []
|
syncedFiles: [],
|
||||||
|
deletedFiles: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,13 +223,15 @@ class GitHubJsonService {
|
|||||||
success: false,
|
success: false,
|
||||||
message: 'No enabled repositories found',
|
message: 'No enabled repositories found',
|
||||||
count: 0,
|
count: 0,
|
||||||
syncedFiles: []
|
syncedFiles: [],
|
||||||
|
deletedFiles: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Found ${enabledRepos.length} enabled repositories`);
|
console.log(`Found ${enabledRepos.length} enabled repositories`);
|
||||||
|
|
||||||
const allSyncedFiles = [];
|
const allSyncedFiles = [];
|
||||||
|
const allDeletedFiles = [];
|
||||||
const processedSlugs = new Set();
|
const processedSlugs = new Set();
|
||||||
let totalSynced = 0;
|
let totalSynced = 0;
|
||||||
|
|
||||||
@@ -222,6 +242,7 @@ class GitHubJsonService {
|
|||||||
const result = await this.syncJsonFilesForRepo(repo.url);
|
const result = await this.syncJsonFilesForRepo(repo.url);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
allDeletedFiles.push(...(result.deletedFiles ?? []));
|
||||||
const newFiles = result.syncedFiles.filter(file => {
|
const newFiles = result.syncedFiles.filter(file => {
|
||||||
const slug = file.replace('.json', '');
|
const slug = file.replace('.json', '');
|
||||||
if (processedSlugs.has(slug)) {
|
if (processedSlugs.has(slug)) {
|
||||||
@@ -243,11 +264,16 @@ class GitHubJsonService {
|
|||||||
|
|
||||||
await this.updateExistingFilesWithRepositoryUrl();
|
await this.updateExistingFilesWithRepositoryUrl();
|
||||||
|
|
||||||
|
const msg =
|
||||||
|
allDeletedFiles.length > 0
|
||||||
|
? `Successfully synced ${totalSynced} JSON files from ${enabledRepos.length} repositories, removed ${allDeletedFiles.length} obsolete file(s).`
|
||||||
|
: `Successfully synced ${totalSynced} JSON files from ${enabledRepos.length} repositories`;
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Successfully synced ${totalSynced} JSON files from ${enabledRepos.length} repositories`,
|
message: msg,
|
||||||
count: totalSynced,
|
count: totalSynced,
|
||||||
syncedFiles: allSyncedFiles
|
syncedFiles: allSyncedFiles,
|
||||||
|
deletedFiles: allDeletedFiles
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Multi-repository JSON sync failed:', error);
|
console.error('Multi-repository JSON sync failed:', error);
|
||||||
@@ -255,7 +281,8 @@ class GitHubJsonService {
|
|||||||
success: false,
|
success: false,
|
||||||
message: `Failed to sync JSON files: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
message: `Failed to sync JSON files: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
count: 0,
|
count: 0,
|
||||||
syncedFiles: []
|
syncedFiles: [],
|
||||||
|
deletedFiles: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,6 +324,32 @@ class GitHubJsonService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteLocalFilesRemovedFromRepo(repoUrl, remoteFilenames) {
|
||||||
|
this.initializeConfig();
|
||||||
|
const localFiles = await this.getLocalJsonFiles();
|
||||||
|
const deletedFiles = [];
|
||||||
|
|
||||||
|
for (const file of localFiles) {
|
||||||
|
try {
|
||||||
|
const filePath = join(this.localJsonDirectory, file);
|
||||||
|
const content = await readFile(filePath, 'utf-8');
|
||||||
|
const script = JSON.parse(content);
|
||||||
|
|
||||||
|
if (script.repository_url === repoUrl && !remoteFilenames.has(file)) {
|
||||||
|
await unlink(filePath);
|
||||||
|
const slug = file.replace(/\.json$/, '');
|
||||||
|
this.scriptCache.delete(slug);
|
||||||
|
deletedFiles.push(file);
|
||||||
|
console.log(`Removed obsolete script JSON: ${file} (no longer in ${repoUrl})`);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If we can't read or parse the file, skip (do not delete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deletedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
async findFilesToSyncForRepo(repoUrl, githubFiles, localFiles) {
|
async findFilesToSyncForRepo(repoUrl, githubFiles, localFiles) {
|
||||||
const filesToSync = [];
|
const filesToSync = [];
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { writeFile, mkdir, readdir, readFile } from 'fs/promises';
|
import { writeFile, mkdir, readdir, readFile, unlink } from 'fs/promises';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { env } from '../../env.js';
|
import { env } from '../../env.js';
|
||||||
import type { Script, ScriptCard, GitHubFile } from '../../types/script';
|
import type { Script, ScriptCard, GitHubFile } from '../../types/script';
|
||||||
@@ -158,7 +158,7 @@ export class GitHubJsonService {
|
|||||||
/**
|
/**
|
||||||
* Sync JSON files from a specific repository
|
* Sync JSON files from a specific repository
|
||||||
*/
|
*/
|
||||||
async syncJsonFilesForRepo(repoUrl: string): Promise<{ success: boolean; message: string; count: number; syncedFiles: string[] }> {
|
async syncJsonFilesForRepo(repoUrl: string): Promise<{ success: boolean; message: string; count: number; syncedFiles: string[]; deletedFiles: string[] }> {
|
||||||
try {
|
try {
|
||||||
console.log(`Starting JSON sync from repository: ${repoUrl}`);
|
console.log(`Starting JSON sync from repository: ${repoUrl}`);
|
||||||
|
|
||||||
@@ -170,28 +170,45 @@ export class GitHubJsonService {
|
|||||||
const localFiles = await this.getLocalJsonFiles();
|
const localFiles = await this.getLocalJsonFiles();
|
||||||
console.log(`Found ${localFiles.length} local JSON files`);
|
console.log(`Found ${localFiles.length} local JSON files`);
|
||||||
|
|
||||||
|
// Delete local JSON files that belong to this repo but are no longer in the remote
|
||||||
|
const remoteFilenames = new Set(githubFiles.map((f) => f.name));
|
||||||
|
const deletedFiles = await this.deleteLocalFilesRemovedFromRepo(repoUrl, remoteFilenames);
|
||||||
|
if (deletedFiles.length > 0) {
|
||||||
|
console.log(`Removed ${deletedFiles.length} obsolete JSON file(s) no longer in ${repoUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Compare and find files that need syncing
|
// Compare and find files that need syncing
|
||||||
// For multi-repo support, we need to check if file exists AND if it's from this repo
|
// For multi-repo support, we need to check if file exists AND if it's from this repo
|
||||||
const filesToSync = await this.findFilesToSyncForRepo(repoUrl, githubFiles, localFiles);
|
const filesToSync = await this.findFilesToSyncForRepo(repoUrl, githubFiles, localFiles);
|
||||||
console.log(`Found ${filesToSync.length} files that need syncing from ${repoUrl}`);
|
console.log(`Found ${filesToSync.length} files that need syncing from ${repoUrl}`);
|
||||||
|
|
||||||
if (filesToSync.length === 0) {
|
if (filesToSync.length === 0) {
|
||||||
|
const msg =
|
||||||
|
deletedFiles.length > 0
|
||||||
|
? `All JSON files are up to date for repository: ${repoUrl}. Removed ${deletedFiles.length} obsolete file(s).`
|
||||||
|
: `All JSON files are up to date for repository: ${repoUrl}`;
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `All JSON files are up to date for repository: ${repoUrl}`,
|
message: msg,
|
||||||
count: 0,
|
count: 0,
|
||||||
syncedFiles: []
|
syncedFiles: [],
|
||||||
|
deletedFiles
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download and save only the files that need syncing
|
// Download and save only the files that need syncing
|
||||||
const syncedFiles = await this.syncSpecificFiles(repoUrl, filesToSync);
|
const syncedFiles = await this.syncSpecificFiles(repoUrl, filesToSync);
|
||||||
|
|
||||||
|
const msg =
|
||||||
|
deletedFiles.length > 0
|
||||||
|
? `Successfully synced ${syncedFiles.length} JSON files from ${repoUrl}, removed ${deletedFiles.length} obsolete file(s).`
|
||||||
|
: `Successfully synced ${syncedFiles.length} JSON files from ${repoUrl}`;
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Successfully synced ${syncedFiles.length} JSON files from ${repoUrl}`,
|
message: msg,
|
||||||
count: syncedFiles.length,
|
count: syncedFiles.length,
|
||||||
syncedFiles
|
syncedFiles,
|
||||||
|
deletedFiles
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`JSON sync failed for ${repoUrl}:`, error);
|
console.error(`JSON sync failed for ${repoUrl}:`, error);
|
||||||
@@ -199,7 +216,8 @@ export class GitHubJsonService {
|
|||||||
success: false,
|
success: false,
|
||||||
message: `Failed to sync JSON files from ${repoUrl}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
message: `Failed to sync JSON files from ${repoUrl}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
count: 0,
|
count: 0,
|
||||||
syncedFiles: []
|
syncedFiles: [],
|
||||||
|
deletedFiles: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,7 +225,7 @@ export class GitHubJsonService {
|
|||||||
/**
|
/**
|
||||||
* Sync JSON files from all enabled repositories (main repo has priority)
|
* Sync JSON files from all enabled repositories (main repo has priority)
|
||||||
*/
|
*/
|
||||||
async syncJsonFiles(): Promise<{ success: boolean; message: string; count: number; syncedFiles: string[] }> {
|
async syncJsonFiles(): Promise<{ success: boolean; message: string; count: number; syncedFiles: string[]; deletedFiles: string[] }> {
|
||||||
try {
|
try {
|
||||||
console.log('Starting multi-repository JSON sync...');
|
console.log('Starting multi-repository JSON sync...');
|
||||||
|
|
||||||
@@ -218,13 +236,15 @@ export class GitHubJsonService {
|
|||||||
success: false,
|
success: false,
|
||||||
message: 'No enabled repositories found',
|
message: 'No enabled repositories found',
|
||||||
count: 0,
|
count: 0,
|
||||||
syncedFiles: []
|
syncedFiles: [],
|
||||||
|
deletedFiles: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Found ${enabledRepos.length} enabled repositories`);
|
console.log(`Found ${enabledRepos.length} enabled repositories`);
|
||||||
|
|
||||||
const allSyncedFiles: string[] = [];
|
const allSyncedFiles: string[] = [];
|
||||||
|
const allDeletedFiles: string[] = [];
|
||||||
const processedSlugs = new Set<string>(); // Track slugs we've already processed
|
const processedSlugs = new Set<string>(); // Track slugs we've already processed
|
||||||
let totalSynced = 0;
|
let totalSynced = 0;
|
||||||
|
|
||||||
@@ -236,6 +256,7 @@ export class GitHubJsonService {
|
|||||||
const result = await this.syncJsonFilesForRepo(repo.url);
|
const result = await this.syncJsonFilesForRepo(repo.url);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
allDeletedFiles.push(...(result.deletedFiles ?? []));
|
||||||
// Only count files that weren't already processed from a higher priority repo
|
// Only count files that weren't already processed from a higher priority repo
|
||||||
const newFiles = result.syncedFiles.filter(file => {
|
const newFiles = result.syncedFiles.filter(file => {
|
||||||
const slug = file.replace('.json', '');
|
const slug = file.replace('.json', '');
|
||||||
@@ -259,11 +280,16 @@ export class GitHubJsonService {
|
|||||||
// Also update existing files that don't have repository_url set (backward compatibility)
|
// Also update existing files that don't have repository_url set (backward compatibility)
|
||||||
await this.updateExistingFilesWithRepositoryUrl();
|
await this.updateExistingFilesWithRepositoryUrl();
|
||||||
|
|
||||||
|
const msg =
|
||||||
|
allDeletedFiles.length > 0
|
||||||
|
? `Successfully synced ${totalSynced} JSON files from ${enabledRepos.length} repositories, removed ${allDeletedFiles.length} obsolete file(s).`
|
||||||
|
: `Successfully synced ${totalSynced} JSON files from ${enabledRepos.length} repositories`;
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Successfully synced ${totalSynced} JSON files from ${enabledRepos.length} repositories`,
|
message: msg,
|
||||||
count: totalSynced,
|
count: totalSynced,
|
||||||
syncedFiles: allSyncedFiles
|
syncedFiles: allSyncedFiles,
|
||||||
|
deletedFiles: allDeletedFiles
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Multi-repository JSON sync failed:', error);
|
console.error('Multi-repository JSON sync failed:', error);
|
||||||
@@ -271,7 +297,8 @@ export class GitHubJsonService {
|
|||||||
success: false,
|
success: false,
|
||||||
message: `Failed to sync JSON files: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
message: `Failed to sync JSON files: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
count: 0,
|
count: 0,
|
||||||
syncedFiles: []
|
syncedFiles: [],
|
||||||
|
deletedFiles: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -316,6 +343,36 @@ export class GitHubJsonService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete local JSON files that belong to this repo but are no longer in the remote list.
|
||||||
|
* Returns the list of deleted filenames.
|
||||||
|
*/
|
||||||
|
private async deleteLocalFilesRemovedFromRepo(repoUrl: string, remoteFilenames: Set<string>): Promise<string[]> {
|
||||||
|
this.initializeConfig();
|
||||||
|
const localFiles = await this.getLocalJsonFiles();
|
||||||
|
const deletedFiles: string[] = [];
|
||||||
|
|
||||||
|
for (const file of localFiles) {
|
||||||
|
try {
|
||||||
|
const filePath = join(this.localJsonDirectory!, file);
|
||||||
|
const content = await readFile(filePath, 'utf-8');
|
||||||
|
const script = JSON.parse(content) as Script;
|
||||||
|
|
||||||
|
if (script.repository_url === repoUrl && !remoteFilenames.has(file)) {
|
||||||
|
await unlink(filePath);
|
||||||
|
const slug = file.replace(/\.json$/, '');
|
||||||
|
this.scriptCache.delete(slug);
|
||||||
|
deletedFiles.push(file);
|
||||||
|
console.log(`Removed obsolete script JSON: ${file} (no longer in ${repoUrl})`);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If we can't read or parse the file, skip (do not delete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deletedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find files that need syncing for a specific repository
|
* Find files that need syncing for a specific repository
|
||||||
* This checks if file exists locally AND if it's from the same repository
|
* This checks if file exists locally AND if it's from the same repository
|
||||||
|
|||||||
Reference in New Issue
Block a user