Node文件操作那些事儿

3,621 阅读8分钟
  • 关于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...