Javascript 用于浏览器的客户端虚拟文件系统,可用于分块

Javascript 用于浏览器的客户端虚拟文件系统,可用于分块,javascript,browser,client-side,indexeddb,virtualfilesystem,Javascript,Browser,Client Side,Indexeddb,Virtualfilesystem,我正在尝试移植桌面应用程序的某些部分,以便能够在浏览器(客户端)中运行。我需要一种虚拟文件系统,在其中我可以读取和写入文件(二进制数据)。据我所知,IndexedDB是唯一一个在浏览器中广泛使用的选项。然而,我有点疏远试图寻找的例子,读或写更大的文件。API似乎只支持向数据库(blob或字节数组)传递/获取整个文件内容 我试图找到的是,我可以连续地“流式传输”虚拟文件系统中的数据,类似于在任何其他非浏览器应用程序上执行此操作的方式。例如(伪代码) val in=new FileInputStre

我正在尝试移植桌面应用程序的某些部分,以便能够在浏览器(客户端)中运行。我需要一种虚拟文件系统,在其中我可以读取和写入文件(二进制数据)。据我所知,IndexedDB是唯一一个在浏览器中广泛使用的选项。然而,我有点疏远试图寻找的例子,读或写更大的文件。API似乎只支持向数据库(blob或字节数组)传递/获取整个文件内容

我试图找到的是,我可以连续地“流式传输”虚拟文件系统中的数据,类似于在任何其他非浏览器应用程序上执行此操作的方式。例如(伪代码)

val in=new FileInputStream(someURLorPath)
val chunkSize=4096
val buf=新数组[字节](chunkSize)
当(在剩余时间内){
val sz=min(chunkSize,单位:剩余)
in.read(buf,0,sz)
processSome(buf、0、sz)
...
)
in.close()
我知道同步API对浏览器来说是个问题;如果
read
是一种异步方法,那也没关系。但是我想一块一块地浏览这个文件,它可能很大,比如几个100MB。块大小无关紧要。读和写都是如此

随机访问(能够搜索虚拟文件中的某个位置)将是一个优势,但不是强制性的


我的一个想法是,一个存储=一个虚拟文件,然后键是块索引?有点像,但每个记录都是固定大小的blob或数组。这有意义吗?有更好的API或方法吗



从概念上讲,这似乎是我正在寻找的API,但我不知道如何“流式传输到/从”IndexedDB之类的虚拟文件系统。

假设您希望能够透明地处理本地缓存(且一致)的初始远程资源,您可以通过
获取
(使用
范围:
请求)和
IndexedDB

顺便说一句,您确实需要使用TypeScript来实现这一点,因为在纯JavaScript中使用
Promise
是一个PITA

可以说是只读的,也可以说是只附加写的。严格地说,我不需要能够覆盖文件内容(尽管这样做会很方便)

像这样的

我是从MDN的文档中拼凑出来的-我还没有测试过,但我希望它能让您找到正确的方向:

第1部分-
LocalFileStore
这些类允许您以4096字节的块存储任意二进制数据,其中每个块由
ArrayBuffer
表示

IndexedDB API一开始令人困惑,因为它不使用本机ECMAScript
Promise
s,而是使用自己的
IDBRequest
-API和命名奇怪的属性,但其要点是:

  • 名为
    'files'
    的单个IndexedDB数据库保存本地缓存的所有文件
  • 每个文件都由其自己的
    IDBObjectStore
    实例表示
  • 每个文件的每个4096字节块由
    IDBObjectStore
    中自己的记录/条目/键值对表示,其中
    key
    是文件中与
    4096
    对齐的偏移量。
    • 请注意,所有IndexedDB操作都发生在
      IDBTransaction
      上下文中,因此为什么
      class LocalFile
      包装
      IDBTransaction
      对象而不是
      IDBObjectStore
      对象
第三部分-流?
由于上面的类使用了
ArrayBuffer
,您可以利用现有的
ArrayBuffer
功能来创建流兼容的或类似流的表示—当然,它必须是异步的,但是
async
+
await
可以简化。您可以编写一个生成器函数(也称为迭代器)这只是异步产生每个数据块。

数据实际上是要驻留在用户的本地计算机上吗?还是可以通过HTTP从远程存储服务器进行流式传输?如果可以,您可以使用一个简单的范围请求吗?@Dai谢谢您的问题。事实上,这两种情况都是相关的。假设这些是声音和视频文件。我必须能够同时处理这两种情况从唯一的资源中检索它们并将它们放在本地存储中(我不希望在获取和放置之间的某个时间点内存中有一个完整的300 MB文件),但web应用程序也必须能够在虚拟系统中处理和创建新的声音文件。在大多数情况下,在用户关闭浏览器选项卡后,这些文件不需要保留,但如果这是可能的话,那就太好了。听起来您将很快遇到存储大小限制here@charlietfl我认为storage manager报告为2GB(Firefox)这在大多数情况下都可以。你能在用户的机器上运行无头Web服务器进程吗?或者要求他们使用基于Chromium的浏览器吗?如果你只想坚持W3C推荐的浏览器支持范围很广的话,你就有点拘泥于任何类型的本地存储选项(甚至不考虑IE11支持).谢谢你的详尽回答!我确实是沿着这些思路思考的。我发现的另一个正交的东西是。而不是每个文件都有多个键(每个块一个键),它一次又一次地更新一个
Blob
对象,然后将其重新放入存储中。起初,这似乎是违反直觉的,但我认为Blob不必驻留在内存中,它们确实像文件,因此可能浏览器已针对此类更新进行了优化。@0\uuo从技术上讲,浏览器/引擎可以自由地提取任何内容和所有内容在脚本中。请注意,
Blob
并不表示磁盘上的文件或内存中的任何内容,它只是对任何固定长度的任意二进制数据的抽象(例如,您也可以从
获取
Blob
响应
),我同意将
Blob
连接起来
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.
    }
}
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." );
    }
}