Node檔案操作那些事兒

Cris_冷崢子發表於2018-03-20
  • 關於flag
    • 基本分類
    • w和a的區別
    • 修飾操作
  • 關於mode
  • 關於encoding
  • 關於fd
  • 拷貝檔案
    • 原生拷貝API的缺陷
    • buffer級的copy實現
    • 關於writeFile和write的區別
  • write簡寫
  • 建立和刪除檔案
  • 建立和刪除資料夾
    • 級聯建立資料夾
    • 遞迴刪除資料夾
      • 先序深度非同步刪除
      • 先序廣度非同步刪除
  • 關於fs.constants
  • stats中的三個time

關於flag

通過設定讀寫檔案API的flag屬性,我們能控制我們操作檔案的方式以及一些操作細節。

基本分類

首先我們要知道一個檔案的操作方式主要分為哪幾種:

  • r : 讀檔案
  • w : 寫檔案
  • a : 追加

當我們呼叫read系的API時,預設的flag即為r,同樣,當我們呼叫write系的API時,預設的flag即為w。其中wa的區別在於。

w和a的區別

w每次會清空檔案,a則是向裡新增。

檔案不存在時w會建立檔案,而a則會報錯。

修飾操作

So,這些flag最基礎的作用是標識我們這個操作是讀檔案還是寫檔案。但flag的作用不僅於此,我們還能在此基礎之上新增一些輔助標識來定製一些我們讀寫檔案時的細節操作。

其中若添上+表示對基本的操作方式進行取反加強(如果是寫就還能讀,如果是讀就還能寫),如果是x則就是排他。

  • r+ 讀取並寫入 檔案不存在報錯
  • rs 同步讀取檔案並忽略快取
  • wx 排他寫入檔案
  • w+ 讀取並寫入檔案,不存在則建立,存在則清空 和r+區別在於不會報錯會建立不建立的檔案
  • wx+ 和w+類似 其他方式開啟
  • ax 排他
  • a+ 讀取並追加寫入,不存在則建立 和w+相比是不會覆蓋
  • ax+ 作用於a+類似,但是以排他方式開啟檔案

容易記混淆的是創不建立報不報錯,我們要注意即使r 用了+ 也並不能在檔案不創在時建立一個檔案,相比之下a則是可以的,但a+相較於w+它永遠保持它追加的本質,不會像w清空再寫入。

關於mode

寫入相關的API我們能設定mode,嗯。。。w可以建立檔案,檔案建立時需要設定一個許可權。(So,如果是r,則不存在mode的配置需要)

許可權是用八進位制表示的,0o開頭後面再跟三個數(代表三種使用者的許可權,我,所屬使用者組,遊客),0777表示最大許可權。

數字7代表一個使用者組被賦予了最大許可權,可讀可寫可執行,讀寫執行的權重分別為421,加起來為7,故一個使用者組最大許可權為7。

記憶口訣: 兒(2)媳(寫)一直(執)死(4)讀書

[info] windows中檔案不能直接執行,故我們常常用0o666而不是0o777

關於encoding

讀寫檔案時都有encoding的配置項,唯一的區別是他們的預設配置。

寫檔案時預設的編碼是utf8讀檔案時預設沒有編碼,它讀取的是buffer

關於fd

fd是被開啟檔案的檔案標識,我們稱之為它為控制程式碼,控制程式碼控制程式碼,就是一個把手,我們只要握住它就能操控整個被標識的檔案。

它是一個從3開始的數字,為什麼是從3開始的呢?它本身是該從1開始的,但是1、2都被佔用了,1代表標準輸出(process.stdout.write()),2代表錯誤輸出(process.stderr.write())。至於什麼是標準輸出錯誤輸出,你可以想一下console.log/infoconsole.error/warn,他們是等價的。

另外還有一點灰常重要的點需要注意,linux中fd是有上限的,且一旦開啟一個檔案fd就會++,故我們需要記得手動關閉這些檔案防止溢位。

拷貝檔案

拷貝檔案可能是最常用的檔案操作方式了,它將資料從一個地點流向了另外一個地點。

原生拷貝API的缺陷

Node8.5以上的版本為我們提供了拷貝的API——copyFile,但Ta是基於readFilewriteFile的,這樣有個缺點,那就是readFile是將檔案一次性讀到記憶體中後倉讓writeFIle直接寫入到檔案中,嗯,假若檔案過大,大過記憶體,那。。。就gg了。

buffer級的copy實現

let fs = require('fs');
let path = require('path');

function copy(source,target,speed,cb){
  if(typeof(speed)==='function'){
    cb = speed;
    speed = 10;
  }
  fs.open(path.join(__dirname,source),'r+',0o666,function(err,rfd){
    if(err)console.error('開啟檔案失敗!');    
    fs.open(path.join(__dirname,target),'w+',0o666,function(err,wfd){
      if(err)console.error('開啟檔案失敗!');
      let buf = Buffer.alloc(speed)
        ,length = buf.length;
      function next(){
        fs.read(rfd,buf,0,length,null,(err,bytesRead)=>{
          if(err)console.error('讀取檔案失敗!');
          if(!bytesRead){
            fs.close(rfd,function(err){});
            fs.fsync(wfd,function(err){
              fs.close(wfd,function(err){});
            });
            return cb&&cb();
          }
          fs.write(wfd,buf,0,bytesRead,null,(err,bytesWritten)=>{
            if(err)console.error('寫入檔案失敗!');
            next();
          });
        });

      }
      next();
    });
  });
}

copy('test.js','test2.js',()=>{console.log('拷貝完畢')});
複製程式碼

關於writeFile和write的區別

除了他們一個是將整個檔案一次性寫入,一個可以控制寫入的顆粒大小以外,

writeFile是直接寫入檔案不需要經過快取,而write是會先寫入快取的,

So如果我們是使用write寫入檔案,我們常常在關閉一個寫入的檔案之前利用fsyncAPI強制將快取中的內容寫入到本地檔案後再close關閉檔案。

write的簡寫方式

write有一種不常見的簡寫方式

fs.open(path.join(__dirname,'1.txt'),'w+',function(err,fd){

    let buf = Buffer.from('你好呀');
    fs.write(fd,buf,0,buf.length,null,function(err,bytesWritten){
        if(err)return console.log('fail');
        console.log('ok');
    })

    //以上可以這麼簡寫 省略了offset length position引數
    fs.write(fd,Buffer.from('你好呀'),function(err,bytesWritten){
        if(err)return console.log('fail');
        console.log('ok');
    })
})
複製程式碼

建立和刪除檔案

node中沒有類似於mkfiletouch filename這樣的建立檔案的API,

取而代之的是write系列API,上文中我們說過write系列的API的預設flagw,這意味著如果一個檔案不存在的話我們會先建立這個檔案再寫入。

同樣的我們也能使用open來建立檔案,只需要將flag設定為w即可

fs.open(path.join(__dirname,'1.txt'),'w+',function(err,fd){
  console.log('建立成功');
})
複製程式碼

刪除檔案在node中很簡單

fs.unlink('xxx',function(err){})
複製程式碼

建立和刪除資料夾

刪除資料夾

fs.rmdir('xxx',function(err){})
複製程式碼

建立資料夾

fs.mkdir('xxx',function(err){})
複製程式碼

API很簡單,但不論是刪除還是建立資料夾都有些需要注意的地方。

級聯建立資料夾

node中建立資料夾也逃不過不能級聯建立目錄的命運。

如果我們想建立一個目錄a/b/c,那麼要先有資料夾a,在檔案a下還得有資料夾b,才能在a中的b下建立資料夾c。

So,我們需要自己動手Lu一個

function mkdirP(dir,cb){
  let paths = dir.split('/');
  // [a,b,c,d]
  //a  a/b  a/b/c
  function next(index){
    if(index>paths.length){
      return cb&&cb(err);
    }
    let newPath = paths.slice(0,index).join('/');
    fs.access(newPath,function(err){
      if(err){ // 如果檔案不存在就建立這個檔案
        fs.mkdir(newPath,function(err){
          next(++index);
        })
      }else{
        next(++index); //說明已有資料夾,跳過繼續建立下一個資料夾
      }
    });
  }
  next(1)
}
複製程式碼

遞迴刪除資料夾

刪除資料夾需要注意一點,如果要刪除的資料夾裡有東西,我們需要把裡面的東西都刪完了才能刪掉這個資料夾,否則就會報錯。

So,我們需要對資料夾進行遞迴遍歷操作,

我們選擇採用先序深度和先序廣度兩種方式去實現遞迴刪除資料夾

先序深度非同步刪除

function rmdir(dir,cb){
  fs.readdir(dir,function(err,files){
    // 讀取到檔案
    function next(index){
      if(index===files.length)return fs.rmdir(dir,cb); //=== 表示能遍歷的都遍歷完了,刪除該層目錄
      let newPath = path.join(dir,files[index]);
      fs.stat(newPath,function(err,stats){
        if(stats.isDirectory()){ // 如果是資料夾
          // 要讀的是b裡的第一個 而不是去讀c
          // 如果b裡的內容沒有了 應該去遍歷c
          rmdir(newPath,()=>next(index++));
        }else{
          //刪除檔案後繼續遍歷即可
          fs.unlink(newPath,next(index++));
        }
      });
    }
    next(0);
  });
}
複製程式碼

先序廣度非同步刪除

function rmdirp(dir,cb){
  let dirs = [dir]
    ,index = 0;
  function rmdir(){
    let current = dirs[--index];
    if(current){
      fs.stat(current,(err,stat)=>{
        if(stat.isDirectory()){
          fs.rmdir(current,rmdir);
        }else{
          fs.unlink(current,rmdir);
        }
      });
    }
  }
  !function next(){
    if(index===dir.length)return rmdir(); //說明index停止增長,所有檔案已遍歷完畢
    let current = dirs[index++];
    fs.stat(current,function(err,stat){
      if(err)return cb(err);
      if(stat.isDirectory()){
        fs.readdir(current,function(err,files){
          dirs = [...dirs,...files.map(item=>path.join(current,item))];
        });
      }else{
        next(); //說明是檔案,那在上一輪next中已經被新增進陣列中,直接跳過
      }
    });
  }();
}
複製程式碼

關於fs.constants

  • fs.constants.F_OK :path is visible to the calling process. This is useful for determining if a file exists, but says nothing about rwx permissions. Default if no mode is specified.
  • fs.constants.R_OK :path can be read by the calling process.
  • fs.constants.W_OK - path can be written by the calling process.
  • fs.constants.X_OK - path can be executed by the calling process. This has no effect on Windows (will behave like fs.constants.F_OK).

主要是配合fs.access,作為其第二個引數,預設是F_OK

stats中的三個time

  • atime 檔案被訪問時(read)會觸發修改
  • mtime 檔案被更新時(write)會觸發修改
  • ctime 相當於atime+mtime+chmod+rename...

相關文章