@apr144/generic-filehandle
Version:
uniform interface for accessing binary data from local files, remote HTTP resources, and browser Blob data
178 lines • 8.33 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const buffer_1 = require("buffer");
const base_64_1 = require("base-64");
class RemoteFile {
getBufferFromResponse(response) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof response.buffer === 'function') {
return response.buffer();
}
else if (typeof response.arrayBuffer === 'function') {
const resp = yield response.arrayBuffer();
return buffer_1.Buffer.from(resp);
}
else {
throw new TypeError('invalid HTTP response object, has no buffer method, and no arrayBuffer method');
}
});
}
constructor(source, opts = {}) {
this.auth = {};
this.baseOverrides = {};
this.url = source;
this.auth = opts.auth || {};
const fetch = opts.fetch || globalThis.fetch.bind(globalThis);
if (!fetch) {
throw new TypeError(`no fetch function supplied, and none found in global environment`);
}
if (opts.overrides) {
this.baseOverrides = opts.overrides;
}
this.fetchImplementation = fetch;
}
fetch(input, init) {
return __awaiter(this, void 0, void 0, function* () {
let response;
try {
init = init || {};
if (this.auth && this.auth.user && this.auth.password) {
init.credentials = 'include';
init.headers = {};
init.headers.Authorization = `Basic ${(0, base_64_1.encode)(this.auth.user + ':' + this.auth.password)}`;
}
response = yield this.fetchImplementation(input, Object.assign({}, init));
}
catch (e) {
if (`${e}`.includes('Failed to fetch')) {
// refetch to to help work around a chrome bug (discussed in
// generic-filehandle issue #72) in which the chrome cache returns a
// CORS error for content in its cache. see also
// https://github.com/GMOD/jbrowse-components/pull/1511
console.warn(`generic-filehandle: refetching ${input} to attempt to work around chrome CORS header caching bug`);
init = init || {};
if (this.auth && this.auth.user && this.auth.password) {
init.credentials = 'include';
init.headers = {};
init.headers.Authorization = `Basic ${(0, base_64_1.encode)(this.auth.user + ':' + this.auth.password)}`;
}
response = yield this.fetchImplementation(input, Object.assign(Object.assign({}, init), {
// headers: headers,
cache: 'reload' }));
}
else {
throw e;
}
}
return response;
});
}
read(buffer, offset = 0, length, position = 0, opts = {}) {
return __awaiter(this, void 0, void 0, function* () {
const { headers = {}, signal, overrides = {} } = opts;
if (length < Infinity) {
headers.range = `bytes=${position}-${position + length}`;
}
else if (length === Infinity && position !== 0) {
headers.range = `bytes=${position}-`;
}
const args = Object.assign(Object.assign(Object.assign({}, this.baseOverrides), overrides), { headers: Object.assign(Object.assign(Object.assign({}, headers), overrides.headers), this.baseOverrides.headers), method: 'GET', redirect: 'follow', mode: 'cors', signal });
if (this.auth && this.auth.user && this.auth.password) {
headers.Authorization = `Basic ${(0, base_64_1.encode)(this.auth.user + ':' + this.auth.password)}`;
args.credentials = 'include';
}
const response = yield this.fetch(this.url, args);
if (!response.ok) {
throw new Error(`HTTP ${response.status} ${response.statusText} ${this.url}`);
}
if ((response.status === 200 && position === 0) ||
response.status === 206) {
const responseData = yield this.getBufferFromResponse(response);
const bytesCopied = responseData.copy(buffer, offset, 0, Math.min(length, responseData.length));
// try to parse out the size of the remote file
const res = response.headers.get('content-range');
const sizeMatch = /\/(\d+)$/.exec(res || '');
if (sizeMatch && sizeMatch[1]) {
this._stat = { size: parseInt(sizeMatch[1], 10) };
}
return { bytesRead: bytesCopied, buffer };
}
if (response.status === 200) {
throw new Error('${this.url} fetch returned status 200, expected 206');
}
if (response.status === 404) {
return { bytesRead: 0, buffer };
}
// TODO: try harder here to gather more information about what the problem is
throw new Error(`HTTP ${response.status} fetching ${this.url}`);
});
}
readFile(options = {}) {
return __awaiter(this, void 0, void 0, function* () {
let encoding;
let opts;
if (typeof options === 'string') {
encoding = options;
opts = {};
}
else {
encoding = options.encoding;
opts = options;
delete opts.encoding;
}
const { headers = {}, signal, overrides = {} } = opts;
const args = Object.assign(Object.assign({ headers, method: 'GET', redirect: 'follow', mode: 'cors', signal }, this.baseOverrides), overrides);
if (this.auth && this.auth.user && this.auth.password) {
headers.Authorization = `Basic ${(0, base_64_1.encode)(this.auth.user + ':' + this.auth.password)}`;
args.credentials = 'include';
}
const response = yield this.fetch(this.url, args);
if (!response) {
throw new Error('generic-filehandle failed to fetch');
}
if (response.status === 404) {
return buffer_1.Buffer.alloc(1);
}
if (response.status !== 200) {
throw Object.assign(new Error(`HTTP ${response.status} fetching ${this.url}`), {
status: response.status,
});
}
if (encoding === 'utf8') {
return response.text();
}
if (encoding) {
throw new Error(`unsupported encoding: ${encoding}`);
}
return this.getBufferFromResponse(response);
});
}
stat() {
return __awaiter(this, void 0, void 0, function* () {
if (!this._stat) {
const buf = buffer_1.Buffer.allocUnsafe(10);
yield this.read(buf, 0, 10, 0);
if (!this._stat) {
throw new Error(`unable to determine size of file at ${this.url}`);
}
}
return this._stat;
});
}
close() {
return __awaiter(this, void 0, void 0, function* () {
return;
});
}
}
exports.default = RemoteFile;
//# sourceMappingURL=remoteFile.js.map