适用于分块的浏览器的客户端虚拟文件系统
A client-side virtual file system for browsers that works with chunking
我正在尝试移植桌面应用程序的某些部分,以便能够 运行 在浏览器(客户端)中。我需要一种虚拟文件系统,我可以在其中读取和写入文件(二进制数据)。据我所知,唯一可以广泛跨浏览器使用的选项之一是 IndexedDB。但是,我有点疏远试图找到读取或写入更大文件的示例。 API 似乎只支持 passing/obtaining 整个文件内容 to/from 数据库(blob 或字节数组)。
我试图找到的是我可以在其中连续“传输”数据 to/from 虚拟文件系统的东西,类似于您在任何其他非浏览器上的操作方式应用。例如。 (伪代码)
val in = new FileInputStream(someURLorPath)
val chunkSize = 4096
val buf = new Array[Byte](chunkSize)
while (in.hasRemaining) {
val sz = min(chunkSize, in.remaining)
in.read(buf, 0, sz)
processSome(buf, 0, sz)
...
)
in.close()
我理解同步 API 是浏览器的问题;如果 read
是一个异步方法,那也可以。但我想通过文件 - 这可能是巨大的,例如几个 100 MB - 逐块。块大小无关紧要。这既适用于阅读也适用于写作。
随机访问(能够在虚拟文件中寻找某个位置)将是一个优势,但不是强制性的。
我的一个想法是一个存储=一个虚拟文件,然后键是块索引?有点像 cursor example on MDN,但每条记录都是一个固定大小的 blob 或数组。那有意义吗?有没有更好的API或方法?
似乎 Streams 在概念上就是我正在寻找的 API,但我不知道如何“流式传输 to/from” 虚拟文件系统,例如IndexedDB.
假设您希望能够透明地使用 最初 本地缓存的远程资源(和 一致),您可以抽象fetch
(有 Range:
个请求)和 IndexedDB
.
顺便说一句,你真的很想为此使用 TypeScript,因为在纯 JavaScript 中使用 Promise<T>
是一个 PITA。
one could say either read-only or append-only write. Strictly speaking, I don't need to be able to overwrite file contents (although it would be convenient to have)
像这样..
我从 MDN 的文档中拼凑了这个 - 我还没有测试过,但我希望它能让你朝着正确的方向前进:
第 1 部分 - LocalFileStore
这些 classes 允许您以 4096 字节的块的形式存储任意二进制数据,其中每个块由 ArrayBuffer
.
表示
IndexedDB API 起初令人困惑,因为它不使用本机 ECMAScript Promise<T>
s 而是使用它自己的 IDBRequest
-API 并且具有奇怪的命名属性 - 但它的要点是:
- 名为
'files'
的单个 IndexedDB 数据库包含本地缓存的所有文件。
- 每个文件都由其自己的
IDBObjectStore
个实例表示。
- 每个文件的每个 4096 字节块在
IDBObjectStore
中由它自己的 record/entry/key-value-pair 表示,其中 key
是 4096
对齐的偏移量文件。
- 请注意,所有 IndexedDB 操作都发生在
IDBTransaction
上下文中,因此 class LocalFile
包装 IDBTransaction
object 而不是 IDBObjectStore
[=106] =].
class LocalFileStore {
static open(): Promise<IDBDatabase> {
return new Promise<IDBDatabase> ( function( accept, reject ) {
// Surprisingly, the IndexedDB API is designed such that you add the event-handlers *after* you've made the `open` request. Weird.
const openReq = indexedDB.open( 'files' );
openReq.addEventListener( 'error', function( err ) {
reject( err );
};
openReq.addEventListener( 'success', function() {
const db = openReq.result;
accept( db );
};
} );
}
constructor(
private readonly db: IDBDatabase
) {
}
openFile( fileName: string, write: boolean ): LocalFile {
const transaction = this.db.transaction( fileName, write ? 'readwrite' : 'readonly', 'strict' );
return new LocalFile( fileName, transaction, write );
}
}
class LocalFile {
constructor(
public readonly fileName: string,
private readonly t: IDBTransaction,
public readonly writable: boolean
) {
}
getChunk( offset: BigInt ): Promise<ArrayBuffer> {
if( offset % 4096 !== 0 ) throw new Error( "Offset value must be a multiple of 4096." );
return new Promise<ArrayBuffer>( function( accept, reject ) {
const key = offset.ToString()
const req = t.objectStore( this.fileName ).get( key );
req.addEventListener( 'error', function( err ) {
reject( err );
} );
req.addEventListener( 'success', function() {
const entry = req.result;
if( typeof entry === 'object' && entry !== null ) {
if( entry instanceof ArrayBuffer ) {
accept( entry as ArrayBuffer );
return;
}
}
else if( typeof entry === 'undefined' ) {
accept( null );
return;
}
reject( "Entry was not an ArrayBuffer or 'undefined'." );
} );
} );
}
putChunk( offset: BigInt, bytes: ArrayBuffer ): Promise<void> {
if( offset % 4096 !== 0 ) throw new Error( "Offset value must be a multiple of 4096." );
if( bytes.length > 4096 ) throw new Error( "Chunk size cannot exceed 4096 bytes." );
return new Promise<ArrayBuffer>( function( accept, reject ) {
const key = offset.ToString();
const req = t.objectStore( this.fileName ).put( bytes, key );
req.addEventListener( 'error', function( err ) {
reject( err );
} );
req.addEventListener( 'success', function() {
accept();
} );
} );
}
existsLocally(): Promise<boolean> {
// TODO: Implement check to see if *any* data for this file exists locally.
}
}
第 2 部分:AbstractFile
- 这个 class 包装了上面的 IndexedDB-based
LocalFileStore
和 LocalFile
class 并且还使用了 fetch
.
- 当您对某个范围的文件发出读取请求时:
- 它首先检查
LocalFileStore
;如果它有必要的块,那么它将检索它们。
- 如果它在范围内缺少任何块,那么它将回退到使用
fetch
和 Range:
header 检索请求的范围,并在本地缓存这些块。
- 当您向文件发出写入请求时:
- 我实际上还没有实现那一点,但这是留给 reader 的练习:)
class AbstractFileStore {
private readonly LocalFileStore lfs;
constructor() {
this.lfs = LocalFileStore.open();
}
openFile( fileName: string, writeable: boolean ): AbstractFile {
return new AbstractFile( fileName, this.lfs.openFile( fileName, writeable ) );
}
}
class AbstractFile {
private static const BASE_URL = 'https://storage.example.com/'
constructor(
public readonly fileName: string,
private readonly localFile: LocalFile
) {
}
read( offset: BigInt, length: number ): Promise<ArrayBuffer> {
const anyExistsLocally = await this.localFile.existsLocally();
if( !anyExistsLocally ) {
return this.readUsingFetch( chunk, 4096 ); // TODO: Cache the returned data into the localFile store.
}
const concat = new Uint8Array( length );
let count = 0;
for( const chunkOffset of calculateChunks( offset, length ) ) {
// TODO: Exercise for the reader: Split `offset + length` into a series of 4096-sized chunks.
const fromLocal = await this.localFile.getChunk( chunk );
if( fromLocal !== null ) {
concat.set( new Uint8Array( fromLocal ), count );
count += fromLocal.length;
}
else {
const fromFetch = this.readUsingFetch( chunk, 4096 );
concat.set( new Uint8Array( fromFetch ), count );
count += fromFetch.length;
}
}
return concat;
}
private readUsingFetch( offset: BigInt, length: number ): Promise<ArrayBuffer> {
const url = AbstractFile.BASE_URL + this.fileName;
const headers = new Headers();
headers.append( 'Range', 'bytes=' + offset + '-' + ( offset + length ).toString() );
const opts = {
credentials: 'include',
headers : headers
};
const resp = await fetch( url, opts );
return await resp.arrayBuffer();
}
write( offset: BigInt, data: ArrayBuffer ): Promise<void> {
throw new Error( "Not yet implemented." );
}
}
第 3 部分 - 流?
由于上面的 class 使用了 ArrayBuffer
,您可以 make-use 现有的 ArrayBuffer
功能来创建 Stream-compatible 或 Stream-like表示 - 它当然必须是异步的,但是 async
+ await
使这很容易。您可以编写一个 generator-function(又名迭代器)来简单地异步生成每个块。
我正在尝试移植桌面应用程序的某些部分,以便能够 运行 在浏览器(客户端)中。我需要一种虚拟文件系统,我可以在其中读取和写入文件(二进制数据)。据我所知,唯一可以广泛跨浏览器使用的选项之一是 IndexedDB。但是,我有点疏远试图找到读取或写入更大文件的示例。 API 似乎只支持 passing/obtaining 整个文件内容 to/from 数据库(blob 或字节数组)。
我试图找到的是我可以在其中连续“传输”数据 to/from 虚拟文件系统的东西,类似于您在任何其他非浏览器上的操作方式应用。例如。 (伪代码)
val in = new FileInputStream(someURLorPath)
val chunkSize = 4096
val buf = new Array[Byte](chunkSize)
while (in.hasRemaining) {
val sz = min(chunkSize, in.remaining)
in.read(buf, 0, sz)
processSome(buf, 0, sz)
...
)
in.close()
我理解同步 API 是浏览器的问题;如果 read
是一个异步方法,那也可以。但我想通过文件 - 这可能是巨大的,例如几个 100 MB - 逐块。块大小无关紧要。这既适用于阅读也适用于写作。
随机访问(能够在虚拟文件中寻找某个位置)将是一个优势,但不是强制性的。
我的一个想法是一个存储=一个虚拟文件,然后键是块索引?有点像 cursor example on MDN,但每条记录都是一个固定大小的 blob 或数组。那有意义吗?有没有更好的API或方法?
似乎 Streams 在概念上就是我正在寻找的 API,但我不知道如何“流式传输 to/from” 虚拟文件系统,例如IndexedDB.
假设您希望能够透明地使用 最初 本地缓存的远程资源(和 一致),您可以抽象fetch
(有 Range:
个请求)和 IndexedDB
.
顺便说一句,你真的很想为此使用 TypeScript,因为在纯 JavaScript 中使用 Promise<T>
是一个 PITA。
one could say either read-only or append-only write. Strictly speaking, I don't need to be able to overwrite file contents (although it would be convenient to have)
像这样..
我从 MDN 的文档中拼凑了这个 - 我还没有测试过,但我希望它能让你朝着正确的方向前进:
第 1 部分 - LocalFileStore
这些 classes 允许您以 4096 字节的块的形式存储任意二进制数据,其中每个块由 ArrayBuffer
.
IndexedDB API 起初令人困惑,因为它不使用本机 ECMAScript Promise<T>
s 而是使用它自己的 IDBRequest
-API 并且具有奇怪的命名属性 - 但它的要点是:
- 名为
'files'
的单个 IndexedDB 数据库包含本地缓存的所有文件。 - 每个文件都由其自己的
IDBObjectStore
个实例表示。 - 每个文件的每个 4096 字节块在
IDBObjectStore
中由它自己的 record/entry/key-value-pair 表示,其中key
是4096
对齐的偏移量文件。- 请注意,所有 IndexedDB 操作都发生在
IDBTransaction
上下文中,因此class LocalFile
包装IDBTransaction
object 而不是IDBObjectStore
[=106] =].
- 请注意,所有 IndexedDB 操作都发生在
class LocalFileStore {
static open(): Promise<IDBDatabase> {
return new Promise<IDBDatabase> ( function( accept, reject ) {
// Surprisingly, the IndexedDB API is designed such that you add the event-handlers *after* you've made the `open` request. Weird.
const openReq = indexedDB.open( 'files' );
openReq.addEventListener( 'error', function( err ) {
reject( err );
};
openReq.addEventListener( 'success', function() {
const db = openReq.result;
accept( db );
};
} );
}
constructor(
private readonly db: IDBDatabase
) {
}
openFile( fileName: string, write: boolean ): LocalFile {
const transaction = this.db.transaction( fileName, write ? 'readwrite' : 'readonly', 'strict' );
return new LocalFile( fileName, transaction, write );
}
}
class LocalFile {
constructor(
public readonly fileName: string,
private readonly t: IDBTransaction,
public readonly writable: boolean
) {
}
getChunk( offset: BigInt ): Promise<ArrayBuffer> {
if( offset % 4096 !== 0 ) throw new Error( "Offset value must be a multiple of 4096." );
return new Promise<ArrayBuffer>( function( accept, reject ) {
const key = offset.ToString()
const req = t.objectStore( this.fileName ).get( key );
req.addEventListener( 'error', function( err ) {
reject( err );
} );
req.addEventListener( 'success', function() {
const entry = req.result;
if( typeof entry === 'object' && entry !== null ) {
if( entry instanceof ArrayBuffer ) {
accept( entry as ArrayBuffer );
return;
}
}
else if( typeof entry === 'undefined' ) {
accept( null );
return;
}
reject( "Entry was not an ArrayBuffer or 'undefined'." );
} );
} );
}
putChunk( offset: BigInt, bytes: ArrayBuffer ): Promise<void> {
if( offset % 4096 !== 0 ) throw new Error( "Offset value must be a multiple of 4096." );
if( bytes.length > 4096 ) throw new Error( "Chunk size cannot exceed 4096 bytes." );
return new Promise<ArrayBuffer>( function( accept, reject ) {
const key = offset.ToString();
const req = t.objectStore( this.fileName ).put( bytes, key );
req.addEventListener( 'error', function( err ) {
reject( err );
} );
req.addEventListener( 'success', function() {
accept();
} );
} );
}
existsLocally(): Promise<boolean> {
// TODO: Implement check to see if *any* data for this file exists locally.
}
}
第 2 部分:AbstractFile
- 这个 class 包装了上面的 IndexedDB-based
LocalFileStore
和LocalFile
class 并且还使用了fetch
. - 当您对某个范围的文件发出读取请求时:
- 它首先检查
LocalFileStore
;如果它有必要的块,那么它将检索它们。 - 如果它在范围内缺少任何块,那么它将回退到使用
fetch
和Range:
header 检索请求的范围,并在本地缓存这些块。
- 它首先检查
- 当您向文件发出写入请求时:
- 我实际上还没有实现那一点,但这是留给 reader 的练习:)
class AbstractFileStore {
private readonly LocalFileStore lfs;
constructor() {
this.lfs = LocalFileStore.open();
}
openFile( fileName: string, writeable: boolean ): AbstractFile {
return new AbstractFile( fileName, this.lfs.openFile( fileName, writeable ) );
}
}
class AbstractFile {
private static const BASE_URL = 'https://storage.example.com/'
constructor(
public readonly fileName: string,
private readonly localFile: LocalFile
) {
}
read( offset: BigInt, length: number ): Promise<ArrayBuffer> {
const anyExistsLocally = await this.localFile.existsLocally();
if( !anyExistsLocally ) {
return this.readUsingFetch( chunk, 4096 ); // TODO: Cache the returned data into the localFile store.
}
const concat = new Uint8Array( length );
let count = 0;
for( const chunkOffset of calculateChunks( offset, length ) ) {
// TODO: Exercise for the reader: Split `offset + length` into a series of 4096-sized chunks.
const fromLocal = await this.localFile.getChunk( chunk );
if( fromLocal !== null ) {
concat.set( new Uint8Array( fromLocal ), count );
count += fromLocal.length;
}
else {
const fromFetch = this.readUsingFetch( chunk, 4096 );
concat.set( new Uint8Array( fromFetch ), count );
count += fromFetch.length;
}
}
return concat;
}
private readUsingFetch( offset: BigInt, length: number ): Promise<ArrayBuffer> {
const url = AbstractFile.BASE_URL + this.fileName;
const headers = new Headers();
headers.append( 'Range', 'bytes=' + offset + '-' + ( offset + length ).toString() );
const opts = {
credentials: 'include',
headers : headers
};
const resp = await fetch( url, opts );
return await resp.arrayBuffer();
}
write( offset: BigInt, data: ArrayBuffer ): Promise<void> {
throw new Error( "Not yet implemented." );
}
}
第 3 部分 - 流?
由于上面的 class 使用了 ArrayBuffer
,您可以 make-use 现有的 ArrayBuffer
功能来创建 Stream-compatible 或 Stream-like表示 - 它当然必须是异步的,但是 async
+ await
使这很容易。您可以编写一个 generator-function(又名迭代器)来简单地异步生成每个块。