@bitblit/asyncglk
Version:
A Typescript Glk library
140 lines (136 loc) • 4.54 kB
JavaScript
/*
Download/upload provider
========================
Copyright (c) 2024 Dannii Willis
MIT licenced
https://github.com/curiousdannii/asyncglk
*/
import { NullProvider } from './common.js';
import { parse_base64, read_response } from '../../common/file.js';
import { utf8decoder } from '../../common/misc.js';
export class DownloadProvider {
browseable = false;
next = new NullProvider();
options;
store = new Map();
constructor(options) {
this.options = options;
}
async download(url, progress_callback) {
const data = await fetch_storyfile(this.options, url, progress_callback);
const path = url_to_path(url);
this.store.set(path, data);
return path;
}
async upload(file) {
const data = await read_uploaded_file(file);
const path = '/upload/' + file.name;
this.store.set(path, data);
return path;
}
async delete(path) {
if (this.store.has(path)) {
this.store.delete(path);
}
else {
return this.next.delete(path);
}
}
async exists(path) {
if (this.store.has(path)) {
return true;
}
else {
return this.next.exists(path);
}
}
async read(path) {
if (this.store.has(path)) {
return this.store.get(path);
}
else {
return this.next.read(path);
}
// TODO: try downloading a sibling file
}
write(files) {
return this.next.write(files);
}
}
/** Fetch a storyfile, using the proxy if necessary, and handling JSified stories */
export async function fetch_storyfile(options, url, progress_callback) {
// Handle a relative URL
const story_url = new URL(url, document.URL);
const story_domain = story_url.hostname;
const same_protocol = story_url.protocol === document.location.protocol;
const same_domain = story_domain === document.location.hostname;
const proxy_url = `${options.proxy_url}?url=${story_url}`;
let response;
// Load an embedded storyfile
if (story_url.protocol === 'embedded:') {
const data = document.getElementById(story_url.pathname).text;
return parse_base64(data);
}
// Only directly access files same origin files or those from the list of reliable domains
let direct_access = (same_protocol && same_domain) || story_url.protocol === 'data:';
if (!direct_access) {
for (const domain of options.direct_domains) {
if (story_domain.endsWith(domain)) {
// all direct domains require HTTPS
story_url.protocol = 'https:';
direct_access = true;
break;
}
}
}
if (direct_access) {
try {
response = await fetch('' + story_url);
}
// We can't specifically detect CORS errors but that's probably what happened
catch {
throw new Error('Failed to fetch storyfile (possible CORS error)');
}
}
// Otherwise use the proxy
else {
if (options.use_proxy) {
response = await fetch(proxy_url);
}
else {
throw new Error('Storyfile not in list of direct domains and proxy disabled');
}
}
if (!response.ok) {
throw new Error(`Could not fetch storyfile, got ${response.status}`);
}
// It would be nice to check here if the file was compressed, but we can only read the Content-Encoding header for same domain files
const data = await read_response(response, progress_callback);
// Handle JSified stories
if (url.endsWith('.js')) {
const text = utf8decoder.decode(data);
const matched = /processBase64Zcode\(['"]([a-zA-Z0-9+/=]+)['"]\)/.exec(text);
if (!matched) {
throw new Error('Abnormal JSified story');
}
return parse_base64(matched[1]);
}
return data;
}
/** Read an uploaded file and return it as a Uint8Array */
export function read_uploaded_file(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = () => reject(reader.error);
reader.onload = () => resolve(new Uint8Array(reader.result));
reader.readAsArrayBuffer(file);
});
}
function url_to_path(url) {
if (url.startsWith('https:')) {
return '/download/https/' + url.substring(8);
}
else {
return '/download/http/' + url.substring(7);
}
}