281 lines
8.5 KiB
JavaScript
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 };
|