javascript - 适用于分块的浏览器的客户端虚拟文件系统

标签 javascript browser client-side indexeddb virtualfilesystem

我正在尝试移植桌面应用程序的某些部分以便能够在浏览器(客户端)中运行。我需要一种虚拟文件系统,我可以在其中读取和写入文件(二进制数据)。据我所知,唯一可以广泛跨浏览器使用的选项之一是 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, 0, sz)
  processSome(buf, 0, sz)
  ...
)
in.close()

我知道同步 API 对浏览器来说是个问题;如果 read 是一个异步方法,那也可以。但我想通过文件 - 这可能是巨大的,例如几个 100 MB - 逐 block 。 block 大小无关紧要。这既适用于阅读,也适用于写作。

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


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


似乎Streams从概念上讲是我正在寻找的 API,但我不知道如何“流入/流出”虚拟文件系统,例如 IndexedDB。

最佳答案

假设您希望能够透明地使用本地缓存(并且一致)的远程资源,最初,您可以对fetch 进行抽象(使用 Range: 请求)和 IndexedDB .

顺便说一句,您真的很想为此使用 TypeScript,因为要使用 Promise<T>在纯 JavaScript 中是一个 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

这些类允许您将任意二进制数据存储在 4096 字节的 block 中,其中每个 block 由 ArrayBuffer 表示.

IndexedDB API 起初令人困惑,因为它不使用原生 ECMAScript Promise<T> s 而是它自己的 IDBRequest -API 和具有奇怪命名的属性 - 但其要点是:

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

  • 此类包装了基于 IndexedDB 的 LocalFileStoreLocalFile上面的类,也使用 fetch .
  • 当您对某个范围的文件发出读取请求时:
    1. 它首先检查 LocalFileStore ;如果它有必要的 block ,那么它将检索它们。
    2. 如果它在范围内缺少任何 block ,那么它将回退到使用 fetch 检索请求的范围。用Range: header ,并在本地缓存这些 block 。
  • 当您向文件发出写入请求时:
    • 我实际上还没有实现那一点,但这是留给读者的练习:)
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 部分 - 流?

因为上面的类使用 ArrayBuffer ,您可以利用现有的 ArrayBuffer创建与 Stream 兼容或类似 Stream 的表示的功能 - 当然必须是异步的,但是 async + await让它变得简单。您可以编写一个生成器函数(又名迭代器)来简单地异步生成每个 block 。

关于javascript - 适用于分块的浏览器的客户端虚拟文件系统,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64518969/

相关文章:

javascript - 我怎么知道我的 js/CSS 文件是否太大?

javascript - JSF/PrimeFaces 客户端输入操作

javascript - 使用 Highcharts 时的宽度和显示问题

javascript - 打破 promise 链

javascript - Summernote - 插入带有 Angular Directive(指令)的模板

javascript - 优化 if else 条件 javaScript

GWT - 如果服务器更新,客户端如何检测到它的 javascript 不同步

browser - 浏览器之间的MIDI或OSC控件

javascript - 如何在没有后端服务器的情况下 24 小时后触发桌面通知?

asp.net - ASPXGridView ClientSideEvents 如何获取选定行的 KeyField 值