import get from 'lodash-es/get.js'
import ispint from './ispint.mjs'
import cint from './cint.mjs'
import delay from './delay.mjs'
import fsExistsCore from './fsExistsCore.mjs'
import fsIsFileCore from './fsIsFileCore.mjs'
import fsMissFns from './_fsMissFns.mjs'
import errToStr from './_errToStr.mjs'
import getTrace from './getTrace.mjs'
/**
* 後端nodejs刪除檔案,支援失敗重試機制
*
* Unit Test: {@link https://github.com/yuda-lyu/wsemi/blob/master/test/fsDeleteFileSafeCore.test.mjs Github}
* @memberOf wsemi
* @param {String} pah 輸入路徑字串
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @param {Object} [opt.fs=null] 輸入fs物件,須提供statSync、lstatSync、unlinkSync,預設null;Core由呼叫端傳入(對外函數fsDeleteFileSafe預設傳入內建fs,呼叫端可再以opt.fs覆蓋);null或缺少statSync時reject字串`opt.fs is invalid: ...`(不視為標的不存在),缺少其他函數時其呼叫之TypeError以字串reject
* @param {Integer} [opt.numRetry=5] 輸入嘗試刪除次數整數,預設5;遇EBUSY才重試,每次重試前等待300ms,最後一次嘗試失敗後不再等待而直接reject
* @param {Function} [opt.onTrace=null] 輸入觀察回調函數,於內部事件發生當下同步呼叫,傳入一個凍結之事件物件{ type, ... },供測試以事件(而非等待時間)同步與診斷;type為'attempt'時為即將呼叫unlinkSync嘗試刪除(含n為由1起算之嘗試序號),'retry'時為第n次嘗試遇EBUSY而即將等待ms毫秒(固定300)(含n、code為'EBUSY'、ms;僅在之後還有下一次嘗試時通知,故n小於numRetry),'done'時為第n次嘗試刪除成功而即將resolve(含n),'skip'時為不需(再)刪除而即將resolve或reject(含n與reason;reason為'not-exist'代表前置檢查標的不存在而resolve、'not-file'代表前置檢查標的非檔案而reject,此二者n為0;reason為'vanished'代表第n次嘗試得ENOENT且標的確已不存在(重試期間被他方刪除)而resolve),'giveup'時為重試耗盡仍遇EBUSY而即將reject(含n為numRetry、code為'EBUSY';於最後一次嘗試失敗當下通知,不再等待),'fail'時為前置檢查拋錯或opt.fs不合(n為0,opt.fs不合時code為null)或第n次嘗試遇不可重試之錯誤而即將reject(含n、code為該錯誤之code字串,無則null);事件皆於resolve或reject之前通知;回調不帶接收者呼叫,其拋錯與回傳值皆忽略,不影響刪除,預設null
* @returns {Promise} 回傳Promise,resolve回傳成功訊息,reject回傳錯誤訊息字串(含重試耗盡仍被占用)
* @example
* need test in nodejs.
*
* //see fsDeleteFileSafe
*
*/
async function fsDeleteFileSafeCore(pah, opt = {}) {
let numRetry = 5
let fs = null
let trace = null
//讀取opt與check置於try內: opt本身或其屬性不可讀(getter拋錯、已撤銷之Proxy)時get拋出(於async函數內未捕捉即以Error物件reject), 注入之fs之lstatSync可拋出任意值, 皆轉字串reject
try {
//numRetry
numRetry = get(opt, 'numRetry')
if (!ispint(numRetry)) {
numRetry = 5
}
numRetry = cint(numRetry)
//fs
fs = get(opt, 'fs', null)
//trace, 觀察回調(opt.onTrace), 未給時為null; 置於numRetry與fs之讀取之後, 使opt不可讀之既有失敗路徑不變
trace = getTrace(opt)
//check fs, 須提供statSync: fsExistsCore吞掉statSync之所有錯誤, fs為null或無statSync時對其呼叫之TypeError被吞而誤判標的不存在, 回傳成功卻沒有刪除任何東西
// 缺少lstatSync或unlinkSync者, 其呼叫之TypeError不被吞(前置檢查與重試迴圈皆在try內)而依既有路徑以字串reject, 不需另檢查
let miss = fsMissFns(fs, ['statSync'])
if (miss.length > 0) {
if (trace) {
trace('fail', { n: 0, code: null })
}
return Promise.reject('opt.fs is invalid: it must be an object providing statSync (and lstatSync, unlinkSync)')
}
//check, 需先判斷
if (!fsExistsCore(pah, { fs })) {
// return {
// success: 'file does not exist: ' + pah //目標不存在但仍算是刪除成功, 故需先判斷
// }
if (trace) {
trace('skip', { n: 0, reason: 'not-exist' })
}
return 'file does not exist: ' + pah //目標不存在但仍算是刪除成功, 故需先判斷
}
if (!fsIsFileCore(pah, { fs })) {
// return {
// error: 'input path is not a file: ' + pah //若存在但又不是檔案, 則視為錯誤
// }
if (trace) {
trace('skip', { n: 0, reason: 'not-file' })
}
return Promise.reject('input path is not a file: ' + pah) //若存在但又不是檔案, 則視為錯誤
}
}
catch (err) {
if (trace) {
let code = null
try {
code = err.code
}
catch (e) {}
trace('fail', { n: 0, code: typeof code === 'string' ? code : null })
}
return Promise.reject(errToStr(err))
}
//retry
let done = false
let errLast = null
for (let i = 0; i < numRetry; i++) {
//attempt
if (trace) {
trace('attempt', { n: i + 1 })
}
//unlinkSync
try {
fs.unlinkSync(pah)
if (trace) {
trace('done', { n: i + 1 })
}
done = true
break
}
catch (err) {
// return {
// error: err
// }
errLast = err
//code, 拋出之值可為null、無原型物件、已撤銷之Proxy, 讀屬性須保護
let code = null
try {
code = err.code
}
catch (e) {}
if (code === 'EBUSY') {
//最後一次嘗試失敗後已無下一次嘗試, 不再等待(原多等300ms才reject)也不通知retry, 直接離開迴圈由下方giveup處理
if (i < numRetry - 1) {
if (trace) {
trace('retry', { n: i + 1, code: 'EBUSY', ms: 300 })
}
await delay(300)
}
}
else if (code === 'ENOENT' && !fsExistsCore(pah, { fs })) {
//標的已不存在(重試期間被他方刪除): 與前置檢查之標的不存在同視為成功(fsDeleteFolderSafeCore之rmSync以force吞ENOENT亦為成功); 原為非EBUSY即reject; 須標的確已不存在, 注入之unlinkSync對仍存在之標的拋ENOENT者仍reject
if (trace) {
trace('skip', { n: i + 1, reason: 'vanished' })
}
return 'file does not exist: ' + pah
}
else {
if (trace) {
trace('fail', { n: i + 1, code: typeof code === 'string' ? code : null })
}
return Promise.reject(errToStr(err))
}
}
}
//check, 重試耗盡仍EBUSY: 原為跳出迴圈後回傳done(偽成功), 改以最後一次之錯誤reject
if (!done) {
if (trace) {
trace('giveup', { n: numRetry, code: 'EBUSY' })
}
return Promise.reject(errToStr(errLast))
}
// return {
// success: 'done: ' + pah
// }
return 'done: ' + pah
}
export default fsDeleteFileSafeCore