用Nodejs进行文件上传-下载-浏览-横扫File-System-API

    以前知道用Nodejs进行上传下载是很容易的,用个formidable就可以了,也就没有去管它,然后昨天晚上有空,就写了个小demo,就发现了自己的一些问题。比如对File System的API不熟悉。用的时候还要去查。尤其是对createReadStream 和 writeReadStream这一类流处理不熟悉,下面是我的整理和学习。基本上是一个完整的demo,有上传,有下载,还有浏览文件。


    上传下载

    关于formidable

    This module was developed for Transloadit, a service focused on uploading and encoding images and videos. It has been battle-tested against hundreds of GB of file uploads from a large variety of clients and is considered production-ready.

    具体的一些用法,大家可以去这个上面去看,比较简单了。一个官网的小demo如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    var formidable = require('formidable'),
    http = require('http'),
    util = require('util');
    http.createServer(function(req, res) {
    if (req.url == '/upload' && req.method.toLowerCase() == 'post') {
    // parse a file upload
    var form = new formidable.IncomingForm();
    form.parse(req, function(err, fields, files) {
    res.writeHead(200, {'content-type': 'text/plain'});
    res.write('received upload:\n\n');
    res.end(util.inspect({fields: fields, files: files}));
    });
    return;
    }
    // show a file upload form
    res.writeHead(200, {'content-type': 'text/html'});
    res.end(
    '<form action="/upload" enctype="multipart/form-data" method="post">'+
    '<input type="text" name="title"><br>'+
    '<input type="file" name="upload" multiple="multiple"><br>'+
    '<input type="submit" value="Upload">'+
    '</form>'
    );
    }).listen(8080);

    这就基本已经完成上传的功能了。但是我们可以让它更丰富,我们下面加一下预览文件并下载的功能。这里面有几个小知识点先解释下:

    1. form上传的时候,必须是enctype=”multipart/form-data”这种格式,否则上传不了。
    2. util.inspect是nodejs里面util模块的一个方法。它可以将任意对象转换 为字符串的方法。比如这里就是把fields里面和files两个对象合为一个对象,然后再转换为字符串。
    3. util.inherits则是一个实现对象间原型继承 的函数。注意这个是只继承原型里面的。原来的属性和方法并不会被继承,也就是在function(){this里面生成的不会被继承},并且继承过来的原型方法也不会被输出。

    预览和下载

    大概做成以后是这个样子的。样子是不是很丑,确实不想管样式,咱们还是注重功能吧。点击upload可以上传文件,点击下面的文件可以直接下载到本地。注意这里要保证文件不重名,我利用的是date。

    预览效果

    代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    "use strict";
    var formidable = require('formidable'),
    http = require('http'),
    util = require('util'),
    fs = require('fs'),
    path = require('path'),
    querystring = require('querystring'),
    url = require('url');
    http.createServer(function (req, res) {
    var urlObj = url.parse(req.url);
    if (urlObj.pathname == '/upload' && req.method.toLowerCase() == 'post') {
    var form = new formidable.IncomingForm();
    form.encoding = 'utf-8';
    form.uploadDir = "dir/";
    form.maxFieldsSize = 2 * 1024 * 1024;
    form.keepExtensions = true;
    form.parse(req, function (err, fields, files) {
    if (err) {
    console.log(err);
    }
    var name = files.upload.name;
    var ext = /\.[^\.]+$/.exec(name)[0];
    var date = new Date();
    fs.renameSync(files.upload.path, "dir\\" + Date.parse(date) + ext);
    res.writeHead(200, {'Content-Type': 'text/plain;charset=utf-8'});
    res.write('received upload: \n\n');
    res.end(util.inspect({fields: fields, files: files}));
    });
    return;
    }
    if (urlObj.pathname == '/download') {
    var query = urlObj.query;
    var name = querystring.parse(query).name;
    var downloadFilePath = "./dir/" + name;
    var filesize = fs.readFileSync(downloadFilePath).length;
    res.setHeader('Content-Disposition', 'attachment;filename=' + name);//此处是关键
    res.setHeader('Content-Length', filesize);
    res.setHeader('Content-Type', 'application/octet-stream');
    var fileStream = fs.createReadStream(downloadFilePath, {bufferSize: 1024 * 1024});
    fileStream.pipe(res, {end: true});
    return;
    }
    function send(str) {
    res.writeHead(200, {'content-type': 'text/html'});
    res.end(
    '<form action="/upload" enctype="multipart/form-data" method="post">' +
    '<input type="text" name="title"><br>' +
    '<input type="file" name="upload" multiple="multiple"><br>' +
    '<input type="submit" value="Upload">' +
    '</form>' +
    '<br /><br />' + str
    );
    }
    function respond() {
    var str = "";
    fs.readdir('dir/', function (err, files) {
    if (err) return console.error(err);
    files.forEach(function (file) {
    str += `<a href='/download?name=${file}'>${file}</a><br />`;
    });
    send(str);
    });
    }
    respond();
    }).listen(8080);

    看代码应该很清楚了,我们这里调用了一个readdir,然后遍历里面的图片,拿到图片的名称,然后当点击的时候,发起请求到download,并传递自己的name,收到以后,我们找到这个图片,然后设置下载需要的相应头就行了。注意下载里面的setHeader是重点。设置的类型是Disposition。

    记录一个我犯的错误,成功以后,发现显示信息一直是乱码,我检查了文件都已经被设置成了utf-8,也设置了相应头writehead是utf-8为什么还乱码呢?后来检查,是{‘Content-Type’: ‘text/plain;charset=utf-8’}这里的charset前面的分号写成了逗号,也是无奈,都怪自己粗心。谨记。

    下面就开始把其他的关于file的api给梳理一下了。


    File System Api

    createReadStream && createWriteStream

    一般情况下,我们可以用这两个API来拷贝文件。nodejs文件操作里面没有直接来copy文件的方法。我们可以先用最开始我们的方法,比如:

    1
    2
    var source = fs.readFileSync('source', {encoding: 'utf8'});
    fs.writeFileSync('destination', source);

    但这容易产生一个问题。因为这种方式是一次性把文件的内容全部读进内存里面,一般小一点的文本文件问题不大,但如果是很大的文件,比如音频视频,一般几个G的,这种。就容易使内存爆仓。这个时候我们流的读写方式就很好了。我们可以先读一会,再写一会。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    "use strict";
    var rs = fs.createReadStream('tmp/7.js');
    var ws = fs.createWriteStream('tmp/9.js');
    rs.on('data', function(data){
    ws.write(data);
    });
    rs.on('end', function(){
    console.log("end of read");
    ws.end();
    });

    但很明显,上面这也会有问题,比如我们读的时候速度明显快于写的速度时候,就会可能产生数据丢失或者不完善的现象。所以我们要对这两者的平衡进行一个控制。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    rs.on('data',function(chunk){
    if(ws.write(chunk) === false){
    rs.pause();
    }
    });
    ws.on('drain',function(){
    rs.resume();
    });
    rs.on('end', function(){
    ws.end();
    });

    所以我们改成上面这样。但是下面这种写法利用pipe,可以更简洁。pipe完成的就是data和end的工作。

    1
    fs.createReadStream('tmp/5.js').pipe(fs.createWriteStream('tmp/10.js'));

    下面我们看一个这个例子的更详细的例子。这个就是一个拷贝文件的例子。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    "use strict";
    var filePath = "tmp/2.mp3";
    var destPath = "tmp/5.mp3";
    var rs = fs.createReadStream(filePath);
    var ws = fs.createWriteStream(destPath);
    //获取文件大小
    var stat = fs.statSync(filePath);
    var totalSize = stat.size;
    //当前已读取长度
    var currentLength = 0;
    var lastSize = 0;
    var startTime = Date.now();
    rs.on('data', function(data){
    ws.write(data);
    });
    rs.on('end', function(){
    console.log("end of read");
    ws.end();
    });
    rs.on('data',function(chunk){
    currentLength += chunk.length;
    if(ws.write(chunk) === false){
    rs.pause();
    }
    });
    ws.on('drain',function(){
    rs.resume();
    });
    rs.on('end', function(){
    ws.end();
    });
    var timer = setTimeout(function displayInfo(){
    var percent = Math.ceil((currentLength / totalSize )*100);
    var size = Math.ceil(currentLength /1000000);
    var diff = size - lastSize;
    lastSize = size;
    //利用了process.stdout输出信息
    process.stdout.clearLine();
    process.stdout.cursorTo(0);
    process.stdout.write(`已完成: ${size}, 百分比: ${percent}, 速度: ${diff*2} MB/s `);
    if(currentLength < totalSize){
    setTimeout(displayInfo,500);
    }else{
    clearTimeout(timer);
    var endTime = Date.now();
    console.log(`共用时: + ${(endTime - startTime) / 1000} s `);
    }
    },500);

    result
    这里我们主要处理的就是对文件拷贝细节的处理。这各部分关于百分比和速度这块学习自sf的一篇文章。感谢,文章末尾有该文章的链接。

    所以对流处理我们可以理解成下面这个样子:

    stream

    可以想象,如果大水杯也就是stream里面的的水流的太快了,小水杯不久一下就满了,所以多的水就溢出去了。所以我们进行一个控制。水在一点一点的流动,而不是一下子全部倒进去。这就是我理解的流。也不知道对不对。如果错误请指正。其实真正的可以这么理解:我们读是从文件读到内存,写是从内存写入磁盘的另一个文件。如果我们读的太快,写的太慢,东西是不是都还在内存里面?更有点像你买了东西,但是不消费,家里面就越堆越多了,想法,总会有房子太满放不下的情况,但是如果你买了就消费出去了,这样你的家里就会保持平衡很干净,你也有时间在房子里做别的事情。所以对于水杯之外,其实还有中间的管子,这个管子就相当于我们这里的内存啦。


    读取文件

    readFile/readFileSync

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var fs = require("fs");
    //异步读取
    fs.readFile('./input.txt',function(err,data){
    if(err){
    return console.error(err);
    }
    console.log("read file input.txt:" + data.toString());
    });
    //同步读取
    var data = fs.readFileSync('./input.txt');
    console.log("sync read :" + data.toString());
    console.log("after read");

    结果很明显,后面同步的先执行,前面的异步会后执行。因为文件的读取也耗费时间。

    sync read :this is input txt
    
    after read
    read file input.txt:this is input txt
    

    打开文件

    1
    2
    3
    4
    5
    6
    fs.open('input.txt','r+',function(err,fd){
    if(err){
    return console.error(err);
    }
    console.log("文件打开成功");
    })

    查看文件信息

    1
    2
    3
    4
    5
    6
    fs.stat('input.txt',function(err,stats){
    if(err){
    return console.error(err);
    }
    console.log(stats);
    })

    写入文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fs.writeFile('write.txt','我是写入文件的内容',function(err){
    if(err){
    return console.log(err);
    }
    console.log('文件已写入');
    fs.readFile('write.txt',function(err,data){
    if(err) return console.log(err);
    console.log('写入内容为: ' + data.toString());
    })
    });

    删除文件

    1
    2
    3
    4
    fs.unlink('input.txt',function(err){
    if(err) return console.error(err);
    console.log("文件删除成功");
    });

    创建目录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    fs.mkdir('tmp/test',function(err){
    if(err) return console.error(err);
    console.log("目录创建成功");
    fs.rmdir('tmp/test',function(err){
    if(err) return console.error(err);
    console.log("删除目录成功");
    fs.readdir('tmp',function(err,files){
    if(err) return console.error(err);
    files.forEach(function(file){
    console.log(file);
    })
    })
    })
    })
    目录创建成功
    删除目录成功
    1.js
    2.js
    dirtmp 
    

    更改权限

    1
    2
    3
    4
    5
    fs.chmod('tmp/1.js', 0600 ,function(err){
    if(err)
    return console.error(err);
    console.log("修改权限成功");
    });

    文件重命名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    "use strict";
    fs.rename('tmp/5.js','tmp/7.js',function(err){
    if(err) throw err;
    console.log("4.js has been renamed");
    fs.stat('tmp/7.js',(err,stat)=>{
    if(err) throw err;
    console.log(`stat is : ${JSON.stringify(stat)}`);
    });
    });

    创建硬链接

    1
    2
    3
    4
    5
    6
    7
    8
    //硬链接就是备份,软连接就是快捷方式
    fs.link('tmp/3.js','tmp/5.js',function(err){
    if(err)
    return console.error(err);
    console.log("硬链接创建成功");
    });
    fs.unlink('tmp/3.js');

    获取文件绝对路径

    1
    2
    3
    4
    fs.realpath('tmp/2.js',function(err,resolvedPath){
    if(err) return console.error(err);
    console.log("文件的绝对路径是:" + resolvedPath);
    });

    注意事项

    由于利用了异步方法,所以在写的时候一定要注意顺序。比如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    "use strict";
    fs.rename('tmp/5.js','tmp/7.js',function(err){
    if(err) throw err;
    console.log("4.js has been renamed");
    });
    fs.stat('tmp/7.js',(err,stat)=>{
    if(err) throw err;
    console.log(`stat is : ${JSON.stringify(stat)}`);
    })

    会得到:

    if(err) throw err;
                ^
    
    Error: ENOENT: no such file or directory, stat 'F:\uploadNodejs\testFile\tmp\7.js'
    at Error (native)
    

    我们只需要放进去就可以啦。


    总结

    快花了一天去弄这个东西了,先前总说遇到API就去查,但是我有点不同意,因为这样效率会很低。而且会导致变懒惰的后果。还是要多进行刻意的练习,才能有质的飞跃。这是自己所欠缺的。下面推荐几篇文章和阅读,也感谢这些优秀的文章:

    1. Nodejs API /api/fs.html
    2. http://my.oschina.net/cmw/blog/110107
    3. http://www.runoob.com/nodejs/nodejs-util.html
    4. https://segmentfault.com/a/1190000004057022