WConverhpServer.mjs

import path from 'path'
import fs from 'fs'
import stream from 'stream'
import Hapi from '@hapi/hapi'
import Inert from '@hapi/inert' //提供靜態檔案
import get from 'lodash-es/get.js'
import isNumber from 'lodash-es/isNumber.js'
import genPm from 'wsemi/src/genPm.mjs'
import evem from 'wsemi/src/evem.mjs'
import iseobj from 'wsemi/src/iseobj.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import isp0int from 'wsemi/src/isp0int.mjs'
import ispint from 'wsemi/src/ispint.mjs'
import isearr from 'wsemi/src/isearr.mjs'
import isbol from 'wsemi/src/isbol.mjs'
import isfun from 'wsemi/src/isfun.mjs'
import ispm from 'wsemi/src/ispm.mjs'
import cint from 'wsemi/src/cint.mjs'
import cstr from 'wsemi/src/cstr.mjs'
import str2b64 from 'wsemi/src/str2b64.mjs'
import haskey from 'wsemi/src/haskey.mjs'
import obj2u8arr from 'wsemi/src/obj2u8arr.mjs'
import u8arr2obj from 'wsemi/src/u8arr2obj.mjs'
import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
import fsCreateFolder from 'wsemi/src/fsCreateFolder.mjs'
import mmg from './managerMergeSlices.mjs'
// import checkTotalHash from './checkTotalHash.mjs'
import checkTotalHash from './checkTotalHash.wk.umd.js'
// import checkSlicesHash from './checkSlicesHash.mjs'
import checkSlicesHash from './checkSlicesHash.wk.umd.js'


//回傳前端stream時(POST或GET皆可), 前端會須等stream傳完才能判斷是否為大檔或錯誤訊息, 此會導致若回傳超大檔, 會需要對超大檔進行解析會有記憶體上限問題, 故需要通過header提供基本成功或失敗訊息, 讓前端能進行解析判斷
//回傳前端(nodejs)時, 針對超大檔, 只能用POST並用stream回傳
//回傳前端(browser)時, 針對超大檔, 可用POST並用stream回傳但還要處理進度條, 若要交由瀏覽器下載器處理, 只能用GET並用stream回傳, 且前端只能用window.location.href或a.href+a.click()下載


/**
 * 建立Hapi伺服器
 *
 * @class
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {Integer} [opt.port=8080] 輸入Hapi伺服器所在port正整數,預設8080
 * @param {Boolean} [opt.useInert=true] 輸入是否提供瀏覽pathStaticFiles資料夾內檔案之布林值,預設true
 * @param {String} [opt.pathStaticFiles='dist'] 輸入當useInert=true時提供瀏覽資料夾字串,預設'dist'
 * @param {String} [opt.apiName='api'] 輸入API名稱字串,預設'api'
 * @param {String} [opt.pathUploadTemp='./uploadTemp'] 輸入暫時存放切片上傳檔案資料夾字串,預設'./uploadTemp'
 * @param {String} [opt.tokenType='Bearer'] 輸入token類型字串,預設'Bearer'
 * @param {Integer} [opt.sizeSlice=1024*1024] 輸入切片上傳檔案之切片檔案大小整數,單位為Byte,預設為1024*1024
 * @param {Function} [opt.verifyConn=()=>{return true}] 輸入呼叫API時檢測函數,預設()=>{return true}
 * @param {Array} [opt.corsOrigins=['*']] 輸入允許跨域網域陣列,若給予['*']代表允許全部,預設['*']
 * @param {Integer} [opt.delayForSlice=100] 輸入切片上傳檔案API用延遲響應時間,單位ms,預設100
 * @param {Boolean} [opt.serverHapi=null] 輸入外部提供Hapi伺服器物件,預設null
 * @returns {Object} 回傳事件物件,可監聽事件execute、upload、handler
 * @example
 *
 * import fs from 'fs'
 * import _ from 'lodash-es'
 * import w from 'wsemi'
 * import WConverhpServer from './src/WConverhpServer.mjs'
 *
 * let ms = []
 *
 * let opt = {
 *     port: 8080,
 *     apiName: 'api',
 *     pathStaticFiles: '.', //要存取專案資料夾下web.html, 故不能給dist
 *     verifyConn: async ({ apiType, authorization, headers, query }) => {
 *         console.log('verifyConn', `apiType[${apiType}]`, `authorization[${authorization}]`)
 *         let token = w.strdelleft(authorization, 7) //刪除Bearer
 *         if (!w.isestr(token)) {
 *             return false
 *         }
 *         // await w.delay(3000)
 *         return true
 *     },
 * }
 *
 * //new
 * let wo = new WConverhpServer(opt)
 *
 * wo.on('execute', (func, input, pm) => {
 *     // console.log(`Server[port:${opt.port}]: execute`, func, input)
 *     console.log(`Server[port:${opt.port}]: execute`, func)
 *
 *     try {
 *
 *         if (func === 'add') {
 *
 *             if (_.get(input, 'p.d.u8a', null)) {
 *                 console.log('input.p.d.u8a', input.p.d.u8a)
 *                 ms.push({ 'input.p.d.u8a': input.p.d.u8a })
 *             }
 *
 *             let r = {
 *                 _add: input.p.a + input.p.b,
 *                 _data: [11, 22.22, 'abc', { x: '21', y: 65.43, z: 'test中文' }],
 *                 _bin: {
 *                     name: 'zdata.b2',
 *                     u8a: new Uint8Array([52, 66, 97, 115]),
 *                 },
 *             }
 *
 *             pm.resolve(r)
 *
 *         }
 *         else {
 *             console.log('invalid func')
 *             pm.reject('invalid func')
 *         }
 *
 *     }
 *     catch (err) {
 *         console.log('execute error', err)
 *         pm.reject('execute error')
 *     }
 *
 * })
 * wo.on('upload', (input, pm) => {
 *     console.log(`Server[port:${opt.port}]: upload`, input)
 *
 *     try {
 *         ms.push({ 'receive and return': input })
 *         let output = input
 *         pm.resolve(output)
 *     }
 *     catch (err) {
 *         console.log('upload error', err)
 *         pm.reject('upload error')
 *     }
 *
 * })
 * wo.on('download', (input, pm) => {
 *     console.log(`Server[port:${opt.port}]: download`, input)
 *
 *     let streamRead = null
 *     try {
 *         ms.push({ 'download': input })
 *
 *         //fp
 *         let fp = `./test/1mb.7z`
 *
 *         //check, 檔案存在才往下
 *
 *         //fileSize
 *         let stats = fs.statSync(fp)
 *         let fileSize = stats.size
 *
 *         //streamRead
 *         streamRead = fs.createReadStream(fp)
 *
 *         //filename
 *         let filename = `1mb中文.7z` //測試支援中文
 *
 *         //fileType
 *         let fileType = 'application/x-7z-compressed'
 *
 *         //output
 *         let output = {
 *             streamRead,
 *             filename,
 *             fileSize,
 *             fileType,
 *         }
 *
 *         pm.resolve(output)
 *     }
 *     catch (err) {
 *         console.log('download error', err)
 *         // try {
 *         //     streamRead.destroy() //若fs.createReadStream早於fs.statSync執行, 但fs.statSync發生錯誤時, stream得要destroy
 *         // }
 *         // catch (err) {}
 *         pm.reject('download error')
 *     }
 *
 * })
 * wo.on('error', (err) => {
 *     console.log(`Server[port:${opt.port}]: error`, err)
 * })
 * wo.on('handler', (data) => {
 *     // console.log(`Server[port:${opt.port}]: handler`, data)
 * })
 *
 * setTimeout(() => {
 *     console.log('ms', ms)
 *     // console.log('ms', JSON.stringify(ms))
 *     wo.stop()
 * }, 3000)
 *
 */
function WConverhpServer(opt = {}) {

    //port
    let port = get(opt, 'port')
    if (!ispint(port)) {
        port = 8080
    }

    //useInert
    let useInert = get(opt, 'useInert')
    if (!isbol(useInert)) {
        useInert = true
    }

    //pathStaticFiles
    let pathStaticFiles = get(opt, 'pathStaticFiles')
    if (!isestr(pathStaticFiles)) {
        pathStaticFiles = 'dist'
    }

    //apiName
    let apiName = get(opt, 'apiName')
    if (!isestr(apiName)) {
        apiName = 'api'
    }

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

    //tokenType
    let tokenType = get(opt, 'tokenType')
    if (!isestr(tokenType)) {
        tokenType = 'Bearer'
    }

    //sizeSlice
    let sizeSlice = get(opt, 'sizeSlice')
    if (!ispint(sizeSlice)) {
        sizeSlice = 1024 * 1024 //1m
    }

    //verifyConn
    let verifyConn = get(opt, 'verifyConn')
    if (!isfun(verifyConn)) {
        verifyConn = () => {
            return true
        }
    }

    //corsOrigins
    let corsOrigins = get(opt, 'corsOrigins', [])
    if (!isearr(corsOrigins)) {
        corsOrigins = ['*']
    }

    //delayForSlice
    let delayForSlice = get(opt, 'delayForSlice', '')
    if (!isp0int(delayForSlice)) {
        delayForSlice = 100
    }
    delayForSlice = cint(delayForSlice)

    //server
    let server = null
    if (get(opt, 'serverHapi')) {

        //use serverHapi
        server = opt.serverHapi

    }
    else {

        //create server
        server = Hapi.server({
            //host: 'localhost',
            port,
            routes: {
                timeout: {
                    server: false, //關閉伺服器超時
                    socket: false, //關閉socket超時
                },
                cors: {
                    origin: corsOrigins, //Access-Control-Allow-Origin
                    credentials: false, //Access-Control-Allow-Credentials
                },
            },
        })

    }

    //ee
    let ee = evem() //new events.EventEmitter()

    //eeEmit
    let eeEmit = (name, ...args) => {
        setTimeout(() => {
            ee.emit(name, ...args)
        }, 1)
    }

    //procDeal
    async function procDeal(data) {

        //pm, pmm
        let pm = genPm()
        let pmm = genPm()

        //重新處理回傳結果
        pmm
            .then((output) => {

                //add output
                data['output'] = output

                //delete input, 因input可能很大故回傳數據不包含原input
                delete data['input']

                pm.resolve(data)
            })
            .catch((err) => {
                pm.reject(err)
            })

        if (true) {

            //func
            let func = get(data, 'func', '')

            //input
            let input = get(data, 'input', null)

            //execute 執行
            eeEmit('execute', func, input, pmm)

        }

        return pm
    }

    //procUpload
    async function procUpload(input) {
        // console.log('procUpload', input)

        //pm, pmm
        let pm = genPm()
        let pmm = genPm()

        //重新處理回傳結果
        pmm
            .then((output) => {

                //resolve
                pm.resolve(output)

            })
            .catch((err) => {
                pm.reject(err)
            })

        if (true) {

            //upload, 上傳檔案
            eeEmit('upload', input, pmm)

        }

        return pm
    }

    //procDownload
    async function procDownload(input) {
        // console.log('procDownload', input)

        //pm, pmm
        let pm = genPm()
        let pmm = genPm()

        //重新處理回傳結果
        pmm
            .then((output) => {

                //resolve
                pm.resolve(output)

            })
            .catch((err) => {
                pm.reject(err)
            })

        if (true) {

            //download, 下載檔案
            eeEmit('download', input, pmm)

        }

        return pm
    }

    //responseU8aStream
    function responseU8aStream(res, u8a, opt = {}) {

        //stream
        let smr = new stream.Readable()
        smr._read = () => {}
        smr.push(u8a)
        smr.push(null)

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

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

        //r
        let r = res.response(smr)
            .header('Cache-Control', 'no-cache, no-store, must-revalidate')
            .header('Content-Type', 'application/octet-stream')
            .header('Content-Length', smr.readableLength)
        if (isestr(returnType)) {
            r.header('Return-Type', returnType)
        }
        if (isestr(returnMsg)) {
            r.header('Return-Msg', returnMsg)
        }

        return r
    }

    //responseU8aStreamWithError
    function responseU8aStreamWithError(res, msg) {

        //check
        if (!isestr(msg)) {
            console.log('msg', msg)
            console.log(`msg is not an effective string, set msg=''`)
            msg = ''
        }

        //u8aOut
        let u8aOut = obj2u8arr({
            error: msg,
        })
        // console.log('download u8aOut', u8aOut)

        //str2b64
        // msg = str2b64(msg) //預期程式內調用皆為英文, 不須轉base64來支援中文

        return responseU8aStream(res, u8aOut, { returnType: 'error', returnMsg: msg })
    }

    //apiMain
    let apiMain = {
        path: `/${apiName}`,
        method: 'POST',
        options: {
            payload: {
                maxBytes: 1024 * 1024 * 1024 * 1024, //預設為1mb, 調整至1tb, 也就是給予3次方
                maxParts: 1000 * 1000 * 1000, //預設為1000, 給予3次方
                timeout: false, //避免請求未完成時中斷
                output: 'stream', //代表前端用stream傳至伺服器(Content-Type為application/octet-stream)
                parse: false,
            },
            timeout: {
                server: false, //關閉伺服器超時
                socket: false, //關閉socket超時
            },
        },
        handler: async function (req, res) {
            // console.log(req, res)
            // console.log('payload', req.payload)

            //headers
            let headers = get(req, 'headers')
            headers = iseobj(headers) ? headers : ''
            // console.log('headers', headers)

            //query
            let query = get(req, 'query')
            query = iseobj(query) ? query : ''
            // console.log('query', query)

            //authorization
            let authorization = get(headers, 'authorization', '')
            authorization = isestr(authorization) ? authorization : ''

            //check
            if (true) {

                //verifyConn
                let m = verifyConn({ apiType: 'main', authorization, headers, query })
                if (ispm(m)) {
                    m = await m
                }

                //check
                if (m !== true) {
                    return responseU8aStreamWithError(res, 'permission denied')
                }

            }

            //eeEmit
            eeEmit('handler', {
                api: 'apiMain',
                headers,
                query,
            })

            //receive
            let receive = () => {

                //pm
                let pm = genPm()

                //chunks
                let chunks = []
                let smw = new stream.Writable({
                    write(chunk, encoding, cb) {
                        // console.log('smw receive payload', chunk)

                        //push
                        chunks.push(chunk)
                        // console.log('chunk.length', chunk.length)

                        //cb
                        cb()

                    }
                })

                //finish
                smw.on('finish', () => {
                    // console.log(`smw finish`)
                })

                //pipe
                req.payload.pipe(smw)

                //end
                req.payload.on('end', () => {
                    // console.log(`req.payload end`)

                    //bb
                    let bb = Buffer.concat(chunks)
                    // console.log('bb', bb, bb.length)

                    //clear, 釋放記憶體
                    chunks = []

                    //resolve
                    pm.resolve(bb)

                })

                //close
                req.payload.on('close', () => {
                    // console.log(`req.payload close`)
                })

                //error
                req.payload.on('error', (err) => {
                    // console.log(`req.payload error`, err)
                    eeEmit('error', `receive payload err`)
                    pm.reject(err.message)
                })

                return pm
            }

            //receive
            let bbInp = await receive()
            // console.log('bbInp', bbInp)

            //u8aInp
            let u8aInp = new Uint8Array(bbInp)
            // console.log('u8aInp', u8aInp)

            //u8arr2obj
            let inp = u8arr2obj(u8aInp)
            // console.log('inp', inp)

            //procDeal
            let out = {}
            let returnType = ''
            let returnMsg = ''
            await procDeal(inp)
                .then((res) => {
                    out.success = res
                    returnType = 'success'
                    returnMsg = 'need to parse'
                })
                .catch((err) => {
                    out.error = err
                    returnType = 'error'
                    returnMsg = 'need to parse'
                })
            // console.log('out', out)

            //u8aOut
            let u8aOut = obj2u8arr(out)
            // console.log('u8aOut', u8aOut)

            return responseU8aStream(res, u8aOut, { returnType, returnMsg })
        },
    }

    //apiUploadCheck
    let apiUploadCheck = {
        path: `/${apiName}ulctr`,
        method: 'POST',
        options: {
            payload: {
                maxBytes: 1024 * 1024 * 1024 * 1024, //預設為1mb, 調整至1tb, 也就是給予3次方
                maxParts: 1000 * 1000 * 1000, //預設為1000, 給予3次方
                timeout: false, //避免請求未完成時中斷
                // output: 'stream',
                parse: true, //前端送obj過來須自動解析
            },
            timeout: {
                server: false, //關閉伺服器超時
                socket: false, //關閉socket超時
            },
        },
        handler: async function (req, res) {
            // console.log(req, res)
            // console.log('payload', req.payload)

            //headers
            let headers = get(req, 'headers')
            headers = iseobj(headers) ? headers : ''
            // console.log('headers', headers)

            //query
            let query = get(req, 'query')
            query = iseobj(query) ? query : ''
            // console.log('query', query)

            //authorization
            let authorization = get(headers, 'authorization', '')
            authorization = isestr(authorization) ? authorization : ''

            //check
            if (true) {

                //verifyConn
                let m = verifyConn({ apiType: 'upload-controller', authorization, headers, query })
                if (ispm(m)) {
                    m = await m
                }

                //check
                if (m !== true) {
                    return responseU8aStreamWithError(res, 'permission denied')
                }

            }

            //eeEmit
            eeEmit('handler', {
                api: 'apiUploadCheck',
                headers,
                query,
            })

            //mode, 從payload接收
            let mode = get(req, 'payload.mode', '')

            //check
            if (mode !== 'check-total-hash' && mode !== 'check-slices-hash' && mode !== 'merge-slices-push' && mode !== 'merge-slices-get') {
                // console.log('invalid mode in payload')
                return responseU8aStreamWithError(res, `invalid mode[${mode}] in payload`)
            }

            //fileHash, 從payload接收
            let fileHash = get(req, 'payload.fileHash', '')
            // console.log(mode, 'fileHash', fileHash)

            //check
            if (!isestr(fileHash)) {
                // console.log('invalid fileHash in payload')
                return responseU8aStreamWithError(res, 'invalid fileHash in payload')
            }

            //procCore
            let procCore = async() => {
                let out = null
                if (mode === 'check-total-hash') {

                    //filename, 從payload接收
                    let filename = get(req, 'payload.filename', '')
                    // console.log(mode, 'filename', filename)

                    //fileSize, 從payload接收
                    let fileSize = get(req, 'payload.fileSize', '')
                    // console.log(mode, 'fileSize', fileSize)

                    //checkTotalHash
                    out = await checkTotalHash(fileSize, sizeSlice, fileHash, pathUploadTemp)
                    // console.log(mode, 'out', out)

                    //check, 因合併大檔後可能非預期中斷而重傳, 每次偵測有合併完成大檔, 就得調用procUpload讓伺服器攔截函數處理
                    if (out.bAllHash) {

                        //ri
                        let ri = {
                            filename,
                            path: out.path,
                        }

                        //procUpload
                        // console.log('procUpload start')
                        let ro = await procUpload(ri)
                        // console.log('procUpload done', ro)

                        //out merge, 直接添加ro就回傳, 此處不使用msg儲存
                        out = {
                            ...out,
                            ...ro,
                        }

                    }

                }
                else if (mode === 'check-slices-hash') {

                    //fileSliceHashs, 從payload接收
                    let fileSliceHashs = get(req, 'payload.fileSliceHashs', [])
                    // console.log(mode, 'fileSliceHashs', fileSliceHashs)

                    //checkSlicesHash
                    out = await checkSlicesHash(fileSliceHashs, fileHash, pathUploadTemp)
                    // console.log(mode, 'out', out)

                }
                else if (mode === 'merge-slices-push') {

                    //chunkTotal, 從payload接收
                    let chunkTotal = get(req, 'payload.chunkTotal', '')
                    // console.log(mode, 'chunkTotal', chunkTotal)

                    //mmg.push
                    let queueId = mmg.push(fileHash, chunkTotal, pathUploadTemp)

                    //out
                    out = {
                        queueId,
                    }

                }
                else if (mode === 'merge-slices-get') {

                    //filename, 從payload接收
                    let filename = get(req, 'payload.filename', '')
                    // console.log(mode, 'filename', filename)

                    //queueId, 從payload接收
                    let queueId = get(req, 'payload.queueId', '')
                    // console.log(mode, 'queueId', queueId)

                    //mmg.get
                    let r = mmg.get(queueId, pathUploadTemp)

                    //out
                    out = {
                        state: r.state,
                        msg: r.msg, //state為'error'時會於msg提供錯誤訊息
                        queueId,
                        filename,
                        path: r.path,
                    }

                    //check
                    if (r.state === 'success') {

                        //ri
                        let ri = {
                            filename,
                            path: r.path,
                        }

                        //procUpload, 偵測有合併完成大檔, 得調用procUpload讓伺服器攔截函數處理
                        // console.log('procUpload start')
                        let ro = await procUpload(ri)
                        // console.log('procUpload done', ro)

                        //out merge, ro須附加至msg, 才供前端偵測state為'success'時提取msg顯示
                        out = {
                            ...out, //state為'success'時msg為空字串, 直接被ro複寫至msg即可
                            msg: ro,
                        }

                    }

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

                return out
            }

            //procCore
            let out = {}
            let returnType = ''
            let returnMsg = ''
            await procCore()
                .then((res) => {
                    out.success = res
                    returnType = 'success'
                    returnMsg = 'need to parse'
                })
                .catch((err) => {
                    out.error = err
                    returnType = 'error'
                    returnMsg = 'need to parse'
                })
            // console.log('out', out)

            //u8aOut
            let u8aOut = obj2u8arr(out)
            // console.log('u8aOut', u8aOut)

            return responseU8aStream(res, u8aOut, { returnType, returnMsg })
        },
    }

    //apiUploadSlice
    let apiUploadSlice = {
        path: `/${apiName}slc`,
        method: 'POST',
        options: {
            payload: {
                maxBytes: 1024 * 1024 * 1024 * 1024, //預設為1mb, 調整至1tb, 也就是給予3次方
                maxParts: 1000 * 1000 * 1000, //預設為1000, 給予3次方
                timeout: false, //避免請求未完成時中斷
                output: 'stream', //代表前端用stream傳至伺服器(Content-Type為application/octet-stream)
                parse: false,
            },
            timeout: {
                server: false, //關閉伺服器超時
                socket: false, //關閉socket超時
            },
        },
        handler: async function (req, res) {
            // console.log(req, res)
            // console.log('payload', req.payload)

            //headers
            let headers = get(req, 'headers')
            headers = iseobj(headers) ? headers : ''
            // console.log('headers', headers)

            //query
            let query = get(req, 'query')
            query = iseobj(query) ? query : ''
            // console.log('query', query)

            //authorization
            let authorization = get(headers, 'authorization', '')
            authorization = isestr(authorization) ? authorization : ''

            //check
            if (true) {

                //verifyConn
                let m = verifyConn({ apiType: 'upload-slice', authorization, headers, query })
                if (ispm(m)) {
                    m = await m
                }

                //check
                if (m !== true) {
                    return responseU8aStreamWithError(res, 'permission denied')
                }

            }

            //eeEmit
            eeEmit('handler', {
                api: 'apiUploadSlice',
                headers,
                query,
            })

            //chunkIndex, chunkTotal, packageId, 從headers接收
            let chunkIndex = get(headers, 'chunk-index', '')
            let chunkTotal = get(headers, 'chunk-total', '')
            let packageId = get(headers, 'package-id', '')

            //check
            if (!isp0int(chunkIndex)) {
                // console.log('invalid chunkIndex in headers')
                return responseU8aStreamWithError(res, 'invalid chunkIndex in headers')
            }
            chunkIndex = cint(chunkIndex)
            if (!isp0int(chunkTotal)) {
                // console.log('invalid chunkTotal in headers')
                return responseU8aStreamWithError(res, 'invalid chunkTotal in headers')
            }
            chunkTotal = cint(chunkTotal)
            if (!isestr(packageId)) {
                // console.log('invalid packageId in headers')
                return responseU8aStreamWithError(res, 'invalid packageId in headers')
            }

            //pathFileChunk
            let pathFileChunk = path.join(pathUploadTemp, `${packageId}_${chunkIndex}`)
            // console.log('pathFileChunk', pathFileChunk)

            //streamWrite
            let streamWrite = fs.createWriteStream(pathFileChunk)

            //receive
            let receive = () => {

                //pm
                let pm = genPm()

                //pipe
                req.payload.pipe(streamWrite)
                // console.log(`receiving chunk[${chunkIndex + 1}/${chunkTotal}]...`)

                //end
                req.payload.on('end', () => {
                    // console.log(`receive chunk[${chunkIndex + 1}/${chunkTotal}] done`)

                    //setTimeout, 切片上傳添加延遲處理, 避免佔滿伺服器CPU與流量
                    setTimeout(() => {
                        pm.resolve(`chunk[${chunkIndex}/${chunkTotal}] of packageId[${packageId}] done`)
                    }, delayForSlice)

                })

                //error
                req.payload.on('error', (err) => {
                    // console.log(`receive chunk[${chunkIndex + 1}/${chunkTotal}] of packageId[${packageId}] err`, err)
                    eeEmit('error', `receive chunk[${chunkIndex}/${chunkTotal}] of packageId[${packageId}] err`)
                    pm.reject(`apiUploadSlice receive error: ${err.message}`)
                })

                return pm
            }

            //receive
            let out = {}
            let returnType = ''
            let returnMsg = ''
            await receive()
                .then((res) => {
                    out.success = res
                    returnType = 'success'
                    returnMsg = 'need to parse'
                })
                .catch((err) => {
                    out.error = err
                    returnType = 'error'
                    returnMsg = 'need to parse'
                    eeEmit('error', err)
                })
            // console.log('out', out)

            //u8aOut
            let u8aOut = obj2u8arr(out)
            // console.log('u8aOut', u8aOut)

            return responseU8aStream(res, u8aOut, { returnType, returnMsg })
        },
    }

    //apiDownloadGetFilename
    let apiDownloadGetFilename = {
        path: `/${apiName}dwgfn`,
        method: 'POST',
        options: {
            payload: {
                maxBytes: 1024 * 1024 * 1024 * 1024, //預設為1mb, 調整至1tb, 也就是給予3次方
                maxParts: 1000 * 1000 * 1000, //預設為1000, 給予3次方
                timeout: false, //避免請求未完成時中斷
                // output: 'stream',
                parse: true, //前端送obj過來須自動解析
            },
            timeout: {
                server: false, //關閉伺服器超時
                socket: false, //關閉socket超時
            },
        },
        handler: async function (req, res) {
            // console.log(req, res)
            // console.log('payload', req.payload)

            //headers
            let headers = get(req, 'headers')
            headers = iseobj(headers) ? headers : ''
            // console.log('headers', headers)

            //query
            let query = get(req, 'query')
            query = iseobj(query) ? query : ''
            // console.log('query', query)

            //authorization
            let authorization = get(headers, 'authorization', '')
            authorization = isestr(authorization) ? authorization : ''

            //check
            if (true) {

                //verifyConn
                let m = verifyConn({ apiType: 'download-get-filename', authorization, headers, query })
                if (ispm(m)) {
                    m = await m
                }

                //check
                if (m !== true) {
                    return responseU8aStreamWithError(res, 'permission denied')
                }

            }

            //eeEmit
            eeEmit('handler', {
                api: 'apiDownloadGetFilename',
                headers,
                query,
            })

            //fileId, 從payload接收
            let fileId = get(req, 'payload.fileId', '')
            // console.log('fileId', fileId)

            //check
            if (!isestr(fileId)) {
                // console.log('invalid fileId in payload')
                return responseU8aStreamWithError(res, 'invalid fileId in payload')
            }

            //inp
            let inp = { fileId }

            //procDownload
            let out = {}
            let returnType = ''
            let returnMsg = ''
            await procDownload(inp)
                .then((res) => {
                    out.success = res
                    returnType = 'success'
                    returnMsg = 'need to parse'
                })
                .catch((err) => {
                    out.error = err
                    returnType = 'error'
                    returnMsg = 'need to parse'
                })
            // console.log('out', out)

            //r
            let r = get(out, 'success')

            //streamRead
            let streamRead = get(r, 'streamRead')

            //destroy, 不提供stream故須預先destroy
            try {
                streamRead.destroy()
            }
            catch (err) {}

            //filename
            let filename = get(r, 'filename')
            if (!isestr(filename)) {
                //已於前面destroy
                return responseU8aStreamWithError(res, 'invalid filename')
            }

            //重新提供out
            out = {
                success: {
                    filename,
                },
            }

            //u8aOut
            let u8aOut = obj2u8arr(out)
            // console.log('u8aOut', u8aOut)

            return responseU8aStream(res, u8aOut, { returnType, returnMsg })
        },
    }

    //apiDownloadGetFile
    let apiDownloadGetFile = {
        path: `/${apiName}dwgf`,
        method: 'GET',
        options: {
            timeout: {
                server: false, //關閉伺服器超時
                socket: false, //關閉socket超時
            },
        },
        handler: async function (req, res) {
            // console.log(req, res)
            // console.log('payload', req.payload)

            //headers
            let headers = get(req, 'headers')
            headers = iseobj(headers) ? headers : ''
            // console.log('headers', headers)

            //query
            let query = get(req, 'query')
            query = iseobj(query) ? query : ''
            // console.log('query', query)

            //token
            let token = get(query, 'token', '')
            token = isestr(token) ? token : ''
            // console.log('token', token)

            //authorization
            let authorization = ''
            if (isestr(token)) {
                authorization = `${tokenType} ${token}`
            }

            //check
            if (true) {

                //verifyConn
                let m = verifyConn({ apiType: 'download-get-file', authorization, headers, query })
                if (ispm(m)) {
                    m = await m
                }

                //check
                if (m !== true) {
                    return responseU8aStreamWithError(res, 'permission denied')
                }

            }

            //eeEmit
            eeEmit('handler', {
                api: 'apiDownloadGetFilename',
                headers,
                query,
            })

            //fileId
            let fileId = get(query, 'fileId', '')
            fileId = isestr(fileId) ? fileId : ''
            // console.log('fileId', fileId)

            //check
            if (!isestr(fileId)) {
                // console.log('invalid fileId in query')
                return responseU8aStreamWithError(res, 'invalid fileId in query')
            }

            //inp
            let inp = { fileId }

            //procDownload
            let out = {}
            await procDownload(inp)
                .then((res) => {
                    out.success = res
                })
                .catch((err) => {
                    out.error = err
                })
            // console.log('out', out)

            //return
            if (haskey(out, 'error')) {
                // console.log('out.error', out.error)
                return responseU8aStreamWithError(res, `can not get file from fileId`)
            }

            //r
            let r = get(out, 'success')

            //streamRead
            let streamRead = get(r, 'streamRead')

            //fileSize
            let fileSize = get(r, 'fileSize')
            if (!isNumber(fileSize)) {
                try {
                    streamRead.destroy() //提供stream前發生錯誤, 得強制destroy
                }
                catch (err) {}
                return responseU8aStreamWithError(res, 'invalid fileSize')
            }
            // fileSize = cstr(fileSize)

            //fileType
            let fileType = get(r, 'fileType')
            if (!isestr(fileType)) {
                try {
                    streamRead.destroy() //提供stream前發生錯誤, 得強制destroy
                }
                catch (err) {}
                return responseU8aStreamWithError(res, 'invalid fileType')
            }
            fileType = cstr(fileType)

            return res.response(streamRead)
                .type(fileType)
                // .header('Content-Disposition', `attachment; filename="${filename}"`) //chrome會優先使用header內filename, 但header內支援中文度很差須用base64, 此導致chrome下載檔名只能為base64, 故一律改由前端(browser)先取得真實檔名後直接給予下載檔名, 避免用header提供真實檔名
                .header('Content-Length', fileSize)
        },
    }

    //apiDownload
    let apiDownload = {
        path: `/${apiName}dw`,
        method: 'POST',
        options: {
            payload: {
                maxBytes: 1024 * 1024 * 1024 * 1024, //預設為1mb, 調整至1tb, 也就是給予3次方
                maxParts: 1000 * 1000 * 1000, //預設為1000, 給予3次方
                timeout: false, //避免請求未完成時中斷
                // output: 'stream',
                parse: true, //前端送obj過來須自動解析
            },
            timeout: {
                server: false, //關閉伺服器超時
                socket: false, //關閉socket超時
            },
        },
        handler: async function (req, res) {
            // console.log(req, res)
            // console.log('payload', req.payload)

            //headers
            let headers = get(req, 'headers')
            headers = iseobj(headers) ? headers : ''
            // console.log('headers', headers)

            //query
            let query = get(req, 'query')
            query = iseobj(query) ? query : ''
            // console.log('query', query)

            //authorization
            let authorization = get(headers, 'authorization', '')
            authorization = isestr(authorization) ? authorization : ''

            //check
            if (true) {

                //verifyConn
                let m = verifyConn({ apiType: 'download', authorization, headers, query })
                if (ispm(m)) {
                    m = await m
                }

                //check
                if (m !== true) {
                    return responseU8aStreamWithError(res, 'permission denied')
                }

            }

            //eeEmit
            eeEmit('handler', {
                api: 'apiDownload',
                headers,
                query,
            })

            //fileId, 從payload接收
            let fileId = get(req, 'payload.fileId', '')
            // console.log('fileId', fileId)

            //check
            if (!isestr(fileId)) {
                // console.log('invalid fileId in payload')
                return responseU8aStreamWithError(res, 'invalid fileId in payload')
            }

            //inp
            let inp = { fileId }

            //procDownload
            let out = {}
            await procDownload(inp)
                .then((res) => {
                    out.success = res
                })
                .catch((err) => {
                    out.error = err
                })
            // console.log('out', out)

            //return
            if (haskey(out, 'error')) {
                // console.log('out.error', out.error)
                return responseU8aStreamWithError(res, `can not get file from fileId`)
            }

            //r
            let r = get(out, 'success')

            //streamRead
            let streamRead = get(r, 'streamRead')

            //filename
            let filename = get(r, 'filename')
            if (!isestr(filename)) {
                try {
                    streamRead.destroy() //提供stream前發生錯誤, 得強制destroy
                }
                catch (err) {}
                return responseU8aStreamWithError(res, 'invalid filename')
            }
            filename = str2b64(filename) //headers內對中文支援度不佳須用base64傳

            //fileSize
            let fileSize = get(r, 'fileSize')
            if (!isNumber(fileSize)) {
                try {
                    streamRead.destroy() //提供stream前發生錯誤, 得強制destroy
                }
                catch (err) {}
                return responseU8aStreamWithError(res, 'invalid fileSize')
            }
            // fileSize = cstr(fileSize)

            //fileType
            let fileType = get(r, 'fileType')
            if (!isestr(fileType)) {
                try {
                    streamRead.destroy() //提供stream前發生錯誤, 得強制destroy
                }
                catch (err) {}
                return responseU8aStreamWithError(res, 'invalid fileType')
            }
            fileType = cstr(fileType)

            return res.response(streamRead)
                .type(fileType)
                .header('Content-Disposition', `attachment; filename="${filename}"`) //針對前端(nodejs)用POST下載, 可基於header內base64檔名解析出並直接給予檔名, 不用預先取得檔名
                .header('Content-Length', fileSize)
        },
    }

    //startServer
    async function startServer() {

        //register inert
        if (useInert) {
            await server.register(Inert)
        }

        //apiRoutes
        let apiRoutes = []
        if (useInert) {
            let api = {
                method: 'GET',
                path: '/{file*}',
                handler: {
                    directory: {
                        path: `${pathStaticFiles}/`
                    }
                },
            }
            apiRoutes = [
                ...apiRoutes,
                api,
            ]
        }
        if (true) {
            apiRoutes = [
                ...apiRoutes,
                apiMain,
                apiUploadCheck,
                apiUploadSlice,
                // apiUploadSliceMerge,
                apiDownloadGetFilename,
                apiDownloadGetFile,
                apiDownload,
            ]
        }

        //route
        server.route(apiRoutes)

        //start
        await server.start()

        console.log(`Server running at: ${server.info.uri}`)

    }

    //start
    if (get(opt, 'serverHapi')) {
        // server.route([apiMain, apiUploadCheck, apiUploadSlice, apiUploadSliceMerge, apiDownloadGetFilename, apiDownloadGetFile, apiDownload])
        server.route([apiMain, apiUploadCheck, apiUploadSlice, apiDownloadGetFilename, apiDownloadGetFile, apiDownload])
    }
    else {
        startServer()
    }

    //stop
    let stop = () => {
        server.stop()
    }

    //save
    ee.stop = stop

    return ee
}


export default WConverhpServer