Skip to content

Commit

Permalink
Merge remote-tracking branch 'wmhilton/window.fetch'
Browse files Browse the repository at this point in the history
  • Loading branch information
John Vilk committed Aug 8, 2017
2 parents f837c7d + 70f8b2e commit b3684b9
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 30 deletions.
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"node": ">= 0.10"
},
"bin": {
"make_xhrfs_index": "./dist/scripts/make_xhrfs_index.js"
"make_http_index": "./dist/scripts/make_http_index.js"
},
"devDependencies": {
"@types/archiver": "^2.0.0",
Expand All @@ -32,6 +32,7 @@
"@types/express": "^4.0.36",
"@types/filesystem": "0.0.28",
"@types/mocha": "^2.2.41",
"@types/isomorphic-fetch": "^0.0.34",
"@types/node": "~7.0",
"@types/rimraf": "~0.0.28",
"archiver": "^2.0.0",
Expand Down Expand Up @@ -100,9 +101,9 @@
"script:make_fixture_loader": "node build/scripts/make_fixture_loader.js",
"script:make_test_launcher": "node build/scripts/make_test_launcher.js",
"script:make_zip_fixtures": "node build/scripts/make_zip_fixtures",
"script:make_xhrfs_index": "node build/scripts/make_xhrfs_index.js test/fixtures/xhrfs/listings.json",
"script:make_http_index": "node build/scripts/make_http_index.js test/fixtures/xhrfs/listings.json",
"test:karma": "karma start karma.config.js",
"test:prepare": "npm-run-all build:scripts script:make_fixture_loader script:make_test_launcher test:build script:make_zip_fixtures script:make_xhrfs_index",
"test:prepare": "npm-run-all build:scripts script:make_fixture_loader script:make_test_launcher test:build script:make_zip_fixtures script:make_http_index",
"test": "npm-run-all test:prepare test:karma",
"watch-test": "npm-run-all test:prepare --parallel watch:scripts test:watch test:karma",
"prepublish": "npm run dist",
Expand Down
File renamed without changes.
80 changes: 64 additions & 16 deletions src/backend/XmlHttpRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {copyingSlice} from '../core/util';
import {File} from '../core/file';
import Stats from '../core/node_fs_stats';
import {NoSyncFile} from '../generic/preload_file';
import {asyncDownloadFile, syncDownloadFile, getFileSizeAsync, getFileSizeSync} from '../generic/xhr';
import {xhrIsAvailable, asyncDownloadFile, syncDownloadFile, getFileSizeAsync, getFileSizeSync} from '../generic/xhr';
import {fetchIsAvailable, fetchFileAsync, fetchFileSizeAsync} from '../generic/fetch';
import {FileIndex, isFileInode, isDirInode} from '../generic/file_index';

/**
Expand All @@ -26,23 +27,42 @@ function tryToString(buff: Buffer, encoding: string, cb: BFSCallback<string>) {
* Configuration options for an XmlHttpRequest file system.
*/
export interface XmlHttpRequestOptions {
// URL to a file index as a JSON file or the file index object itself, generated with the make_xhrfs_index script.
// URL to a file index as a JSON file or the file index object itself, generated with the make_http_index script.
// Defaults to `index.json`.
index?: string | object;
// Used as the URL prefix for fetched files.
// Default: Fetch files relative to the index.
baseUrl?: string;
// Whether to prefer XmlHttpRequest or fetch for async operations if both are available.
// Default: false
preferXHR?: boolean;
}

interface AsyncDownloadFileMethod {
(p: string, type: 'buffer', cb: BFSCallback<Buffer>): void;
(p: string, type: 'json', cb: BFSCallback<any>): void;
(p: string, type: string, cb: BFSCallback<any>): void;
}

interface SyncDownloadFileMethod {
(p: string, type: 'buffer'): Buffer;
(p: string, type: 'json'): any;
(p: string, type: string): any;
}

function syncNotAvailableError(): never {
throw new ApiError(ErrorCode.ENOTSUP, `Synchronous HTTP download methods are not available in this environment.`);
}

/**
* A simple filesystem backed by XMLHttpRequests. You must create a directory listing using the
* `make_xhrfs_index` tool provided by BrowserFS.
* A simple filesystem backed by HTTP downloads. You must create a directory listing using the
* `make_http_index` tool provided by BrowserFS.
*
* If you install BrowserFS globally with `npm i -g browserfs`, you can generate a listing by
* running `make_xhrfs_index` in your terminal in the directory you would like to index:
* running `make_http_index` in your terminal in the directory you would like to index:
*
* ```
* make_xhrfs_index > index.json
* make_http_index > index.json
* ```
*
* Listings objects look like the following:
Expand All @@ -69,12 +89,17 @@ export default class XmlHttpRequest extends BaseFileSystem implements FileSystem
index: {
type: ["string", "object"],
optional: true,
description: "URL to a file index as a JSON file or the file index object itself, generated with the make_xhrfs_index script. Defaults to `index.json`."
description: "URL to a file index as a JSON file or the file index object itself, generated with the make_http_index script. Defaults to `index.json`."
},
baseUrl: {
type: "string",
optional: true,
description: "Used as the URL prefix for fetched files. Default: Fetch files relative to the index."
},
preferXHR: {
type: "boolean",
optional: true,
description: "Whether to prefer XmlHttpRequest or fetch for async operations if both are available. Default: false"
}
};

Expand All @@ -97,21 +122,42 @@ export default class XmlHttpRequest extends BaseFileSystem implements FileSystem
cb(null, new XmlHttpRequest(opts.index, opts.baseUrl));
}
}

public static isAvailable(): boolean {
return typeof(XMLHttpRequest) !== "undefined" && XMLHttpRequest !== null;
return xhrIsAvailable || fetchIsAvailable;
}

public readonly prefixUrl: string;
private _index: FileIndex<{}>;
private _requestFileAsyncInternal: AsyncDownloadFileMethod;
private _requestFileSizeAsyncInternal: (p: string, cb: BFSCallback<number>) => void;
private _requestFileSyncInternal: SyncDownloadFileMethod;
private _requestFileSizeSyncInternal: (p: string) => number;

private constructor(index: object, prefixUrl: string = '') {
private constructor(index: object, prefixUrl: string = '', preferXHR: boolean = false) {
super();
// prefix_url must end in a directory separator.
if (prefixUrl.length > 0 && prefixUrl.charAt(prefixUrl.length - 1) !== '/') {
prefixUrl = prefixUrl + '/';
}
this.prefixUrl = prefixUrl;
this._index = FileIndex.fromListing(index);

if (fetchIsAvailable && (!preferXHR || !xhrIsAvailable)) {
this._requestFileAsyncInternal = fetchFileAsync;
this._requestFileSizeAsyncInternal = fetchFileSizeAsync;
} else {
this._requestFileAsyncInternal = asyncDownloadFile;
this._requestFileSizeAsyncInternal = getFileSizeAsync;
}

if (xhrIsAvailable) {
this._requestFileSyncInternal = syncDownloadFile;
this._requestFileSizeSyncInternal = getFileSizeSync;
} else {
this._requestFileSyncInternal = syncNotAvailableError;
this._requestFileSizeSyncInternal = syncNotAvailableError;
}
}

public empty(): void {
Expand Down Expand Up @@ -143,11 +189,12 @@ export default class XmlHttpRequest extends BaseFileSystem implements FileSystem
}

public supportsSynch(): boolean {
return true;
// Synchronous operations are only available via the XHR interface for now.
return xhrIsAvailable;
}

/**
* Special XHR function: Preload the given file into the index.
* Special HTTPFS function: Preload the given file into the index.
* @param [String] path
* @param [BrowserFS.Buffer] buffer
*/
Expand Down Expand Up @@ -358,7 +405,7 @@ export default class XmlHttpRequest extends BaseFileSystem implements FileSystem
}
}

private getXhrPath(filePath: string): string {
private _getHTTPPath(filePath: string): string {
if (filePath.charAt(0) === '/') {
filePath = filePath.slice(1);
}
Expand All @@ -372,7 +419,7 @@ export default class XmlHttpRequest extends BaseFileSystem implements FileSystem
private _requestFileAsync(p: string, type: 'json', cb: BFSCallback<any>): void;
private _requestFileAsync(p: string, type: string, cb: BFSCallback<any>): void;
private _requestFileAsync(p: string, type: string, cb: BFSCallback<any>): void {
asyncDownloadFile(this.getXhrPath(p), type, cb);
this._requestFileAsyncInternal(this._getHTTPPath(p), type, cb);
}

/**
Expand All @@ -382,16 +429,17 @@ export default class XmlHttpRequest extends BaseFileSystem implements FileSystem
private _requestFileSync(p: string, type: 'json'): any;
private _requestFileSync(p: string, type: string): any;
private _requestFileSync(p: string, type: string): any {
return syncDownloadFile(this.getXhrPath(p), type);
return this._requestFileSyncInternal(this._getHTTPPath(p), type);
}

/**
* Only requests the HEAD content, for the file size.
*/
private _requestFileSizeAsync(path: string, cb: BFSCallback<number>): void {
getFileSizeAsync(this.getXhrPath(path), cb);
this._requestFileSizeAsyncInternal(this._getHTTPPath(path), cb);
}

private _requestFileSizeSync(path: string): number {
return getFileSizeSync(this.getXhrPath(path));
return this._requestFileSizeSyncInternal(this._getHTTPPath(path));
}
}
66 changes: 66 additions & 0 deletions src/generic/fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Contains utility methods using 'fetch'.
*/

import {ApiError, ErrorCode} from '../core/api_error';
import {BFSCallback} from '../core/file_system';

export const fetchIsAvailable = (typeof(fetch) !== "undefined" && fetch !== null);

/**
* Asynchronously download a file as a buffer or a JSON object.
* Note that the third function signature with a non-specialized type is
* invalid, but TypeScript requires it when you specialize string arguments to
* constants.
* @hidden
*/
export function fetchFileAsync(p: string, type: 'buffer', cb: BFSCallback<Buffer>): void;
export function fetchFileAsync(p: string, type: 'json', cb: BFSCallback<any>): void;
export function fetchFileAsync(p: string, type: string, cb: BFSCallback<any>): void;
export function fetchFileAsync(p: string, type: string, cb: BFSCallback<any>): void {
let request;
try {
request = fetch(p);
} catch (e) {
// XXX: fetch will throw a TypeError if the URL has credentials in it
return cb(new ApiError(ErrorCode.EINVAL, e.message));
}
request
.then((res) => {
if (!res.ok) {
return cb(new ApiError(ErrorCode.EIO, `fetch error: response returned code ${res.status}`));
} else {
switch (type) {
case 'buffer':
res.arrayBuffer()
.then((buf) => cb(null, Buffer.from(buf)))
.catch((err) => cb(new ApiError(ErrorCode.EIO, err.message)));
break;
case 'json':
res.json()
.then((json) => cb(null, json))
.catch((err) => cb(new ApiError(ErrorCode.EIO, err.message)));
break;
default:
cb(new ApiError(ErrorCode.EINVAL, "Invalid download type: " + type));
}
}
})
.catch((err) => cb(new ApiError(ErrorCode.EIO, err.message)));
}

/**
* Asynchronously retrieves the size of the given file in bytes.
* @hidden
*/
export function fetchFileSizeAsync(p: string, cb: BFSCallback<number>): void {
fetch(p, { method: 'HEAD' })
.then((res) => {
if (!res.ok) {
return cb(new ApiError(ErrorCode.EIO, `fetch HEAD error: response returned code ${res.status}`));
} else {
return cb(null, parseInt(res.headers.get('Content-Length') || '-1', 10));
}
})
.catch((err) => cb(new ApiError(ErrorCode.EIO, err.message)));
}
10 changes: 6 additions & 4 deletions src/generic/xhr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {isIE, emptyBuffer} from '../core/util';
import {ApiError, ErrorCode} from '../core/api_error';
import {BFSCallback} from '../core/file_system';

export const xhrIsAvailable = (typeof(XMLHttpRequest) !== "undefined" && XMLHttpRequest !== null);

/**
* @hidden
*/
Expand Down Expand Up @@ -50,7 +52,7 @@ function asyncDownloadFileModern(p: string, type: string, cb: BFSCallback<any>):
}
}
} else {
return cb(new ApiError(req.status, "XHR error."));
return cb(new ApiError(ErrorCode.EIO, `XHR error: response returned code ${req.status}`));
}
}
};
Expand Down Expand Up @@ -93,7 +95,7 @@ function syncDownloadFileModern(p: string, type: string): any {
return;
}
} else {
err = new ApiError(req.status, "XHR error.");
err = new ApiError(ErrorCode.EIO, `XHR error: response returned code ${req.status}`);
return;
}
}
Expand Down Expand Up @@ -140,7 +142,7 @@ function syncDownloadFileIE10(p: string, type: string): any {
break;
}
} else {
err = new ApiError(req.status, "XHR error.");
err = new ApiError(ErrorCode.EIO, `XHR error: response returned code ${req.status}`);
}
}
};
Expand All @@ -167,7 +169,7 @@ function getFileSize(async: boolean, p: string, cb: BFSCallback<number>): void {
return cb(new ApiError(ErrorCode.EIO, "XHR HEAD error: Could not read content-length."));
}
} else {
return cb(new ApiError(req.status, "XHR HEAD error."));
return cb(new ApiError(ErrorCode.EIO, `XHR HEAD error: response returned code ${req.status}`));
}
}
};
Expand Down
21 changes: 14 additions & 7 deletions test/harness/factories/xhrfs_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@ export default function XHRFSFactory(cb: (name: string, objs: FileSystem[]) => v
if (XmlHttpRequestFS.isAvailable()) {
XmlHttpRequestFS.Create({
index: 'test/fixtures/xhrfs/listings.json',
baseUrl: '../'
}, (e, fs) => {
if (e) {
cb('XmlHttpRequest', []);
} else {
cb('XmlHttpRequest', [fs]);
}
baseUrl: '../',
preferXHR: true
}, (e1, xhrFS) => {
XmlHttpRequestFS.Create({
index: 'test/fixtures/xhrfs/listings.json',
baseUrl: '../',
preferXHR: false
}, (e2, fetchFS) => {
if (e1 || e2) {
throw e1 || e2;
} else {
cb('XmlHttpRequest', [xhrFS, fetchFS]);
}
});
});
} else {
cb('XmlHttpRequest', []);
Expand Down
4 changes: 4 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@
version "9.1.9"
resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-9.1.9.tgz#ed6336955eaf233b75eb7923b9b1f373d045ef01"

"@types/isomorphic-fetch@^0.0.34":
version "0.0.34"
resolved "https://registry.yarnpkg.com/@types/isomorphic-fetch/-/isomorphic-fetch-0.0.34.tgz#3c3483e606c041378438e951464f00e4e60706d6"

"@types/lodash@^4.14.37":
version "4.14.71"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.71.tgz#0dc383f78981216ac76e2f2c3afd998e0450e4c1"
Expand Down

0 comments on commit b3684b9

Please sign in to comment.