Enable updater and remove unzipping and installing update text (#2918)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Switch } from '../../ui/switch';
|
||||
import UpdateSection from './UpdateSection';
|
||||
import { UPDATES_ENABLED } from '../../../updates';
|
||||
|
||||
interface AppSettingsSectionProps {
|
||||
scrollToSection?: string;
|
||||
@@ -11,7 +12,6 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
|
||||
const [dockIconEnabled, setDockIconEnabled] = useState(true);
|
||||
const [isMacOS, setIsMacOS] = useState(false);
|
||||
const [isDockSwitchDisabled, setIsDockSwitchDisabled] = useState(false);
|
||||
const [updatesEnabled, setUpdatesEnabled] = useState(false);
|
||||
const updateSectionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Check if running on macOS
|
||||
@@ -19,25 +19,6 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
|
||||
setIsMacOS(window.electron.platform === 'darwin');
|
||||
}, []);
|
||||
|
||||
// Load updater state
|
||||
useEffect(() => {
|
||||
window.electron.getUpdaterEnabled().then((enabled) => {
|
||||
setUpdatesEnabled(enabled);
|
||||
});
|
||||
|
||||
// Listen for updater state changes
|
||||
const handleUpdaterStateChange = (enabled: boolean) => {
|
||||
setUpdatesEnabled(enabled);
|
||||
};
|
||||
|
||||
window.electron.onUpdaterStateChanged(handleUpdaterStateChange);
|
||||
|
||||
// Cleanup listener on unmount
|
||||
return () => {
|
||||
window.electron.removeUpdaterStateListener(handleUpdaterStateChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Handle scrolling to update section
|
||||
useEffect(() => {
|
||||
if (scrollToSection === 'update' && updateSectionRef.current) {
|
||||
@@ -144,7 +125,7 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
|
||||
</div>
|
||||
|
||||
{/* Update Section */}
|
||||
{updatesEnabled && (
|
||||
{UPDATES_ENABLED && (
|
||||
<div ref={updateSectionRef} className="mt-8 pt-8 border-t border-gray-200">
|
||||
<UpdateSection />
|
||||
</div>
|
||||
|
||||
@@ -147,8 +147,6 @@ export default function UpdateSection() {
|
||||
};
|
||||
|
||||
const installUpdate = () => {
|
||||
setUpdateStatus('installing');
|
||||
// This will quit the app and install the update
|
||||
window.electron.installUpdate();
|
||||
};
|
||||
|
||||
@@ -158,8 +156,6 @@ export default function UpdateSection() {
|
||||
return 'Checking for updates...';
|
||||
case 'downloading':
|
||||
return `Downloading update... ${Math.round(progress)}%`;
|
||||
case 'installing':
|
||||
return 'Installing update...';
|
||||
case 'ready':
|
||||
return 'Update downloaded and ready to install!';
|
||||
case 'success':
|
||||
@@ -180,7 +176,6 @@ export default function UpdateSection() {
|
||||
switch (updateStatus) {
|
||||
case 'checking':
|
||||
case 'downloading':
|
||||
case 'installing':
|
||||
return <Loader2 className="w-4 h-4 animate-spin" />;
|
||||
case 'success':
|
||||
return <CheckCircle className="w-4 h-4 text-green-500" />;
|
||||
@@ -257,9 +252,10 @@ export default function UpdateSection() {
|
||||
{/* Update information */}
|
||||
{updateInfo.isUpdateAvailable && (
|
||||
<div className="text-xs text-textSubtle mt-4 space-y-1">
|
||||
<p>Update will be downloaded and automatically extracted to your Downloads folder.</p>
|
||||
<p>Update will be downloaded to your Downloads folder.</p>
|
||||
<p className="text-xs text-amber-600">
|
||||
After download, move the Goose app to /Applications to complete the update.
|
||||
After download, extract Goose-{updateInfo.latestVersion}.zip and move the Goose app
|
||||
to /Applications to complete the update.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+3
-52
@@ -44,22 +44,10 @@ import {
|
||||
} from './utils/autoUpdater';
|
||||
import { UPDATES_ENABLED } from './updates';
|
||||
|
||||
// Updater toggle functions (moved here to keep updates.ts minimal for release replacement)
|
||||
let updatesEnabled = UPDATES_ENABLED;
|
||||
|
||||
function toggleUpdates(): boolean {
|
||||
updatesEnabled = !updatesEnabled;
|
||||
return updatesEnabled;
|
||||
}
|
||||
|
||||
function getUpdatesEnabled(): boolean {
|
||||
// Only return the toggle state, ignore ENABLE_DEV_UPDATES for UI visibility
|
||||
return updatesEnabled;
|
||||
}
|
||||
|
||||
// Updater functions (moved here to keep updates.ts minimal for release replacement)
|
||||
function shouldSetupUpdater(): boolean {
|
||||
// Setup updater if either the toggle is enabled OR dev updates are enabled
|
||||
return updatesEnabled || process.env.ENABLE_DEV_UPDATES === 'true';
|
||||
// Setup updater if either the flag is enabled OR dev updates are enabled
|
||||
return UPDATES_ENABLED || process.env.ENABLE_DEV_UPDATES === 'true';
|
||||
}
|
||||
|
||||
// Define temp directory for pasted images
|
||||
@@ -1222,38 +1210,6 @@ app.whenReady().then(async () => {
|
||||
// Register the default global hotkey
|
||||
registerGlobalHotkey('CommandOrControl+Alt+Shift+G');
|
||||
|
||||
// Register hidden key combination to toggle updater (Cmd+Shift+U+P+D+A+T+E)
|
||||
globalShortcut.register('CommandOrControl+Shift+U', () => {
|
||||
// This is a multi-key sequence, we'll use a simpler approach
|
||||
// Register a hidden key combination: Cmd/Ctrl + Alt + Shift + U for "Update toggle"
|
||||
const newState = toggleUpdates();
|
||||
log.info(
|
||||
`Updater toggled via keyboard shortcut. New state: ${newState ? 'ENABLED' : 'DISABLED'}`
|
||||
);
|
||||
|
||||
// Show a notification to the user
|
||||
new Notification({
|
||||
title: 'Goose Updater',
|
||||
body: `Updates ${newState ? 'enabled' : 'disabled'}`,
|
||||
}).show();
|
||||
|
||||
// If we're enabling updates and haven't set up the auto-updater yet, set it up now
|
||||
if (newState) {
|
||||
try {
|
||||
setupAutoUpdater(tray || undefined);
|
||||
log.info('Auto-updater setup completed after keyboard toggle');
|
||||
} catch (error) {
|
||||
log.error('Error setting up auto-updater after keyboard toggle:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Notify all windows about the updater state change
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
windows.forEach((win) => {
|
||||
win.webContents.send('updater-state-changed', newState);
|
||||
});
|
||||
});
|
||||
|
||||
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
|
||||
details.requestHeaders['Origin'] = 'http://localhost:5173';
|
||||
callback({ cancel: false, requestHeaders: details.requestHeaders });
|
||||
@@ -1664,11 +1620,6 @@ app.whenReady().then(async () => {
|
||||
ipcMain.on('get-app-version', (event) => {
|
||||
event.returnValue = app.getVersion();
|
||||
});
|
||||
|
||||
// Handler for getting updater state
|
||||
ipcMain.handle('get-updater-enabled', () => {
|
||||
return getUpdatesEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -89,10 +89,6 @@ type ElectronAPI = {
|
||||
restartApp: () => void;
|
||||
onUpdaterEvent: (callback: (event: UpdaterEvent) => void) => void;
|
||||
getUpdateState: () => Promise<{ updateAvailable: boolean; latestVersion?: string } | null>;
|
||||
// Updater state functions
|
||||
getUpdaterEnabled: () => Promise<boolean>;
|
||||
onUpdaterStateChanged: (callback: (enabled: boolean) => void) => void;
|
||||
removeUpdaterStateListener: (callback: (enabled: boolean) => void) => void;
|
||||
};
|
||||
|
||||
type AppConfigAPI = {
|
||||
@@ -100,12 +96,6 @@ type AppConfigAPI = {
|
||||
getAll: () => Record<string, unknown>;
|
||||
};
|
||||
|
||||
// Store callback wrappers for proper cleanup
|
||||
const updaterStateCallbacks = new Map<
|
||||
(enabled: boolean) => void,
|
||||
(event: Electron.IpcRendererEvent, enabled: boolean) => void
|
||||
>();
|
||||
|
||||
const electronAPI: ElectronAPI = {
|
||||
platform: process.platform,
|
||||
reactReady: () => ipcRenderer.send('react-ready'),
|
||||
@@ -193,22 +183,6 @@ const electronAPI: ElectronAPI = {
|
||||
getUpdateState: (): Promise<{ updateAvailable: boolean; latestVersion?: string } | null> => {
|
||||
return ipcRenderer.invoke('get-update-state');
|
||||
},
|
||||
// Updater state functions
|
||||
getUpdaterEnabled: (): Promise<boolean> => {
|
||||
return ipcRenderer.invoke('get-updater-enabled');
|
||||
},
|
||||
onUpdaterStateChanged: (callback: (enabled: boolean) => void): void => {
|
||||
const wrapper = (_event: Electron.IpcRendererEvent, enabled: boolean) => callback(enabled);
|
||||
updaterStateCallbacks.set(callback, wrapper);
|
||||
ipcRenderer.on('updater-state-changed', wrapper);
|
||||
},
|
||||
removeUpdaterStateListener: (callback: (enabled: boolean) => void): void => {
|
||||
const wrapper = updaterStateCallbacks.get(callback);
|
||||
if (wrapper) {
|
||||
ipcRenderer.off('updater-state-changed', wrapper);
|
||||
updaterStateCallbacks.delete(callback);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const appConfigAPI: AppConfigAPI = {
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const UPDATES_ENABLED = false;
|
||||
export const UPDATES_ENABLED = true;
|
||||
|
||||
@@ -201,16 +201,11 @@ export function registerUpdateIpcHandlers() {
|
||||
}
|
||||
|
||||
// Show dialog to inform user about manual installation
|
||||
const isExtracted = !!githubUpdateInfo.extractedPath;
|
||||
const dialogResult = (await dialog.showMessageBox({
|
||||
type: 'info',
|
||||
title: 'Update Ready',
|
||||
message: isExtracted
|
||||
? 'The update has been downloaded and extracted to your Downloads folder.'
|
||||
: 'The update has been downloaded to your Downloads folder.',
|
||||
detail: isExtracted
|
||||
? `Please move the Goose app from ${path.basename(updatePath)} to your Applications folder to complete the update.`
|
||||
: `Please extract ${path.basename(updatePath)} and move the Goose app to your Applications folder to complete the update.`,
|
||||
title: 'Update Downloaded',
|
||||
message: 'The update has been downloaded to your Downloads folder.',
|
||||
detail: `Please extract ${path.basename(updatePath)} and move the Goose app to your Applications folder to complete the update.`,
|
||||
buttons: ['Open Downloads', 'Cancel'],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { app } from 'electron';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import * as fs from 'fs/promises';
|
||||
import { createWriteStream } from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as yauzl from 'yauzl';
|
||||
import log from './logger';
|
||||
|
||||
interface GitHubRelease {
|
||||
@@ -191,70 +189,8 @@ export class GitHubUpdater {
|
||||
|
||||
log.info(`GitHubUpdater: Update downloaded to ${downloadPath}`);
|
||||
|
||||
// Auto-unzip the downloaded file using yauzl (secure ZIP library)
|
||||
try {
|
||||
const tempExtractDir = path.join(downloadsDir, `temp-extract-${Date.now()}`);
|
||||
|
||||
// Create temp extraction directory
|
||||
await fs.mkdir(tempExtractDir, { recursive: true });
|
||||
|
||||
log.info(`GitHubUpdater: Extracting ${fileName} to temp directory using yauzl`);
|
||||
|
||||
// Use yauzl to extract the ZIP file securely
|
||||
await extractZipFile(downloadPath, tempExtractDir);
|
||||
|
||||
// Check if Goose.app exists in the extracted content
|
||||
const appPath = path.join(tempExtractDir, 'Goose.app');
|
||||
try {
|
||||
await fs.access(appPath);
|
||||
log.info(`GitHubUpdater: Found Goose.app at ${appPath}`);
|
||||
} catch (error) {
|
||||
log.error('GitHubUpdater: Goose.app not found in extracted content');
|
||||
throw new Error('Goose.app not found in extracted content');
|
||||
}
|
||||
|
||||
// Move Goose.app to Downloads folder
|
||||
const finalAppPath = path.join(downloadsDir, 'Goose.app');
|
||||
|
||||
// Remove existing Goose.app if it exists
|
||||
try {
|
||||
await fs.rm(finalAppPath, { recursive: true, force: true });
|
||||
} catch (e) {
|
||||
// File might not exist, that's fine
|
||||
}
|
||||
|
||||
// Move the app to Downloads
|
||||
log.info(`GitHubUpdater: Moving Goose.app to Downloads folder`);
|
||||
await fs.rename(appPath, finalAppPath);
|
||||
|
||||
// Verify the move was successful
|
||||
try {
|
||||
await fs.access(finalAppPath);
|
||||
log.info(`GitHubUpdater: Successfully moved Goose.app to Downloads`);
|
||||
} catch (error) {
|
||||
log.error('GitHubUpdater: Failed to move Goose.app');
|
||||
throw new Error('Failed to move Goose.app to Downloads');
|
||||
}
|
||||
|
||||
// Clean up temp directory and zip file
|
||||
try {
|
||||
await fs.rm(tempExtractDir, { recursive: true, force: true });
|
||||
await fs.unlink(downloadPath);
|
||||
log.info(`GitHubUpdater: Cleaned up temporary files`);
|
||||
} catch (cleanupError) {
|
||||
log.warn(`GitHubUpdater: Failed to clean up temporary files: ${cleanupError}`);
|
||||
}
|
||||
|
||||
return { success: true, downloadPath: finalAppPath, extractedPath: downloadsDir };
|
||||
} catch (unzipError) {
|
||||
log.error('GitHubUpdater: Error extracting update:', unzipError);
|
||||
// Still return success for download, but note the extraction error
|
||||
return {
|
||||
success: true,
|
||||
downloadPath,
|
||||
error: `Downloaded successfully but extraction failed: ${unzipError instanceof Error ? unzipError.message : 'Unknown error'}`,
|
||||
};
|
||||
}
|
||||
// Return success - user will handle extraction manually
|
||||
return { success: true, downloadPath, extractedPath: downloadsDir };
|
||||
} catch (error) {
|
||||
log.error('GitHubUpdater: Error downloading update:', error);
|
||||
return {
|
||||
@@ -265,108 +201,5 @@ export class GitHubUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Securely extract a ZIP file using yauzl with security checks
|
||||
* @param zipPath Path to the ZIP file
|
||||
* @param extractDir Directory to extract to
|
||||
*/
|
||||
async function extractZipFile(zipPath: string, extractDir: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!zipfile) {
|
||||
reject(new Error('Failed to open ZIP file'));
|
||||
return;
|
||||
}
|
||||
|
||||
zipfile.readEntry();
|
||||
|
||||
zipfile.on('entry', async (entry: yauzl.Entry) => {
|
||||
try {
|
||||
// Security check: prevent directory traversal attacks
|
||||
if (entry.fileName.includes('..') || path.isAbsolute(entry.fileName)) {
|
||||
log.warn(`GitHubUpdater: Skipping potentially dangerous path: ${entry.fileName}`);
|
||||
zipfile.readEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
const fullPath = path.join(extractDir, entry.fileName);
|
||||
|
||||
// Ensure the resolved path is still within the extraction directory
|
||||
const resolvedPath = path.resolve(fullPath);
|
||||
const resolvedExtractDir = path.resolve(extractDir);
|
||||
if (!resolvedPath.startsWith(resolvedExtractDir + path.sep)) {
|
||||
log.warn(`GitHubUpdater: Path traversal attempt detected: ${entry.fileName}`);
|
||||
zipfile.readEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle directories
|
||||
if (entry.fileName.endsWith('/')) {
|
||||
await fs.mkdir(fullPath, { recursive: true });
|
||||
zipfile.readEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle files
|
||||
zipfile.openReadStream(entry, async (err, readStream) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!readStream) {
|
||||
reject(new Error('Failed to open read stream'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure parent directory exists
|
||||
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
||||
|
||||
// Create write stream
|
||||
const writeStream = createWriteStream(fullPath);
|
||||
|
||||
readStream.on('end', () => {
|
||||
writeStream.end();
|
||||
zipfile.readEntry();
|
||||
});
|
||||
|
||||
readStream.on('error', (streamErr) => {
|
||||
writeStream.destroy();
|
||||
reject(streamErr);
|
||||
});
|
||||
|
||||
writeStream.on('error', (writeErr: Error) => {
|
||||
reject(writeErr);
|
||||
});
|
||||
|
||||
// Pipe the data
|
||||
readStream.pipe(writeStream);
|
||||
} catch (fileErr) {
|
||||
reject(fileErr);
|
||||
}
|
||||
});
|
||||
} catch (entryErr) {
|
||||
reject(entryErr);
|
||||
}
|
||||
});
|
||||
|
||||
zipfile.on('end', () => {
|
||||
log.info('GitHubUpdater: ZIP extraction completed successfully');
|
||||
resolve();
|
||||
});
|
||||
|
||||
zipfile.on('error', (zipErr) => {
|
||||
reject(zipErr);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const githubUpdater = new GitHubUpdater();
|
||||
|
||||
Reference in New Issue
Block a user