refactor: remove auto-updater and implement Gitea update handling

This commit is contained in:
2026-03-25 06:35:41 +11:00
parent 1fea022306
commit b54a003a96

View File

@@ -5,7 +5,6 @@ const { exec } = require('child_process');
const { promisify } = require('util'); const { promisify } = require('util');
const Store = require('electron-store'); const Store = require('electron-store');
const open = require('open'); const open = require('open');
const { autoUpdater } = require('electron-updater');
const PaarrotAPIServer = require('./api-server'); const PaarrotAPIServer = require('./api-server');
const https = require('https'); const https = require('https');
const http = require('http'); const http = require('http');
@@ -78,91 +77,199 @@ let apiServer = null;
// Set app name for notifications and system tray // Set app name for notifications and system tray
app.setName('Paarrot'); app.setName('Paarrot');
// Configure auto-updater
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;
// Gitea update configuration // Gitea update configuration
const GITEA_API_BASE = 'http://synbox.ruv.wtf:8418/api/v1'; const GITEA_API_BASE = 'http://synbox.ruv.wtf:8418/api/v1';
const GITEA_REPO = 'litruv/cinny-desktop'; const GITEA_REPO = 'litruv/cinny-desktop';
// Function to fetch latest release from Gitea and configure update feed let pendingUpdateInfo = null;
async function configureGiteaUpdateFeed() { let downloadedUpdatePath = null;
try {
const url = `${GITEA_API_BASE}/repos/${GITEA_REPO}/releases?limit=1`;
console.log('Fetching latest release from:', url);
/**
* Compares two semver-style version strings.
* @param {string} v1
* @param {string} v2
* @returns {number} 1 if v1 > v2, -1 if v1 < v2, 0 if equal
*/
function compareVersions(v1, v2) {
const p1 = v1.split('.').map(Number);
const p2 = v2.split('.').map(Number);
for (let i = 0; i < Math.max(p1.length, p2.length); i++) {
const a = p1[i] || 0;
const b = p2[i] || 0;
if (a > b) return 1;
if (a < b) return -1;
}
return 0;
}
/**
* Performs an HTTP/HTTPS GET request and resolves with parsed JSON.
* Follows redirects automatically.
* @param {string} url
* @returns {Promise<any>}
*/
function httpGetJson(url) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.get(url, (res) => { const protocol = url.startsWith('https') ? https : http;
const options = { headers: { 'User-Agent': `Paarrot/${app.getVersion()}` } };
protocol.get(url, options, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
return httpGetJson(res.headers.location).then(resolve).catch(reject);
}
let data = ''; let data = '';
res.on('data', (chunk) => { data += chunk; }); res.on('data', (chunk) => { data += chunk; });
res.on('end', () => { res.on('end', () => {
try { try { resolve(JSON.parse(data)); }
const releases = JSON.parse(data); catch { resolve(data); }
if (releases && releases.length > 0) {
const latestRelease = releases[0];
const version = latestRelease.tag_name.replace(/^v/, '');
const downloadUrl = `http://synbox.ruv.wtf:8418/${GITEA_REPO}/releases/download/${latestRelease.tag_name}`;
console.log('Latest release version:', version);
console.log('Download URL:', downloadUrl);
// Configure auto-updater with the versioned release URL
autoUpdater.setFeedURL({
provider: 'generic',
url: downloadUrl,
channel: 'latest'
});
resolve({ version, url: downloadUrl });
} else {
reject(new Error('No releases found'));
}
} catch (err) {
reject(err);
}
}); });
}).on('error', reject); }).on('error', reject);
}); });
} catch (error) {
console.error('Failed to configure Gitea update feed:', error);
throw error;
}
} }
// Auto-updater event handlers /**
autoUpdater.on('checking-for-update', () => { * Downloads a file from a URL to a destination path, reporting progress.
console.log('Checking for updates...'); * Follows redirects automatically.
* @param {string} url
* @param {string} dest
* @param {(percent: number) => void} onProgress
* @returns {Promise<void>}
*/
function downloadFile(url, dest, onProgress) {
return new Promise((resolve, reject) => {
const doRequest = (requestUrl) => {
const protocol = requestUrl.startsWith('https') ? https : http;
const options = { headers: { 'User-Agent': `Paarrot/${app.getVersion()}` } };
protocol.get(requestUrl, options, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
doRequest(res.headers.location);
return;
}
const total = parseInt(res.headers['content-length'] || '0', 10);
let transferred = 0;
const file = fs.createWriteStream(dest);
res.on('data', (chunk) => {
transferred += chunk.length;
if (total > 0) onProgress(Math.round((transferred / total) * 100));
}); });
res.pipe(file);
file.on('finish', () => file.close(resolve));
file.on('error', reject);
}).on('error', reject);
};
doRequest(url);
});
}
/**
* Checks the Gitea API for a newer release. Emits 'update-available' to the
* renderer if one is found. Does NOT require a latest.yml asset.
* @returns {Promise<{updateAvailable: boolean, info?: object}>}
*/
async function checkForGiteaUpdate() {
const currentVersion = app.getVersion();
console.log('Checking for updates, current version:', currentVersion);
const releases = await httpGetJson(`${GITEA_API_BASE}/repos/${GITEA_REPO}/releases?limit=1`);
if (!Array.isArray(releases) || releases.length === 0) {
console.log('No releases found');
return { updateAvailable: false };
}
const latest = releases[0];
const latestVersion = latest.tag_name.replace(/^v/, '');
console.log('Latest release version:', latestVersion);
if (compareVersions(latestVersion, currentVersion) <= 0) {
console.log('No update available');
return { updateAvailable: false };
}
const assets = latest.assets || [];
let downloadUrl = null;
if (process.platform === 'win32') {
const asset = assets.find((a) => /win.*\.exe$/i.test(a.name) || /Setup.*\.exe$/i.test(a.name));
downloadUrl = asset?.browser_download_url || null;
} else if (process.platform === 'linux') {
const asset = assets.find((a) => a.name.endsWith('.AppImage'));
downloadUrl = asset?.browser_download_url || null;
} else if (process.platform === 'darwin') {
const asset = assets.find((a) => a.name.endsWith('.dmg'));
downloadUrl = asset?.browser_download_url || null;
}
pendingUpdateInfo = {
version: latestVersion,
releaseNotes: latest.body || '',
releaseDate: latest.published_at,
downloadUrl,
htmlUrl: latest.html_url,
};
console.log('Update available:', latestVersion, '| asset URL:', downloadUrl || '(none, will use release page)');
autoUpdater.on('update-available', (info) => {
console.log('Update available:', info.version);
if (mainWindow) { if (mainWindow) {
mainWindow.webContents.send('update-available', info); mainWindow.webContents.send('update-available', pendingUpdateInfo);
} }
});
autoUpdater.on('update-not-available', (info) => { return { updateAvailable: true, info: pendingUpdateInfo };
console.log('No updates available'); }
});
autoUpdater.on('error', (err) => { /**
console.error('Auto-updater error:', err); * Downloads the pending update asset to the temp directory, reporting progress.
}); * If no direct asset URL is available, opens the release page in the browser.
* @returns {Promise<{success: boolean, opened?: boolean}>}
*/
async function downloadGiteaUpdate() {
if (!pendingUpdateInfo) throw new Error('No pending update info');
autoUpdater.on('download-progress', (progressObj) => { const { downloadUrl, htmlUrl, version } = pendingUpdateInfo;
console.log(`Download progress: ${progressObj.percent}%`);
if (!downloadUrl) {
shell.openExternal(htmlUrl);
return { success: true, opened: true };
}
const ext = downloadUrl.split('.').pop();
const tmpDir = path.join(app.getPath('temp'), 'paarrot-update');
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
const destPath = path.join(tmpDir, `Paarrot-${version}-update.${ext}`);
await downloadFile(downloadUrl, destPath, (percent) => {
console.log(`Download progress: ${percent}%`);
if (mainWindow) { if (mainWindow) {
mainWindow.webContents.send('update-download-progress', progressObj); mainWindow.webContents.send('update-download-progress', { percent, transferred: 0, total: 0 });
} }
}); });
autoUpdater.on('update-downloaded', (info) => { downloadedUpdatePath = destPath;
console.log('Update downloaded:', info.version); console.log('Update downloaded to:', destPath);
if (mainWindow) { if (mainWindow) {
mainWindow.webContents.send('update-downloaded', info); mainWindow.webContents.send('update-downloaded', pendingUpdateInfo);
}
return { success: true };
}
/**
* Launches the downloaded installer (or opens the release page if no file was
* downloaded) and quits the app.
*/
function installGiteaUpdate() {
if (downloadedUpdatePath && fs.existsSync(downloadedUpdatePath)) {
if (process.platform === 'win32') {
exec(`"${downloadedUpdatePath}"`);
} else {
shell.openPath(downloadedUpdatePath);
}
} else if (pendingUpdateInfo?.htmlUrl) {
shell.openExternal(pendingUpdateInfo.htmlUrl);
}
app.quit();
} }
});
// Single instance lock // Single instance lock
@@ -551,8 +658,7 @@ app.whenReady().then(() => {
// Check for updates on start (after 3 seconds) // Check for updates on start (after 3 seconds)
setTimeout(async () => { setTimeout(async () => {
try { try {
await configureGiteaUpdateFeed(); await checkForGiteaUpdate();
await autoUpdater.checkForUpdates();
} catch (err) { } catch (err) {
console.error('Failed to check for updates:', err); console.error('Failed to check for updates:', err);
} }
@@ -561,8 +667,7 @@ app.whenReady().then(() => {
// Check for updates every 6 hours // Check for updates every 6 hours
setInterval(async () => { setInterval(async () => {
try { try {
await configureGiteaUpdateFeed(); await checkForGiteaUpdate();
await autoUpdater.checkForUpdates();
} catch (err) { } catch (err) {
console.error('Failed to check for updates:', err); console.error('Failed to check for updates:', err);
} }
@@ -802,9 +907,7 @@ ipcMain.handle('check-for-updates', async () => {
return { success: false, error: 'Updates are not available in development mode' }; return { success: false, error: 'Updates are not available in development mode' };
} }
try { try {
// First, fetch latest release info from Gitea and configure feed URL const result = await checkForGiteaUpdate();
await configureGiteaUpdateFeed();
const result = await autoUpdater.checkForUpdates();
return { success: true, data: result }; return { success: true, data: result };
} catch (error) { } catch (error) {
return { success: false, error: error.message }; return { success: false, error: error.message };
@@ -816,8 +919,8 @@ ipcMain.handle('download-update', async () => {
return { success: false, error: 'Updates are not available in development mode' }; return { success: false, error: 'Updates are not available in development mode' };
} }
try { try {
await autoUpdater.downloadUpdate(); const result = await downloadGiteaUpdate();
return { success: true }; return { success: true, data: result };
} catch (error) { } catch (error) {
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
@@ -827,7 +930,7 @@ ipcMain.handle('install-update', () => {
if (isDev) { if (isDev) {
return { success: false, error: 'Updates are not available in development mode' }; return { success: false, error: 'Updates are not available in development mode' };
} }
autoUpdater.quitAndInstall(false, true); installGiteaUpdate();
return { success: true }; return { success: true };
}); });