├── .gitignore ├── CHANGELOG.md ├── lib ├── cloudfiles.js └── cloudfiles │ ├── config.js │ ├── container.js │ ├── storage-object.js │ ├── common.js │ └── core.js ├── package.json ├── test ├── fixtures │ ├── fillerama.txt │ └── fillerama2.txt ├── container-destroy-test.js ├── authentication-test.js ├── servicenet-test.js ├── storage-object-large-test.js ├── storage-object-metadata-test.js ├── helpers.js ├── storage-object-test.js └── container-test.js ├── LICENSE ├── vendor └── mkdirp.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .cache/ 3 | .cache/* 4 | test/fixtures/test-config.json 5 | test/fixtures/fillerama3.txt 6 | test/fixtures/bigfile.raw 7 | node_modules 8 | npm-debug.log -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## ChangeLog for: node-cloudfiles 2 | 3 | ### Version 0.2.1 - 01/19/2011 4 | - Fix: Update uri when authenticating first 5 | 6 | ### Version 0.2.0 - 12/25/2010 7 | - Improved test coverage 8 | - Pushed test config out of lib/ to test/data/test-config.json 9 | 10 | #### Breaking Changes 11 | - cloudfiles no longer exposes methods on the module itself. Replace cloudfiles.* with client.* where var client = cloudfiles.createClient(config). -------------------------------------------------------------------------------- /lib/cloudfiles.js: -------------------------------------------------------------------------------- 1 | /* 2 | * cloudfiles.js: Wrapper for node-cloudfiles object 3 | * 4 | * (C) 2010 Nodejitsu Inc. 5 | * MIT LICENSE 6 | * 7 | */ 8 | 9 | var cloudfiles = exports; 10 | 11 | // 12 | // Load package information using `pkginfo`. 13 | // 14 | require('pkginfo')(module, 'version'); 15 | 16 | // 17 | // Resources 18 | // 19 | cloudfiles.Config = require('./cloudfiles/config').Config; 20 | cloudfiles.Cloudfiles = require('./cloudfiles/core').Cloudfiles; 21 | cloudfiles.Container = require('./cloudfiles/container').Container; 22 | cloudfiles.StorageObject = require('./cloudfiles/storage-object').StorageObject; 23 | 24 | // 25 | // Core methods 26 | // 27 | cloudfiles.createClient = require('./cloudfiles/core').createClient; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudfiles", 3 | "description": "A client implementation for Rackspace CloudFiles in node.js", 4 | "version": "0.3.4", 5 | "author": "Nodejitsu Inc. ", 6 | "maintainers": [ 7 | "indexzero ", 8 | "bmeck ", 9 | "indutny " 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "http://github.com/nodejitsu/node-cloudfiles.git" 14 | }, 15 | "keywords": [ 16 | "cloud computing", 17 | "api", 18 | "rackspace cloud", 19 | "cloudfiles" 20 | ], 21 | "dependencies": { 22 | "async": "0.1.x", 23 | "mime": "1.2.x", 24 | "pkginfo": "0.2.x", 25 | "request": "2.x.x" 26 | }, 27 | "devDependencies": { 28 | "vows": "0.6.x" 29 | }, 30 | "main": "./lib/cloudfiles", 31 | "scripts": { 32 | "test": "vows test/*-test.js --spec" 33 | }, 34 | "engines": { 35 | "node": ">= 0.4.0" 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /test/fixtures/fillerama.txt: -------------------------------------------------------------------------------- 1 | Yes, I saw. You were doing well, until everyone died. You seem malnourished. Are you suffering from intestinal parasites? We're rescuing ya. Now what? Say what? Yes. You gave me a dollar and some candy. 2 | 3 | No, I'm Santa Claus! Moving along… Oh, I always feared he might run off like this. Why, why, why didn't I break his legs? Robot 1-X, save my friends! And Zoidberg! Are you crazy? I can't swallow that. For the last time, I don't like lilacs! Your 'first' wife was the one who liked lilacs! 4 | 5 | You can see how I lived before I met you. Then we'll go with that data file! Now, now. Perfectly symmetrical violence never solved anything. 6 | 7 | Throw her in the brig. Oh, I don't have time for this. I have to go and buy a single piece of fruit with a coupon and then return it, making people wait behind me while I complain. Say it in Russian! Oh no! The professor will hit me! But if Zoidberg 'fixes' it... then perhaps gifts! I had more, but you go ahead. Ow, my spirit! 8 | 9 | You're going back for the Countess, aren't you? You've killed me! Oh, you've killed me! No. We're on the top. Bite my shiny metal ass. -------------------------------------------------------------------------------- /test/fixtures/fillerama2.txt: -------------------------------------------------------------------------------- 1 | Yes, I saw. You were doing well, until everyone died. You seem malnourished. Are you suffering from intestinal parasites? We're rescuing ya. Now what? Say what? Yes. You gave me a dollar and some candy. 2 | 3 | No, I'm Santa Claus! Moving along… Oh, I always feared he might run off like this. Why, why, why didn't I break his legs? Robot 1-X, save my friends! And Zoidberg! Are you crazy? I can't swallow that. For the last time, I don't like lilacs! Your 'first' wife was the one who liked lilacs! 4 | 5 | You can see how I lived before I met you. Then we'll go with that data file! Now, now. Perfectly symmetrical violence never solved anything. 6 | 7 | Throw her in the brig. Oh, I don't have time for this. I have to go and buy a single piece of fruit with a coupon and then return it, making people wait behind me while I complain. Say it in Russian! Oh no! The professor will hit me! But if Zoidberg 'fixes' it... then perhaps gifts! I had more, but you go ahead. Ow, my spirit! 8 | 9 | You're going back for the Countess, aren't you? You've killed me! Oh, you've killed me! No. We're on the top. Bite my shiny metal ass. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | node-cloudfiles 2 | 3 | Copyright (c) 2010 Nodejitsu Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /test/container-destroy-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * container-destroy-test.js: Tests for destroying Rackspace Cloudfiles containers 3 | * 4 | * (C) 2010 Nodejitsu Inc. 5 | * MIT LICENSE 6 | * 7 | */ 8 | 9 | var path = require('path'), 10 | vows = require('vows'), 11 | assert = require('assert'), 12 | cloudfiles = require('../lib/cloudfiles'), 13 | helpers = require('./helpers'); 14 | 15 | var client = helpers.createClient(); 16 | 17 | vows.describe('node-cloudfiles/containers').addBatch({ 18 | "The node-cloudfiles client": { 19 | "the destroyContainer() method": { 20 | "when deleting test_container": { 21 | topic: function () { 22 | client.destroyContainer('test_container', this.callback) 23 | }, 24 | "should return true": function (err, success) { 25 | assert.isTrue(success); 26 | } 27 | }, 28 | "when deleting test_cdn_container": { 29 | topic: function () { 30 | client.destroyContainer('test_cdn_container', this.callback) 31 | }, 32 | "should return true": function (err, success) { 33 | assert.isTrue(success); 34 | } 35 | } 36 | } 37 | } 38 | }).export(module); 39 | -------------------------------------------------------------------------------- /vendor/mkdirp.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | 4 | exports.mkdirp = exports.mkdirP = function mkdirP (p, mode, f) { 5 | var cb = f || function () {}; 6 | if (p.charAt(0) != '/') { cb(new Error('Relative path: ' + p)); return } 7 | 8 | var ps = path.normalize(p).split('/'); 9 | path.exists(p, function (exists) { 10 | if (exists) cb(null); 11 | else mkdirP(ps.slice(0,-1).join('/'), mode, function (err) { 12 | if (err && err.code !== 'EEXIST') cb(err) 13 | else fs.mkdir(p, mode, function (err) { 14 | if (err && err.code !== 'EEXIST') cb(err) 15 | else cb() 16 | }); 17 | }); 18 | }); 19 | }; 20 | 21 | exports.mkdirpSync = exports.mkdirPSync = function mkdirPSync (p, mode) { 22 | if (p.charAt(0) != '/') { throw new Error('Relative path: ' + p); return; } 23 | 24 | var ps = path.normalize(p).split('/'), 25 | exists = path.existsSync(p); 26 | 27 | function tryMkdirSync () { 28 | try { fs.mkdirSync(p, mode); } 29 | catch (ex) { if (ex.code !== 'EEXIST') throw ex; } 30 | } 31 | 32 | if (exists) return; 33 | else { 34 | try { 35 | mkdirPSync(ps.slice(0,-1).join('/'), mode); 36 | tryMkdirSync(); 37 | } 38 | catch (ex) { 39 | console.dir(ex); 40 | if (ex.code !== 'EEXIST') { throw ex; } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/cloudfiles/config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * config.js: Configuration information for your Rackspace Cloud account 3 | * 4 | * (C) 2010 Nodejitsu Inc. 5 | * MIT LICENSE 6 | * 7 | */ 8 | 9 | var fs = require('fs'), 10 | path = require('path'); 11 | 12 | var defaultAuthHost = 'auth.api.rackspacecloud.com'; 13 | 14 | // 15 | // function createConfig (defaults) 16 | // Creates a new instance of the configuration 17 | // object based on default values 18 | // 19 | exports.createConfig = function (options) { 20 | return new Config(options); 21 | }; 22 | 23 | // 24 | // Config (defaults) 25 | // Constructor for the Config object 26 | // 27 | var Config = exports.Config = function (options) { 28 | if (!options.auth) throw new Error ('options.auth is required to create Config'); 29 | 30 | this.auth = { 31 | username: options.auth.username, 32 | apiKey: options.auth.apiKey, 33 | host: options.auth.host || defaultAuthHost 34 | }; 35 | 36 | 37 | if (options.cdn) { 38 | this.cdn.ttl = options.cdn.ttl || this.cdn.ttl; 39 | this.logRetention = options.cdn.logRetention || this.cdn.logRetention; 40 | } 41 | 42 | var cachePath = path.join(this.cache.path, this.auth.username); 43 | this.cache.path = options.cache ? options.cache.cachePath || cachePath : cachePath; 44 | 45 | this.servicenet = options.servicenet === true; 46 | }; 47 | 48 | Config.prototype = { 49 | cdn: { 50 | ttl: 43200, // Default X-TTL time-out to 12 hours, 51 | logRetention: true // Default X-LOG-RETENTION to true 52 | }, 53 | cache: { 54 | path: path.join(__dirname, '..', '..', '.cache') 55 | }, 56 | servicenet: false 57 | }; 58 | 59 | // 60 | // ### function setStorageUrl (storageUrl) 61 | // ### @storageUrl {string} Rackspace Cloudfiles storage URL 62 | // Sets the storage URL for this instance, updating to the Serive Net URL if 63 | // Service Net transfer has been specified. 64 | Config.prototype.setStorageUrl = function(storageUrl) { 65 | if (this.servicenet === true) { 66 | this.storageUrl = storageUrl.replace('https://', 'https://snet-'); 67 | } else { 68 | this.storageUrl = storageUrl; 69 | } 70 | }; 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /test/authentication-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * authentication-test.js: Tests for Rackspace Cloudfiles authentication 3 | * 4 | * (C) 2010 Nodejitsu Inc. 5 | * MIT LICENSE 6 | * 7 | */ 8 | 9 | var path = require('path'), 10 | vows = require('vows'), 11 | assert = require('assert'), 12 | helpers = require('./helpers'), 13 | cloudfiles = require('../lib/cloudfiles'); 14 | 15 | 16 | var client = helpers.createClient(); 17 | 18 | vows.describe('node-cloudfiles/authentication').addBatch({ 19 | "The node-cloudfiles client": { 20 | "with a valid username and api key": { 21 | topic: function () { 22 | client.setAuth(this.callback); 23 | }, 24 | "should respond with 204 and appropriate headers": function (err, res) { 25 | assert.isNull(err); 26 | assert.equal(res.statusCode, 204); 27 | assert.isObject(res.headers); 28 | assert.include(res.headers, 'x-server-management-url'); 29 | assert.include(res.headers, 'x-storage-url'); 30 | assert.include(res.headers, 'x-cdn-management-url'); 31 | assert.include(res.headers, 'x-auth-token'); 32 | }, 33 | "should update the config with appropriate urls": function (err, res) { 34 | assert.isNull(err); 35 | assert.equal(res.headers['x-server-management-url'], client.config.serverUrl); 36 | assert.equal(res.headers['x-storage-url'], client.config.storageUrl); 37 | assert.equal(res.headers['x-cdn-management-url'], client.config.cdnUrl); 38 | assert.equal(res.headers['x-auth-token'], client.config.authToken); 39 | } 40 | }, 41 | "with an invalid username and api key": { 42 | topic: function () { 43 | var invalidClient = cloudfiles.createClient({ 44 | auth: { 45 | username: 'invalid-username', 46 | apiKey: 'invalid-apikey' 47 | } 48 | }); 49 | 50 | invalidClient.setAuth(this.callback); 51 | }, 52 | "should respond with 401 and return an error": function (err, res) { 53 | assert.ok(err instanceof Error); 54 | assert.equal(res.statusCode, 401); 55 | } 56 | }, 57 | } 58 | }).export(module); 59 | -------------------------------------------------------------------------------- /lib/cloudfiles/container.js: -------------------------------------------------------------------------------- 1 | /* 2 | * container.js: Instance of a single Rackspace Cloudfiles container 3 | * 4 | * (C) 2010 Nodejitsu Inc. 5 | * MIT LICENSE 6 | * 7 | */ 8 | 9 | var cloudfiles = require('../cloudfiles'); 10 | 11 | var Container = exports.Container = function (client, details) { 12 | if (!details) { 13 | throw new Error("Container must be constructed with at least basic details.") 14 | } 15 | 16 | this.files = []; 17 | this.client = client; 18 | this._setProperties(details); 19 | }; 20 | 21 | Container.prototype = { 22 | addFile: function (file, local, options, callback) { 23 | return this.client.addFile(this.name, file, local, options, callback); 24 | }, 25 | 26 | destroy: function (callback) { 27 | this.client.destroyContainer(this.name, callback); 28 | }, 29 | 30 | getFiles: function (download, callback) { 31 | var self = this; 32 | 33 | // download can be omitted: (...).getFiles(callback); 34 | // In this case first argument will be a function 35 | if (typeof download === 'function' && !(download instanceof RegExp)) { 36 | callback = download; 37 | download = false; 38 | } 39 | 40 | this.client.getFiles(this.name, download, function (err, files) { 41 | if (err) { 42 | return callback(err); 43 | } 44 | 45 | self.files = files; 46 | callback(null, files); 47 | }); 48 | }, 49 | 50 | removeFile: function (file, callback) { 51 | this.client.destroyFile(this.name, file, callback); 52 | }, 53 | 54 | // Remark: Not fully implemented 55 | updateCdn: function (options, callback) { 56 | if (!this.cdnEnabled) { 57 | callback(new Error('Cannot call updateCdn on a container that is not CDN enabled')); 58 | } 59 | 60 | // TODO: Write the rest of this method 61 | }, 62 | 63 | _setProperties: function (details) { 64 | this.name = details.name; 65 | this.cdnEnabled = details.cdnEnabled || false; 66 | this.cdnUri = details.cdnUri; 67 | this.cdnSslUri = details.cdnSslUri; 68 | this.ttl = details.ttl; 69 | this.logRetention = details.logRetention; 70 | this.count = details.count || 0; 71 | this.bytes = details.bytes || 0; 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /test/servicenet-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * servicenet-test.js: Tests for Rackspace Cloudfiles Service Net transfer 3 | * 4 | * MIT LICENSE 5 | * 6 | */ 7 | 8 | var path = require('path'), 9 | vows = require('vows'), 10 | assert = require('assert'), 11 | helpers = require('./helpers'), 12 | cloudfiles = require('../lib/cloudfiles'); 13 | 14 | // Create a config that has servicenet = true 15 | var testConfig = helpers.loadConfig(), 16 | snConfig = {}; 17 | for (var key in testConfig) { 18 | if (testConfig.hasOwnProperty(key)) snConfig[key] = testConfig[key]; 19 | } 20 | snConfig.servicenet = true; 21 | 22 | var client = helpers.createClient(), 23 | snClient = cloudfiles.createClient(snConfig); 24 | 25 | vows.describe('node-cloudfiles/servicenet').addBatch({ 26 | "The node-cloudfiles client": { 27 | "with valid credentials and not specifying ServiceNet transfer": { 28 | topic: function () { 29 | client.setAuth(this.callback); 30 | }, 31 | "should respond with 204 and appropriate headers": function (err, res) { 32 | assert.equal(res.statusCode, 204); 33 | assert.isObject(res.headers); 34 | assert.include(res.headers, 'x-server-management-url'); 35 | assert.include(res.headers, 'x-storage-url'); 36 | assert.include(res.headers, 'x-cdn-management-url'); 37 | assert.include(res.headers, 'x-auth-token'); 38 | }, 39 | "should update the config with non-ServiceNet storage url": function (err, res) { 40 | assert.equal(res.headers['x-storage-url'], client.config.storageUrl); 41 | assert.ok(client.config.storageUrl.substring(0, 13) != 'https://snet-'); 42 | } 43 | }, 44 | "with valid credentials and specifying ServiceNet transfer": { 45 | topic: function () { 46 | snClient.setAuth(this.callback); 47 | }, 48 | "should respond with 204 and appropriate headers": function (err, res) { 49 | assert.equal(res.statusCode, 204); 50 | assert.isObject(res.headers); 51 | assert.include(res.headers, 'x-server-management-url'); 52 | assert.include(res.headers, 'x-storage-url'); 53 | assert.include(res.headers, 'x-cdn-management-url'); 54 | assert.include(res.headers, 'x-auth-token'); 55 | }, 56 | "should update the config with non-ServiceNet storage url": function (err, res) { 57 | assert.notEqual(res.headers['x-storage-url'], snClient.config.storageUrl); 58 | assert.ok(snClient.config.storageUrl.substring(0, 13) == 'https://snet-'); 59 | } 60 | }, 61 | } 62 | }).export(module); 63 | 64 | -------------------------------------------------------------------------------- /test/storage-object-large-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * container-test.js: Tests for Rackspace Cloudfiles containers 3 | * 4 | * (C) 2010 Nodejitsu Inc. 5 | * MIT LICENSE 6 | * 7 | */ 8 | 9 | var path = require('path'), 10 | vows = require('vows'), 11 | fs = require('fs'), 12 | assert = require('assert'), 13 | cloudfiles = require('../lib/cloudfiles'), 14 | helpers = require('./helpers'); 15 | 16 | var testData = {}, 17 | container = 'test_container', 18 | client = helpers.createClient(), 19 | sampleData = new Buffer(3 * 1024 * 1024), 20 | sampleFile = path.join(__dirname, '..', 'test', 'fixtures', 'bigfile.raw'); 21 | 22 | fs.writeFileSync(sampleFile, sampleData); 23 | 24 | vows.describe('node-cloudfiles/storage-object/large').addBatch( 25 | helpers.requireAuth(client) 26 | ).addBatch({ 27 | "The node-cloudfiles client": { 28 | "the addFile() method": { 29 | topic: function () { 30 | var ustream = client.addFile(container, { 31 | remote: 'bigfile.raw', 32 | local: sampleFile 33 | }, function () { }); 34 | 35 | ustream.on('end', this.callback); 36 | }, 37 | "should raise the `end` event": function () { 38 | assert.isTrue(true); 39 | } 40 | } 41 | } 42 | }).addBatch({ 43 | "The node-cloudfiles client": { 44 | "the getFiles() method": { 45 | topic: function () { 46 | client.getFiles(container, this.callback); 47 | }, 48 | "should return a valid list of files": function (err, files) { 49 | assert.isNull(err); 50 | assert.ok(files.length >= 1); 51 | assert.ok(files.some(function (file) { 52 | return /bigfile.raw/.test(file.name); 53 | })); 54 | } 55 | } 56 | } 57 | }).addBatch({ 58 | "The node-cloudfiles client": { 59 | "the getFile() method": { 60 | "for a file that exists": { 61 | topic: function () { 62 | client.getFile(container, 'bigfile.raw', this.callback); 63 | }, 64 | "should return a file with correct content": function (err, file) { 65 | assert.isNull(err); 66 | helpers.assertEqualBuffers(fs.readFileSync(file.local), sampleData); 67 | } 68 | } 69 | } 70 | } 71 | }).addBatch({ 72 | "The node-cloudfiles client": { 73 | "the destroyFile() method": { 74 | "for a file that exists": { 75 | topic: function () { 76 | client.destroyFile(container, 'bigfile.raw', this.callback); 77 | fs.unlinkSync(sampleFile); 78 | }, 79 | "should return true": function (err, deleted) { 80 | assert.isNull(err); 81 | assert.isTrue(deleted); 82 | } 83 | } 84 | } 85 | } 86 | }).export(module); 87 | -------------------------------------------------------------------------------- /test/storage-object-metadata-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * container-test.js: Tests for Rackspace Cloudfiles containers 3 | * 4 | * (C) 2010 Nodejitsu Inc. 5 | * MIT LICENSE 6 | * 7 | */ 8 | 9 | var path = require('path'), 10 | vows = require('vows'), 11 | fs = require('fs'), 12 | assert = require('assert'), 13 | cloudfiles = require('../lib/cloudfiles'), 14 | helpers = require('./helpers'); 15 | 16 | var testData = {}, 17 | client = helpers.createClient(), 18 | sampleData = fs.readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'fillerama.txt')).toString(); 19 | 20 | vows.describe('node-cloudfiles/storage-object').addBatch({ 21 | "The node-cloudfiles client": { 22 | "an instance of a Container object": { 23 | "the addFile() method": { 24 | topic: function () { 25 | var ustream = client.addFile('test_container', { 26 | remote: 'file1.txt', 27 | local: path.join(__dirname, '..', 'test', 'fixtures', 'fillerama.txt') 28 | }, function () { }); 29 | 30 | ustream.on('end', this.callback) 31 | }, 32 | "should raise the `end` event": function () { 33 | assert.isTrue(true); 34 | } 35 | } 36 | } 37 | } 38 | }).addBatch({ 39 | "The node-cloudfiles client": { 40 | "the getFile() method": { 41 | "for a file that exists": { 42 | topic: function () { 43 | client.getFile('test_container', 'file1.txt', this.callback); 44 | }, 45 | "should return a valid StorageObject": function (err, file) { 46 | helpers.assertFile(file); 47 | testData.file = file; 48 | } 49 | } 50 | } 51 | } 52 | })/*.addBatch({ 53 | "The node-cloudfiles client": { 54 | "the addMetadata() method": { 55 | topic: function () { 56 | testData.file.addMetadata({ "ninja": "true" }, this.callback); 57 | }, 58 | "should response with true": function (err, added) { 59 | assert.isTrue(added); 60 | } 61 | } 62 | } 63 | }).addBatch({ 64 | "The node-cloudfiles client": { 65 | "the getMetadata() method": { 66 | topic: function () { 67 | testData.file.getMetadata(this.callback); 68 | }, 69 | "should response with true": function (err, added) { 70 | assert.isTrue(added); 71 | } 72 | } 73 | } 74 | })*/.addBatch({ 75 | "The node-cloudfiles client": { 76 | "the destroyFile() method": { 77 | "for a file that exists": { 78 | topic: function () { 79 | client.destroyFile('test_container', 'file1.txt', this.callback); 80 | }, 81 | "should return true": function (err, deleted) { 82 | assert.isTrue(deleted); 83 | } 84 | } 85 | } 86 | } 87 | }).export(module); 88 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | /* 2 | * helpers.js: Test helpers for node-cloudservers 3 | * 4 | * (C) 2010 Nodejitsu Inc. 5 | * MIT LICENSE 6 | * 7 | */ 8 | 9 | var util = require('util'), 10 | fs = require('fs'), 11 | path = require('path'), 12 | vows = require('vows'), 13 | assert = require('assert'), 14 | cloudfiles = require('../lib/cloudfiles'); 15 | 16 | var helpers = exports, 17 | testConfig, 18 | client; 19 | 20 | helpers.loadConfig = function () { 21 | try { 22 | var configFile = path.join(__dirname, 'fixtures', 'test-config.json'), 23 | stats = fs.statSync(configFile), 24 | config = JSON.parse(fs.readFileSync(configFile).toString()); 25 | 26 | if (config.auth.username === 'test-username' 27 | || config.auth.apiKey === 'test-apiKey') { 28 | util.puts('Config file test-config.json must be updated with valid data before running tests.'); 29 | process.exit(0); 30 | } 31 | 32 | testConfig = config; 33 | return config; 34 | 35 | } 36 | catch (ex) { 37 | util.puts('Config file test/fixtures/test-config.json must be created with valid data before running tests.'); 38 | process.exit(0); 39 | } 40 | }; 41 | 42 | helpers.createClient = function () { 43 | if (!testConfig) { 44 | helpers.loadConfig(); 45 | } 46 | 47 | if (!client) { 48 | client = cloudfiles.createClient(testConfig); 49 | } 50 | 51 | return client; 52 | }; 53 | 54 | helpers.assertContainer = function (container) { 55 | assert.instanceOf(container, cloudfiles.Container); 56 | assert.isNotNull(container.name); 57 | assert.isNotNull(container.count); 58 | assert.isNotNull(container.bytes); 59 | }; 60 | 61 | helpers.assertCdnContainer = function (container) { 62 | helpers.assertContainer(container); 63 | assert.isTrue(typeof container.ttl === 'number'); 64 | assert.isTrue(typeof container.logRetention === 'boolean'); 65 | assert.isTrue(typeof container.cdnUri === 'string'); 66 | assert.isTrue(typeof container.cdnSslUri === 'string'); 67 | assert.ok(container.cdnSslUri.match(/^https:/)); 68 | assert.isTrue(container.cdnEnabled); 69 | }; 70 | 71 | helpers.assertFile = function (file) { 72 | assert.instanceOf(file, cloudfiles.StorageObject); 73 | assert.isNotNull(file.name); 74 | assert.isNotNull(file.bytes); 75 | assert.isNotNull(file.etag || file.hash); 76 | assert.isNotNull(file.lastModified); 77 | assert.isNotNull(file.contentType); 78 | }; 79 | 80 | helpers.assertEqualBuffers = function (a, b) { 81 | var result = a.length === b.length, 82 | i = 0; 83 | 84 | for (var len = a.length; result && i < len; i++) { 85 | if (a[i] !== b[i]) result = false; 86 | } 87 | 88 | return result; 89 | }; 90 | 91 | helpers.countTestContainers = function (containers) { 92 | return containers.reduce(function (count,container) { 93 | if (container.name == "test_container" || container.name == "test_cdn_container") { 94 | count++; 95 | } 96 | return count; 97 | }, 0); 98 | }; 99 | 100 | helpers.requireAuth = function () { 101 | return { 102 | "This test required Rackspace authorization": { 103 | topic: function () { 104 | if (client.authorized) { 105 | return this.callback(); 106 | } 107 | 108 | client.setAuth(this.callback); 109 | }, 110 | "the client is now authorized": function () { 111 | assert.isTrue(client.authorized); 112 | } 113 | } 114 | } 115 | }; 116 | -------------------------------------------------------------------------------- /lib/cloudfiles/storage-object.js: -------------------------------------------------------------------------------- 1 | /* 2 | * storage-object.js: Instance of a single rackspace cloudserver 3 | * 4 | * (C) 2010 Nodejitsu Inc. 5 | * MIT LICENSE 6 | * 7 | */ 8 | 9 | var fs = require('fs'), 10 | cloudfiles = require('../cloudfiles'), 11 | common = require('./common'); 12 | 13 | var StorageObject = exports.StorageObject = function (client, details) { 14 | if (!details) { 15 | throw new Error("StorageObject must be constructed with at least basic details.") 16 | } 17 | 18 | this.client = client; 19 | this._setProperties(details); 20 | }; 21 | 22 | StorageObject.prototype = { 23 | // Remark: Not fully implemented 24 | addMetadata: function (metadata, callback) { 25 | var newMetadata = clone(this.metadata); 26 | Object.keys(metadata).forEach(function (key) { 27 | newMetadata[key] = metadata[key]; 28 | }); 29 | 30 | var options = { 31 | uri: this.fullPath, 32 | method: 'POST', 33 | headers: this._createHeaders(newMetadata) 34 | }; 35 | 36 | common.rackspace(options, callback, function (body, res) { 37 | this.metadata = newMetadata; 38 | callback(null, true); 39 | }); 40 | }, 41 | 42 | // Remark: This method is untested 43 | copy: function (container, destination, callback) { 44 | var copyOptions = { 45 | method: 'PUT', 46 | uri: this.fullPath, 47 | headers: { 48 | 'X-COPY-DESTINATION': [container, destination].join('/'), 49 | 'CONTENT-LENGTH': this.bytes 50 | } 51 | }; 52 | 53 | common.rackspace(copyOptions, callback, function (body, res) { 54 | callback(null, true); 55 | }); 56 | }, 57 | 58 | destroy: function (callback) { 59 | this.client.destroyFile(this.containerName, this.name, callback); 60 | }, 61 | 62 | // Remark: Not fully implemented 63 | getMetadata: function (callback) { 64 | common.rackspace('HEAD', this.fullPath, function (body, res) { 65 | var metadata = {}; 66 | Object.keys(res.headers).forEach(function (header) { 67 | var match; 68 | if (match = header.match(/x-object-meta-(\w+)/i)) { 69 | metadata[match[1]] = res.headers[header]; 70 | } 71 | }); 72 | 73 | callback(null, metadata); 74 | }); 75 | }, 76 | 77 | // Remark: Not fully implemented 78 | removeMetadata: function (keys, callback) { 79 | var newMetadata = {}; 80 | Object.keys(this.metadata).forEach(function (key) { 81 | if (keys.indexOf(key) !== -1) { 82 | newMetadata[key] = this.metadata[key]; 83 | } 84 | }); 85 | 86 | // TODO: Finish writing this method 87 | }, 88 | 89 | save: function (options, callback) { 90 | var self = this; 91 | var fileStream = fs.createWriteStream(options.local, { 92 | flags: options.flags || 'w+', 93 | encoding: options.encoding || null, 94 | mode: options.mode || 0666 95 | }); 96 | 97 | fs.readFile(this.local, function (err, data) { 98 | if (err) { 99 | return callback(err); 100 | } 101 | 102 | function endWrite() { 103 | fileStream.end(); 104 | callback(null, options.local); 105 | } 106 | 107 | var written = false; 108 | fileStream.on('drain', function () { 109 | if (!written) { 110 | endWrite(); 111 | } 112 | }); 113 | 114 | written = fileStream.write(data); 115 | if (written) { 116 | endWrite(); 117 | } 118 | }); 119 | }, 120 | 121 | update: function (data, callback) { 122 | 123 | }, 124 | 125 | get fullPath() { 126 | return this.client.storageUrl(this.containerName, this.name); 127 | }, 128 | 129 | get containerName() { 130 | return this.container instanceof cloudfiles.Container ? this.container.name : this.container; 131 | }, 132 | 133 | _setProperties: function (details) { 134 | // TODO: Should probably take this in from details or something. 135 | this.metadata = {}; 136 | 137 | this.container = details.container || null; 138 | this.name = details.name || null; 139 | this.etag = details.etag || null; 140 | this.hash = details.hash || null; 141 | this.bytes = details.bytes || null; 142 | this.local = details.local || null; 143 | this.contentType = details.content_type || null; 144 | this.lastModified = details.last_modified || null; 145 | }, 146 | 147 | _createHeaders: function (metadata) { 148 | var headers = {}; 149 | Object.keys(metadata).forEach(function (key) { 150 | var header = "x-object-meta-" + key; 151 | headers[header] = metadata[key]; 152 | }); 153 | 154 | return headers; 155 | } 156 | }; 157 | 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-cloudfiles 2 | 3 | **THIS LIBRARY IS DEPRECATED. ALL FIXES SHOULD BE DIRECTED to [pkgcloud](https://github.com/nodejitsu/pkgcloud)** 4 | 5 | A client implementation for Rackspace CloudFiles in node.js 6 | 7 | ## Installation 8 | 9 | ### Installing npm (node package manager) 10 | ``` bash 11 | $ curl http://npmjs.org/install.sh | sh 12 | ``` 13 | 14 | ### Installing node-cloudfiles 15 | ``` bash 16 | $ npm install cloudfiles 17 | ``` 18 | 19 | ### [Getting Rackspace Account][4] 20 | 21 | ## Usage 22 | 23 | The node-cloudfiles library is compliant with the [Rackspace CloudFiles API][0]. Using node-cloudfiles is easy for a variety of scenarios: authenticating, creating and working with both containers and storage objects. 24 | 25 | ### Getting Started 26 | Before we can do anything with cloudfiles, we have to create a client with valid credentials. Cloudfiles will authenticate for you automatically: 27 | 28 | ``` js 29 | var cloudfiles = require('cloudfiles'); 30 | var config = { 31 | auth : { 32 | username: 'your-username', 33 | apiKey: 'your-api-key' 34 | } 35 | }; 36 | 37 | var client = cloudfiles.createClient(config); 38 | ``` 39 | 40 | ### Working with Containers 41 | Rackspace Cloudfiles divides files into 'Containers'. These are very similar to S3 Buckets if you are more familiar with Amazon. There are a couple of simple operations exposed by node-cloudfiles: 42 | 43 | ``` js 44 | // Creating a container 45 | client.setAuth(function () { 46 | client.createContainer('myContainer', function (err, container) { 47 | // Listing files in the Container 48 | container.getFiles(function (err, files) { 49 | 50 | }); 51 | }); 52 | }); 53 | ``` 54 | 55 | ### Uploading and Downloading Files 56 | Each Container has a set of 'StorageObjects' (or files) which can be retrieved via a Cloudfiles client. Files are downloaded to a local file cache that can be configured per client. 57 | 58 | ``` js 59 | client.createContainer('myContainer', function (err, container) { 60 | // 61 | // Uploading a file 62 | // 63 | client.addFile('myContainer', { remote: 'remoteName.txt', local: 'path/to/local/file.txt' }, function (err, uploaded) { 64 | // File has been uploaded 65 | }); 66 | 67 | // 68 | // Downloading a file 69 | // 70 | client.getFile('myContainer', 'remoteName.txt', function (err, file) { 71 | // 72 | // Save it to a location outside the cache 73 | // 74 | file.save({ local: 'path/to/local/file.txt' }, function (err, filename) { 75 | // 76 | // File has been saved. 77 | // 78 | }); 79 | }); 80 | }); 81 | ``` 82 | 83 | ## Authentication Service 84 | 85 | Use the 'host' key in the auth configuration to specify the url to use for authentication: 86 | 87 | ``` js 88 | var cloudfiles = require('cloudfiles'); 89 | var config = { 90 | auth : { 91 | username: 'your-username', 92 | apiKey: 'your-api-key', 93 | host : "lon.auth.api.rackspacecloud.com" 94 | } 95 | }; 96 | 97 | var client = cloudfiles.createClient(config); 98 | ``` 99 | 100 | ## Transfer over ServiceNet 101 | 102 | Rackspace Cloud Servers have a private interface, known as ServiceNet, that is unmetered and has double the throughput of the public interface. When transferring files between a Cloud Server and Cloud Files, ServiceNet can be used instead of the public interface. 103 | 104 | By default, ServiceNet is not used. To use ServiceNet for the transfer, set the 'servicenet' key to `true` in your client config: 105 | 106 | ``` js 107 | var cloudfiles = require('cloudfiles'); 108 | var config = { 109 | auth : { 110 | username: 'your-username', 111 | apiKey: 'your-api-key', 112 | host : "lon.auth.api.rackspacecloud.com" 113 | }, 114 | servicenet: true 115 | }; 116 | 117 | var client = cloudfiles.createClient(config); 118 | ``` 119 | 120 | NOTE: ServiceNet can only be used to transfer files between Cloud Servers and Cloud Files within the same datacenter. Rackspace support can migrate both Cloud Servers and Cloud Files to the same datacenter if needed. 121 | 122 | ## Roadmap 123 | 124 | 1. Implement Storage Object metadata APIs. 125 | 126 | ## Run Tests 127 | All of the node-cloudservers tests are written in [vows][2], and cover all of the use cases described above. You will need to add your Rackspace API username and API key to test/fixtures/test-config.json before running tests: 128 | 129 | ``` js 130 | { 131 | "auth": { 132 | "username": "your-username", 133 | "apiKey": "your-apikey" 134 | } 135 | } 136 | ``` 137 | 138 | Once you have valid Rackspace credentials you can run tests with [vows][2]: 139 | 140 | ``` bash 141 | vows test/*-test.js --spec 142 | ``` 143 | 144 | #### Author: [Charlie Robbins](http://www.charlierobbins.com) 145 | #### Contributors: [Fedor Indutny](http://github.com/donnerjack13589), [aaronds](https://github.com/aaronds) 146 | 147 | [0]: http://docs.rackspacecloud.com/files/api/cf-devguide-latest.pdf 148 | [1]: http://nodejitsu.com 149 | [2]: http://vowsjs.org 150 | [3]: http://blog.nodejitsu.com/nodejs-cloud-server-in-three-minutes 151 | [4]: http://www.rackspacecloud.com/1469-0-3-13.html 152 | -------------------------------------------------------------------------------- /test/storage-object-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * container-test.js: Tests for Rackspace Cloudfiles containers 3 | * 4 | * (C) 2010 Nodejitsu Inc. 5 | * MIT LICENSE 6 | * 7 | */ 8 | 9 | var path = require('path'), 10 | vows = require('vows'), 11 | fs = require('fs'), 12 | assert = require('assert'), 13 | cloudfiles = require('../lib/cloudfiles'), 14 | helpers = require('./helpers'); 15 | 16 | var testData = {}, client = helpers.createClient(), 17 | sampleData = fs.readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'fillerama.txt')).toString(); 18 | 19 | vows.describe('node-cloudfiles/storage-object').addBatch(helpers.requireAuth(client)).addBatch({ 20 | "The node-cloudfiles client": { 21 | "the addFile() method": { 22 | topic: function () { 23 | var ustream = client.addFile('test_container', { 24 | remote: 'file1.txt', 25 | local: path.join(__dirname, '..', 'test', 'fixtures', 'fillerama.txt') 26 | }, function () { }); 27 | 28 | ustream.on('end', this.callback); 29 | }, 30 | "should raise the `end` event": function () { 31 | assert.isTrue(true); 32 | } 33 | } 34 | } 35 | }).addBatch({ 36 | "The node-cloudfiles client": { 37 | "the addFile() method called a second time": { 38 | topic: function () { 39 | var ustream = client.addFile('test_container', { 40 | remote: 'file2.txt', 41 | local: path.join(__dirname, '..', 'test', 'fixtures', 'fillerama.txt') 42 | }, function () { }); 43 | 44 | ustream.on('end', this.callback) 45 | }, 46 | "should raise the `end` event": function () { 47 | assert.isTrue(true); 48 | } 49 | } 50 | } 51 | }).addBatch({ 52 | "The node-cloudfiles client": { 53 | "the addFile() method with a pre-provided read stream": { 54 | topic: function () { 55 | var fileName = path.join(__dirname, '..', 'test', 'fixtures', 'fillerama.txt'), 56 | readStream = fs.createReadStream(fileName), 57 | headers = { 'content-length': fs.statSync(fileName).size }, 58 | ustream; 59 | 60 | ustream = client.addFile('test_container', { 61 | remote: 'file3.txt', 62 | stream: readStream, 63 | headers: headers 64 | }, this.callback); 65 | 66 | ustream.on('end', this.callback); 67 | }, 68 | "should raise the `end` event": function () { 69 | assert.isTrue(true); 70 | } 71 | }, 72 | "the addFile() method using a stream without a predefined length": { 73 | topic: function () { 74 | var fileName = path.join(__dirname, '..', 'test', 'fixtures', 'fillerama.txt'), 75 | readStream = fs.createReadStream(fileName), 76 | ustream; 77 | 78 | ustream = client.addFile('test_container', { 79 | remote: 'file3.txt', 80 | stream: readStream 81 | }, this.callback); 82 | 83 | ustream.on('end', this.callback); 84 | }, 85 | "should raise the `end` event": function () { 86 | assert.isTrue(true); 87 | } 88 | } 89 | } 90 | }).addBatch({ 91 | "The node-cloudfiles client": { 92 | "the getFiles() method": { 93 | topic: function () { 94 | client.getFiles('test_container', this.callback); 95 | }, 96 | "should return a valid list of files": function (err, files) { 97 | files.forEach(function (file) { 98 | helpers.assertFile(file); 99 | }); 100 | } 101 | } 102 | } 103 | }).addBatch({ 104 | "The node-cloudfiles client": { 105 | "the getFile() method": { 106 | "for a file that exists": { 107 | topic: function () { 108 | client.getFile('test_container', 'file2.txt', this.callback); 109 | }, 110 | "should return a valid StorageObject": function (err, file) { 111 | helpers.assertFile(file); 112 | testData.file = file; 113 | } 114 | } 115 | } 116 | } 117 | }).addBatch({ 118 | "The node-cloudfiles client": { 119 | "an instance of StorageObject": { 120 | "the save() method": { 121 | topic: function () { 122 | var self = this; 123 | testData.file.save({ local: path.join(__dirname, 'fixtures', 'fillerama3.txt') }, function (err, filename) { 124 | if (err) { 125 | return self.callback(err); 126 | } 127 | 128 | fs.stat(filename, self.callback) 129 | }); 130 | }, 131 | "should write the file to the specified location": function (err, stats) { 132 | assert.isNull(err); 133 | assert.isNotNull(stats); 134 | } 135 | } 136 | } 137 | } 138 | }).addBatch({ 139 | "The node-cloudfiles client": { 140 | "the destroyFile() method": { 141 | "for a file that exists": { 142 | topic: function () { 143 | client.destroyFile('test_container', 'file1.txt', this.callback); 144 | }, 145 | "should return true": function (err, deleted) { 146 | assert.isTrue(deleted); 147 | } 148 | } 149 | , "for a file that does not exist": { 150 | topic: function () { 151 | client.destroyFile('test_container', 'file0.txt', this.callback); 152 | }, 153 | "should return error": function (err, deleted) { 154 | assert.ok(err instanceof Error); 155 | } 156 | } 157 | } 158 | } 159 | }).addBatch({ 160 | "The node-cloudfiles client": { 161 | "an instance of a StorageObject": { 162 | "the destroy() method": { 163 | "for a file that exists": { 164 | topic: function () { 165 | testData.file.destroy(this.callback); 166 | }, 167 | "should return true": function (err, deleted) { 168 | assert.isTrue(deleted); 169 | } 170 | } 171 | } 172 | } 173 | } 174 | }).export(module); 175 | -------------------------------------------------------------------------------- /lib/cloudfiles/common.js: -------------------------------------------------------------------------------- 1 | /* 2 | * core.js: Core functions for accessing Rackspace CloudFiles 3 | * 4 | * (C) 2010 Nodejitsu Inc. 5 | * MIT LICENSE 6 | * 7 | */ 8 | 9 | var fs = require('fs'), 10 | http = require('http'), 11 | https = require('https'), 12 | url = require('url'), 13 | mkdirpSync = require('../../vendor/mkdirp').mkdirpSync, 14 | request = require('request'), 15 | cloudfiles = require('../cloudfiles'); 16 | 17 | var common = exports; 18 | 19 | // 20 | // Failure HTTP Response codes based 21 | // off Rackspace CloudFiles specification. 22 | // 23 | var failCodes = common.failCodes = { 24 | 400: "Bad Request", 25 | 401: "Unauthorized", 26 | 403: "Resize not allowed", 27 | 404: "Item not found", 28 | 409: "Build in progress", 29 | 413: "Over Limit", 30 | 415: "Bad Media Type", 31 | 500: "Fault", 32 | 503: "Service Unavailable" 33 | }; 34 | 35 | // 36 | // Success HTTP Response codes based 37 | // off Rackspace CloudFiles specification. 38 | // 39 | var successCodes = common.successCodes = { 40 | 200: "OK", 41 | 202: "Accepted", 42 | 203: "Non-authoritative information", 43 | 204: "No content", 44 | }; 45 | 46 | // 47 | // ### function statOrMkDirp (path) 48 | // #### @path {string} Path to validate or create. 49 | // Checks if a directory exists. If not, creates that directory 50 | // 51 | common.statOrMkdirp = function (path) { 52 | try { fs.statSync(path) } 53 | catch (ex) { mkdirpSync(path, 0755) } 54 | 55 | return path; 56 | }; 57 | 58 | // 59 | // ### function clone (obj) 60 | // #### @obj {Object} Object to clone. 61 | // Clones the specified object, `obj`. 62 | // 63 | common.clone = function (obj) { 64 | var clone = {}; 65 | for (var i in obj) { 66 | clone[i] = obj[i]; 67 | } 68 | return clone; 69 | }; 70 | 71 | // 72 | // ### function mixin (target, [source1,] [source2,]) 73 | // #### @target {Object} Target object to mixin to 74 | // Mixes in the arguments to the `target` object. 75 | // 76 | common.mixin = function (target) { 77 | var objs = Array.prototype.slice.call(arguments, 1); 78 | objs.forEach(function (o) { 79 | Object.keys(o).forEach(function (k) { 80 | if (! o.__lookupGetter__(k)) { 81 | target[k] = o[k]; 82 | } 83 | }); 84 | }); 85 | 86 | return target; 87 | }; 88 | 89 | 90 | // 91 | // Core method that actually sends requests to Rackspace. 92 | // This method is designed to be flexible w.r.t. arguments 93 | // and continuation passing given the wide range of different 94 | // requests required to fully implement the CloudFiles API. 95 | // 96 | // Continuations: 97 | // 1. 'callback': The callback passed into every node-cloudfiles method 98 | // 2. 'success': A callback that will only be called on successful requests. 99 | // This is used throughout node-cloudfiles to conditionally 100 | // do post-request processing such as JSON parsing. 101 | // 102 | // Possible Arguments (1 & 2 are equivalent): 103 | // 1. common.rackspace('some-fully-qualified-url', client, callback, success) 104 | // 2. common.rackspace('GET', 'some-fully-qualified-url', client, callback, success) 105 | // 3. common.rackspace('DELETE', 'some-fully-qualified-url', client, callback, success) 106 | // 4. common.rackspace({ method: 'POST', uri: 'some-url', client: new Cloudfiles(), body: { some: 'body'} }, callback, success) 107 | // 108 | common.rackspace = function () { 109 | var args = Array.prototype.slice.call(arguments), 110 | success = (typeof(args[args.length - 1]) === 'function') && args.pop(), 111 | callback = (typeof(args[args.length - 1]) === 'function') && args.pop(), 112 | options = { headers: {} }, 113 | client, 114 | download, 115 | upload, 116 | rstream; 117 | 118 | // 119 | // Now that we've popped off the two callbacks 120 | // We can make decisions about other arguments 121 | // 122 | if (args.length == 1) { 123 | client = args[0]['client']; 124 | options.headers = args[0]['headers'] || {}; 125 | options.method = args[0]['method'] || 'GET'; 126 | options.body = args[0]['body']; 127 | options.uri = args[0]['uri']; 128 | 129 | // 130 | // Attempt to grab the `upload` or `download` streams 131 | // (if they exist). 132 | // 133 | upload = args[0].upload; 134 | download = args[0].download; 135 | } 136 | else if (args.length === 2) { 137 | // 138 | // If we got a string assume that it's the URI 139 | // 140 | client = args[1]; 141 | options.method = 'GET'; 142 | options.uri = args[0]; 143 | } 144 | else { 145 | client = args[2]; 146 | options.method = args[0]; 147 | options.uri = args[1]; 148 | } 149 | 150 | if (!client.authorized) { 151 | return callback(new Error('Cannot make Rackspace request if not authorized')); 152 | } 153 | 154 | // 155 | // Append the `x-auth-token` header for Rackspace authentication 156 | // 157 | options.headers['x-auth-token'] = client.config.authToken; 158 | 159 | // 160 | // If no `content-type` header has been sent then assume JSON. 161 | // 162 | if (typeof options.body !== 'undefined' && !options.headers['content-type']) { 163 | options.headers['content-type'] = 'application/json'; 164 | options.body = JSON.stringify(options.body); 165 | } 166 | 167 | if (upload) { 168 | if (!options.headers['content-length']) { 169 | options.headers['transfer-encoding'] = 'chunked'; 170 | } 171 | } 172 | 173 | rstream = request(options, function (err, res, body) { 174 | if (err) { 175 | if (callback) { 176 | callback(err); 177 | } 178 | 179 | return; 180 | } 181 | 182 | var statusCode = res.statusCode.toString(); 183 | if (Object.keys(failCodes).indexOf(statusCode) !== -1) { 184 | if (callback) { 185 | callback(new Error('Rackspace Error (' + statusCode + '): ' + failCodes[statusCode])); 186 | } 187 | 188 | return; 189 | } 190 | 191 | success(body, res); 192 | }); 193 | 194 | if (upload) { 195 | upload.pipe(rstream); 196 | } 197 | else if (download) { 198 | rstream.pipe(download); 199 | } 200 | 201 | return rstream; 202 | }; 203 | 204 | // Runs a array of functions with callback argument 205 | // And calls finish at the end of execution 206 | common.runBatch = function (fns, finish) { 207 | var finished = 0, 208 | total = fns.length, 209 | errors = new Array(total), 210 | was_error = false, 211 | results = new Array(total); 212 | 213 | fns.forEach(function (fn, i) { 214 | var once = false; 215 | 216 | function next(err, value) { 217 | // Can be called only one time 218 | if (once) { 219 | return; 220 | } 221 | 222 | once = true; 223 | 224 | // If have error - push them into errors 225 | if (err) { 226 | was_error = true; 227 | errors[i] = err; 228 | } 229 | else { 230 | results[i] = value; 231 | } 232 | 233 | if (++finished === total) { 234 | finish(was_error ? errors : null, results); 235 | } 236 | } 237 | 238 | var value = fn(next); 239 | 240 | // If function returns value - it won't be working in async mode 241 | if (value !== undefined) { 242 | next(null, value); 243 | } 244 | }); 245 | 246 | if (fns.length === 0) { 247 | finish(null, []); 248 | } 249 | }; 250 | -------------------------------------------------------------------------------- /test/container-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * container-test.js: Tests for Rackspace Cloudfiles containers 3 | * 4 | * (C) 2010 Nodejitsu Inc. 5 | * MIT LICENSE 6 | * 7 | */ 8 | 9 | var path = require('path'), 10 | fs = require('fs'), 11 | vows = require('vows'), 12 | assert = require('assert'), 13 | cloudfiles = require('../lib/cloudfiles'), 14 | helpers = require('./helpers'); 15 | 16 | var testData = {}, 17 | client = helpers.createClient(), 18 | sampleData = fs.readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'fillerama.txt')).toString(); 19 | 20 | vows.describe('node-cloudfiles/containers').addBatch({ 21 | "The node-cloudfiles client": { 22 | "the createContainer() method": { 23 | "when creating a container": { 24 | topic: function () { 25 | client.createContainer(new (cloudfiles.Container)(client, { name: 'test_container'}), this.callback); 26 | }, 27 | "should return a valid container": function (err, container) { 28 | helpers.assertContainer(container); 29 | } 30 | }, 31 | "when creating a CDN-enabled container": { 32 | topic: function () { 33 | client.createContainer(new (cloudfiles.Container)(client, { 34 | name: 'test_cdn_container', 35 | cdnEnabled: true 36 | }), this.callback); 37 | }, 38 | "should return a valid cdn container": function (err, container) { 39 | helpers.assertCdnContainer(container); 40 | } 41 | } 42 | } 43 | } 44 | }).addBatch({ 45 | "The node-cloudfiles client": { 46 | "the createContainer() method": { 47 | "when creating a container that already exists": { 48 | topic: function () { 49 | client.createContainer(new (cloudfiles.Container)(client, { name: 'test_container' }), this.callback); 50 | }, 51 | "should return a valid container": function (err, container) { 52 | helpers.assertContainer(container); 53 | } 54 | }, 55 | }, 56 | "the getContainers() method": { 57 | "when requesting non-CDN containers": { 58 | topic: function () { 59 | client.getContainers(this.callback); 60 | }, 61 | "should return a list of containers": function (err, containers) { 62 | assert.isArray(containers); 63 | assert.equal(helpers.countTestContainers(containers),2); 64 | containers.forEach(function (container) { 65 | helpers.assertContainer(container); 66 | }); 67 | } 68 | }, 69 | "when requesting CDN containers": { 70 | topic: function () { 71 | client.getContainers(true, this.callback); 72 | }, 73 | "should return a list of cdn containers": function (err, containers) { 74 | assert.isArray(containers); 75 | assert.equal(helpers.countTestContainers(containers), 1); 76 | containers.forEach(function (container) { 77 | helpers.assertCdnContainer(container); 78 | }); 79 | } 80 | } 81 | }, 82 | "the getContainer() method": { 83 | "when requesting non-CDN container": { 84 | topic: function () { 85 | client.getContainer('test_container', this.callback); 86 | }, 87 | "should return a valid container": function (err, container) { 88 | helpers.assertContainer(container); 89 | testData.container = container; 90 | } 91 | }, 92 | "when requesting CDN container": { 93 | "with a valid CDN container": { 94 | topic: function () { 95 | client.getContainer('test_cdn_container', true, this.callback); 96 | }, 97 | "should return a valid cdn container": function (err, container) { 98 | helpers.assertCdnContainer(container); 99 | } 100 | }, 101 | "with an invalid CDN container": { 102 | topic: function () { 103 | client.getContainer('test_container', true, this.callback); 104 | }, 105 | "should respond with an error": function (err, container) { 106 | assert.isNotNull(err); 107 | } 108 | } 109 | } 110 | } 111 | } 112 | }).addBatch({ 113 | "The node-cloudfiles client": { 114 | "an instance of a Container object": { 115 | "the addFile() method": { 116 | topic: function () { 117 | var ustream = client.addFile('test_container', { 118 | remote: 'file1.txt', 119 | local: path.join(__dirname, '..', 'test', 'fixtures', 'fillerama.txt') 120 | }, function () { }); 121 | if (ustream) { 122 | ustream.on('end', this.callback) 123 | } 124 | }, 125 | "should raise the `end` event": function () { 126 | assert.isTrue(true); 127 | } 128 | } 129 | } 130 | } 131 | }).addBatch({ 132 | "The node-cloudfiles client": { 133 | "an instance of a Container object": { 134 | "the getFiles() method": { 135 | topic: function () { 136 | testData.container.getFiles(this.callback); 137 | }, 138 | "should response with a list of files": function (err, files) { 139 | assert.isArray(files); 140 | assert.lengthOf(files, 1); 141 | assert.isArray(testData.container.files); 142 | assert.lengthOf(testData.container.files, 1); 143 | } 144 | } 145 | } 146 | } 147 | }).addBatch({ 148 | "The node-cloudfiles client": { 149 | "an instance of a Container object": { 150 | "the getFiles(true) method": { 151 | topic: function () { 152 | testData.container.getFiles(true, this.callback); 153 | }, 154 | "should response with a list of files with content": function (err, files) { 155 | assert.isArray(files); 156 | assert.lengthOf(files, 1); 157 | assert.isArray(testData.container.files); 158 | assert.lengthOf(testData.container.files, 1); 159 | assert.isNotNull(files[0].local); 160 | } 161 | } 162 | } 163 | } 164 | }).addBatch({ 165 | "The node-cloudfiles client": { 166 | "an instance of a Container object": { 167 | "the getFiles(new RegExp(...)) method": { 168 | topic: function () { 169 | testData.container.getFiles(/^file/, this.callback); 170 | }, 171 | "should response with a list of files with content": function (err, files) { 172 | assert.isArray(files); 173 | assert.lengthOf(files, 1); 174 | assert.isArray(testData.container.files); 175 | assert.lengthOf(testData.container.files, 1); 176 | assert.isTrue(/^file/.test(files[0].name)); 177 | } 178 | } 179 | } 180 | } 181 | }).addBatch({ 182 | "The node-cloudfiles client": { 183 | "an instance of a Container object": { 184 | "the getFiles(new RegExp(...) with no matches) method": { 185 | topic: function () { 186 | testData.container.getFiles(/^no_matches/, this.callback); 187 | }, 188 | "should response with a empty list": function (err, files) { 189 | assert.isArray(files); 190 | assert.lengthOf(files, 0); 191 | assert.isArray(testData.container.files); 192 | assert.lengthOf(testData.container.files, 0); 193 | } 194 | } 195 | } 196 | } 197 | }).addBatch({ 198 | "The node-cloudfiles client": { 199 | "an instance of a Container object": { 200 | "the getFiles([filenames]) method": { 201 | topic: function () { 202 | testData.container.getFiles(['file1.txt'], this.callback); 203 | }, 204 | "should response with a list of files with content": function (err, files) { 205 | assert.isArray(files); 206 | assert.lengthOf(files, 1); 207 | assert.isArray(testData.container.files); 208 | assert.lengthOf(testData.container.files, 1); 209 | assert.equal(files[0].name, 'file1.txt'); 210 | } 211 | } 212 | } 213 | } 214 | }).addBatch({ 215 | "The node-cloudfiles client": { 216 | "an instance of a Container object": { 217 | "the getFiles([not-existing-filenames]) method": { 218 | topic: function () { 219 | testData.container.getFiles(['not-exists.txt'], this.callback); 220 | }, 221 | "should response with a error": function (err, files) { 222 | assert.isArray(err); 223 | assert.lengthOf(err, 1); 224 | assert.instanceOf(err[0], Error); 225 | } 226 | } 227 | } 228 | } 229 | }).addBatch({ 230 | "The node-cloudfiles client": { 231 | "an instance of a Container object": { 232 | "the removeFile() method": { 233 | topic: function () { 234 | testData.container.removeFile('file1.txt', this.callback); 235 | }, 236 | "should response with true": function (err, removed) { 237 | assert.isTrue(removed); 238 | } 239 | } 240 | } 241 | } 242 | }).export(module); 243 | -------------------------------------------------------------------------------- /lib/cloudfiles/core.js: -------------------------------------------------------------------------------- 1 | /* 2 | * core.js: Core functions for accessing Rackspace CloudFiles 3 | * 4 | * (C) 2010 Nodejitsu Inc. 5 | * MIT LICENSE 6 | * 7 | */ 8 | 9 | var http = require('http'), 10 | fs = require('fs'), 11 | path = require('path'), 12 | url = require('url'), 13 | async = require('async'), 14 | mime = require('mime'), 15 | request = require('request'), 16 | cloudfiles = require('../cloudfiles'), 17 | config = require('./config'), 18 | common = require('./common'); 19 | 20 | // 21 | // ### function createClient (options) 22 | // #### @options {Object} Options for this instance. 23 | // Creates a new instance of a Loggly client. 24 | // 25 | exports.createClient = function (options) { 26 | return new Cloudfiles(config.createConfig(options)); 27 | }; 28 | 29 | // 30 | // ### function Cloudfiles (config) 31 | // #### @config {loggly.Config} Configuration object for this instance. 32 | // Constructor function for the `Cloudfiles` object responsible for exposing 33 | // the core `node-cloudfiles` API methods. 34 | // 35 | var Cloudfiles = exports.Cloudfiles = function (config) { 36 | this.config = config; 37 | this.authorized = false; 38 | 39 | // 40 | // Create the cache path for this instance immediately. 41 | // 42 | common.statOrMkdirp(this.config.cache.path) 43 | }; 44 | 45 | // 46 | // ### function setAuth (callback) 47 | // #### @callback {function} Continuation to respond to when complete. 48 | // Authenticates node-cloudfiles with the options specified 49 | // in the Config object for this instance 50 | // 51 | Cloudfiles.prototype.setAuth = function (callback) { 52 | var authOptions = { 53 | uri: 'https://' + this.config.auth.host + '/v1.0', 54 | headers: { 55 | 'HOST': this.config.auth.host, 56 | 'X-AUTH-USER': this.config.auth.username, 57 | 'X-AUTH-KEY': this.config.auth.apiKey 58 | } 59 | }; 60 | 61 | var self = this; 62 | request(authOptions, function (err, res, body) { 63 | if (err) { 64 | return callback(err); 65 | } 66 | 67 | var statusCode = res.statusCode.toString(); 68 | if (Object.keys(common.failCodes).indexOf(statusCode) !== -1) { 69 | err = new Error('Rackspace Error (' + statusCode + '): ' + common.failCodes[statusCode]); 70 | return callback(err, res); 71 | } 72 | 73 | self.authorized = true; 74 | self.config.serverUrl = res.headers['x-server-management-url']; 75 | self.config.setStorageUrl(res.headers['x-storage-url']); 76 | self.config.cdnUrl = res.headers['x-cdn-management-url']; 77 | self.config.authToken = res.headers['x-auth-token']; 78 | self.config.storageToken = res.headers['x-storage-token'] 79 | 80 | callback(null, res, self.config); 81 | }); 82 | }; 83 | 84 | // 85 | // ### function getContainers ([cdn,] callback) 86 | // #### @cdn {boolean} Value indicating if CDN containers should be returned 87 | // #### @callback {function} Continuation to respond to when complete. 88 | // Gets all Rackspace Cloudfiles containers for this instance. 89 | // 90 | Cloudfiles.prototype.getContainers = function () { 91 | var self = this, 92 | args = Array.prototype.slice.call(arguments), 93 | callback = (typeof(args[args.length - 1]) === 'function') && args.pop(), 94 | isCdn = args.length > 0 && (typeof(args[args.length - 1]) === 'boolean') && args.pop(), 95 | url = isCdn ? this.cdnUrl(true) : this.storageUrl(true); 96 | 97 | common.rackspace(url, this, callback, function (body) { 98 | var results = [], 99 | containers = JSON.parse(body); 100 | 101 | containers.forEach(function (container) { 102 | if (isCdn) { 103 | // 104 | // The cdn properties are normaly set in response headers 105 | // when requesting single cdn containers 106 | // 107 | container.cdnEnabled = container.cdn_enabled == "true"; 108 | container.logRetention = container.log_retention == "true"; 109 | container.cdnUri = container.cdn_uri; 110 | container.cdnSslUri = container.cdn_ssl_uri; 111 | } 112 | 113 | results.push(new (cloudfiles.Container)(self, container)); 114 | }); 115 | 116 | callback(null, results); 117 | }); 118 | }; 119 | 120 | // 121 | // ### function getContainer (containerName, [cdn,] callback) 122 | // #### @containerName {string} Name of the container to return 123 | // #### @cdn {boolean} Value indicating if this is a CDN container. 124 | // #### @callback {function} Continuation to respond to when complete. 125 | // Responds with the Rackspace Cloudfiles container for the specified 126 | // `containerName`. 127 | // 128 | Cloudfiles.prototype.getContainer = function () { 129 | var self = this, 130 | args = Array.prototype.slice.call(arguments), 131 | callback = (typeof(args[args.length - 1]) === 'function') && args.pop(), 132 | isCdn = args.length > 0 && (typeof(args[args.length - 1]) === 'boolean') && args.pop(), 133 | containerName = args.pop(), 134 | containerOptions; 135 | 136 | containerOptions = { 137 | method: 'HEAD', 138 | uri: isCdn ? this.cdnUrl(containerName) : this.storageUrl(containerName), 139 | cdn: isCdn, 140 | client: this 141 | } 142 | 143 | common.rackspace(containerOptions, callback, function (body, res) { 144 | var container = { 145 | name: containerName, 146 | count: new Number(res.headers['x-container-object-count']), 147 | bytes: new Number(res.headers['x-container-bytes-used']) 148 | }; 149 | 150 | if (isCdn) { 151 | container.cdnUri = res.headers['x-cdn-uri']; 152 | container.cdnSslUri = res.headers['x-cdn-ssl-uri']; 153 | container.cdnEnabled = res.headers['x-cdn-enabled'].toLowerCase() == "true"; 154 | container.ttl = parseInt(res.headers['x-ttl']); 155 | container.logRetention = res.headers['x-log-retention'].toLowerCase() == "true"; 156 | 157 | delete container.count; 158 | delete container.bytes; 159 | } 160 | 161 | callback(null, new (cloudfiles.Container)(self, container)); 162 | }); 163 | }; 164 | 165 | // 166 | // ### function createContainer (container, callback) 167 | // #### @container {string|Container} Container to create in Rackspace Cloudfiles. 168 | // #### @callback {function} Continuation to respond to when complete. 169 | // Creates the specified `container` in the Rackspace Cloudfiles associated 170 | // with this instance. 171 | // 172 | Cloudfiles.prototype.createContainer = function (container, callback) { 173 | var self = this, 174 | containerName = container instanceof cloudfiles.Container ? container.name : container; 175 | 176 | common.rackspace('PUT', this.storageUrl(containerName), this, callback, function (body, res) { 177 | if (typeof container.cdnEnabled !== 'undefined' && container.cdnEnabled) { 178 | container.ttl = container.ttl || self.config.cdn.ttl; 179 | container.logRetention = container.logRetention || self.config.cdn.logRetention; 180 | 181 | var cdnOptions = { 182 | uri: self.cdnUrl(containerName), 183 | method: 'PUT', 184 | client: self, 185 | headers: { 186 | 'X-TTL': container.ttl, 187 | 'X-LOG-RETENTION': container.logRetention 188 | } 189 | }; 190 | 191 | common.rackspace(cdnOptions, callback, function (body, res) { 192 | container.cdnUri = res.headers['x-cdn-uri']; 193 | container.cdnSslUri = res.headers['x-cdn-ssl-uri']; 194 | callback(null, new (cloudfiles.Container)(self, container)); 195 | }); 196 | } 197 | else { 198 | callback(null, new (cloudfiles.Container)(self, container)); 199 | } 200 | }); 201 | }; 202 | 203 | // 204 | // ### function destroyContainer (container, callback) 205 | // #### @container {string} Name of the container to destroy 206 | // #### @callback {function} Continuation to respond to when complete. 207 | // Destroys the specified `container` and all files in it. 208 | // 209 | Cloudfiles.prototype.destroyContainer = function (container, callback) { 210 | var self = this; 211 | this.getFiles(container, function (err, files) { 212 | if (err) { 213 | return callback(err); 214 | } 215 | 216 | function deleteContainer (err) { 217 | if (err) { 218 | return callback(err); 219 | } 220 | 221 | common.rackspace('DELETE', self.storageUrl(container), self, callback, function (body, res) { 222 | callback(null, true); 223 | }); 224 | } 225 | 226 | function destroyFile (file, next) { 227 | file.destroy(next); 228 | } 229 | 230 | if (files.length === 0) { 231 | return deleteContainer(); 232 | } 233 | 234 | async.forEach(files, destroyFile, deleteContainer); 235 | }); 236 | }; 237 | 238 | Cloudfiles.prototype.getFiles = function (container, download, callback) { 239 | var self = this; 240 | 241 | // 242 | // Download is optional argument 243 | // And can be only: true, false, [array of files] 244 | // 245 | // Also download can be omitted: (...).getFiles(container, callback); 246 | // In this case second argument will be a function 247 | // 248 | if (typeof download === 'function' && !(download instanceof RegExp)) { 249 | callback = download; 250 | download = false; 251 | } 252 | 253 | common.rackspace(this.storageUrl(container, true), this, callback, function (body) { 254 | var files = JSON.parse(body); 255 | 256 | // If download == false or wasn't defined 257 | if (!download) { 258 | var results = files.map(function (file) { 259 | file.container = container; 260 | return new (cloudfiles.StorageObject)(self, file); 261 | }); 262 | 263 | callback(null, results); 264 | return; 265 | } 266 | 267 | var batch; 268 | 269 | if (download instanceof RegExp || download == true) { 270 | // If download == true 271 | // Download all files 272 | if (download !== true) { 273 | files = files.filter(function (file) { 274 | return download.test(file.name); 275 | }); 276 | } 277 | 278 | // Create a batch 279 | batch = files.map(function (file) { 280 | return function (callback) { 281 | self.getFile(container, file.name, callback); 282 | } 283 | }); 284 | } 285 | else if (Array.isArray(download)) { 286 | // Go through all files that we've asked to download 287 | batch = download.map(function (file) { 288 | var exists = files.some(function (item) { 289 | return item.name == file 290 | }); 291 | 292 | // If file exists - get it 293 | // If not report about error 294 | return exists ? 295 | function (callback) { 296 | self.getFile(container, file, callback); 297 | } : 298 | function (callback) { 299 | callback(Error('File : ' + file + ' doesn\'t exists')); 300 | }; 301 | }); 302 | } 303 | else { 304 | callback(Error('"download" argument can be only boolean, array or regexp')); 305 | } 306 | 307 | // Run batch 308 | common.runBatch(batch, callback); 309 | }); 310 | }; 311 | 312 | Cloudfiles.prototype.getFile = function (container, filename, callback) { 313 | var self = this, 314 | containerPath = path.join(this.config.cache.path, container), 315 | cacheFile = path.join(containerPath, filename), 316 | options; 317 | 318 | common.statOrMkdirp(containerPath); 319 | 320 | var lstream = fs.createWriteStream(cacheFile), 321 | rstream, 322 | options; 323 | 324 | options = { 325 | method: 'GET', 326 | client: self, 327 | uri: self.storageUrl(container, filename), 328 | download: lstream 329 | }; 330 | 331 | rstream = common.rackspace(options, callback, function (body, res) { 332 | var file = { 333 | local: cacheFile, 334 | container: container, 335 | name: filename, 336 | bytes: res.headers['content-length'], 337 | etag: res.headers['etag'], 338 | last_modified: res.headers['last-modified'], 339 | content_type: res.headers['content-type'] 340 | }; 341 | 342 | callback(null, new (cloudfiles.StorageObject)(self, file)); 343 | }); 344 | }; 345 | 346 | // 347 | // options 348 | // remote 349 | // local 350 | // stream 351 | // mime 352 | // 353 | Cloudfiles.prototype.addFile = function (container, options, callback) { 354 | if (typeof options === 'function' && !callback) { 355 | callback = options; 356 | options = {}; 357 | } 358 | if (!options.remote) { 359 | return callback(new Error('.remote is required to addFile')); 360 | } 361 | 362 | var lstream, 363 | addOptions, 364 | size; 365 | 366 | if (options.local) { 367 | lstream = fs.createReadStream(options.local, options.fs); 368 | options.headers = options.headers || {}; 369 | options.headers['content-length'] = fs.statSync(options.local).size; 370 | } 371 | else if (options.stream) { 372 | lstream = options.stream; 373 | } 374 | 375 | if (!lstream) { 376 | return callback(new Error('.local or .stream is required to addFile.')); 377 | } 378 | 379 | addOptions = { 380 | method: 'PUT', 381 | client: this, 382 | upload: lstream, 383 | uri: this.storageUrl(container, options.remote), 384 | headers: options.headers || {} 385 | }; 386 | 387 | if (options.headers && !options.headers['content-type']) { 388 | options.headers['content-type'] = mime.lookup(options.remote); 389 | } 390 | 391 | return common.rackspace(addOptions, callback, function (body, res) { 392 | callback(null, true); 393 | }); 394 | }; 395 | 396 | // 397 | // ### function destroyFile (container, file, callback) 398 | // #### @container {string} Name of the container to destroy the file in 399 | // #### @file {string} Name of the file to destroy. 400 | // #### @callback {function} Continuation to respond to when complete. 401 | // Destroys the `file` in the specified `container`. 402 | // 403 | Cloudfiles.prototype.destroyFile = function (container, file, callback) { 404 | common.rackspace('DELETE', this.storageUrl(container, file), this, callback, function (body, res) { 405 | callback(null, true); 406 | }); 407 | }; 408 | 409 | // 410 | // ### function storageUrl (arguments) 411 | // #### @arguments {Array} Lists of arguments to convert into a storage url. 412 | // Helper method that concats the string params into a url to request against 413 | // the authenticated node-cloudfiles storageUrl. 414 | // 415 | Cloudfiles.prototype.storageUrl = function () { 416 | var args = Array.prototype.slice.call(arguments), 417 | json = (typeof(args[args.length - 1]) === 'boolean') && args.pop(); 418 | 419 | return [this.config.storageUrl].concat(args).join('/') + (json ? '?format=json' : ''); 420 | }; 421 | 422 | // 423 | // ### function cdnUrl (arguments) 424 | // #### @arguments {Array} Lists of arguments to convert into a cdn url. 425 | // Helper method that concats the string params into a url 426 | // to request against the authenticated node-cloudfiles cdnUrl. 427 | // 428 | Cloudfiles.prototype.cdnUrl = function () { 429 | var args = Array.prototype.slice.call(arguments), 430 | json = (typeof(args[args.length - 1]) === 'boolean') && args.pop(); 431 | 432 | return [this.config.cdnUrl].concat(args).join('/') + (json ? '?format=json' : ''); 433 | }; 434 | --------------------------------------------------------------------------------