Skip to content

Commit 27543ac

Browse files
committed
Implement LRU strategy #6
1 parent 4a7b14e commit 27543ac

File tree

4 files changed

+173
-3
lines changed

4 files changed

+173
-3
lines changed

index.js

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ const sprintf = require("sprintf-js").sprintf;
44
const fs = require("fs");
55
const path = require("path");
66

7+
const getFolderSize = require("get-folder-size");
8+
79
function makeDirIfNotExists(dir) {
810
if (!fs.existsSync(dir)) {
911
fs.mkdirSync(dir);
@@ -63,13 +65,67 @@ function touch(path) {
6365
}
6466
}
6567

68+
function evictLeastRecentlyUsed(cacheDir, maxSize, logger) {
69+
getFolderSize(cacheDir, (err, size) => {
70+
if (err) {
71+
throw err;
72+
}
73+
74+
if (size >= maxSize) {
75+
try {
76+
// find least recently used file
77+
const leastRecentlyUsed = findLeastRecentlyUsed(cacheDir);
78+
79+
// and delete it
80+
const { dir } = path.parse(leastRecentlyUsed.path);
81+
fs.unlinkSync(leastRecentlyUsed.path);
82+
fs.rmdirSync(dir);
83+
84+
if (logger) {
85+
logger.info(`Evicted ${leastRecentlyUsed.path} from cache`);
86+
}
87+
88+
evictLeastRecentlyUsed(cacheDir, maxSize);
89+
} catch (e) {
90+
logger.error(e);
91+
}
92+
}
93+
});
94+
}
95+
96+
function findLeastRecentlyUsed(dir, result) {
97+
let files = fs.readdirSync(dir);
98+
result = result || { atime: Date.now(), path: "" };
99+
100+
files.forEach(file => {
101+
const newBase = path.join(dir, file);
102+
103+
if (fs.statSync(newBase).isDirectory()) {
104+
result = findLeastRecentlyUsed(newBase, result);
105+
} else {
106+
const { atime } = fs.statSync(newBase);
107+
108+
if (atime < result.atime) {
109+
result = {
110+
atime,
111+
path: newBase
112+
};
113+
}
114+
}
115+
});
116+
117+
return result;
118+
}
119+
66120
const middleWare = (module.exports = function(options) {
67121
return async function(req, res, next) {
68122
options = options || {};
69123
options.cacheDir =
70124
options && options.cacheDir
71125
? options.cacheDir
72126
: path.join(process.cwd(), "/tmp");
127+
options.maxSize =
128+
options && options.maxSize ? options.maxSize : 1024 * 1024 * 1024;
73129

74130
const {
75131
dir1,
@@ -88,7 +144,7 @@ const middleWare = (module.exports = function(options) {
88144
const firstFile = fs.readdirSync(assetCachePath)[0];
89145

90146
// touch file for LRU eviction
91-
touch(`${assetCachePath}/${firstFile}`);
147+
middleWare.touch(`${assetCachePath}/${firstFile}`);
92148

93149
const [contentType, contentLength] = middleWare.decodeAssetCacheName(
94150
firstFile
@@ -124,6 +180,12 @@ const middleWare = (module.exports = function(options) {
124180
res.locals.contentType = blob.type;
125181
res.locals.contentLength = blob.size;
126182

183+
middleWare.evictLeastRecentlyUsed(
184+
options.cacheDir,
185+
options.maxSize,
186+
options.logger
187+
);
188+
127189
fs.writeFileSync(`${assetCachePath}/${fileName}`, res.locals.buffer);
128190

129191
const [seconds, nanoSeconds] = process.hrtime(startTime);
@@ -137,7 +199,6 @@ const middleWare = (module.exports = function(options) {
137199

138200
next();
139201
} catch (e) {
140-
console.log(e);
141202
// in case fs.writeFileSync writes partial data and fails
142203
if (fs.existsSync(assetCachePath)) {
143204
fs.unlinkSync(assetCachePath);
@@ -157,3 +218,6 @@ middleWare.makeAssetCachePath = makeAssetCachePath;
157218
middleWare.makeDirIfNotExists = makeDirIfNotExists;
158219
middleWare.encodeAssetCacheName = encodeAssetCacheName;
159220
middleWare.decodeAssetCacheName = decodeAssetCacheName;
221+
middleWare.findLeastRecentlyUsed = findLeastRecentlyUsed;
222+
middleWare.evictLeastRecentlyUsed = evictLeastRecentlyUsed;
223+
middleWare.touch = touch;

package-lock.json

Lines changed: 20 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
},
2323
"homepage": "https://github.com/julianrubisch/express-asset-file-cache-middleware#readme",
2424
"dependencies": {
25+
"get-folder-size": "^2.0.1",
2526
"node-fetch": "^2.6.0",
2627
"sprintf-js": "^1.1.2"
2728
},

test/test.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ describe("Middleware", function() {
3030
arrayBuffer: sinon.stub().resolves([])
3131
})
3232
});
33+
sinon.stub(middleware, "evictLeastRecentlyUsed");
3334
this.makePathSpy = sinon
3435
.stub(middleware, "makeAssetCachePath")
3536
.returns({ dir1: "a1", dir2: "b2", path: "./a1/b2/0123456789abcdef" });
@@ -69,6 +70,8 @@ describe("Middleware", function() {
6970
.stub(fs, "readdirSync")
7071
.withArgs("./a1/b2/0123456789abcdef")
7172
.returns(["dW5kZWZpbmVkOnVuZGVmaW5lZA=="]);
73+
sinon.stub(middleware, "touch");
74+
7275
const mw = middleware({ cacheDir: "." });
7376

7477
await mw(
@@ -83,6 +86,8 @@ describe("Middleware", function() {
8386
"./a1/b2/0123456789abcdef/dW5kZWZpbmVkOnVuZGVmaW5lZA=="
8487
)
8588
.and.returned(Buffer.from([]));
89+
90+
fs.readdirSync.restore();
8691
});
8792

8893
// it falls back to a default cache key
@@ -132,6 +137,11 @@ describe("Middleware", function() {
132137
this.nextSpy.resetHistory();
133138
this.makePathSpy.resetHistory();
134139
});
140+
141+
after(function() {
142+
path.join.restore();
143+
middleware.evictLeastRecentlyUsed.restore();
144+
});
135145
});
136146

137147
describe("asset cache name en/decoding", function() {
@@ -147,4 +157,80 @@ describe("Middleware", function() {
147157
).to.deep.equal(["image/png", "4096"]);
148158
});
149159
});
160+
161+
describe("evicting of least recently used files", function() {
162+
before(function() {
163+
sinon
164+
.stub(fs, "readdirSync")
165+
.withArgs("/tmp")
166+
.returns(["test1", "test2"])
167+
.withArgs("/tmp/test1")
168+
.returns(["foo"])
169+
.withArgs("/tmp/test2")
170+
.returns(["bar"]);
171+
sinon
172+
.stub(fs, "statSync")
173+
.withArgs("/tmp/test1")
174+
.returns({
175+
isDirectory() {
176+
return true;
177+
}
178+
})
179+
.withArgs("/tmp/test2")
180+
.returns({
181+
isDirectory() {
182+
return true;
183+
}
184+
})
185+
.withArgs("/tmp/test1")
186+
.returns({
187+
isDirectory() {
188+
return true;
189+
}
190+
})
191+
.withArgs("/tmp/test2")
192+
.returns({
193+
isDirectory() {
194+
return true;
195+
}
196+
})
197+
.withArgs("/tmp/test1/foo")
198+
.returns({
199+
isDirectory() {
200+
return false;
201+
},
202+
atime: 1000
203+
})
204+
.withArgs("/tmp/test2/bar")
205+
.returns({
206+
isDirectory() {
207+
return false;
208+
},
209+
atime: 2000
210+
});
211+
sinon
212+
.stub(path, "join")
213+
.withArgs("/tmp", "test1")
214+
.returns("/tmp/test1")
215+
.withArgs("/tmp/test1", "foo")
216+
.returns("/tmp/test1/foo")
217+
.withArgs("/tmp", "test2")
218+
.returns("/tmp/test2")
219+
.withArgs("/tmp/test2", "bar")
220+
.returns("/tmp/test2/bar");
221+
});
222+
223+
it("returns the least recently used file", function() {
224+
expect(middleware.findLeastRecentlyUsed("/tmp")).to.deep.equal({
225+
atime: 1000,
226+
path: "/tmp/test1/foo"
227+
});
228+
});
229+
230+
after(function() {
231+
fs.readdirSync.restore();
232+
fs.statSync.restore();
233+
path.join.restore();
234+
});
235+
});
150236
});

0 commit comments

Comments
 (0)