以前知道用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 | var formidable = require('formidable'), |
这就基本已经完成上传的功能了。但是我们可以让它更丰富,我们下面加一下预览文件并下载的功能。这里面有几个小知识点先解释下:
- form上传的时候,必须是enctype=”multipart/form-data”这种格式,否则上传不了。
- util.inspect是nodejs里面util模块的一个方法。它可以将任意对象转换 为字符串的方法。比如这里就是把fields里面和files两个对象合为一个对象,然后再转换为字符串。
- 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 ;
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
2var source = fs.readFileSync('source', {encoding: 'utf8'});
fs.writeFileSync('destination', source);
但这容易产生一个问题。因为这种方式是一次性把文件的内容全部读进内存里面,一般小一点的文本文件问题不大,但如果是很大的文件,比如音频视频,一般几个G的,这种。就容易使内存爆仓。这个时候我们流的读写方式就很好了。我们可以先读一会,再写一会。1
2
3
4
5
6
7
8
9
10
11
12 ;
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
13rs.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
;
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);
这里我们主要处理的就是对文件拷贝细节的处理。这各部分关于百分比和速度这块学习自sf的一篇文章。感谢,文章末尾有该文章的链接。
所以对流处理我们可以理解成下面这个样子:
可以想象,如果大水杯也就是stream里面的的水流的太快了,小水杯不久一下就满了,所以多的水就溢出去了。所以我们进行一个控制。水在一点一点的流动,而不是一下子全部倒进去。这就是我理解的流。也不知道对不对。如果错误请指正。其实真正的可以这么理解:我们读是从文件读到内存,写是从内存写入磁盘的另一个文件。如果我们读的太快,写的太慢,东西是不是都还在内存里面?更有点像你买了东西,但是不消费,家里面就越堆越多了,想法,总会有房子太满放不下的情况,但是如果你买了就消费出去了,这样你的家里就会保持平衡很干净,你也有时间在房子里做别的事情。所以对于水杯之外,其实还有中间的管子,这个管子就相当于我们这里的内存啦。
读取文件
readFile/readFileSync1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16var 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 | fs.open('input.txt','r+',function(err,fd){ |
查看文件信息
1 | fs.stat('input.txt',function(err,stats){ |
写入文件
1 | fs.writeFile('write.txt','我是写入文件的内容',function(err){ |
删除文件
1 | fs.unlink('input.txt',function(err){ |
创建目录
1 | fs.mkdir('tmp/test',function(err){ |
目录创建成功
删除目录成功
1.js
2.js
dirtmp
更改权限
1 | fs.chmod('tmp/1.js', 0600 ,function(err){ |
文件重命名
1 | ; |
创建硬链接
1 | //硬链接就是备份,软连接就是快捷方式 |
获取文件绝对路径
1 | fs.realpath('tmp/2.js',function(err,resolvedPath){ |
注意事项
由于利用了异步方法,所以在写的时候一定要注意顺序。比如:1
2
3
4
5
6
7
8
9
10
11 ;
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就去查,但是我有点不同意,因为这样效率会很低。而且会导致变懒惰的后果。还是要多进行刻意的练习,才能有质的飞跃。这是自己所欠缺的。下面推荐几篇文章和阅读,也感谢这些优秀的文章: