import fs from 'fs' ; import path from 'path' ; import { app , dialog , shell } from 'electron' ; import { UpdateInfo } from 'electron-updater' ; import log from 'electron-log' ; import fetch from 'cross-fetch' ; import { ProjectInformation } from '@/domain/ProjectInformation' ; import { OperatingSystem } from '@/domain/OperatingSystem' ; import { UpdateProgressBar } from './UpdateProgressBar' ; export function requiresManualUpdate ( ) { return process . platform === 'darwin' ; } export async function handleManualUpdate ( info ) { const result = await askForVisitingWebsiteForManualUpdate ( ) ; if ( result === ManualDownloadDialogResult . NoAction ) { return ; } const project = new ProjectInformation ( process . env . VUE_APP_NAME , info . version , process . env . VUE_APP_REPOSITORY_URL , process . env . VUE_APP_HOMEPAGE_URL ) ; if ( result === ManualDownloadDialogResult . VisitReleasesPage ) { await shell . openExternal ( project . releaseUrl ) ; } else if ( result === ManualDownloadDialogResult . UpdateNow ) { await download ( info , project ) ; } } enum ManualDownloadDialogResult { NoAction = 0 , UpdateNow = 1 , VisitReleasesPage = 2 } async function askForVisitingWebsiteForManualUpdate ( ) { const visitPageResult = await dialog . showMessageBox ( { type : 'info' , buttons : [ 'Not now' , 'Download and manually update' , 'Visit releases page' , ] , message : 'Update available\n\nWould you like to update manually?' , detail : 'There are new updates available.' + ' privacy.sexy does not support fully auto-update for macOS due to code signing costs.' + ' Please manually update your version, because newer versions fix issues and improve privacy and security.' , defaultId : ManualDownloadDialogResult . UpdateNow , cancelId : ManualDownloadDialogResult . NoAction , } ) ; return visitPageResult . response ; } async function download ( info , project ) { log . info ( 'Downloading update manually' ) ; const progressBar = new UpdateProgressBar ( ) ; progressBar . showIndeterminateState ( ) ; try { const filePath = `${ path . dirname ( app . getPath ( 'temp' ) ) }/privacy.sexy/${ info . version }-installer.dmg` ; const parentFolder = path . dirname ( filePath ) ; if ( fs . existsSync ( filePath ) ) { log . info ( 'Update is already downloaded' ) ; await fs . promises . unlink ( filePath ) ; log . info ( `Deleted ${ filePath }` ) ; } else { await fs . promises . mkdir ( parentFolder , { recursive : true } ) ; } const dmgFileUrl = project . getDownloadUrl ( OperatingSystem . macOS ) ; await downloadFileWithProgress ( dmgFileUrl , filePath , ( percentage ) => { progressBar . showPercentage ( percentage ) ; } ) ; await shell . openPath ( filePath ) ; progressBar . close ( ) ; app . quit ( ) ; } catch ( e ) { progressBar . showError ( e ) ; } } type ProgressCallback = ( progress : number ) => void ; async function downloadFileWithProgress ( url , filePath , progressHandler ) { log . info ( `Fetching ${ url }` ) ; const response = await fetch ( url ) ; if ( ! response . ok ) { throw Error ( `Unable to download, server returned ${ response . status } ${ response . statusText }` ) ; } const contentLength = + response . headers . get ( 'content-length' ) ; const writer = fs . createWriteStream ( filePath ) ; log . info ( `Writing to ${ filePath }, content length: ${ contentLength }` ) ; if ( ! contentLength ) { log . error ( 'Unknown content-length' , Array . from ( response . headers . entries ( ) ) ) ; progressHandler = ( ) => { } ; } const reader = getReader ( response ) ; if ( ! reader ) { throw new Error ( 'No response body' ) ; } await streamWithProgress ( contentLength , reader , writer , progressHandler ) ; } async function streamWithProgress ( totalLength , readStream , writeStream , progressHandler ) { let receivedLength = 0 ; for await ( const chunk of readStream ) { if ( ! chunk ) { throw Error ( 'Empty chunk received during download' ) ; } writeStream . write ( Buffer . from ( chunk ) ) ; receivedLength += chunk . length ; const percentage = Math . floor ( ( receivedLength / totalLength ) * 100 ) ; progressHandler ( percentage ) ; log . debug ( `Received ${ receivedLength } of ${ totalLength }` ) ; } log . info ( 'Downloaded successfully' ) ; } function getReader ( response ) { return response . body as unknown as NodeJS . ReadableStream ; }