1 | 'use strict';
|
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 Crixalis = require('../controller'),
|
29 | fs = require('fs'),
|
30 | zlib = require('zlib'),
|
31 | crypto = require('crypto'),
|
32 | mime = require('mime'),
|
33 | normalize = require('path').normalize,
|
34 | AsyncCache = require('async-cache'),
|
35 | extensions = {
|
36 | gzip : '.gz',
|
37 | deflate : '.def'
|
38 | };
|
39 |
|
40 | function send (context, file, size) {
|
41 | context.emit('response');
|
42 |
|
43 | context.sendHeaders();
|
44 |
|
45 | if (!context.is_head && file) {
|
46 | if (size > context._streamLimit) {
|
47 |
|
48 | fs.createReadStream(file)
|
49 | .on('end', function () {
|
50 | context._destroy();
|
51 | })
|
52 | .pipe(context.res);
|
53 | } else {
|
54 |
|
55 | context._fileCache.get(file, function (error, result) {
|
56 | context.res.end(result);
|
57 | context._destroy();
|
58 | });
|
59 | }
|
60 | } else {
|
61 | context.res.end();
|
62 | context._destroy();
|
63 | }
|
64 | }
|
65 |
|
66 | module.exports = function (options) {
|
67 | options = Object(options);
|
68 |
|
69 |
|
70 |
|
71 | var stat = options.symlinks === false? fs.stat : fs.lstat,
|
72 | cacheTime = Number(options.cacheTime || 307),
|
73 | statCache = new AsyncCache({
|
74 | max : 1 << 7,
|
75 | maxAge : cacheTime,
|
76 | length : function () { return 1 },
|
77 | load : function (file, callback) {
|
78 | stat(file, function (error, result) {
|
79 | fileCache.del(file);
|
80 | callback(error, result);
|
81 | })
|
82 | }
|
83 | }),
|
84 | fileCache = new AsyncCache({
|
85 | max : 1 << 20,
|
86 | length : function () { return arguments[0].length },
|
87 | load : fs.readFile
|
88 | });
|
89 |
|
90 |
|
91 | if (options.route !== false) {
|
92 | Crixalis.router().set({
|
93 | methods: ['GET', 'HEAD'],
|
94 | pattern: /^\/(.+?)(\.[^.\/]+)$/,
|
95 | capture: {
|
96 | '$1': 'path',
|
97 | '$2': 'extension'
|
98 | }
|
99 | }).to(function () {
|
100 | var that = this,
|
101 | extension = this.params.extension.slice(1),
|
102 | path = normalize(this.staticPath + '/' + this.params.path + '.' + extension);
|
103 |
|
104 | this.async = true;
|
105 |
|
106 | if (path.indexOf(normalize(this.staticPath))) {
|
107 | this.emit('default');
|
108 | that.render();
|
109 | return;
|
110 | }
|
111 |
|
112 | this[typeof this[extension] === 'function'? extension : 'serve'](path, function (error) {
|
113 | that.emit('default');
|
114 | that.render();
|
115 | });
|
116 | });
|
117 | }
|
118 |
|
119 | |
120 |
|
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 | Crixalis.define('property', 'staticPath', 'public');
|
127 |
|
128 | |
129 |
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 | Crixalis.define('property', 'cachePath', '.');
|
136 |
|
137 | |
138 |
|
139 |
|
140 |
|
141 |
|
142 | Crixalis.define('property', 'expires', {});
|
143 |
|
144 | |
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 | Crixalis.define('property', '_streamLimit', 32768);
|
152 |
|
153 | |
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 | Crixalis.define('property', '_fileCache', fileCache, { writable: false });
|
160 |
|
161 | |
162 |
|
163 |
|
164 |
|
165 |
|
166 |
|
167 | Crixalis.define('property', '_statCache', statCache, { writable: false });
|
168 |
|
169 | |
170 |
|
171 |
|
172 |
|
173 |
|
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 | Crixalis.define('method', 'serve', function (path, callback) {
|
184 | var that = this,
|
185 | stats;
|
186 |
|
187 | callback = callback || this.error;
|
188 |
|
189 | if (typeof callback !== 'function') {
|
190 | throw new Error('Callback must be a function');
|
191 | }
|
192 |
|
193 | this._statCache.get(path, function (error, result) {
|
194 | if (error) {
|
195 | callback.call(that, error);
|
196 | return;
|
197 | }
|
198 |
|
199 | if (!result.isFile()) {
|
200 | callback.call(that, new Error('Not a file'));
|
201 | return;
|
202 | }
|
203 |
|
204 | that.stash.path = path;
|
205 | that.stash.stat = result;
|
206 |
|
207 | that.render('file');
|
208 | });
|
209 |
|
210 | return this;
|
211 | });
|
212 |
|
213 | Crixalis.define('view', 'file', function () {
|
214 | var that = this,
|
215 | stash = this.stash,
|
216 | headers = this.headers,
|
217 | contentType = mime.lookup(stash.path),
|
218 | expires = this.expires[contentType],
|
219 | mtime = stash.stat.mtime,
|
220 | file, compression;
|
221 |
|
222 |
|
223 | contentType += '; charset=';
|
224 | contentType += stash.charset || 'utf-8';
|
225 |
|
226 | headers['Last-Modified'] = mtime.toUTCString();
|
227 |
|
228 |
|
229 |
|
230 |
|
231 | if (+mtime === Date.parse(this.req.headers['if-modified-since'])) {
|
232 | this.code = 304;
|
233 | send(this);
|
234 | return false;
|
235 | }
|
236 |
|
237 | headers['Vary'] = 'Accept-Encoding';
|
238 | headers['Content-Length'] = stash.stat.size;
|
239 | headers['Content-Type'] = contentType;
|
240 |
|
241 | if (expires) {
|
242 | headers['Expires'] = (new Date(this.start + expires)).toUTCString();
|
243 | }
|
244 |
|
245 |
|
246 | if (typeof this.compression === 'function') {
|
247 | compression = this.compression();
|
248 | }
|
249 |
|
250 |
|
251 | if (compression) {
|
252 | headers['Content-Encoding'] = compression;
|
253 |
|
254 | file = this.cachePath;
|
255 | file += '/';
|
256 |
|
257 |
|
258 | file += crypto.createHash('md5')
|
259 | .update(stash.path + mtime)
|
260 | .digest('hex');
|
261 | file += extensions[compression];
|
262 |
|
263 | this._statCache.get(file, function (error, result) {
|
264 | if (!error) {
|
265 | send(that, file, headers['Content-Length'] = result.size);
|
266 | return;
|
267 | }
|
268 |
|
269 | var compressor = zlib[compression === 'gzip'? 'createGzip' : 'createDeflate'](),
|
270 | length = 0,
|
271 |
|
272 |
|
273 | stream = fs.createReadStream(stash.path),
|
274 |
|
275 |
|
276 | cache = fs.createWriteStream(file);
|
277 |
|
278 | compressor
|
279 | .on('data', function (data) {
|
280 | length += data.length;
|
281 | });
|
282 |
|
283 | cache
|
284 | .on('close', function () {
|
285 | send(that, file, headers['Content-Length'] = length);
|
286 | });
|
287 |
|
288 | stream
|
289 | .pipe(compressor)
|
290 | .pipe(cache);
|
291 | });
|
292 |
|
293 | return false;
|
294 | }
|
295 |
|
296 |
|
297 | send(this, stash.path, headers['Content-Length']);
|
298 |
|
299 | return false;
|
300 | });
|
301 | };
|