Fix file retention after updates on macOS #417

This fixes issue #417 where autoupdate installer files were not deleted
on macOS, leading to accumulation of old installers.

Key changes:

- Store update files in application-specific directory
- Clear update files directory on every app launch

Other supporting changes:

- Refactor file system operations to be more testable and reusable
- Improve separation of concerns in directory management
- Enhance dependency injection for auto-update logic
- Fix async completion to support `await` operations
- Add additional logging and revise some log messages during updates
This commit is contained in:
undergroundwires
2024-10-07 17:33:47 +02:00
parent 4e06d543b3
commit 2f31bc7b06
44 changed files with 1484 additions and 590 deletions

View File

@@ -7,63 +7,100 @@ import type { ProgressInfo } from 'electron-builder';
export async function handleAutoUpdate() {
const autoUpdater = getAutoUpdater();
if (await askDownloadAndInstall() === DownloadDialogResult.NotNow) {
if (await askDownloadAndInstall() === UpdateDialogResult.Postpone) {
ElectronLogger.info('User chose to postpone update');
return;
}
startHandlingUpdateProgress(autoUpdater);
await autoUpdater.downloadUpdate();
}
function startHandlingUpdateProgress(autoUpdater: AppUpdater) {
const progressBar = new UpdateProgressBar();
progressBar.showIndeterminateState();
autoUpdater.on('error', (e) => {
progressBar.showError(e);
});
autoUpdater.on('download-progress', (progress: ProgressInfo) => {
/*
On macOS, download-progress event is not called.
So the indeterminate progress will continue until download is finished.
*/
ElectronLogger.debug('@download-progress@\n', progress);
if (progressBar.isOpen) { // May be closed by the user
progressBar.showProgress(progress);
}
});
autoUpdater.on('update-downloaded', async (info: UpdateInfo) => {
ElectronLogger.info('@update-downloaded@\n', info);
progressBar.closeIfOpen();
await handleUpdateDownloaded(autoUpdater);
});
}
async function handleUpdateDownloaded(autoUpdater: AppUpdater) {
if (await askRestartAndInstall() === InstallDialogResult.NotNow) {
return;
ElectronLogger.info('User chose to download and install update');
try {
await startHandlingUpdateProgress(autoUpdater);
} catch (error) {
ElectronLogger.error('Failed to handle auto-update process', { error });
}
setTimeout(() => autoUpdater.quitAndInstall(), 1);
}
enum DownloadDialogResult {
Install = 0,
NotNow = 1,
function startHandlingUpdateProgress(autoUpdater: AppUpdater): Promise<void> {
return new Promise((resolve, reject) => { // Block until update process completes
const progressBar = new UpdateProgressBar();
progressBar.showIndeterminateState();
autoUpdater.on('error', (e) => {
progressBar.showError(e);
reject(e);
});
autoUpdater.on('download-progress', (progress: ProgressInfo) => {
/*
On macOS, download-progress event is not called.
So the indeterminate progress will continue until download is finished.
*/
ElectronLogger.debug('Update download progress', { progress });
if (progressBar.isOpen) { // May be closed by the user
progressBar.showProgress(progress);
}
});
autoUpdater.on('update-downloaded', async (info: UpdateInfo) => {
ElectronLogger.info('Update downloaded successfully', { version: info.version });
progressBar.closeIfOpen();
try {
await handleUpdateDownloaded(autoUpdater);
} catch (error) {
ElectronLogger.error('Failed to handle downloaded update', { error });
reject(error);
}
resolve();
});
autoUpdater.downloadUpdate();
});
}
async function askDownloadAndInstall(): Promise<DownloadDialogResult> {
async function handleUpdateDownloaded(
autoUpdater: AppUpdater,
): Promise<void> {
return new Promise((resolve, reject) => { // Block until update download process completes
askRestartAndInstall()
.then((result) => {
if (result === InstallDialogResult.InstallAndRestart) {
ElectronLogger.info('User chose to install and restart for update');
setTimeout(() => {
try {
autoUpdater.quitAndInstall();
resolve();
} catch (error) {
ElectronLogger.error('Failed to quit and install update', { error });
reject(error);
}
}, 1);
} else {
ElectronLogger.info('User chose to postpone update installation');
resolve();
}
})
.catch((error) => {
ElectronLogger.error('Failed to prompt user for restart and install', { error });
reject(error);
});
});
}
enum UpdateDialogResult {
Update = 0,
Postpone = 1,
}
async function askDownloadAndInstall(): Promise<UpdateDialogResult> {
const updateDialogResult = await dialog.showMessageBox({
type: 'question',
buttons: ['Install', 'Not now'],
title: 'Confirm Update',
message: 'Update available.\n\nWould you like to download and install new version?',
detail: 'Application will automatically restart to apply update after download',
defaultId: DownloadDialogResult.Install,
cancelId: DownloadDialogResult.NotNow,
defaultId: UpdateDialogResult.Update,
cancelId: UpdateDialogResult.Postpone,
});
return updateDialogResult.response;
}
enum InstallDialogResult {
InstallAndRestart = 0,
NotNow = 1,
Postpone = 1,
}
async function askRestartAndInstall(): Promise<InstallDialogResult> {
const installDialogResult = await dialog.showMessageBox({
@@ -72,7 +109,7 @@ async function askRestartAndInstall(): Promise<InstallDialogResult> {
message: `A new version of ${app.name} has been downloaded.`,
detail: 'It will be installed the next time you restart the application.',
defaultId: InstallDialogResult.InstallAndRestart,
cancelId: InstallDialogResult.NotNow,
cancelId: InstallDialogResult.Postpone,
});
return installDialogResult.response;
}

View File

@@ -1,10 +1,8 @@
import { existsSync, createWriteStream, type WriteStream } from 'node:fs';
import { unlink, mkdir } from 'node:fs/promises';
import path from 'node:path';
import { app } from 'electron/main';
import { createWriteStream, type WriteStream } from 'node:fs';
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
import type { Logger } from '@/application/Common/Log/Logger';
import { UpdateProgressBar } from '../ProgressBar/UpdateProgressBar';
import { retryFileSystemAccess } from './RetryFileSystemAccess';
import { provideUpdateInstallationFilepath, type InstallationFilepathProvider } from './InstallationFiles/InstallationFilepathProvider';
import type { UpdateInfo } from 'electron-updater';
import type { ReadableStream } from 'node:stream/web';
@@ -18,18 +16,25 @@ export type DownloadUpdateResult = {
readonly installerPath: string;
};
interface UpdateDownloadUtilities {
readonly logger: Logger;
readonly provideInstallationFilePath: InstallationFilepathProvider;
}
export async function downloadUpdate(
info: UpdateInfo,
remoteFileUrl: string,
progressBar: UpdateProgressBar,
utilities: UpdateDownloadUtilities = DefaultUtilities,
): Promise<DownloadUpdateResult> {
ElectronLogger.info('Starting manual update download.');
utilities.logger.info('Starting manual update download.');
progressBar.showIndeterminateState();
try {
const { filePath } = await downloadInstallerFile(
info.version,
remoteFileUrl,
(percentage) => { progressBar.showPercentage(percentage); },
utilities,
);
return {
success: true,
@@ -47,58 +52,40 @@ async function downloadInstallerFile(
version: string,
remoteFileUrl: string,
progressHandler: ProgressCallback,
utilities: UpdateDownloadUtilities,
): Promise<{ readonly filePath: string; }> {
const filePath = `${path.dirname(app.getPath('temp'))}/privacy.sexy/${version}-installer.dmg`;
if (!await ensureFilePathReady(filePath)) {
throw new Error(`Failed to prepare the file path for the installer: ${filePath}`);
}
const filePath = await utilities.provideInstallationFilePath(version);
await downloadFileWithProgress(
remoteFileUrl,
filePath,
progressHandler,
utilities,
);
return { filePath };
}
async function ensureFilePathReady(filePath: string): Promise<boolean> {
return retryFileSystemAccess(async () => {
try {
const parentFolder = path.dirname(filePath);
if (existsSync(filePath)) {
ElectronLogger.info(`Existing update file found and will be replaced: ${filePath}`);
await unlink(filePath);
} else {
await mkdir(parentFolder, { recursive: true });
}
return true;
} catch (error) {
ElectronLogger.error(`Failed to prepare file path for update: ${filePath}`, error);
return false;
}
});
}
type ProgressCallback = (progress: number) => void;
async function downloadFileWithProgress(
url: string,
filePath: string,
progressHandler: ProgressCallback,
utilities: UpdateDownloadUtilities,
) {
// autoUpdater cannot handle DMG files, requiring manual download management for these file types.
ElectronLogger.info(`Retrieving update from ${url}.`);
utilities.logger.info(`Retrieving update from ${url}.`);
const response = await fetch(url);
if (!response.ok) {
throw Error(`Download failed: Server responded with ${response.status} ${response.statusText}.`);
}
const contentLength = getContentLengthFromResponse(response);
const contentLength = getContentLengthFromResponse(response, utilities);
await withWriteStream(filePath, async (writer) => {
ElectronLogger.info(contentLength.isValid
? `Saving file to ${filePath} (Size: ${contentLength.totalLength} bytes).`
: `Saving file to ${filePath}.`);
utilities.logger.info(contentLength.isValid
? `Saving file to '${filePath}' (Size: ${contentLength.totalLength} bytes).`
: `Saving file to '${filePath}'.`);
await withReadableStream(response, async (reader) => {
await streamWithProgress(contentLength, reader, writer, progressHandler);
await streamWithProgress(contentLength, reader, writer, progressHandler, utilities);
});
ElectronLogger.info(`Successfully saved the file: '${filePath}'`);
});
}
@@ -109,16 +96,19 @@ type ResponseContentLength = {
readonly isValid: false;
};
function getContentLengthFromResponse(response: Response): ResponseContentLength {
function getContentLengthFromResponse(
response: Response,
utilities: UpdateDownloadUtilities,
): ResponseContentLength {
const contentLengthString = response.headers.get('content-length');
const headersInfo = Array.from(response.headers.entries());
if (!contentLengthString) {
ElectronLogger.warn('Missing \'Content-Length\' header in the response.', headersInfo);
utilities.logger.warn('Missing \'Content-Length\' header in the response.', headersInfo);
return { isValid: false };
}
const contentLength = Number(contentLengthString);
if (Number.isNaN(contentLength) || contentLength <= 0) {
ElectronLogger.error('Unable to determine download size from server response.', headersInfo);
utilities.logger.error('Unable to determine download size from server response.', headersInfo);
return { isValid: false };
}
return { totalLength: contentLength, isValid: true };
@@ -153,6 +143,7 @@ async function streamWithProgress(
readStream: ReadableStream,
writeStream: WriteStream,
progressHandler: ProgressCallback,
utilities: UpdateDownloadUtilities,
): Promise<void> {
let receivedLength = 0;
let logThreshold = 0;
@@ -163,22 +154,23 @@ async function streamWithProgress(
writeStream.write(Buffer.from(chunk));
receivedLength += chunk.length;
notifyProgress(contentLength, receivedLength, progressHandler);
const progressLog = logProgress(receivedLength, contentLength, logThreshold);
const progressLog = logProgress(receivedLength, contentLength, logThreshold, utilities);
logThreshold = progressLog.nextLogThreshold;
}
ElectronLogger.info('Update download completed successfully.');
utilities.logger.info('Update download completed successfully.');
}
function logProgress(
receivedLength: number,
contentLength: ResponseContentLength,
logThreshold: number,
utilities: UpdateDownloadUtilities,
): { readonly nextLogThreshold: number; } {
const {
shouldLog, nextLogThreshold,
} = shouldLogProgress(receivedLength, contentLength, logThreshold);
if (shouldLog) {
ElectronLogger.debug(`Download progress: ${receivedLength} bytes received.`);
utilities.logger.debug(`Download progress: ${receivedLength} bytes received.`);
}
return { nextLogThreshold };
}
@@ -220,3 +212,8 @@ function createReader(response: Response): ReadableStream {
// https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/65542#discussioncomment-6071004
return response.body as ReadableStream;
}
const DefaultUtilities: UpdateDownloadUtilities = {
logger: ElectronLogger,
provideInstallationFilePath: provideUpdateInstallationFilepath,
};

View File

@@ -1,15 +1,21 @@
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
export function retryFileSystemAccess(
fileOperation: () => Promise<boolean>,
): Promise<boolean> {
export interface FileSystemAccessorWithRetry {
(
fileOperation: () => Promise<boolean>,
): Promise<boolean>;
}
export const retryFileSystemAccess: FileSystemAccessorWithRetry = (
fileOperation,
) => {
return retryWithExponentialBackoff(
fileOperation,
TOTAL_RETRIES,
INITIAL_DELAY_MS,
);
}
};
// These values provide a balanced approach for handling transient file system
// issues without excessive waiting.

View File

@@ -0,0 +1,107 @@
import type { Logger } from '@/application/Common/Log/Logger';
import type { FileSystemOperations } from '@/infrastructure/FileSystem/FileSystemOperations';
import type { ApplicationDirectoryProvider } from '@/infrastructure/FileSystem/Directory/ApplicationDirectoryProvider';
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
import { PersistentApplicationDirectoryProvider } from '@/infrastructure/FileSystem/Directory/PersistentApplicationDirectoryProvider';
import { NodeElectronFileSystemOperations } from '@/infrastructure/FileSystem/NodeElectronFileSystemOperations';
export interface InstallationFileCleaner {
(
utilities?: UpdateFileUtilities,
): Promise<void>;
}
interface UpdateFileUtilities {
readonly logger: Logger;
readonly directoryProvider: ApplicationDirectoryProvider;
readonly fileSystem: FileSystemOperations;
}
export const clearUpdateInstallationFiles: InstallationFileCleaner = async (
utilities = DefaultUtilities,
) => {
utilities.logger.info('Starting update installation files cleanup');
const { success, error, directoryAbsolutePath } = await utilities.directoryProvider.provideDirectory('update-installation-files');
if (!success) {
utilities.logger.error('Failed to locate installation files directory', { error });
throw new Error('Cannot locate the installation files directory path');
}
const installationFileNames = await readDirectoryContents(directoryAbsolutePath, utilities);
if (installationFileNames.length === 0) {
utilities.logger.info('No update installation files found');
return;
}
utilities.logger.debug(`Found ${installationFileNames.length} installation files to delete`);
utilities.logger.info('Deleting installation files');
const errors = await executeIndependentTasksAndCollectErrors(
installationFileNames.map(async (fileOrFolderName) => {
await deleteItemFromDirectory(directoryAbsolutePath, fileOrFolderName, utilities);
}),
);
if (errors.length > 0) {
utilities.logger.error('Failed to delete some installation files', { errors });
throw new Error(`Failed to delete some items:\n${errors.join('\n')}`);
}
utilities.logger.info('Update installation files cleanup completed successfully');
};
async function deleteItemFromDirectory(
directoryPath: string,
fileOrFolderName: string,
utilities: UpdateFileUtilities,
): Promise<void> {
const itemPath = utilities.fileSystem.combinePaths(
directoryPath,
fileOrFolderName,
);
try {
utilities.logger.debug(`Deleting installation artifact: ${itemPath}`);
await utilities.fileSystem.deletePath(itemPath);
utilities.logger.debug(`Successfully deleted installation artifact: ${itemPath}`);
} catch (error) {
utilities.logger.error(`Failed to delete installation artifact: ${itemPath}`, { error });
throw error;
}
}
async function readDirectoryContents(
directoryPath: string,
utilities: UpdateFileUtilities,
): Promise<string[]> {
try {
utilities.logger.debug(`Reading directory contents: ${directoryPath}`);
const items = await utilities.fileSystem.listDirectoryContents(directoryPath);
utilities.logger.debug(`Read ${items.length} items from directory: ${directoryPath}`);
return items;
} catch (error) {
utilities.logger.error('Failed to read directory contents', { directoryPath, error });
throw new Error('Failed to read directory contents', { cause: error });
}
}
async function executeIndependentTasksAndCollectErrors(
tasks: (Promise<void>)[],
): Promise<string[]> {
const results = await Promise.allSettled(tasks);
const errors = results
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
.map((result) => result.reason);
return errors.map((error) => {
if (!error) {
return 'unknown error';
}
if (error instanceof Error) {
return error.message;
}
if (typeof error === 'string') {
return error;
}
return String(error);
});
}
const DefaultUtilities: UpdateFileUtilities = {
logger: ElectronLogger,
directoryProvider: new PersistentApplicationDirectoryProvider(),
fileSystem: NodeElectronFileSystemOperations,
};

View File

@@ -0,0 +1,73 @@
import type { Logger } from '@/application/Common/Log/Logger';
import type { ApplicationDirectoryProvider } from '@/infrastructure/FileSystem/Directory/ApplicationDirectoryProvider';
import { PersistentApplicationDirectoryProvider } from '@/infrastructure/FileSystem/Directory/PersistentApplicationDirectoryProvider';
import type { FileSystemOperations } from '@/infrastructure/FileSystem/FileSystemOperations';
import { NodeElectronFileSystemOperations } from '@/infrastructure/FileSystem/NodeElectronFileSystemOperations';
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
import { retryFileSystemAccess, type FileSystemAccessorWithRetry } from '../FileSystemAccessorWithRetry';
export interface InstallationFilepathProvider {
(
version: string,
utilities?: InstallationFilepathProviderUtilities,
): Promise<string>;
}
interface InstallationFilepathProviderUtilities {
readonly logger: Logger;
readonly directoryProvider: ApplicationDirectoryProvider;
readonly fileSystem: FileSystemOperations;
readonly accessFileSystemWithRetry: FileSystemAccessorWithRetry;
}
export const InstallerFileSuffix = '-installer.dmg';
export const provideUpdateInstallationFilepath: InstallationFilepathProvider = async (
version,
utilities = DefaultUtilities,
) => {
const {
success, error, directoryAbsolutePath,
} = await utilities.directoryProvider.provideDirectory('update-installation-files');
if (!success) {
utilities.logger.error('Error when providing download directory', error);
throw new Error('Failed to provide download directory.');
}
const filepath = utilities.fileSystem.combinePaths(directoryAbsolutePath, `${version}${InstallerFileSuffix}`);
if (!await makeFilepathAvailable(filepath, utilities)) {
throw new Error(`Failed to prepare the file path for the installer: ${filepath}`);
}
return filepath;
};
async function makeFilepathAvailable(
filePath: string,
utilities: InstallationFilepathProviderUtilities,
): Promise<boolean> {
let isFileAvailable = false;
try {
isFileAvailable = await utilities.fileSystem.isFileAvailable(filePath);
} catch (error) {
throw new Error('File availability check failed');
}
if (!isFileAvailable) {
return true;
}
return utilities.accessFileSystemWithRetry(async () => {
try {
utilities.logger.info(`Existing update file found and will be replaced: ${filePath}`);
await utilities.fileSystem.deletePath(filePath);
return true;
} catch (error) {
utilities.logger.error(`Failed to prepare file path for update: ${filePath}`, error);
return false;
}
});
}
const DefaultUtilities: InstallationFilepathProviderUtilities = {
logger: ElectronLogger,
directoryProvider: new PersistentApplicationDirectoryProvider(),
fileSystem: NodeElectronFileSystemOperations,
accessFileSystemWithRetry: retryFileSystemAccess,
};

View File

@@ -1,7 +1,7 @@
import { app } from 'electron/main';
import { shell } from 'electron/common';
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
import { retryFileSystemAccess } from './RetryFileSystemAccess';
import { retryFileSystemAccess } from './FileSystemAccessorWithRetry';
export async function startInstallation(filePath: string): Promise<boolean> {
return retryFileSystemAccess(async () => {

View File

@@ -1,7 +1,7 @@
import { createHash } from 'node:crypto';
import { createReadStream } from 'node:fs';
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
import { retryFileSystemAccess } from './RetryFileSystemAccess';
import { retryFileSystemAccess } from './FileSystemAccessorWithRetry';
export async function checkIntegrity(
filePath: string,

View File

@@ -14,29 +14,43 @@ import {
import { type DownloadUpdateResult, downloadUpdate } from './Downloader';
import { checkIntegrity } from './Integrity';
import { startInstallation } from './Installer';
import { clearUpdateInstallationFiles } from './InstallationFiles/InstallationFileCleaner';
import type { UpdateInfo } from 'electron-updater';
export function requiresManualUpdate(): boolean {
return process.platform === 'darwin';
export function requiresManualUpdate(
nodePlatform: string = process.platform,
): boolean {
// autoUpdater cannot handle DMG files, requiring manual download management for these file types.
return nodePlatform === 'darwin';
}
export async function startManualUpdateProcess(info: UpdateInfo) {
try {
await clearUpdateInstallationFiles();
} catch (error) {
ElectronLogger.warn('Failed to clear previous update installation files', { error });
} finally {
await executeManualUpdateProcess(info);
}
}
async function executeManualUpdateProcess(info: UpdateInfo): Promise<void> {
try {
const updateAction = await promptForManualUpdate();
if (updateAction === ManualUpdateChoice.NoAction) {
ElectronLogger.info('User cancelled the update.');
ElectronLogger.info('User chose to cancel the update');
return;
}
const { releaseUrl, downloadUrl } = getRemoteUpdateUrls(info.version);
if (updateAction === ManualUpdateChoice.VisitReleasesPage) {
ElectronLogger.info(`Navigating to release page: ${releaseUrl}`);
ElectronLogger.info('User chose to visit release page', { url: releaseUrl });
await shell.openExternal(releaseUrl);
} else if (updateAction === ManualUpdateChoice.UpdateNow) {
ElectronLogger.info('Initiating update download and installation.');
ElectronLogger.info('User chose to download and install update');
await downloadAndInstallUpdate(downloadUrl, info);
}
} catch (err) {
ElectronLogger.error('Unexpected error during updates', err);
ElectronLogger.error('Failed to execute auto-update process', { error: err });
await handleUnexpectedError(info);
}
}
@@ -56,9 +70,10 @@ async function downloadAndInstallUpdate(fileUrl: string, info: UpdateInfo) {
}
const userAction = await promptIntegrityCheckFailure();
if (userAction === IntegrityCheckChoice.RetryDownload) {
ElectronLogger.info('User chose to retry download after integrity check failure');
await startManualUpdateProcess(info);
} else if (userAction === IntegrityCheckChoice.ContinueAnyway) {
ElectronLogger.warn('Proceeding to install with failed integrity check.');
ElectronLogger.warn('User chose to proceed with installation despite failed integrity check');
await openInstaller(download.installerPath, info);
}
}
@@ -66,9 +81,9 @@ async function downloadAndInstallUpdate(fileUrl: string, info: UpdateInfo) {
async function handleFailedDownload(info: UpdateInfo) {
const userAction = await promptDownloadError();
if (userAction === DownloadErrorChoice.Cancel) {
ElectronLogger.info('Update download canceled.');
ElectronLogger.info('User chose to cancel update download');
} else if (userAction === DownloadErrorChoice.RetryDownload) {
ElectronLogger.info('Retrying update download.');
ElectronLogger.info('User chose to retry update download');
await startManualUpdateProcess(info);
}
}
@@ -76,9 +91,9 @@ async function handleFailedDownload(info: UpdateInfo) {
async function handleUnexpectedError(info: UpdateInfo) {
const userAction = await showUnexpectedError();
if (userAction === UnexpectedErrorChoice.Cancel) {
ElectronLogger.info('Unexpected error handling canceled.');
ElectronLogger.info('User chose to cancel update process after unexpected error');
} else if (userAction === UnexpectedErrorChoice.RetryUpdate) {
ElectronLogger.info('Retrying the update process.');
ElectronLogger.info('User chose to retry update process after unexpected error');
await startManualUpdateProcess(info);
}
}
@@ -89,8 +104,10 @@ async function openInstaller(installerPath: string, info: UpdateInfo) {
}
const userAction = await promptInstallerOpenError();
if (userAction === InstallerErrorChoice.RetryDownload) {
ElectronLogger.info('User chose to retry download after installer open error');
await startManualUpdateProcess(info);
} else if (userAction === InstallerErrorChoice.RetryOpen) {
ElectronLogger.info('User chose to retry opening installer');
await openInstaller(installerPath, info);
}
}
@@ -119,16 +136,16 @@ async function isIntegrityPreserved(
function getRemoteSha512Hash(info: UpdateInfo, fileUrl: string): string | undefined {
const fileInfos = info.files.filter((file) => fileUrl.includes(file.url));
if (!fileInfos.length) {
ElectronLogger.error(`Remote hash not found for the URL: ${fileUrl}`, info.files);
ElectronLogger.error('Failed to find remote hash for download URL', { url: fileUrl, files: info.files });
if (info.files.length > 0) {
const firstHash = info.files[0].sha512;
ElectronLogger.info(`Selecting the first available hash: ${firstHash}`);
ElectronLogger.info('Using first available hash due to missing match', { hash: firstHash });
return firstHash;
}
return undefined;
}
if (fileInfos.length > 1) {
ElectronLogger.error(`Found multiple file entries for the URL: ${fileUrl}`, fileInfos);
ElectronLogger.warn('Multiple file entries found for download URL', { url: fileUrl, entries: fileInfos });
}
return fileInfos[0].sha512;
}