-
Notifications
You must be signed in to change notification settings - Fork 196
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
19 changed files
with
436 additions
and
414 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// loaders.gl, MIT license | ||
|
||
import {ReadableFile} from './file'; | ||
|
||
export class BlobFile implements ReadableFile { | ||
readonly handle: Blob; | ||
readonly size: number; | ||
readonly url: string; | ||
|
||
constructor(blob: Blob | File) { | ||
this.handle = blob; | ||
this.size = blob.size; | ||
this.url = (blob as File).name || ''; | ||
} | ||
|
||
async close() {} | ||
|
||
async stat() { | ||
return { | ||
size: this.handle.size, | ||
isDirectory: false | ||
}; | ||
} | ||
|
||
async read(start: number, length: number): Promise<ArrayBuffer> { | ||
const arrayBuffer = await this.handle.slice(start, start + length).arrayBuffer(); | ||
return arrayBuffer; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
export type Stat = { | ||
size: number; | ||
isDirectory: boolean; | ||
}; | ||
|
||
export interface ReadableFile { | ||
/** The underlying file handle (Blob, Node.js file descriptor etc) */ | ||
handle: unknown; | ||
/** Length of file in bytes, if available */ | ||
size: number; | ||
/** Read data */ | ||
read(start?: number, end?: number): Promise<ArrayBuffer>; | ||
/** Read data */ | ||
fetchRange?(offset: number, length: number, signal?: AbortSignal): Promise<Response>; | ||
/** Get information about file */ | ||
stat?(): Promise<Stat>; | ||
/** Close the file */ | ||
close(): Promise<void>; | ||
} | ||
|
||
export interface WritableFile { | ||
handle: unknown; | ||
/** Write to file. The number of bytes written will be returned */ | ||
write: (arrayBuffer: ArrayBuffer, offset?: number, length?: number) => Promise<number>; | ||
/** Get information about the file */ | ||
stat?(): Promise<Stat>; | ||
/** Close the file */ | ||
close(): Promise<void>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
// loaders.gl, MIT license | ||
|
||
import {ReadableFile, Stat} from './file'; | ||
|
||
export class HttpFile implements ReadableFile { | ||
readonly handle: string; | ||
readonly size: number = 0; | ||
readonly url: string; | ||
|
||
constructor(url: string) { | ||
this.handle = url; | ||
this.url = url; | ||
} | ||
|
||
async close(): Promise<void> {} | ||
|
||
async stat(): Promise<Stat> { | ||
const response = await fetch(this.handle, {method: 'HEAD'}); | ||
if (!response.ok) { | ||
throw new Error(`Failed to fetch HEAD ${this.handle}`); | ||
} | ||
return { | ||
size: parseInt(response.headers.get('Content-Length') || '0'), | ||
isDirectory: false | ||
}; | ||
} | ||
|
||
async read(offset: number, length: number): Promise<ArrayBuffer> { | ||
const response = await this.fetchRange(offset, length); | ||
const arrayBuffer = await response.arrayBuffer(); | ||
return arrayBuffer; | ||
} | ||
|
||
/** | ||
* | ||
* @param offset | ||
* @param length | ||
* @param signal | ||
* @returns | ||
* @see https://github.com/protomaps/PMTiles | ||
*/ | ||
async fetchRange(offset: number, length: number, signal?: AbortSignal): Promise<Response> { | ||
let controller: AbortController | undefined; | ||
if (!signal) { | ||
// ToDO why is it so important to abort in case 200? | ||
// TODO check this works or assert 206 | ||
controller = new AbortController(); | ||
signal = controller.signal; | ||
} | ||
|
||
const url = this.handle; | ||
let response = await fetch(url, { | ||
signal, | ||
headers: {Range: `bytes=${offset}-${offset + length - 1}`} | ||
}); | ||
|
||
switch (response.status) { | ||
case 206: // Partial Content success | ||
// This is the expected success code for a range request | ||
break; | ||
|
||
case 200: | ||
// some well-behaved backends, e.g. DigitalOcean CDN, respond with 200 instead of 206 | ||
// but we also need to detect no support for Byte Serving which is returning the whole file | ||
const content_length = response.headers.get('Content-Length'); | ||
if (!content_length || Number(content_length) > length) { | ||
if (controller) { | ||
controller.abort(); | ||
} | ||
throw Error( | ||
'content-length header missing or exceeding request. Server must support HTTP Byte Serving.' | ||
); | ||
} | ||
|
||
case 416: // "Range Not Satisfiable" | ||
// some HTTP servers don't accept ranges beyond the end of the resource. | ||
// Retry with the exact length | ||
// TODO: can return 416 with offset > 0 if content changed, which will have a blank etag. | ||
// See https://github.com/protomaps/PMTiles/issues/90 | ||
if (offset === 0) { | ||
const content_range = response.headers.get('Content-Range'); | ||
if (!content_range || !content_range.startsWith('bytes *')) { | ||
throw Error('Missing content-length on 416 response'); | ||
} | ||
const actual_length = Number(content_range.substr(8)); | ||
response = await fetch(this.url, { | ||
signal, | ||
headers: {Range: `bytes=0-${actual_length - 1}`} | ||
}); | ||
} | ||
break; | ||
|
||
default: | ||
if (response.status >= 300) { | ||
throw Error(`Bad response code: ${response.status}`); | ||
} | ||
} | ||
|
||
return response; | ||
// const data = await response.arrayBuffer(); | ||
// return { | ||
// data, | ||
// etag: response.headers.get('ETag') || undefined, | ||
// cacheControl: response.headers.get('Cache-Control') || undefined, | ||
// expires: response.headers.get('Expires') || undefined | ||
// }; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
// loaders.gl, MIT license | ||
|
||
import {isBrowser} from '../env-utils/globals'; | ||
import {ReadableFile, WritableFile, Stat} from './file'; | ||
|
||
const NOT_IMPLEMENTED = new Error('Not implemented'); | ||
|
||
/** This class is a facade that gets replaced with an actual NodeFile instance */ | ||
export class NodeFileFacade implements ReadableFile, WritableFile { | ||
/** The underlying file handle (Blob, Node.js file descriptor etc) */ | ||
handle: unknown; | ||
/** Length of file in bytes, if available */ | ||
size: number = 0; | ||
|
||
constructor(options) { | ||
// Return the actual implementation instance | ||
if (globalThis.loaders?.NodeFile) { | ||
return new globalThis.loaders.NodeFile(options); | ||
} | ||
if (isBrowser) { | ||
throw new Error("Can't instantiate NodeFile in browser."); | ||
} | ||
throw new Error("Can't instantiate NodeFile. Make sure to import @loaders.gl/polyfills first."); | ||
} | ||
/** Read data */ | ||
async read(start?: number, end?: number): Promise<ArrayBuffer> { | ||
throw NOT_IMPLEMENTED; | ||
} | ||
/** Write to file. The number of bytes written will be returned */ | ||
async write(arrayBuffer: ArrayBuffer, offset?: number, length?: number): Promise<number> { | ||
throw NOT_IMPLEMENTED; | ||
} | ||
/** Get information about file */ | ||
async stat(): Promise<Stat> { | ||
throw NOT_IMPLEMENTED; | ||
} | ||
/** Close the file */ | ||
async close(): Promise<void> {} | ||
} |
File renamed without changes.
Oops, something went wrong.