Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,16 @@ The exact JSON document provided to `PUT /content` will be returned.

An HTTP status of 404 will be returned if the content ID isn't recognized.

### `POST /asset`
### `POST /asset[?named=true]`

Fingerprint and publish one or more static assets to a CDN-enabled Cloud Files container. Return the full URLs to the published assets.

*Request*

The request payload must be a `multipart/form-data` file upload containing the assets to upload. The content type of each file must be set appropriately.

If the query parameter `named=true` is provided, each asset's CDN URI will also be persisted in the *layout asset map* with a key derived from its form name and inserted in the `assets` object of every outgoing metadata envelope.

*Response*

```json
Expand Down
2 changes: 1 addition & 1 deletion app.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ var

server.name = config.info.name;

connection.setup(config, function (err) {
connection.setup(function (err) {
if (err) {
throw err;
}
Expand Down
5 changes: 5 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
---
mongo:
image: mongo:2.6
content:
build: .
ports:
- "9000:8080"
links:
- "mongo:mongo"
environment:
RACKSPACE_USERNAME:
RACKSPACE_APIKEY:
RACKSPACE_REGION: DFW
CONTENT_CONTAINER: deconst-dev-content
ASSET_CONTAINER: deconst-dev-assets
MONGODB_URL: mongodb://mongo:27017/content
CONTENT_LOG_LEVEL: debug
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"license": "MIT",
"dependencies": {
"async": "0.9.0",
"mongodb": "2.0.27",
"pkgcloud": "1.1.0",
"restify": "3.0.0",
"winston": "0.9.0"
Expand Down
4 changes: 4 additions & 0 deletions script/mongo-cli
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash

exec docker-compose run --rm mongo \
sh -c 'exec mongo "${MONGO_PORT_27017_TCP_ADDR}:${MONGO_PORT_27017_TCP_PORT}/content"'
61 changes: 41 additions & 20 deletions src/assets.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ var log = logging.getLogger(config.content_log_level());
* @description Calculate a checksum of an uploaded file's contents to generate
* the fingerprinted asset name.
*/
function fingerprint(asset, callback) {
function fingerprint_asset(asset, callback) {
var
sha256sum = crypto.createHash('sha256'),
asset_file = fs.createReadStream(asset.path),
Expand All @@ -39,6 +39,7 @@ function fingerprint(asset, callback) {
log.debug("Fingerprinted asset [" + asset.name + "] as [" + fingerprinted + "].");

callback(null, {
key: asset.key,
original: asset.name,
chunks: chunks,
filename: fingerprinted,
Expand All @@ -50,7 +51,7 @@ function fingerprint(asset, callback) {
/**
* @description Upload an asset's contents to the asset container.
*/
function publish(asset, callback) {
function publish_asset(asset, callback) {
var up = connection.client.upload({
container: config.asset_container(),
remote: asset.filename,
Expand All @@ -63,6 +64,8 @@ function publish(asset, callback) {
up.on('finish', function () {
log.debug("Successfully uploaded asset [" + asset.filename + "].");

var base_uri = connection.asset_container.cdnSslUri;
asset.public_url = base_uri + '/' + encodeURIComponent(asset.filename);
callback(null, asset);
});

Expand All @@ -74,15 +77,39 @@ function publish(asset, callback) {
}

/**
* @description Process a single asset.
* @description Give this asset a name. The final name and CDL URI of this
* asset will be included in all outgoing metadata envelopes, for use by
* layouts.
*/
function handle_asset(asset, callback) {
log.debug("Processing uploaded asset [" + asset.name + "].");
function name_asset(asset, callback) {
log.debug("Naming asset [" + asset.original + "] as [" + asset.key + "].");

connection.db.collection("layout_assets").updateOne(
{ key: asset.key },
{ $set: { key: asset.key, public_url: asset.public_url } },
{ upsert: true },
function (err) { callback(err, asset); }
);
}

/**
* @description Create and return a function that processes a single asset.
*/
function make_asset_handler(should_name) {
return function(asset, callback) {
log.debug("Processing uploaded asset [" + asset.name + "].");

var steps = [
async.apply(fingerprint_asset, asset),
publish_asset
];

async.waterfall([
async.apply(fingerprint, asset),
publish
], callback);
if (should_name) {
steps.push(name_asset);
}

async.waterfall(steps, callback);
};
}

/**
Expand All @@ -92,16 +119,12 @@ function handle_asset(asset, callback) {
*/
exports.accept = function (req, res, next) {
var asset_data = Object.getOwnPropertyNames(req.files).map(function (key) {
return req.files[key];
var asset = req.files[key];
asset.key = key;
return asset;
});

var base_uri = connection.asset_container.cdnSslUri;

if (! base_uri) {
log.error("Asset container does not have a CDN URI. Is it CDN-enabled?");
}

async.map(asset_data, handle_asset, function (err, results) {
async.map(asset_data, make_asset_handler(req.query.named), function (err, results) {
if (err) {
log.error("Unable to process an asset.", err);

Expand All @@ -114,9 +137,7 @@ exports.accept = function (req, res, next) {

var summary = {};
results.forEach(function (result) {
var public_url = base_uri + '/' + encodeURIComponent(result.filename);

summary[result.original] = public_url;
summary[result.original] = result.public_url;
});
log.debug("All assets have been processed succesfully.", summary);

Expand Down
1 change: 1 addition & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ var configuration = {
rackspace_region: null,
content_container: null,
asset_container: null,
mongodb_url: null,
content_log_level: "info"
};

Expand Down
22 changes: 21 additions & 1 deletion src/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
var
async = require('async'),
pkgcloud = require('pkgcloud'),
mongo = require('mongodb'),
config = require('./config'),
logging = require('./logging');

Expand Down Expand Up @@ -55,7 +56,25 @@ function refresh(client, container_name, logical_name, callback) {
});
}

exports.setup = function (config, callback) {
/**
* @description Authenticate to MongoDB and export the active MongoDB connection as "db".
*/
function mongo_auth(callback) {
mongo.MongoClient.connect(config.mongodb_url(), function (err, db) {
if (err) {
callback(err);
return;
}

log.debug("Connected to MongoDB database at [" + config.mongodb_url() + "].");

exports.db = db;

callback(null);
});
}

exports.setup = function (callback) {
var client = pkgcloud.providers.rackspace.storage.createClient({
username: config.rackspace_username(),
apiKey: config.rackspace_apikey(),
Expand All @@ -66,5 +85,6 @@ exports.setup = function (config, callback) {
async.parallel([
make_container_creator(client, config.content_container(), "content_container", false),
make_container_creator(client, config.asset_container(), "asset_container", true),
mongo_auth
], callback);
};
80 changes: 71 additions & 9 deletions src/content.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,91 @@
// Store, retrieve, and delete metadata envelopes.

var
async = require('async'),
config = require('./config'),
connection = require('./connection'),
logging = require('./logging');

var log = logging.getLogger(config.content_log_level());

/**
* @description Download the raw metadata envelope from Cloud Files.
*/
function download_content(content_id, callback) {
var source = connection.client.download({
container: config.content_container(),
remote: encodeURIComponent(content_id)
});
var chunks = [];

source.on('error', function (err) {
callback(err);
});

source.on('data', function (chunk) {
chunks.push(chunk);
});

source.on('end', function () {
var
complete = Buffer.concat(chunks),
envelope = JSON.parse(complete);

callback(null, envelope);
});
}

/**
* @description Inject asset variables included from the /assets endpoint into
* an outgoing metadata envelope.
*/
function inject_asset_vars(envelope, callback) {
log.debug("Collecting asset variables to inject into the envelope.");

connection.db.collection("layout_assets").find().toArray(function (err, asset_vars) {
if (err) {
callback(err);
return;
}

log.debug("Injecting " + asset_vars.length + " variables into the envelope.");

var assets = {};

asset_vars.forEach(function (asset_var) {
assets[asset_var.key] = asset_var.public_url;
});

envelope.assets = assets;

callback(null, envelope);
});
}

/**
* @description Retrieve content from the store by content ID.
*/
exports.retrieve = function (req, res, next) {
log.debug("Requesting content ID: [" + req.params.id + "]");

var source = connection.client.download({
container: config.content_container(),
remote: encodeURIComponent(req.params.id)
});
async.waterfall([
async.apply(download_content, req.params.id),
inject_asset_vars
], function (err, envelope) {
if (err) {
log.error("Failed to retrieve a metadata envelope", err);

// This directly sends the response to the caller, long term we'll
// probably not want to do this, but it allows the prototype to get functional
//
source.pipe(res);
res.status(err.statusCode || 500);
res.send();
next();

next();
return;
}

res.status(200);
res.json(envelope);
next();
});
};

/**
Expand Down
2 changes: 1 addition & 1 deletion src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ exports.loadRoutes = function (server) {
server.put('/content/:id', content.store);
server.del('/content/:id', content.delete);

server.post('/assets', restify.bodyParser(), assets.accept);
server.post('/assets', restify.bodyParser(), restify.queryParser(), assets.accept);
};