From 2abe5243d08f6ee16880f4ca59dfba76c605fca2 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Sat, 17 Jan 2026 23:13:16 +1100 Subject: [PATCH] Enhance EJS_Download to handle non-http(s) URLs and delegate URL-based downloads in EmulatorJS --- data/src/cache.js | 75 ++++++++++++++++++++++++++++++++++++++++++++ data/src/emulator.js | 42 +++++++------------------ 2 files changed, 87 insertions(+), 30 deletions(-) diff --git a/data/src/cache.js b/data/src/cache.js index 9e6cbd2..d60d269 100644 --- a/data/src/cache.js +++ b/data/src/cache.js @@ -20,8 +20,72 @@ class EJS_Download { this.EJS = EJS; } + /** + * Handles downloading non-http(s) URLs (blob:, data:, file:, etc.) + * @param {string} url - The non-http(s) URL to fetch + * @param {string} type - The type of the file + * @param {string} method - The HTTP method (HEAD returns empty, others fetch) + * @param {string} responseType - The response type ("arraybuffer" or "text") + * @returns {Promise} - The fetched data as a cache item, or null for HEAD requests + */ + async handleNonHttpUrl(url, type, method = "GET", responseType = "arraybuffer") { + console.log("[EJS Download] Handling non-http(s) URL:", url); + + if (method === "HEAD") { + // HEAD requests just return empty for non-http URLs + return null; + } + + try { + let res = await fetch(url); + let data; + + if (responseType === "arraybuffer" || !responseType) { + data = await res.arrayBuffer(); + data = new Uint8Array(data); + } else { + data = await res.text(); + // Try to parse as JSON if it looks like JSON + try { data = JSON.parse(data) } catch(e) {} + } + + // Clean up blob URLs to free memory + if (url.startsWith("blob:")) { + URL.revokeObjectURL(url); + } + + // Create a cache item for consistency + const filename = url.split("/").pop() || "downloaded.bin"; + const now = Date.now(); + + // Ensure data is Uint8Array for file item + let fileData; + if (data instanceof Uint8Array) { + fileData = data; + } else if (typeof data === "string") { + const encoder = new TextEncoder(); + fileData = encoder.encode(data); + } else if (data instanceof ArrayBuffer) { + fileData = new Uint8Array(data); + } else { + const encoder = new TextEncoder(); + fileData = encoder.encode(String(data)); + } + + const files = [new EJS_FileItem(filename, fileData)]; + const key = this.storageCache ? this.storageCache.generateCacheKey(fileData) : "temp-" + Date.now(); + + // Don't cache non-http URLs (they're typically temporary or special) + return new EJS_CacheItem(key, files, now, type, responseType, filename, url, null); + } catch(e) { + console.error("[EJS Download] Failed to fetch non-http URL:", url, e); + throw new Error(`Failed to fetch non-http URL: ${e}`); + } + } + /** * Downloads a file from the given URL with the specified options. + * Automatically detects and handles both http(s) and non-http(s) URLs (blob:, data:, etc.) * @param {string} url - The URL to download the file from. * @param {string} type - The type of the file to download (e.g. "ROM", "CORE", "BIOS", etc). * @param {string} method - The HTTP method to use (default is "GET"). @@ -43,6 +107,17 @@ class EJS_Download { console.log("[EJS Download] Downloading " + responseType + " file: " + url + cacheActiveText); return new Promise(async (resolve, reject) => { try { + // Check if this is a non-http(s) URL (blob:, data:, file:, etc.) + let urlObj; + try { urlObj = new URL(url) } catch(e) {}; + + if (urlObj && !["http:", "https:"].includes(urlObj.protocol)) { + // Handle non-http(s) URLs directly + const result = await this.handleNonHttpUrl(url, type, method, responseType); + resolve(result); + return; + } + // Use the provided storageCache or create a temporary one if (!this.storageCache) { console.warn("No storageCache provided to EJS_Download, downloads will not be cached"); diff --git a/data/src/emulator.js b/data/src/emulator.js index 0b8e529..277e893 100644 --- a/data/src/emulator.js +++ b/data/src/emulator.js @@ -91,6 +91,8 @@ class EmulatorJS { } /** * Downloads a file from the specified path. + * Helper method that delegates to EJS_Download system for all URL-based downloads. + * Handles direct data objects (ArrayBuffer, Uint8Array, Blob) and constructs proper paths. * @param {*} path The path to the file to download. * @param {*} type The expected type of the file. * @param {*} progress A callback function for progress updates. @@ -103,7 +105,7 @@ class EmulatorJS { downloadFile(path, type, progress, notWithPath, opts, forceExtract = false, dontCache = false) { if (this.debug) console.log("[EJS " + type + "] Downloading " + path); return new Promise(async (resolve) => { - // Handle data types (ArrayBuffer, Uint8Array, Blob) + // Handle direct data objects (ArrayBuffer, Uint8Array, Blob) const data = this.toData(path); if (data) { data.then((game) => { @@ -116,40 +118,14 @@ class EmulatorJS { return; } - // Construct the full path + // Construct the full path/URL const basePath = notWithPath ? "" : this.config.dataPath; let fullPath = basePath + path; if (!notWithPath && this.config.filePaths && typeof this.config.filePaths[path.split("/").pop()] === "string") { fullPath = this.config.filePaths[path.split("/").pop()]; } - // Check if it's a URL - let url; - try { url = new URL(fullPath) } catch(e) {}; - - // Handle non-http(s) URLs (blob:, data:, etc.) - if (url && !["http:", "https:"].includes(url.protocol)) { - if (opts.method === "HEAD") { - resolve({ headers: {} }); - return; - } - try { - let res = await fetch(fullPath); - if ((opts.type && opts.type.toLowerCase() === "arraybuffer") || !opts.type) { - res = await res.arrayBuffer(); - } else { - res = await res.text(); - try { res = JSON.parse(res) } catch(e) {} - } - if (fullPath.startsWith("blob:")) URL.revokeObjectURL(fullPath); - resolve({ data: res, headers: {} }); - } catch(e) { - resolve(-1); - } - return; - } - - // Use EJS_Download for http(s) downloads + // Delegate all URL downloads (http, https, blob, data, etc.) to EJS_Download try { const onProgress = progress instanceof Function ? (status, percentage, loaded, total) => { if (status === "downloading") { @@ -183,8 +159,14 @@ class EmulatorJS { dontCache ); + // Handle HEAD requests (returns null) + if (!cacheItem) { + resolve({ headers: {} }); + return; + } + // Extract the data from the cache item - if (cacheItem && cacheItem.files && cacheItem.files.length > 0) { + if (cacheItem.files && cacheItem.files.length > 0) { // If there are files, return the entire cache item // so the caller can access all extracted files if (cacheItem.files.length > 0) {