coolify integration.
This commit is contained in:
280
app/main/server-scanner.js
Normal file
280
app/main/server-scanner.js
Normal file
@@ -0,0 +1,280 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user