Skip to content

Commit

Permalink
Merge pull request #52 from ringo/features/static-caching
Browse files Browse the repository at this point in the history
Advances the config options for static middleware
  • Loading branch information
botic authored Oct 24, 2016
2 parents a72d7b6 + bf790c6 commit 845d78b
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 4 deletions.
41 changes: 38 additions & 3 deletions lib/middleware/static.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,21 @@
* * `options`: an object with fine-grained configuration options
* ** `servePrecompressed`: if true (default), static resources with a pre-compressed gzip equivalent will be
* served instead of the original file.
* ** `maxAge`: set the `Cache-Control` header in seconds, default is 0.
* ** `lastModified`: if true (default), set the `Last-Modified` header to the modification date of the resource.
* ** `setHeaders`: allows a user to specify a function which returns additional headers to be set for a given path.
* The given function produces an object containing the header names as keys and takes the path as
* argument. User-provided headers take precedence over all other headers.
* ** `dotfiles`: determines how should files starting with a dot character be treated.
* `allow` (default) serves dotfiles, `deny` respond with status 403, `ignore` respond with status 404.
*
* You can call `static()` multiple times to register multiple resources to be served.
*/

const RFC_822 = "EEE, dd MMM yyyy HH:mm:ss z";

var objects = require("ringo/utils/objects");
var dates = require("ringo/utils/dates");
var response = require("ringo/jsgi/response");
var {mimeType} = require("ringo/mime");

Expand All @@ -34,7 +44,11 @@ exports.middleware = function static(next, app) {

app.static = function(base, index, baseURI, options) {
var opts = objects.merge(options || {}, {
"servePrecompressed": true
"servePrecompressed": true,
"dotfiles": "allow",
"lastModified": true,
"maxAge": 0,
"setHeaders": null
});
var baseRepository;
if (typeof base === "string") {
Expand Down Expand Up @@ -63,18 +77,39 @@ exports.middleware = function static(next, app) {
if (path.length > 1) {
var resource = config.repository.getResource(path);
if (resource && resource.exists()) {
if (resource.getName().charAt(0) === ".") {
switch (config.options.dotfiles.toLowerCase()) {
case "deny": return response.text("403 Forbidden").setStatus(403);
case "ignore": return response.text("404 Not Found").setStatus(404);
}
}

let userHeaders = typeof config.options.setHeaders === "function" ? config.options.setHeaders() : {};

let defaultHeaders = {
"Cache-Control": "max-age=" + config.options.maxAge || 0
};

if (config.options.lastModified === true) {
defaultHeaders["Last-Modified"] = dates.format(new Date(resource.lastModified()), RFC_822, "en", "GMT");
}

// check if precompressed gzip resource is available and it's serving is enabled
let acceptEncoding = (request.headers["accept-encoding"] || "").toLowerCase();
if (acceptEncoding.indexOf("gzip") > -1 && config.options.servePrecompressed === true) {
let gzippedResource = config.repository.getResource(path + ".gz");
if (gzippedResource && gzippedResource.exists()) {
let jsgiResponse = response.static(gzippedResource, mimeType(path, "text/plain"));
jsgiResponse.headers["Content-Encoding"] = "gzip";
jsgiResponse.headers = objects.merge(userHeaders, jsgiResponse.headers, {
"Content-Encoding": "gzip"
}, defaultHeaders);
return jsgiResponse;
}
}

return response.static(resource, mimeType(path, "text/plain"));
let jsgiResponse = response.static(resource, mimeType(path, "text/plain"));
jsgiResponse.headers = objects.merge(userHeaders, jsgiResponse.headers, defaultHeaders);
return jsgiResponse;
}
}
}
Expand Down
1 change: 1 addition & 0 deletions test/middleware/fixtures/.DotfileExample
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is a dotfile
172 changes: 171 additions & 1 deletion test/middleware/static_test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const fs = require("fs");
const system = require("system");
const assert = require("assert");

Expand Down Expand Up @@ -58,10 +59,174 @@ exports.testStaticFile = function() {
});
assert.equal(response.headers["Content-Type"], "text/html");
assert.equal(bodyAsString(response.body, "utf-8"), "<!DOCTYPE html>");
assert.equal(response.headers["Cache-Control"], "max-age=0");
};

exports.testPrecompressedStaticFile = function() {
exports.testMaxAge = function() {
let app = new Application();

app.configure("static");
app.static(module.resolve("./fixtures"), "index.html", "/customStatic", {
maxAge: 1000
});

let response = app({
method: 'GET',
headers: {},
env: {},
pathInfo: '/customStatic/'
});
assert.equal(response.headers["Content-Type"], "text/html");
assert.equal(bodyAsString(response.body, "utf-8"), "<!DOCTYPE html>");
assert.equal(response.headers["Cache-Control"], "max-age=1000");

app = new Application();

app.configure("static");
app.static(module.resolve("./fixtures"), "index.html", "/customStatic");

response = app({
method: 'GET',
headers: {},
env: {},
pathInfo: '/customStatic/'
});
assert.equal(response.headers["Content-Type"], "text/html");
assert.equal(bodyAsString(response.body, "utf-8"), "<!DOCTYPE html>");
assert.equal(response.headers["Cache-Control"], "max-age=0");
};

exports.testLastModified = function() {
var app = new Application();
app.configure("static");
app.static(module.resolve("./fixtures"), "index.html", "/customStatic", {
lastModified: false
});

let response = app({
method: 'GET',
headers: {},
env: {},
pathInfo: '/customStatic/'
});
assert.equal(response.headers["Content-Type"], "text/html");
assert.equal(bodyAsString(response.body, "utf-8"), "<!DOCTYPE html>");
assert.isUndefined(response.headers["Last-Modified"]);

app = new Application();
app.configure("static");
app.static(module.resolve("./fixtures"), "index.html", "/customStatic");

response = app({
method: 'GET',
headers: {},
env: {},
pathInfo: '/customStatic/'
});
assert.equal(response.headers["Content-Type"], "text/html");
assert.equal(bodyAsString(response.body, "utf-8"), "<!DOCTYPE html>");
assert.isNotUndefined(response.headers["Last-Modified"]);

let lastModified = fs.lastModified(fs.join(module.resolve("./fixtures"), "index.html"));
let sdf = new java.text.SimpleDateFormat("E, d MMM yyyy HH:mm:ss z", java.util.Locale.US);
sdf.setTimeZone(java.util.TimeZone.getTimeZone("GMT"));
assert.equal(response.headers["Last-Modified"], sdf.format(lastModified));
};

exports.testSetHeaders = function() {
let app = new Application();

app.configure("static");
app.static(module.resolve("./fixtures"), "index.html", "/customStatic", {
setHeaders: function() {
return {
"x-foo": "bar",
"x-seestadt-cafe": "united in cycling",
"Cache-Control": "max-age=123456"
}
}
});

let response = app({
method: 'GET',
headers: {
"accept-encoding": "gzip"
},
env: {},
pathInfo: '/customStatic/'
});

assert.equal(response.status, 200);
assert.equal(response.headers["Content-Type"], "text/html");
assert.equal(response.headers["Content-Encoding"], "gzip");
assert.equal(response.headers["Cache-Control"], "max-age=123456");
assert.equal(response.headers["x-foo"], "bar");
assert.equal(response.headers["x-seestadt-cafe"], "united in cycling");
};

exports.testDotfiles = function() {
let app = new Application();

app.configure("static");
app.static(module.resolve("./fixtures"), "index.html", "/customStatic", {
"dotfiles": "allow"
});

let response = app({
method: 'GET',
headers: {
"accept-encoding": "gzip"
},
env: {},
pathInfo: '/customStatic/.DotfileExample'
});

assert.equal(response.status, 200);
assert.equal(response.headers["Content-Type"], "text/plain");

// DENY
app = new Application();

app.configure("static");
app.static(module.resolve("./fixtures"), "index.html", "/customStatic", {
"dotfiles": "deny"
});

response = app({
method: 'GET',
headers: {
"accept-encoding": "gzip"
},
env: {},
pathInfo: '/customStatic/.DotfileExample'
});

assert.equal(response.status, 403);
assert.equal(response.headers["content-type"], "text/plain; charset=utf-8");

// IGNORE
app = new Application();

app.configure("static");
app.static(module.resolve("./fixtures"), "index.html", "/customStatic", {
"dotfiles": "IgNoRe"
});

response = app({
method: 'GET',
headers: {
"accept-encoding": "gzip"
},
env: {},
pathInfo: '/customStatic/.DotfileExample'
});

assert.equal(response.status, 404);
assert.equal(response.headers["content-type"], "text/plain; charset=utf-8");
};

exports.testPrecompressedStaticFile = function() {
let app = new Application();

app.configure("static");
app.static(module.resolve("./fixtures"), "index.html", "/customStatic", {});
Expand All @@ -78,6 +243,7 @@ exports.testPrecompressedStaticFile = function() {
assert.equal(response.status, 200);
assert.equal(response.headers["Content-Type"], "text/html");
assert.equal(response.headers["Content-Encoding"], "gzip");
assert.equal(response.headers["Cache-Control"], "max-age=0");

response = app({
method: 'GET',
Expand All @@ -91,6 +257,7 @@ exports.testPrecompressedStaticFile = function() {
assert.equal(response.status, 200);
assert.equal(response.headers["Content-Type"], "text/html");
assert.equal(response.headers["Content-Encoding"], "gzip");
assert.equal(response.headers["Cache-Control"], "max-age=0");

const ba = new ByteArray([0x1F, 0x8B, 0x08, 0x08, 0x81, 0xF1, 0xD6, 0x56, 0x00, 0x03, 0x69, 0x6E, 0x64, 0x65, 0x78,
0x2E, 0x68, 0x74, 0x6D, 0x6C, 0x00, 0xB3, 0x51, 0x74, 0xF1, 0x77, 0x0E, 0x89, 0x0C, 0x70,
Expand All @@ -105,6 +272,7 @@ exports.testPrecompressedStaticFile = function() {
});
assert.equal(response.headers["Content-Type"], "text/html");
assert.equal(bodyAsString(response.body, "utf-8"), "<!DOCTYPE html><foo></foo>");
assert.equal(response.headers["Cache-Control"], "max-age=0");
};

exports.testDeactivatedPrecompression = function() {
Expand All @@ -123,6 +291,7 @@ exports.testDeactivatedPrecompression = function() {
});
assert.equal(response.headers["Content-Type"], "text/html");
assert.equal(bodyAsString(response.body, "utf-8"), "<!DOCTYPE html>");
assert.equal(response.headers["Cache-Control"], "max-age=0");

response = app({
method: 'GET',
Expand All @@ -132,6 +301,7 @@ exports.testDeactivatedPrecompression = function() {
});
assert.equal(response.headers["Content-Type"], "text/html");
assert.equal(bodyAsString(response.body, "utf-8"), "<!DOCTYPE html><foo></foo>");
assert.equal(response.headers["Cache-Control"], "max-age=0");
};

if (require.main === module) {
Expand Down

0 comments on commit 845d78b

Please sign in to comment.