├── CNAME ├── docs ├── robots.txt ├── _config.yml ├── css │ ├── common.styl │ ├── default.styl │ ├── api.styl │ └── syntax.styl ├── _layouts │ └── default.html ├── _includes │ └── header.html ├── index.md └── development.md ├── index.js ├── .hgignore ├── .gitignore ├── jake.sh ├── lib ├── provider │ ├── aws │ │ ├── index.js │ │ ├── connection.js │ │ ├── blob │ │ │ ├── blob.js │ │ │ └── container.js │ │ └── authentication.js │ └── google │ │ ├── index.js │ │ ├── connection.js │ │ └── authentication.js ├── base │ ├── blob │ │ ├── index.js │ │ ├── container.js │ │ └── blob.js │ ├── authentication.js │ └── connection.js ├── index.js ├── utils.js ├── errors.js ├── config.js ├── request.js └── stream.js ├── .npmignore ├── docs_api ├── templates │ └── jsdoc │ │ ├── allclasses.tmpl │ │ ├── symbol.tmpl │ │ ├── header.tmpl │ │ ├── index.tmpl │ │ ├── allfiles.tmpl │ │ └── publish.js ├── plugins │ └── markdown.js └── jsdoc-conf.js ├── CHANGES.md ├── package.json ├── LICENSE.txt ├── COPYING.txt ├── test ├── live │ ├── container.js │ ├── utils.js │ └── connection.js ├── core.test.js └── live.test.js ├── TODO.md ├── dev └── jshint.json ├── README.md └── Jakefile /CNAME: -------------------------------------------------------------------------------- 1 | sunnyjs.org -------------------------------------------------------------------------------- /docs/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib'); 2 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | \.git 2 | \.hg 3 | 4 | \.DS_Store 5 | \.project 6 | node_modules 7 | npm-debug\.log 8 | 9 | local 10 | docs_html 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | \.git 2 | \.hg 3 | 4 | \.DS_Store 5 | \.project 6 | node_modules 7 | npm-debug\.log 8 | 9 | local 10 | docs_html 11 | -------------------------------------------------------------------------------- /jake.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Simple wrapper for locally installed Jake. 4 | # Use: ./jake.sh 5 | 6 | ./node_modules/jake/bin/cli.js $@ 7 | -------------------------------------------------------------------------------- /lib/provider/aws/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name provider.aws 3 | */ 4 | /** @ignore */ 5 | module.exports.Authentication = require("./authentication").Authentication; 6 | -------------------------------------------------------------------------------- /lib/provider/google/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name provider.google 3 | */ 4 | /** @ignore */ 5 | module.exports.Authentication = require("./authentication").Authentication; 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | \.DS_Store 2 | \.project 3 | npm-debug\.log 4 | 5 | docs*/** 6 | local/** 7 | node_modules/** 8 | test/** 9 | 10 | jake\.sh 11 | Jakefile 12 | TODO\.md 13 | -------------------------------------------------------------------------------- /lib/base/blob/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name base.blob 3 | */ 4 | /** @ignore */ 5 | module.exports.Container = require("./container").Container; 6 | module.exports.Blob = require("./blob").Blob; 7 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** @ignore */ 2 | module.exports.Configuration = require("./config").Configuration; 3 | 4 | // Extra JsDoc namespaces to add. 5 | /** 6 | * @name base 7 | */ 8 | /** 9 | * @name provider 10 | */ 11 | -------------------------------------------------------------------------------- /docs_api/templates/jsdoc/allclasses.tmpl: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | version: 0.0.6 2 | safe: false 3 | auto: false 4 | server: false 5 | 6 | baseurl: http://sunnyjs.org/ 7 | source: . 8 | destination: ../docs_html 9 | plugins: ./_plugins 10 | exclude: ["css"] 11 | 12 | future: true 13 | lsi: false 14 | pygments: true 15 | markdown: maruku 16 | permalink: date 17 | -------------------------------------------------------------------------------- /docs/css/common.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * Common functions and definitions. 3 | */ 4 | vendor(prop, args) 5 | -webkit-{prop} args 6 | -moz-{prop} args 7 | {prop} args 8 | 9 | border-radius() 10 | vendor('border-radius', arguments) 11 | 12 | border-radius-part(topBottom, leftRight, val) 13 | -webkit-border-{topBottom}-{leftRight}-radius: val 14 | -moz-border-radius-{topBottom}{leftRight}: val 15 | border-{topBottom}-{leftRight}-radius: val 16 | 17 | box-shadow(hor, vert, blur, color) 18 | vendor('box-shadow', arguments) 19 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## v.0.0.6 4 | * Updated docs and env for `authUrl`. 5 | 6 | ## v.0.0.5 7 | * Add support for utf8 blob key (HTTP path) names for AWS. Google still has 8 | problems with this, so throws an explicit error instead. (Patches for utf8 9 | blob names in Google are welcome!) 10 | * Fix failing Google tests. 11 | 12 | ## v.0.0.4 13 | * Fix canonical headers order bug. 14 | 15 | ## v.0.0.3 16 | * Fix header/metdata options bug for GET/PUT to/from file operations. 17 | 18 | ## v.0.0.2 19 | * Fix ReadStream status code bug. 20 | 21 | ## v.0.0.1 22 | * Initial release. 23 | * Basic support for Blob/Container operations on Amazon S3 and Google Storage. 24 | * Live test suite for all cloud operations. 25 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sunny.js - {{ page.title }} - v{{ site.version }} 8 | 10 | 12 | 13 | 14 |
15 | {% include header.html %} 16 |
17 | {{ content }} 18 |
19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sunny", 3 | "description": "Multi-cloud datastore client.", 4 | "version": "0.0.6", 5 | "author": "Ryan Roemer ", 6 | "url": "http://sunnyjs.org/", 7 | "main": "index", 8 | "engines": { 9 | "node": ">=0.4.5" 10 | }, 11 | "dependencies": { 12 | "xml2js": "0.1.9" 13 | }, 14 | "devDependencies": { 15 | "jake": "0.1.19", 16 | "findit": "0.1.1", 17 | "jshint": "0.9.1", 18 | "nodeunit": "0.7.4", 19 | "node-uuid": "1.2.0", 20 | "showdown": "0.0.1", 21 | "async": "0.1.22", 22 | "stylus": "0.15.1", 23 | "cakepop": "0.1.0" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git://github.com/ryan-roemer/node-sunny.git" 28 | }, 29 | "keywords": [ 30 | "sunny", "cloud", 31 | "amazon", "aws", "amazon web services", "s3", "simple storage service", 32 | "google storage for developers" 33 | ] 34 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Ryan Roemer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /docs_api/templates/jsdoc/symbol.tmpl: -------------------------------------------------------------------------------- 1 | 2 | {+data.name+} 3 | {+data.memberOf+} 4 | {+data.isStatic+} 5 | {+data.isa+} 6 | {+data.desc+} 7 | {+data.classDesc+} 8 | 9 | 10 | 11 | {+method.name+} 12 | {+method.memberOf+} 13 | {+method.isStatic+} 14 | {+method.desc+} 15 | 16 | 17 | {+param.type+} 18 | {+param.name+} 19 | {+param.desc+} 20 | {+param.defaultValue+} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {+property.name+} 29 | {+property.memberOf+} 30 | {+property.isStatic+} 31 | {+property.desc+} 32 | {+property.type+} 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /lib/provider/google/connection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Google Connection. 3 | */ 4 | 5 | /** @ignore */ 6 | (function () { 7 | var util = require('util'), 8 | utils = require("../../utils"), 9 | BaseConnection = require("../aws/connection").Connection, 10 | GoogleConnection; 11 | 12 | /** 13 | * Connection class. 14 | * 15 | * @param {Authentication} auth Authentication object. 16 | * @extends provider.aws.Connection 17 | * @exports GoogleConnection as provider.google.Connection 18 | * @constructor 19 | */ 20 | GoogleConnection = function (auth) { 21 | var self = this; 22 | 23 | BaseConnection.apply(self, arguments); 24 | 25 | // Header prefixes. 26 | self._HEADER_PREFIX = "x-goog-"; 27 | self._METADATA_PREFIX = "x-goog-meta-"; 28 | 29 | // Update variables. 30 | self._ERRORS.CONTAINER_OTHER_OWNER.attrs = { 31 | statusCode: 409, 32 | errorCode: "BucketNameUnavailable" 33 | }; 34 | self._ERRORS.CONTAINER_ALREADY_OWNED_BY_YOU.attrs = { 35 | statusCode: 409, 36 | errorCode: "BucketAlreadyOwnedByYou" 37 | }; 38 | }; 39 | 40 | util.inherits(GoogleConnection, BaseConnection); 41 | 42 | module.exports.Connection = GoogleConnection; 43 | }()); 44 | -------------------------------------------------------------------------------- /docs_api/plugins/markdown.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Markdown plugin for JsDoc Toolkit 2. 3 | * 4 | * Idea from: http://code.google.com/p/jsdoc-toolkit/issues/detail?id=86 5 | * 6 | * Note: Requires showdown to be installed in a well-known node_modules 7 | * location (Showdown npm installs, but is pure JavaScript, and thus can be 8 | * used with Rhino for JsDoc 2). 9 | */ 10 | (function () { 11 | // Include showdown 12 | try { 13 | load("node_modules/showdown/src/showdown.js"); 14 | LOG.inform("Showdown loaded."); 15 | } catch(e){ 16 | LOG.error("Showdown not installed. Try: 'npm install showdown'."); 17 | } 18 | 19 | var SHOWDOWN = new Showdown.converter(); 20 | 21 | /** 22 | * Markdown processor. 23 | * @class 24 | */ 25 | var markdownPlugin = { 26 | // Full source in comment.src 27 | //onDocCommentSrc: function (comment) { 28 | //}, 29 | onDocCommentTags: function (comment) { 30 | var prop, 31 | tag; 32 | 33 | // Process markdown on all description tags. 34 | for (prop in comment.tags) { 35 | tag = comment.tags[prop]; 36 | if (comment.tags.hasOwnProperty(prop) && tag.desc !== '' && 37 | (tag.title === 'desc' || tag.title === 'fileOverview')) { 38 | tag.desc = SHOWDOWN.makeHtml(tag.desc); 39 | } 40 | } 41 | } 42 | }; 43 | 44 | JSDOC.PluginManager.registerPlugin("JSDOC.markdown", markdownPlugin); 45 | }()); 46 | -------------------------------------------------------------------------------- /COPYING.txt: -------------------------------------------------------------------------------- 1 | # Sunny 2 | 3 | Sunny is licensed under the MIT license (see "LICENSE.txt"). 4 | 5 | # JsDoc Toolkit 6 | 7 | Template code has been copied and modified from JsDoc Toolkit source. JsDoc 8 | Toolkit is licensed under the X11/MIT license, reproduced from source here: 9 | 10 | JsDoc Toolkit is Copyright (c)2009 Michael Mathews 11 | 12 | This program is free software; you can redistribute it and/or 13 | modify it under the terms below. 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining 16 | a copy of this software and associated documentation files (the 17 | "Software"), to deal in the Software without restriction, including 18 | without limitation the rights to use, copy, modify, merge, publish, 19 | distribute, sublicense, and/or sell copies of the Software, and to 20 | permit persons to whom the Software is furnished to do so, subject to 21 | the following conditions: The above copyright notice and this 22 | permission notice must be included in all copies or substantial 23 | portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 27 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 28 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 29 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 30 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 31 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | -------------------------------------------------------------------------------- /docs/_includes/header.html: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | Site Docs Header 3 | ---------------- 4 | **Note**: Need to keep sister jsdoc template file at: 5 | "docs_api/templates/jsdoc/header.tmpl" in sync with this template. 6 | {% endcomment %} 7 | 8 | Fork me on GitHub 9 | 10 | 24 | 25 | 41 | -------------------------------------------------------------------------------- /docs_api/templates/jsdoc/header.tmpl: -------------------------------------------------------------------------------- 1 | {! 2 | /** 3 | * API Docs Header 4 | * --------------- 5 | * **Note**: Need to keep sister jekyll template file at: 6 | * "docs/_includes/header.html" in sync with this template. 7 | */ 8 | var SITE_BASE = ("../" + (Link.base || ".")).replace(/\/*$/, ''); 9 | !} 10 | 11 | Fork me on GitHub 12 | 13 | 27 | 28 | 44 | -------------------------------------------------------------------------------- /docs_api/jsdoc-conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JsDoc configuration. 3 | */ 4 | 5 | // Hack this up a bit, as the code is eval()'ed in as: 6 | // eval("JSDOC.conf = " + IO.readFile(JSDOC.opt.c)); 7 | // 8 | // We thus use a closure here to jump in before the parsing begins. 9 | (function () { 10 | // Define real JsDoc options. 11 | var opt = { 12 | // Source files. (From command line). 13 | //_: [], 14 | 15 | // Document all functions. 16 | a: true, 17 | 18 | // Document @private functions. 19 | p: false, 20 | 21 | // Extra variables (available as JSDOC.opt.D.). 22 | D: { 23 | generatedBy: "Ryan Roemer", 24 | copyright: "2011" 25 | }, 26 | 27 | // Output. (From command line). 28 | //d: "docs_html", 29 | 30 | // Recursion. 31 | r: 5, 32 | 33 | // Templates. 34 | t: "docs_api/templates/jsdoc", 35 | 36 | // Plugins. 37 | plugins: "docs_api/plugins" 38 | }; 39 | 40 | // Get the version number out of package.json 41 | var pkg = null; 42 | eval("pkg = " + IO.readFile("package.json")); 43 | opt.D.version = pkg.version || null; 44 | 45 | // Load all our custom plugins (before) the parsing begins. 46 | if (opt.plugins) { 47 | var plugins, 48 | plugin, 49 | key; 50 | 51 | LOG.inform("Found custom plugin directory: " + opt.plugins); 52 | LOG.inform("Loading modules:"); 53 | 54 | plugins = IO.ls(opt.plugins); 55 | for (key in plugins) { 56 | if (plugins.hasOwnProperty(key)) { 57 | plugin = plugins[key]; 58 | LOG.inform(" * " + plugin); 59 | load(plugin); 60 | } 61 | } 62 | } 63 | 64 | // Now pass through the options. 65 | return opt; 66 | }()); 67 | 68 | 69 | -------------------------------------------------------------------------------- /docs/css/default.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * Site (jekyll) styles. 3 | */ 4 | @import "common" 5 | 6 | body 7 | font-family: 'Trebuchet MS', Trebuchet, sans-serif 8 | font-size: 13px 9 | color: #333333 10 | 11 | link-color = #f09000 12 | a 13 | color: link-color 14 | text-decoration: none 15 | font-weight: bold 16 | &:hover 17 | text-decoration: underline 18 | 19 | h1, 20 | h2, 21 | h3 22 | font-weight: bold 23 | text-shadow: #cccccc 0.1em 0.1em 0.2em 24 | 25 | h1 26 | font-size: 28px 27 | 28 | h2 29 | font-size: 24px 30 | margin-left: 5px 31 | 32 | h3 33 | font-size: 20px 34 | margin-left: 10px 35 | 36 | #container 37 | margin: 0 auto 38 | width: 550px 39 | 40 | #header 41 | margin: 0 auto 42 | #title 43 | margin-bottom: 15px 44 | text-align: center 45 | text-shadow: #cfcfcf 0.1em 0.1em 0.1em 46 | font-size: 32px 47 | font-weight: bold 48 | color: #333333 49 | #menu 50 | padding: 0px 51 | padding-bottom: 30px 52 | margin-bottom: 10px 53 | ul 54 | margin-left: 55px 55 | display: block 56 | list-style: none 57 | li 58 | display: inline 59 | a 60 | display: block 61 | color: #ffffff 62 | background-color: #333333 63 | border: 1px solid #cccccc 64 | border-radius(3px) 65 | box-shadow(4px, 6px, 10px, #999999) 66 | margin: 0 2px 67 | padding: 4px 15px 68 | float: left 69 | font-weight: bold 70 | &:hover 71 | text-decoration: none 72 | color: #000000 73 | background-color: #ffffff 74 | border: 1px solid #333333 75 | 76 | pre 77 | background-color: #f9f9f9 78 | padding: 10px 15px 79 | margin-bottom: 15px 80 | border: 1px solid #333333 81 | border-radius(3px) 82 | box-shadow(2px, 5px, 5px, #999999) 83 | -------------------------------------------------------------------------------- /lib/provider/google/authentication.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Google Storage for Developers Authentication. 3 | */ 4 | 5 | /** @ignore */ 6 | (function () { 7 | // Requires. 8 | var http = require('http'), 9 | crypto = require('crypto'), 10 | parse = require('url').parse, 11 | util = require('util'), 12 | utils = require("../../utils"), 13 | AwsAuthentication = require("../aws").Authentication, 14 | GoogleAuthentication; 15 | 16 | /** 17 | * Google Authentication class. 18 | * 19 | * Google implements the AWS S3 API, so we just reuse everything. 20 | * 21 | * @param {Object} options Options object. 22 | * @config {string} account Account name. 23 | * @config {string} secretKey Secret key. 24 | * @config {string} [ssl=false] Use SSL? 25 | * @config {string} [authUrl] Authentication URL. 26 | * @config {number} [timeout] HTTP timeout in seconds. 27 | * @extends provider.aws.Authentication 28 | * @exports GoogleAuthentication as provider.google.Authentication 29 | * @constructor 30 | */ 31 | GoogleAuthentication = function (options) { 32 | // Patch GSFD-specific options. 33 | options = utils.extend(options); 34 | options.authUrl = options.authUrl || "commondatastorage.googleapis.com"; 35 | 36 | // Call superclass. 37 | AwsAuthentication.call(this, options); 38 | 39 | this._CUSTOM_HEADER_PREFIX = "x-goog"; 40 | this._CUSTOM_HEADER_RE = /^x-goog-/i; 41 | this._SIGNATURE_ID = "GOOG1"; 42 | this._CONN_CLS = require("./connection").Connection; 43 | }; 44 | 45 | util.inherits(GoogleAuthentication, AwsAuthentication); 46 | 47 | /** Test provider (Google Storage). */ 48 | GoogleAuthentication.prototype.isGoogle = function () { 49 | return true; 50 | }; 51 | 52 | module.exports.Authentication = GoogleAuthentication; 53 | }()); 54 | -------------------------------------------------------------------------------- /docs_api/templates/jsdoc/index.tmpl: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Sunny.js - API 10 | <if test="JSDOC.opt.D.version"> 11 | - v{+JSDOC.opt.D.version+} 12 | </if> 13 | 14 | 15 | 17 | 19 | 20 | 21 | 22 |
23 | {+includeTmpl("header.tmpl")+} 24 | 25 |
26 | {! /*+publish.classesIndex+*/ !} 27 |
28 | 29 |
30 |

31 | Classes 32 | 33 | - v{+JSDOC.opt.D.version+} 34 | 35 |

36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 51 | 54 | 55 | 56 |
Class Summary
ClassDescription
49 | {+(new Link().toSymbol(item.alias))+} 50 | 52 | {+resolveLinks(summarize(item.desc))+} 53 |
57 |
58 |
59 | 60 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Utilties 3 | */ 4 | 5 | (function () { 6 | /** 7 | * @class 8 | * @export utils as utils 9 | */ 10 | var utils = { 11 | /** 12 | * Translate buffer of arrays / strings to single string. 13 | */ 14 | bufToStr: function (bufs, encodingSrc, encodingDest) { 15 | var results = []; 16 | 17 | encodingSrc = encodingSrc || null; 18 | encodingDest = encodingDest || 'utf8'; 19 | 20 | bufs.forEach(function (buf) { 21 | // Start with assumption we have a correctly encoded string. 22 | var bufTrans = buf; 23 | 24 | // If have a string of wrong encoding, make back to a buffer. 25 | if (typeof buf === 'string' && encodingDest !== encodingSrc) { 26 | buf = new Buffer(buf, encodingSrc); 27 | } 28 | 29 | // If buffer, make a string. 30 | if (typeof buf !== 'string') { 31 | bufTrans = buf.toString(encodingDest); 32 | } 33 | 34 | bufs.push(bufTrans); 35 | }); 36 | 37 | return bufs.join(''); 38 | }, 39 | 40 | /** 41 | * Extract headers, cloudHeaders, metadata properties into new object. 42 | */ 43 | extractMeta: function (obj) { 44 | obj = obj || {}; 45 | return { 46 | headers: obj.headers || {}, 47 | cloudHeaders: obj.cloudHeaders || {}, 48 | metadata: obj.metadata || {} 49 | }; 50 | }, 51 | 52 | /** 53 | * Create a new object with objects merged. 54 | * 55 | * @exports extend as utils.extend 56 | */ 57 | extend: function () { 58 | var objs = Array.prototype.slice.call(arguments, 0), 59 | merged = {}; 60 | 61 | objs.forEach(function (obj) { 62 | var keys, 63 | key, 64 | i, 65 | len; 66 | 67 | if (!obj) { 68 | return; 69 | } 70 | 71 | keys = Object.keys(obj); 72 | for (i = 0, len = keys.length; i < len; i += 1) { 73 | key = keys[i]; 74 | merged[key] = obj[key]; 75 | } 76 | }); 77 | 78 | return merged; 79 | } 80 | }; 81 | 82 | module.exports = utils; 83 | }()); 84 | -------------------------------------------------------------------------------- /docs/css/api.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * API (jsdoc-toolkit) styles. 3 | */ 4 | @import "common" 5 | 6 | // Variables 7 | table-border = 1px #333333 solid 8 | 9 | // Mixed Styles 10 | #index hr, 11 | #content hr 12 | border: none 0 13 | border-top: 1px solid #7F8FB1 14 | height: 1px 15 | 16 | .summaryTable caption, 17 | div.sectionTitle 18 | background-color: #cccccc 19 | color: #333333 20 | font-size: 130% 21 | text-align: left 22 | padding: 2px 6px 2px 6px 23 | border: table-border 24 | 25 | div.sectionTitle 26 | margin-bottom: 14px 27 | box-shadow(2px, 5px, 5px, #999999) 28 | 29 | div.fixedFont, 30 | .summaryTable td.attributes 31 | line-height: 15px 32 | font-family: "Courier New",Courier,monospace 33 | font-size: 16px 34 | margin-bottom: 10px 35 | 36 | // Style trees 37 | #index 38 | ul.classList 39 | position: fixed; 40 | top: 5px; 41 | right: 5px; 42 | margin: 0; 43 | padding: 10px; 44 | background: rgba(255, 255, 255, 0.3); 45 | border: 1px solid #dddddd; 46 | border-radius(5px); 47 | box-shadow(0px, 4px, 4px, rgba(34,34,34,0.1)) 48 | text-align: left; 49 | list-style: none 50 | 51 | .details, 52 | .description 53 | h1 54 | font-size: 18px 55 | h2, .detailList .heading 56 | font-size: 16px 57 | h3 58 | font-size: 14px 59 | 60 | div.sectionTitle 61 | margin-bottom: 8px 62 | 63 | .detailList 64 | .heading 65 | font-weight: bold 66 | margin-left: 5px 67 | margin-bottom: 5px 68 | font-size: 16px 69 | text-shadow: #cccccc 0.1em 0.1em 0.2em 70 | dt 71 | margin-left: 20px 72 | line-height: 15px 73 | margin-left: 20px 74 | dt, dd 75 | .fixedFont 76 | font-style: italic 77 | font-family: "Courier New",Courier,monospace 78 | 79 | .summaryTable 80 | margin-bottom: 18px 81 | box-shadow(2px, 5px, 5px, #999999) 82 | 83 | thead 84 | display: none 85 | 86 | td 87 | vertical-align: top 88 | border-bottom: table-border 89 | border-right: table-border 90 | padding: 5px 91 | 92 | p 93 | margin-top: 0px 94 | padding-top: 0px 95 | 96 | &.attributes, &.fileName, &.className 97 | border-left: table-border 98 | width: 100px 99 | text-align: right 100 | font-size: 13px 101 | 102 | &.nameDescription, div.fixedFont 103 | text-align: left 104 | font-size: 13px 105 | -------------------------------------------------------------------------------- /test/live/container.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Live Container tests. 3 | */ 4 | 5 | /** 6 | * @name test.live.container 7 | */ 8 | (function () { 9 | var assert = require('assert'), 10 | async = require('async'), 11 | utils = require("./utils"), 12 | Tests; 13 | 14 | /** 15 | * @exports Tests as test.live.container.Tests 16 | * @namespace 17 | */ 18 | Tests = {}; 19 | 20 | Tests["Connection (w/ 1 Blob)"] = utils.createTestSetup([ 21 | "blob001.txt" 22 | ], { 23 | "DELETE non-empty container.": function (test, opts) { 24 | var self = this, 25 | request = self.container.del(); 26 | 27 | test.expect(1); 28 | request.on('error', function (err) { 29 | test.deepEqual(err.isNotEmpty(), true); 30 | test.done(); 31 | }); 32 | request.on('end', function (results) { 33 | test.ok(false, "Should not have completion. Got: " + results); 34 | test.done(); 35 | }); 36 | request.end(); 37 | } 38 | }); 39 | 40 | Tests["Container (w/ Blobs)"] = utils.createTestSetup([ 41 | "foo/blob001.txt", 42 | "foo/blob002.txt", 43 | "foo/blob003.txt", 44 | "foo/zed/blob005.txt" 45 | ], { 46 | "GET list of blobs (maxResults).": function (test, opts) { 47 | var self = this, 48 | request = self.container.getBlobs({ maxResults: 2 }); 49 | 50 | test.expect(6); 51 | request.on('error', utils.errHandle(test)); 52 | request.on('end', function (results) { 53 | test.ok(results.blobs, "Should have results."); 54 | test.deepEqual(2, results.blobs.length); 55 | test.deepEqual("foo/blob001.txt", results.blobs[0].name); 56 | test.deepEqual("foo/blob002.txt", results.blobs[1].name); 57 | 58 | test.ok(results.dirNames, []); 59 | 60 | test.ok(results.hasNext, "Should have next."); 61 | test.done(); 62 | }); 63 | request.end(); 64 | }, 65 | "GET list of blobs (prefix, delim, marker).": function (test, opts) { 66 | var self = this, 67 | request = self.container.getBlobs({ 68 | prefix: "foo/", 69 | delimiter: "/", 70 | marker: "foo/blob001.txt" 71 | }); 72 | 73 | test.expect(6); 74 | request.on('error', utils.errHandle(test)); 75 | request.on('end', function (results) { 76 | test.ok(results.blobs, "Should have results."); 77 | test.deepEqual(2, results.blobs.length); 78 | test.deepEqual("foo/blob002.txt", results.blobs[0].name); 79 | test.deepEqual("foo/blob003.txt", results.blobs[1].name); 80 | 81 | test.ok(results.dirNames, ["zed"]); 82 | 83 | test.ok(!results.hasNext, "Should not have next."); 84 | test.done(); 85 | }); 86 | request.end(); 87 | } 88 | }); 89 | 90 | module.exports.Tests = Tests; 91 | }()); 92 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # To Do / Planning 2 | 3 | ## API 4 | - Streams: Add 'meta' event for when metadata is first available. 5 | (Maybe: this doesn't totally solve the "need content-type" issue. Probably 6 | need a full HEAD request). 7 | - COPY Blob. 8 | - UPDATE Blob Metadata only (no data). Really a COPY/REPLACE operation. 9 | See: http://stackoverflow.com/questions/4754383/ 10 | how-to-change-metadata-on-an-object-in-amazon-s3 11 | - Add more attributes (like created date, size, etc.) from cloud requests. 12 | - Add ``withMd5`` option on PUT operations. Refactor to add headers after 13 | ``end()`` is called. 14 | 15 | - Retries: Add some intelligent wrapper / utility. 16 | - Throttling: Also handling throttling (along with retries). 17 | 18 | ## Cloud 19 | - Add keep-alive connection pooling. 20 | - Add 100-Continue support. 21 | - Add OpenStack support. 22 | - Add Rackspace support. 23 | - Switch to GSFD v2 API (instead of AWS legacy). 24 | - AWS / GSFD: Enumerate through the full REST API and add missing features, 25 | e.g.: 26 | - acl's 27 | - policies 28 | - logging 29 | 30 | ## Tests 31 | - Capitalization in container names (might mess with string-to-sign). 32 | - Test Coverage: add 'node-jscoverage'. 33 | 34 | ## Cloud Errors / Error Handling 35 | ### Timeouts 36 | :: 37 | 38 | node.js:134 39 | throw e; // process.nextTick error, or 'error' event on first tick 40 | ^ 41 | Error: ETIMEDOUT, Operation timed out 42 | at Client._onConnect (net.js:601:18) 43 | at IOWatcher.onWritable [as callback] (net.js:186:12) 44 | 45 | ### Internal Errors: 46 | :: 47 | 48 | 49 | InternalError 50 | We encountered an internal error. Please try again. 51 |
ABCemspwA3LqfUrwhUr7Fbz592Lwk3+nWMW7kUeT3OpaeE7gLUB1S/U8bp5svLKgHx3AXmI+c2nm1AeYftWMOI5J0vg3VncQDQ8i/seyEF26CGs8YH2/U/xysDtIwInNEC/G2QLj4Wf5u4moPxfMWtnNuHcC5+FMvzylaex4yykMO0+NgVrBxFySWfqJylh3asaXSjijQek91gyF9btAOIHORRgu7XKmkfK1QTsLErOOPYAygfvpqVkn/aKqZupvlQQfQEpFKfVXU8QqZ226tCPO/X5y0t/UDQ0o+mC//UkvLSMGEZsG8Ul7yuxK0FF2rInmTLZu5fTi5544xR/RxCjz8+TOkuSfitsPGNY97GWvDoKvfQ7kAET1ycxYGOlaTyfO5PDI9TWfVopYou/579DQMs5EnWQTP5ZN0eawr7VBMUXvbwmTPAxNzSffETRoHambdbWB0rSF2dJG0NQ2l9r0We0BFAhtV6jq2ZKQOcA3LMsYW6Ilo3w=
52 |
53 | 54 | ### Operation Aborted: 55 | :: 56 | 57 | 58 | OperationAborted 59 | A conflicting conditional operation is currently in progress against this resource. Please try again. 60 | 93EEA161A40AD419> 61 | NCVEcANu0ANdJvOlTQcXn31uUf01LLfjjUq5YISbaWNAGTzTYnlZn6xIfDo9lQN+ 62 | 63 | 64 | ### Throttling (Google v1): 65 | :: 66 | 67 | 68 | SlowDown 69 | Please reduce your request rate. 70 | 71 | -------------------------------------------------------------------------------- /docs/css/syntax.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * Site (jekyll) syntax highlighting. 3 | */ 4 | .highlight 5 | .hll 6 | background-color: #ffffcc 7 | .c 8 | color: #408080 9 | font-style: italic 10 | .err 11 | border: 1px solid #FF0000 12 | .k 13 | color: #008000 14 | font-weight: bold 15 | .o 16 | color: #666666 17 | .cm 18 | color: #408080 19 | font-style: italic 20 | .cp 21 | color: #BC7A00 22 | .c1 23 | color: #408080 24 | font-style: italic 25 | .cs 26 | color: #408080 27 | font-style: italic 28 | .gd 29 | color: #A00000 30 | .ge 31 | font-style: italic 32 | .gr 33 | color: #FF0000 34 | .gh 35 | color: #000080 36 | font-weight: bold 37 | .gi 38 | color: #00A000 39 | .go 40 | color: #808080 41 | .gp 42 | color: #000080 43 | font-weight: bold 44 | .gs 45 | font-weight: bold 46 | .gu 47 | color: #800080 48 | font-weight: bold 49 | .gt 50 | color: #0040D0 51 | .kc 52 | color: #008000 53 | font-weight: bold 54 | .kd 55 | color: #008000 56 | font-weight: bold 57 | .kn 58 | color: #008000 59 | font-weight: bold 60 | .kp 61 | color: #008000 62 | .kr 63 | color: #008000 64 | font-weight: bold 65 | .kt 66 | color: #B00040 67 | .m 68 | color: #666666 69 | .s 70 | color: #BA2121 71 | .na 72 | color: #7D9029 73 | .nb 74 | color: #008000 75 | .nc 76 | color: #0000FF 77 | font-weight: bold 78 | .no 79 | color: #880000 80 | .nd 81 | color: #AA22FF 82 | .ni 83 | color: #999999 84 | font-weight: bold 85 | .ne 86 | color: #D2413A 87 | font-weight: bold 88 | .nf 89 | color: #0000FF 90 | .nl 91 | color: #A0A000 92 | .nn 93 | color: #0000FF 94 | font-weight: bold 95 | .nt 96 | color: #008000 97 | font-weight: bold 98 | .nv 99 | color: #19177C 100 | .ow 101 | color: #AA22FF 102 | font-weight: bold 103 | .w 104 | color: #bbbbbb 105 | .mf 106 | color: #666666 107 | .mh 108 | color: #666666 109 | .mi 110 | color: #666666 111 | .mo 112 | color: #666666 113 | .sb 114 | color: #BA2121 115 | .sc 116 | color: #BA2121 117 | .sd 118 | color: #BA2121 119 | font-style: italic 120 | .s2 121 | color: #BA2121 122 | .se 123 | color: #BB6622 124 | font-weight: bold 125 | .sh 126 | color: #BA2121 127 | .si 128 | color: #BB6688 129 | font-weight: bold 130 | .sx 131 | color: #008000 132 | .sr 133 | color: #BB6688 134 | .s1 135 | color: #BA2121 136 | .ss 137 | color: #19177C 138 | .bp 139 | color: #008000 140 | .vc 141 | color: #19177C 142 | .vg 143 | color: #19177C 144 | .vi 145 | color: #19177C 146 | .il 147 | color: #666666 148 | -------------------------------------------------------------------------------- /docs_api/templates/jsdoc/allfiles.tmpl: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | {! Link.base = ""; /* all generated links will be relative to this */ !} 9 | 10 | Sunny.js - Files 11 | <if test="JSDOC.opt.D.version"> 12 | - v{+JSDOC.opt.D.version+} 13 | </if> 14 | 15 | 16 | 18 | 20 | 21 | 22 | 23 |
24 | {+includeTmpl("header.tmpl")+} 25 | 26 |
27 |

28 | Files 29 | 30 | - v{+JSDOC.opt.D.version+} 31 | 32 |

33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 48 | 68 | 69 | 70 | 71 |
File Summary
File NameDescription
46 | {+new Link().toSrc(item.alias).withText(item.name)+} 47 | 49 | {+resolveLinks(item.desc)+} 50 |
51 | 52 |
Author:
53 |
{+item.author+}
54 |
55 | 56 |
Version:
57 |
{+item.version+}
58 |
59 | {! var locations = item.comment.getTag('location').map(function($){return $.toString().replace(/(^\$ ?| ?\$$)/g, '').replace(/^HeadURL: https:/g, 'http:');}) !} 60 | 61 |
Location:
62 | 63 |
{+location+}
64 |
65 |
66 |
67 |
72 |
73 |
74 | 75 | -------------------------------------------------------------------------------- /dev/jshint.json: -------------------------------------------------------------------------------- 1 | { 2 | // Settings 3 | "passfail" : false, // Stop on first error. 4 | "maxerr" : 100, // Maximum error before stopping. 5 | 6 | // Environments (predefined globals). 7 | "browser" : false, // Standard browser globals e.g. `window`,. 8 | "node" : true, 9 | "rhino" : false, 10 | "couch" : false, 11 | "wsh" : false, // Windows Scripting Host. 12 | 13 | // Libraries (predefined globals). 14 | "jquery" : false, 15 | "prototypejs" : false, 16 | "mootools" : false, 17 | "dojo" : false, 18 | 19 | // Custom globals. 20 | "predef" : [ 21 | //"exampleVar", 22 | //"anotherCoolGlobal", 23 | //"iLoveDouglas" 24 | ], 25 | 26 | // Development. 27 | "debug" : false, // Allow debugger statements. 28 | "devel" : true, // Allow dev. statements e.g. `console.log();`. 29 | 30 | // ECMAScript 5. 31 | "es5" : true, // Allow ECMAScript 5 syntax. 32 | "strict" : false, // Require `use strict` pragma in every file. 33 | "globalstrict" : false, // Allow global "use strict" 34 | // (also enables 'strict'). 35 | 36 | // The Good Parts. 37 | "asi" : false, // Tolerate Automatic Semicolon Insertion. 38 | "laxbreak" : true, // Tolerate unsafe line breaks e.g. 39 | // `return [\n] x` without semicolons. 40 | "bitwise" : true, // Prohibit bitwise operators (&, |, ^, etc.). 41 | "boss" : false, // Tolerate assignments inside if, for & while. 42 | "curly" : true, // Require {} for every new block or scope. 43 | "eqeqeq" : true, // Require triple equals i.e. `===`. 44 | "eqnull" : false, // Tolerate use of `== null`. 45 | "evil" : false, // Tolerate use of `eval`. 46 | "expr" : false, // Tolerate `ExpressionStatement` as Programs. 47 | "forin" : false, // Tolerate `for in` loops without 48 | // `hasOwnPrototype`. 49 | "immed" : true, // Require immediate invocations to be wrapped in 50 | // parens e.g. `( function(){}() );` 51 | "latedef" : true, // Prohibit variable use before definition. 52 | "loopfunc" : false, // Allow functions to be defined within loops. 53 | "noarg" : true, // Prohibit use of `arguments.caller` and 54 | // `arguments.callee`. 55 | "regexp" : true, // Prohibit `.` and `[^...]` in regular expressions. 56 | "regexdash" : false, // Tolerate unescaped last dash i.e. `[-...]`. 57 | "scripturl" : true, // Tolerate script-targeted URLs. 58 | "shadow" : false, // Allows re-defined variables later in code. 59 | "supernew" : false, // Tolerate `new function () { ... };` and 60 | // `new Object;`. 61 | "undef" : true, // Require all non-global variables be declared 62 | // before they are used. 63 | 64 | // Personal styling preferences. 65 | "newcap" : true, // Require capitalization of all constructor 66 | // functions e.g. `new F()`. 67 | "noempty" : true, // Prohibit use of empty blocks. 68 | "nonew" : true, // Prohibit use of constructors for side-effects. 69 | "nomen" : false, // Prohibit use of initial or trailing underbars. 70 | "onevar" : true, // Allow only one `var` statement per function. 71 | "onecase" : true, // if one case switch statements should be allowed 72 | "plusplus" : false, // Prohibit use of `++` & `--`. 73 | "sub" : true, // Tolerate all forms of subscript notation. 74 | "trailing" : true, // Prohibit trailing whitespaces. 75 | "white" : true, // Check against strict whitespace / indent rules. 76 | "maxlen" : 80, // Line length. 77 | "indent" : 2 // Specify indentation spacing 78 | } -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Error classes. 3 | */ 4 | 5 | /** 6 | * @name errors 7 | */ 8 | (function () { 9 | var util = require('util'), 10 | CloudError; 11 | 12 | /** 13 | * Base cloud error class. 14 | * 15 | * ## Note 16 | * A ``CloudError`` is generally thrown only on a failed cloud operation, 17 | * so that calling code can make intelligent retry / failure handling 18 | * decisions. 19 | * 20 | * Sunny throws straight ``Error``'s for programming / calling errors 21 | * (e.g., missing required parameters, invalid parameter input). Any 22 | * ``Error`` indicates a code and/or Sunny library error and should be 23 | * fixed. 24 | * 25 | * @param {String} [message] Exception message. 26 | * @param {Object} [options] Options object. 27 | * @config {Error} [error] Underlying error object. 28 | * @config {Object|Array|String} 29 | * [types] List/object of error types. 30 | * @config {HttpResponse} [response] Offending response object. 31 | * @exports CloudError as errors.CloudError 32 | * @constructor 33 | */ 34 | CloudError = function (message, options) { 35 | options = options || {}; 36 | message = message || (options.error ? options.error.message : ''); 37 | 38 | var self = this, 39 | error = options.error || new Error(), 40 | types = options.types || {}, 41 | typesMap = {}, 42 | response = options.response || {}; 43 | 44 | Error.apply(self, [message]); 45 | 46 | /** 47 | * Error message. 48 | * @name errors.CloudError#message 49 | * @type string 50 | */ 51 | self.message = message; 52 | 53 | /** 54 | * HTTP status code (if any). 55 | * @name errors.CloudError#statusCode 56 | * @type number 57 | */ 58 | self.statusCode = response.statusCode || null; 59 | 60 | // Patch in other error parts. 61 | self.stack = error.stack; 62 | self.arguments = error.arguments; 63 | self.type = error.type; 64 | 65 | // Set appropriate cloud-specific errors. 66 | self.TYPES = CloudError.TYPES; 67 | if (typeof types === 'string') { 68 | // Convert string. 69 | self._types = {}; 70 | self._types[types] = true; 71 | } else if (Array.isArray(types)) { 72 | // Convert array. 73 | types.forEach(function (key) { 74 | typesMap[key] = true; 75 | }); 76 | self._types = typesMap; 77 | } else { 78 | // Already an object. 79 | self._types = types; 80 | } 81 | }; 82 | 83 | util.inherits(CloudError, Error); 84 | 85 | /** Prototype of all available errors. */ 86 | CloudError.TYPES = (function (keys) { 87 | var types = {}; 88 | keys.forEach(function (key) { 89 | // Bind key and value to string for object. 90 | types[key] = key; 91 | }); 92 | return types; 93 | }([ 94 | 'NOT_FOUND', 95 | 'NOT_EMPTY', 96 | 'INVALID_NAME', 97 | 'NOT_OWNER', 98 | 'ALREADY_OWNED_BY_YOU' 99 | ])); 100 | 101 | /** 102 | * Return true if error is of this type. 103 | * @private 104 | */ 105 | CloudError.prototype._is = function (errorType) { 106 | if (!errorType || this.TYPES[errorType] !== errorType) { 107 | throw new Error("Unknown error type: " + errorType); 108 | } 109 | 110 | return this._types[errorType] === true; 111 | }; 112 | 113 | /**#@+ 114 | * @returns {boolean} True if given error type. 115 | */ 116 | CloudError.prototype.isNotFound = function () { 117 | return this._is(this.TYPES.NOT_FOUND); 118 | }; 119 | CloudError.prototype.isNotEmpty = function () { 120 | return this._is(this.TYPES.NOT_EMPTY); 121 | }; 122 | CloudError.prototype.isInvalidName = function () { 123 | return this._is(this.TYPES.INVALID_NAME); 124 | }; 125 | CloudError.prototype.isNotOwner = function () { 126 | return this._is(this.TYPES.NOT_OWNER); 127 | }; 128 | CloudError.prototype.isAlreadyOwnedByYou = function () { 129 | return this._is(this.TYPES.ALREADY_OWNED_BY_YOU); 130 | }; 131 | /**#@-*/ 132 | 133 | module.exports.CloudError = CloudError; 134 | }()); 135 | -------------------------------------------------------------------------------- /test/core.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Runs core tests against module. 3 | */ 4 | 5 | /** 6 | * @name test 7 | */ 8 | (function () { 9 | var sunny = require("../lib"), 10 | errors = require("../lib/errors"), 11 | CloudError = errors.CloudError, 12 | Tests; 13 | 14 | /** 15 | * Core API unit tests. 16 | * 17 | * **Note**: These tests should run without cloud credentials or even a 18 | * network connection. 19 | * 20 | * @exports Tests as test.core 21 | * @namespace 22 | */ 23 | Tests = { 24 | "Empty CloudError.": function (test) { 25 | var err = new CloudError(); 26 | 27 | test.deepEqual(err['message'], ""); 28 | test.deepEqual(err['statusCode'], null); 29 | test.notEqual(err['stack'], ""); 30 | test.ok(!err.isNotFound()); 31 | test.ok(!err.isInvalidName()); 32 | test.ok(!err.isNotOwner()); 33 | test.done(); 34 | }, 35 | "Simple CloudError.": function (test) { 36 | var err = new CloudError("Hi."); 37 | 38 | test.deepEqual(err['message'], "Hi."); 39 | test.deepEqual(err['statusCode'], null); 40 | test.notEqual(err['stack'], ""); 41 | test.done(); 42 | }, 43 | "CloudError wraps Error.": function (test) { 44 | var realErr = new Error("Hello"), 45 | err = new CloudError(null, { error: realErr }); 46 | 47 | test.deepEqual(err['message'], "Hello"); 48 | test.deepEqual(err['statusCode'], null); 49 | test.deepEqual(err['stack'], realErr.stack); 50 | test.done(); 51 | }, 52 | "CloudError wraps Response.": function (test) { 53 | var err = new CloudError("Yo.", { response: { statusCode: 404 } }); 54 | 55 | test.deepEqual(err['message'], "Yo."); 56 | test.deepEqual(err['statusCode'], 404); 57 | test.notEqual(err['stack'], ""); 58 | test.ok(!err.isNotFound()); 59 | test.ok(!err.isInvalidName()); 60 | test.ok(!err.isNotOwner()); 61 | test.done(); 62 | }, 63 | "CloudError not found.": function (test) { 64 | var errs = [ 65 | new CloudError("Yippee.", { 66 | response: { statusCode: 404 }, 67 | types: [CloudError.TYPES.NOT_FOUND] 68 | }), 69 | new CloudError("Yippee.", { 70 | response: { statusCode: 404 }, 71 | types: CloudError.TYPES.NOT_FOUND 72 | }), 73 | new CloudError("Yippee.", { 74 | response: { statusCode: 404 }, 75 | types: (function () { 76 | var types = {}; 77 | // Set some of the attributes (only need NOT_FOUND). 78 | types[CloudError.TYPES.NOT_FOUND] = true; 79 | types[CloudError.TYPES.INVALID_NAME] = false; 80 | return types; 81 | }()) 82 | }) 83 | ]; 84 | 85 | errs.forEach(function (err) { 86 | test.deepEqual(err['message'], "Yippee."); 87 | test.deepEqual(err['statusCode'], 404); 88 | test.notEqual(err['stack'], ""); 89 | test.ok(err.isNotFound()); 90 | test.ok(!err.isInvalidName()); 91 | test.ok(!err.isNotOwner()); 92 | }); 93 | test.done(); 94 | }, 95 | "CloudError not found and invalid.": function (test) { 96 | var errs = [ 97 | new CloudError("Wow.", { 98 | response: { statusCode: 409 }, 99 | types: [ 100 | CloudError.TYPES.NOT_FOUND, 101 | CloudError.TYPES.INVALID_NAME 102 | ] 103 | }), 104 | new CloudError("Wow.", { 105 | response: { statusCode: 409 }, 106 | types: (function () { 107 | var types = {}; 108 | types[CloudError.TYPES.NOT_FOUND] = true; 109 | types[CloudError.TYPES.INVALID_NAME] = true; 110 | return types; 111 | }()) 112 | }) 113 | ]; 114 | 115 | errs.forEach(function (err) { 116 | test.deepEqual(err['message'], "Wow."); 117 | test.deepEqual(err['statusCode'], 409); 118 | test.notEqual(err['stack'], ""); 119 | test.ok(err.isNotFound()); 120 | test.ok(err.isInvalidName()); 121 | test.ok(!err.isNotOwner()); 122 | }); 123 | test.done(); 124 | } 125 | }; 126 | 127 | module.exports = Tests; 128 | }()); 129 | -------------------------------------------------------------------------------- /lib/provider/aws/connection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview AWS Connection. 3 | */ 4 | 5 | (function () { 6 | var util = require('util'), 7 | utils = require("../../utils"), 8 | CloudError = require("../../errors").CloudError, 9 | BaseConnection = require("../../base/connection").Connection, 10 | Container = require("./blob/container").Container, 11 | AwsConnection; 12 | 13 | /** 14 | * Connection class. 15 | * 16 | * @param {Authentication} auth Authentication object. 17 | * @extends base.Connection 18 | * @exports AwsConnection as provider.aws.Connection 19 | * @constructor 20 | */ 21 | AwsConnection = function (auth) { 22 | var self = this, 23 | invalid; 24 | 25 | BaseConnection.apply(self, arguments); 26 | 27 | // Header prefixes. 28 | self._HEADER_PREFIX = "x-amz-"; 29 | self._METADATA_PREFIX = "x-amz-meta-"; 30 | 31 | // Update variables. 32 | invalid = self._ERRORS.CONTAINER_INVALID_NAME; 33 | self._ERRORS.CONTAINER_INVALID_NAME = utils.extend(invalid, { 34 | attrs: { 35 | statusCode: 400, 36 | errorCode: "InvalidBucketName" 37 | }, 38 | errorMap: { 39 | // Invalid container name with GET is both not found and invalid. 40 | 'GET': { 41 | message: self._ERRORS.CONTAINER_INVALID_NAME.error.message, 42 | types: [ 43 | CloudError.TYPES.NOT_FOUND, 44 | CloudError.TYPES.INVALID_NAME 45 | ] 46 | } 47 | } 48 | }); 49 | self._ERRORS.CONTAINER_NOT_FOUND.attrs = { 50 | statusCode: 404, 51 | errorCode: "NoSuchBucket", 52 | errorHtml: "Not Found" 53 | }; 54 | self._ERRORS.CONTAINER_NOT_EMPTY.attrs = { 55 | statusCode: 409, 56 | errorCode: "BucketNotEmpty" 57 | }; 58 | self._ERRORS.CONTAINER_OTHER_OWNER.attrs = { 59 | statusCode: 409, 60 | errorCode: "BucketAlreadyExists" 61 | }; 62 | self._ERRORS.BLOB_NOT_FOUND.attrs = { 63 | statusCode: 404, 64 | errorCode: "NoSuchKey", 65 | errorHtml: "Not Found" 66 | }; 67 | }; 68 | 69 | util.inherits(AwsConnection, BaseConnection); 70 | 71 | /** 72 | * Create container object. 73 | * @private 74 | */ 75 | AwsConnection.prototype._createContainer = function (name) { 76 | return new Container(this, { name: name }); 77 | }; 78 | 79 | /** 80 | * Check if known error. 81 | * 82 | * @returns {boolean} True if error. 83 | * @private 84 | */ 85 | AwsConnection.prototype._isError = function (errItem, err, response) { 86 | var attrs = errItem.attrs; 87 | 88 | // Test error codes or html 89 | /** @private */ 90 | function errorCodeMatches() { 91 | if (!attrs.errorCode) { return false; } 92 | var regex = new RegExp("" + attrs.errorCode + ""); 93 | return regex.test(err.message); 94 | } 95 | 96 | /** @private */ 97 | function errorHtmlMatches() { 98 | if (!attrs.errorHtml) { return false; } 99 | var regex = new RegExp("" + attrs.errorHtml + ""); 100 | return regex.test(err.message); 101 | } 102 | 103 | return attrs && response && response.statusCode === attrs.statusCode && 104 | (errorCodeMatches() || errorHtmlMatches()); 105 | }; 106 | 107 | /** 108 | * @see base.Connection#getContainers 109 | */ 110 | AwsConnection.prototype.getContainers = function (options) { 111 | var self = this, 112 | meta = utils.extractMeta(options); 113 | 114 | options = options || {}; 115 | return self._auth.createXmlRequest(utils.extend(meta, { 116 | encoding: 'utf8', 117 | path: "/", 118 | headers: { 119 | 'content-length': 0 120 | }, 121 | resultsFn: function (result) { 122 | var buckets = result.Buckets.Bucket || [], 123 | containers = []; 124 | 125 | // Create container objects. 126 | buckets.forEach(function (bucket) { 127 | containers.push(new Container(self, { 128 | name: bucket.Name, 129 | created: bucket.CreationDate 130 | })); 131 | }); 132 | 133 | return { containers: containers }; 134 | } 135 | })); 136 | }; 137 | 138 | module.exports.Connection = AwsConnection; 139 | }()); 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sunny.js 2 | Sunny is a multi-cloud datastore client for [Node.js](http://nodejs.org). 3 | Sunny aims for an efficient, event-based common interface to various cloud 4 | stores to enable cloud-agnostic programming that retains flexibility and 5 | speed. 6 | 7 | * [Sunny.js Documentation](http://sunnyjs.org): Guides and API documentation. 8 | * [Sunny.js GitHub Page](http://github.com/ryan-roemer/node-sunny): Source 9 | repository and issue tracker. 10 | 11 | ## Features 12 | * Abstracts cloud provider differences. Focus on writing your application, 13 | not juggling "x-amz-" vs. "x-goog-" headers. 14 | * Fully configurable headers, cloud headers, cloud metadata. 15 | * Sensible and cloud-agnostic error handling. 16 | * "One-shot" requests whenever possible. 17 | * SSL support. 18 | * Blob GET/PUT operations implement Node [Readable][ReadStream] and 19 | [Writable][WriteStream] Stream interfaces. 20 | 21 | [ReadStream]: http://nodejs.org/docs/v0.4.9/api/streams.html#readable_Stream 22 | [WriteStream]: http://nodejs.org/docs/v0.4.9/api/streams.html#writable_Stream 23 | 24 | ## Cloud providers 25 | Sunny has full blob support for: 26 | 27 | * [Amazon S3][S3]: Amazon Simple Storage Service. 28 | * [Google Storage][GSFD]: Google Storage for Developers. 29 | 30 | [S3]: http://aws.amazon.com/s3/ 31 | [GSFD]: http://code.google.com/apis/storage/ 32 | 33 | Future support is planned for: 34 | 35 | * [Rackspace Cloud Files][CF]: Rackspace Cloud Files 36 | * [OpenStack Storage][OS]: OpenStack Storage 37 | * (Maybe) local file system as a dummy cloud provider. 38 | 39 | [CF]: http://www.rackspacecloud.com/cloud_hosting_products/files/ 40 | [OS]: http://openstack.org/projects/storage/ 41 | 42 | ## Installation / Getting Started 43 | Install Sunny directly from [npm][NPM]: 44 | 45 | $ npm install sunny 46 | 47 | or the [GitHub][SGH] repository: 48 | 49 | $ git clone git@github.com:ryan-roemer/node-sunny.git 50 | $ npm install ./node-sunny 51 | 52 | [NPM]: http://npmjs.org/ 53 | [SGH]: https://github.com/ryan-roemer/node-sunny 54 | 55 | Please read the docs (in source at "docs/") and review the "live" tests 56 | (in source at "test/live") that perform the entire range of Sunny operations 57 | against real cloud datastores. 58 | 59 | ## Project Goals 60 | ### A common cloud interface. 61 | The cloud providers that Sunny supports (or will support) provide similar, but 62 | not quite equivalent interfaces. Amazon S3 and Google Storage share a nearly 63 | identical interface, as well as Rackspace Cloud Files and OpenStack Storage. 64 | However, there are some subtle differences, particularly with naming, errors, 65 | status codes, etc. 66 | 67 | ### Extensible and accessible. 68 | Notwithstanding the goal for a common API, Sunny aims to provide access to 69 | as much of the internals of a given cloud datastore as possible. In the current 70 | development phase, that means exposing as many of the query / header / metadata 71 | features and functionality in the underlying store as won't make maintaining 72 | a common interface unpalatable. 73 | 74 | ### Strong bias for "one-shot" requests. 75 | Most cloud operations can be performed with a single HTTP request. However, 76 | many cloud client libraries add in extra HTTP calls along the way for say 77 | a blob file GET request (perhaps first requesting an authorization URL, 78 | checking the container path for existence, etc.). 79 | 80 | Sunny aims to perform the minimum amount of calls possible by default. That 81 | said, sometimes it is good to have a few sanity check intermediate operations, 82 | so Sunny can make calls with validation (e.g., checking a bucket exists first). 83 | 84 | ## Cloud Operations 85 | ### Supported 86 | Sunny currently supports the following cloud operations: 87 | 88 | * List containers: 89 | ``connection.getContainers()`` 90 | * PUT / DELETE container: 91 | ``container.put()``, 92 | ``container.del()`` 93 | * List blobs in a container: 94 | ``container.getBlobs()`` 95 | * PUT / HEAD / GET / DELETE blob: 96 | ``blob.put()``, 97 | ``blob.putFromFile()``, 98 | ``blob.head()``, 99 | ``blob.get()``, 100 | ``blob.getToFile()``, 101 | ``blob.del()`` 102 | 103 | ### Future 104 | Sunny is under rapid development. Some areas for enhancements: 105 | 106 | * Copy blob. 107 | * Update blob metadata without PUT. 108 | * Set blob / container ACL, policies, etc. -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Home 4 | --- 5 | 6 | # Sunny.js 7 | Sunny is a multi-cloud datastore client for [Node.js](http://nodejs.org). 8 | Sunny aims for an efficient, event-based common interface to various cloud 9 | stores to enable cloud-agnostic programming that retains flexibility and 10 | speed. 11 | 12 | * [Sunny.js Documentation](http://sunnyjs.org): Guides and API documentation. 13 | * [Sunny.js GitHub Page](http://github.com/ryan-roemer/node-sunny): Source 14 | repository and issue tracker. 15 | 16 | ## Features 17 | * Abstracts cloud provider differences. Focus on writing your application, 18 | not juggling "x-amz-" vs. "x-goog-" headers. 19 | * Fully configurable headers, cloud headers, cloud metadata. 20 | * Sensible and cloud-agnostic error handling. 21 | * "One-shot" requests whenever possible. 22 | * SSL support. 23 | * Blob GET/PUT operations implement Node [Readable][ReadStream] and 24 | [Writable][WriteStream] Stream interfaces. 25 | 26 | [ReadStream]: http://nodejs.org/docs/v0.4.9/api/streams.html#readable_Stream 27 | [WriteStream]: http://nodejs.org/docs/v0.4.9/api/streams.html#writable_Stream 28 | 29 | ## Cloud providers 30 | Sunny has full blob support for: 31 | 32 | * [Amazon S3][S3]: Amazon Simple Storage Service. 33 | * [Google Storage][GSFD]: Google Storage for Developers. 34 | 35 | [S3]: http://aws.amazon.com/s3/ 36 | [GSFD]: http://code.google.com/apis/storage/ 37 | 38 | Future support is planned for: 39 | 40 | * [Rackspace Cloud Files][CF]: Rackspace Cloud Files 41 | * [OpenStack Storage][OS]: OpenStack Storage 42 | * (Maybe) local file system as a dummy cloud provider. 43 | 44 | [CF]: http://www.rackspacecloud.com/cloud_hosting_products/files/ 45 | [OS]: http://openstack.org/projects/storage/ 46 | 47 | ## Installation / Getting Started 48 | Install Sunny directly from [npm][NPM]: 49 | 50 | $ npm install sunny 51 | 52 | or the [GitHub][SGH] repository: 53 | 54 | $ git clone git@github.com:ryan-roemer/node-sunny.git 55 | $ npm install ./node-sunny 56 | 57 | [NPM]: http://npmjs.org/ 58 | [SGH]: https://github.com/ryan-roemer/node-sunny 59 | 60 | Please read the docs (in source at "docs/") and review the "live" tests 61 | (in source at "test/live") that perform the entire range of Sunny operations 62 | against real cloud datastores. 63 | 64 | ## Project Goals 65 | ### A common cloud interface. 66 | The cloud providers that Sunny supports (or will support) provide similar, but 67 | not quite equivalent interfaces. Amazon S3 and Google Storage share a nearly 68 | identical interface, as well as Rackspace Cloud Files and OpenStack Storage. 69 | However, there are some subtle differences, particularly with naming, errors, 70 | status codes, etc. 71 | 72 | ### Extensible and accessible. 73 | Notwithstanding the goal for a common API, Sunny aims to provide access to 74 | as much of the internals of a given cloud datastore as possible. In the current 75 | development phase, that means exposing as many of the query / header / metadata 76 | features and functionality in the underlying store as won't make maintaining 77 | a common interface unpalatable. 78 | 79 | ### Strong bias for "one-shot" requests. 80 | Most cloud operations can be performed with a single HTTP request. However, 81 | many cloud client libraries add in extra HTTP calls along the way for say 82 | a blob file GET request (perhaps first requesting an authorization URL, 83 | checking the container path for existence, etc.). 84 | 85 | Sunny aims to perform the minimum amount of calls possible by default. That 86 | said, sometimes it is good to have a few sanity check intermediate operations, 87 | so Sunny can make calls with validation (e.g., checking a bucket exists first). 88 | 89 | ## Cloud Operations 90 | ### Supported 91 | Sunny currently supports the following cloud operations: 92 | 93 | * List containers: 94 | ``connection.getContainers()`` 95 | * PUT / DELETE container: 96 | ``container.put()``, 97 | ``container.del()`` 98 | * List blobs in a container: 99 | ``container.getBlobs()`` 100 | * PUT / HEAD / GET / DELETE blob: 101 | ``blob.put()``, 102 | ``blob.putFromFile()``, 103 | ``blob.head()``, 104 | ``blob.get()``, 105 | ``blob.getToFile()``, 106 | ``blob.del()`` 107 | 108 | ### Future 109 | Sunny is under rapid development. Some areas for enhancements: 110 | 111 | * Copy blob. 112 | * Update blob metadata without PUT. 113 | * Set blob / container ACL, policies, etc. 114 | -------------------------------------------------------------------------------- /test/live.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Live cloud tests. 3 | * 4 | * See full warnings and dev. notes below. 5 | */ 6 | 7 | (function () { 8 | var assert = require('assert'), 9 | testCase = require('nodeunit').testCase, 10 | sunny = require("../lib"), 11 | utils = require("../lib/utils"), 12 | 13 | // Tests 14 | ConnectionTests = require("./live/connection").Tests, 15 | ContainerTests = require("./live/container").Tests, 16 | BlobTests = require("./live/blob").Tests, 17 | 18 | // Variables 19 | Tests, 20 | configPath = process.env.SUNNY_LIVE_TEST_CONFIG, 21 | testsConfig, 22 | testsProto; 23 | 24 | /** 25 | * Live cloud tests. 26 | * 27 | * ## Warning - Mutates Datastore! 28 | * This actually **adds/deletes** containers / files on a live cloud 29 | * account. Care has been taken to not collide with any real data, but you 30 | * are strongly advised to **not** run the tests against a production cloud 31 | * storage account. 32 | * 33 | * ## Configuration 34 | * Tests require a configuration in the format of: 35 | * 36 | * Configuration = [ 37 | * { 38 | * provider: 'aws', 39 | * acount: '', 40 | * secretKey: '', 41 | * [ssl: false] 42 | * }, 43 | * { // ... other providers ... 44 | * } 45 | * ]; 46 | * 47 | * Taken from a file. Tests are run against each configuration provided. 48 | * Currently supported providers are: 49 | * - 'aws' AWS Simple Storage Service (S3) 50 | * - 'google': Google Storage For Developers 51 | * 52 | * ## Setup/Teardown and Wrappers (Dev) 53 | * The nodeunit tests are wrapped to inject cloud-specific information and 54 | * to run suites against each different cloud configuration. The underlying 55 | * test prototypes look pretty similar to ordinary nodeunit tests, except 56 | * the function signature is: 57 | * 58 | * function (test, opts) 59 | * 60 | * instead of: 61 | * 62 | * function (test) 63 | * 64 | * where ``opts`` are injected cloud information. 65 | * 66 | * Several tests use a setup/teardown wrapper that creates a (hopefully) 67 | * unique and non-existent test container, optionally adds blobs, and wipes 68 | * out everything on teardown. See ``test.live.utils.createTestSetup()`` 69 | * for more information. 70 | * 71 | * @exports Tests as test.live 72 | * @namespace 73 | */ 74 | Tests = {}; 75 | 76 | // Get the configuration. 77 | if (!configPath) { 78 | assert.fail("Live tests require a configuration file."); 79 | } 80 | 81 | try { 82 | testsConfig = require(configPath).Configuration; 83 | } catch (err) { 84 | console.warn(err); 85 | assert.fail("Invalid configuration file / path: " + configPath); 86 | } 87 | 88 | // Define all tests (prototype). 89 | testsProto = utils.extend( 90 | ConnectionTests, 91 | ContainerTests, 92 | BlobTests 93 | ); 94 | 95 | /** 96 | * Bind tests to specific configurations. 97 | * @private 98 | */ 99 | function bindTests(config, num) { 100 | var prefix = config.provider + "(" + num + ")", 101 | sunnyOpts, 102 | wrapFn, 103 | protoKey, 104 | proto, 105 | testKey, 106 | testGroup; 107 | 108 | sunnyOpts = { 109 | config: config, 110 | conn: config.connection 111 | }; 112 | 113 | /** 114 | * Test wrapper that injects options to each test. 115 | * @private 116 | */ 117 | wrapFn = function (testFn) { 118 | return function (testOrCallback) { 119 | return testFn(testOrCallback, sunnyOpts); 120 | }; 121 | }; 122 | 123 | // Create a new test group for config 124 | Tests[prefix] = {}; 125 | 126 | // Re-bind test prefix with specific configs. 127 | for (protoKey in testsProto) { 128 | if (testsProto.hasOwnProperty(protoKey)) { 129 | proto = testsProto[protoKey]; 130 | 131 | // Wrap up a test case. 132 | testGroup = {}; 133 | for (testKey in proto) { 134 | if (proto.hasOwnProperty(testKey)) { 135 | // Wrap up actual test. 136 | testGroup[testKey] = wrapFn(proto[testKey]); 137 | } 138 | } 139 | Tests[prefix][protoKey] = testCase(testGroup); 140 | } 141 | } 142 | } 143 | 144 | // Create tests for each separate config object. 145 | testsConfig.forEach(function (options, index) { 146 | var config = sunny.Configuration.fromObj(options); 147 | bindTests(config, index); 148 | }); 149 | 150 | // Bind all tests to exports. 151 | module.exports = Tests; 152 | }()); 153 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Base Configuration. 3 | */ 4 | 5 | (function () { 6 | var CloudError = require("./errors").CloudError, 7 | Configuration; 8 | 9 | /** 10 | * Configuration class. 11 | * 12 | * @param {Object} options Options object. 13 | * @config {string} provider Cloud provider enum. 14 | * @config {string} account Account name. 15 | * @config {string} secretKey Secret key. 16 | * @config {string} [ssl=false] Use SSL? 17 | * @config {string} [authUrl] Authentication URL. 18 | * @config {number} [timeout] HTTP timeout in seconds. 19 | * @constructor 20 | */ 21 | Configuration = function (options) { 22 | var self = this, 23 | provider = null, 24 | key; 25 | 26 | // Argument parsing and validation. 27 | options = options || {}; 28 | if (!options.account) { throw new Error("No account name."); } 29 | if (!options.secretKey) { throw new Error("No secret key."); } 30 | 31 | // Manually bind constants. 32 | self.PROVIDERS = Configuration.PROVIDERS; 33 | 34 | // Defaults. 35 | self._auth = null; 36 | self._provider = options.provider; 37 | 38 | // Find provider 39 | for (key in self.PROVIDERS) { 40 | if (self.PROVIDERS.hasOwnProperty(key)) { 41 | provider = self.PROVIDERS[key]; 42 | if (options.provider === provider.name) { 43 | // Found a provider. Set up auth and connection. 44 | self._auth = provider.authFn(options); 45 | break; 46 | } 47 | } 48 | } 49 | 50 | if (!self._auth) { 51 | throw new Error("Not a valid provider: \"" + options.provider + "\""); 52 | } 53 | }; 54 | 55 | /** 56 | * Provider dictionary with delayed object creation. 57 | */ 58 | Configuration.PROVIDERS = { 59 | /**#@+ @ignore */ 60 | AWS: { name: 'aws', authFn: function (options) { 61 | var Authentication = require("./provider/aws").Authentication; 62 | return new Authentication(options); 63 | }}, 64 | GOOGLE: { name: 'google', authFn: function (options) { 65 | var Authentication = require("./provider/google").Authentication; 66 | return new Authentication(options); 67 | }} 68 | /**#@-*/ 69 | }; 70 | 71 | Object.defineProperties(Configuration.prototype, { 72 | /** 73 | * Connection object. 74 | * 75 | * @name Configuration#connection 76 | * @type base.Connection 77 | */ 78 | connection: { 79 | get: function () { 80 | return this._auth.connection; 81 | } 82 | }, 83 | 84 | /** 85 | * Provider string. 86 | * 87 | * @name Configuration#provider 88 | * @type string 89 | */ 90 | provider: { 91 | get: function () { 92 | return this._provider; 93 | } 94 | } 95 | }); 96 | 97 | /** Test provider (AWS). */ 98 | Configuration.prototype.isAws = function () { 99 | return this._provider === this.PROVIDERS.AWS.name; 100 | }; 101 | 102 | /** Test provider (Google Storage). */ 103 | Configuration.prototype.isGoogle = function () { 104 | return this._provider === this.PROVIDERS.GOOGLE.name; 105 | }; 106 | 107 | /** 108 | * Get configuration from options (settings) object. 109 | * 110 | * @param {Object} options Options object. 111 | * @config {string} provider Cloud provider enum. 112 | * @config {string} account Account name. 113 | * @config {string} secretKey Secret key. 114 | * @config {string} [ssl=false] Use SSL? 115 | * @config {string} [authUrl] Authentication URL. 116 | * @config {number} [timeout] HTTP timeout in seconds. 117 | * @returns {Configuration} Configuration object. 118 | */ 119 | Configuration.fromObj = function (options) { 120 | return new Configuration(options); 121 | }; 122 | 123 | /** 124 | * Get configuration from environment. 125 | * 126 | * ## Environment Variables 127 | * Sunny can use the following environment variables: 128 | * 129 | * - **SUNNY_PROVIDER**: ``"aws"`` or ``"google"`` 130 | * - **SUNNY_ACCOUNT**: ``"ACCOUNT_NAME"`` 131 | * - **SUNNY_SECRET_KEY**: ``"ACCOUNT_SECRET_KEY"`` 132 | * - **SUNNY_AUTH_URL**: ``"s3.amazonaws.com"`` 133 | * - **SUNNY_SSL**: ``true`` or ``false`` 134 | * 135 | * @returns {Configuration} Configuration object. 136 | */ 137 | Configuration.fromEnv = function () { 138 | var ssl = process.env.SUNNY_SSL, 139 | useSsl = !!(typeof ssl === 'string' && ssl.toLowerCase() === 'true'); 140 | 141 | return new Configuration({ 142 | provider: process.env.SUNNY_PROVIDER, 143 | account: process.env.SUNNY_ACCOUNT, 144 | secretKey: process.env.SUNNY_SECRET_KEY, 145 | authUrl: process.env.SUNNY_AUTH_URL || null, 146 | ssl: useSsl 147 | }); 148 | }; 149 | 150 | module.exports.Configuration = Configuration; 151 | }()); 152 | -------------------------------------------------------------------------------- /lib/base/authentication.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Base Authentication. 3 | */ 4 | 5 | (function () { 6 | // Requires. 7 | var http = require('http'), 8 | crypto = require('crypto'), 9 | parse = require('url').parse, 10 | CloudError = require("../errors").CloudError, 11 | AuthenticatedRequest = require("../request").AuthenticatedRequest, 12 | AuthenticatedXmlRequest = require("../request").AuthenticatedXmlRequest, 13 | Authentication; 14 | 15 | /** 16 | * Authentication class. 17 | * 18 | * @param {Object} options Options object. 19 | * @config {string} account Account name. 20 | * @config {string} secretKey Secret key. 21 | * @config {string} [ssl=false] Use SSL? 22 | * @config {string} [authUrl] Authentication URL. 23 | * @config {number} [timeout] HTTP timeout in seconds. 24 | * @exports Authentication as base.Authentication 25 | * @constructor 26 | */ 27 | Authentication = function (options) { 28 | // Argument parsing and validation. 29 | options = options || {}; 30 | if (!options.account) { throw new Error("No account name."); } 31 | if (!options.secretKey) { throw new Error("No secret key."); } 32 | if (!options.authUrl) { throw new Error("No authentication URL."); } 33 | 34 | // Member variables. 35 | this._account = options.account; 36 | this._secretKey = options.secretKey; 37 | this._ssl = options.ssl || false; 38 | this._port = options.port || (this._ssl ? 443 : 80); 39 | this._authUrl = options.authUrl; 40 | this._timeout = options.timeout || 5; 41 | this._conn = null; 42 | }; 43 | 44 | Object.defineProperties(Authentication.prototype, { 45 | /** 46 | * Use SSL? 47 | * 48 | * @name Authentication#ssl 49 | * @type boolean 50 | */ 51 | ssl: { 52 | get: function () { 53 | return this._ssl; 54 | } 55 | }, 56 | 57 | /** 58 | * Port number. 59 | * 60 | * @name Authentication#port 61 | * @type number 62 | */ 63 | port: { 64 | get: function () { 65 | return this._port; 66 | } 67 | }, 68 | 69 | /** 70 | * Connection object. 71 | * 72 | * @name Authentication#connection 73 | * @type base.Connection 74 | */ 75 | connection: { 76 | get: function () { 77 | var self = this; 78 | 79 | if (self._conn === null) { 80 | if (!self._CONN_CLS) { 81 | throw new Error("Subclass must define _CONN_CLS."); 82 | } 83 | 84 | self._conn = new self._CONN_CLS(self); 85 | } 86 | 87 | return self._conn; 88 | } 89 | } 90 | }); 91 | 92 | /** Test provider (AWS). */ 93 | Authentication.prototype.isAws = function () { 94 | return false; 95 | }; 96 | 97 | /** Test provider (Google Storage). */ 98 | Authentication.prototype.isGoogle = function () { 99 | return false; 100 | }; 101 | 102 | /** 103 | * Return authorization url. 104 | * 105 | * @param {string} [name] Name to prepend to URL. 106 | */ 107 | Authentication.prototype.authUrl = function (name) { 108 | if (name) { 109 | return name + "." + this._authUrl; 110 | } 111 | return this._authUrl; 112 | }; 113 | 114 | /** 115 | * Create basic request headers. 116 | * @private 117 | */ 118 | Authentication.prototype._getHeaders = function (headers) { 119 | var lowHeaders = {}, 120 | header; 121 | 122 | // Get lower-cased headers. 123 | headers = headers || {}; 124 | for (header in headers) { 125 | if (headers.hasOwnProperty(header)) { 126 | lowHeaders[header.toString().toLowerCase()] = headers[header]; 127 | } 128 | } 129 | 130 | // Add default parameters. 131 | lowHeaders['date'] = lowHeaders['date'] || new Date().toUTCString(); 132 | lowHeaders['host'] = lowHeaders['host'] || this._authUrl; 133 | 134 | return lowHeaders; 135 | }; 136 | 137 | /** 138 | * Sign request headers and return new headers. 139 | * 140 | * @param {string} [method] HTTP method (verb). 141 | * @param {string} [path] HTTP path. 142 | * @param {Object} [headers] HTTP headers. 143 | * @returns {Object} Signed headers. 144 | */ 145 | Authentication.prototype.sign = function (method, path, headers) { 146 | // Nop. 147 | return headers; 148 | }; 149 | 150 | /** 151 | * Create a new signed request object. 152 | * 153 | * @param {Object} options Options object. 154 | * @config {string} [method] HTTP method (verb). 155 | * @config {string} [path] HTTP path. 156 | * @config {Object} [headers] HTTP headers. 157 | * @config {Function} [resultsFn] Successful results data transform. 158 | */ 159 | Authentication.prototype.createRequest = function (options) { 160 | return new AuthenticatedRequest(this, options); 161 | }; 162 | 163 | /** 164 | * Create a new signed request object with JSON results from XML. 165 | * 166 | * @param {Object} options Options object. 167 | * @config {string} [method] HTTP method (verb). 168 | * @config {string} [path] HTTP path. 169 | * @config {Object} [headers] HTTP headers. 170 | * @config {Function} [resultsFn] Successful results data transform. 171 | */ 172 | Authentication.prototype.createXmlRequest = function (options) { 173 | return new AuthenticatedXmlRequest(this, options); 174 | }; 175 | 176 | module.exports.Authentication = Authentication; 177 | }()); 178 | -------------------------------------------------------------------------------- /lib/provider/aws/blob/blob.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview AWS Blob. 3 | */ 4 | 5 | (function () { 6 | var util = require('util'), 7 | utils = require("../../../utils"), 8 | CloudError = require("../../../errors").CloudError, 9 | ReadStream = require("../../../stream").ReadStream, 10 | WriteStream = require("../../../stream").WriteStream, 11 | BaseBlob = require("../../../base/blob").Blob, 12 | AwsBlob; 13 | 14 | /** 15 | * Blob class. 16 | * 17 | * @param {base.Container} cont Container object. 18 | * @param {Object} attrs Attributes. 19 | * @config {string} name Name. 20 | * @config {string} created Creation date. 21 | * @config {string} lastModified Last modified date. 22 | * @config {number} size Byte size of object. 23 | * @config {string} etag ETag. 24 | * @extends base.blob.Blob 25 | * @exports AwsBlob as provider.aws.blob.Blob 26 | * @constructor 27 | */ 28 | AwsBlob = function () { 29 | BaseBlob.apply(this, arguments); 30 | }; 31 | 32 | util.inherits(AwsBlob, BaseBlob); 33 | 34 | /** 35 | * @private 36 | */ 37 | AwsBlob.prototype._errorFnRequest = function () { 38 | var conn = this.container.connection; 39 | return function (err, request, response) { 40 | var translated = conn._translateErrors(err, request, response); 41 | request.emit('error', translated || err); 42 | }; 43 | }; 44 | 45 | /** 46 | * @private 47 | */ 48 | AwsBlob.prototype._errorFnStream = function () { 49 | var conn = this.container.connection; 50 | return function (err, stream, response) { 51 | var translated = conn._translateErrors(err, stream.request, response); 52 | stream.emit('error', translated || err); 53 | }; 54 | }; 55 | 56 | /** 57 | * Common request wrapper. 58 | * 59 | * @param {string} method HTTP verb. 60 | * @param {Object} [options] Request options. 61 | * @param {Object} [extra] Extra Auth Request configs. 62 | * @private 63 | */ 64 | AwsBlob.prototype._basicRequest = function (method, options, extra) { 65 | options = options || {}; 66 | extra = extra || {}; 67 | var self = this, 68 | auth = self.container.connection.authentication, 69 | meta = utils.extractMeta(options); 70 | 71 | if (!method) { throw new CloudError("Method required."); } 72 | 73 | return auth.createRequest(utils.extend(meta, { 74 | method: method, 75 | encoding: 'utf8', 76 | path: "/" + self.name, 77 | headers: utils.extend(options.headers, { 78 | 'host': auth.authUrl(self.container.name) 79 | }), 80 | resultsFn: extra.resultsFn || function () { 81 | return { 82 | blob: self 83 | }; 84 | }, 85 | errorFn: extra.errorFn || self._errorFnRequest() 86 | })); 87 | }; 88 | 89 | /** 90 | * @see base.blob.Blob#get 91 | */ 92 | AwsBlob.prototype.get = function (options) { 93 | options = options || {}; 94 | var self = this, 95 | conn = self.container.connection, 96 | auth = conn.authentication, 97 | meta = utils.extractMeta(options), 98 | encoding = options.encoding || null, 99 | stream; 100 | 101 | stream = new ReadStream(auth.createRequest(utils.extend(meta, { 102 | encoding: encoding, 103 | path: "/" + self.name, 104 | headers: utils.extend(options.headers, { 105 | 'host': auth.authUrl(self.container.name) 106 | }) 107 | })), { 108 | errorFn: self._errorFnStream(), 109 | endFn: function () { 110 | return { 111 | blob: self 112 | }; 113 | } 114 | }); 115 | return stream; 116 | }; 117 | 118 | /** 119 | * @see base.blob.Blob#head 120 | */ 121 | AwsBlob.prototype.head = function (options) { 122 | return this._basicRequest("HEAD", options); 123 | }; 124 | 125 | /** 126 | * @see base.blob.Blob#put 127 | */ 128 | AwsBlob.prototype.put = function (options) { 129 | options = options || {}; 130 | var self = this, 131 | auth = self.container.connection.authentication, 132 | meta = utils.extractMeta(options), 133 | encoding = options.encoding || null; 134 | 135 | return new WriteStream(auth.createRequest(utils.extend(meta, { 136 | method: "PUT", 137 | encoding: encoding, 138 | path: "/" + self.name, 139 | headers: utils.extend(options.headers, { 140 | 'host': auth.authUrl(self.container.name) 141 | }) 142 | })), { 143 | errorFn: self._errorFnStream(), 144 | endFn: function () { 145 | return { 146 | blob: self 147 | }; 148 | } 149 | }); 150 | }; 151 | 152 | /** 153 | * @see base.blob.Blob#del 154 | */ 155 | AwsBlob.prototype.del = function (options) { 156 | var self = this, 157 | conn = self.container.connection, 158 | notFound = false, 159 | getResults; 160 | 161 | /** @private */ 162 | getResults = function () { 163 | return { 164 | blob: self, 165 | notFound: notFound 166 | }; 167 | }; 168 | 169 | return this._basicRequest("DELETE", options, { 170 | resultsFn: getResults, 171 | errorFn: function (err, request, response) { 172 | var translated = conn._translateErrors(err, request, response); 173 | if (translated && translated.isNotFound()) { 174 | // NotFound is OK. 175 | notFound = true; 176 | request.emit('end', getResults()); 177 | } else { 178 | request.emit('error', translated || err); 179 | } 180 | } 181 | }); 182 | }; 183 | 184 | module.exports.Blob = AwsBlob; 185 | }()); 186 | -------------------------------------------------------------------------------- /lib/provider/aws/authentication.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview AWS Authentication. 3 | */ 4 | 5 | (function () { 6 | // Requires. 7 | var http = require('http'), 8 | crypto = require('crypto'), 9 | parse = require('url').parse, 10 | util = require('util'), 11 | utils = require("../../utils"), 12 | BaseAuthentication = require("../../base/authentication").Authentication, 13 | AwsAuthentication; 14 | 15 | /** 16 | * AWS Authentication class. 17 | * 18 | * *See*: http://docs.amazonwebservices.com/AmazonS3/latest/API/ 19 | * 20 | * @param {Object} options Options object. 21 | * @config {string} account Account name. 22 | * @config {string} secretKey Secret key. 23 | * @config {string} [ssl=false] Use SSL? 24 | * @config {string} [authUrl] Authentication URL. 25 | * @config {number} [timeout] HTTP timeout in seconds. 26 | * @exports AwsAuthentication as provider.aws.Authentication 27 | * @extends base.Authentication 28 | * @constructor 29 | */ 30 | AwsAuthentication = function (options) { 31 | // Patch AWS-specific options. 32 | options.authUrl = options.authUrl || "s3.amazonaws.com"; 33 | 34 | // Call superclass. 35 | BaseAuthentication.call(this, options); 36 | 37 | this._CUSTOM_HEADER_PREFIX = "x-amz"; 38 | this._CUSTOM_HEADER_RE = /^x-amz-/i; 39 | this._SIGNATURE_ID = "AWS"; 40 | this._CONN_CLS = require("./connection").Connection; 41 | }; 42 | 43 | util.inherits(AwsAuthentication, BaseAuthentication); 44 | 45 | /** Test provider (AWS). */ 46 | AwsAuthentication.prototype.isAws = function () { 47 | return true; 48 | }; 49 | 50 | /** 51 | * Return canonical AMZ headers. 52 | * 53 | * > "x-amz headers are canonicalized by: 54 | * > Lower-case header name 55 | * > Headers sorted by header name 56 | * > The values of headers whose names occur more than once should be white 57 | * > space-trimmed and concatenated with comma separators to be compliant 58 | * > with section 4.2 of RFC 2616. 59 | * > remove any whitespace around the colon in the header 60 | * > remove any newlines ('\n') in continuation lines 61 | * > separate headers by newlines ('\n')" 62 | * 63 | * @see http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html 64 | */ 65 | AwsAuthentication.prototype._getCanonicalHeaders = function (headers) { 66 | var canonHeaders = [], 67 | header, 68 | value, 69 | customHeaders; 70 | 71 | // Extract all Custom headers. 72 | for (header in headers) { 73 | if (headers.hasOwnProperty(header)) { 74 | if (this._CUSTOM_HEADER_RE.test(header)) { 75 | // Extract value and flatten. 76 | value = headers[header]; 77 | if (Array.isArray(value)) { 78 | value = value.join(','); 79 | } 80 | 81 | canonHeaders.push(header.toString().toLowerCase() + ":" + value); 82 | } 83 | } 84 | } 85 | 86 | // Sort and create string. 87 | return canonHeaders.sort().join("\n"); 88 | }; 89 | 90 | /** 91 | * Return canonical AMZ resource. 92 | * 93 | * > "The resource is the bucket and key (if applicable), separated by a '/'. 94 | * > If the request you are signing is for an ACL or a torrent file, you 95 | * > should include ?acl or ?torrent in the resource part of the canonical 96 | * > string. No other query string parameters should be included, however." 97 | * 98 | * @see http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html 99 | */ 100 | AwsAuthentication.prototype._getCanonicalResource = function ( 101 | resource, 102 | headers 103 | ) { 104 | // Strip off cname bucket, if present. 105 | var canonResource = parse(resource, true).pathname, 106 | bucketRe = new RegExp("." + this._authUrl + "$"), 107 | bucketName; 108 | 109 | if (bucketRe.test(headers.host)) { 110 | bucketName = headers.host.replace(bucketRe, ''); 111 | canonResource = parse("/" + bucketName + resource, true).pathname; 112 | } 113 | 114 | return canonResource; 115 | }; 116 | 117 | /** 118 | * Create string to sign. 119 | * 120 | * > "The string to be signed is formed by appending the REST verb, 121 | * > content-md5 value, content-type value, date value, canonicalized x-amz 122 | * > headers (see recipe below), and the resource; all separated by newlines. 123 | * > (If you cannot set the Date header, use the x-amz-date header as 124 | * > described below.)" 125 | * 126 | * Also: 127 | * 128 | * > "The content-type and content-md5 values are optional, but if you do 129 | * > not include them you must still insert a newline at the point where these 130 | * > values would normally be inserted." 131 | * 132 | * @see http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html 133 | */ 134 | AwsAuthentication.prototype._getStringToSign = function ( 135 | method, 136 | path, 137 | headers 138 | ) { 139 | var customHeaders = this._getCanonicalHeaders(headers), 140 | customResource = this._getCanonicalResource(path, headers), 141 | parts; 142 | 143 | parts = [ 144 | method, 145 | headers['content-md5'] || '', 146 | headers['content-type'] || '', 147 | headers['date'] ? headers['date'] : '', 148 | ]; 149 | 150 | if (customHeaders) { 151 | parts.push(customHeaders); 152 | } 153 | if (customResource) { 154 | parts.push(customResource); 155 | } 156 | 157 | return parts.join("\n"); 158 | }; 159 | 160 | /** 161 | * Create signature. 162 | * @private 163 | */ 164 | function getSignature(secretKey, stringToSign) { 165 | return crypto 166 | .createHmac('sha1', secretKey) 167 | .update(stringToSign, "utf8") 168 | .digest('base64'); 169 | } 170 | 171 | /** 172 | * @see base.Authentication#sign 173 | */ 174 | AwsAuthentication.prototype.sign = function (method, path, headers) { 175 | path = path || "/"; 176 | headers = utils.extend(this._getHeaders(headers)); 177 | headers['authorization'] = this._SIGNATURE_ID + " " + this._account + ":" + 178 | getSignature(this._secretKey, 179 | this._getStringToSign(method, path, headers)); 180 | 181 | return headers; 182 | }; 183 | 184 | module.exports.Authentication = AwsAuthentication; 185 | }()); 186 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Development 4 | --- 5 | 6 | # Development Guide 7 | Sunny development is mostly controlled by [Jake][jake] tasks, uses 8 | [NodeUnit][nu] and [JsHint][jshint] for testing, and has a horribly contrived 9 | document build system. 10 | 11 | [jake]: https://github.com/mde/jake 12 | [nu]: https://github.com/caolan/nodeunit 13 | [jshint]: https://github.com/jshint/jshint/ 14 | 15 | Development help is most welcome! There is a long task list in source at 16 | "./TODO.md" of improvements and enhancements that would be great to get 17 | going. 18 | 19 | This guide is for Sunny version: **v{{ site.version }}**. 20 | 21 | ## Dev. Installation 22 | To start developing on Sunny, just clone the repository and NPM install 23 | the dev. dependencies: 24 | 25 | $ git clone git@github.com:ryan-roemer/node-sunny.git 26 | $ cd node-sunny 27 | $ npm install 28 | 29 | which should install all the required node packages to "./node_modules". 30 | 31 | By default, no packages are installed globally (makes for a cleaner 32 | environment). The Jake targets all assume a "./node_modules" based path. 33 | And there is a simple shell wrapper script for invoking Jake: 34 | 35 | $ ./jake.sh 36 | 37 | If you globally installed Jake, you can just use ``jake `` instead. 38 | 39 | ## Jake Targets 40 | Here are some relevant Jake targets: 41 | 42 | $ ./jake.sh -T 43 | jake dev:cruft # Check for debugging snippets and 44 | bad code style. 45 | jake dev:style # Run style checks. 46 | jake test:all # Run all tests. 47 | jake test:core # Run core module tests. 48 | jake test:live # Run unit tests on real datastores 49 | (requires configuration(s)). 50 | jake build:clean # Clean all build files. 51 | jake build:docs # Build all documentation. 52 | ... 53 | 54 | ## Tests and Checks 55 | The Sunny codebase should pass all tests and quality checks. 56 | 57 | ### Core Tests 58 | The core unit tests do not require a configuration or network connection, and 59 | can be run with: 60 | 61 | $ ./jake.sh test:core 62 | 63 | ### Live Tests 64 | The live unit tests perform cloud operations against **real datastores**. 65 | While unlikely to cause harm, you should **not** run the live tests against 66 | a production datastore. The live tests create test containers of the form: 67 | "``sunnyjs-livetest-``" in a setup phase. These containers should 68 | be deleted on teardown, regardless of whether or not the unit tests passed, 69 | but the whole test framework sometimes breaks down on failures (particularly 70 | in development). So, developers may end up having to manually cleanup a 71 | cloud account and delete all "``sunnyjs-livetest-*``" containers. 72 | 73 | Note that live tests *can* randomly fail due to cloud provider service, 74 | network, throttling, etc. issues. In particular, (anecdotally) Google Storage 75 | is much more apt to random failures than AWS. For example, sometimes Google 76 | Storage will return "bucket not found" for containers that clearly exist (in a 77 | manner that other cloud clients like CyberDuck throw a similar error). Also, 78 | Google Storage throws throttling errors (``SlowDown`` error code) on 79 | occasion. Throttling retries are on the list of future enhancements, but for 80 | the present, the live tests just have to deal with intermittent failures. 81 | Finally, make sure to check your appropriate cloud status if the tests are 82 | failing in unusual or unexpected ways. 83 | 84 | To run the live tests, first create a configuration JavaScript file to be 85 | ``require``'ed by Node: 86 | 87 | {% highlight javascript %} 88 | module.exports.Configuration = [ 89 | { 90 | provider: 'aws', 91 | account: "ACCOUNT_NAME", 92 | secretKey: "ACCOUNT_SECRET_KEY", 93 | ssl: true 94 | }, 95 | { 96 | provider: 'google', 97 | account: "ACCOUNT_NAME", 98 | secretKey: "ACCOUNT_SECRET_KEY", 99 | ssl: false 100 | } 101 | /* Other account configurations... */ 102 | ]; 103 | {% endhighlight %} 104 | 105 | By default, Sunny looks for this file in "./local/live-test-config.js", so 106 | place it there if you can. 107 | 108 | Sunny will run the full suite of live tests against *each* configuration 109 | object. As this has real network back-and-forth, the live tests do take a 110 | while -- a single configuration on a MacBook Pro currently takes around 111 | 50-60 seconds. 112 | 113 | Once you have your configuration file, try: 114 | 115 | $ ./jake.sh test:live[PATH/TO/YOUR/test-config.js] 116 | 117 | ... or with the default path "./local/live-test-config.js": 118 | 119 | $ ./jake.sh test:live 120 | 121 | ### Run All Tests 122 | If you have a live test configuration with the default path, you can run: 123 | 124 | $ ./jake.sh test:all 125 | 126 | to execute all tests. 127 | 128 | ### Style 129 | We have a simple Jake target for JsHint style checks: 130 | 131 | $ ./jake.sh dev:style 132 | 133 | The whole library and tests should pass. 134 | 135 | ## Documentation 136 | Building the Sunny documentation unfortunately requires a bit of an extensive 137 | setup process. 138 | 139 | Sunny use [JsDoc Toolkit 2][jsdoc] for API documentation, run through a 140 | custom Markdown plugin (dependency handle already by NPM). For the rest of 141 | the site documentation (including this page), Sunny uses [Jekyll][jekyll] with 142 | a [Pygments][pyg] installation, with [Stylus][stylus] for CSS generation 143 | (dependency handled already). 144 | 145 | [jsdoc]: http://code.google.com/p/jsdoc-toolkit/ 146 | [jekyll]: https://github.com/mojombo/jekyll 147 | [pyg]: http://pygments.org/ 148 | [stylus]: http://learnboost.github.com/stylus/ 149 | 150 | The Jekyll source docs are located in "./docs" and the JsDoc source docs 151 | are in "./docs_api". The Jake build target cobbles everything together, 152 | processes the CSS and outputs everything to "./docs_html" in final form. 153 | 154 | ### Installation 155 | First, install [JsDoc Toolkit 2][jsdoc]. On a Mac, this can be done with: 156 | 157 | $ brew install jsdoc-toolkit 158 | 159 | Then, install [Jekyll][jekyll]. (You may need ``sudo`` depending on if you 160 | are in a virtual Ruby environment). 161 | 162 | $ gem install jekyll 163 | 164 | Finally, install [Pygments][pyg]. (You may need ``sudo`` depending on if you 165 | are in a virtual Python environment). 166 | 167 | $ pip install pygments 168 | 169 | ### Build the Documents 170 | To build all of the documents, run the Jake target: 171 | 172 | $ ./jake.sh build:docs 173 | 174 | which will output the complete documents (site and API) to "./docs_html". 175 | -------------------------------------------------------------------------------- /test/live/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Live test utilities. 3 | */ 4 | 5 | /** 6 | * @name test.live.utils 7 | */ 8 | (function () { 9 | var assert = require('assert'), 10 | async = require('async'), 11 | uuid = require('node-uuid'), 12 | sunnyUtils = require("../../lib/utils"), 13 | Utils; 14 | 15 | /** 16 | * @exports Utils as test.live.utils.Utils 17 | * @namespace 18 | */ 19 | Utils = { 20 | /** 21 | * Create string UUID. 22 | */ 23 | getUuid: function (len) { 24 | len = len || 32; 25 | return uuid().toString().replace(/-/g, '').substr(0, len); 26 | }, 27 | 28 | /** 29 | * Create test container name. 30 | * 31 | * AWS: 32 | * 33 | * - Bucket names MUST be between 3 and 255 characters long. 34 | * - Bucket names should be between 3 and 63 characters long. 35 | * 36 | * Rackspace: 37 | * 38 | * - The URL encoded name must be less than 256 bytes and cannot contain a 39 | * forward slash '/' character. 40 | */ 41 | testContainerName: function (len) { 42 | return "sunnyjs-livetest-" + Utils.getUuid(len); 43 | }, 44 | 45 | /** 46 | * Common error handler. 47 | */ 48 | errHandle: function (test) { 49 | return function (err) { 50 | test.ok(false, "Should not have error. Got: " + err); 51 | test.done(); 52 | }; 53 | }, 54 | 55 | /** 56 | * Create a test object with test container and blobs from list. 57 | * 58 | * @param {Array} blobs List of blobs to create. 59 | * @param {Object} tests Object (hash) of tests. 60 | */ 61 | createTestSetup: function (blobs, tests) { 62 | blobs = blobs || []; 63 | tests = tests || {}; 64 | 65 | function errHandle(err) { 66 | assert.ok(false, "Should not have error. Got: " + err); 67 | } 68 | 69 | return sunnyUtils.extend({ 70 | setUp: function (setUpCallback, opts) { 71 | var self = this; 72 | 73 | self.containerName = Utils.testContainerName(); 74 | self.container = null; 75 | self.blobs = []; 76 | 77 | async.series([ 78 | // Create a simple, random container. 79 | function (asyncCallback) { 80 | var request = opts.conn.putContainer(self.containerName); 81 | request.on('error', errHandle); 82 | request.on('end', function (results) { 83 | self.container = results.container; 84 | assert.ok(self.container, "Should have container."); 85 | asyncCallback(null); 86 | }); 87 | request.end(); 88 | }, 89 | 90 | // Fill empty blobs (parallel). 91 | function (asyncCallback) { 92 | var innerSeries = []; 93 | 94 | if (blobs.length === 0) { 95 | asyncCallback(null); 96 | 97 | } else { 98 | // PUT in parallel, and go to outer series when done. 99 | blobs.forEach(function (blobName, index) { 100 | innerSeries.push(function (innerCb) { 101 | var stream = self.container.putBlob(blobName); 102 | stream.on('error', errHandle); 103 | stream.on('end', function (results) { 104 | self.blobs.push(results.blob); 105 | assert.ok(results.blob, "Should have PUT blob."); 106 | innerCb(null); 107 | }); 108 | stream.end(blobName); 109 | }); 110 | }); 111 | 112 | async.parallel(innerSeries, function (err) { 113 | assert.ok(!err, "Should not have error."); 114 | asyncCallback(); 115 | }); 116 | } 117 | } 118 | ], function (err) { 119 | assert.ok(!err, "Should not have error."); 120 | setUpCallback(); 121 | }); 122 | }, 123 | tearDown: function (tearDownCallback, opts) { 124 | var self = this, 125 | seriesResults; 126 | 127 | // Find all blobs in container and delete them all. 128 | async.series([ 129 | // Get container. 130 | function (asyncCallback) { 131 | var request = opts.conn.getContainer(self.containerName, { 132 | validate: true 133 | }); 134 | request.on('error', errHandle); 135 | request.on('end', function (results) { 136 | seriesResults = results; 137 | asyncCallback(null); 138 | }); 139 | request.end(); 140 | }, 141 | 142 | // Get list of blobs (assumed under default=1000) 143 | function (asyncCallback) { 144 | assert.ok(seriesResults.container); 145 | 146 | var request = seriesResults.container.getBlobs(); 147 | request.on('error', errHandle); 148 | request.on('end', function (results) { 149 | seriesResults = results; 150 | asyncCallback(null); 151 | }); 152 | request.end(); 153 | }, 154 | 155 | // Delete all blobs. 156 | function (asyncCallback) { 157 | var innerSeries = []; 158 | 159 | assert.ok(seriesResults.blobs); 160 | if (seriesResults.blobs.length === 0) { 161 | asyncCallback(null); 162 | 163 | } else { 164 | // DELETE in parallel, and go to outer series when done. 165 | seriesResults.blobs.forEach(function (blob, index) { 166 | innerSeries.push(function (innerCb) { 167 | var request = blob.del(); 168 | request.on('error', errHandle); 169 | request.on('end', function (results) { 170 | innerCb(null); 171 | }); 172 | request.end(); 173 | }); 174 | }); 175 | 176 | async.parallel(innerSeries, function (err) { 177 | assert.ok(!err, "Should not have error."); 178 | asyncCallback(null); 179 | }); 180 | } 181 | }, 182 | 183 | // Delete random container. 184 | function (asyncCallback) { 185 | var request = opts.conn.delContainer(self.containerName); 186 | request.on('error', errHandle); 187 | request.on('end', function (results) { 188 | asyncCallback(null); 189 | }); 190 | request.end(); 191 | } 192 | ], function (err) { 193 | assert.ok(!err, "Should not have error."); 194 | tearDownCallback(); 195 | }); 196 | } 197 | }, tests); 198 | } 199 | }; 200 | 201 | module.exports = Utils; 202 | }()); 203 | -------------------------------------------------------------------------------- /lib/base/connection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Base Connection. 3 | */ 4 | 5 | (function () { 6 | var utils = require("../utils"), 7 | CloudError = require("../errors").CloudError, 8 | Connection; 9 | 10 | /** 11 | * Abstract base connection class. 12 | * 13 | * @param {base.Authentication} auth Authentication object. 14 | * @exports Connection as base.Connection 15 | * @constructor 16 | */ 17 | Connection = function (auth) { 18 | var self = this; 19 | 20 | // Validation. 21 | if (!auth) { throw new Error("No authentication object."); } 22 | 23 | self._auth = auth; 24 | 25 | // Header prefixes. 26 | self._HEADER_PREFIX = null; 27 | self._METADATA_PREFIX = null; 28 | 29 | // Map errors to class. 30 | self._ERRORS = { 31 | CONTAINER_NOT_FOUND: { 32 | attrs: null, 33 | error: { 34 | message: "Container not found.", 35 | types: CloudError.TYPES.NOT_FOUND 36 | }, 37 | errorMap: null 38 | }, 39 | CONTAINER_NOT_EMPTY: { 40 | attrs: null, 41 | error: { 42 | message: "Container not empty.", 43 | types: CloudError.TYPES.NOT_EMPTY 44 | }, 45 | errorMap: null 46 | }, 47 | CONTAINER_INVALID_NAME: { 48 | attrs: null, 49 | error: { 50 | message: "Invalid container name.", 51 | types: CloudError.TYPES.INVALID_NAME 52 | }, 53 | errorMap: null 54 | }, 55 | CONTAINER_OTHER_OWNER: { 56 | attrs: null, 57 | error: { 58 | message: "Container already owned by another.", 59 | types: CloudError.TYPES.NOT_OWNER 60 | }, 61 | errorMap: null 62 | }, 63 | // **Note**: This is mapped to non-error for GSFD and not used elsewhere. 64 | CONTAINER_ALREADY_OWNED_BY_YOU: { 65 | attrs: null, 66 | error: { 67 | message: "Container already owned by you.", 68 | types: CloudError.TYPES.ALREADY_OWNED_BY_YOU 69 | }, 70 | errorMap: null 71 | }, 72 | BLOB_NOT_FOUND: { 73 | attrs: null, 74 | error: { 75 | message: "Blob not found.", 76 | types: CloudError.TYPES.NOT_FOUND 77 | }, 78 | errorMap: null 79 | }, 80 | BLOB_INVALID_NAME: { 81 | attrs: null, 82 | error: { 83 | message: "Invalid blob name.", 84 | types: CloudError.TYPES.INVALID_NAME 85 | }, 86 | errorMap: null 87 | } 88 | }; 89 | }; 90 | 91 | Object.defineProperties(Connection.prototype, { 92 | /** 93 | * Cloud header prefix (e.g., 'x-amz-'). 94 | * 95 | * @name Connection#headerPrefix 96 | * @type string 97 | */ 98 | headerPrefix: { 99 | get: function () { 100 | return this._HEADER_PREFIX; 101 | } 102 | }, 103 | 104 | /** 105 | * Cloud header metadata prefix (e.g., 'x-amz-meta-'). 106 | * 107 | * @name Connection#metadataPrefix 108 | * @type string 109 | */ 110 | metadataPrefix: { 111 | get: function () { 112 | return this._METADATA_PREFIX; 113 | } 114 | }, 115 | 116 | /** 117 | * Authentication object. 118 | * 119 | * @name Connection#authentication 120 | * @type base.Authentication 121 | */ 122 | authentication: { 123 | get: function () { 124 | return this._auth; 125 | } 126 | } 127 | }); 128 | 129 | /** 130 | * Create container object. 131 | * @private 132 | */ 133 | Connection.prototype._createContainer = function (name) { 134 | throw new Error("Not implemented."); 135 | }; 136 | 137 | /** 138 | * Check if known error. 139 | * 140 | * @returns {boolean} True if error. 141 | * @private 142 | */ 143 | Connection.prototype._isError = function (errItem, err, response) { 144 | throw new Error("Not implemented."); 145 | }; 146 | 147 | /** 148 | * Translate errors. 149 | * 150 | * @returns {Error|errors.CloudError} Translated error. 151 | * @private 152 | */ 153 | Connection.prototype._translateErrors = function (err, request, response) { 154 | var self = this, 155 | key, 156 | errObj, 157 | error; 158 | 159 | for (key in self._ERRORS) { 160 | if (self._ERRORS.hasOwnProperty(key)) { 161 | errObj = self._ERRORS[key]; 162 | if (self._isError(errObj, err, response)) { 163 | // Favor errorMap over default error. 164 | error = errObj.errorMap 165 | ? (errObj.errorMap[request.method] || errObj.error) 166 | : errObj.error; 167 | 168 | if (!error) { 169 | throw new Error("Undefined error for errObj: " + errObj + 170 | ", err: " + err); 171 | } 172 | 173 | return new CloudError(error.message, { 174 | error: err, 175 | types: error.types, 176 | response: response 177 | }); 178 | } 179 | } 180 | } 181 | 182 | return null; 183 | }; 184 | 185 | /** 186 | * Completion event ('``end``'). 187 | * 188 | * @name base.Connection#getContainers_end 189 | * @event 190 | * @param {Object} results Results object. 191 | * @config {Array} containers List of container objects. 192 | */ 193 | /** 194 | * Error event ('``error``'). 195 | * 196 | * @name base.Connection#getContainers_error 197 | * @event 198 | * @param {Error|errors.CloudError} err Error object. 199 | */ 200 | /** 201 | * Get a list of Containers. 202 | * 203 | * ## Events 204 | * - [``end(results, meta)``](#getContainers_end) 205 | * - [``error(err)``](#getContainers_error) 206 | * 207 | * ## Note 208 | * Both AWS and GSFD only offer listing **all** containers, without 209 | * paging or prefix options. 210 | * 211 | * @param {Object} [options] Options object. 212 | * @config {Object} [headers] Raw headers to add. 213 | * @config {Object} [cloudHeaders] Cloud provider headers to add. 214 | * @config {Object} [metadata] Cloud metadata to add. 215 | * @returns {AuthenticatedRequest} Request object. 216 | */ 217 | Connection.prototype.getContainers = function (options) { 218 | throw new Error("Not implemented."); 219 | }; 220 | 221 | /** 222 | * Create container object and GET. 223 | * 224 | * @see base.blob.Container#get 225 | */ 226 | Connection.prototype.getContainer = function (name, options) { 227 | return this._createContainer(name).get(options); 228 | }; 229 | 230 | /** 231 | * Create container object and PUT. 232 | * 233 | * @see base.blob.Container#put 234 | */ 235 | Connection.prototype.putContainer = function (name, options) { 236 | return this._createContainer(name).put(options); 237 | }; 238 | 239 | /** 240 | * Create container object and DELETE. 241 | * 242 | * @see base.blob.Container#del 243 | */ 244 | Connection.prototype.delContainer = function (name, options) { 245 | return this._createContainer(name).del(options); 246 | }; 247 | 248 | module.exports.Connection = Connection; 249 | }()); 250 | -------------------------------------------------------------------------------- /lib/provider/aws/blob/container.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview AWS Container. 3 | */ 4 | 5 | /** 6 | * @name provider.aws.blob 7 | */ 8 | (function () { 9 | var util = require('util'), 10 | utils = require("../../../utils"), 11 | DummyRequest = require("../../../request").DummyRequest, 12 | Blob = require("./blob").Blob, 13 | BaseContainer = require("../../../base/blob").Container, 14 | AwsContainer; 15 | 16 | /** 17 | * Container class. 18 | * 19 | * @param {base.Connection} conn Connection object. 20 | * @param {Object} attrs Attributes. 21 | * @config {string} name Name. 22 | * @config {string} created Creation date. 23 | * @extends base.blob.Container 24 | * @exports AwsContainer as provider.aws.blob.Container 25 | * @constructor 26 | */ 27 | AwsContainer = function () { 28 | BaseContainer.apply(this, arguments); 29 | }; 30 | 31 | util.inherits(AwsContainer, BaseContainer); 32 | 33 | /** 34 | * Upgrade to array of single element if not already. 35 | */ 36 | function mkArray(val) { 37 | return Array.isArray(val) ? val : [val]; 38 | } 39 | 40 | /** 41 | * @see base.blob.Container#_createBlob 42 | * @private 43 | */ 44 | AwsContainer.prototype._createBlob = function (name) { 45 | return new Blob(this, { name: name }); 46 | }; 47 | 48 | /** 49 | * GET Container from cloud. 50 | * 51 | * **Note**: AWS cannot tell if the container already exists on a PUT, 52 | * so ``alreadyCreated`` result is always false. Google Storage can. 53 | * 54 | * @see base.blob.Container#get 55 | */ 56 | AwsContainer.prototype.get = function (options) { 57 | var self = this, 58 | conn = self.connection, 59 | auth = conn.authentication, 60 | meta = utils.extractMeta(options), 61 | alreadyCreated = false, 62 | getResults; 63 | 64 | // Strict boolean values for options to preserve defaults. 65 | options = utils.extend(options); 66 | options.validate = options.validate === true; 67 | options.create = options.create === true; 68 | 69 | /** @private */ 70 | getResults = function () { 71 | return { 72 | container: self, 73 | alreadyCreated: alreadyCreated 74 | }; 75 | }; 76 | 77 | if (options.create) { 78 | // Do a PUT and trap the "already exists" error. 79 | return auth.createRequest(utils.extend(meta, { 80 | encoding: 'utf8', 81 | method: "PUT", 82 | path: "/", 83 | headers: { 84 | 'content-length': 0, 85 | 'host': auth.authUrl(self.name) 86 | }, 87 | errorFn: function (err, request, response) { 88 | // AlreadyOwnedByYou is OK. 89 | var translated = conn._translateErrors(err, request, response); 90 | if (translated && translated.isAlreadyOwnedByYou()) { 91 | alreadyCreated = true; 92 | request.emit('end', getResults()); 93 | } else { 94 | request.emit('error', translated || err); 95 | } 96 | }, 97 | resultsFn: getResults 98 | })); 99 | } else if (options.validate) { 100 | // Do an empty list on the bucket. 101 | alreadyCreated = true; 102 | return auth.createRequest(utils.extend(meta, { 103 | encoding: 'utf8', 104 | path: "/?max-keys=0", 105 | headers: { 106 | 'content-length': 0, 107 | 'host': auth.authUrl(self.name) 108 | }, 109 | errorFn: function (err, request, response) { 110 | var translated = conn._translateErrors(err, request, response); 111 | request.emit('error', translated || err); 112 | }, 113 | resultsFn: getResults 114 | })); 115 | } else { 116 | // Return empty object. 117 | return new DummyRequest(utils.extend(meta, { 118 | resultsFn: getResults 119 | })); 120 | } 121 | }; 122 | 123 | /** 124 | * @see base.blob.Container#del 125 | */ 126 | AwsContainer.prototype.del = function (options) { 127 | var self = this, 128 | conn = self.connection, 129 | auth = conn.authentication, 130 | meta = utils.extractMeta(options), 131 | notFound = false, 132 | getResults; 133 | 134 | options = options || {}; 135 | 136 | /** @private */ 137 | getResults = function () { 138 | return { 139 | container: self, 140 | notFound: notFound 141 | }; 142 | }; 143 | 144 | return auth.createRequest(utils.extend(meta, { 145 | encoding: 'utf8', 146 | method: "DELETE", 147 | path: "/", 148 | headers: utils.extend(options.headers, { 149 | 'content-length': 0, 150 | 'host': auth.authUrl(self.name) 151 | }), 152 | errorFn: function (err, request, response) { 153 | var trans = conn._translateErrors(err, request, response); 154 | if (trans && (trans.isNotFound() || trans.isInvalidName())) { 155 | // NotFound, InvalidName is OK. 156 | notFound = true; 157 | request.emit('end', getResults()); 158 | } else { 159 | request.emit('error', trans || err); 160 | } 161 | }, 162 | resultsFn: getResults 163 | })); 164 | }; 165 | 166 | /** 167 | * @see base.blob.Container#getBlobs 168 | */ 169 | AwsContainer.prototype.getBlobs = function (options) { 170 | var self = this, 171 | conn = self.connection, 172 | auth = conn.authentication, 173 | meta = utils.extractMeta(options), 174 | params = {}; 175 | 176 | function _paramsAdd(key, value) { 177 | if (value) { 178 | params[key] = value; 179 | } 180 | } 181 | 182 | // Assemble parameters 183 | options = options || {}; 184 | params['max-keys'] = options.maxResults || 1000; 185 | _paramsAdd('prefix', options.prefix); 186 | _paramsAdd('delimiter', options.delimiter); 187 | _paramsAdd('marker', options.marker); 188 | 189 | return conn.authentication.createXmlRequest(utils.extend(meta, { 190 | encoding: 'utf8', 191 | path: "/", 192 | params: params, 193 | headers: utils.extend(options.headers, { 194 | 'content-length': 0, 195 | 'host': auth.authUrl(self.name) 196 | }), 197 | resultsFn: function (results) { 198 | var prefixes = mkArray(results.CommonPrefixes || []), 199 | keys = mkArray(results.Contents || []), 200 | hasNext = results.IsTruncated === 'true', 201 | blobs = [], 202 | dirNames = []; 203 | 204 | // Blobs. 205 | keys.forEach(function (obj) { 206 | blobs.push(new Blob(self, { 207 | name: obj.Key, 208 | lastModified: obj.LastModified, 209 | size: obj.Size, 210 | etag: obj.ETag 211 | })); 212 | }); 213 | 214 | // Pseudo-directories. 215 | prefixes.forEach(function (obj) { 216 | dirNames.push(obj.Prefix); 217 | }); 218 | 219 | return { 220 | blobs: blobs, 221 | dirNames: dirNames, 222 | hasNext: hasNext 223 | }; 224 | } 225 | })); 226 | }; 227 | 228 | module.exports.Container = AwsContainer; 229 | }()); 230 | -------------------------------------------------------------------------------- /docs_api/templates/jsdoc/publish.js: -------------------------------------------------------------------------------- 1 | /** Publish (called by jsdoc). */ 2 | function publish(symbolSet) { 3 | publish.conf = { // trailing slash expected for dirs 4 | ext: ".html", 5 | outDir: JSDOC.opt.d || SYS.pwd+"../out/jsdoc/", 6 | templatesDir: JSDOC.opt.t || SYS.pwd+"../templates/jsdoc/", 7 | symbolsDir: "symbols/", 8 | srcDir: "symbols/src/" 9 | }; 10 | 11 | // is source output is suppressed, just display the links to the source file 12 | if (JSDOC.opt.s && defined(Link) && Link.prototype._makeSrcLink) { 13 | Link.prototype._makeSrcLink = function(srcFilePath) { 14 | return "<"+srcFilePath+">"; 15 | } 16 | } 17 | 18 | // create the folders and subfolders to hold the output and static. 19 | IO.mkPath((publish.conf.outDir+"symbols/src").split("/")); 20 | //IO.mkPath((publish.conf.outDir+"static").split("/")); 21 | 22 | // used to allow Link to check the details of things being linked to 23 | Link.symbolSet = symbolSet; 24 | 25 | // create the required templates 26 | try { 27 | var classTemplate = new JSDOC.JsPlate(publish.conf.templatesDir+"class.tmpl"); 28 | var classesTemplate = new JSDOC.JsPlate(publish.conf.templatesDir+"allclasses.tmpl"); 29 | } 30 | catch(e) { 31 | print("Couldn't create the required templates: "+e); 32 | quit(); 33 | } 34 | 35 | // some utility filters 36 | function hasNoParent($) {return ($.memberOf == "")} 37 | function isaFile($) {return ($.is("FILE"))} 38 | function isaClass($) {return ($.is("CONSTRUCTOR") || $.isNamespace)} 39 | 40 | // get an array version of the symbolset, useful for filtering 41 | var symbols = symbolSet.toArray(); 42 | 43 | // Copy static files. 44 | //var staticSrc = publish.conf.templatesDir + "static"; 45 | //var destSrc = publish.conf.outDir + "static"; 46 | //var staticFiles = IO.ls(staticSrc); 47 | //for (var i = 0, len = staticFiles.length; i < len; ++i) { 48 | // var srcFile = staticFiles[i]; 49 | // var destPath = srcFile.replace(staticSrc, destSrc); 50 | // var destDir = dirname(destPath); 51 | // var destFile = basename(destPath); 52 | 53 | //// Make destination directory and copy. 54 | // IO.mkPath((destDir).split("/")); 55 | // IO.copyFile(srcFile, destDir, destFile); 56 | //} 57 | 58 | // create the hilited source code files 59 | var files = JSDOC.opt.srcFiles; 60 | for (var i = 0, l = files.length; i < l; i++) { 61 | var file = files[i]; 62 | var srcDir = publish.conf.outDir + "symbols/src/"; 63 | makeSrcFile(file, srcDir); 64 | } 65 | 66 | // get a list of all the classes in the symbolset 67 | var classes = symbols.filter(isaClass).sort(makeSortby("alias")); 68 | 69 | // create a filemap in which outfiles must be to be named uniquely, ignoring case 70 | if (JSDOC.opt.u) { 71 | var filemapCounts = {}; 72 | Link.filemap = {}; 73 | for (var i = 0, l = classes.length; i < l; i++) { 74 | var lcAlias = classes[i].alias.toLowerCase(); 75 | 76 | if (!filemapCounts[lcAlias]) filemapCounts[lcAlias] = 1; 77 | else filemapCounts[lcAlias]++; 78 | 79 | Link.filemap[classes[i].alias] = 80 | (filemapCounts[lcAlias] > 1)? 81 | lcAlias+"_"+filemapCounts[lcAlias] : lcAlias; 82 | } 83 | } 84 | 85 | // create a class index, displayed in the left-hand column of every class page 86 | Link.base = "../"; 87 | publish.classesIndex = classesTemplate.process(classes); // kept in memory 88 | 89 | // create each of the class pages 90 | for (var i = 0, l = classes.length; i < l; i++) { 91 | var symbol = classes[i]; 92 | 93 | symbol.events = symbol.getEvents(); // 1 order matters 94 | symbol.methods = symbol.getMethods(); // 2 95 | 96 | var output = ""; 97 | output = classTemplate.process(symbol); 98 | 99 | IO.saveFile(publish.conf.outDir+"symbols/", ((JSDOC.opt.u)? Link.filemap[symbol.alias] : symbol.alias) + publish.conf.ext, output); 100 | } 101 | 102 | // regenerate the index with different relative links, used in the index pages 103 | Link.base = ""; 104 | publish.classesIndex = classesTemplate.process(classes); 105 | 106 | // create the class index page 107 | try { 108 | var classesindexTemplate = new JSDOC.JsPlate(publish.conf.templatesDir+"index.tmpl"); 109 | } 110 | catch(e) { print(e.message); quit(); } 111 | 112 | var classesIndex = classesindexTemplate.process(classes); 113 | IO.saveFile(publish.conf.outDir, "index"+publish.conf.ext, classesIndex); 114 | classesindexTemplate = classesIndex = classes = null; 115 | 116 | // create the file index page 117 | try { 118 | var fileindexTemplate = new JSDOC.JsPlate(publish.conf.templatesDir+"allfiles.tmpl"); 119 | } 120 | catch(e) { print(e.message); quit(); } 121 | 122 | var documentedFiles = symbols.filter(isaFile); // files that have file-level docs 123 | var allFiles = []; // not all files have file-level docs, but we need to list every one 124 | 125 | for (var i = 0; i < files.length; i++) { 126 | allFiles.push(new JSDOC.Symbol(files[i], [], "FILE", new JSDOC.DocComment("/** */"))); 127 | } 128 | 129 | for (var i = 0; i < documentedFiles.length; i++) { 130 | var offset = files.indexOf(documentedFiles[i].alias); 131 | allFiles[offset] = documentedFiles[i]; 132 | } 133 | 134 | allFiles = allFiles.sort(makeSortby("name")); 135 | 136 | // output the file index page 137 | var filesIndex = fileindexTemplate.process(allFiles); 138 | IO.saveFile(publish.conf.outDir, "files"+publish.conf.ext, filesIndex); 139 | fileindexTemplate = filesIndex = files = null; 140 | } 141 | 142 | function basename(path) { 143 | return path.replace(/\\/g,'/').replace( /.*\//, '' ); 144 | } 145 | function dirname(path) { 146 | return path.replace(/\\/g,'/').replace(/\/[^\/]*$/, '');; 147 | } 148 | 149 | /** Just the first sentence (up to a full stop). Should not break on dotted variable names. */ 150 | function summarize(desc) { 151 | if (typeof desc != "undefined") 152 | return desc.match(/([\w\W]+?\.)[^a-z0-9_$]/i)? RegExp.$1 : desc; 153 | } 154 | 155 | /** Make a symbol sorter by some attribute. */ 156 | function makeSortby(attribute) { 157 | return function(a, b) { 158 | if (a[attribute] != undefined && b[attribute] != undefined) { 159 | a = a[attribute].toLowerCase(); 160 | b = b[attribute].toLowerCase(); 161 | if (a < b) return -1; 162 | if (a > b) return 1; 163 | return 0; 164 | } 165 | } 166 | } 167 | 168 | /** Pull in the contents of an external file at the given path. */ 169 | function include(path) { 170 | var path = publish.conf.templatesDir+path; 171 | return IO.readFile(path); 172 | } 173 | 174 | /** Pull in the contents of an external file at the given path. */ 175 | function includeTmpl(path) { 176 | try { 177 | var src = new JSDOC.JsPlate(publish.conf.templatesDir+path); 178 | } catch (e) { print(e.message); quit(); } 179 | 180 | return src.process(); 181 | } 182 | 183 | /** Turn a raw source file into a code-hilited page in the docs. */ 184 | function makeSrcFile(path, srcDir, name) { 185 | if (JSDOC.opt.s) return; 186 | 187 | if (!name) { 188 | name = path.replace(/\.\.?[\\\/]/g, "").replace(/[\\\/]/g, "_"); 189 | name = name.replace(/\:/g, "_"); 190 | } 191 | 192 | var src = {path: path, name:name, charset: IO.encoding, hilited: ""}; 193 | 194 | if (defined(JSDOC.PluginManager)) { 195 | JSDOC.PluginManager.run("onPublishSrc", src); 196 | } 197 | 198 | if (src.hilited) { 199 | IO.saveFile(srcDir, name+publish.conf.ext, src.hilited); 200 | } 201 | } 202 | 203 | /** Build output for displaying function parameters. */ 204 | function makeSignature(params) { 205 | if (!params) return "()"; 206 | var signature = "(" 207 | + 208 | params.filter( 209 | function($) { 210 | return $.name.indexOf(".") == -1; // don't show config params in signature 211 | } 212 | ).map( 213 | function($) { 214 | return $.name; 215 | } 216 | ).join(", ") 217 | + 218 | ")"; 219 | return signature; 220 | } 221 | 222 | /** Find symbol {@link ...} strings in text and turn into html links */ 223 | function resolveLinks(str, from) { 224 | str = str.replace(/\{@link ([^} ]+) ?\}/gi, 225 | function(match, symbolName) { 226 | return new Link().toSymbol(symbolName); 227 | } 228 | ); 229 | 230 | return str; 231 | } 232 | 233 | // Flush remaining log output. 234 | if (LOG.out) { 235 | LOG.out.flush(); 236 | LOG.out.close(); 237 | } 238 | -------------------------------------------------------------------------------- /Jakefile: -------------------------------------------------------------------------------- 1 | /** 2 | * Jakefile. 3 | */ 4 | /*global namespace: true, desc: true, task: true, complete: true, fail: true */ 5 | /////////////////////////////////////////////////////////////////////////////// 6 | // Requires 7 | var fs = require('fs'), 8 | path = require('path'), 9 | exec = require('child_process').exec, 10 | spawn = require('child_process').spawn, 11 | findit = require('findit'), 12 | cakepop = require('cakepop'), 13 | style = new cakepop.Style({ js: { config: "dev/jshint.json" }}); 14 | 15 | /////////////////////////////////////////////////////////////////////////////// 16 | // Constants 17 | var FILES_RE = { 18 | EXCLUDE: new RegExp("(^\\./(\\.git|node_modules|docs.*|local))"), 19 | ALL_JS: new RegExp("(Jakefile|\\.js$)"), 20 | LIB_JS: new RegExp("(\\.js$)") 21 | }; 22 | 23 | var NODE_UNIT_BIN = "./node_modules/nodeunit/bin/nodeunit"; 24 | var TESTS = { 25 | CORE: { 26 | path: "test/core.test.js" 27 | }, 28 | LIVE: { 29 | path: "test/live.test.js", 30 | config: "local/live-test-config.js" 31 | } 32 | }; 33 | 34 | var JSHINT_BIN = "./node_modules/.bin/jshint"; 35 | var JSHINT_CONF = "./dev/jshint.json"; 36 | 37 | var CRUFT_RE = [ 38 | "require\\(.*\\.js", 39 | "console\\.", 40 | ].join("|"); 41 | 42 | var DOCS_OUT = "./docs_html"; 43 | var DOCS_CSS_SRC = "./docs/css"; 44 | var DOCS_CSS_OUT = "./docs_html/css"; 45 | 46 | var STYLUS_BIN = "./node_modules/stylus/bin/stylus"; 47 | var STYLUS_OPTIONS = [ 48 | // "--compress", 49 | "--out " + DOCS_CSS_OUT 50 | ]; 51 | 52 | 53 | var JEKYLL_BIN = "jekyll"; 54 | var JEKYLL_SRC = "./docs"; 55 | 56 | var JSDOC_BIN = "jsdoc"; 57 | var JSDOC_CFG = "./docs_api/jsdoc-conf.js"; 58 | var JSDOC_OUT = DOCS_OUT + "/api"; 59 | var JSDOC_OPTIONS = [ 60 | "--conf=" + JSDOC_CFG, 61 | "--directory=" + JSDOC_OUT, 62 | "lib", 63 | "test" 64 | ]; 65 | 66 | var BUILD_FILES = [ 67 | DOCS_OUT 68 | ]; 69 | 70 | /////////////////////////////////////////////////////////////////////////////// 71 | // Helpers 72 | /** 73 | * Run process and dump output to stdout, stderr. 74 | * 75 | * Arguments are same as for child_process.spawn. 76 | */ 77 | var runProcess = function (command, args, options) { 78 | var psDesc = command + (args ? " " + args.join(" ") : ""), 79 | print = function (msg) { console.log(String(msg).replace(/\n$/, '')); }, 80 | failOnErr = options.failOnErr !== false, 81 | stderr = [], 82 | stdout = [], 83 | both = [], 84 | ps, 85 | pid; 86 | 87 | options = options || {}; 88 | 89 | // Create process and bind handlers. 90 | ps = spawn(command, args); 91 | ps.stdout.on('data', function (msg) { 92 | msg = String(msg); 93 | both.push(msg); 94 | stdout.push(msg); 95 | if (options.stdoutFn) { 96 | options.stdoutFn(msg); 97 | } else { 98 | print(msg); 99 | } 100 | }); 101 | ps.stderr.on('data', function (msg) { 102 | msg = String(msg); 103 | both.push(msg); 104 | stderr.push(msg); 105 | if (options.stderrFn) { 106 | options.stderrFn(msg); 107 | } else { 108 | print(msg); 109 | } 110 | }); 111 | ps.on('exit', function (code) { 112 | var msg = "Process: \"" + command + "\" (" + pid + 113 | ") exited w/ code: " + code; 114 | if (options.endFn) { 115 | options.endFn(code, { both: both, stdout: stdout, stderr: stderr }); 116 | } 117 | if (code !== 0 && failOnErr) { 118 | fail(msg); 119 | } else { 120 | console.log(msg); 121 | } 122 | }); 123 | 124 | pid = ps.pid; 125 | console.log("Starting process (%s): \"%s\"", pid, psDesc); 126 | }; 127 | 128 | var findFiles = function (options, callback) { 129 | options = options || {}; 130 | var walker = findit.find(options.root || "."), 131 | files = []; 132 | 133 | walker.on('file', function (file) { 134 | if ((!options.exclude || !options.exclude.test(file)) && 135 | (!options.include || options.include.test(file))) { 136 | files.push(file); 137 | } 138 | }); 139 | walker.on('end', function () { 140 | callback(files); 141 | }); 142 | }; 143 | 144 | /////////////////////////////////////////////////////////////////////////////// 145 | // Targets 146 | namespace('dev', function () { 147 | desc("Check for debugging snippets and bad code style."); 148 | task('cruft', function () { 149 | findFiles({ 150 | root: ".", 151 | exclude: FILES_RE.EXCLUDE, 152 | include: FILES_RE.LIB_JS 153 | }, function (files) { 154 | runProcess("egrep", [].concat(["-n"], CRUFT_RE, files), { 155 | endFn: function (code, output) { 156 | if (code !== 0) { 157 | fail("Cruft found!"); 158 | } 159 | complete(); 160 | } 161 | }); 162 | }); 163 | }, true); 164 | 165 | desc("Run style checks."); 166 | task('style', function () { 167 | findFiles({ 168 | root: ".", 169 | exclude: FILES_RE.EXCLUDE, 170 | include: FILES_RE.ALL_JS 171 | }, function (files) { 172 | // Strip Jakefile... 173 | files = files.filter(function (file) { return file !== "./Jakefile"; }); 174 | 175 | style.jshint(files, function () { 176 | console.log(arguments); 177 | complete(); 178 | }); 179 | }); 180 | }, true); 181 | }); 182 | 183 | namespace('test', function () { 184 | desc("Run all tests."); 185 | task('all', ['test:core', 'test:live']); 186 | 187 | desc("Run core module tests."); 188 | task('core', function (config) { 189 | runProcess(NODE_UNIT_BIN, [TESTS.CORE.path], { 190 | endFn: complete 191 | }); 192 | }, true); 193 | 194 | desc("Run unit tests on real datastores (requires configuration(s))."); 195 | task('live', function (config) { 196 | config = config || process.env.config || TESTS.LIVE.config || null; 197 | console.log("Using configuration: " + config); 198 | 199 | // Resolve path to absolute (if not already); 200 | if (new RegExp("^[^/]").test(config)) { 201 | config = path.join(__dirname, config); 202 | } 203 | 204 | // Patch path into environment. 205 | process.env.SUNNY_LIVE_TEST_CONFIG = config; 206 | 207 | runProcess(NODE_UNIT_BIN, [TESTS.LIVE.path], { 208 | endFn: complete 209 | }); 210 | }, true); 211 | }); 212 | 213 | namespace('build', function () { 214 | desc("Clean all build files."); 215 | task('clean', function () { 216 | runProcess("rm", ["-rf"].concat(BUILD_FILES), { 217 | endFn: complete 218 | }); 219 | }, true); 220 | 221 | desc("Compile styles."); 222 | task('css', function () { 223 | var mkdir = "mkdir -p " + DOCS_CSS_OUT + " && ", 224 | opts = STYLUS_OPTIONS.join(" "), 225 | stylus = [mkdir, STYLUS_BIN, opts, DOCS_CSS_SRC].join(" "); 226 | exec(stylus, function (error, stdout, stderr) { 227 | console.log(stdout); 228 | if (error !== null) { 229 | console.log("STDERR: " + stderr); 230 | console.log("ERROR: " + error); 231 | } else { 232 | console.log("Built CSS at: " + DOCS_CSS_OUT); 233 | } 234 | 235 | complete(); 236 | }); 237 | }, true); 238 | 239 | desc("Build all documentation."); 240 | task('docs', ['build:siteDocs', 'build:apiDocs']); 241 | 242 | desc("Build Site documentation."); 243 | task('siteDocs', ['build:_siteDocsRaw', 'build:_CNAME', 'build:css']); 244 | 245 | desc("Copy CNAME file (internal)."); 246 | task('_CNAME', function () { 247 | runProcess("cp", ["CNAME", DOCS_OUT + "/CNAME"], { 248 | endFn: complete 249 | }); 250 | }, true); 251 | 252 | desc("Build Site (internal)."); 253 | task('_siteDocsRaw', function () { 254 | var jekyll = "cd " + JEKYLL_SRC + " && " + JEKYLL_BIN; 255 | exec(jekyll, function (error, stdout, stderr) { 256 | console.log(stdout); 257 | if (error !== null) { 258 | console.log("STDERR: " + stderr); 259 | console.log("ERROR: " + error); 260 | } else { 261 | console.log("Built Site docs at: " + DOCS_OUT); 262 | } 263 | 264 | complete(); 265 | }); 266 | }, true); 267 | 268 | desc("Build API documentation."); 269 | task('apiDocs', ['build:css'], function () { 270 | var jsdoc = [JSDOC_BIN].concat(JSDOC_OPTIONS).join(" "); 271 | exec(jsdoc, function (error, stdout, stderr) { 272 | console.log(stdout); 273 | if (error !== null) { 274 | console.log("STDERR: " + stderr); 275 | console.log("ERROR: " + error); 276 | } else { 277 | console.log("Built API docs at: " + JSDOC_OUT); 278 | } 279 | 280 | complete(); 281 | }); 282 | }, true); 283 | }); 284 | -------------------------------------------------------------------------------- /lib/base/blob/container.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Base Container. 3 | */ 4 | 5 | (function () { 6 | var utils = require("../../utils"), 7 | CloudError = require("../../errors").CloudError, 8 | Container; 9 | 10 | /** 11 | * Container class. 12 | * 13 | * @param {base.blob.Container} conn Connection object. 14 | * @param {Object} attrs Attributes. 15 | * @config {string} name Name. 16 | * @config {string} created Creation date. 17 | * @exports Container as base.blob.Container 18 | * @constructor 19 | */ 20 | Container = function (conn, attrs) { 21 | // Validation. 22 | if (!conn) { throw new Error("No connection object."); } 23 | if (!attrs || !attrs.name) { throw new Error("No container name."); } 24 | 25 | this._conn = conn; 26 | this._name = attrs.name; 27 | this._created = attrs.created || null; 28 | }; 29 | 30 | Object.defineProperties(Container.prototype, { 31 | /** 32 | * Connection object. 33 | * 34 | * @name Container#connection 35 | * @type base.Connection 36 | */ 37 | connection: { 38 | get: function () { 39 | return this._conn; 40 | } 41 | }, 42 | 43 | /** 44 | * Container name. 45 | * 46 | * @name Container#name 47 | * @type string 48 | */ 49 | name: { 50 | get: function () { 51 | return this._name; 52 | } 53 | } 54 | }); 55 | 56 | /** 57 | * Create blob object. 58 | * @private 59 | */ 60 | Container.prototype._createBlob = function (name) { 61 | throw new Error("Not implemented."); 62 | }; 63 | 64 | /** 65 | * Completion event ('``end``'). 66 | * 67 | * @name base.blob.Container#get_end 68 | * @event 69 | * @param {Object} results Results object. 70 | * @config {base.blob.Container} container Container object. 71 | * @config {boolean} alreadyCreated True if object already exists. 72 | * Note: Not all providers can tell. 73 | * @param {Object} meta Headers, meta object. 74 | * @config {Object} [headers] HTTP headers. 75 | * @config {Object} [cloudHeaders] Cloud provider headers. 76 | * @config {Object} [metadata] Cloud metadata. 77 | */ 78 | /** 79 | * Error event ('``error``'). 80 | * 81 | * @name base.blob.Container#get_error 82 | * @event 83 | * @param {Error|errors.CloudError} err Error object. 84 | */ 85 | /** 86 | * GET (or PUT) Container from cloud. 87 | * 88 | * ## Events 89 | * - [``end(results, meta)``](#get_end) 90 | * - [``error(err)``](#get_error) 91 | * 92 | * ## Note - Unvalidated GET's 93 | * If both ``validate`` and ``create`` are false, there is typically 94 | * no actual network operation, just a dummy (and immediate) callback. 95 | * Subsequent code cannot assume the container actually exists in the 96 | * cloud and must handle 404's, etc. 97 | * 98 | * @param {Object} [options] Options object. 99 | * @config {bool} [validate=false] Validate? 100 | * @config {bool} [create=false] Create (PUT) if doesn't exist? 101 | * @config {Object} [headers] Raw headers to add. 102 | * @config {Object} [cloudHeaders] Cloud provider headers to add. 103 | * @returns {request.AuthenticatedRequest} Request object. 104 | */ 105 | Container.prototype.get = function (options) { 106 | throw new Error("Not implemented."); 107 | }; 108 | 109 | /** 110 | * PUT container in cloud. 111 | * 112 | * Alias of ``get()`` with '``create=true``' option. 113 | * @see base.blob.Container#get 114 | */ 115 | Container.prototype.put = function (options) { 116 | options = utils.extend(options, { create: true }); 117 | return this.get(options); 118 | }; 119 | 120 | /** 121 | * Completion event ('``end``'). 122 | * 123 | * Event emission / callback indicates the object no longer exists. 124 | * 125 | * @name base.blob.Container#del_end 126 | * @event 127 | * @param {Object} results Results object. 128 | * @config {base.blob.Container} container Container object. 129 | * @config {boolean} notFound True if object was not found. 130 | * @param {Object} meta Headers, meta object. 131 | * @config {Object} [headers] HTTP headers. 132 | * @config {Object} [cloudHeaders] Cloud provider headers. 133 | * @config {Object} [metadata] Cloud metadata. 134 | */ 135 | /** 136 | * Error event ('``error``'). 137 | * 138 | * @name base.blob.Container#del_error 139 | * @event 140 | * @param {Error|errors.CloudError} err Error object. 141 | */ 142 | /** 143 | * DELETE a container. 144 | * 145 | * ## Events 146 | * - [``end(results, meta)``](#del_end) 147 | * - [``error(err)``](#del_error) 148 | * 149 | * @param {Object} [options] Options object. 150 | * @config {Object} [headers] Raw headers to add. 151 | * @config {Object} [cloudHeaders] Cloud provider headers to add. 152 | * @config {Object} [metadata] Cloud metadata to add. 153 | * @returns {request.AuthenticatedRequest} Request object. 154 | */ 155 | Container.prototype.del = function (options) { 156 | throw new Error("Not implemented."); 157 | }; 158 | 159 | /** 160 | * Completion event ('``end``'). 161 | * 162 | * @name base.blob.Container#getBlobs_end 163 | * @event 164 | * @param {Object} results Results object. 165 | * @config {Array} blobs List of blob objects. 166 | * @config {Array} dirNames List of pseudo-directory name strings. 167 | * @config {boolean} hasNext True if more results are available. 168 | * @param {Object} meta Headers, meta object. 169 | * @config {Object} [headers] HTTP headers. 170 | * @config {Object} [cloudHeaders] Cloud provider headers. 171 | * @config {Object} [metadata] Cloud metadata. 172 | */ 173 | /** 174 | * Error event ('``error``'). 175 | * 176 | * @name base.blob.Container#getBlobs_error 177 | * @event 178 | * @param {Error|errors.CloudError} err Error object. 179 | */ 180 | /** 181 | * GET a list of blob objects. 182 | * 183 | * ## Events 184 | * - [``end(results, meta)``](#getBlobs_end) 185 | * - [``error(err)``](#getBlobs_error) 186 | * 187 | * @param {Object} [options] Options object. 188 | * @config {string} [prefix] Prefix of blob namespace to filter. 189 | * @config {string} [delimiter] Implied directory character. 190 | * @config {string} [marker] Starting blob name for next results. 191 | * @config {number} [maxResults=1000] Max. blobs to return. 192 | * @config {Object} [headers] Raw headers to add. 193 | * @config {Object} [cloudHeaders] Cloud provider headers to add. 194 | * @config {Object} [metadata] Cloud metadata to add. 195 | * @returns {request.AuthenticatedRequest} Request object. 196 | */ 197 | Container.prototype.getBlobs = function (options) { 198 | throw new Error("Not implemented."); 199 | }; 200 | 201 | /** 202 | * Create blob object and GET data. 203 | * 204 | * @see base.blob.Blob#get 205 | */ 206 | Container.prototype.getBlob = function (name, options) { 207 | return this._createBlob(name).get(options); 208 | }; 209 | 210 | /** 211 | * Create blob object and GET data to file. 212 | * 213 | * @see base.blob.Blob#getToFile 214 | */ 215 | Container.prototype.getBlobToFile = function (name, filePath, options) { 216 | return this._createBlob(name).getToFile(filePath, options); 217 | }; 218 | 219 | /** 220 | * Create blob object and HEAD headers, metadata. 221 | * 222 | * @see base.blob.Blob#head 223 | */ 224 | Container.prototype.headBlob = function (name, options) { 225 | return this._createBlob(name).head(options); 226 | }; 227 | 228 | /** 229 | * Create blob object and PUT data. 230 | * 231 | * @see base.blob.Blob#put 232 | */ 233 | Container.prototype.putBlob = function (name, options) { 234 | return this._createBlob(name).put(options); 235 | }; 236 | 237 | /** 238 | * Create blob object and PUT data from file. 239 | * 240 | * @see base.blob.Blob#putFromFile 241 | */ 242 | Container.prototype.putBlobFromFile = function (name, filePath, options) { 243 | return this._createBlob(name).putFromFile(filePath, options); 244 | }; 245 | 246 | /** 247 | * Create blob object and DELETE. 248 | * 249 | * @see base.blob.Blob#del 250 | */ 251 | Container.prototype.delBlob = function (name, options) { 252 | return this._createBlob(name).del(options); 253 | }; 254 | 255 | module.exports.Container = Container; 256 | }()); 257 | -------------------------------------------------------------------------------- /test/live/connection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Live Connection tests. 3 | */ 4 | 5 | /** 6 | * @name test.live.connection 7 | */ 8 | (function () { 9 | var assert = require('assert'), 10 | sunny = require("../../lib"), 11 | CloudError = require("../../lib/errors").CloudError, 12 | Connection = require("../../lib/base/connection").Connection, 13 | Container = require("../../lib/base/blob/container").Container, 14 | utils = require("./utils"), 15 | Tests; 16 | 17 | /** 18 | * @exports Tests as test.live.connection.Tests 19 | * @namespace 20 | */ 21 | Tests = {}; 22 | 23 | Tests["Connection"] = { 24 | "Configuration object.": function (test, opts) { 25 | test.expect(1); 26 | test.ok(opts.config instanceof sunny.Configuration); 27 | test.done(); 28 | }, 29 | "Connection object.": function (test, opts) { 30 | test.expect(1); 31 | test.ok(opts.conn instanceof Connection); 32 | test.done(); 33 | }, 34 | "LIST containers (no parameters).": function (test, opts) { 35 | var request = opts.conn.getContainers(); 36 | request.on('error', utils.errHandle(test)); 37 | request.on('end', function (results) { 38 | test.ok(results); 39 | test.ok(results.hasOwnProperty('containers')); 40 | test.expect(2 + results.containers.length); 41 | results.containers.forEach(function (cont) { 42 | test.ok(cont instanceof Container); 43 | }); 44 | test.done(); 45 | }); 46 | request.end(); 47 | }, 48 | "GET nonexistent container, no verification.": function (test, opts) { 49 | // Choose highly-unlikely-to-exist name. 50 | var contName = "sunny-nonexistent-container-name-test", 51 | request = opts.conn.getContainer(contName); 52 | 53 | test.expect(3); 54 | request.on('error', utils.errHandle(test)); 55 | request.on('end', function (results) { 56 | test.ok(results); 57 | test.ok(results.hasOwnProperty('container')); 58 | test.ok(results.container instanceof Container); 59 | test.done(); 60 | }); 61 | request.end(); 62 | }, 63 | "GET nonexistent container, with verification.": function (test, opts) { 64 | // Choose highly-unlikely-to-exist name. 65 | var contName = "sunny_nonexistent_container_name_test", 66 | request = opts.conn.getContainer(contName, { validate: true }); 67 | 68 | test.expect(1); 69 | request.on('error', function (err) { 70 | test.deepEqual(err.isNotFound(), true); 71 | test.done(); 72 | }); 73 | request.on('end', function (results) { 74 | test.ok(false, "Should not have completion. Got: " + results); 75 | test.done(); 76 | }); 77 | request.end(); 78 | }, 79 | "PUT invalid-named container.": function (test, opts) { 80 | var contName = "--SUNNYJS_BAD_NAME_FOR_PUT--", 81 | request; 82 | 83 | // Despite the name explicitly violating AWS S3 guidelines, this 84 | // will actually succeed, so let's add some slashes. (This incidentally 85 | // kills GSFD with an "unknown" 400 error). 86 | contName = opts.config.isAws() ? contName.replace("-", "/") : contName; 87 | 88 | test.expect(1); 89 | request = opts.conn.putContainer(contName); 90 | request.on('error', function (err) { 91 | test.deepEqual(err.isInvalidName(), true); 92 | test.done(); 93 | }); 94 | request.on('end', function (results) { 95 | test.ok(false, "Should not have completion. Got: " + results); 96 | test.done(); 97 | }); 98 | request.end(); 99 | }, 100 | "GET invalid-named container, with verification.": function (test, opts) { 101 | var contName = "SUNNYJS_BAD_NAME_FOR_GET-", 102 | request; 103 | 104 | // Patch name for AWS. 105 | contName = opts.config.isAws() ? contName.replace("-", "/") : contName; 106 | 107 | test.expect(2); 108 | request = opts.conn.getContainer(contName, { validate: true }); 109 | request.on('error', function (err) { 110 | test.deepEqual(err.isNotFound(), true); 111 | test.deepEqual(err.isInvalidName(), true); 112 | test.done(); 113 | }); 114 | request.on('end', function (results) { 115 | test.ok(false, "Should not have completion. Got: " + results); 116 | test.done(); 117 | }); 118 | request.end(); 119 | }, 120 | "GET weirder, invalid-named container.": function (test, opts) { 121 | var contName = "--SUNNYJS_WEIRDER_BAD_NAME_FOR_GET--", 122 | request; 123 | 124 | // Patch name for AWS. 125 | contName = opts.config.isAws() ? contName.replace("-", "/") : contName; 126 | 127 | test.expect(2); 128 | request = opts.conn.getContainer(contName, { validate: true }); 129 | request.on('error', function (err) { 130 | test.deepEqual(err.isNotFound(), true); 131 | // Google used to have a problem with this. Now ok. (?) 132 | test.deepEqual(err.isInvalidName(), true); 133 | test.done(); 134 | }); 135 | request.on('end', function (results) { 136 | test.ok(false, "Should not have completion. Got: " + results); 137 | test.done(); 138 | }); 139 | request.end(); 140 | }, 141 | "PUT container owned by another.": function (test, opts) { 142 | // "test" bucket is already taken on AWS, GSFD. 143 | // **Note**: If this bucket got delete in the future, this test could 144 | // theoretically succeed for someone. 145 | var request = opts.conn.putContainer("test"); 146 | 147 | test.expect(1); 148 | request.on('error', function (err) { 149 | test.deepEqual(err.isNotOwner(), true); 150 | test.done(); 151 | }); 152 | request.on('end', function (results) { 153 | test.ok(false, "Should not have completion. Got: " + results); 154 | test.done(); 155 | }); 156 | request.end(); 157 | }, 158 | "DELETE non-existent container.": function (test, opts) { 159 | var contName = utils.testContainerName(), 160 | request = opts.conn.delContainer(contName); 161 | 162 | test.expect(3); 163 | request.on('error', utils.errHandle(test)); 164 | request.on('end', function (results) { 165 | var container = results.container, 166 | notFound = results.notFound; 167 | 168 | test.ok(container, "Container should not be empty."); 169 | test.deepEqual(container.name, contName); 170 | test.deepEqual(notFound, true); 171 | test.done(); 172 | }); 173 | request.end(); 174 | }, 175 | "DELETE invalid-named container.": function (test, opts) { 176 | var contName = "SUNNYJS_BAD_NAME_FOR_DELETE-", 177 | request; 178 | 179 | // Patch name for AWS. 180 | contName = opts.config.isAws() ? contName.replace("-", "/") : contName; 181 | 182 | test.expect(3); 183 | request = opts.conn.delContainer(contName); 184 | request.on('error', utils.errHandle(test)); 185 | request.on('end', function (results) { 186 | var container = results.container, 187 | notFound = results.notFound; 188 | 189 | test.ok(container, "Container should not be empty."); 190 | test.deepEqual(container.name, contName); 191 | test.deepEqual(notFound, true); 192 | test.done(); 193 | }); 194 | request.end(); 195 | } 196 | //, 197 | //"TODO: DELETE container owned by another.": function (test, opts) { 198 | // test.done(); 199 | //} 200 | }; 201 | 202 | Tests["Connection (w/ Cont)"] = utils.createTestSetup(null, { 203 | "PUT container that already exists.": function (test, opts) { 204 | var self = this, 205 | request = opts.conn.putContainer(self.containerName); 206 | 207 | test.expect(3); 208 | request.on('error', utils.errHandle(test)); 209 | request.on('end', function (results) { 210 | test.ok(results, "Should have valid result."); 211 | test.deepEqual(results.container.name, self.containerName); 212 | if (opts.config.isAws()) { 213 | test.deepEqual(results.alreadyCreated, false); 214 | } else if (opts.config.isGoogle()) { 215 | test.deepEqual(results.alreadyCreated, true); 216 | } 217 | test.done(); 218 | }); 219 | request.end(); 220 | }, 221 | "GET and validate container.": function (test, opts) { 222 | var self = this, 223 | request = opts.conn.getContainer(self.containerName, { 224 | validate: true 225 | }); 226 | 227 | test.expect(11); 228 | request.on('error', utils.errHandle(test)); 229 | request.on('end', function (results, meta) { 230 | test.ok(results, "Should have valid result."); 231 | test.deepEqual(results.container.name, self.containerName); 232 | test.deepEqual(results.alreadyCreated, true); 233 | 234 | // Test meta. (Make sure equal # of tests AWS/GSFD for easier expect). 235 | test.ok(meta.headers); 236 | test.ok(meta.headers['date']); 237 | test.ok(meta.headers['server']); 238 | test.ok(meta.headers['content-type']); 239 | test.ok(meta.cloudHeaders); 240 | test.ok(meta.metadata); 241 | if (opts.config.isAws()) { 242 | test.deepEqual(meta.headers['server'], "AmazonS3"); 243 | test.ok(meta.cloudHeaders['request-id']); 244 | } else if (opts.config.isGoogle()) { 245 | test.ok(meta.headers['server'].indexOf("HTTP Upload Server") > -1); 246 | test.ok(meta.headers['expires']); 247 | } 248 | 249 | test.done(); 250 | }); 251 | request.end(); 252 | } 253 | }); 254 | 255 | module.exports.Tests = Tests; 256 | }()); 257 | -------------------------------------------------------------------------------- /lib/base/blob/blob.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Base Blob. 3 | */ 4 | 5 | (function () { 6 | var fs = require('fs'), 7 | DummyRequest = require("../../request").DummyRequest, 8 | CloudError = require("../../errors").CloudError, 9 | Blob; 10 | 11 | /** 12 | * Blob class. 13 | * 14 | * @param {base.Container} cont Container object. 15 | * @param {Object} attrs Attributes. 16 | * @config {string} name Name. 17 | * @config {string} created Creation date. 18 | * @config {string} lastModified Last modified date. 19 | * @config {number} size Byte size of object. 20 | * @config {string} etag ETag. 21 | * @exports Blob as base.blob.Blob 22 | * @constructor 23 | */ 24 | Blob = function (cont, attrs) { 25 | // Validation. 26 | if (!cont) { throw new Error("No container object."); } 27 | if (!attrs || !attrs.name) { throw new Error("No blob name."); } 28 | 29 | this._cont = cont; 30 | this._name = attrs.name; 31 | this._created = attrs.created || null; 32 | this._lastModified = attrs.lastModified || null; 33 | this._size = (typeof attrs.size === 'string') 34 | ? parseInt(attrs.size, 10) 35 | : null; 36 | this._etag = attrs.etag || null; 37 | }; 38 | 39 | Object.defineProperties(Blob.prototype, { 40 | /** 41 | * Container object. 42 | * 43 | * @name Blob#container 44 | * @type base.blob.Container 45 | */ 46 | container: { 47 | get: function () { 48 | return this._cont; 49 | } 50 | }, 51 | 52 | /** 53 | * Blob name. 54 | * 55 | * @name Blob#name 56 | * @type string 57 | */ 58 | name: { 59 | get: function () { 60 | return this._name; 61 | } 62 | } 63 | }); 64 | 65 | /** 66 | * Data event ('``data``'). 67 | * 68 | * @name base.blob.Blob#get_data 69 | * @event 70 | * @param {Buffer|string} chunk Data chunk. 71 | * @param {Object} meta Headers, meta object. 72 | * @config {Object} [headers] HTTP headers. 73 | * @config {Object} [cloudHeaders] Cloud provider headers. 74 | * @config {Object} [metadata] Cloud metadata. 75 | */ 76 | /** 77 | * Completion event ('``end``'). 78 | * 79 | * @name base.blob.Blob#get_end 80 | * @event 81 | * @param {Object} results Results object. 82 | * @config {base.blob.Blob} blob Blob object. 83 | * @param {Object} meta Headers, meta object. 84 | * @config {Object} [headers] HTTP headers. 85 | * @config {Object} [cloudHeaders] Cloud provider headers. 86 | * @config {Object} [metadata] Cloud metadata. 87 | */ 88 | /** 89 | * Error event ('``error``'). 90 | * 91 | * @name base.blob.Blob#get_error 92 | * @event 93 | * @param {Error|errors.CloudError} err Error object. 94 | */ 95 | /** 96 | * Get blob data (and metadata). 97 | * 98 | * ## Events 99 | * - [``data(chunk)``](#get_data) 100 | * - [``end(results, meta)``](#get_end) 101 | * - [``error(err)``](#get_error) 102 | * 103 | * @param {Object} [options] Options object. 104 | * @config {string} [encoding] Encoding to use (if set, a string 105 | * will be passed to 'data' or 'end' 106 | * instead of array of Buffer objects). 107 | * @config {bool} [validate=false] Validate? 108 | * @config {Object} [headers] Raw headers to add. 109 | * @config {Object} [cloudHeaders] Cloud provider headers to add. 110 | * @config {Object} [metadata] Cloud metadata to add. 111 | * @returns {stream.ReadStream} Readable cloud stream object. 112 | */ 113 | Blob.prototype.get = function (options) { 114 | throw new Error("Not implemented."); 115 | }; 116 | 117 | /** 118 | * Get blob data to file. 119 | * 120 | * ## Note 121 | * Just a wrapper around a writable file stream and a GET. 122 | * Must still call ``end()`` to invoke. 123 | * 124 | * ## Events 125 | * - [``end(results, meta)``](#get_end) 126 | * - [``error(err)``](#get_error) 127 | * 128 | * @param {string} filePath Path to file. 129 | * @param {Object} [options] Options object. 130 | * @config {string} [encoding] Encoding to use. 131 | * @config {Object} [headers] Raw headers to add. 132 | * @config {Object} [cloudHeaders] Cloud provider headers to add. 133 | * @config {Object} [metadata] Cloud metadata to add. 134 | * @returns {request.DummyRequest} Request object. 135 | */ 136 | // TOOD: Blob.getToFile - consider fs.writeFile() implementation instead. 137 | Blob.prototype.getToFile = function (filePath, options) { 138 | options = options || {}; 139 | var self = this, 140 | encoding = options.encoding || null, 141 | getResults = {}, 142 | getMeta = {}, 143 | getFinished = false, 144 | writeFinished = false, 145 | endEmitted = false, 146 | errorEmitted = false, 147 | request, 148 | getStream, 149 | writeStream, 150 | errHandler, 151 | endHandler; 152 | 153 | // Update encoding. 154 | options.encoding = encoding; 155 | 156 | // Set up streams. 157 | getStream = self.get({ encoding: encoding }); 158 | writeStream = fs.createWriteStream(filePath, options); 159 | 160 | // Request setup. 161 | // Request.end() starts the pipe, WriteStream:end completes request. 162 | request = new DummyRequest({ 163 | endFn: function () { 164 | // Start the pipe and call 'end()'. 165 | getStream.pipe(writeStream); 166 | getStream.end(); 167 | } 168 | }); 169 | 170 | // Need to handle errors in **both** streams. 171 | /** @private */ 172 | errHandler = function (err) { 173 | if (!errorEmitted) { 174 | errorEmitted = true; 175 | request.emit('error', err); 176 | } 177 | getStream.destroy(); 178 | writeStream.destroy(); 179 | }; 180 | 181 | // End when we have both (1) cloud results, and (2) closed file stream. 182 | /** @private */ 183 | endHandler = function () { 184 | if (getFinished && writeFinished && !endEmitted) { 185 | endEmitted = true; 186 | request.emit('end', getResults, getMeta); 187 | } 188 | }; 189 | 190 | // Stream handlers. 191 | getStream.on('error', errHandler); 192 | getStream.on('end', function (results, meta) { 193 | // GET blob finishes, and we store values. 194 | getFinished = true; 195 | getResults = results; 196 | getMeta = meta; 197 | endHandler(); 198 | }); 199 | writeStream.on('error', errHandler); 200 | writeStream.on('close', function () { 201 | // File stream is closed. 202 | writeFinished = true; 203 | endHandler(); 204 | }); 205 | 206 | return request; 207 | }; 208 | 209 | /** 210 | * Completion event ('``end``'). 211 | * 212 | * @name base.blob.Blob#head_end 213 | * @event 214 | * @param {Object} results Results object. 215 | * @config {base.blob.Blob} blob Blob object. 216 | * @param {Object} meta Headers, meta object. 217 | * @config {Object} [headers] HTTP headers. 218 | * @config {Object} [cloudHeaders] Cloud provider headers. 219 | * @config {Object} [metadata] Cloud metadata. 220 | */ 221 | /** 222 | * Error event ('``error``'). 223 | * 224 | * @name base.blob.Blob#head_error 225 | * @event 226 | * @param {Error|errors.CloudError} err Error object. 227 | */ 228 | /** 229 | * HEAD blob (check blob exists and return metadata). 230 | * 231 | * ## Events 232 | * - [``end(results, meta)``](#head_end) 233 | * - [``error(err)``](#head_error) 234 | * 235 | * @param {Object} [options] Options object. 236 | * @config {Object} [headers] Raw headers to add. 237 | * @config {Object} [cloudHeaders] Cloud provider headers to add. 238 | * @config {Object} [metadata] Cloud metadata to add. 239 | * @returns {request.AuthenticatedRequest} Request object. 240 | */ 241 | Blob.prototype.head = function (options) { 242 | throw new Error("Not implemented."); 243 | }; 244 | 245 | /** 246 | * Completion event ('``end``'). 247 | * 248 | * @name base.blob.Blob#put_end 249 | * @event 250 | * @param {Object} results Results object. 251 | * @config {base.blob.Blob} blob Blob object. 252 | * @param {Object} meta Headers, meta object. 253 | * @config {Object} [headers] HTTP headers. 254 | * @config {Object} [cloudHeaders] Cloud provider headers. 255 | * @config {Object} [metadata] Cloud metadata. 256 | */ 257 | /** 258 | * Error event ('``error``'). 259 | * 260 | * @name base.blob.Blob#put_error 261 | * @event 262 | * @param {Error|errors.CloudError} err Error object. 263 | */ 264 | /** 265 | * Put blob data (and metadata). 266 | * 267 | * ## Events 268 | * - [``end(results, meta)``](#get_end) 269 | * - [``error(err)``](#get_error) 270 | * 271 | * @param {Object} [options] Options object. 272 | * @config {string} [encoding] Encoding to use. 273 | * @config {Object} [headers] Raw headers to add. 274 | * @config {Object} [cloudHeaders] Cloud provider headers to add. 275 | * @config {Object} [metadata] Cloud metadata to add. 276 | * @returns {stream.WriteStream} Writable cloud stream object. 277 | */ 278 | Blob.prototype.put = function (options) { 279 | throw new Error("Not implemented."); 280 | }; 281 | 282 | /** 283 | * Put blob data from file. 284 | * 285 | * ## Note 286 | * Just a wrapper around a readable file stream and a PUT. 287 | * Must still call ``end()`` to invoke. 288 | * 289 | * ## Events 290 | * - [``end(results, meta)``](#get_end) 291 | * - [``error(err)``](#get_error) 292 | * 293 | * @param {string} filePath Path to file. 294 | * @param {Object} [options] Options object. 295 | * @config {string} [encoding] Encoding to use. 296 | * @config {Object} [headers] Raw headers to add. 297 | * @config {Object} [cloudHeaders] Cloud provider headers to add. 298 | * @config {Object} [metadata] Cloud metadata to add. 299 | * @returns {request.DummyRequest} Request object. 300 | */ 301 | // TOOD: Blob.putFromFile - consider fs.readFile() implementation instead. 302 | Blob.prototype.putFromFile = function (filePath, options) { 303 | options = options || {}; 304 | var self = this, 305 | encoding = options.encoding || null, 306 | errorEmitted = false, 307 | request, 308 | readStream, 309 | putStream, 310 | errHandler; 311 | 312 | // Update encoding. 313 | options.encoding = encoding; 314 | 315 | // Set up streams. 316 | readStream = fs.createReadStream(filePath, { encoding: encoding }); 317 | putStream = self.put(options); 318 | 319 | // Request setup. 320 | // Request.end() starts the pipe, PutStream:end completes request. 321 | request = new DummyRequest({ 322 | endFn: function () { 323 | // Start the pipe. 324 | readStream.pipe(putStream); 325 | } 326 | }); 327 | 328 | // Need to handle errors in **both** streams. 329 | /** @private */ 330 | errHandler = function (err) { 331 | if (!errorEmitted) { 332 | errorEmitted = true; 333 | request.emit('error', err); 334 | } 335 | readStream.destroy(); 336 | putStream.destroy(); 337 | }; 338 | 339 | // Stream handlers. 340 | readStream.on('error', errHandler); 341 | putStream.on('error', errHandler); 342 | putStream.on('end', function (results, meta) { 343 | // PUT finishes, and we signal request. 344 | request.emit('end', results, meta); 345 | }); 346 | 347 | return request; 348 | }; 349 | 350 | /** 351 | * Completion event ('``end``'). 352 | * 353 | * **Note**: Callback indicates the object no longer exists. 354 | * 355 | * @name base.blob.Blob#del_end 356 | * @event 357 | * @param {Object} results Results object. 358 | * @config {base.blob.Blob} blob Blob object. 359 | * @config {boolean} notFound True if object was not found. 360 | * @param {Object} meta Headers, meta object. 361 | * @config {Object} [headers] HTTP headers. 362 | * @config {Object} [cloudHeaders] Cloud provider headers. 363 | * @config {Object} [metadata] Cloud metadata. 364 | */ 365 | /** 366 | * Error event ('``error``'). 367 | * 368 | * @name base.blob.Blob#del_error 369 | * @event 370 | * @param {Error|errors.CloudError} err Error object. 371 | */ 372 | /** 373 | * Delete a blob. 374 | * 375 | * ## Events 376 | * - [``end(results, meta)``](#del_end) 377 | * - [``error(err)``](#del_error) 378 | * 379 | * ## Not Found Blobs 380 | * This method emits '``end``' and **not** '``error``' for a not found 381 | * blob, reasoning that it becomes easier to have multiple deletes at 382 | * the same time. Moreover, AWS S3 doesn't return a 404, so we can't really 383 | * even detect this (although Google Storage does). 384 | * 385 | * On ``end``, ``result.notFound`` is returned that at least for Google 386 | * Storage indicates if the blob didn't exist. 387 | * 388 | * @param {Object} [options] Options object. 389 | * @config {Object} [headers] Raw headers to add. 390 | * @config {Object} [cloudHeaders] Cloud provider headers to add. 391 | * @config {Object} [metadata] Cloud metadata to add. 392 | * @returns {request.AuthenticatedRequest} Request object. 393 | */ 394 | Blob.prototype.del = function (options) { 395 | throw new Error("Not implemented."); 396 | }; 397 | 398 | module.exports.Blob = Blob; 399 | }()); 400 | -------------------------------------------------------------------------------- /lib/request.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Cloud requests. 3 | * 4 | * For most of these classes, we wrap up events for both the request and 5 | * response into a single "request" class. 6 | */ 7 | 8 | /** 9 | * @name request 10 | */ 11 | (function () { 12 | var http = require('http'), 13 | https = require('https'), 14 | url = require('url'), 15 | util = require('util'), 16 | EventEmitter = require('events').EventEmitter, 17 | xml2js = require('xml2js'), 18 | CloudError = require("./errors").CloudError, 19 | utils = require("./utils"), 20 | Request, 21 | DummyRequest, 22 | AuthenticatedRawRequest, 23 | AuthenticatedRequest, 24 | AuthenticatedXmlRequest; 25 | 26 | function startsWith(value, prefix) { 27 | return value.slice(0, prefix.length) === prefix; 28 | } 29 | 30 | // From: http://stackoverflow.com/questions/5185326 31 | function isAscii(value) { 32 | return (/^[\000-\177]*$/).test(value); 33 | } 34 | 35 | /** 36 | * Completion event ('``end``'). 37 | * 38 | * @name request.Request#event:end 39 | * @event 40 | * @param {Object} data Results data. 41 | * @param {Object} meta Headers, meta object. 42 | * @config {Object} [headers] HTTP headers. 43 | * @config {Object} [cloudHeaders] Cloud provider headers. 44 | * @config {Object} [metadata] Cloud metadata. 45 | */ 46 | /** 47 | * Error event ('``error``'). 48 | * 49 | * @name request.Request#event:error 50 | * @event 51 | * @param {Error|errors.CloudError} err Error object. 52 | */ 53 | /** 54 | * Abstract base request class. 55 | * 56 | * @param {Object} options Options object. 57 | * @exports Request as request.Request 58 | * @constructor 59 | */ 60 | Request = function (options) { 61 | this._ended = false; 62 | }; 63 | 64 | Request.prototype = new EventEmitter(); 65 | 66 | /** 67 | * End the request. 68 | * 69 | * Typically starts the async code execution. 70 | * 71 | * *Note*: This function can be called multiple times without bad effect. 72 | * Calling code has the option to call ``end()`` once the request is set 73 | * up, or leave it to the end user. 74 | */ 75 | Request.prototype.end = function () { 76 | if (!this._ended) { 77 | this._end.apply(this, arguments); 78 | this._ended = true; 79 | } 80 | }; 81 | 82 | /** 83 | * End implementation. 84 | */ 85 | Request.prototype._end = function () { 86 | throw new Error("Not implemented."); 87 | }; 88 | 89 | module.exports.Request = Request; 90 | 91 | /** 92 | * Nop dummy request wrapper class. 93 | * 94 | * @param {Object} options Options object. 95 | * @config {Function} [endFn] Function to invoke 'end' event. 96 | * @config {Function} [resultsFn] 'end' event results callback. 97 | * @config {Function} [metaFn] 'end' event meta callback. 98 | * @extends request.Request 99 | * @exports DummyRequest as request.DummyRequest 100 | * @constructor 101 | */ 102 | DummyRequest = function (options) { 103 | var self = this; 104 | 105 | self._resultsFn = options.resultsFn || function () {}; 106 | self._metaFn = options.metaFn || function () { 107 | return utils.extractMeta(); 108 | }; 109 | 110 | self._endFn = options.endFn || function () { 111 | self.emit('end', self._resultsFn(), self._metaFn()); 112 | }; 113 | }; 114 | 115 | util.inherits(DummyRequest, Request); 116 | 117 | /** @see request.Request#_end */ 118 | DummyRequest.prototype._end = function () { 119 | this._endFn(); 120 | }; 121 | 122 | module.exports.DummyRequest = DummyRequest; 123 | 124 | /** 125 | * Authenticated raw request wrapper class. 126 | * 127 | * @param {base.Authentication} auth Authentication object. 128 | * @param {Object} options Options object. 129 | * @config {string} [method] HTTP method (verb). 130 | * @config {string} [path] HTTP path. 131 | * @config {Object} [params] HTTP path parameters. 132 | * @config {Object} [headers] HTTP headers. 133 | * @config {Object} [cloudHeaders] Cloud provider headers to add. 134 | * @config {Object} [metadata] Cloud metadata to add. 135 | * @config {string} [encoding] Response encoding. 136 | * @extends request.Request 137 | * @exports AuthenticatedRawRequest as request.AuthenticatedRawRequest 138 | * @constructor 139 | */ 140 | AuthenticatedRawRequest = function (auth, options) { 141 | options = options || {}; 142 | var self = this, 143 | method = options.method || "GET", 144 | path = options.path || "/", 145 | params = options.params || {}, 146 | urlObj, 147 | reqOpts; 148 | 149 | // Parse object path. 150 | urlObj = url.parse(path, true); 151 | 152 | // Unicode: Need `encodeURIComponent` to handle utf8 characters, which are 153 | // valid for AWS, etc. key names. 154 | if (auth.isGoogle()) { 155 | // Unfortunately, Google doesn't work with this. 156 | // Throw error if non-ascii characters. 157 | if (!isAscii(urlObj.pathname)) { 158 | self._err = new Error( 159 | "Google requires ascii path: " + urlObj.pathname); 160 | } 161 | } else { 162 | // Encode for UTF8 support. 163 | urlObj.pathname = encodeURIComponent(urlObj.pathname); 164 | } 165 | 166 | // Patch path to add in query params. 167 | if (Object.keys(params).length > 0) { 168 | urlObj.query = utils.extend(urlObj.query, params); 169 | } 170 | 171 | // Set finished, normalized path. 172 | path = url.format(urlObj); 173 | 174 | // Sign the headers, create the request. 175 | self._auth = auth; 176 | self._method = method; 177 | self._encoding = options.encoding || null; 178 | 179 | // Set headers last (so other object members are created). 180 | self._headers = auth.sign(method, path, self._getHeaders(options)); 181 | self._protocol = auth.ssl ? https : http; 182 | self._request = self._protocol.request({ 183 | host: auth.authUrl(), 184 | port: auth.port, 185 | path: path, 186 | method: method, 187 | headers: self._headers 188 | }); 189 | }; 190 | 191 | util.inherits(AuthenticatedRawRequest, Request); 192 | 193 | Object.defineProperties(AuthenticatedRawRequest.prototype, { 194 | /** 195 | * Authentication object. 196 | * 197 | * @name AuthenticatedRawRequest#auth 198 | * @type Authentication 199 | */ 200 | auth: { 201 | get: function () { 202 | return this._auth; 203 | } 204 | }, 205 | 206 | /** 207 | * HTTP method verb. 208 | * 209 | * @name AuthenticatedRawRequest#method 210 | * @type string 211 | */ 212 | method: { 213 | get: function () { 214 | return this._method; 215 | } 216 | }, 217 | 218 | /** 219 | * Real HTTP request object. 220 | * 221 | * @name AuthenticatedRawRequest#realRequest 222 | * @type http.ClientRequest 223 | */ 224 | realRequest: { 225 | get: function () { 226 | return this._request; 227 | } 228 | } 229 | }); 230 | 231 | /** Set request encoding. */ 232 | AuthenticatedRawRequest.prototype.setEncoding = function (encoding) { 233 | this._encoding = encoding; 234 | this._request.setEncoding(encoding); 235 | }; 236 | 237 | /** Set header. */ 238 | AuthenticatedRawRequest.prototype.setHeader = function (name, value) { 239 | name = name ? name.toLowerCase() : null; 240 | this._request.setHeader(name, value); 241 | this._headers[name] = value; 242 | }; 243 | 244 | /** 245 | * Return full headers from cloud headers and metadata. 246 | * 247 | * @param {Object} options Options object. 248 | * @config {Object} [headers] HTTP headers. 249 | * @config {Object} [cloudHeaders] Cloud provider headers to add. 250 | * @config {Object} [metadata] Cloud metadata to add. 251 | * @returns {Object} HTTP headers. 252 | * @private 253 | */ 254 | AuthenticatedRawRequest.prototype._getHeaders = function (options) { 255 | options = options || {}; 256 | var conn = this._auth.connection, 257 | headerPrefix = conn.headerPrefix, 258 | metaPrefix = conn.metadataPrefix, 259 | rawHeaders = {}, 260 | headers = options.headers || {}, 261 | cloudHeaders = options.cloudHeaders || {}, 262 | metadata = options.metadata || {}; 263 | 264 | // Order is metadata, cloud headers, headers. 265 | Object.keys(metadata).forEach(function (header) { 266 | rawHeaders[metaPrefix + header.toLowerCase()] = metadata[header]; 267 | }); 268 | Object.keys(cloudHeaders).forEach(function (header) { 269 | rawHeaders[headerPrefix + header.toLowerCase()] = cloudHeaders[header]; 270 | }); 271 | Object.keys(headers).forEach(function (header) { 272 | rawHeaders[header.toLowerCase()] = headers[header]; 273 | }); 274 | 275 | return rawHeaders; 276 | }; 277 | 278 | /** 279 | * Return separate headers, cloud headers and metadata from headers. 280 | * 281 | * @param {HttpResponse} response Response object. 282 | * @returns {Object} Object of headers, cloudHeaders, metadata. 283 | */ 284 | AuthenticatedRawRequest.prototype.getMeta = function (response) { 285 | var conn = this._auth.connection, 286 | headerPrefix = conn.headerPrefix, 287 | metaPrefix = conn.metadataPrefix, 288 | rawHeaders = response.headers, 289 | headers = {}, 290 | cloudHeaders = {}, 291 | metadata = {}; 292 | 293 | // First try to get metadata, then cloud headers, then headers. 294 | Object.keys(rawHeaders).forEach(function (header) { 295 | var key = header.toLowerCase(); 296 | if (startsWith(key, metaPrefix)) { 297 | key = key.substring(metaPrefix.length); 298 | metadata[key] = rawHeaders[header]; 299 | } else if (startsWith(key, headerPrefix)) { 300 | key = key.substring(headerPrefix.length); 301 | cloudHeaders[key] = rawHeaders[header]; 302 | } else { 303 | headers[key] = rawHeaders[header]; 304 | } 305 | }); 306 | 307 | return { 308 | headers: headers, 309 | cloudHeaders: cloudHeaders, 310 | metadata: metadata 311 | }; 312 | }; 313 | 314 | /** 315 | * @see request.Request#_end 316 | * @private 317 | */ 318 | AuthenticatedRawRequest.prototype._end = function () { 319 | var req = this._request, 320 | err = this._err; 321 | 322 | // Apply any stashed errors. 323 | if (err) { 324 | return req.emit("error", err); 325 | } 326 | 327 | req.end.apply(req, arguments); 328 | }; 329 | 330 | module.exports.AuthenticatedRawRequest = AuthenticatedRawRequest; 331 | 332 | /** 333 | * Authenticated request wrapper class. 334 | * 335 | * **Note**: Accumulates data for final 'end' event instead of passing 336 | * through via typical 'data' events. 337 | * 338 | * @param {base.Authentication} auth Authentication object. 339 | * @param {Object} options Options object. 340 | * @config {string} [method] HTTP method (verb). 341 | * @config {string} [path] HTTP path. 342 | * @config {Object} [params] HTTP path parameters. 343 | * @config {Object} [headers] HTTP headers. 344 | * @config {Object} [cloudHeaders] Cloud provider headers to add. 345 | * @config {Object} [metadata] Cloud metadata to add. 346 | * @config {string} [encoding] Response encoding. 347 | * @config {Function} [errorFn] errorFn(err, request, [response]) 348 | * Error handler (stops further emission). 349 | * @config {Function} [resultsFn] resultsFn(results, request, [response]) 350 | * Successful results data transform. 351 | * @extends request.Request 352 | * @exports AuthenticatedRequest as request.AuthenticatedRequest 353 | * @constructor 354 | */ 355 | AuthenticatedRequest = function (auth, options) { 356 | var self = this; 357 | 358 | AuthenticatedRawRequest.apply(self, arguments); 359 | 360 | // Additional members. 361 | self._buf = []; 362 | self._resultsFn = options.resultsFn || null; 363 | self._errorFn = options.errorFn || null; 364 | self._handleErr = function (err, response) { 365 | if (self._errorFn) { 366 | self._errorFn(err, self, response); 367 | } else { 368 | self.emit('error', err, response); 369 | } 370 | }; 371 | 372 | // Set up bindings. 373 | self._request.on('error', function (err) { 374 | self._handleErr(err); 375 | }); 376 | self._request.on('response', function (response) { 377 | if (self._encoding) { 378 | response.setEncoding(self._encoding); 379 | } 380 | 381 | // Shortcut: If no-data response, just return here. 382 | // 383 | // **Note**: For some reason, when using HTTPS for both AWS and GSFD, 384 | // DELETE blob responses would not have an 'end' event. This avoids 385 | // the problem by not even bothering with listening. 386 | switch (response.statusCode) { 387 | case 200: // Check 200 OK for no bytes. 388 | if (response.headers && response.headers['content-length'] === "0") { 389 | self.processResults(null, response); 390 | return; 391 | } 392 | break; 393 | case 204: // Response has no content. 394 | self.processResults(null, response); 395 | return; 396 | default: 397 | // Do nothing - continue processing. 398 | break; 399 | } 400 | 401 | // Need more processing, listen to response. 402 | response.on('data', function (chunk) { 403 | self._buf.push(chunk); 404 | }); 405 | response.on('end', function () { 406 | var data = null, 407 | getData, 408 | msg, 409 | err; 410 | 411 | // Handle the buffer, if we have any. 412 | if (self._buf.length > 0) { 413 | if (self._encoding) { 414 | // If encoding, then join as strings. 415 | data = self._buf 416 | .map(function (buf) { return buf.toString(self._encoding); }) 417 | .join(""); 418 | } else { 419 | // Else, return array of buffers. 420 | data = self._buf; 421 | } 422 | } 423 | 424 | switch (response.statusCode) { 425 | case 200: 426 | // processResults emits 'end'. 427 | self.processResults(data, response); 428 | break; 429 | default: 430 | // Everything unknown is an error. 431 | msg = utils.bufToStr(self._buf, self._encoding, 'utf8'); 432 | err = new CloudError(msg, { response: response }); 433 | self._handleErr(err, response); 434 | } 435 | }); 436 | }); 437 | }; 438 | 439 | util.inherits(AuthenticatedRequest, AuthenticatedRawRequest); 440 | 441 | /** 442 | * Process data. 443 | * 444 | * Also emits '``end``' event on processed data. 445 | */ 446 | AuthenticatedRequest.prototype.processResults = function (data, response) { 447 | var self = this, 448 | meta = self.getMeta(response), 449 | results; 450 | 451 | results = self._resultsFn ? self._resultsFn(data, self, response) : data; 452 | 453 | self.emit('end', results, meta); 454 | }; 455 | 456 | module.exports.AuthenticatedRequest = AuthenticatedRequest; 457 | 458 | /** 459 | * Authenticated request wrapper class with JSON results (from XML). 460 | * 461 | * **Note**: Accumulates data for final 'end' event instead of passing 462 | * through via typical 'data' events. 463 | * 464 | * @param {base.Authentication} auth Authentication object. 465 | * @param {Object} options Options object. 466 | * @config {string} [method] HTTP method (verb). 467 | * @config {string} [path] HTTP path. 468 | * @config {Object} [headers] HTTP headers. 469 | * @config {Object} [cloudHeaders] Cloud provider headers to add. 470 | * @config {Object} [metadata] Cloud metadata to add. 471 | * @config {Function} [errorFn] errorFn(err, request, [response]) 472 | * Error handler (if return True, no further 473 | * error handling takes place). 474 | * @config {Function} [resultsFn] resultsFn(results, request, [response]) 475 | * Successful results data transform. 476 | * @extends request.AuthenticatedRequest 477 | * @exports AuthenticatedXmlRequest as request.AuthenticatedXmlRequest 478 | * @constructor 479 | */ 480 | AuthenticatedXmlRequest = function (auth, options) { 481 | AuthenticatedRequest.apply(this, arguments); 482 | }; 483 | 484 | util.inherits(AuthenticatedXmlRequest, AuthenticatedRequest); 485 | 486 | /** @see request.AuthenticatedXmlRequest#processResults */ 487 | AuthenticatedXmlRequest.prototype.processResults = function (data, response) { 488 | var self = this, 489 | meta = self.getMeta(response), 490 | parser = new xml2js.Parser(); 491 | 492 | // Parse the XML response to JSON. 493 | parser.on('end', function (data) { 494 | var results = self._resultsFn 495 | ? self._resultsFn(data, self, response) 496 | : data; 497 | self.emit('end', results, meta); 498 | }); 499 | parser.on('error', function (err) { 500 | self.emit('error', err, response); 501 | }); 502 | 503 | parser.parseString(data); 504 | }; 505 | 506 | module.exports.AuthenticatedXmlRequest = AuthenticatedXmlRequest; 507 | }()); 508 | -------------------------------------------------------------------------------- /lib/stream.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Cloud file steams. 3 | * 4 | * Used for file reading / writing. 5 | */ 6 | // TODO: Could have common Stream base class and share _handleErr. 7 | 8 | /** 9 | * @name stream 10 | */ 11 | (function () { 12 | var util = require('util'), 13 | Stream = require('stream').Stream, 14 | EventEmitter = require('events').EventEmitter, 15 | utils = require("./utils"), 16 | CloudError = require("./errors").CloudError, 17 | ReadStream, 18 | WriteStream; 19 | 20 | /** 21 | * Data event ('``data``'). 22 | * 23 | * *Implements* **[``Event 'data'``][0]** 24 | * [0]: http://nodejs.org/docs/v0.4.9/api/streams.html#event_data_ 25 | * 26 | * @name stream.ReadStream#event:data 27 | * @event 28 | * @param {Object} data Data chunk. 29 | * @param {Object} meta Headers, meta object. 30 | * @config {Object} [headers] HTTP headers. 31 | * @config {Object} [cloudHeaders] Cloud provider headers. 32 | * @config {Object} [metadata] Cloud metadata. 33 | */ 34 | /** 35 | * Completion event ('``end``'). 36 | * 37 | * *Implements* **[``Event 'end'``][0]** 38 | * [0]: http://nodejs.org/docs/v0.4.9/api/streams.html#event_end_ 39 | * 40 | * @name stream.ReadStream#event:end 41 | * @event 42 | * @param {Object} results Results object. 43 | * @param {Object} meta Headers, meta object. 44 | * @config {Object} [headers] HTTP headers. 45 | * @config {Object} [cloudHeaders] Cloud provider headers. 46 | * @config {Object} [metadata] Cloud metadata. 47 | */ 48 | /** 49 | * Error event ('``error``'). 50 | * 51 | * *Implements* **[``Event 'error'``][0]** 52 | * [0]: http://nodejs.org/docs/v0.4.9/api/streams.html#event_error_ 53 | * 54 | * @name stream.ReadStream#event:error 55 | * @event 56 | * @param {Error|errors.CloudError} err Error object. 57 | */ 58 | /** 59 | * Readable cloud stream. 60 | * 61 | * *Implements* **[``Readable Stream``][0]** interface. 62 | * [0]: http://nodejs.org/docs/v0.4.9/api/streams.html#readable_Stream 63 | * 64 | * @param {request.AuthenticatedRawRequest} 65 | * request Request object. 66 | * @param {Object} [options] Options object. 67 | * @config {Function} [errorFn] Function to call with error. 68 | * @config {Function} [endFn] Function to call with results. Called: 69 | * endFn(response). 70 | * @exports ReadStream as stream.ReadStream 71 | * @constructor 72 | */ 73 | ReadStream = function (request, options) { 74 | options = options || {}; 75 | var self = this; 76 | 77 | // Members. 78 | self._readable = true; 79 | self._request = request; 80 | self._realRequest = request.realRequest; 81 | self._requestDone = false; 82 | self._errorHandled = false; 83 | self._response = null; 84 | self._buf = []; 85 | self._errorFn = options.errorFn || null; 86 | self._endFn = options.endFn || null; 87 | 88 | // Called state 89 | self._endCalled = false; 90 | self._destroyCalled = false; 91 | self._destroySoonCalled = false; 92 | 93 | // Error handling 94 | self._handleErr = function (err, response) { 95 | if (!self._errorHandled) { 96 | self._errorHandled = true; 97 | self._readable = false; 98 | self._requestDone = true; 99 | if (self._errorFn) { 100 | self._errorFn(err, self, response); 101 | } else { 102 | self.emit('error', err); 103 | } 104 | } 105 | }; 106 | 107 | // Errors: straight pass-through (AuthReq has response). 108 | self._request.on('error', function (err, response) { 109 | self._handleErr(err, response); 110 | }); 111 | 112 | // Event binding. 113 | self._realRequest.on('response', function (response) { 114 | var encoding = self._request._encoding || null, 115 | meta = self._request.getMeta(response); 116 | 117 | // Store response and set encoding. 118 | self._response = response; 119 | if (encoding) { 120 | response.setEncoding(encoding); 121 | } 122 | 123 | // Add bindings. 124 | response.on('data', function (chunk) { 125 | var doData = self.listeners('data').length > 0, 126 | isError = response.statusCode && response.statusCode >= 400; 127 | 128 | if (doData && !isError) { 129 | // If we have data listeners and not error, emit the data. 130 | self.emit('data', chunk, meta); 131 | } else { 132 | // Otherwise, accumulate as string. 133 | self._buf.push(chunk); 134 | } 135 | }); 136 | response.on('end', function () { 137 | var results = null, 138 | msg, 139 | err; 140 | 141 | // Set state. 142 | self._readable = false; 143 | self._requestDone = true; 144 | 145 | if (self._endFn) { 146 | results = self._endFn(response); 147 | } 148 | 149 | switch (response.statusCode) { 150 | case 200: 151 | self.emit('end', results, meta); 152 | break; 153 | default: 154 | // Everything unknown is an error. 155 | msg = utils.bufToStr(self._buf, encoding, 'utf8'); 156 | err = new CloudError(msg, { response: response }); 157 | self._handleErr(err, response); 158 | } 159 | }); 160 | }); 161 | }; 162 | 163 | util.inherits(ReadStream, Stream); 164 | 165 | Object.defineProperties(ReadStream.prototype, { 166 | /** 167 | * *Implements* **[``readable``][0]** 168 | * [0]: http://nodejs.org/docs/v0.4.9/api/streams.html#stream.readable 169 | * 170 | * "A boolean that is true by default, but turns false after an 'error' 171 | * occurred, the stream came to an 'end', or destroy() was called." 172 | * 173 | * @name ReadStream#readable 174 | * @type boolean 175 | */ 176 | readable: { 177 | get: function () { 178 | return this._readable; 179 | } 180 | } 181 | }); 182 | 183 | /** 184 | * *Implements* **[``setEncoding``][0]** 185 | * [0]: http://nodejs.org/docs/v0.4.9/api/streams.html#stream.setEncoding 186 | * 187 | * "Makes the data event emit a string instead of a Buffer. encoding can be 188 | * 'utf8', 'ascii', or 'base64'." 189 | */ 190 | // TODO: Test ReadStream.setEncoding 191 | ReadStream.prototype.setEncoding = function (encoding) { 192 | this._request.setEncoding(encoding); 193 | }; 194 | 195 | /** 196 | * *Implements* **[``pause``][0]** 197 | * [0]: http://nodejs.org/docs/v0.4.9/api/streams.html#stream.pause 198 | * 199 | * "Pauses the incoming 'data' events." 200 | */ 201 | // TODO: Test ReadStream.pause 202 | ReadStream.prototype.pause = function () { 203 | if (this._response) { 204 | this._response.pause(); 205 | } 206 | }; 207 | 208 | /** 209 | * *Implements* **[``resume``][0]** 210 | * [0]: http://nodejs.org/docs/v0.4.9/api/streams.html#stream.resume 211 | * 212 | * "Resumes the incoming 'data' events after a pause()." 213 | */ 214 | // TODO: Test ReadStream.resume 215 | ReadStream.prototype.resume = function () { 216 | if (this._response) { 217 | this._response.resume(); 218 | } 219 | }; 220 | 221 | /** 222 | * *Implements* **[``destroy``][0]** 223 | * [0]: http://nodejs.org/docs/v0.4.9/api/streams.html#stream.destroy 224 | * 225 | * "Closes the underlying file descriptor. Stream will not emit any more 226 | * events." 227 | */ 228 | ReadStream.prototype.destroy = function () { 229 | var self = this, 230 | oldDestroyCalled = self._destroyCalled; 231 | 232 | self._destroyCalled = true; 233 | if (!oldDestroyCalled) { 234 | // Remove all listeners. 235 | self.removeAllListeners('data'); 236 | self.removeAllListeners('end'); 237 | self.removeAllListeners('error'); 238 | self._realRequest.removeAllListeners('data'); 239 | self._realRequest.removeAllListeners('end'); 240 | self._realRequest.removeAllListeners('error'); 241 | 242 | // Set sink for errors. 243 | self.on('error', function () {}); 244 | self._realRequest.on('error', function () {}); 245 | 246 | // Close the connection. 247 | self._requestDone = true; 248 | if (self._realRequest && self._realRequest.connection) { 249 | self._realRequest.connection.end(); 250 | } 251 | } 252 | }; 253 | 254 | /** 255 | * *Implements* **[``destroySoon``][0]** 256 | * [0]: http://nodejs.org/docs/v0.4.9/api/streams.html#stream.destroySoon 257 | * 258 | * "After the write queue is drained, close the file descriptor." 259 | */ 260 | // TODO: Test ReadStream.destroySoon 261 | ReadStream.prototype.destroySoon = function () { 262 | var self = this, 263 | oldDestroySoonCalled = self._destroySoonCalled, 264 | finishUp; 265 | 266 | self._destroySoonCalled = true; 267 | if (!oldDestroySoonCalled) { 268 | // The write queue here is the GET data from cloud. 269 | /** @private */ 270 | finishUp = function () { 271 | self._requestDone = true; 272 | self.destroy(); 273 | }; 274 | 275 | self._request.on('end', finishUp); 276 | self._request.on('error', finishUp); 277 | self._realRequest.on('end', finishUp); 278 | self._realRequest.on('error', finishUp); 279 | } 280 | }; 281 | 282 | /* 283 | * *Implements* **[``pipe``][0]** 284 | * [0]: http://nodejs.org/docs/v0.4.9/api/streams.html#stream.pipe 285 | * 286 | * "Connects this read stream to destination WriteStream. Incoming data on 287 | * this stream gets written to destination. The destination and source 288 | * streams are kept in sync by pausing and resuming as necessary." 289 | * 290 | * @param {WriteStream} destination Destination stream. 291 | * @name stream.ReadStream.pipe 292 | */ 293 | // Inherited from stream.Stream.pipe... 294 | //ReadStream.prototype.pipe = function (destination, options) { 295 | //}; 296 | 297 | /** 298 | * End the underlying cloud request. 299 | * 300 | * Typically starts the async code execution. 301 | * 302 | * *Note*: This function can be called multiple times without bad effect. 303 | * Calling code has the option to call ``end()`` once the request is set 304 | * up, or leave it to the end user. 305 | */ 306 | ReadStream.prototype.end = function () { 307 | var self = this, 308 | oldEndCalled = self._endCalled; 309 | 310 | self._endCalled = true; 311 | if (!oldEndCalled && !self._destroyCalled && !self._destroySoonCalled) { 312 | self._request.end(); 313 | } 314 | }; 315 | 316 | module.exports.ReadStream = ReadStream; 317 | 318 | /** 319 | * Completion event ('``end``'). 320 | * 321 | * Extra event to allow caller to know that writing has been completed. 322 | * 323 | * @name stream.WriteStream#event:end 324 | * @event 325 | * @param {Object} results Results object. 326 | * @param {Object} meta Headers, meta object. 327 | * @config {Object} [headers] HTTP headers. 328 | * @config {Object} [cloudHeaders] Cloud provider headers. 329 | * @config {Object} [metadata] Cloud metadata. 330 | */ 331 | /** 332 | * Drain event ('``drain``'). 333 | * 334 | * *Implements* **[``Event 'drain'``][0]** 335 | * [0]: http://nodejs.org/docs/v0.4.9/api/streams.html#event_drain_ 336 | * 337 | * @name stream.WriteStream#event:drain 338 | * @event 339 | */ 340 | /** 341 | * Error event ('``error``'). 342 | * 343 | * *Implements* **[``Event 'error'``][0]** 344 | * [0]: http://nodejs.org/docs/v0.4.9/api/streams.html#event_error_ 345 | * 346 | * @name stream.WriteStream#event:error 347 | * @event 348 | * @param {Error|errors.CloudError} err Error object. 349 | */ 350 | /** 351 | * Close event ('``close``'). 352 | * 353 | * *Implements* **[``Event 'close'``][0]** 354 | * [0]: http://nodejs.org/docs/v0.4.9/api/streams.html#event_close_ 355 | * 356 | * @name stream.WriteStream#event:close 357 | * @event 358 | */ 359 | /** 360 | * Pipe event ('``pipe``'). 361 | * 362 | * *Implements* **[``Event 'pipe'``][0]** 363 | * [0]: http://nodejs.org/docs/v0.4.9/api/streams.html#event_pipe_ 364 | * 365 | * @name stream.WriteStream#event:pipe 366 | * @param {Object} src A Readable Stream object. 367 | * @event 368 | */ 369 | /** 370 | * Writable cloud stream. 371 | * 372 | * *Implements* **[``Writable Stream``][0]** interface. 373 | * [0]: http://nodejs.org/docs/v0.4.9/api/streams.html#writable_Stream 374 | * 375 | * @param {request.AuthenticatedRawRequest} 376 | * request Request object. 377 | * @param {Object} [options] Options object. 378 | * @config {Function} [errorFn] Function to call with error. 379 | * @config {Function} [endFn] Function to call with results. Called: 380 | * endFn(response). 381 | * @exports WriteStream as stream.WriteStream 382 | * @constructor 383 | */ 384 | WriteStream = function (request, options) { 385 | options = options || {}; 386 | var self = this; 387 | 388 | // Members. 389 | self._request = request; 390 | self._writable = true; 391 | self._realRequest = request.realRequest; 392 | self._requestDone = false; 393 | self._errorHandled = false; 394 | self._response = null; 395 | self._buf = []; 396 | self._errorFn = options.errorFn || null; 397 | self._endFn = options.endFn || null; 398 | 399 | // Called state 400 | self._endCalled = false; 401 | self._destroyCalled = false; 402 | self._destroySoonCalled = false; 403 | 404 | // Accumulate writes. 405 | self._writeBuf = new Buffer(0); 406 | 407 | // Error handling 408 | // TODO: Could have common Stream base class and share _handleErr. 409 | self._handleErr = function (err, response) { 410 | if (!self._errorHandled) { 411 | self._errorHandled = true; 412 | self._writable = false; 413 | self._requestDone = true; 414 | if (self._errorFn) { 415 | self._errorFn(err, self, response); 416 | } else { 417 | self.emit('error', err); 418 | } 419 | } 420 | }; 421 | 422 | // Errors: straight pass-through (AuthReq has response). 423 | self._request.on('error', function (err, response) { 424 | self._handleErr(err, response); 425 | }); 426 | 427 | // Event binding. 428 | self._realRequest.on('response', function (response) { 429 | var encoding = 'utf8'; 430 | 431 | // Store response and set encoding. 432 | self._response = response; 433 | response.setEncoding(encoding); 434 | 435 | // Add bindings. 436 | response.on('data', function (chunk) { 437 | self._buf.push(chunk); 438 | }); 439 | response.on('end', function () { 440 | var results = null, 441 | msg, 442 | err; 443 | 444 | // Set state. 445 | self._writable = false; 446 | self._requestDone = true; 447 | 448 | if (self._endFn) { 449 | results = self._endFn(response); 450 | } 451 | 452 | switch (response.statusCode) { 453 | case 200: 454 | self.emit('end', results, self._request.getMeta(response)); 455 | break; 456 | default: 457 | // Everything unknown is an error. 458 | msg = utils.bufToStr(self._buf, encoding, 'utf8'); 459 | err = new CloudError(msg, { response: response }); 460 | self._handleErr(err, response); 461 | } 462 | }); 463 | }); 464 | }; 465 | 466 | util.inherits(WriteStream, EventEmitter); 467 | 468 | /** 469 | * *Implements* **[``writable``][0]** 470 | * [0]: http://nodejs.org/docs/v0.4.9/api/streams.html#stream.writable 471 | * 472 | * "A boolean that is true by default, but turns false after an 'error' 473 | * occurred or end() / destroy() was called." 474 | * 475 | * @name WriteStream.writable 476 | * @type boolean 477 | */ 478 | Object.defineProperties(WriteStream.prototype, { 479 | writable: { 480 | get: function () { 481 | return this._writable; 482 | } 483 | } 484 | }); 485 | 486 | /** 487 | * Write to internal buffer. 488 | * 489 | * Used to accumulate writes for real one-shot cloud PUT on ``end()``. 490 | * 491 | * @param {Buffer|string} value Buffer / string to write. 492 | * @param {string='utf8'} encoding Encoding to use if string value. 493 | * @private 494 | */ 495 | WriteStream.prototype._addToBuffer = function (value, encoding) { 496 | var self = this, 497 | oldBuf, 498 | oldBufLength, 499 | newBuf, 500 | newBufLength, 501 | concatBuf; 502 | 503 | if (value) { 504 | // Convert to buffer if not already. 505 | if (typeof value === 'string') { 506 | encoding = encoding || 'utf8'; 507 | value = new Buffer(value, encoding); 508 | } 509 | 510 | // Append the buffer. 511 | if (value instanceof Buffer) { 512 | // Helper variables. 513 | oldBuf = self._writeBuf; 514 | oldBufLength = oldBuf.length; 515 | newBuf = value; 516 | newBufLength = value.length; 517 | 518 | // Copy the buffers. 519 | concatBuf = new Buffer(oldBufLength + newBufLength); 520 | oldBuf.copy(concatBuf, 0, 0, oldBufLength); 521 | newBuf.copy(concatBuf, oldBufLength, 0, newBufLength); 522 | 523 | // Update the internal buffer. 524 | self._writeBuf = concatBuf; 525 | 526 | } else { 527 | throw new Error("Unknown value type: " + (typeof value)); 528 | } 529 | } 530 | }; 531 | 532 | /** 533 | * Write string/buffer to stream. 534 | * 535 | * Argument based options: 536 | * 537 | * - ``write(string, encoding='utf8', [fd])`` 538 | * - ``write(buffer)`` 539 | * 540 | * **Note**: ``fd`` parameter is not currently supported. 541 | * 542 | * **Note**: AWS/GSFD does not support ``Transfer-Encoding: chunked``, so 543 | * we accumulate buffers and write it all in one shot at the ``end()`` call. 544 | * In the future, parallel upload might be supported. 545 | * 546 | * This also means that nothing actually happens on the network until an 547 | * ``end()`` is called. 548 | * 549 | * *Implements* **[``write``][0]** 550 | * [0]: http://nodejs.org/docs/v0.4.9/api/streams.html#stream.write 551 | * 552 | * "Writes string with the given encoding to the stream. Returns true if the 553 | * string has been flushed to the kernel buffer. Returns false to indicate 554 | * that the kernel buffer is full, and the data will be sent out in the 555 | * future. The 'drain' event will indicate when the kernel buffer is empty 556 | * again. The encoding defaults to 'utf8'. 557 | * 558 | * If the optional fd parameter is specified, it is interpreted as an 559 | * integral file descriptor to be sent over the stream. This is only 560 | * supported for UNIX streams, and is silently ignored otherwise. When 561 | * writing a file descriptor in this manner, closing the descriptor before 562 | * the stream drains risks sending an invalid (closed) FD." 563 | * 564 | * ... and ... 565 | * 566 | * "Same as the above except with a raw buffer." 567 | * 568 | * @param {Buffer|string} value Buffer / string to write. 569 | * @param {string='utf8'} encoding Encoding to use if string value. 570 | * @return {boolean} If buffer is available (always true). 571 | */ 572 | WriteStream.prototype.write = function (value, encoding) { 573 | if (this._writable) { 574 | this._addToBuffer(value, encoding); 575 | } 576 | 577 | return this._writable; 578 | }; 579 | 580 | /** 581 | * *Implements* **[``end``][0]** 582 | * [0]: http://nodejs.org/docs/v0.4.9/api/streams.html#stream.end 583 | * 584 | * "Terminates the stream with EOF or FIN." 585 | * 586 | * Argument based options: 587 | * 588 | * - ``end()`` 589 | * - ``end(string, encoding)`` 590 | * - ``end(buffer)`` 591 | * 592 | * @param {Buffer|string} value Buffer / string to write. 593 | * @param {string='utf8'} encoding Encoding to use if string value. 594 | */ 595 | WriteStream.prototype.end = function (value, encoding) { 596 | var self = this, 597 | req = self._request, 598 | byteLength; 599 | 600 | if (self._writable && !self._endCalled) { 601 | // Set object state. 602 | self._endCalled = true; 603 | self._writable = false; 604 | 605 | // Write any values to internal buffer. 606 | self._addToBuffer(value, encoding); 607 | 608 | // Add headers for one-shot request, and actually end() request with 609 | // the write buffer. 610 | req.setHeader('content-length', self._writeBuf.length); 611 | req.end(self._writeBuf); 612 | } 613 | }; 614 | 615 | /** 616 | * *Implements* **[``destroy``][0]** 617 | * [0]: http://nodejs.org/docs/v0.4.9/api/streams.html#stream.destroy 618 | * 619 | * "Closes the underlying file descriptor. Stream will not emit any more 620 | * events." 621 | */ 622 | WriteStream.prototype.destroy = function () { 623 | var self = this, 624 | oldDestroyCalled = self._destroyCalled; 625 | 626 | self._destroyCalled = true; 627 | if (!oldDestroyCalled) { 628 | // Remove all listeners. 629 | self.removeAllListeners('data'); 630 | self.removeAllListeners('end'); 631 | self.removeAllListeners('error'); 632 | self._realRequest.removeAllListeners('data'); 633 | self._realRequest.removeAllListeners('end'); 634 | self._realRequest.removeAllListeners('error'); 635 | 636 | // Set sink for errors. 637 | self.on('error', function () {}); 638 | self._realRequest.on('error', function () {}); 639 | 640 | // Close the connection. 641 | self._requestDone = true; 642 | if (self._realRequest && self._realRequest.connection) { 643 | self._realRequest.connection.end(); 644 | } 645 | } 646 | }; 647 | 648 | /** 649 | * *Implements* **[``destroySoon``][0]** 650 | * [0]: http://nodejs.org/docs/v0.4.9/api/streams.html#stream.destroySoon 651 | * 652 | * "After the write queue is drained, close the file descriptor. 653 | * destroySoon() can still destroy straight away, as long as there is no 654 | * data left in the queue for writes." 655 | */ 656 | // TODO: Test WriteStream.destroySoon. 657 | WriteStream.prototype.destroySoon = function () { 658 | var self = this, 659 | oldDestroySoonCalled = self._destroySoonCalled, 660 | finishUp; 661 | 662 | self._destroySoonCalled = true; 663 | if (!oldDestroySoonCalled && self._endCalled) { 664 | if (self._endCalled) { 665 | // Destroy immediately - we have all the write data. 666 | self.destroy(); 667 | 668 | } else { 669 | // If end() is not called, we should wait a bit. 670 | /** @private */ 671 | finishUp = function () { 672 | self._requestDone = true; 673 | self.destroy(); 674 | }; 675 | 676 | self.on('end', finishUp); 677 | self.on('error', finishUp); 678 | self._request.on('end', finishUp); 679 | self._request.on('error', finishUp); 680 | self._realRequest.on('end', finishUp); 681 | self._realRequest.on('error', finishUp); 682 | } 683 | } 684 | }; 685 | 686 | module.exports.WriteStream = WriteStream; 687 | }()); 688 | --------------------------------------------------------------------------------