Node.js文件 I/O 是由简单封装的标准 POSIX 函数提供的[1]。 fs(File System)是Node.js的内置模块,可直接通过
const fs = require('fs');
使用该模块。 fs 模块所有的API都有异步和同步的形式。异步形式始终以完成回调作为它最后一个参数。 传给完成回调的参数取决于具体方法,但第一个参数总是留给异常。 如果操作成功完成,则第一个参数会是 null 或 undefined。当使用同步形式时,任何异常都会被立即抛出。 可以使用 try/catch 来处理异常,或让它们往上冒泡。使用异步API无法保证函数调用顺序. 下列代码中使用fs.rename更改一文件名称,然后使用fs.stat检验改名操作结果。如果使用异步API,fs.stat可能会在调用fs.rename前执行[1].
fs.rename('/tmp/hello', '/tmp/world', function(err){
if (err) throw err;
console.log('renamed complete');
});
fs.stat('/tmp/world', function(err, stats){
if (err) throw err;
console.log(`stats: ${JSON.stringify(stats)}`);
});
正确的做法是使用一系列回调函数来保证API调用顺序
fs.rename('/tmp/hello', '/tmp/world', function(err){
if (err) throw err;
fs.stat('/tmp/world', function(err, stats){
if (err) throw err;
console.log(`stats: ${JSON.stringify(stats)}`);
});
});
使用readFile读文件样例[2]
var fs = require('fs');
var readFileName = './foo.txt';
var writeFileName = './target.txt';
/** Using the readFile API - Asynchronous */
fs.readFile( readFileName, "utf8", function(err, data){
if ( err ){ throw err;}
console.log("Reading file asynchronously");
console.log(data);
});
使用readFileSync读文件样例[2]
var fs = require('fs');
var readFileName = './foo.txt';
var writeFileName = './target.txt';
/** Using the readFile API - Asynchronous */
console.log("Reading file synchronously");
var fileData = fs.readFileSync( readFileName, "utf8");
console.log(fileData);
使用ReadStream读文件样例[2]
var fs = require('fs');
var readFileName = './foo.txt';
var writeFileName = './target.txt';
/** Reading file using ReadStream API */
//Creating a stream out of the file
var readStreamObject = fs.createReadStream( readFileName, { flags: 'r',
encoding:"utf8",
fd: null,
mode: 0666,
autoClose: true
});
//Setting up event handlers on the stream object
//readable - this event is fired when data can be read from stream
readStreamObject.on('readable', function(){
console.log("*** Reading from file using ReadStream");
});
//data - this event is fired when data is available to be read from stream
readStreamObject.on('data', function(data){
console.log(data);
});
使用writeFileSync写文件样例[2]
var fs = require('fs');
var readFileName = './foo.txt';
var writeFileName = './target.txt';
fs.writeFileSync( writeFileName, "Writing to a file synchronously from node.js", {"encoding":'utf8'});
console.log("*** File written successfully");
//Now reading the same file to confirm data written
fs.readFile( writeFileName, "utf8", function(err, data){
if ( err ){ throw err;}
console.log("*** Reading just written file");
console.log(data);
});
使用writeFile写文件样例[2]
var fs = require('fs');
var readFileName = './foo.txt';
var writeFileName = './target.txt';
fs.writeFile( writeFileName, "Writing to a file from node.js", {"encoding":'utf8'}, function(err){
if ( err ) { throw err; }
console.log("*** File written successfully");
//Now reading the same file to confirm data written
fs.readFile( writeFileName, "utf8", function(err, data){
if ( err ){ throw err;}
console.log("*** Reading just written file");
console.log(data);
});
});
多次对同一文件使用 fs.write 且不等待回调,是不安全的.推荐使用 fs.createWriteStream[1] 。
var fs = require('fs');
var readFileName = './foo.txt';
var writeFileName = './target.txt';
//Create a stream with the required path
var writeStreamObject = fs.createWriteStream( writeFileName );
//write to the stream using the API
writeStreamObject.write("Writing to a file using WriteStream", "utf8");
//Now read the same file to verify that the contents have been successfully written
fs.readFile( writeFileName, "utf8", function(err, data){
if ( err ){ throw err;}
console.log("*** Reading the recently written file");
console.log(data);
});
文件复制样例[3]
var fs = require('fs'),
path = require('path'),
out = process.stdout;
var stat
var filePath = './foo.mkv';
var readStream = fs.createReadStream(filePath);
var writeStream = fs.createWriteStream('file.mkv');
try {
stat = fs.statSync(filePath);
console.log("File exists.");
}
catch (e) {
if (e.code == 'ENOENT') {
console.log("File does not exist.");
//return false;
}
console.log("Exception fs.statSync (" + filePath + "): " + e);
// console.log(filePath + " does not exist.");
}
var totalSize = stat.size;
var passedLength = 0;
var lastSize = 0;
var startTime = Date.now();
readStream.on('data', function(chunk) {
passedLength += chunk.length;
if (writeStream.write(chunk) === false) {
readStream.pause();
}
});
readStream.on('end', function() {
writeStream.end();
});
writeStream.on('drain', function() {
readStream.resume();
});
setTimeout(function show() {
var percent = Math.ceil((passedLength / totalSize) * 100);
var size = Math.ceil(passedLength / 1000000);
var diff = size - lastSize;
lastSize = size;
out.clearLine();
out.cursorTo(0);
out.write('已完成' + size + 'MB, ' + percent + '%, 速度:' + diff * 2 + 'MB/s');
if (passedLength < totalSize) {
setTimeout(show, 500);
} else {
var endTime = Date.now();
console.log();
console.log('共用时:' + (endTime - startTime) / 1000 + '秒。');
}
}, 500);
代码样例[7]
copyFileSync = (srcFile, destFile)
function copyFileSync(srcFile, destFile) {
var BUF_LENGTH = 64*1024
var buff = new Buffer(BUF_LENGTH)
var fdr = fs.openSync(srcFile, 'r')
var fdw = fs.openSync(destFile, 'w')
var bytesRead = 1
var pos = 0
while( bytesRead > 0 )
{
bytesRead = fs.readSync(fdr, buff, 0, BUF_LENGTH, pos)
fs.writeSync(fdw,buff,0,bytesRead)
pos += bytesRead
}
fs.closeSync(fdr);
fs.closeSync(fdw);
}
Results of testing influences of the buffer size for time consumption shows that there is a lower limitation of the parameter. For one file about 300 MB, the coping processes with various buffer sizes, which over 2 KB, take roughly similar time. If the buffer is lower than 2 KB, however, time computation would increase significantly.
stream version in one line [8]
var fs = require('fs');
fs.createReadStream('test.log').pipe(fs.createWriteStream('newLog.log'));
stream version with errors handling provided by Mike Schilling [8]
function copyFile(source, target, cb) {
var cbCalled = false;
var rd = fs.createReadStream(source);
rd.on("error", function(err) {
done(err);
});
var wr = fs.createWriteStream(target);
wr.on("error", function(err) {
done(err);
});
wr.on("close", function(ex) {
done();
});
rd.pipe(wr);
function done(err) {
if (!cbCalled) {
cb(err);
cbCalled = true;
}
}
}
stream version with errors handling presented by Jens Hauke [8]
function copyFile(source, target, cb) {
var cbCalled = false;
var rd = fs.createReadStream(source);
rd.on("error", done);
var wr = fs.createWriteStream(target);
wr.on("error", done);
wr.on("close", function(ex) {
done();
});
rd.pipe(wr);
function done(err) {
if (!cbCalled) {
cb(err);
cbCalled = true;
}
}
}
大多数情况下读文件的速度总比写文件的速度快,这样便导致大量的数据被积压在内存中,当要读取的文件很大时,甚至会导致因占用内存太多而导致整个 Node.js 进程崩溃。
当读取速度超出我们期望的值时,可以执行pause()先暂停,待时机符合时再执行resume()重新开始[5]。
var util = require('util');
var events = require('events');
var fs = require('fs');
// 一个几百 M 的文本文件
var inputFile = '/Volumes/foo.txt';
function ReadStreamThrottle (stream, speed) {
this._stream = stream;
this._readBytes = 0;
this._speed = speed;
this._ended = false;
this._readBytesSecond = 0;
this._lastTimestamp = Date.now();
this._paused = false;
var self = this;
// 检查速度是否太快
function isTooFast () {
var t = (Date.now() - self._lastTimestamp) / 1000;
var bps = self._readBytesSecond / t;
return bps > speed;
}
// 每隔一段时间检查速度
function checkSpeed () {
if (isTooFast()) {
self.pause();
// 直到平均速度放缓到预设的值时继续读流
var tid = setInterval(function () {
if (!isTooFast()) {
clearInterval(tid);
self.resume();
}
}, 100);
} else {
self.resume();
}
}
stream.on('data', function (chunk) {
self._readBytes += chunk.length;
self._readBytesSecond += chunk.length;
self.emit('data', chunk);
checkSpeed();
});
stream.on('end', function () {
self._ended = true;
self.emit('end');
});
}
util.inherits(ReadStreamThrottle, events.EventEmitter);
ReadStreamThrottle.prototype.pause = function () {
this._paused = true;
this._stream.pause();
};
ReadStreamThrottle.prototype.resume = function () {
this._paused = false;
this._stream.resume();
};
// 读取文件,限制速度不大于 10MB/S
var MB = 1024 * 1024;
var s = new ReadStreamThrottle(fs.createReadStream(inputFile), MB * 10);
var bytes = 0;
var t = Date.now();
s.on('data', function (c) {
bytes += c.length;
var spent = (Date.now() - t) / 1000;
console.log('read %s bytes, speed: %sMB/S', bytes, (bytes / MB / spent).toFixed(2));
});
s.on('end', function () {
console.log('end. total %s bytes', bytes);
});
根据"RFC 1867 - Form-based File Upload in HTML"协议,使用Node.js实现文件上传[9]
在浏览器上通过form 表单来上传文件时使用的数据包格式如下:
POSThttp://www.foo.com/ HTTP/1.1
Host: www.foo.com
Content-Length: 199
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarywr3X7sXBYQQ4ZF5G
------WebKitFormBoundarywr3X7sXBYQQ4ZF5G
Content-Disposition: form-data; name="myfile"; filename="upload.txt"
Content-Type: text/plain
hello world
------WebKitFormBoundarywr3X7sXBYQQ4ZF5G--
样例代码[9]
const http = require('http');
const fs = require('fs');
//生成分隔数据
var boundaryKey = '----WebKitFormBoundaryjLVkbqXtIi0YGpaB';
var currentDir = __dirname + '/';
var sourceFileName = 'BMW5_torsion.bdf'
var filePath = currentDir + sourceFileName;
// checking file
try {
stat = fs.statSync(filePath);
console.log("File exists.");
}
catch (e) {
if (e.code == 'ENOENT') {
console.log("File does not exist.");
//return false;
}
console.log("Exception fs.statSync (" + filePath + "): " + e);
// console.log(filePath + " does not exist.");
}
//
var options = {
hostname: 'localhost',
port: 80,
path: '/fileupload',
method: 'POST'
}
//读取需要上传的文件内容
fs.readFile( filePath, function (err, data) {
//拼装分隔数据段
var payload = '--' + boundaryKey + '\r\n' ;
payload += 'Content-Disposition:form-data; name="myfile"; filename="'+ sourceFileName + '"\r\n' ;
payload += 'Content-Type:text/plain\r\n\r\n';
payload += data;
payload += '\r\n--' + boundaryKey + '--';
//发送请求
var req = http.request(options, function (res) {
res.setEncoding('utf8');
res.on('data', function (chunk) {
console.log('body:' + chunk);
});
});
req.on('error', function(e) {
console.error("error:"+e);
});
//把boundary、要发送的数据大小以及数据本身写进请求
req.setHeader('Content-Type', 'multipart/form-data; boundary='+boundaryKey+'');
req.setHeader('Content-Length', Buffer.byteLength(payload, 'utf8'));
req.write(payload);
req.end();
});
var urlparse = require('url').parse
, http = require('http')
, fs = require('fs');
function upload(url, uploadfile, callback) {
var urlinfo = urlparse(url);
var options = {
method: 'POST',
host: urlinfo.host,
path: urlinfo.pathname
};
if(urlinfo.port) {
options.port = urlinfo.port;
}
if(urlinfo.search) {
options.path += urlinfo.search;
}
var req = http.request(options, function(res) {
var chunks = [], length = 0;
res.on('data', function(chunk) {
length += chunk.length;
chunks.push(chunk);
});
res.on('end', function() {
var buffer = new Buffer(length);
// delay copy
for(var i = 0, pos = 0, size = chunks.length; i < size; i++) {
chunks[i].copy(buffer, pos);
pos += chunks[i].length;
}
res.body = buffer;
callback(null, res);
});
});
var readstream = fs.createReadStream(uploadfile);
readstream.on('data', function(chunk) {
console.log('write', chunk.length);
req.write(chunk);
});
readstream.on('end', function() {
req.end();
});
};
upload('http://weibo.com/', '/tmp/bigfile.pdf', function(err, res) {
console.log(res.statusCode, res.headers);
});
大文件下载样例[4]
var urlparse = require('url').parse
var http = require('http')
var fs = require('fs');
function download(url, savefile, callback) {
console.log('download', url, 'to', savefile)
var urlinfo = urlparse(url);
var options = {
method: 'GET',
host: urlinfo.host,
path: urlinfo.pathname
};
if(urlinfo.port) {
options.port = urlinfo.port;
}
if(urlinfo.search) {
options.path += urlinfo.search;
}
var req = http.request(options, function(res) {
var writestream = fs.createWriteStream(savefile);
writestream.on('close', function() {
callback(null, res);
});
res.pipe(writestream);
});
req.end();
};
download('http://web.mit.edu/cocosci/Papers/sci_reprint.pdf', '/temp/isomap.pdf', function(err, res) {
console.log(res.statusCode, res.headers);
});
大文件的下载以及断点续传基本原理是,在文件的下载断开以后。客户端继续向服务器端请求的时候,http请求的头文件中会多了一个参数“Range”,来标示当前下载的文件所断开的位置[6]。
function Transfer(req, resp) {
this.req = req;
this.resp = resp;
}
/**
* [@description](/user/description) 计算上次的断点信息
* [@param](/user/param) {string} Range 请求http头文件中的断点信息,如果没有则为undefined,格式(range: bytes=232323-)
* [@return](/user/return) {integer} startPos 开始的下载点
*/
Transfer.prototype._calStartPosition = function(Range) {
var startPos = 0;
if( typeof Range != 'undefined') {
var startPosMatch = /^bytes=([0-9]+)-$/.exec(Range);
startPos = Number(startPosMatch[1]);
}
return startPos;
}
/**
* [@description](/user/description) 配置头文件
* [@param](/user/param) {object} Config 头文件配置信息(包含了下载的起始位置和文件的大小)
*/
Transfer.prototype._configHeader = function(Config) {
var startPos = Config.startPos,
fileSize = Config.fileSize,
resp = this.resp;
// 如果startPos为0,表示文件从0开始下载的,否则则表示是断点下载的。
if(startPos == 0) {
resp.setHeader('Accept-Range', 'bytes');
} else {
resp.setHeader('Content-Range', 'bytes ' + startPos + '-' + (fileSize - 1) + '/' + fileSize);
}
resp.writeHead(206, 'Partial Content', {
'Content-Type' : 'application/octet-stream',
});
}
/**
* [@description](/user/description) 初始化配置信息
* [@param](/user/param) {string} filePath
* [@param](/user/param) {function} down 下载开始的回调函数
*/
Transfer.prototype._init = function(filePath, down) {
var config = {};
var self = this;
fs.stat(filePath, function(error, state) {
if(error)
throw error;
config.fileSize = state.size;
var range = self.req.headers.range;
config.startPos = self._calStartPosition(range);
self.config = config;
self._configHeader(config);
down();
});
}
/**
* [@description](/user/description) 生成大文件文档流,并发送
* [@param](/user/param) {string} filePath 文件地址
*/
Transfer.prototype.Download = function(filePath) {
var self = this;
path.exists(filePath, function(exist) {
if(exist) {
self._init(filePath, function() {
var config = self.config
resp = self.resp;
fReadStream = fs.createReadStream(filePath, {
encoding : 'binary',
bufferSize : 1024 * 1024,
start : config.startPos,
end : config.fileSize
});
fReadStream.on('data', function(chunk) {
resp.write(chunk, 'binary');
});
fReadStream.on('end', function() {
resp.end();
});
});
} else {
console.log('文件不存在!');
return;
}
});
}
var fs = require('fs')
http = require('http')
path = require('path');
var server = http.createServer(function(req, resp) {
var transfer = new Transfer(req, resp);
var filePath = '/Users/xukai/Downloads/love.rmvb';
transfer.Download(filePath);
});
server.listen('8000');
Node.js 中 fs 模块的 isFile() 函数使用样例
var fs = require('fs');
var path = '.\foo.txt';
// 同步版
fs.statSync( path ).isFile() ;
// 异步版
fs.stat( path, function(s){
callback(s.isFile() );
});
参考文献
[1] http://nodejs.cn/api/fs
[2] http://javabeat.net/nodejs-read-write-file/
[3] https://segmentfault.com/a/1190000000519006
[4] http://www.cnblogs.com/fengmk2/archive/2011/08/16/2140460.html
[5] http://morning.work/page/2015-07/read_and_write_big_file_in_nodejs.html
[6] https://cnodejs.org/topic/4f5b47c42373009b5c04e9cb
[7] http://procbits.com/2011/11/15/synchronous-file-copy-in-node-js
[8] http://stackoverflow.com/questions/11293857/fastest-way-to-copy-file-in-node-js
[9] http://www.aichengxu.com/javascript/24609788.htm