Files
idea.llm.gitea.repo.docker.…/app/main/server-scanner.js
2026-02-27 08:55:41 -06:00

281 lines
8.5 KiB
JavaScript

const { SSHService } = require('./ssh-service');
const fs = require('fs');
const path = require('path');
class ServerScanner {
constructor(sshConfig) {
this.sshConfig = sshConfig;
this.ssh = new SSHService(sshConfig);
}
async scan() {
const deployed = [];
try {
await this.ssh.connect();
// List directories in ~/containers
const result = await this.ssh.exec('ls -1 ~/containers 2>/dev/null || echo ""');
const projectDirs = result.split('\n').filter(Boolean);
for (const dir of projectDirs) {
const remotePath = `~/containers/${dir}`;
const info = await this.getProjectInfo(remotePath, dir);
if (info) {
deployed.push(info);
}
}
this.ssh.disconnect();
} catch (err) {
console.error('Failed to scan server:', err);
throw err;
}
return deployed;
}
async getProjectInfo(remotePath, name) {
try {
// Check what files exist
const filesResult = await this.ssh.exec(`ls -la ${remotePath} 2>/dev/null || echo ""`);
const hasDockerCompose = filesResult.includes('docker-compose.yml');
const hasEnv = filesResult.includes('.env');
const hasData = filesResult.includes('data');
const tarMatch = filesResult.match(/(\S+\.tar)/);
const tarFile = tarMatch ? tarMatch[1] : null;
// Get docker-compose.yml content if exists
let dockerComposeContent = null;
if (hasDockerCompose) {
try {
dockerComposeContent = await this.ssh.exec(`cat ${remotePath}/docker-compose.yml 2>/dev/null`);
} catch (err) {
// ignore
}
}
return {
name,
remotePath,
hasDockerCompose,
hasEnv,
hasData,
tarFile,
dockerComposeContent
};
} catch (err) {
return null;
}
}
async compareFiles(localProjectPath, remotePath, additionalFiles = []) {
const diff = {
dockerCompose: { status: 'unknown', localContent: null, remoteContent: null },
env: { status: 'unknown' },
data: { status: 'unknown' },
// New: array of all compared files for UI
files: []
};
try {
await this.ssh.connect();
// Compare docker-compose.yml
const localComposePath = path.join(localProjectPath, 'docker-compose.yml');
if (fs.existsSync(localComposePath)) {
diff.dockerCompose.localContent = fs.readFileSync(localComposePath, 'utf-8');
try {
diff.dockerCompose.remoteContent = await this.ssh.exec(`cat ${remotePath}/docker-compose.yml 2>/dev/null`);
if (diff.dockerCompose.localContent.trim() === diff.dockerCompose.remoteContent.trim()) {
diff.dockerCompose.status = 'match';
} else {
diff.dockerCompose.status = 'different';
}
} catch (err) {
diff.dockerCompose.status = 'remote-missing';
}
} else {
diff.dockerCompose.status = 'local-missing';
}
// Add to files array
diff.files.push({
name: 'docker-compose.yml',
type: 'file',
status: diff.dockerCompose.status,
localContent: diff.dockerCompose.localContent,
remoteContent: diff.dockerCompose.remoteContent,
localPath: localComposePath,
remotePath: `${remotePath}/docker-compose.yml`
});
// Check .env exists on both and compare content
const localEnvPath = path.join(localProjectPath, '.env');
const hasLocalEnv = fs.existsSync(localEnvPath);
// Read local .env content
if (hasLocalEnv) {
diff.env.localContent = fs.readFileSync(localEnvPath, 'utf-8');
}
// Check and read remote .env
try {
await this.ssh.exec(`test -f ${remotePath}/.env`);
diff.env.hasRemote = true;
try {
diff.env.remoteContent = await this.ssh.exec(`cat ${remotePath}/.env 2>/dev/null`);
} catch (err) {
diff.env.remoteContent = null;
}
} catch (err) {
diff.env.hasRemote = false;
}
diff.env.hasLocal = hasLocalEnv;
// Determine status based on content comparison
if (hasLocalEnv && diff.env.hasRemote) {
if (diff.env.localContent && diff.env.remoteContent) {
diff.env.status = diff.env.localContent.trim() === diff.env.remoteContent.trim()
? 'match' : 'different';
} else {
diff.env.status = 'both-exist';
}
} else if (hasLocalEnv) {
diff.env.status = 'remote-missing';
} else if (diff.env.hasRemote) {
diff.env.status = 'local-missing';
} else {
diff.env.status = 'neither';
}
// Add to files array
diff.files.push({
name: '.env',
type: 'file',
status: diff.env.status,
localContent: diff.env.localContent,
remoteContent: diff.env.remoteContent,
localPath: localEnvPath,
remotePath: `${remotePath}/.env`,
sensitive: true // Mark as sensitive for masking in UI
});
// Check data directory
const localDataPath = path.join(localProjectPath, 'data');
const hasLocalData = fs.existsSync(localDataPath);
try {
await this.ssh.exec(`test -d ${remotePath}/data`);
diff.data.hasRemote = true;
} catch (err) {
diff.data.hasRemote = false;
}
diff.data.hasLocal = hasLocalData;
diff.data.status = hasLocalData && diff.data.hasRemote ? 'both-exist' :
hasLocalData ? 'remote-missing' :
diff.data.hasRemote ? 'local-missing' : 'neither';
// Add to files array
diff.files.push({
name: 'data/',
type: 'directory',
status: diff.data.status,
localPath: localDataPath,
remotePath: `${remotePath}/data`
});
// Compare additional files from project config
for (const fileSpec of additionalFiles) {
const fileName = typeof fileSpec === 'string' ? fileSpec : fileSpec.local;
const isDir = fileName.endsWith('/');
const cleanName = fileName.replace(/\/$/, '');
const localFilePath = path.join(localProjectPath, cleanName);
const remoteFilePath = `${remotePath}/${cleanName}`;
// Skip if already compared (default files)
if (['docker-compose.yml', '.env', 'data', 'data/'].includes(fileName)) {
continue;
}
const fileInfo = {
name: fileName,
type: isDir ? 'directory' : 'file',
status: 'unknown',
localPath: localFilePath,
remotePath: remoteFilePath
};
const hasLocal = fs.existsSync(localFilePath);
if (isDir) {
// Directory comparison
let hasRemote = false;
try {
await this.ssh.exec(`test -d ${remoteFilePath}`);
hasRemote = true;
} catch (err) {
hasRemote = false;
}
fileInfo.status = hasLocal && hasRemote ? 'both-exist' :
hasLocal ? 'remote-missing' :
hasRemote ? 'local-missing' : 'neither';
} else {
// File comparison with content
if (hasLocal) {
try {
fileInfo.localContent = fs.readFileSync(localFilePath, 'utf-8');
} catch (err) {
// Binary file or read error
fileInfo.localContent = null;
}
}
try {
fileInfo.remoteContent = await this.ssh.exec(`cat ${remoteFilePath} 2>/dev/null`);
if (hasLocal && fileInfo.localContent !== null && fileInfo.remoteContent !== null) {
fileInfo.status = fileInfo.localContent.trim() === fileInfo.remoteContent.trim()
? 'match' : 'different';
} else if (hasLocal) {
fileInfo.status = 'different'; // Local exists but can't compare content
} else {
fileInfo.status = 'local-missing';
}
} catch (err) {
fileInfo.status = hasLocal ? 'remote-missing' : 'neither';
}
}
diff.files.push(fileInfo);
}
this.ssh.disconnect();
} catch (err) {
console.error('Failed to compare files:', err);
throw err;
}
return diff;
}
async getRemoteFileContent(remotePath) {
try {
await this.ssh.connect();
const content = await this.ssh.exec(`cat ${remotePath} 2>/dev/null`);
this.ssh.disconnect();
return content;
} catch (err) {
throw err;
}
}
}
module.exports = { ServerScanner };