From abaf1b1065153185a73db9c00be82e51c14f7536 Mon Sep 17 00:00:00 2001 From: Nat3z <66748576+Nat3z@users.noreply.github.com> Date: Sun, 1 Mar 2026 07:25:54 -0800 Subject: [PATCH 1/9] Add blockmap generation to release workflow --- .github/workflows/build-release.yml | 23 ++ updater/src/main.js | 505 ++++++++++++++-------------- 2 files changed, 269 insertions(+), 259 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 4291c13d..41d415b1 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -69,6 +69,13 @@ jobs: run: | Compress-Archive -Path application/dist/win-unpacked/* -Destination application/dist/OpenGameInstaller-Portable.zip + - name: Generate Windows blockmaps + if: matrix.os == 'windows-latest' && steps.check_changes.outputs.has_application_changes == 'true' + shell: bash + run: | + bunx --bun app-builder-bin blockmap --input "application/dist/OpenGameInstaller-Portable.zip" --output "application/dist/OpenGameInstaller-Portable.zip.blockmap" + bunx --bun app-builder-bin blockmap --input "updater/dist/OpenGameInstaller-Setup.exe" --output "updater/dist/OpenGameInstaller-Setup.exe.blockmap" + - name: Upload Windows Assets as Artifacts if: matrix.os == 'windows-latest' && steps.check_changes.outputs.has_application_changes == 'true' uses: actions/upload-artifact@v4 @@ -76,7 +83,16 @@ jobs: name: windows-assets path: | application/dist/OpenGameInstaller-Portable.zip + application/dist/OpenGameInstaller-Portable.zip.blockmap updater/dist/OpenGameInstaller-Setup.exe + updater/dist/OpenGameInstaller-Setup.exe.blockmap + + - name: Generate Linux blockmaps + if: matrix.os == 'ubuntu-latest' && steps.check_changes.outputs.has_application_changes == 'true' + shell: bash + run: | + bunx --bun app-builder-bin blockmap --input "application/dist/OpenGameInstaller-linux-pt.AppImage" --output "application/dist/OpenGameInstaller-linux-pt.AppImage.blockmap" + bunx --bun app-builder-bin blockmap --input "updater/dist/OpenGameInstaller-Setup.AppImage" --output "updater/dist/OpenGameInstaller-Setup.AppImage.blockmap" - name: Upload Linux Assets as Artifacts if: matrix.os == 'ubuntu-latest' && steps.check_changes.outputs.has_application_changes == 'true' @@ -85,7 +101,9 @@ jobs: name: linux-assets path: | updater/dist/OpenGameInstaller-Setup.AppImage + updater/dist/OpenGameInstaller-Setup.AppImage.blockmap application/dist/OpenGameInstaller-linux-pt.AppImage + application/dist/OpenGameInstaller-linux-pt.AppImage.blockmap release: permissions: contents: write @@ -169,6 +187,7 @@ jobs: run: | mkdir -p assets-flat find assets -type f -name "*.zip" -exec cp {} assets-flat/ \; + find assets -type f -name "*.blockmap" -exec cp {} assets-flat/ \; find assets -type f -name "*.exe" -exec cp {} assets-flat/ \; find assets -type f -name "*.AppImage" -exec cp {} assets-flat/ \; ls -la assets-flat/ @@ -197,6 +216,10 @@ jobs: prerelease: true files: | assets-flat/OpenGameInstaller-Portable.zip + assets-flat/OpenGameInstaller-Portable.zip.blockmap assets-flat/OpenGameInstaller-Setup.exe + assets-flat/OpenGameInstaller-Setup.exe.blockmap assets-flat/OpenGameInstaller-Setup.AppImage + assets-flat/OpenGameInstaller-Setup.AppImage.blockmap assets-flat/OpenGameInstaller-linux-pt.AppImage + assets-flat/OpenGameInstaller-linux-pt.AppImage.blockmap diff --git a/updater/src/main.js b/updater/src/main.js index 575294a2..9c501b80 100644 --- a/updater/src/main.js +++ b/updater/src/main.js @@ -3,6 +3,7 @@ import axios from 'axios'; import fs from 'fs'; import path, { join } from 'path'; import yauzl from 'yauzl'; +import zlib from 'zlib'; import { spawn, exec } from 'child_process'; let mainWindow; import pjson from '../package.json' with { type: 'json' }; @@ -124,273 +125,52 @@ async function createWindow() { { timeout: 10000 } // 10 second timeout for update check ); mainWindow.webContents.send('text', 'Checking for Updates'); - // if the version is different, download the new version - let release; - for (const rel of response.data) { - console.log(rel.tag_name, localVersion); - if (rel.tag_name === localVersion) { - break; - } - if ( - rel.prerelease && - usingBleedingEdge && - rel.tag_name !== localVersion - ) { - release = rel; - break; - } else if (!rel.prerelease && rel.tag_name !== localVersion) { - release = rel; - break; - } - } - let updating = release !== undefined; - if (release) { - // check if a local cache of the update exists in temp - const localCache = path.join( - app.getPath('temp'), - 'ogi-' + release.tag_name.replace('v', '') + '-cache' - ); - if (fs.existsSync(localCache)) { - const files = fs.readdirSync(localCache); - - // Check if the expected executable exists in the cache - const expectedExecutable = - process.platform === 'win32' - ? 'OpenGameInstaller.exe' - : 'OpenGameInstaller.AppImage'; - const executablePath = path.join(localCache, expectedExecutable); - - if (!fs.existsSync(executablePath)) { - console.log('Executable not found in cache, redownloading...'); - // Remove the incomplete cache and proceed with download - fs.rmSync(localCache, { recursive: true, force: true }); - } else { - mainWindow.webContents.send('text', 'Copying Cached Version...'); - for (const file of files) { - const sourcePath = path.join(localCache, file); - const destPath = path.join(__dirname, 'update', file); - - // On Windows, if the destination file exists and is locked, try to rename it first - if (process.platform === 'win32' && fs.existsSync(destPath)) { - try { - const backupPath = destPath + '.backup'; - if (fs.existsSync(backupPath)) { - fs.unlinkSync(backupPath); - } - fs.renameSync(destPath, backupPath); - } catch (renameErr) { - console.log( - 'Could not backup existing file during cache copy:', - renameErr.message - ); - // Continue anyway, cpSync might still work - } - } - - fs.cpSync(sourcePath, destPath, { force: true, recursive: true }); - } - // update the version file - fs.writeFileSync(`./version.txt`, release.tag_name); - if (process.platform === 'linux') { - fs.chmodSync(`./update/OpenGameInstaller.AppImage`, '755'); - } - - mainWindow.webContents.send('text', 'Launching OpenGameInstaller'); - launchApp(true); - return; - } - } - - // download the new version usinng axios stream - if (process.platform === 'win32') { - const writer = fs.createWriteStream(`./update.zip`); - mainWindow.webContents.send('text', 'Downloading Update'); - const assetWithPortable = release.assets.find( - (asset) => - asset.name.toLowerCase().includes('portable') || - asset.name.toLowerCase().includes('portrable') - ); - if (!assetWithPortable) { - mainWindow.webContents.send('text', 'No Portable Version Found'); - setTimeout(() => { - launchApp(net.isOnline()); - }, 2000); - return; - } - const response = await axios({ - url: assetWithPortable.browser_download_url, - method: 'GET', - responseType: 'stream', - }); - response.data.pipe(writer); - const startTime = Date.now(); - const fileSize = response.headers['content-length']; - response.data.on('data', () => { - const elapsedTime = (Date.now() - startTime) / 1000; // in seconds - const downloadSpeed = response.data.socket.bytesRead / elapsedTime; + const releases = response.data.filter((rel) => + usingBleedingEdge ? rel.prerelease : !rel.prerelease + ); + const localIndex = releases.findIndex((rel) => rel.tag_name === localVersion); + const targetRelease = releases[0]; + let updating = Boolean(targetRelease) && localIndex !== 0; + if (targetRelease && updating) { + const releasePath = + localIndex > 0 ? releases.slice(0, localIndex).reverse() : [targetRelease]; + const gap = localIndex > 0 ? releasePath.length : Number.POSITIVE_INFINITY; + let updateApplied = false; + + if (Number.isFinite(gap) && gap > 0 && gap <= 3) { + mainWindow.webContents.send('text', 'Preparing incremental update path'); + try { + await applyBlockmapPath(releasePath); + updateApplied = true; + } catch (patchErr) { + console.error('Incremental patching failed, falling back:', patchErr); mainWindow.webContents.send( 'text', - 'Downloading Update', - writer.bytesWritten, - fileSize, - correctParsingSize(downloadSpeed) + '/s' + 'Falling back to full download', + patchErr.message ); - }); - response.data.on('end', async () => { - mainWindow.webContents.send('text', 'Download Complete'); - // extract the zip file - const prefix = __dirname + '/update'; - if (!fs.existsSync(prefix)) { - fs.mkdirSync(prefix, { recursive: true }); - } - - try { - await new Promise(async (resolve, reject) => { - mainWindow.webContents.send('text', 'Extracting Update'); - // unzip files to the cache folder - if (!fs.existsSync(localCache)) { - fs.mkdirSync(localCache, { recursive: true }); - } - console.log('Unzipping to', localCache); - - try { - await unzip(`./update.zip`, localCache); - - // Wait a bit to ensure all files are fully written - await new Promise((resolve) => setTimeout(resolve, 1000)); - - mainWindow.webContents.send('text', 'Copying Update Files'); - - // copy the files to the update folder with better error handling - const files = fs.readdirSync(localCache); - for (const file of files) { - const sourcePath = path.join(localCache, file); - const destPath = path.join(prefix, file); - - // On Windows, if the destination file exists and is locked, try to rename it first - if (process.platform === 'win32' && fs.existsSync(destPath)) { - try { - const backupPath = destPath + '.backup'; - if (fs.existsSync(backupPath)) { - fs.unlinkSync(backupPath); - } - fs.renameSync(destPath, backupPath); - } catch (renameErr) { - console.log( - 'Could not backup existing file:', - renameErr.message - ); - // Continue anyway, cpSync might still work - } - } - - try { - fs.cpSync(sourcePath, destPath, { - force: true, - recursive: true, - }); - } catch (copyErr) { - console.error( - 'Failed to copy file:', - file, - copyErr.message - ); - reject(copyErr); - return; - } - } - resolve(); - } catch (unzipErr) { - console.error('Unzip failed:', unzipErr); - reject(unzipErr); - } - }); - - // delete the zip file - fs.unlinkSync(`./update.zip`); - // update the version file - fs.writeFileSync(`./version.txt`, release.tag_name); - // restart the app - console.log('App Ready.'); - - mainWindow.webContents.send('text', 'Launching OpenGameInstaller'); - // Add a small delay before launching to ensure all file operations are complete - setTimeout(() => { - launchApp(true); - }, 500); - } catch (updateErr) { - console.error('Update failed:', updateErr); - mainWindow.webContents.send( - 'text', - 'Update Failed', - updateErr.message - ); - // Try to launch the existing version - setTimeout(() => { - launchApp(true); - }, 2000); - } - }); - } else if (process.platform === 'linux') { - if (!fs.existsSync(`./update`)) { - fs.mkdirSync(`./update`); } - const writer = fs.createWriteStream( - `./update/OpenGameInstaller.AppImage` + } else if (!Number.isFinite(gap)) { + mainWindow.webContents.send( + 'text', + 'Falling back to full download', + 'Local version missing from release feed' ); - mainWindow.webContents.send('text', 'Downloading Update'); - const assetWithPortable = release.assets.find((asset) => - asset.name.toLowerCase().includes('linux-pt.appimage') + } else { + mainWindow.webContents.send( + 'text', + 'Falling back to full download', + 'Version too old for incremental update' ); + } - if (!assetWithPortable) { - mainWindow.webContents.send('text', 'No Portable Version Found'); - setTimeout(() => { - launchApp(net.isOnline()); - }, 2000); - return; - } - - const response = await axios({ - url: assetWithPortable.browser_download_url, - method: 'GET', - responseType: 'stream', - }); - response.data.pipe(writer); - const startTime = Date.now(); - const fileSize = response.headers['content-length']; - response.data.on('data', () => { - const elapsedTime = (Date.now() - startTime) / 1000; // in seconds - const downloadSpeed = response.data.socket.bytesRead / elapsedTime; - mainWindow.webContents.send( - 'text', - 'Downloading Update', - writer.bytesWritten, - fileSize, - correctParsingSize(downloadSpeed) + '/s' - ); - }); - response.data.on('end', async () => { - mainWindow.webContents.send('text', 'Download Complete'); - fs.writeFileSync(`./version.txt`, release.tag_name); - console.log('App Ready.'); - - // copy the file to the cache folder - const item = __dirname + '/update/OpenGameInstaller.AppImage'; - if (!fs.existsSync(localCache)) { - fs.mkdirSync(localCache); - } - fs.copyFileSync( - item, - path.join(localCache, 'OpenGameInstaller.AppImage') - ); - mainWindow.webContents.send('text', 'Launching OpenGameInstaller'); - // make the file executable - fs.chmodSync(`./update/OpenGameInstaller.AppImage`, '755'); - writer.close(); - launchApp(true); - }); + if (!updateApplied) { + await downloadFullRelease(targetRelease); } + fs.writeFileSync(`./version.txt`, targetRelease.tag_name); + mainWindow.webContents.send('text', 'Launching OpenGameInstaller'); + launchApp(true); + return; } if (!updating) { mainWindow.webContents.send( @@ -413,6 +193,213 @@ async function createWindow() { } } +function getVersionCache(tagName) { + return path.join(app.getPath('temp'), `ogi-${tagName.replace('v', '')}-cache`); +} + +function getPlatformAsset(release) { + if (process.platform === 'win32') { + return release.assets.find( + (asset) => + asset.name.toLowerCase().includes('portable') || + asset.name.toLowerCase().includes('portrable') + ); + } + return release.assets.find((asset) => + asset.name.toLowerCase().includes('linux-pt.appimage') + ); +} + +function getBlockmapAsset(release, targetAsset) { + return release.assets.find( + (asset) => + asset.name.toLowerCase() === `${targetAsset.name.toLowerCase()}.blockmap` + ); +} + +async function downloadToFile(url, destination, status) { + const writer = fs.createWriteStream(destination); + const response = await axios({ url, method: 'GET', responseType: 'stream' }); + response.data.pipe(writer); + const startTime = Date.now(); + const fileSize = response.headers['content-length']; + response.data.on('data', () => { + const elapsedTime = (Date.now() - startTime) / 1000; + const downloadSpeed = response.data.socket.bytesRead / Math.max(elapsedTime, 1); + mainWindow.webContents.send( + 'text', + status, + writer.bytesWritten, + fileSize, + correctParsingSize(downloadSpeed) + '/s' + ); + }); + await new Promise((resolve, reject) => { + writer.on('finish', resolve); + writer.on('error', reject); + response.data.on('error', reject); + }); +} + +function copyCacheToUpdate(cacheDir) { + const files = fs.readdirSync(cacheDir); + const destRoot = path.join(__dirname, 'update'); + fs.mkdirSync(destRoot, { recursive: true }); + for (const file of files) { + fs.cpSync(path.join(cacheDir, file), path.join(destRoot, file), { + force: true, + recursive: true, + }); + } +} + +async function downloadFullRelease(release) { + const assetWithPortable = getPlatformAsset(release); + if (!assetWithPortable) { + throw new Error('No portable asset found for this platform'); + } + const localCache = getVersionCache(release.tag_name); + fs.mkdirSync(localCache, { recursive: true }); + mainWindow.webContents.send('text', 'Downloading Update'); + const downloadPath = + process.platform === 'win32' ? './update.zip' : './update/OpenGameInstaller.AppImage'; + if (process.platform === 'linux') { + fs.mkdirSync('./update', { recursive: true }); + } + await downloadToFile(assetWithPortable.browser_download_url, downloadPath, 'Downloading Update'); + mainWindow.webContents.send('text', 'Download Complete'); + + if (process.platform === 'win32') { + mainWindow.webContents.send('text', 'Extracting Update'); + await unzip(`./update.zip`, localCache); + mainWindow.webContents.send('text', 'Copying Update Files'); + copyCacheToUpdate(localCache); + fs.copyFileSync('./update.zip', path.join(localCache, assetWithPortable.name)); + fs.unlinkSync('./update.zip'); + } else { + const item = path.join(__dirname, 'update', 'OpenGameInstaller.AppImage'); + fs.copyFileSync(item, path.join(localCache, 'OpenGameInstaller.AppImage')); + fs.copyFileSync(item, path.join(localCache, assetWithPortable.name)); + fs.chmodSync(item, '755'); + } +} + +async function applyBlockmapPath(releasePath) { + let currentTag = localVersion; + for (let i = 0; i < releasePath.length; i++) { + const nextRelease = releasePath[i]; + mainWindow.webContents.send('text', `Applying patch ${i + 1} of ${releasePath.length}`); + const fromCache = getVersionCache(currentTag); + const nextCache = getVersionCache(nextRelease.tag_name); + const targetAsset = getPlatformAsset(nextRelease); + if (!targetAsset) { + throw new Error(`Portable asset missing for ${nextRelease.tag_name}`); + } + const newBlockmapAsset = getBlockmapAsset(nextRelease, targetAsset); + if (!newBlockmapAsset) { + throw new Error(`Blockmap missing for ${nextRelease.tag_name}`); + } + const sourceArtifact = path.join(fromCache, targetAsset.name); + if (!fs.existsSync(sourceArtifact)) { + throw new Error(`Missing local source artifact for ${currentTag}`); + } + fs.mkdirSync(nextCache, { recursive: true }); + const newBlockmapPath = path.join(nextCache, `${targetAsset.name}.blockmap`); + await downloadToFile(newBlockmapAsset.browser_download_url, newBlockmapPath, 'Downloading blockmap'); + const oldBlockmapPath = path.join(fromCache, `${targetAsset.name}.blockmap`); + if (!fs.existsSync(oldBlockmapPath)) { + throw new Error(`Missing old blockmap for ${currentTag}`); + } + const outputArtifact = path.join(nextCache, targetAsset.name); + await applyBlockmapPatch(sourceArtifact, oldBlockmapPath, outputArtifact, newBlockmapPath, targetAsset.browser_download_url); + + if (process.platform === 'win32') { + await unzip(outputArtifact, nextCache); + } else { + fs.copyFileSync(outputArtifact, path.join(nextCache, 'OpenGameInstaller.AppImage')); + } + currentTag = nextRelease.tag_name; + } + copyCacheToUpdate(getVersionCache(releasePath[releasePath.length - 1].tag_name)); + if (process.platform === 'linux') { + fs.chmodSync('./update/OpenGameInstaller.AppImage', '755'); + } +} + +async function applyBlockmapPatch( + sourceArtifact, + oldBlockmapPath, + outputArtifact, + newBlockmapPath, + targetUrl +) { + const oldMap = JSON.parse(zlib.gunzipSync(fs.readFileSync(oldBlockmapPath))); + const newMap = JSON.parse(zlib.gunzipSync(fs.readFileSync(newBlockmapPath))); + const oldFile = oldMap.files?.[0]; + const newFile = newMap.files?.[0]; + if (!oldFile || !newFile) { + throw new Error('Invalid blockmap payload'); + } + const checksumToBlocks = new Map(); + let oldOffset = oldFile.offset || 0; + for (let i = 0; i < oldFile.checksums.length; i++) { + const key = oldFile.checksums[i]; + const block = { offset: oldOffset, size: oldFile.sizes[i] }; + const current = checksumToBlocks.get(key) || []; + current.push(block); + checksumToBlocks.set(key, current); + oldOffset += oldFile.sizes[i]; + } + + fs.mkdirSync(path.dirname(outputArtifact), { recursive: true }); + const sourceFd = fs.openSync(sourceArtifact, 'r'); + const outFd = fs.openSync(outputArtifact, 'w'); + let writeOffset = newFile.offset || 0; + const misses = []; + + for (let i = 0; i < newFile.checksums.length; i++) { + const size = newFile.sizes[i]; + const blocks = checksumToBlocks.get(newFile.checksums[i]); + const matched = blocks?.shift(); + if (matched) { + const buffer = Buffer.alloc(size); + fs.readSync(sourceFd, buffer, 0, size, matched.offset); + fs.writeSync(outFd, buffer, 0, size, writeOffset); + } else { + misses.push({ offset: writeOffset, size }); + } + writeOffset += size; + } + + const mergedMisses = []; + for (const miss of misses) { + const last = mergedMisses[mergedMisses.length - 1]; + if (last && last.offset + last.size === miss.offset) { + last.size += miss.size; + } else { + mergedMisses.push({ ...miss }); + } + } + + for (const miss of mergedMisses) { + const end = miss.offset + miss.size - 1; + const rangeResponse = await axios({ + url: targetUrl, + method: 'GET', + responseType: 'arraybuffer', + headers: { Range: `bytes=${miss.offset}-${end}` }, + }); + const chunk = Buffer.from(rangeResponse.data); + fs.writeSync(outFd, chunk, 0, chunk.length, miss.offset); + } + + fs.closeSync(sourceFd); + fs.closeSync(outFd); + if (!fs.existsSync(outputArtifact) || fs.statSync(outputArtifact).size === 0) { + throw new Error('Patched artifact is empty'); + } +} + /** * Launches the installed OpenGameInstaller, rotating logs, spawning the platform-specific executable in a detached process, and terminating the updater. * From 17febc8e9f4b5fc4a280665d67f02462cec551fd Mon Sep 17 00:00:00 2001 From: Nat3z <66748576+Nat3z@users.noreply.github.com> Date: Sun, 1 Mar 2026 07:39:56 -0800 Subject: [PATCH 2/9] Harden blockmap patching and cache blockmaps for full downloads --- updater/src/main.js | 129 +++++++++++++++++++++++++++++++------------- 1 file changed, 91 insertions(+), 38 deletions(-) diff --git a/updater/src/main.js b/updater/src/main.js index 9c501b80..b10afac9 100644 --- a/updater/src/main.js +++ b/updater/src/main.js @@ -260,6 +260,11 @@ async function downloadFullRelease(release) { } const localCache = getVersionCache(release.tag_name); fs.mkdirSync(localCache, { recursive: true }); + const blockmapAsset = getBlockmapAsset(release, assetWithPortable); + if (!blockmapAsset) { + throw new Error(`Blockmap missing for ${release.tag_name}`); + } + mainWindow.webContents.send('text', 'Downloading Update'); const downloadPath = process.platform === 'win32' ? './update.zip' : './update/OpenGameInstaller.AppImage'; @@ -267,6 +272,11 @@ async function downloadFullRelease(release) { fs.mkdirSync('./update', { recursive: true }); } await downloadToFile(assetWithPortable.browser_download_url, downloadPath, 'Downloading Update'); + await downloadToFile( + blockmapAsset.browser_download_url, + path.join(localCache, `${assetWithPortable.name}.blockmap`), + 'Downloading blockmap' + ); mainWindow.webContents.send('text', 'Download Complete'); if (process.platform === 'win32') { @@ -352,49 +362,92 @@ async function applyBlockmapPatch( } fs.mkdirSync(path.dirname(outputArtifact), { recursive: true }); - const sourceFd = fs.openSync(sourceArtifact, 'r'); - const outFd = fs.openSync(outputArtifact, 'w'); - let writeOffset = newFile.offset || 0; - const misses = []; - - for (let i = 0; i < newFile.checksums.length; i++) { - const size = newFile.sizes[i]; - const blocks = checksumToBlocks.get(newFile.checksums[i]); - const matched = blocks?.shift(); - if (matched) { - const buffer = Buffer.alloc(size); - fs.readSync(sourceFd, buffer, 0, size, matched.offset); - fs.writeSync(outFd, buffer, 0, size, writeOffset); - } else { - misses.push({ offset: writeOffset, size }); + let sourceFd; + let outFd; + + try { + sourceFd = fs.openSync(sourceArtifact, 'r'); + outFd = fs.openSync(outputArtifact, 'w'); + let writeOffset = newFile.offset || 0; + const misses = []; + + for (let i = 0; i < newFile.checksums.length; i++) { + const size = newFile.sizes[i]; + const blocks = checksumToBlocks.get(newFile.checksums[i]); + const matched = blocks?.shift(); + if (matched) { + const buffer = Buffer.alloc(size); + fs.readSync(sourceFd, buffer, 0, size, matched.offset); + fs.writeSync(outFd, buffer, 0, size, writeOffset); + } else { + misses.push({ offset: writeOffset, size }); + } + writeOffset += size; } - writeOffset += size; - } - const mergedMisses = []; - for (const miss of misses) { - const last = mergedMisses[mergedMisses.length - 1]; - if (last && last.offset + last.size === miss.offset) { - last.size += miss.size; - } else { - mergedMisses.push({ ...miss }); + const mergedMisses = []; + for (const miss of misses) { + const last = mergedMisses[mergedMisses.length - 1]; + if (last && last.offset + last.size === miss.offset) { + last.size += miss.size; + } else { + mergedMisses.push({ ...miss }); + } } - } - for (const miss of mergedMisses) { - const end = miss.offset + miss.size - 1; - const rangeResponse = await axios({ - url: targetUrl, - method: 'GET', - responseType: 'arraybuffer', - headers: { Range: `bytes=${miss.offset}-${end}` }, - }); - const chunk = Buffer.from(rangeResponse.data); - fs.writeSync(outFd, chunk, 0, chunk.length, miss.offset); - } + for (const miss of mergedMisses) { + const end = miss.offset + miss.size - 1; + const requestedRange = `bytes=${miss.offset}-${end}`; + const rangeResponse = await axios({ + url: targetUrl, + method: 'GET', + responseType: 'arraybuffer', + headers: { Range: requestedRange }, + }); + + const expectedSize = end - miss.offset + 1; + const actualSize = Buffer.byteLength(rangeResponse.data); + const contentRange = rangeResponse.headers['content-range']; + const expectedContentRangePrefix = `bytes ${miss.offset}-${end}/`; - fs.closeSync(sourceFd); - fs.closeSync(outFd); + if (rangeResponse.status !== 206) { + throw new Error( + `Invalid range response status ${rangeResponse.status} for ${requestedRange}` + ); + } + if (actualSize !== expectedSize || actualSize !== miss.size) { + throw new Error( + `Invalid range response length ${actualSize} for ${requestedRange}; expected ${expectedSize}` + ); + } + if ( + typeof contentRange !== 'string' || + !contentRange.startsWith(expectedContentRangePrefix) + ) { + throw new Error( + `Invalid content-range header for ${requestedRange}: ${contentRange}` + ); + } + + const chunk = Buffer.from(rangeResponse.data); + fs.writeSync(outFd, chunk, 0, chunk.length, miss.offset); + } + } finally { + if (typeof sourceFd === 'number') { + try { + fs.closeSync(sourceFd); + } catch (closeErr) { + console.error('Failed to close source file descriptor:', closeErr); + } + } + if (typeof outFd === 'number') { + try { + fs.closeSync(outFd); + } catch (closeErr) { + console.error('Failed to close output file descriptor:', closeErr); + } + } + } if (!fs.existsSync(outputArtifact) || fs.statSync(outputArtifact).size === 0) { throw new Error('Patched artifact is empty'); } From 91adb30b0aa7bf9a16d8081257b7fd55b7d5e0d0 Mon Sep 17 00:00:00 2001 From: Nat3z <66748576+Nat3z@users.noreply.github.com> Date: Sun, 1 Mar 2026 10:56:16 -0800 Subject: [PATCH 3/9] Sort release feed newest-first before patch path planning --- updater/src/main.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/updater/src/main.js b/updater/src/main.js index b10afac9..e9d8539c 100644 --- a/updater/src/main.js +++ b/updater/src/main.js @@ -125,9 +125,13 @@ async function createWindow() { { timeout: 10000 } // 10 second timeout for update check ); mainWindow.webContents.send('text', 'Checking for Updates'); - const releases = response.data.filter((rel) => - usingBleedingEdge ? rel.prerelease : !rel.prerelease - ); + const releases = response.data + .filter((rel) => (usingBleedingEdge ? rel.prerelease : !rel.prerelease)) + .sort( + (a, b) => + new Date(b.published_at || b.created_at || 0).getTime() - + new Date(a.published_at || a.created_at || 0).getTime() + ); const localIndex = releases.findIndex((rel) => rel.tag_name === localVersion); const targetRelease = releases[0]; let updating = Boolean(targetRelease) && localIndex !== 0; From 6cf52d55892c99130eb475cda523871604f64a68 Mon Sep 17 00:00:00 2001 From: Nat3z <66748576+Nat3z@users.noreply.github.com> Date: Sun, 1 Mar 2026 11:12:48 -0800 Subject: [PATCH 4/9] fix: fixed build-release blockmaps --- .github/workflows/build-release.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 41d415b1..d71b089c 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -69,13 +69,6 @@ jobs: run: | Compress-Archive -Path application/dist/win-unpacked/* -Destination application/dist/OpenGameInstaller-Portable.zip - - name: Generate Windows blockmaps - if: matrix.os == 'windows-latest' && steps.check_changes.outputs.has_application_changes == 'true' - shell: bash - run: | - bunx --bun app-builder-bin blockmap --input "application/dist/OpenGameInstaller-Portable.zip" --output "application/dist/OpenGameInstaller-Portable.zip.blockmap" - bunx --bun app-builder-bin blockmap --input "updater/dist/OpenGameInstaller-Setup.exe" --output "updater/dist/OpenGameInstaller-Setup.exe.blockmap" - - name: Upload Windows Assets as Artifacts if: matrix.os == 'windows-latest' && steps.check_changes.outputs.has_application_changes == 'true' uses: actions/upload-artifact@v4 From 3f306439c391fca916763890d3bf2ec54e7fd528 Mon Sep 17 00:00:00 2001 From: Nat3z <66748576+Nat3z@users.noreply.github.com> Date: Sun, 1 Mar 2026 11:19:48 -0800 Subject: [PATCH 5/9] fix: fixed a bunch of blockmap resolution stuff --- .github/workflows/build-release.yml | 13 ++-- updater/src/main.js | 93 ++++++++++++++++++++++------- 2 files changed, 76 insertions(+), 30 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index d71b089c..8ac085d5 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -69,6 +69,12 @@ jobs: run: | Compress-Archive -Path application/dist/win-unpacked/* -Destination application/dist/OpenGameInstaller-Portable.zip + - name: Generate Portable ZIP blockmap + if: matrix.os == 'windows-latest' && steps.check_changes.outputs.has_application_changes == 'true' + run: | + $appBuilder = node -p "require('app-builder-bin').appBuilderPath" + & $appBuilder blockmap --input "application/dist/OpenGameInstaller-Portable.zip" --output "application/dist/OpenGameInstaller-Portable.zip.blockmap" + - name: Upload Windows Assets as Artifacts if: matrix.os == 'windows-latest' && steps.check_changes.outputs.has_application_changes == 'true' uses: actions/upload-artifact@v4 @@ -80,13 +86,6 @@ jobs: updater/dist/OpenGameInstaller-Setup.exe updater/dist/OpenGameInstaller-Setup.exe.blockmap - - name: Generate Linux blockmaps - if: matrix.os == 'ubuntu-latest' && steps.check_changes.outputs.has_application_changes == 'true' - shell: bash - run: | - bunx --bun app-builder-bin blockmap --input "application/dist/OpenGameInstaller-linux-pt.AppImage" --output "application/dist/OpenGameInstaller-linux-pt.AppImage.blockmap" - bunx --bun app-builder-bin blockmap --input "updater/dist/OpenGameInstaller-Setup.AppImage" --output "updater/dist/OpenGameInstaller-Setup.AppImage.blockmap" - - name: Upload Linux Assets as Artifacts if: matrix.os == 'ubuntu-latest' && steps.check_changes.outputs.has_application_changes == 'true' uses: actions/upload-artifact@v4 diff --git a/updater/src/main.js b/updater/src/main.js index e9d8539c..87571b2b 100644 --- a/updater/src/main.js +++ b/updater/src/main.js @@ -132,17 +132,25 @@ async function createWindow() { new Date(b.published_at || b.created_at || 0).getTime() - new Date(a.published_at || a.created_at || 0).getTime() ); - const localIndex = releases.findIndex((rel) => rel.tag_name === localVersion); + const localIndex = releases.findIndex( + (rel) => rel.tag_name === localVersion + ); const targetRelease = releases[0]; let updating = Boolean(targetRelease) && localIndex !== 0; if (targetRelease && updating) { const releasePath = - localIndex > 0 ? releases.slice(0, localIndex).reverse() : [targetRelease]; - const gap = localIndex > 0 ? releasePath.length : Number.POSITIVE_INFINITY; + localIndex > 0 + ? releases.slice(0, localIndex).reverse() + : [targetRelease]; + const gap = + localIndex > 0 ? releasePath.length : Number.POSITIVE_INFINITY; let updateApplied = false; if (Number.isFinite(gap) && gap > 0 && gap <= 3) { - mainWindow.webContents.send('text', 'Preparing incremental update path'); + mainWindow.webContents.send( + 'text', + 'Preparing incremental update path' + ); try { await applyBlockmapPath(releasePath); updateApplied = true; @@ -198,7 +206,10 @@ async function createWindow() { } function getVersionCache(tagName) { - return path.join(app.getPath('temp'), `ogi-${tagName.replace('v', '')}-cache`); + return path.join( + app.getPath('temp'), + `ogi-${tagName.replace('v', '')}-cache` + ); } function getPlatformAsset(release) { @@ -229,7 +240,8 @@ async function downloadToFile(url, destination, status) { const fileSize = response.headers['content-length']; response.data.on('data', () => { const elapsedTime = (Date.now() - startTime) / 1000; - const downloadSpeed = response.data.socket.bytesRead / Math.max(elapsedTime, 1); + const downloadSpeed = + response.data.socket.bytesRead / Math.max(elapsedTime, 1); mainWindow.webContents.send( 'text', status, @@ -265,22 +277,27 @@ async function downloadFullRelease(release) { const localCache = getVersionCache(release.tag_name); fs.mkdirSync(localCache, { recursive: true }); const blockmapAsset = getBlockmapAsset(release, assetWithPortable); - if (!blockmapAsset) { - throw new Error(`Blockmap missing for ${release.tag_name}`); - } mainWindow.webContents.send('text', 'Downloading Update'); const downloadPath = - process.platform === 'win32' ? './update.zip' : './update/OpenGameInstaller.AppImage'; + process.platform === 'win32' + ? './update.zip' + : './update/OpenGameInstaller.AppImage'; if (process.platform === 'linux') { fs.mkdirSync('./update', { recursive: true }); } - await downloadToFile(assetWithPortable.browser_download_url, downloadPath, 'Downloading Update'); await downloadToFile( - blockmapAsset.browser_download_url, - path.join(localCache, `${assetWithPortable.name}.blockmap`), - 'Downloading blockmap' + assetWithPortable.browser_download_url, + downloadPath, + 'Downloading Update' ); + if (blockmapAsset) { + await downloadToFile( + blockmapAsset.browser_download_url, + path.join(localCache, `${assetWithPortable.name}.blockmap`), + 'Downloading blockmap' + ); + } mainWindow.webContents.send('text', 'Download Complete'); if (process.platform === 'win32') { @@ -288,7 +305,10 @@ async function downloadFullRelease(release) { await unzip(`./update.zip`, localCache); mainWindow.webContents.send('text', 'Copying Update Files'); copyCacheToUpdate(localCache); - fs.copyFileSync('./update.zip', path.join(localCache, assetWithPortable.name)); + fs.copyFileSync( + './update.zip', + path.join(localCache, assetWithPortable.name) + ); fs.unlinkSync('./update.zip'); } else { const item = path.join(__dirname, 'update', 'OpenGameInstaller.AppImage'); @@ -302,7 +322,10 @@ async function applyBlockmapPath(releasePath) { let currentTag = localVersion; for (let i = 0; i < releasePath.length; i++) { const nextRelease = releasePath[i]; - mainWindow.webContents.send('text', `Applying patch ${i + 1} of ${releasePath.length}`); + mainWindow.webContents.send( + 'text', + `Applying patch ${i + 1} of ${releasePath.length}` + ); const fromCache = getVersionCache(currentTag); const nextCache = getVersionCache(nextRelease.tag_name); const targetAsset = getPlatformAsset(nextRelease); @@ -318,23 +341,44 @@ async function applyBlockmapPath(releasePath) { throw new Error(`Missing local source artifact for ${currentTag}`); } fs.mkdirSync(nextCache, { recursive: true }); - const newBlockmapPath = path.join(nextCache, `${targetAsset.name}.blockmap`); - await downloadToFile(newBlockmapAsset.browser_download_url, newBlockmapPath, 'Downloading blockmap'); - const oldBlockmapPath = path.join(fromCache, `${targetAsset.name}.blockmap`); + const newBlockmapPath = path.join( + nextCache, + `${targetAsset.name}.blockmap` + ); + await downloadToFile( + newBlockmapAsset.browser_download_url, + newBlockmapPath, + 'Downloading blockmap' + ); + const oldBlockmapPath = path.join( + fromCache, + `${targetAsset.name}.blockmap` + ); if (!fs.existsSync(oldBlockmapPath)) { throw new Error(`Missing old blockmap for ${currentTag}`); } const outputArtifact = path.join(nextCache, targetAsset.name); - await applyBlockmapPatch(sourceArtifact, oldBlockmapPath, outputArtifact, newBlockmapPath, targetAsset.browser_download_url); + await applyBlockmapPatch( + sourceArtifact, + oldBlockmapPath, + outputArtifact, + newBlockmapPath, + targetAsset.browser_download_url + ); if (process.platform === 'win32') { await unzip(outputArtifact, nextCache); } else { - fs.copyFileSync(outputArtifact, path.join(nextCache, 'OpenGameInstaller.AppImage')); + fs.copyFileSync( + outputArtifact, + path.join(nextCache, 'OpenGameInstaller.AppImage') + ); } currentTag = nextRelease.tag_name; } - copyCacheToUpdate(getVersionCache(releasePath[releasePath.length - 1].tag_name)); + copyCacheToUpdate( + getVersionCache(releasePath[releasePath.length - 1].tag_name) + ); if (process.platform === 'linux') { fs.chmodSync('./update/OpenGameInstaller.AppImage', '755'); } @@ -452,7 +496,10 @@ async function applyBlockmapPatch( } } } - if (!fs.existsSync(outputArtifact) || fs.statSync(outputArtifact).size === 0) { + if ( + !fs.existsSync(outputArtifact) || + fs.statSync(outputArtifact).size === 0 + ) { throw new Error('Patched artifact is empty'); } } From 7449cd32b786b324d938a85661daeed434998e65 Mon Sep 17 00:00:00 2001 From: Nat3z <66748576+Nat3z@users.noreply.github.com> Date: Sun, 1 Mar 2026 12:41:18 -0800 Subject: [PATCH 6/9] Remove blockmap generation step from build workflow Removed the step for generating the Portable ZIP blockmap in the build workflow. --- .github/workflows/build-release.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 8ac085d5..7d576b09 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -69,12 +69,6 @@ jobs: run: | Compress-Archive -Path application/dist/win-unpacked/* -Destination application/dist/OpenGameInstaller-Portable.zip - - name: Generate Portable ZIP blockmap - if: matrix.os == 'windows-latest' && steps.check_changes.outputs.has_application_changes == 'true' - run: | - $appBuilder = node -p "require('app-builder-bin').appBuilderPath" - & $appBuilder blockmap --input "application/dist/OpenGameInstaller-Portable.zip" --output "application/dist/OpenGameInstaller-Portable.zip.blockmap" - - name: Upload Windows Assets as Artifacts if: matrix.os == 'windows-latest' && steps.check_changes.outputs.has_application_changes == 'true' uses: actions/upload-artifact@v4 From 5ad49aa480e123abef63a06283942aabdff78a69 Mon Sep 17 00:00:00 2001 From: Nat3z <66748576+Nat3z@users.noreply.github.com> Date: Sun, 1 Mar 2026 12:56:33 -0800 Subject: [PATCH 7/9] fix: fixed blockmap not uploading --- .github/workflows/build-release.yml | 4 ++++ .github/workflows/js/check-application-changes.js | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 7d576b09..b1554134 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -69,6 +69,10 @@ jobs: run: | Compress-Archive -Path application/dist/win-unpacked/* -Destination application/dist/OpenGameInstaller-Portable.zip + - name: What's in the folders? (Windows) + if: steps.check_changes.outputs.has_application_changes == 'true' + run: ls -la application/dist/ + - name: Upload Windows Assets as Artifacts if: matrix.os == 'windows-latest' && steps.check_changes.outputs.has_application_changes == 'true' uses: actions/upload-artifact@v4 diff --git a/.github/workflows/js/check-application-changes.js b/.github/workflows/js/check-application-changes.js index 0e8d9205..685a72ea 100644 --- a/.github/workflows/js/check-application-changes.js +++ b/.github/workflows/js/check-application-changes.js @@ -31,7 +31,11 @@ function checkApplicationChanges() { file.startsWith('application/') ); - if (hasChanges) { + const hasWorkflowChanges = changedFiles.some((file) => + file.startsWith('.github/workflows/') + ); + + if (hasChanges || hasWorkflowChanges) { setOutput('has_application_changes', 'true'); console.log('Found changes in application directory'); } else { From fd7b891ed1914f42e6e04e2a2e9f76cdc613d760 Mon Sep 17 00:00:00 2001 From: Nat3z <66748576+Nat3z@users.noreply.github.com> Date: Sun, 1 Mar 2026 13:14:46 -0800 Subject: [PATCH 8/9] Add CI blockmap generation --- .github/workflows/build-release.yml | 15 ++++- .github/workflows/js/generate-blockmaps.mjs | 71 +++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/js/generate-blockmaps.mjs diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index b1554134..95efb93f 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -69,9 +69,22 @@ jobs: run: | Compress-Archive -Path application/dist/win-unpacked/* -Destination application/dist/OpenGameInstaller-Portable.zip + - name: Generate Windows blockmaps + if: matrix.os == 'windows-latest' && steps.check_changes.outputs.has_application_changes == 'true' + run: | + bun run .github/workflows/js/generate-blockmaps.mjs application/dist/OpenGameInstaller-Portable.zip updater/dist/OpenGameInstaller-Setup.exe + + - name: Generate Linux blockmaps + if: matrix.os == 'ubuntu-latest' && steps.check_changes.outputs.has_application_changes == 'true' + run: | + bun run .github/workflows/js/generate-blockmaps.mjs updater/dist/OpenGameInstaller-Setup.AppImage application/dist/OpenGameInstaller-linux-pt.AppImage + - name: What's in the folders? (Windows) if: steps.check_changes.outputs.has_application_changes == 'true' - run: ls -la application/dist/ + shell: bash + run: | + ls -la application/dist/ + ls -la updater/dist/ - name: Upload Windows Assets as Artifacts if: matrix.os == 'windows-latest' && steps.check_changes.outputs.has_application_changes == 'true' diff --git a/.github/workflows/js/generate-blockmaps.mjs b/.github/workflows/js/generate-blockmaps.mjs new file mode 100644 index 00000000..50e7586d --- /dev/null +++ b/.github/workflows/js/generate-blockmaps.mjs @@ -0,0 +1,71 @@ +import { existsSync } from 'node:fs'; +import path from 'node:path'; +import { createRequire } from 'node:module'; +import { spawn } from 'node:child_process'; + +const require = createRequire(import.meta.url); + +function resolveAppBuilderPath() { + const searchPaths = [ + process.cwd(), + path.join(process.cwd(), 'node_modules'), + path.join(process.cwd(), 'application'), + path.join(process.cwd(), 'application', 'node_modules'), + path.join(process.cwd(), 'updater'), + path.join(process.cwd(), 'updater', 'node_modules'), + ]; + + for (const searchPath of searchPaths) { + try { + const modulePath = require.resolve('app-builder-bin', { + paths: [searchPath], + }); + return require(modulePath).appBuilderPath; + } catch { + // Try the next candidate path. + } + } + + throw new Error( + 'Unable to resolve app-builder-bin. Ensure dependencies are installed before generating blockmaps.' + ); +} + +const appBuilderPath = resolveAppBuilderPath(); + +function runAppBuilder(args) { + return new Promise((resolve, reject) => { + const child = spawn(appBuilderPath, args, { stdio: 'inherit' }); + child.on('exit', (code) => { + if (code === 0) { + resolve(); + return; + } + reject(new Error(`app-builder exited with code ${code}`)); + }); + child.on('error', reject); + }); +} + +async function main() { + const args = process.argv.slice(2); + if (args.length === 0) { + console.error('Usage: bun run .github/workflows/js/generate-blockmaps.mjs [file...]'); + process.exit(1); + } + + for (const artifact of args) { + const artifactPath = path.resolve(artifact); + const blockmapPath = `${artifactPath}.blockmap`; + + if (!existsSync(artifactPath)) { + console.error(`Artifact not found: ${artifactPath}`); + process.exit(1); + } + + console.log(`Generating blockmap: ${blockmapPath}`); + await runAppBuilder(['blockmap', '--input', artifactPath, '--output', blockmapPath]); + } +} + +await main(); From 38893085e73a1e4706e6744c45829dc80f82b5a2 Mon Sep 17 00:00:00 2001 From: Nat3z <66748576+Nat3z@users.noreply.github.com> Date: Sun, 1 Mar 2026 13:29:26 -0800 Subject: [PATCH 9/9] Add blockmap download handling --- updater/src/main.js | 107 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 83 insertions(+), 24 deletions(-) diff --git a/updater/src/main.js b/updater/src/main.js index 87571b2b..12b7df2a 100644 --- a/updater/src/main.js +++ b/updater/src/main.js @@ -152,7 +152,7 @@ async function createWindow() { 'Preparing incremental update path' ); try { - await applyBlockmapPath(releasePath); + await applyBlockmapPath(releasePath, releases); updateApplied = true; } catch (patchErr) { console.error('Incremental patching failed, falling back:', patchErr); @@ -232,6 +232,59 @@ function getBlockmapAsset(release, targetAsset) { ); } +function getReleaseByTag(releases, tagName) { + return releases.find((release) => release.tag_name === tagName); +} + +async function ensureCachedSourceArtifact(cacheDir, release, asset) { + const sourceArtifactPath = path.join(cacheDir, asset.name); + if (fs.existsSync(sourceArtifactPath)) { + return sourceArtifactPath; + } + + fs.mkdirSync(cacheDir, { recursive: true }); + + // On Linux we usually have the currently installed AppImage available locally. + if (process.platform === 'linux') { + const installedAppImage = path.join( + __dirname, + 'update', + 'OpenGameInstaller.AppImage' + ); + if (fs.existsSync(installedAppImage)) { + fs.copyFileSync(installedAppImage, sourceArtifactPath); + return sourceArtifactPath; + } + } + + await downloadToFile( + asset.browser_download_url, + sourceArtifactPath, + `Downloading base artifact ${release.tag_name}` + ); + return sourceArtifactPath; +} + +async function ensureCachedBlockmap(cacheDir, release, asset) { + const blockmapAsset = getBlockmapAsset(release, asset); + if (!blockmapAsset) { + throw new Error(`Blockmap missing for ${release.tag_name}`); + } + + const blockmapPath = path.join(cacheDir, `${asset.name}.blockmap`); + if (fs.existsSync(blockmapPath)) { + return blockmapPath; + } + + fs.mkdirSync(cacheDir, { recursive: true }); + await downloadToFile( + blockmapAsset.browser_download_url, + blockmapPath, + `Downloading blockmap ${release.tag_name}` + ); + return blockmapPath; +} + async function downloadToFile(url, destination, status) { const writer = fs.createWriteStream(destination); const response = await axios({ url, method: 'GET', responseType: 'stream' }); @@ -318,52 +371,58 @@ async function downloadFullRelease(release) { } } -async function applyBlockmapPath(releasePath) { +async function applyBlockmapPath(releasePath, releases) { let currentTag = localVersion; for (let i = 0; i < releasePath.length; i++) { + const currentRelease = getReleaseByTag(releases, currentTag); const nextRelease = releasePath[i]; mainWindow.webContents.send( 'text', `Applying patch ${i + 1} of ${releasePath.length}` ); + if (!currentRelease) { + throw new Error(`Release metadata missing for ${currentTag}`); + } const fromCache = getVersionCache(currentTag); const nextCache = getVersionCache(nextRelease.tag_name); - const targetAsset = getPlatformAsset(nextRelease); - if (!targetAsset) { + const currentAsset = getPlatformAsset(currentRelease); + if (!currentAsset) { + throw new Error(`Portable asset missing for ${currentTag}`); + } + const nextAsset = getPlatformAsset(nextRelease); + if (!nextAsset) { throw new Error(`Portable asset missing for ${nextRelease.tag_name}`); } - const newBlockmapAsset = getBlockmapAsset(nextRelease, targetAsset); + const newBlockmapAsset = getBlockmapAsset(nextRelease, nextAsset); if (!newBlockmapAsset) { throw new Error(`Blockmap missing for ${nextRelease.tag_name}`); } - const sourceArtifact = path.join(fromCache, targetAsset.name); - if (!fs.existsSync(sourceArtifact)) { - throw new Error(`Missing local source artifact for ${currentTag}`); - } - fs.mkdirSync(nextCache, { recursive: true }); - const newBlockmapPath = path.join( - nextCache, - `${targetAsset.name}.blockmap` - ); - await downloadToFile( - newBlockmapAsset.browser_download_url, - newBlockmapPath, - 'Downloading blockmap' + const sourceArtifact = await ensureCachedSourceArtifact( + fromCache, + currentRelease, + currentAsset ); - const oldBlockmapPath = path.join( + const oldBlockmapPath = await ensureCachedBlockmap( fromCache, - `${targetAsset.name}.blockmap` + currentRelease, + currentAsset ); - if (!fs.existsSync(oldBlockmapPath)) { - throw new Error(`Missing old blockmap for ${currentTag}`); + fs.mkdirSync(nextCache, { recursive: true }); + const newBlockmapPath = path.join(nextCache, `${nextAsset.name}.blockmap`); + if (!fs.existsSync(newBlockmapPath)) { + await downloadToFile( + newBlockmapAsset.browser_download_url, + newBlockmapPath, + 'Downloading blockmap' + ); } - const outputArtifact = path.join(nextCache, targetAsset.name); + const outputArtifact = path.join(nextCache, nextAsset.name); await applyBlockmapPatch( sourceArtifact, oldBlockmapPath, outputArtifact, newBlockmapPath, - targetAsset.browser_download_url + nextAsset.browser_download_url ); if (process.platform === 'win32') {