用于浏览器的客户端虚拟文件系统,可与分块配合使用 第3部分-流?

问题描述

我正在尝试移植桌面应用程序的某些部分以便能够在浏览器(客户端)中运行。我需要一种虚拟文件系统,可以在其中读写文件(二进制数据)。据我了解,IndexedDB是在浏览器中广泛使用的唯一选项之一。但是,我对于寻找可以读取或写入较大文件的示例有些疏远。看来该API仅支持数据库(blob或字节数组)传递/从数据库获取整个文件内容

我要寻找的东西是可以连续“流式传输”的东西,以便说出虚拟文件系统中的数据,这与您在任何其他非浏览器应用程序上的处理方式类似。例如。 (伪代码

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,sz)
  processSome(buf,sz)
  ...
)
in.close()

我了解同步API对于浏览器来说是一个问题;如果read异步方法,也可以。但是我想浏览文件-可能很大,例如几个100 MB-逐块。块大小无关紧要。阅读和写作都可以。

随机访问(能够在虚拟文件中定位)是一个加号,但不是强制性的。


我有一个想法是一个商店=一个虚拟文件,然后这些键就是块索引?有点像cursor example on MDN,但是每个记录都是一个固定大小的Blob或数组。那有意义吗?有更好的API或方法吗?


从概念上讲,Streams似乎是我正在寻找的API,但是我不知道如何“往返于” IndexedDB这样的虚拟文件系统。

解决方法

假设您希望能够透明地使用 本地缓存(和一致)的远程资源,则可以在fetch上进行抽象(使用{ {1}}个请求)和Range:

顺便说一句,您真的要为此使用TypeScript,因为在纯JavaScript中使用IndexedDB是PITA。

可以说是只读或仅追加写入。严格来说,我不需要覆盖文件内容(尽管这样做很方便)

这样的事情。

我从MDN的文档中整理了这些内容-我尚未对其进行测试,但希望它能为您提供正确的方向:

第1部分-Promise<T>

这些类允许您以4096字节的块的形式存储任意二进制数据,其中每个块均由LocalFileStore表示。

IndexedDB API最初令人困惑,因为它不使用本机ECMAScript ArrayBuffer,而是使用其自己的Promise<T>-API和具有奇怪名称的属性-但其要旨是:

  • 名为IDBRequest的单个IndexedDB数据库保存本地缓存的所有文件。
  • 每个文件都由自己的'files'实例表示。
  • 每个文件的每个4096字节块由其在IDBObjectStore中的自己的记录/条目/键值对表示,其中IDBObjectStorekey对齐的偏移量到文件中。
    • 请注意,所有IndexedDB操作都在4096上下文中发生,因此IDBTransaction为什么包装class LocalFile对象而不是IDBTransaction对象。
IDBObjectStore

第2部分: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() { accept(); } ); } ); } existsLocally(): Promise<boolean> { // TODO: Implement check to see if *any* data for this file exists locally. } }

  • 该类包装了上面基于IndexedDB的AbstractFileLocalFileStore类,并且还使用了LocalFile
  • 当您请求读取文件范围时:
    1. 首先使用fetch进行检查;如果它具有必要的块,则它将检索它们。
    2. 如果该范围内缺少任何块,那么它将退回到使用带有LocalFileStore标头的fetch来检索请求的范围,并在本地缓存这些块。
  • 向文件发出写请求时:
    • 我实际上还没有实现这一点,但这是留给读者的练习:)
Range:

第3部分-流?

由于上述类使用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." ); } } ,因此您可以利用现有的ArrayBuffer功能来创建与Stream兼容或类似Stream的表示形式-当然,它必须是异步的,但是{ {1}} + ArrayBuffer可以轻松实现。您可以编写一个生成器函数(又称迭代器),该函数简单地异步生成每个块。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...