天天看點

File System 之本地檔案系統

上一篇文章提到了,最近做一個基于 File System/IndexedDB的應用,上一篇是定額和使用的查詢。

因為LocalFileSystem隻有chrome支援,有點尴尬,如果按需加載又何來尴尬。

這一篇是關于檔案和目錄的操作的,怕陷入回調陷阱,基于promise和ES7的await。

首先介紹兩個函數:

第一個是 :toPromise,把那種帶成功失敗回調的函數轉化為Promise,因為File API的File System裡面很多方法都是這種格式的。

/**
 * 轉為promise,主要是把 a.b(param1,param2,successCallback,errorCall) 轉為promise
 * @param {*期待的是函數} obj 
 * @param {*上下文} ctx 
 * @param {*參數} args 
 */
function toPromise(obj, ctx = window, ...args) {
    if (!obj) return obj

    //如果已經是Promise對象
    if ('function' == typeof obj.then) return obj

    //若obj是函數直接轉換
    if ('function' == typeof obj) return _toPromise(obj)

    return obj;

    //函數轉成 promise
    function _toPromise(fn) {
        return new Promise((resolve, reject) => {

            fn.call(ctx, ...args, (...ags) => {
                //多個參數傳回數組,單個直接傳回對象
                resolve(ags && ags.length > 1 ? ags : ags[0] || null)
            }, (err) => {
                reject(err)
            })

        })
    }
}
      

 第二個是 promiseForEach,順序的執行多個Promise,思想也就是then的拼接

/**
 * https://segmentfault.com/q/1010000007499416
 * Promise for forEach
 * @param {*數組} arr 
 * @param {*回調} cb(val)傳回的應該是Promise 
 * @param {*是否需要執行結果集} needResults
 */
function promiseForEach(arr, cb, needResults) {
    let realResult = [], lastResult //lastResult參數暫無用
    let result = Promise.resolve()
    Array.from(arr).forEach((val, index) => {
        result = result.then(() => {
            return cb(val, index).then((res) => {
                lastResult = res
                needResults && realResult.push(res)
            })
        })
    })

    return needResults ? result.then(() => realResult) : result
}
      

 

這兩個方法完畢後,就直接上主體代碼了, hold on。

/**
 * 參考的API:
 * http://w3c.github.io/quota-api/
 * 
 */

if (!window.location.origin) {
    window.location.origin = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : '');
}
//檔案系統請求辨別 
window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem
//根據URL取得檔案的讀取權限 
window.resolveLocalFileSystemURL = window.resolveLocalFileSystemURL || window.webkitResolveLocalFileSystemURL

//臨時儲存和永久存儲
navigator.temporaryStorage = navigator.temporaryStorage || navigator.webkitTemporaryStorage;
navigator.persistentStorage = navigator.persistentStorage || navigator.webkitPersistentStorage;

//常量
const _TEMPORARY = 'temporary',
    _PERSISTENT = 'persistent',
    FS_SCHEME = 'filesystem:'


class LocalFileSystem {

    constructor(fs) {
        this._fs = fs       //檔案系統
        this._root = fs.root //檔案系統的根Entry
        this._instance = null //示例對象
        this._type = null //類型,window.TEMPORAR| window.PERSISTENT
        this._fsBaseUrl = null //檔案系統的基礎位址
    }


    /**
       * 
       * @param {* window.TEMPORAR(0) |window.PERSISTENT(1)} type
       * @param {* 申請空間大小,機關為M } size
       */
    static getInstance(type = window.TEMPORARY, size = 1) {

        if (this._instance) {
            return Promise.resolve(this._instance)
        }
        //類型
        let typeValue = type,
            //檔案系統基礎位址
            fsBaseUrl = FS_SCHEME + location.origin + '/' + (type == 1 ? _PERSISTENT : _TEMPORARY) + '/'
        return new Promise((resolve, reject) => {
            window.requestFileSystem(type, size * 1024 * 1024, fs => {
                this._instance = new LocalFileSystem(fs)
                this._instance._type = typeValue;
                this._instance._fsBaseUrl = fsBaseUrl
                return resolve(this._instance)
            }, (err) => reject(err))
        })

    }

    /**
     * 獲得FileEntry
     * @param {*檔案路徑} path  
     */
    _getFileEntry(path, create = false) {
        return toPromise(this._root.getFile, this._root, path, { create, exclusive: false })
    }

    /**
     * 擷取目錄
     * @param {*路徑} path 
     * @param {*不存在的時候是否建立} create 
     */
    _getDirectory(path = '', create = false) {
        return toPromise(this._root.getDirectory, this._root, path, { create, exclusive: false })
    }

    async _readEntriesRecursively(rootEntry, refResults) {

        if (rootEntry.isFile) {
            return Promise.resolve(rootEntry)
        }
        let reader = rootEntry.createReader()
        let entries = await toPromise(reader.readEntries, reader)
        refResults.push(...entries)
        let psEntries = entries.map(entry => this._readEntriesRecursively(entry, refResults))
        return Promise.all(psEntries)
    }

    /**
     * 獲得Entry
     * @param {*路徑} path 
     */
    resolveLocalFileSystemURL(path) {
        return toPromise(window.resolveLocalFileSystemURL, window, `${this._fsBaseUrl}${path.startsWith('\/') ? path.substr(1) : path}`)
    }

    /**
     * 獲得檔案
     * @param {*檔案路徑} path 
     */
    async getFile(path) {
        let fe = await this._getFileEntry(path)
        return toPromise(fe.file, fe)
    }

    /**
     * 往檔案寫入内容
     * @param {*檔案路徑} path 
     * @param {*寫入的内容} content 
     * @param {*資料類型} type 
     * @param {*是否是append} append 
     */
    async writeToFile(path, content, type = 'text/plain', append = false) {

        let fe = await this._getFileEntry(path, true)
        let writer = await toPromise(fe.createWriter, fe);
        let data = content;

        //不是blob,轉為blob
        if (content instanceof ArrayBuffer) {
            data = new Blob([new Uint8Array(content)], { type })
        } else if (typeof content == 'string') {
            data = new Blob([content], { type: 'text/plain' })
        } else {
            data = new Blob([content])
        }

        if (append) {
            writer.seek(writer.length)
        }

        return new Promise((resolve, reject) => {
            //寫入成功
            writer.onwriteend = () => {
                resolve(true)
            }

            //寫入失敗
            writer.onerror = (err) => {
                reject(err)
            }

            writer.write(data)
        })
    }

    /**
     * 擷取指定目錄下的檔案和檔案夾
     * @param {*路徑} path 
     */
    async readEntries(path = '') {
        let entry = null
        if (!path) {
            entry = this._root
        } else {
            entry = await this.resolveLocalFileSystemURL(path)
        }
        let reader = entry.createReader()
        return toPromise(reader.readEntries, reader);
    }

    /**
     * 擷取所有的檔案和檔案夾,按照路徑排序
     */
    async readAllEntries() {
        let refResults = []
        let entries = await this._readEntriesRecursively(this._root, refResults)
        refResults.sort((a, b) => a.fullPath > b.fullPath)
        return refResults

    }


    /**
     * 确認目錄存在,遞歸檢查,沒有會自動建立
     * @param {*} directory 
     */
    async ensureDirectory(directory = '') {
        //過濾空的目錄,比如 '/music/' => ['','music','']
        let _dirs = directory.split('/').filter(v => !!v)

        if (!_dirs || _dirs.length == 0) {
            return Promise.resolve(true)
        }

        return promiseForEach(_dirs, (dir, index) => {
            return this._getDirectory(_dirs.slice(0, index + 1).join('/'), true)
        }, true).then((rs) => {
            console.log(rs)
            return true
        })
    }



    /**
     * 清除所有的檔案和檔案夾
     */
    async clear() {
        let entries = await this.readEntries()
        let ps_entries = entries.map(e => e.isFile ? toPromise(e.remove, e) : toPromise(e.removeRecursively, e))
        return Promise.all(ps_entries)
    }

    /**
    * Promise裡面的錯誤處理
    * @param {*reject}  
    */
    errorHandler(reject) {
        return (error) => {
            reject(error)
        }
    }

}


// 測試語句
//讀取某個目錄的子目錄和檔案:  LocalFileSystem.getInstance().then(fs=>fs.readEntries()).then(f=>console.log(f))
//寫檔案         LocalFileSystem.getInstance().then(fs=>fs.writeToFile('music/txt.txt','愛死你')).then(f=>console.log(f))
//擷取檔案:     LocalFileSystem.getInstance().then(fs=>fs.getFile('music/txt.txt')).then(f=>console.log(f))
//遞歸建立目錄:  LocalFileSystem.getInstance().then(fs=>fs.ensureDirectory('music/vbox')).then(r=>console.log('r:' + r))
//遞歸擷取:     LocalFileSystem.getInstance().then(fs=>fs.readAllEntries()).then(f=>console.log(f))
//删除所有:     LocalFileSystem.getInstance().then(fs=>fs.clear()).then(f=>console.log(f)).catch(err=>console.log(err)) 
      

  

當然測試語句也在上面了,因為用了 await,那麼大家自然知道了。需要 chrome://flags開啟javascript的特性。

如果你有興趣,代碼位址:

https://github.com/xiangwenhu/BlogCodes/tree/master/client/FileSystem

,下載下傳下來

npm install 之後, node server/app.js就可以查詢demo了

參考:

using-localfilesystem Promise循環串行執行寫法 filer

繼續閱讀