用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