基于Node.js的文件服务器(使用Q重构代码)

之前写过一篇文章,简单介绍了一个基于Node.js的静态文件服务器。那时还只是个人兴趣。最近又有了关于服务器的新的需求,我就想花点时间,好好研究一下。所以把之前的代码拿出来重构了一番,整体代码变得干净很多。

首先最新Node.js是支持generator的,所谓generator,就是javascript中的协程(半协程),不过功能稍弱,仅仅是为了解决js中凶名赫赫的callback hell而诞生的。这里我并没有使用generator,而是使用promise(饭要一口一口吃,先弄明白promise再去学习generator)。promise并不是新的语法,而是一种书写方式。最出名的实现是Q。

关于Q的学习资料可以看这里,非常清楚。

server.js的代码:

'use strict';

var CONFIG = {
	'host': '127.0.0.1',
	'port': 9527,
	'site_base': './site',
	'file_expiry_time': 0, // HTTP cache expiry time, minutes
	'directory_listing': true
};

var MIME_TYPES = {
	'.txt': 'text/plain',
	'.md': 'text/plain',
	'': 'text/plain',
	'.html': 'text/html',
	'.css': 'text/css',
	'.js': 'application/javascript',
	'.json': 'application/json',
	'.jpg': 'image/jpeg',
	'.png': 'image/png',
	'.gif': 'image/gif',
	'.zip': 'text/plain',
	'.cfg': 'text/plain'
};

var EXPIRY_TIME = (CONFIG.file_expiry_time * 60).toString();

var http = require('http');
var Path = require('path');
var Crypto = require('crypto');
var Custard = require('./custard/custard');
var Q = require('q');
var fs = require('./filesystem')
// An object representing a server response

function ResponseObject( metadata ){
	this.status = metadata.status || 200;
	this.data = metadata.data || false;
	this.type = metadata.type || false;
}

ResponseObject.prototype.getEtag = function (){
	var hash = Crypto.createHash( 'md5' );
	hash.update( this.data );
	return hash.digest( 'hex' );
};

function getFileList(files, url, callback, error) {
	var template = new Custard;
	var full_path = CONFIG.site_base + url;
	var i = 0;
	template.addTagSet('h', require('./custard/templates/tags/html'));
	template.addTagSet('c', {
		'title': 'Index of ' + url,
		'file_list': function (h) {
			var items = [];
			var stats;
			for (i = 0; i < files.length; i += 1) {
				stats = fs.statSync(full_path + files[i]);
				if (stats.isDirectory()) {
					files[i] += '/';
				}
				items.push(h.el('li', [
					h.el('a', {'href': url + files[i]}, files[i])
				]));
			}
			return items;
		}
	});

	Q.nbind(template.render, template)
	("h.doctype('html5'),h.html([	h.head([		h.el('title', c.title),	]),	h.body([		h.el('h1', c.title),		h.el('ul', c.file_list(h))	])])"
	).then(callback, error);
}

// Filter server requests by type
function handleRequest(url){
	// hack fix  version没有扩展名,但是是一个文本文件]
	var url = url;
	var deferred = Q.defer();
	if ( Path.extname( url ) === '' && url.indexOf('version') == -1 ) {
		// 路径
		var full_path = CONFIG.site_base + url;
		if (!CONFIG.directory_listing) {
			//	Forbidden
			deferred.resolve(new ResponseObject({'status': 403}))
			return deferred.Promise;
		}

		fs.exists(full_path)
			.then(function() {
				return fs.readdir(full_path)
			}, function() {
				deferred.resolve(new ResponseObject({'status': 404}))
			})
			.then(function(files) {
				getFileList(files, url, function(html) {
					deferred.resolve(new ResponseObject({'data': new Buffer(html), 'type': 'text/html'}));
				}, function(error) {
					deferred.resolve(new ResponseObject({'data': error.stack, 'status': 500}));
				})
		}, function(error) {
			// Internal error
			deferred.resolve(new ResponseObject({'data': error.stack, 'status': 500}));
		})
	} else {
		// 文件
		var path = CONFIG.site_base + url;
		fs.exists(path)
			.then(function() {
				return fs.readFile(path)
			}, function() {
				deferred.resolve(new ResponseObject({'status': 404}));
			})
			.then(function(data){
				deferred.resolve(new ResponseObject( {'data': new Buffer( data ), 'type': MIME_TYPES[Path.extname(path)]}));
			}, function(error) {
				deferred.resolve(new ResponseObject({'data': error.stack, 'status': 500}))
			})
	}

	return deferred.promise;;
}

function parseRange (str, size) {
    if (str.indexOf(",") != -1) {
        return;
    }
    str = str.replace("bytes=", "");
    var range = str.split("-"),
        start = parseInt(range[0], 10),
        end = parseInt(range[1], 10);
    // Case: -100
    if (isNaN(start)) {
        start = size - end;
        end = size - 1;
        // Case: 100-
    } else if (isNaN(end)) {
        end = size - 1;
    }
    // Invalid
    if (isNaN(start) || isNaN(end) || start > end || end > size) {
        return;
    }
    return {
        start: start,
        end: end
    };
};

var compressHandle = function (raw, matched, statusCode, reasonPhrase) {
    var stream = raw;
    var acceptEncoding = request.headers['accept-encoding'] || "";
    if (matched && acceptEncoding.match(/\bgzip\b/)) {
        response.setHeader("Content-Encoding", "gzip");
        stream = raw.pipe(zlib.createGzip());
    } else if (matched && acceptEncoding.match(/\bdeflate\b/)) {
        response.setHeader("Content-Encoding", "deflate");
        stream = raw.pipe(zlib.createDeflate());
    }
    response.writeHead(statusCode, reasonPhrase);
    stream.pipe(response);
};

// Start server
http.createServer(function(request, response) {
	var headers;
	var etag;
	if ( request.method === 'GET' ){
		//	Get response object
		handleRequest( request.url).then(function (response_object) {
			if (!response_object || ! response_object.data || response_object.data.length <= 0 ) {
				// 无文件内容
				response.writeHead(response_object.status);
				response.end();
				return;
			}

			etag = response_object.getEtag();
			if ( request.headers.hasOwnProperty('if-none-match') && request.headers['if-none-match'] === etag ){
				//	Not Modified
				response.writeHead( 304 );
				response.end();
				return;
			}

			var fileFullSize = response_object.data.length;
			if (request.headers["range"]) {
				// 如果有range
				var range = parseRange(request.headers["range"], fileFullSize);
				if (range) {
					var raw = fs.createReadStream(CONFIG.site_base + request.url, {
						"start": range.start,
						"end": range.end
					});
					//console.log(raw);
					headers = {
						'Accept-Ranges': 'bytes',
						'Content-Type': response_object.type,
						'Content-Length' : (range.end - range.start + 1),
						'Cache-Control' : 'max-age=' + EXPIRY_TIME,
						'Content-Range' : "bytes " + range.start + "-" + range.end + "/" + fileFullSize,
						'ETag' : etag
					};
					console.log("range " + range.start + "-" + range.end);
					response.writeHead( response_object.status, headers );
					//response.end( response_object.data );
					raw.pipe(response);
					//raw = "";
					//response.end( raw );
				} else {
					console.log("range format error");
					// range格式错误
					response.removeHeader("Content-Length");
					response.writeHead(416, "Request Range Not Satisfiable");
					response.end();
				}
			} else {
				// 没有range,全文件
				headers = {
					'Accept-Ranges': 'bytes',
					'Content-Type': response_object.type,
					'Content-Length' : response_object.data.length,
					'Cache-Control' : 'max-age=' + EXPIRY_TIME,
					'ETag' : etag
				};
				response.writeHead( response_object.status, headers );
				response.end( response_object.data );
			}
		} );
	} else if ( request.method == 'HEAD') {
		handleRequest(request.url).then(function(response_object){
			if (!response_object || !response_object.data || response_object.data.length <= 0 ) {
				response.writeHead(response_object.status);
				response.end();
				return;
			}

			etag = response_object.getEtag();
			if ( request.headers.hasOwnProperty('if-none-match') && request.headers['if-none-match'] === etag ){
				// Not Modified
				response.writeHead( 304 );
				response.end();
				return;
			}
			headers = {
				'Content-Type': response_object.type,
				'Content-Length' : response_object.data.length,
				'Cache-Control' : 'max-age=' + EXPIRY_TIME,
				'ETag' : etag
			};
			response.writeHead( response_object.status, headers );
			response.end();
		} );
	} else {
		// Forbidden
		response.writeHead(403);
		response.end();
	}
} ).listen( CONFIG.port, CONFIG.host );

console.log( 'Site Online : http://' + CONFIG.host + ':' + CONFIG.port.toString() + '/' );

filesystem.js的代码(在server.js中有用到,只是使用Q简单的封装了下node.js的异步文件操作):

var Q = require('q')
var fs = require('fs')

// 使用Q封装回调形式的文件操作函数
var fs_readFile = Q.nfbind(fs.readFile);
var fs_readdir = Q.nfbind(fs.readdir);
var fs_stat = Q.nfbind(fs.stat);

function exists(path) {
    var defer = Q.defer();

    fs.exists(path, function(exists) {
        if (exists) {
            defer.resolve(exists);
        } else {
            defer.reject(exists);
        }
    });
    return defer.promise;
}

function readFile(path) {
    return fs_readFile(path);
}

function readdir(path) {
    return fs_readdir(path);
}

function stat(path) {
    return fs_stat(path)
}

// 同步函数,直接调用
function statSync(path) {
    return fs.statSync(path);
}

function readFileSync(path) {
    return fs.readFileSync(path)
}

module.exports.exists = exists;
module.exports.readFile = readFile;
module.exports.readFileSync = readFileSync;
module.exports.readdir = readdir;
module.exports.stat = stat;
module.exports.statSync = statSync;

说实话,即便是重构后的代码,也不是非常简洁。使用Q最主要是解决多层回调的问题的。在上面的代码中,其实没有太多的多层调用,所以体现的不是非常明显,不过项目大了,会有显著的效果。

即便使用Q之后,调试起来还是比较麻烦,因为我们无法清晰的知道这个函数是谁,在什么时候调用的,也就是说,很多时候我们获取不到调用堆栈(可以获取到,但是获取到的信息几乎无意义,因为函数都是在Q的task中调用的)。

关于Q封装Node.js的异步函数,这里需要注意一下filesystem.js中的exists()函数。Q.nfbind做的事情其实就是exists中所做的。函数第一个参数必须是error,第二个参数必须是data,这样的函数才能够直接使用Q.nfbind封装,像fs.exits函数,只有一个参数,所以无法使用Q.nfbind封装。

时间: 2024-10-13 00:18:11

基于Node.js的文件服务器(使用Q重构代码)的相关文章

分享:Node.js静态文件服务器实战

博文共赏:Node.js静态文件服务器实战 作者 田永强 发布于 2011年11月13日 | 12 讨论 [编者按]<博文共赏>是InfoQ中文站新推出的一个专栏,精选来自国内外技术社区和个人博客上的技术文章,让更多的读者朋友受益,本栏目转载的内容都经过原作者授权.文章推荐可以发送邮件到[email protected]. 本文是我对V5Node项目的总结,该项目的特性包括: 项目大多数的文件都是属于静态文件,只有数据部分存在动态请求. 数据部分的请求都呈现为RESTful的特性. 所以项目主

基于Node.js的强大爬虫 能直接发布抓取的文章哦

基于Node.js的强大爬虫 能直接发布抓取的文章哦 基于Node.js的强大爬虫能直接发布抓取的文章哦!本爬虫源码基于WTFPL协议,感兴趣的小伙伴们可以参考一下 一.环境配置 1)搞一台服务器,什么linux都行,我用的是CentOS 6.5: 2)装个mysql数据库,5.5或5.6均可,图省事可以直接用lnmp或lamp来装,回头还能直接在浏览器看日志: 3)先安个node.js环境,我用的是0.12.7,更靠后的版本没试过: 4)执行npm -g install forever,安装f

基于Node.js的文件下载服务器

首先表示,并不是我客户端玩腻歪了要玩跨界:另外,代码不是我写的,而是我找的. 直接上代码地址,不想看唠叨的就直接下载代码就ok了. https://github.com/andygrn/Node.js-File-Server 下载代码后,直接把server.js拖到node.exe图标上就可以运行了,在浏览器中访问http://127.0.0.1:80/可以直接连接下载服务器了. 1.为什么要关注下载服务器(文件服务器)? 因为我要写个多线程断点续传的下载库,当然要有一个服务器进行测试.用Apa

Node.js静态文件服务器实战[转]

这是一篇阐述得比较详细的文章,从伺服静态文件,到支持文件夹,缓存,gzip/deflate,range,都是带着讲解完成的,全文转载如下: 我们的app.js文件里的结构很明确: var PORT = 8000; var http = require('http'); var server = http.createServer(function (request, response) { // TODO }); server.listen(PORT); console.log("Server

基于Node.js + jade + Mongoose 模仿gokk.tv

原文摘自我的前端博客,欢迎大家来访问 http://www.hacke2.cn 关于gokk 大学的娱乐活动基本就是在寝室看电影了→_→,一般都会选择去goxiazai.cc上看,里面的资源多,质量高 .站长会推荐评分很高广受好评的电影给大家免费下载,整体来说真是不错,但前两月由于版权问题被迫转型 这也是没办法的事,程序员更应该尊重版权问题,我们也能理解,后来站长又开了gokk个不是给地址让你 下载,而是将网络一些优秀视频站点资源提供出来观看,质量变低了,好怀念以前的goxiazai啊.. 最近

基于Node.js + socket.io实现WebSocket的聊天DEMO

原文摘自我的前端博客,欢迎大家来访问 http://hacke2.github.io 简介 最近看Node.js和HTML5,练手了一个简易版的聊天DEMO,娱乐一下 为什么需要socket.io? node.js提供了高效的服务端运行环境,但是由于浏览器端对HTML5的支持不一, 为了兼容所有浏览器,提供卓越的实时的用户体验,并且为程序员提供客户端与服务端一致的编程体验, 于是socket.io诞生. 简答来说socket.io具体以下特点: 1.socket.io设计的目标是支持任何的浏览器

《基于Node.js实现简易聊天室系列之详细设计》

一个完整的项目基本分为三个部分:前端.后台和数据库.依照软件工程的理论知识,应该依次按照以下几个步骤:需求分析.概要设计.详细设计.编码.测试等.由于缺乏相关知识的储备,导致这个Demo系列的文章层次不是很清楚,索性这一章将所有的过程(前后端以及数据库)做一个介绍,下一章写完总结就OK了吧. (1)前端部分 涉及到的技术:html.css.bootstrap.jquery.jquery UI 登录/注册界面使用的是bootstrap响应式布局,即支持不同尺寸的客户端,以此提高用户的体验.在这之前

基于Node.js和express的日志服务器

首先,这篇文章学习的意义大于实际价值.如果按我的本意,直接在游戏中加入友盟,信息更全,而且非常简单.不过总是有很多人会凭着自己过时或者错误的经验去说别的东西多么不好,自己的东西多么好.好在,我自认为学习能力非常强,解决问题的能力也非常强.真让我做一个服务器+前端,也是在兴趣之中和能力之内. 一.Node.js简介 原本javascript纯粹是一个前端语言,干的基本上是让网页更丰富更炫的事情.不过Node.js出现后,javacript成为了前后端通吃的语言.比如网易的pomelo就是基于Nod

node.js高级编程|node.js 视频教程_基于node.js+Express.js+Jade+MongoDB实战开发

基于node.js+Express.js+Jade+MongoDB开发Web即时聊天系统课程讲师:幻星课程分类:前端开发适合人群:初级课时数量:36课时更新程度:完成用到技术:Node.js NPM. Express.NoSQL,MongoDB涉及项目:匿名聊天网站系统node.js视频教程:http://www.ibeifeng.com/goods-462.htmlnode.js 教程适合人群:node.js视频教程要求学员了解JavaScript,node.js入门教程适合希望更深入的学习N