1. 程式人生 > >使用nodejs進行了簡單的檔案分卷工具

使用nodejs進行了簡單的檔案分卷工具

關鍵詞:node fs readline generator 

(在這之前需要宣告的是這篇部落格的應用範圍應該算是相當狹隘,寫出來主要也就是給自己記錄一下臨時興起寫的一個小工具,僅從功能需求上來說我相信是不適用於大多數讀者的,歡迎有興趣看的朋友給我做一次review)

  最近沉迷漫畫,收集了一堆野生資源,偶爾會遇到一些四格漫畫,觀看體驗不是很好,因為每話篇幅比較短,就獨立成了一個目錄,譬如這樣:

 

  然後我就下意識的想寫個指令碼把這些目錄合併成一個目錄,解決這個問題,需要操作本地檔案,而這是用很多途徑都可以實現的,由於入門程式設計的時候學的是py,我第一時間是想用py的os模組去實現功能,一想上一次用py是快三年前的事情了,很多東西忘得差不多,包括前段時間換了新電腦,也沒裝有py環境,所以還是選擇了語法上更熟悉也不需要額外配環境的node。   在明確了功能是在不改變源目錄中檔案順序的情況下將多個目錄按檔名順序合併到新目錄之後,由於涉及到操作本地檔案(複製檔案、建立目錄等),所以確定了核心功能需要通過fs模組來完成;如果是自己用的話,一些比如輸入輸出目錄這種配置引數,大可直接寫死在程式碼裡,需要用的時候再改就是了,但為了提高靈活性吧,所以還需要做一個微型的CLI,通過使用者在命令列的輸入來決定配置,這需要用到readline模組來提示和獲取使用者在命令列中的輸入(在瞭解readline之後發現這裡會比py麻煩不少,py用一個input方法就可以獲取使用者在命令列裡的輸入了);為了實現單向的流程,以及在提高程式碼在註釋之外的的易理解和可讀性,使用了平時比較少用的generator來實現了一個狀態機並串聯各個獨立操作。   一邊看node文件一邊寫程式碼,在忙活小半天之後,出來一點成果:
const fs = require('fs');
const readline = require('readline');
/**
 * @description 獲取questtion的返回
 * @param {String} question 使用者提示
 * @param {Function} handler 驗證使用者輸入
 * @returns {Promise} rl.question方法本身是通過回撥來處理使用者輸入的,所以選擇了返回promise來做阻塞,有序地丟擲question並接收answer
 */
function getQuestionResult(question,handler){
    return new Promise((res)=>{
        // 建立一個可讀流,用來讀取在cmd中的輸入
        const rl = readline.createInterface({
            input: process.stdin,
            output: process.stdout,
        });
        /**
         * 考慮到即使使用者輸入異常,question方法都會在監聽到換行之後結束,所以把handleResult的結構設計成一個物件,
         * 由一個狀態值success來表示是否通過handler的校驗,success為false時應再次執行並且獲取使用者輸入         */
        rl.question(question, async (ans)=>{
            const handleResult = handler(ans)
            if (handleResult.success){
                rl.close()
                res(handleResult.value)
            } else {
                //handleResult.success為false時,handleResult.value是handler中設定的錯誤提示
                rl.close()
                const rejecthandle = await getQuestionResult(handleResult.value,handler)
                res(rejecthandle)
            }
        })
    })
}
// 這裡建立一個generator例項,我覺得generator的yield單向有序的特性很適合我這個需求
function*gen(){
    function getChangeSettingsFlag(ans){
        return ans==="Y"?{success:true,value:true}:{success:true,value:false}
    }
    const getPath = function (ans) {
        /**
* @description 驗證路徑(是否是目錄)
* @param {String} path
*/ const validatePath = function (path) { try { // node10.x及以下版本不支援readdirSync,需要你需要這種寫法,在執行之前需要切換node版本 const dir = fs.readdirSync(path)
if (dir) { return {success:true,value:path} } } catch(err){ return {success:false,value:"提供的地址不合理,請重新輸入:"} } } return validatePath(ans) } // yield並不會返回值,這裡宣告的changeSettingFlag的值實際上是接收的next方法的引數 const changeSettingFlag = yield getChangeSettingsFlag const settings = { volumeSize: 10, dirName: "新建分卷" } // 改變預設 if (changeSettingFlag){ const volumeSize = yield function(volumeSize){ return Number(volumeSize)>0&&Number(volumeSize)!==Infinity?{success:true,value:volumeSize}:{success:false,value:"輸入的數字不合理,請重新輸入:"} } settings.volumeSize = Number(volumeSize)||settings.volumeSize const dirName = yield function(dirName){ return dirName.trim()?{success:true,value:dirName}:{success:false,value:"輸入的目錄名不合理,請重新輸入:"} } settings.dirName = dirName.trim()||settings.dirName } // 接收路徑 const pathInfo = {
input: "",
output: ""
} const inputPath = yield getPath pathInfo.input = inputPath; const outputPath = yield getPath pathInfo.output = outputPath; const conf = {
pathInfo,
settings
} console.log("conf",conf) yield conf } // run it async function workflow(generator){ const func0 = generator.next().value const changeSettingFlag = await getQuestionResult("當前預設定如下:\n\t輸出的分卷名:“新建分卷”;\n\t容量:10話/卷;\n希望調整預設嗎?(Y/n) ",func0) let getvolumeSize,getdirName if (changeSettingFlag){ const func1 = generator.next(changeSettingFlag).value getvolumeSize = await getQuestionResult("期望的卷容量(話/卷)是: ",func1) const func2 = generator.next(getvolumeSize).value getdirName = await getQuestionResult("期望的分卷名是:",func2) } const func3 = generator.next(getdirName).value const inputPath = await getQuestionResult("選擇的源路徑是:",func3) const func4 = generator.next(inputPath).value const outputPath = await getQuestionResult("期望的輸出路徑是:",func4) const conf = generator.next(outputPath).value // 當前計數,通過在檔名中新增count來保持排序 let currentCount = 0; // 當前分卷 let currentVolume = 0; /** * @param {String} path * @param {String} newFolderName */ async function letsdance(path,newFolderName="") { const childs = fs.opendirSync(path) let chunkNum = 0 let newFolderPath = newFolderName for await (const dirent of childs) { if (dirent.isDirectory()) { // 填充滿一個目錄之後建立一個新目錄 if (currentVolume%conf.settings.volumeSize===0) { chunkNum+=1; newFolderPath = conf.pathInfo.output+"/"+conf.settings.dirName+"_"+chunkNum fs.mkdirSync(newFolderPath) // 如果你不希望命名字尾一直遞增,也可以在新建目錄之後把currentCount重新置為0 } currentVolume+=1 const nextLevelPath = path+"/"+dirent.name letsdance(nextLevelPath,newFolderPath) } else if (dirent.isFile()){ currentCount += 1 const extName = dirent.name.split(".").reverse()[0] const targetFilePath = path+"/"+dirent.name const newFileName = newFolderPath+"/"+currentCount+"."+extName fs.copyFileSync(targetFilePath,newFileName) } } } letsdance(conf.pathInfo.input) } const g = gen() workflow(g)

  執行這個指令碼,如果我有兩個目錄,需要將他們之中的檔案複製到一個新目錄來實現合併的效果的話,在講第一個目錄的檔案按原順序命名為1-n之後。第二個目錄的檔案則會從n+1開始命名,效果如下: