fsTaskCp.mjs

import fs from 'fs'
import get from 'lodash-es/get.js'
import evem from './evem.mjs'
import evEmit from './evEmit.mjs'
import genPm from './genPm.mjs'
import iseobj from './iseobj.mjs'
import haskey from './haskey.mjs'
import j2o from './j2o.mjs'
import o2j from './o2j.mjs'
import debounce from './debounce.mjs'
import ltdtDiffByKey from './ltdtDiffByKey.mjs'
import fsIsFolder from './fsIsFolder.mjs'
import fsCreateFolder from './fsCreateFolder.mjs'
import fsWatchFile from './fsWatchFile.mjs'
import getTrace from './getTrace.mjs'


/**
 * 後端nodejs偵測指定資料夾下之檔案變化,分來源與偵測端
 *
 * Unit Test: {@link https://github.com/yuda-lyu/wsemi/blob/master/test/fsTaskCp.test.mjs Github}
 * @memberOf wsemi
 * @param {String} fdSrc 輸入來源端更新紀錄之資料夾路徑字串
 * @param {String} fdTar 輸入偵測端已紀錄之資料夾路徑字串
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {Function} [opt.onTrace=null] 輸入觀察回調函數,供buildTar所建立之偵測端使用,於內部事件發生當下同步呼叫,傳入一個凍結之事件物件{ type, ... },供測試以事件(而非等待時間)同步與診斷;type為'recv'時為收到來源端紀錄檔之變更(含kind為檔案監看事件種類'add'、'change'或'unlink',隨後排入300ms之debounce),'run'時為debounce到期而開始偵測,'skip'時為來源端無紀錄(紀錄檔不存在、內容為空物件或非物件)而不偵測(含reason為'no src'),'emit'時為即將派發change事件(含add、del、diff、same為kpCmp各類件數),'settle'時為change之pm已settle而即將據以處理(含ok為任務是否成功),'record'時為任務成功後偵測端紀錄檔已更新,'clear'時為已呼叫偵測端之clear;clear後不再通知(已排入debounce者仍會偵測與派發change,但不再通知);回調不帶接收者呼叫,其拋錯與回傳值皆忽略,不影響偵測,預設null
 * @returns {Object} 回傳物件,包含buildSrc與buildTar函數,buildSrc回傳監聽事件物件ev, 可使用ev.set進行紀錄變更,buildTar回傳監聽事件物件,可監聽change事件,並使用接收事件資訊msg內的pm做為回傳執行成功與否狀態。兩事件物件皆為事件物件為原生EventEmitter(eventemitter3)。本模組於派發處以try攔截監聽器之同步拋錯故其不會使行程崩潰;惟依EventEmitter規範,同一次派發中先拋錯之監聽器會中止該次派發,其後之監聽器不再被呼叫。async監聽器之reject不被攔截(規範上emit不觀察監聽器回傳值),須由監聽器自行處理,否則為unhandledRejection。change監聽器同步拋錯時會先reject該次msg.pm(視為執行失敗、不更新紀錄檔)再以error事件回報{ fun: 'listener', name, msg, args }
 * @example
 * //need test in nodejs
 *
 * import fsDeleteFile from './src/fsDeleteFile.mjs'
 * import fsCreateFolder from './src/fsCreateFolder.mjs'
 * import fsDeleteFolder from './src/fsDeleteFolder.mjs'
 * import fsTaskCp from './src/fsTaskCp.mjs'
 *
 * let test = async () => {
 *     return new Promise((resolve, reject) => {
 *         let msSrc = []
 *         let msTar = []
 *
 *         let fp = './_taskcp'
 *         fsDeleteFolder(fp) //預先清除fsTaskCp持久化數據資料夾
 *
 *         let fpc = './_taskcpSrc'
 *         fsDeleteFolder(fpc) //預先清除fsTaskCp持久化數據資料夾
 *
 *         let fpt = './_taskcpTar'
 *         fsDeleteFolder(fpt) //預先清除fsTaskCp持久化數據資料夾
 *
 *         let otk = fsTaskCp(fpc, fpt)
 *
 *         let otkSrc = otk.buildSrc()
 *         otkSrc.on('set', (msg) => {
 *             console.log(`src send task...`, msg)
 *             msSrc.push({ type: 'src-send', msg: JSON.stringify(msg) })
 *         })
 *         otkSrc.on('remove', (msg) => {
 *             console.log(`src send task...`, msg)
 *             msSrc.push({ type: 'src-send', msg: JSON.stringify(msg) })
 *         })
 *
 *         let otkTar = otk.buildTar()
 *         otkTar.on('change', (msg) => {
 *             // console.log('otkTar change', msg)
 *
 *             //接收任務
 *             console.log(`tar recv task...`, msg.kpCmp)
 *             msTar.push({ type: 'tar-recv', msg: JSON.stringify(msg.kpCmp) })
 *
 *             setTimeout(() => {
 *                 console.log(`tar deal task done`)
 *                 msTar.push({ type: 'tar-done', msg: JSON.stringify(msg.kpCmp) })
 *                 msg.pm.resolve()
 *             }, 1000)
 *
 *         })
 *
 *         setTimeout(() => {
 *             fsCreateFolder(fp)
 *         }, 1)
 *
 *         setTimeout(() => {
 *             fs.writeFileSync(`${fp}/abc.txt`, 'abc', 'utf8')
 *             otkSrc.set('abc.txt', 'hash_a1')
 *         }, 3000)
 *
 *         setTimeout(() => {
 *             fs.writeFileSync(`${fp}/abc.txt`, 'mnop', 'utf8')
 *             otkSrc.set('abc.txt', 'hash_a2')
 *             fs.writeFileSync(`${fp}/def.txt`, 'def', 'utf8')
 *             otkSrc.set('def.txt', 'hash_b1')
 *         }, 6000)
 *
 *         setTimeout(() => {
 *             fsDeleteFile(`${fp}/abc.txt`)
 *             otkSrc.remove('abc.txt')
 *         }, 9000)
 *
 *         setTimeout(() => {
 *             otkTar.clear()
 *         }, 12000)
 *
 *         setTimeout(() => {
 *             fsDeleteFolder(fp)
 *             fsDeleteFolder(fpc)
 *             fsDeleteFolder(fpt)
 *             let ms = {
 *                 msSrc,
 *                 msTar,
 *             }
 *             console.log('ms', ms)
 *             resolve(ms)
 *         }, 15000)
 *
 *     })
 * }
 * await test()
 *     .catch((err) => {
 *         console.log(err)
 *     })
 * // src send task... { type: 'set', fp: 'abc.txt', hash: 'hash_a1' }
 * // tar recv task... {
 * //   del: [],
 * //   add: [ { fp: 'abc.txt', hash: 'hash_a1' } ],
 * //   same: [],
 * //   diff: []
 * // }
 * // tar deal task done
 * // src send task... { type: 'set', fp: 'abc.txt', hash: 'hash_a2' }
 * // src send task... { type: 'set', fp: 'def.txt', hash: 'hash_b1' }
 * // tar recv task... {
 * //   del: [],
 * //   add: [ { fp: 'def.txt', hash: 'hash_b1' } ],
 * //   same: [],
 * //   diff: [ { fp: 'abc.txt', hash: 'hash_a2' } ]
 * // }
 * // src send task... { type: 'remove', fp: 'abc.txt' }
 * // tar deal task done
 * // tar recv task... {
 * //   del: [ { fp: 'abc.txt', hash: 'hash_a2' } ],
 * //   add: [],
 * //   same: [ { fp: 'def.txt', hash: 'hash_b1' } ],
 * //   diff: []
 * // }
 * // tar deal task done
 * // ms {
 * //   msSrc: [
 * //     {
 * //       type: 'src-send',
 * //       msg: '{"type":"set","fp":"abc.txt","hash":"hash_a1"}'
 * //     },
 * //     {
 * //       type: 'src-send',
 * //       msg: '{"type":"set","fp":"abc.txt","hash":"hash_a2"}'
 * //     },
 * //     {
 * //       type: 'src-send',
 * //       msg: '{"type":"set","fp":"def.txt","hash":"hash_b1"}'
 * //     },
 * //     { type: 'src-send', msg: '{"type":"remove","fp":"abc.txt"}' }
 * //   ],
 * //   msTar: [
 * //     {
 * //       type: 'tar-recv',
 * //       msg: '{"del":[],"add":[{"fp":"abc.txt","hash":"hash_a1"}],"same":[],"diff":[]}'
 * //     },
 * //     {
 * //       type: 'tar-done',
 * //       msg: '{"del":[],"add":[{"fp":"abc.txt","hash":"hash_a1"}],"same":[],"diff":[]}'
 * //     },
 * //     {
 * //       type: 'tar-recv',
 * //       msg: '{"del":[],"add":[{"fp":"def.txt","hash":"hash_b1"}],"same":[],"diff":[{"fp":"abc.txt","hash":"hash_a2"}]}'
 * //     },
 * //     {
 * //       type: 'tar-done',
 * //       msg: '{"del":[],"add":[{"fp":"def.txt","hash":"hash_b1"}],"same":[],"diff":[{"fp":"abc.txt","hash":"hash_a2"}]}'
 * //     },
 * //     {
 * //       type: 'tar-recv',
 * //       msg: '{"del":[{"fp":"abc.txt","hash":"hash_a2"}],"add":[],"same":[{"fp":"def.txt","hash":"hash_b1"}],"diff":[]}'
 * //     },
 * //     {
 * //       type: 'tar-done',
 * //       msg: '{"del":[{"fp":"abc.txt","hash":"hash_a2"}],"add":[],"same":[{"fp":"def.txt","hash":"hash_b1"}],"diff":[]}'
 * //     }
 * //   ]
 * // }
 *
 */
function fsTaskCp(fdSrc, fdTar, opt = {}) {

    //check fdSrc
    // let fdSrc = get(opt, 'fdSrc', '')
    // if (!isestr(fdSrc)) {
    //     fdSrc = './_taskcpSrc'
    // }
    if (!fsIsFolder(fdSrc)) {
        fsCreateFolder(fdSrc)
    }

    //check fdTar
    // let fdTar = get(opt, 'fdTar', '')
    // if (!isestr(fdTar)) {
    //     fdTar = './_taskcpTar'
    // }
    if (!fsIsFolder(fdTar)) {
        fsCreateFolder(fdTar)
    }

    //fpHashSrc, fpHashTar
    let fpHashSrc = `${fdSrc}/fpHash.json`
    let fpHashTar = `${fdTar}/fpHash.json`

    //_readKpOb
    let _readKpOb = (fpHash) => {
        let r = {} //儲存格式為物件
        try {
            let j = fs.readFileSync(fpHash, 'utf8')
            r = j2o(j)
        }
        catch (err) {}
        return r
    }

    //_writeKpOb
    let _writeKpOb = (fpHash, kp) => {
        try {
            let j = o2j(kp)
            fs.writeFileSync(fpHash, j, 'utf8')
        }
        catch (err) {}
    }

    //_getOb
    let _getOb = (fpHash, fp) => {

        //_readKpOb
        let kpOb = _readKpOb(fpHash)
        // console.log('kpOb', kpOb)

        //r
        let r = get(kpOb, fp, '')
        // console.log(fp, r)

        return r
    }

    //_setOb
    let _setOb = (fpHash, fp, hash) => {

        //_readKpOb
        let kpOb = _readKpOb(fpHash)
        // console.log('kpOb', kpOb)

        //save
        kpOb[fp] = hash

        //_writeKpOb
        _writeKpOb(fpHash, kpOb)

    }

    //_removeOb
    let _removeOb = (fpHash, fp) => {

        //_readKpOb
        let kpOb = _readKpOb(fpHash)
        // console.log('kpOb', kpOb)

        //delete
        if (haskey(kpOb, fp)) {
            delete kpOb[fp]
        }

        //_writeKpOb
        _writeKpOb(fpHash, kpOb)

    }

    let readKpObSrc = () => {
        return _readKpOb(fpHashSrc)
    }
    let writeKpObSrc = (kp) => {
        return _writeKpOb(fpHashSrc, kp)
    }
    let getObSrc = (fp) => {
        return _getOb(fpHashSrc, fp)
    }
    let setObSrc = (fp, hash) => {
        return _setOb(fpHashSrc, fp, hash)
    }
    let removeObSrc = (fp) => {
        return _removeOb(fpHashSrc, fp)
    }

    let readKpObTar = () => {
        return _readKpOb(fpHashTar)
    }
    let writeKpObTar = (kp) => {
        return _writeKpOb(fpHashTar, kp)
    }
    let getObTar = (fp) => {
        return _getOb(fpHashTar, fp)
    }
    let setObTar = (fp, hash) => {
        return _setOb(fpHashTar, fp, hash)
    }
    let removeObTar = (fp) => {
        return _removeOb(fpHashTar, fp)
    }

    let cvKpToLtdt = (kp) => {
        let ltdt = []
        if (kp === null || kp === undefined) { //紀錄檔內容為null時(j2o得null)視為無紀錄, 同原each對null不迭代
            return ltdt
        }
        for (let k of Object.keys(kp)) { //以自有鍵迭代, 檔案路徑鍵為length時不得被lodash each當成陣列樣物件而依索引迭代(R-OBJ-ITER-01)
            ltdt.push({ fp: k, hash: kp[k] })
        }
        return ltdt
    }

    //buildSrc
    let buildSrc = () => {
        //紀錄檔案變更至fpHashSrc, 供buildTar偵測驅動使用

        //ev
        let ev = evem() //set/remove於呼叫端同步派發(不帶pm), 監聽器出錯不得回拋至set/remove而中斷紀錄流程, 故派發處以evEmit攔截

        //_set
        let _set = (fp, hash) => {
            setObSrc(fp, hash)
            evEmit(ev, 'set', [{ type: 'set', fp, hash }], { tag: 'fsTaskCp' }) //監聽器出錯不得回拋至set而中斷紀錄流程
        }

        //_remove
        let _remove = (fp) => {
            removeObSrc(fp)
            evEmit(ev, 'remove', [{ type: 'remove', fp }], { tag: 'fsTaskCp' }) //同上
        }

        //save
        ev.set = _set
        ev.get = getObSrc
        ev.remove = _remove
        ev.readKp = readKpObSrc
        ev.writeKp = writeKpObSrc

        return ev
    }

    //buildTar
    let buildTar = () => {
        //讀取對方紀錄fpHashSrc, 讀取自己備份紀錄fpHashTar, 偵測差異後emit觸發事件使用

        //ev
        let ev = evem() //事件於watcher回呼內派發(change並帶pm), 監聽器出錯不得殺行程, 故派發處以evEmit攔截並先reject pm

        //dbc
        let dbc = debounce(300)

        //trace, 觀察回調(opt.onTrace), 未給時為null; 偵測端clear後設為null不再通知
        let trace = getTrace(opt)

        //core, 回傳pmm(外部任務執行狀態), 來源無紀錄則回傳'no src'; 原碼另建一個永不settle之外層pm回傳, 無人await故無害但為死碼, 已移除
        let core = async (msg) => {
            // console.log('core', msg)

            //kpSrc
            let kpSrc = readKpObSrc()

            //check
            if (!iseobj(kpSrc)) {
                //來源無紀錄檔案, 故不偵測
                if (trace) {
                    trace('skip', { reason: 'no src' })
                }
                return 'no src'
            }

            //kpTar
            let kpTar = readKpObTar()

            //ltdtSrc, ltdtTar
            let ltdtSrc = cvKpToLtdt(kpSrc)
            let ltdtTar = cvKpToLtdt(kpTar)
            // console.log('kpSrc', kpSrc)
            // console.log('kpTar', kpTar)

            //ltdtDiffByKey
            let r = ltdtDiffByKey(ltdtTar, ltdtSrc, 'fp', { withInfor: false })
            // console.log('ltdtDiffByKey', r)

            //pmm, 供外部提供任務執行狀態
            let pmm = genPm()
            pmm
                .then((res) => {
                    // console.log('執行任務成功, 更新紀錄檔案', kpSrc)
                    if (trace) {
                        trace('settle', { ok: true })
                    }
                    writeKpObTar(kpSrc)
                    if (trace) {
                        trace('record', {})
                    }
                })
                .catch(() => {
                    //執行任務失敗, 不更新紀錄檔
                    if (trace) {
                        trace('settle', { ok: false })
                    }
                })

            //trace, 於派發前通知(監聽器同步拋錯時仍已通知)
            if (trace) {
                trace('emit', { add: r.add.length, del: r.del.length, diff: r.diff.length, same: r.same.length })
            }

            //emit, 於watcher回呼內派發且帶pm, 監聽器同步拋錯時須先reject該pm再通報, 否則呼叫端之流程懸置
            evEmit(ev, 'change', [{ kpSrc, kpTar, kpCmp: r, pm: pmm }], {
                tag: 'fsTaskCp',
                funSettle: (err) => {
                    pmm.reject(err)
                },
            })

            return pmm
        }

        //evFile
        let evFile = fsWatchFile(fpHashSrc)
        evFile.on('change', (msg) => {
            // console.log(msg.type, getFileName(msg.fp))

            //trace, kind為檔案監看事件種類(add、change、unlink)
            if (trace) {
                trace('recv', { kind: msg.type })
            }

            dbc(() => {
                if (trace) {
                    trace('run', {})
                }
                core(msg)
                    .catch(() => {})
            })

        })

        //save
        ev.set = setObTar
        ev.get = getObTar
        ev.remove = removeObTar
        ev.readKp = readKpObTar
        ev.writeKp = writeKpObTar
        ev.clear = evFile.clear
        if (trace) {
            //傳了觀察回調才包裝clear(不傳時clear同舊版為evFile.clear), 釋放後不再通知
            ev.clear = () => {
                let r = evFile.clear()
                if (trace) {
                    trace('clear', {})
                    trace = null
                }
                return r
            }
        }

        return ev
    }

    //r
    let r = {
        buildSrc,
        buildTar,
    }

    return r
}


export default fsTaskCp