mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-10-24 08:33:38 +00:00
296 lines
7.3 KiB
JavaScript
296 lines
7.3 KiB
JavaScript
const { PrismaClient } = require("@prisma/client");
|
|
const { exec } = require("node:child_process");
|
|
const { promisify } = require("node:util");
|
|
|
|
const prisma = new PrismaClient();
|
|
const execAsync = promisify(exec);
|
|
|
|
class UpdateScheduler {
|
|
constructor() {
|
|
this.isRunning = false;
|
|
this.intervalId = null;
|
|
this.checkInterval = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
|
|
}
|
|
|
|
// Start the scheduler
|
|
start() {
|
|
if (this.isRunning) {
|
|
console.log("Update scheduler is already running");
|
|
return;
|
|
}
|
|
|
|
console.log("🔄 Starting update scheduler...");
|
|
this.isRunning = true;
|
|
|
|
// Run initial check
|
|
this.checkForUpdates();
|
|
|
|
// Schedule regular checks
|
|
this.intervalId = setInterval(() => {
|
|
this.checkForUpdates();
|
|
}, this.checkInterval);
|
|
|
|
console.log(
|
|
`✅ Update scheduler started - checking every ${this.checkInterval / (60 * 60 * 1000)} hours`,
|
|
);
|
|
}
|
|
|
|
// Stop the scheduler
|
|
stop() {
|
|
if (!this.isRunning) {
|
|
console.log("Update scheduler is not running");
|
|
return;
|
|
}
|
|
|
|
console.log("🛑 Stopping update scheduler...");
|
|
this.isRunning = false;
|
|
|
|
if (this.intervalId) {
|
|
clearInterval(this.intervalId);
|
|
this.intervalId = null;
|
|
}
|
|
|
|
console.log("✅ Update scheduler stopped");
|
|
}
|
|
|
|
// Check for updates
|
|
async checkForUpdates() {
|
|
try {
|
|
console.log("🔍 Checking for updates...");
|
|
|
|
// Get settings
|
|
const settings = await prisma.settings.findFirst();
|
|
const DEFAULT_GITHUB_REPO = "https://github.com/patchMon/patchmon";
|
|
const repoUrl = settings?.githubRepoUrl || DEFAULT_GITHUB_REPO;
|
|
let owner, repo;
|
|
|
|
if (repoUrl.includes("git@github.com:")) {
|
|
const match = repoUrl.match(/git@github\.com:([^/]+)\/([^/]+)\.git/);
|
|
if (match) {
|
|
[, owner, repo] = match;
|
|
}
|
|
} else if (repoUrl.includes("github.com/")) {
|
|
const match = repoUrl.match(
|
|
/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/,
|
|
);
|
|
if (match) {
|
|
[, owner, repo] = match;
|
|
}
|
|
}
|
|
|
|
if (!owner || !repo) {
|
|
console.log(
|
|
"⚠️ Could not parse GitHub repository URL, skipping update check",
|
|
);
|
|
return;
|
|
}
|
|
|
|
let latestVersion;
|
|
const isPrivate = settings.repositoryType === "private";
|
|
|
|
if (isPrivate) {
|
|
// Use SSH for private repositories
|
|
latestVersion = await this.checkPrivateRepo(settings, owner, repo);
|
|
} else {
|
|
// Use GitHub API for public repositories
|
|
latestVersion = await this.checkPublicRepo(owner, repo);
|
|
}
|
|
|
|
if (!latestVersion) {
|
|
console.log(
|
|
"⚠️ Could not determine latest version, skipping update check",
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Read version from package.json dynamically
|
|
let currentVersion = "1.2.8"; // fallback
|
|
try {
|
|
const packageJson = require("../../package.json");
|
|
if (packageJson?.version) {
|
|
currentVersion = packageJson.version;
|
|
}
|
|
} catch (packageError) {
|
|
console.warn(
|
|
"Could not read version from package.json, using fallback:",
|
|
packageError.message,
|
|
);
|
|
}
|
|
const isUpdateAvailable =
|
|
this.compareVersions(latestVersion, currentVersion) > 0;
|
|
|
|
// Update settings with check results
|
|
await prisma.settings.update({
|
|
where: { id: settings.id },
|
|
data: {
|
|
last_update_check: new Date(),
|
|
update_available: isUpdateAvailable,
|
|
latest_version: latestVersion,
|
|
},
|
|
});
|
|
|
|
console.log(
|
|
`✅ Update check completed - Current: ${currentVersion}, Latest: ${latestVersion}, Update Available: ${isUpdateAvailable}`,
|
|
);
|
|
} catch (error) {
|
|
console.error("❌ Error checking for updates:", error.message);
|
|
|
|
// Update last check time even on error
|
|
try {
|
|
const settings = await prisma.settings.findFirst();
|
|
if (settings) {
|
|
await prisma.settings.update({
|
|
where: { id: settings.id },
|
|
data: {
|
|
last_update_check: new Date(),
|
|
update_available: false,
|
|
},
|
|
});
|
|
}
|
|
} catch (updateError) {
|
|
console.error(
|
|
"❌ Error updating last check time:",
|
|
updateError.message,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check private repository using SSH
|
|
async checkPrivateRepo(settings, owner, repo) {
|
|
try {
|
|
let sshKeyPath = settings.sshKeyPath;
|
|
|
|
// Try to find SSH key if not configured
|
|
if (!sshKeyPath) {
|
|
const possibleKeyPaths = [
|
|
"/root/.ssh/id_ed25519",
|
|
"/root/.ssh/id_rsa",
|
|
"/home/patchmon/.ssh/id_ed25519",
|
|
"/home/patchmon/.ssh/id_rsa",
|
|
"/var/www/.ssh/id_ed25519",
|
|
"/var/www/.ssh/id_rsa",
|
|
];
|
|
|
|
for (const path of possibleKeyPaths) {
|
|
try {
|
|
require("node:fs").accessSync(path);
|
|
sshKeyPath = path;
|
|
break;
|
|
} catch {
|
|
// Key not found at this path, try next
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!sshKeyPath) {
|
|
throw new Error("No SSH deploy key found");
|
|
}
|
|
|
|
const sshRepoUrl = `git@github.com:${owner}/${repo}.git`;
|
|
const env = {
|
|
...process.env,
|
|
GIT_SSH_COMMAND: `ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no -o IdentitiesOnly=yes`,
|
|
};
|
|
|
|
const { stdout: sshLatestTag } = await execAsync(
|
|
`git ls-remote --tags --sort=-version:refname ${sshRepoUrl} | head -n 1 | sed 's/.*refs\\/tags\\///' | sed 's/\\^{}//'`,
|
|
{
|
|
timeout: 10000,
|
|
env: env,
|
|
},
|
|
);
|
|
|
|
return sshLatestTag.trim().replace("v", "");
|
|
} catch (error) {
|
|
console.error("SSH Git error:", error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Check public repository using GitHub API
|
|
async checkPublicRepo(owner, repo) {
|
|
try {
|
|
const httpsRepoUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
|
|
|
|
// Get current version for User-Agent
|
|
let currentVersion = "1.2.8"; // fallback
|
|
try {
|
|
const packageJson = require("../../package.json");
|
|
if (packageJson?.version) {
|
|
currentVersion = packageJson.version;
|
|
}
|
|
} catch (packageError) {
|
|
console.warn(
|
|
"Could not read version from package.json for User-Agent, using fallback:",
|
|
packageError.message,
|
|
);
|
|
}
|
|
|
|
const response = await fetch(httpsRepoUrl, {
|
|
method: "GET",
|
|
headers: {
|
|
Accept: "application/vnd.github.v3+json",
|
|
"User-Agent": `PatchMon-Server/${currentVersion}`,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
if (
|
|
errorText.includes("rate limit") ||
|
|
errorText.includes("API rate limit")
|
|
) {
|
|
console.log(
|
|
"⚠️ GitHub API rate limit exceeded, skipping update check",
|
|
);
|
|
return null; // Return null instead of throwing error
|
|
}
|
|
throw new Error(
|
|
`GitHub API error: ${response.status} ${response.statusText}`,
|
|
);
|
|
}
|
|
|
|
const releaseData = await response.json();
|
|
return releaseData.tag_name.replace("v", "");
|
|
} catch (error) {
|
|
console.error("GitHub API error:", error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Compare version strings (semantic versioning)
|
|
compareVersions(version1, version2) {
|
|
const v1parts = version1.split(".").map(Number);
|
|
const v2parts = version2.split(".").map(Number);
|
|
|
|
const maxLength = Math.max(v1parts.length, v2parts.length);
|
|
|
|
for (let i = 0; i < maxLength; i++) {
|
|
const v1part = v1parts[i] || 0;
|
|
const v2part = v2parts[i] || 0;
|
|
|
|
if (v1part > v2part) return 1;
|
|
if (v1part < v2part) return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Get scheduler status
|
|
getStatus() {
|
|
return {
|
|
isRunning: this.isRunning,
|
|
checkInterval: this.checkInterval,
|
|
nextCheck: this.isRunning
|
|
? new Date(Date.now() + this.checkInterval)
|
|
: null,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Create singleton instance
|
|
const updateScheduler = new UpdateScheduler();
|
|
|
|
module.exports = updateScheduler;
|