cloudfiles
Version:
A client implementation for Rackspace CloudFiles in node.js
434 lines (375 loc) • 13.4 kB
JavaScript
/*
* core.js: Core functions for accessing Rackspace CloudFiles
*
* (C) 2010 Nodejitsu Inc.
* MIT LICENSE
*
*/
var http = require('http'),
fs = require('fs'),
path = require('path'),
url = require('url'),
async = require('async'),
mime = require('mime'),
request = require('request'),
cloudfiles = require('../cloudfiles'),
config = require('./config'),
common = require('./common');
//
// ### function createClient (options)
// #### @options {Object} Options for this instance.
// Creates a new instance of a Loggly client.
//
exports.createClient = function (options) {
return new Cloudfiles(config.createConfig(options));
};
//
// ### function Cloudfiles (config)
// #### @config {loggly.Config} Configuration object for this instance.
// Constructor function for the `Cloudfiles` object responsible for exposing
// the core `node-cloudfiles` API methods.
//
var Cloudfiles = exports.Cloudfiles = function (config) {
this.config = config;
this.authorized = false;
//
// Create the cache path for this instance immediately.
//
common.statOrMkdirp(this.config.cache.path)
};
//
// ### function setAuth (callback)
// #### @callback {function} Continuation to respond to when complete.
// Authenticates node-cloudfiles with the options specified
// in the Config object for this instance
//
Cloudfiles.prototype.setAuth = function (callback) {
var authOptions = {
uri: 'https://' + this.config.auth.host + '/v1.0',
headers: {
'HOST': this.config.auth.host,
'X-AUTH-USER': this.config.auth.username,
'X-AUTH-KEY': this.config.auth.apiKey
}
};
var self = this;
request(authOptions, function (err, res, body) {
if (err) {
return callback(err);
}
var statusCode = res.statusCode.toString();
if (Object.keys(common.failCodes).indexOf(statusCode) !== -1) {
err = new Error('Rackspace Error (' + statusCode + '): ' + common.failCodes[statusCode]);
return callback(err, res);
}
self.authorized = true;
self.config.serverUrl = res.headers['x-server-management-url'];
self.config.setStorageUrl(res.headers['x-storage-url']);
self.config.cdnUrl = res.headers['x-cdn-management-url'];
self.config.authToken = res.headers['x-auth-token'];
self.config.storageToken = res.headers['x-storage-token']
callback(null, res, self.config);
});
};
//
// ### function getContainers ([cdn,] callback)
// #### @cdn {boolean} Value indicating if CDN containers should be returned
// #### @callback {function} Continuation to respond to when complete.
// Gets all Rackspace Cloudfiles containers for this instance.
//
Cloudfiles.prototype.getContainers = function () {
var self = this,
args = Array.prototype.slice.call(arguments),
callback = (typeof(args[args.length - 1]) === 'function') && args.pop(),
isCdn = args.length > 0 && (typeof(args[args.length - 1]) === 'boolean') && args.pop(),
url = isCdn ? this.cdnUrl(true) : this.storageUrl(true);
common.rackspace(url, this, callback, function (body) {
var results = [],
containers = JSON.parse(body);
containers.forEach(function (container) {
if (isCdn) {
//
// The cdn properties are normaly set in response headers
// when requesting single cdn containers
//
container.cdnEnabled = container.cdn_enabled == "true";
container.logRetention = container.log_retention == "true";
container.cdnUri = container.cdn_uri;
container.cdnSslUri = container.cdn_ssl_uri;
}
results.push(new (cloudfiles.Container)(self, container));
});
callback(null, results);
});
};
//
// ### function getContainer (containerName, [cdn,] callback)
// #### @containerName {string} Name of the container to return
// #### @cdn {boolean} Value indicating if this is a CDN container.
// #### @callback {function} Continuation to respond to when complete.
// Responds with the Rackspace Cloudfiles container for the specified
// `containerName`.
//
Cloudfiles.prototype.getContainer = function () {
var self = this,
args = Array.prototype.slice.call(arguments),
callback = (typeof(args[args.length - 1]) === 'function') && args.pop(),
isCdn = args.length > 0 && (typeof(args[args.length - 1]) === 'boolean') && args.pop(),
containerName = args.pop(),
containerOptions;
containerOptions = {
method: 'HEAD',
uri: isCdn ? this.cdnUrl(containerName) : this.storageUrl(containerName),
cdn: isCdn,
client: this
}
common.rackspace(containerOptions, callback, function (body, res) {
var container = {
name: containerName,
count: new Number(res.headers['x-container-object-count']),
bytes: new Number(res.headers['x-container-bytes-used'])
};
if (isCdn) {
container.cdnUri = res.headers['x-cdn-uri'];
container.cdnSslUri = res.headers['x-cdn-ssl-uri'];
container.cdnEnabled = res.headers['x-cdn-enabled'].toLowerCase() == "true";
container.ttl = parseInt(res.headers['x-ttl']);
container.logRetention = res.headers['x-log-retention'].toLowerCase() == "true";
delete container.count;
delete container.bytes;
}
callback(null, new (cloudfiles.Container)(self, container));
});
};
//
// ### function createContainer (container, callback)
// #### @container {string|Container} Container to create in Rackspace Cloudfiles.
// #### @callback {function} Continuation to respond to when complete.
// Creates the specified `container` in the Rackspace Cloudfiles associated
// with this instance.
//
Cloudfiles.prototype.createContainer = function (container, callback) {
var self = this,
containerName = container instanceof cloudfiles.Container ? container.name : container;
common.rackspace('PUT', this.storageUrl(containerName), this, callback, function (body, res) {
if (typeof container.cdnEnabled !== 'undefined' && container.cdnEnabled) {
container.ttl = container.ttl || self.config.cdn.ttl;
container.logRetention = container.logRetention || self.config.cdn.logRetention;
var cdnOptions = {
uri: self.cdnUrl(containerName),
method: 'PUT',
client: self,
headers: {
'X-TTL': container.ttl,
'X-LOG-RETENTION': container.logRetention
}
};
common.rackspace(cdnOptions, callback, function (body, res) {
container.cdnUri = res.headers['x-cdn-uri'];
container.cdnSslUri = res.headers['x-cdn-ssl-uri'];
callback(null, new (cloudfiles.Container)(self, container));
});
}
else {
callback(null, new (cloudfiles.Container)(self, container));
}
});
};
//
// ### function destroyContainer (container, callback)
// #### @container {string} Name of the container to destroy
// #### @callback {function} Continuation to respond to when complete.
// Destroys the specified `container` and all files in it.
//
Cloudfiles.prototype.destroyContainer = function (container, callback) {
var self = this;
this.getFiles(container, function (err, files) {
if (err) {
return callback(err);
}
function deleteContainer (err) {
if (err) {
return callback(err);
}
common.rackspace('DELETE', self.storageUrl(container), self, callback, function (body, res) {
callback(null, true);
});
}
function destroyFile (file, next) {
file.destroy(next);
}
if (files.length === 0) {
return deleteContainer();
}
async.forEach(files, destroyFile, deleteContainer);
});
};
Cloudfiles.prototype.getFiles = function (container, download, callback) {
var self = this;
//
// Download is optional argument
// And can be only: true, false, [array of files]
//
// Also download can be omitted: (...).getFiles(container, callback);
// In this case second argument will be a function
//
if (typeof download === 'function' && !(download instanceof RegExp)) {
callback = download;
download = false;
}
common.rackspace(this.storageUrl(container, true), this, callback, function (body) {
var files = JSON.parse(body);
// If download == false or wasn't defined
if (!download) {
var results = files.map(function (file) {
file.container = container;
return new (cloudfiles.StorageObject)(self, file);
});
callback(null, results);
return;
}
var batch;
if (download instanceof RegExp || download == true) {
// If download == true
// Download all files
if (download !== true) {
files = files.filter(function (file) {
return download.test(file.name);
});
}
// Create a batch
batch = files.map(function (file) {
return function (callback) {
self.getFile(container, file.name, callback);
}
});
}
else if (Array.isArray(download)) {
// Go through all files that we've asked to download
batch = download.map(function (file) {
var exists = files.some(function (item) {
return item.name == file
});
// If file exists - get it
// If not report about error
return exists ?
function (callback) {
self.getFile(container, file, callback);
} :
function (callback) {
callback(Error('File : ' + file + ' doesn\'t exists'));
};
});
}
else {
callback(Error('"download" argument can be only boolean, array or regexp'));
}
// Run batch
common.runBatch(batch, callback);
});
};
Cloudfiles.prototype.getFile = function (container, filename, callback) {
var self = this,
containerPath = path.join(this.config.cache.path, container),
cacheFile = path.join(containerPath, filename),
options;
common.statOrMkdirp(containerPath);
var lstream = fs.createWriteStream(cacheFile),
rstream,
options;
options = {
method: 'GET',
client: self,
uri: self.storageUrl(container, filename),
download: lstream
};
rstream = common.rackspace(options, callback, function (body, res) {
var file = {
local: cacheFile,
container: container,
name: filename,
bytes: res.headers['content-length'],
etag: res.headers['etag'],
last_modified: res.headers['last-modified'],
content_type: res.headers['content-type']
};
callback(null, new (cloudfiles.StorageObject)(self, file));
});
};
//
// options
// remote
// local
// stream
// mime
//
Cloudfiles.prototype.addFile = function (container, options, callback) {
if (typeof options === 'function' && !callback) {
callback = options;
options = {};
}
if (!options.remote) {
return callback(new Error('.remote is required to addFile'));
}
var lstream,
addOptions,
size;
if (options.local) {
lstream = fs.createReadStream(options.local, options.fs);
options.headers = options.headers || {};
options.headers['content-length'] = fs.statSync(options.local).size;
}
else if (options.stream) {
lstream = options.stream;
}
if (!lstream) {
return callback(new Error('.local or .stream is required to addFile.'));
}
addOptions = {
method: 'PUT',
client: this,
upload: lstream,
uri: this.storageUrl(container, options.remote),
headers: options.headers || {}
};
if (options.headers && !options.headers['content-type']) {
options.headers['content-type'] = mime.lookup(options.remote);
}
return common.rackspace(addOptions, callback, function (body, res) {
callback(null, true);
});
};
//
// ### function destroyFile (container, file, callback)
// #### @container {string} Name of the container to destroy the file in
// #### @file {string} Name of the file to destroy.
// #### @callback {function} Continuation to respond to when complete.
// Destroys the `file` in the specified `container`.
//
Cloudfiles.prototype.destroyFile = function (container, file, callback) {
common.rackspace('DELETE', this.storageUrl(container, file), this, callback, function (body, res) {
callback(null, true);
});
};
//
// ### function storageUrl (arguments)
// #### @arguments {Array} Lists of arguments to convert into a storage url.
// Helper method that concats the string params into a url to request against
// the authenticated node-cloudfiles storageUrl.
//
Cloudfiles.prototype.storageUrl = function () {
var args = Array.prototype.slice.call(arguments),
json = (typeof(args[args.length - 1]) === 'boolean') && args.pop();
return [this.config.storageUrl].concat(args).join('/') + (json ? '?format=json' : '');
};
//
// ### function cdnUrl (arguments)
// #### @arguments {Array} Lists of arguments to convert into a cdn url.
// Helper method that concats the string params into a url
// to request against the authenticated node-cloudfiles cdnUrl.
//
Cloudfiles.prototype.cdnUrl = function () {
var args = Array.prototype.slice.call(arguments),
json = (typeof(args[args.length - 1]) === 'boolean') && args.pop();
return [this.config.cdnUrl].concat(args).join('/') + (json ? '?format=json' : '');
};