Enhance EJS_Download to handle non-http(s) URLs and delegate URL-based downloads in EmulatorJS

This commit is contained in:
Michael Green 2026-01-17 23:13:16 +11:00
parent 9408a61c35
commit 2abe5243d0
2 changed files with 87 additions and 30 deletions

View File

@ -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<EJS_CacheItem|null>} - 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");

View File

@ -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) {