Skip to content

Commit

Permalink
Merge pull request jamhall#1 from DavidBeale/static-hosting
Browse files Browse the repository at this point in the history
Add Static Web Hosting functionality
  • Loading branch information
jamhall committed May 6, 2015
2 parents 75939b7 + 600afc0 commit 8f745fd
Show file tree
Hide file tree
Showing 7 changed files with 310 additions and 26 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ Please see [Fake S3s wiki page](https://github.com/jubos/fake-s3/wiki/Supported-

Please test, if you encounter any problems please do not hesitate to open an issue :)

## Static Website Hosting

If you specify an *indexDocument* then ```get``` requests will serve the *indexDocument* if it is found, simulating the static website mode of AWS S3. An *errorDocument* can also be set, to serve a custom 404 page.

### Hostname Resolution

By default a bucket name needs to be given. So for a bucket called ```mysite.local```, with an indexDocument of ```index.html```. Visiting ```http://localhost:4568/mysite.local/``` in your browser will display the ```index.html``` file uploaded to the bucket.

However you can also setup a local hostname in your /etc/hosts file pointing at 127.0.0.1
```
localhost 127.0.0.1
mysite.local 127.0.0.1
```
Now you can access the served content at ```http://mysite.local:4568/```

## Tests

> When running the tests with node v0.10.0 the following [error](https://github.com/mochajs/mocha/issues/777) is encountered. This is resolved by running the tests with v0.11.*. I recommend using [NVM](https://github.com/creationix/nvm) to manage your node versions.
Expand Down
4 changes: 4 additions & 0 deletions bin/s3rver.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ program.version(version, '--version');
program.option('-h, --hostname [value]', 'Set the host name or ip for the server', 'localhost')
.option('-p, --port <n>', 'Set the port of the http server', 4568)
.option('-s, --silent', 'Suppress log messages', false)
.option('-i, --indexDocumement', 'Index Document for Static Web Hosting', '')
.option('-e, --errorDocument', 'Custom Error Document for Static Web Hosting', '')
.option('-d, --directory [path]', 'Data directory')
.parse(process.argv);

Expand All @@ -34,6 +36,8 @@ s3rver.setHostname(program.hostname)
.setPort(program.port)
.setDirectory(program.directory)
.setSilent(program.silent)
.setIndexDocument(program.indexDocumement)
.setErrorDocument(program.errorDocument)
.run(function (err, host, port) {
console.log('now listening on host %s and port %d', host, port);
});
18 changes: 15 additions & 3 deletions lib/app.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
'use strict';
var app = function (hostname, port, directory, silent) {
var app = function (hostname, port, directory, silent, indexDocument, errorDocument) {
var express = require('express'),
app = express(),
logger = require('./logger')(silent),
Controllers = require('./controllers'),
controllers = new Controllers(directory, logger),
concat = require('concat-stream');
controllers = new Controllers(directory, logger, indexDocument, errorDocument),
concat = require('concat-stream'),
path = require('path');

/**
* Log all requests
Expand All @@ -24,6 +25,17 @@ var app = function (hostname, port, directory, silent) {
}));
});

app.use(function(req, res, next) {
var host = req.headers.host.split(':')[0];

if (indexDocument && host !== 'localhost' && host !== '127.0.0.1')
{
req.url = path.join('/', host, req.url);
}

next();
});

app.disable('x-powered-by');

/**
Expand Down
139 changes: 117 additions & 22 deletions lib/controllers.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,79 @@
'use strict';
module.exports = function (rootDirectory, logger) {

var FileStore = require('./file-store'),
fileStore = new FileStore(rootDirectory),
templateBuilder = require('./xml-template-builder');
templateBuilder = require('./xml-template-builder'),
path = require('path');

module.exports = function (rootDirectory, logger, indexDocument, errorDocument) {
var fileStore = new FileStore(rootDirectory);

var buildXmlResponse = function (res, status, template) {
res.header('Content-Type', 'application/xml');
res.status(status);
return res.send(template);
};

var buildResponse = function (req, res, status, object, data) {
res.header('Etag', object.md5);
res.header('Last-Modified', new Date(object.modifiedDate).toUTCString());
res.header('Content-Type', object.contentType);
res.header('Content-Length', object.size);
if (object.customMetaData.length > 0) {
object.customMetaData.forEach(function (metaData) {
res.header(metaData.key, metaData.value);
});
}
res.status(status);
if (req.method === 'HEAD') {
return res.end();
}
return res.end(data);
};

var errorResponse = function(req, res, keyName) {
logger.error('Object "%s" in bucket "%s" does not exist', keyName, req.bucket.name);

if (indexDocument)
{
if (errorDocument)
{
fileStore.getObject(req.bucket, errorDocument, function (err, object, data) {
if (err)
{
console.error('Custom Error Document not found: ' + errorDocument);
return notFoundResponse(req, res);
}
else
{
return buildResponse(req, res, 404, object, data);
}
});
}
else
{
return notFoundResponse(req, res);
}
}
else
{
var template = templateBuilder.buildKeyNotFound(keyName);
return buildXmlResponse(res, 404, template);
}
};


var notFoundResponse = function(req, res)
{
var ErrorDoc = '<!DOCTYPE html>\n<html><head><title>404 - Resource Not Found</title></head><body><h1>404 - Resource Not Found</h1></body></html>';

return buildResponse(req, res, 404, {
modifiedDate: new Date(),
contentType: 'text/html',
customMetaData: [],
size: ErrorDoc.length
}, ErrorDoc);
};

/**
* The following methods correspond the S3 api. For more information visit:
* http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html
Expand Down Expand Up @@ -37,6 +101,7 @@ module.exports = function (rootDirectory, logger) {
return buildXmlResponse(res, 200, template);
},
getBucket: function (req, res) {

var options = {
marker: req.query.marker || null,
prefix: req.query.prefix || null,
Expand All @@ -46,8 +111,34 @@ module.exports = function (rootDirectory, logger) {
logger.info('Fetched bucket "%s" with options %s', req.bucket.name, options);
fileStore.getObjects(req.bucket, options, function (err, results) {
logger.info('Found %d objects for bucket "%s"', results.length, req.bucket.name);
var template = templateBuilder.buildBucketQuery(options, results);
return buildXmlResponse(res, 200, template);

var match = false;
if (indexDocument)
{
results.forEach(function(result){
if (result.key === indexDocument)
{
match = true;
fileStore.getObject(req.bucket, result.key, function (err, object, data) {
if (err)
{
return errorResponse(req, res, object.key);
}
else
{
logger.info('Serving Page: %s', object.key);
return buildResponse(req, res, 200, object, data);
}
});
}
});
}

if (!match)
{
var template = templateBuilder.buildBucketQuery(options, results);
return buildXmlResponse(res, 200, template);
}
});
},
putBucket: function (req, res) {
Expand Down Expand Up @@ -107,9 +198,25 @@ module.exports = function (rootDirectory, logger) {
}
fileStore.getObject(req.bucket, keyName, function (err, object, data) {
if (err) {
var template = templateBuilder.buildKeyNotFound(keyName);
logger.error('Object "%s" in bucket "%s" does not exist', keyName, req.bucket.name);
return buildXmlResponse(res, 404, template);

if (indexDocument)
{
keyName = path.join(keyName, indexDocument);
return fileStore.getObject(req.bucket, keyName, function (err, object, data) {
if (err)
{
return errorResponse(req, res, keyName);
}
else
{
return buildResponse(req, res, 200, object, data);
}
});
}
else
{
return errorResponse(req, res, keyName);
}
}

var noneMatch = req.headers['if-none-match'];
Expand All @@ -124,20 +231,8 @@ module.exports = function (rootDirectory, logger) {
return res.status(304).end();
}
}
res.header('Etag', object.md5);
res.header('Last-Modified', new Date(object.modifiedDate).toUTCString());
res.header('Content-Type', object.contentType);
res.header('Content-Length', object.size);
if (object.customMetaData.length > 0) {
object.customMetaData.forEach(function (metaData) {
res.header(metaData.key, metaData.value);
});
}
res.status(200);
if (req.method === 'HEAD') {
return res.end();
}
return res.end(data);

return buildResponse(req, res, 200, object, data);
});
},
putObject: function (req, res) {
Expand Down
14 changes: 13 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ var S3rver = function () {
this.port = 4578;
this.hostname = 'localhost';
this.silent = false;
this.indexDocument = '';
this.errorDocument = '';
};

S3rver.prototype.setPort = function (port) {
Expand All @@ -26,8 +28,18 @@ S3rver.prototype.setSilent = function (silent) {
return this;
};

S3rver.prototype.setIndexDocument = function (indexDocument) {
this.indexDocument = indexDocument;
return this;
};

S3rver.prototype.setErrorDocument = function (errorDocument) {
this.errorDocument = errorDocument;
return this;
};

S3rver.prototype.run = function (done) {
var app = new App(this.hostname, this.port, this.directory, this.silent);
var app = new App(this.hostname, this.port, this.directory, this.silent, this.indexDocument, this.errorDocument);
return app.serve(done);

};
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"fs-extra": "^0.14.0",
"mocha": "^2.1.0",
"moment": "^2.8.4",
"request": "^2.55.0",
"should": "^4.4.2",
"xml2js": "^0.4.4"
},
Expand Down
Loading

0 comments on commit 8f745fd

Please sign in to comment.