diff --git a/src/common/request.js b/src/common/request.js index 5d3ed66..d7463b9 100644 --- a/src/common/request.js +++ b/src/common/request.js @@ -1,71 +1,90 @@ const clampProgress = (n) => Math.max(0, Math.min(1, n)); -const request = ({ - url, - type, - progressCallback, - timeout, - estimatedSize, - abortTarget -}) => new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.onload = () => { - cleanup(); - if (xhr.status === 200) { - resolve(xhr.response); - } else { - reject(new Error(`Couldn't fetch ${url}: status code ${xhr.status}`)); - } - }; - xhr.onerror = () => { - cleanup(); - reject(new Error(`Couldn't fetch ${url}, are you offline?`)); - }; +const request = async (options) => { + const { + type, + progressCallback, + timeout, + estimatedSize, + abortTarget + } = options; - if (progressCallback) { - xhr.onprogress = (e) => { - if (e.lengthComputable) { - progressCallback(clampProgress(e.loaded / e.total)); - } else if (estimatedSize) { - progressCallback(clampProgress(e.loaded / estimatedSize)); + const requestURL = (url) => new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.onload = () => { + cleanup(); + if (xhr.status === 200) { + resolve(xhr.response); + } else { + reject(new Error(`Couldn't fetch ${url}: status code ${xhr.status}`)); } }; - } - - xhr.responseType = type; - xhr.open('GET', url); - xhr.send(); - - const cleanup = () => { - if (cleanupAbortCallback) { - cleanupAbortCallback(); - } - if (timeoutId) { - clearTimeout(timeoutId); - } - }; - - let cleanupAbortCallback; - if (abortTarget) { - const abortCallback = () => { - xhr.abort(); + xhr.onerror = () => { cleanup(); - reject(new Error(`Couldn't fetch ${url}: aborted`)); + reject(new Error(`Couldn't fetch ${url}, are you offline?`)); }; - abortTarget.addEventListener('abort', abortCallback); - cleanupAbortCallback = () => { - abortTarget.removeEventListener('abort', abortCallback); + + if (progressCallback) { + xhr.onprogress = (e) => { + if (e.lengthComputable) { + progressCallback(clampProgress(e.loaded / e.total)); + } else if (estimatedSize) { + progressCallback(clampProgress(e.loaded / estimatedSize)); + } + }; + } + + xhr.responseType = type; + xhr.open('GET', url); + xhr.send(); + + const cleanup = () => { + if (cleanupAbortCallback) { + cleanupAbortCallback(); + } + if (timeoutId) { + clearTimeout(timeoutId); + } }; - } + + let cleanupAbortCallback; + if (abortTarget) { + const abortCallback = () => { + xhr.abort(); + cleanup(); + reject(new Error(`Couldn't fetch ${url}: aborted`)); + }; + abortTarget.addEventListener('abort', abortCallback); + cleanupAbortCallback = () => { + abortTarget.removeEventListener('abort', abortCallback); + }; + } + + let timeoutId; + if (timeout) { + timeoutId = setTimeout(() => { + xhr.abort(); + cleanup(); + reject(new Error(`Couldn't fetch ${url}: timed out`)); + }, timeout); + } + }); - let timeoutId; - if (timeout) { - timeoutId = setTimeout(() => { - xhr.abort(); - cleanup(); - reject(new Error(`Couldn't fetch ${url}: timed out`)); - }, timeout); + const urls = Array.isArray(options.url) ? options.url : [options.url]; + if (urls.length === 0) { + throw new Error('no URLs'); } -}); + let errorToThrow; + for (const url of urls) { + try { + return await requestURL(url); + } catch (e) { + if (!errorToThrow) { + errorToThrow = e; + } + } + } + throw errorToThrow; +}; export default request; diff --git a/src/packager/large-assets.js b/src/packager/large-assets.js index 70f627b..0de0bea 100644 --- a/src/packager/large-assets.js +++ b/src/packager/large-assets.js @@ -1,61 +1,72 @@ -const LARGE_ASSET_BASE = 'https://packagerdata.turbowarp.org/'; -const SCAFFOLDING_ROOT = 'scaffolding/'; +// This defines where files are fetched from when the packager needs to download files. +// Files fetched from an external server have a SHA-256 checksum used to validate the download. +// src is the URL that will be fetched to download the asset. If it's an array of URLs, each URL +// will be tried in succession if the previous one fails, perhaps because it's blocked by a school +// network filter. + +const externalFile = (name) => [ + // Hopefully one of these URLs will not be blocked. + `https://packagerdata.turbowarp.org/${name}`, + `https://blobs.turbowarp.xyz/${name}` +]; + +const relativeScaffolding = (name) => `scaffolding/${name}`; export default { 'nwjs-win64': { - src: LARGE_ASSET_BASE + 'nwjs-v0.54.0-win-x64.zip', + src: externalFile('nwjs-v0.54.0-win-x64.zip'), sha256: '0f082671b67b711f783d98cc989cf5aebacfc9bce3bef78875b57d08fc2a6e86', type: 'arraybuffer' }, 'nwjs-win32': { - src: LARGE_ASSET_BASE + 'nwjs-v0.54.0-win-ia32.zip', + src: externalFile('nwjs-v0.54.0-win-ia32.zip'), sha256: '7a1ed3a6a51b8cdf9280761b90cb4723cf9ee8050e3ed0b58451e8e4e694b203', type: 'arraybuffer' }, 'nwjs-mac': { - src: LARGE_ASSET_BASE + 'nwjs-v0.54.0-osx-x64.zip', + src: externalFile('nwjs-v0.54.0-osx-x64.zip'), sha256: '498c97a264f8feac504c4c2396c1fddc8290c15573aee2fc692e59ff9803cc40', type: 'arraybuffer' }, 'nwjs-linux-x64': { - src: LARGE_ASSET_BASE + 'nwjs-v0.54.0-linux-x64.zip', + src: externalFile('nwjs-v0.54.0-linux-x64.zip'), sha256: '53651a3a12d29ad096cff5b44d9f1e3aa09e9fad970bdcfe8bda07ea23d960d8', type: 'arraybuffer' }, 'electron-win32': { - src: LARGE_ASSET_BASE + 'electron-v15.0.0-win32-ia32.zip', + src: externalFile('electron-v15.0.0-win32-ia32.zip'), sha256: '5b69112b91010d78abf6012f387ea3fe5c0e36657c5f386a4c2bb42138aa1a8e', type: 'arraybuffer' }, 'electron-win64': { - src: LARGE_ASSET_BASE + 'electron-v15.0.0-win32-x64.zip', + src: externalFile('electron-v15.0.0-win32-x64.zip'), sha256: '3d95422d9f2fccb07b745b31007548231d80ff63e3640f49a5f8591498a3afa2', type: 'arraybuffer' }, 'electron-linux64': { - src: LARGE_ASSET_BASE + 'electron-v15.1.1-linux-x64.zip', + src: externalFile('electron-v15.1.1-linux-x64.zip'), sha256: '70de2da51c6a8591b88f08366c82166a51b1719243f67ef1a14eddbb806a115f', type: 'arraybuffer' }, 'webview-mac': { - src: LARGE_ASSET_BASE + 'WebView-macos-3.zip', + src: externalFile('WebView-macos-3.zip'), sha256: '5d4086894f10549c61c20e5f770c808afc25c2e4793da75b5b1cede294449fac', type: 'arraybuffer' }, scaffolding: { - src: SCAFFOLDING_ROOT + 'scaffolding.js', + src: relativeScaffolding('scaffolding.js'), estimatedSize: 4775503, useBuildId: true, type: 'text' }, 'scaffolding-min': { - src: SCAFFOLDING_ROOT + 'scaffolding-min.js', + src: relativeScaffolding('scaffolding-min.js'), estimatedSize: 2540321, useBuildId: true, type: 'text' }, addons: { - src: SCAFFOLDING_ROOT + 'addons.js', + src: relativeScaffolding('addons.js'), estimatedSize: 14339, useBuildId: true, type: 'text' diff --git a/src/packager/web/cache.js b/src/packager/web/cache.js index 4255f28..3a7d63c 100644 --- a/src/packager/web/cache.js +++ b/src/packager/web/cache.js @@ -9,7 +9,9 @@ const STORE_NAME = 'assets'; const db = new Database(DATABASE_NAME, DATABASE_VERSION, STORE_NAME); const getAssetId = (asset) => { - return `${asset.src}-${asset.type}-${asset.sha256 || buildId}`; + const urls = asset.src; + const firstURL = Array.isArray(urls) ? urls[0] : urls; + return `${firstURL}-${asset.type}-${asset.sha256 || buildId}`; }; const removeExtraneous = async () => {