WDwdataFtp.mjs

import fs from 'fs'
import get from 'lodash-es/get.js'
import size from 'lodash-es/size.js'
import each from 'lodash-es/each.js'
import map from 'lodash-es/map.js'
import cloneDeep from 'lodash-es/cloneDeep.js'
import isbol from 'wsemi/src/isbol.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import isp0int from 'wsemi/src/isp0int.mjs'
import isfun from 'wsemi/src/isfun.mjs'
import ispm from 'wsemi/src/ispm.mjs'
import cdbl from 'wsemi/src/cdbl.mjs'
import pmSeries from 'wsemi/src/pmSeries.mjs'
import getErrorMessage from 'wsemi/src/getErrorMessage.mjs'
import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
import fsCopyFile from 'wsemi/src/fsCopyFile.mjs'
import fsCleanFolder from 'wsemi/src/fsCleanFolder.mjs'
import fsCreateFolder from 'wsemi/src/fsCreateFolder.mjs'
import fsDeleteFolder from 'wsemi/src/fsDeleteFolder.mjs'
import fsSyncFolder from 'wsemi/src/fsSyncFolder.mjs'
import fsTreeFolderWithHash from 'wsemi/src/fsTreeFolderWithHash.mjs'
import fsGetFileBasicHash from 'wsemi/src/fsGetFileBasicHash.mjs'
import WDwdataBuilder from 'w-dwdata-builder/src/WDwdataBuilder.mjs'
import downloadFiles from './downloadFiles.mjs'


/**
 * 基於檔案之下載FTP數據與任務建構器
 *
 * 執行階段最新hash數據放置於fdDwAttime,前次hash數據會於結束前自動備份至fdDwCurrent
 *
 * 執行階段最新數據放置於fdDwStorageTemp,前次數據放置於fdDwStorage,於結束前會將fdDwStorage清空,將fdDwStorageTemp複製至fdDwStorage
 *
 * @param {String} st 輸入設定FTP連線資訊物件
 * @param {String} [st.transportation='FTP'] 輸入傳輸協定字串,可選'FTP'、'SFTP',預設'FTP'
 * @param {String} [st.hostname=''] 輸入hostname字串,預設''
 * @param {Integer} [st.port=21|22] 輸入port正整數,當transportation='FTP'預設21,當transportation='SFTP'預設22
 * @param {String} [st.username=''] 輸入帳號字串,預設''
 * @param {String} [st.password=''] 輸入密碼字串,預設''
 * @param {String} [st.fdIni='./'] 輸入同步資料夾字串,預設'./'
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {Boolean} [opt.useExpandOnOldFiles=false] 輸入來源檔案是否僅為增量檔案布林值,預設false
 * @param {Boolean} [opt.useSimulateFiles=false] 輸入是否使用模擬取得FTP數據布林值,預設false
 * @param {String} [opt.fdTagRemove='./_tagRemove'] 輸入暫存標記為刪除數據資料夾字串,預設'./_tagRemove'
 * @param {String} [opt.fdDwStorageTemp='./_dwStorageTemp'] 輸入最新下載檔案存放資料夾字串,預設'./_dwStorageTemp'
 * @param {String} [opt.fdDwStorage='./_dwStorage'] 輸入合併儲存檔案資料夾字串,預設'./_dwStorage'
 * @param {String} [opt.fdDwAttime='./_dwAttime'] 輸入當前下載供比對hash用之數據資料夾字串,預設'./_dwAttime'
 * @param {String} [opt.fdDwCurrent='./_dwCurrent'] 輸入已下載供比對hash用之數據資料夾字串,預設'./_dwCurrent'
 * @param {String} [opt.fdResult=`./_result`] 輸入已下載數據所連動生成數據資料夾字串,預設`./_result`
 * @param {String} [opt.fdTaskCpActualSrc='./_taskCpActualSrc'] 輸入任務狀態之來源端完整資料夾字串,預設'./_taskCpActualSrc'
 * @param {String} [opt.fdTaskCpSrc='./_taskCpSrc'] 輸入任務狀態之來源端資料夾字串,預設'./_taskCpSrc'
 * @param {String} [opt.fdLog='./_logs'] 輸入儲存log資料夾字串,預設'./_logs'
 * @param {Function} [opt.funDownload=null] 輸入取得最新下載檔案hash之函數,回傳資料陣列,預設null
 * @param {Function} [opt.funGetCurrent=null] 輸入取得已下載檔案hash之函數,回傳資料陣列,預設null
 * @param {Function} [opt.funAdd=null] 輸入當有新資料時,需要連動處理之函數,預設null
 * @param {Function} [opt.funModify=null] 輸入當有資料需更新時,需要連動處理之函數,預設null
 * @param {Function} [opt.funRemove=null] 輸入當有資料需刪除時,需要連動處理之函數,預設null
 * @param {Function} [opt.funAfterStart=null] 輸入偵測程序剛開始啟動時,需要處理之函數,預設null
 * @param {Function} [opt.funBeforeEnd=null] 輸入偵測程序要結束前,需要處理之函數,預設null
 * @param {Number} [opt.timeToleranceRemove=0] 輸入刪除任務之防抖時長,單位ms,預設0,代表不使用
 * @returns {Object} 回傳事件物件,可呼叫函數on監聽change事件
 * @example
 *
 * import w from 'wsemi'
 * import WDwdataFtp from './src/WDwdataFtp.mjs'
 *
 * let st = {
 *     'hostname': '{hostname}',
 *     'port': 21,
 *     'username': '{username}',
 *     'password': '{password}',
 *     'fdIni': './'
 * }
 * // console.log('st', st)
 *
 * //fdTagRemove
 * let fdTagRemove = `./_tagRemove`
 * w.fsCleanFolder(fdTagRemove)
 *
 * //fdDwStorageTemp
 * let fdDwStorageTemp = `./_dwStorageTemp`
 * w.fsCleanFolder(fdDwStorageTemp)
 *
 * //fdDwStorage
 * let fdDwStorage = `./_dwStorage`
 * w.fsCleanFolder(fdDwStorage)
 *
 * //fdDwAttime
 * let fdDwAttime = `./_dwAttime`
 * w.fsCleanFolder(fdDwAttime)
 *
 * //fdDwCurrent
 * let fdDwCurrent = `./_dwCurrent`
 * w.fsCleanFolder(fdDwCurrent)
 *
 * //fdResult
 * let fdResult = `./_result`
 * w.fsCleanFolder(fdResult)
 *
 * //fdTaskCpActualSrc
 * let fdTaskCpActualSrc = `./_taskCpActualSrc`
 * w.fsCleanFolder(fdTaskCpActualSrc)
 *
 * //fdTaskCpSrc
 * let fdTaskCpSrc = `./_taskCpSrc`
 * w.fsCleanFolder(fdTaskCpSrc)
 *
 * let opt = {
 *     useExpandOnOldFiles: false, //true, false
 *     fdTagRemove,
 *     fdDwStorageTemp,
 *     fdDwStorage,
 *     fdDwAttime,
 *     fdDwCurrent,
 *     fdResult,
 *     fdTaskCpActualSrc,
 *     fdTaskCpSrc,
 *     // fdLog,
 *     // funDownload,
 *     // funGetCurrent,
 *     // funRemove,
 *     // funAdd,
 *     // funModify,
 * }
 * let ev = await WDwdataFtp(st, opt)
 *     .catch((err) => {
 *         console.log(err)
 *     })
 * ev.on('change', (msg) => {
 *     delete msg.type
 *     console.log('change', msg)
 * })
 * // change { event: 'start', msg: 'running...' }
 * // change { event: 'proc-callfun-afterStart', msg: 'start...' }
 * // change { event: 'proc-callfun-afterStart', msg: 'done' }
 * // change { event: 'proc-callfun-download', msg: 'start...' }
 * // change { event: 'proc-callfun-download', num: 2, msg: 'done' }
 * // change { event: 'proc-callfun-getCurrent', msg: 'start...' }
 * // change { event: 'proc-callfun-getCurrent', num: 0, msg: 'done' }
 * // change { event: 'proc-compare', msg: 'start...' }
 * // change { event: 'proc-compare', numRemove: 0, numAdd: 2, numModify: 0, numSame: 0, msg: 'done' }
 * // change { event: 'proc-add-callfun-add', id: 'test1.txt', msg: 'start...' }
 * // change { event: 'proc-add-callfun-add', id: 'test1.txt', msg: 'done' }
 * // change { event: 'proc-add-callfun-add', id: 'test2.txt', msg: 'start...' }
 * // change { event: 'proc-add-callfun-add', id: 'test2.txt', msg: 'done' }
 * // ...
 *
 */
let WDwdataFtp = async(st, opt = {}) => {

    //useExpandOnOldFiles
    let useExpandOnOldFiles = get(opt, 'useExpandOnOldFiles')
    if (!isbol(useExpandOnOldFiles)) {
        useExpandOnOldFiles = false
    }

    //useSimulateFiles, 檔案得預先給予至fdDwStorageTemp
    let useSimulateFiles = get(opt, 'useSimulateFiles')
    if (!isbol(useSimulateFiles)) {
        useSimulateFiles = false
    }

    //fdTagRemove
    let fdTagRemove = get(opt, 'fdTagRemove')
    if (!isestr(fdTagRemove)) {
        fdTagRemove = `./_tagRemove`
    }

    //fdDwStorageTemp, 最新下載檔案存放資料夾
    let fdDwStorageTemp = get(opt, 'fdDwStorageTemp')
    if (!isestr(fdDwStorageTemp)) {
        fdDwStorageTemp = `./_dwStorageTemp`
    }
    if (!fsIsFolder(fdDwStorageTemp)) {
        fsCreateFolder(fdDwStorageTemp)
    }

    //fdDwStorage, 合併儲存檔案資料夾
    let fdDwStorage = get(opt, 'fdDwStorage')
    if (!isestr(fdDwStorage)) {
        fdDwStorage = `./_dwStorage`
    }
    if (!fsIsFolder(fdDwStorage)) {
        fsCreateFolder(fdDwStorage)
    }

    //fdDwAttime
    let fdDwAttime = get(opt, 'fdDwAttime')
    if (!isestr(fdDwAttime)) {
        fdDwAttime = `./_dwAttime`
    }
    if (!fsIsFolder(fdDwAttime)) {
        fsCreateFolder(fdDwAttime)
    }

    //fdDwCurrent
    let fdDwCurrent = get(opt, 'fdDwCurrent')
    if (!isestr(fdDwCurrent)) {
        fdDwCurrent = `./_dwCurrent`
    }
    if (!fsIsFolder(fdDwCurrent)) {
        fsCreateFolder(fdDwCurrent)
    }

    //fdResult
    let fdResult = get(opt, 'fdResult')
    if (!isestr(fdResult)) {
        fdResult = `./_result`
    }
    if (!fsIsFolder(fdResult)) {
        fsCreateFolder(fdResult)
    }

    //fdTaskCpActualSrc
    let fdTaskCpActualSrc = get(opt, 'fdTaskCpActualSrc')
    if (!isestr(fdTaskCpActualSrc)) {
        fdTaskCpActualSrc = `./_taskCpActualSrc`
    }
    if (!fsIsFolder(fdTaskCpActualSrc)) {
        fsCreateFolder(fdTaskCpActualSrc)
    }

    //fdTaskCpSrc
    let fdTaskCpSrc = get(opt, 'fdTaskCpSrc')
    if (!isestr(fdTaskCpSrc)) {
        fdTaskCpSrc = './_taskCpSrc'
    }
    if (!fsIsFolder(fdTaskCpSrc)) {
        fsCreateFolder(fdTaskCpSrc)
    }

    //fdLog
    let fdLog = get(opt, 'fdLog')
    if (!isestr(fdLog)) {
        fdLog = './_logs'
    }
    if (!fsIsFolder(fdLog)) {
        fsCreateFolder(fdLog)
    }

    //funDownload
    let funDownload = get(opt, 'funDownload')

    //funGetCurrent
    let funGetCurrent = get(opt, 'funGetCurrent')

    //funAdd
    let funAdd = get(opt, 'funAdd')

    //funModify
    let funModify = get(opt, 'funModify')

    //funRemove
    let funRemove = get(opt, 'funRemove')

    //funAfterStartCall
    let funAfterStartCall = get(opt, 'funAfterStart')

    //funBeforeEndCall
    let funBeforeEndCall = get(opt, 'funBeforeEnd')

    //timeToleranceRemove
    let timeToleranceRemove = get(opt, 'timeToleranceRemove')
    if (!isp0int(timeToleranceRemove)) {
        timeToleranceRemove = 0
    }
    timeToleranceRemove = cdbl(timeToleranceRemove)

    //getFilesHash
    let getFilesHash = async(vfps) => {

        //ltdtHash
        let ltdtHash = await pmSeries(vfps, async(v) => {
            let id = v.name //用檔名做id
            let hash = await fsGetFileBasicHash(v.path, { type: 'md5' })
            return {
                id,
                hash,
            }
        })

        return ltdtHash
    }

    //treeFolderAndGetFilesHash
    let treeFolderAndGetFilesHash = async (fd) => {

        //vfps
        let vfps = await fsTreeFolderWithHash(fd, 1, { type: 'md5', forFile: true, forFolder: false })

        //ltdtHash
        let ltdtHash = map(vfps, (v) => {
            let id = v.name //用檔名做id
            let hash = v.hash
            return {
                id,
                hash,
            }
        })

        return ltdtHash
    }

    //cvLtdtToKp
    let cvLtdtToKp = (ltdt) => {
        let kp = {}
        each(ltdt, (v) => {
            kp[v.id] = v.hash
        })
        return kp
    }

    //mergeLtdt
    let mergeLtdt = (ltdtNew, ltdtOld) => {
        let kpNew = cvLtdtToKp(ltdtNew)
        let kpOld = cvLtdtToKp(ltdtOld)
        let kp = cloneDeep(kpOld)
        each(kpNew, (hash, id) => {
            kp[id] = hash
        })
        let ltdt = []
        each(kp, (hash, id) => {
            ltdt.push({
                id,
                hash,
            })
        })
        return ltdt
    }

    //vfpsDw
    let vfpsDw = []

    //funDownloadDef
    let funDownloadDef = async() => {

        //vfpsDw, 為下載所得新增檔案清單, 檔案放置於fdDwStorageTemp內
        vfpsDw = await downloadFiles(st, fdDwStorageTemp, {
            useExpandOnOldFiles,
            useSimulateFiles,
        })
        // console.log('vfpsDw', vfpsDw[0], size(vfpsDw))

        //check, 無檔案須報錯, 底層會中止與計算hash與計算檔案hash差異
        if (size(vfpsDw) === 0) {
            throw new Error(`no files`)
        }

        //ltdtHashNewTemp, 計算新增檔案hash值
        let ltdtHashNewTemp = await getFilesHash(vfpsDw)

        //ltdtHashNew
        let ltdtHashNew = []
        if (useExpandOnOldFiles) {

            //ltdtHashOld, 數據來源為fdDwStorage, 為舊hash清單
            let ltdtHashOld = await treeFolderAndGetFilesHash(fdDwStorage)

            //最新合併後檔案hash值清單
            ltdtHashNew = mergeLtdt(ltdtHashNewTemp, ltdtHashOld)

        }
        else {

            //當前下載檔案為全部檔案, 各檔案計算hash值皆須為新hash
            ltdtHashNew = ltdtHashNewTemp

        }
        // console.log('ltdtHashNew', ltdtHashNew)

        //清空fdDwAttime
        fsCleanFolder(fdDwAttime)

        //儲存新hash檔案至fdDwAttime
        each(ltdtHashNew, (v) => {

            //fp
            let fp = `${fdDwAttime}/${v.id}.json` //v.id雖為檔名但視為id使用, fdDwAttime與fdDwCurrent內檔案皆為對應hash檔案, 副檔名為.json

            //writeFileSync
            fs.writeFileSync(fp, JSON.stringify(v), 'utf8')

        })

        return ltdtHashNew
    }
    if (!isfun(funDownload)) {
        funDownload = funDownloadDef
    }

    //funGetCurrentDef
    let funGetCurrentDef = async() => {

        //ltdtHashOld, 數據來源為fdDwStorage, 為舊hash清單
        let ltdtHashOld = await treeFolderAndGetFilesHash(fdDwStorage)

        return ltdtHashOld
    }
    if (!isfun(funGetCurrent)) {
        funGetCurrent = funGetCurrentDef
    }

    //funRemoveDef
    let funRemoveDef = async(v) => {

        let fd = `${fdResult}/${v.id}`
        if (fsIsFolder(fd)) {

            let r = fsDeleteFolder(fd)

            if (r.error) {
                throw new Error(r.error)
            }

        }

    }
    if (!isfun(funRemove)) {
        funRemove = funRemoveDef
    }

    //funAddDef
    let funAddDef = async(v) => {

        let fd = `${fdResult}/${v.id}`
        if (!fsIsFolder(fd)) {
            fsCreateFolder(fd)
        }
        fsCleanFolder(fd)

        let fpStorage = `${fdDwStorageTemp}/${v.id}` //fdDwStorageTemp為新下載檔案, 使用v.id為實際檔案名稱
        let fpResult = `${fd}/${v.id}`
        let r = fsCopyFile(fpStorage, fpResult)

        if (r.error) {
            throw new Error(r.error)
        }

    }
    if (!isfun(funAdd)) {
        funAdd = funAddDef
    }

    //funModifyDef
    let funModifyDef = async(v) => {

        let fd = `${fdResult}/${v.id}`
        if (!fsIsFolder(fd)) {
            fsCreateFolder(fd)
        }
        fsCleanFolder(fd)

        let fpStorage = `${fdDwStorageTemp}/${v.id}` //fdDwStorageTemp為新下載檔案, 使用v.id為實際檔案名稱
        let fpResult = `${fd}/${v.id}`
        let r = fsCopyFile(fpStorage, fpResult)

        if (r.error) {
            throw new Error(r.error)
        }

    }
    if (!isfun(funModify)) {
        funModify = funModifyDef
    }

    //funBeforeEndNec
    let funBeforeEndNec = async() => {

        try {
            srlog.info({ event: 'move-files-to-storage', msg: 'start...' })

            //check, 無檔案須報錯, 底層會中止與計算hash與計算檔案hash差異
            if (size(vfpsDw) === 0) {
                throw new Error(`no files`)
            }

            //useExpandOnOldFiles
            if (useExpandOnOldFiles) {
                //增量模式, 僅將新下載檔案儲存至fdDwStorage

                //複製fdDwStorageTemp內新下載檔案至合併儲存資料夾fdDwStorage
                each(vfpsDw, (v) => {

                    //fsCopyFile
                    let fpSrc = v.path
                    let fpTar = `${fdDwStorage}/${v.name}`
                    let r = fsCopyFile(fpSrc, fpTar)

                    //check
                    if (r.error) {
                        throw new Error(r.error)
                    }

                })

            }
            else {
                //全量模式, 將新下載檔案視為全部檔案並儲存至fdDwStorage

                //fsSyncFolder, 將fdDwStorageTemp完全同步至fdDwStorage
                await fsSyncFolder(fdDwStorageTemp, fdDwStorage)

            }

            //清空fdDwStorageTemp
            fsCleanFolder(fdDwStorageTemp)

            srlog.info({ event: 'move-files-to-storage', msg: 'done' })
        }
        catch (err) {
            console.log(err)
            srlog.error({ event: 'move-files-to-storage', msg: getErrorMessage(err) })
        }

    }

    let funAfterStart = async() => {

        if (isfun(funAfterStartCall)) {
            let r = funAfterStartCall()
            if (ispm(r)) {
                r = await r
            }
        }

        //無funAfterStartNec

    }

    let funBeforeEnd = async() => {

        await funBeforeEndNec()

        if (isfun(funBeforeEndCall)) {
            let r = funBeforeEndCall()
            if (ispm(r)) {
                r = await r
            }
        }

    }

    let optBdr = {
        fdTagRemove,
        fdDwAttime,
        fdDwCurrent,
        fdResult,
        fdTaskCpActualSrc,
        fdTaskCpSrc,
        fdLog,
        funDownload,
        funGetCurrent,
        funRemove,
        funAdd,
        funModify,
        funAfterStart,
        funBeforeEnd,
        timeToleranceRemove,
    }
    let ev = await WDwdataBuilder(optBdr)

    //srlog
    let srlog = ev.srlog

    return ev
}


export default WDwdataFtp