1 | 'use strict';
|
2 |
|
3 | const Crypto = require('crypto');
|
4 |
|
5 | const Boom = require('@hapi/boom');
|
6 | const Bounce = require('@hapi/bounce');
|
7 | const LruCache = require('lru-cache');
|
8 |
|
9 |
|
10 | const internals = {
|
11 | pendings: Object.create(null)
|
12 | };
|
13 |
|
14 |
|
15 | internals.streamEnd = function (stream) {
|
16 |
|
17 | return new Promise((resolve, reject) => {
|
18 |
|
19 | stream.on('end', resolve);
|
20 | stream.on('error', reject);
|
21 | });
|
22 | };
|
23 |
|
24 |
|
25 | internals.computeHashed = async function (response, stat) {
|
26 |
|
27 | const etags = response.request.server.plugins.inert._etags;
|
28 | if (!etags) {
|
29 | return null;
|
30 | }
|
31 |
|
32 |
|
33 |
|
34 | const path = response.source.path;
|
35 | const cachekey = [path, stat.ino, stat.size, stat.mtime.getTime()].join('-');
|
36 |
|
37 |
|
38 |
|
39 | const cachedEtag = etags.get(cachekey);
|
40 | if (cachedEtag) {
|
41 | return cachedEtag;
|
42 | }
|
43 |
|
44 | let promise = internals.pendings[cachekey];
|
45 | if (promise) {
|
46 | return await promise;
|
47 | }
|
48 |
|
49 |
|
50 |
|
51 | const compute = async () => {
|
52 |
|
53 | try {
|
54 | const hash = await internals.hashFile(response);
|
55 | etags.set(cachekey, hash);
|
56 |
|
57 | return hash;
|
58 | }
|
59 | finally {
|
60 | delete internals.pendings[cachekey];
|
61 | }
|
62 | };
|
63 |
|
64 | internals.pendings[cachekey] = promise = compute();
|
65 |
|
66 | return await promise;
|
67 | };
|
68 |
|
69 |
|
70 | internals.hashFile = async function (response) {
|
71 |
|
72 | const hash = Crypto.createHash('sha1');
|
73 | hash.setEncoding('hex');
|
74 |
|
75 | const fileStream = response.source.file.createReadStream({ autoClose: false });
|
76 | fileStream.pipe(hash);
|
77 |
|
78 | try {
|
79 | await internals.streamEnd(fileStream);
|
80 | return hash.read();
|
81 | }
|
82 | catch (err) {
|
83 | Bounce.rethrow(err, 'system');
|
84 | throw Boom.boomify(err, { message: 'Failed to hash file', data: { path: response.source.path } });
|
85 | }
|
86 | };
|
87 |
|
88 |
|
89 | internals.computeSimple = function (response, stat) {
|
90 |
|
91 | const size = stat.size.toString(16);
|
92 | const mtime = stat.mtime.getTime().toString(16);
|
93 |
|
94 | return size + '-' + mtime;
|
95 | };
|
96 |
|
97 |
|
98 | exports.apply = async function (response, stat) {
|
99 |
|
100 | const etagMethod = response.source.settings.etagMethod;
|
101 | if (etagMethod === false) {
|
102 | return;
|
103 | }
|
104 |
|
105 | let etag;
|
106 | if (etagMethod === 'simple') {
|
107 | etag = internals.computeSimple(response, stat);
|
108 | }
|
109 | else {
|
110 | etag = await internals.computeHashed(response, stat);
|
111 | }
|
112 |
|
113 | if (etag !== null) {
|
114 | response.etag(etag, { vary: true });
|
115 | }
|
116 | };
|
117 |
|
118 |
|
119 | exports.Cache = LruCache;
|