diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e3dff87 --- /dev/null +++ b/.gitignore @@ -0,0 +1,212 @@ +# Created by .ignore support plugin (hsz.mobi) +### Windows template +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk +### Java template +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +### Eclipse template + +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet +### macOS template +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk +### Node template +# Logs +logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + + +*.iws +*.iml +.idea/ +out/ diff --git a/AtmosJS/build.xml b/AtmosJS/build.xml index 9c4d3e4..306783a 100644 --- a/AtmosJS/build.xml +++ b/AtmosJS/build.xml @@ -1,10 +1,11 @@ - + + diff --git a/AtmosJS/package.json b/AtmosJS/package.json index ef22b4d..4e315b4 100644 --- a/AtmosJS/package.json +++ b/AtmosJS/package.json @@ -1,8 +1,8 @@ { - "author": "Chris Arnett ", + "author": "Stu Arnett ", "name": "atmos", "description": "Connector for EMC Atmos cloud storage", - "version": "2.1.1", + "version": "2.1.2", "homepage": "http://code.google.com/p/atmos-js", "repository": { "url": "" diff --git a/AtmosJS/samples/node/create-object.js b/AtmosJS/samples/node/create-object.js index 5af1a84..d2733dd 100644 --- a/AtmosJS/samples/node/create-object.js +++ b/AtmosJS/samples/node/create-object.js @@ -1,4 +1,4 @@ -AtmosJS = require( '../../atmos-min-2.1.1.js' ); +AtmosJS = require( '../../atmos-min-2.1.2.js' ); AtmosRest = AtmosJS.AtmosRest; ListOptions = AtmosJS.ListOptions; AtmosRange = AtmosJS.AtmosRange; diff --git a/AtmosJS/samples/node/delete-object.js b/AtmosJS/samples/node/delete-object.js index 9a0726c..16b9e3f 100644 --- a/AtmosJS/samples/node/delete-object.js +++ b/AtmosJS/samples/node/delete-object.js @@ -1,4 +1,4 @@ -AtmosJS = require( '../../atmos-min-2.1.1.js' ); +AtmosJS = require( '../../atmos-min-2.1.2.js' ); AtmosRest = AtmosJS.AtmosRest; ListOptions = AtmosJS.ListOptions; AtmosRange = AtmosJS.AtmosRange; diff --git a/AtmosJS/samples/node/get-server-info.js b/AtmosJS/samples/node/get-server-info.js index 051f40a..1620c41 100644 --- a/AtmosJS/samples/node/get-server-info.js +++ b/AtmosJS/samples/node/get-server-info.js @@ -1,4 +1,4 @@ -AtmosJS = require( '../../atmos-min-2.1.1.js' ); +AtmosJS = require( '../../atmos-min-2.1.2.js' ); AtmosRest = AtmosJS.AtmosRest; ListOptions = AtmosJS.ListOptions; AtmosRange = AtmosJS.AtmosRange; diff --git a/AtmosJS/samples/node/read-object.js b/AtmosJS/samples/node/read-object.js index 892c6fa..daf9afa 100644 --- a/AtmosJS/samples/node/read-object.js +++ b/AtmosJS/samples/node/read-object.js @@ -1,4 +1,4 @@ -AtmosJS = require( '../../atmos-min-2.1.1.js' ); +AtmosJS = require( '../../atmos-min-2.1.2.js' ); AtmosRest = AtmosJS.AtmosRest; ListOptions = AtmosJS.ListOptions; AtmosRange = AtmosJS.AtmosRange; diff --git a/AtmosJS/src/AtmosRest.js b/AtmosJS/src/AtmosRest.js index 166823e..86e0303 100644 --- a/AtmosJS/src/AtmosRest.js +++ b/AtmosJS/src/AtmosRest.js @@ -35,6 +35,9 @@ if ( isNodejs ) { AtmosRest = function( atmosConfig ) { this.atmosConfig = atmosConfig; + // defaults (param could be in-line object, so can't rely on AtmosConfig constructor) + if (typeof(atmosConfig.enableUtf8) == 'undefined') atmosConfig.enableUtf8 = true; + this.info( "AtmosRest loaded" ); }; @@ -90,99 +93,67 @@ AtmosRest.prototype.getServiceInformation = function( callback ) { /** * Creates an object in Atmos - * @param {Acl} acl an Acl for the new object. If null, the object will get the default Acl. - * @param {Object} meta regular Metadata for the new object. May be null for no regular metadata. - * @param {Object} listableMeta listable Metadata for the new object. May be null for no listable metadata. - * @param {string} form the form element that contains the file(s) to upload. Either form or data must be specified. + * @param {Object} params Parameters for the function. + * @param {string} [params.path] the namespace path in Atmos. Must start with a slash. Must end with a slash if creating a directory. + * @param {Acl} [params.acl] an Acl for the new object. If null, the object will get the default Acl. + * @param {Object} [params.meta] regular Metadata for the new object. May be null for no regular metadata. + * @param {Object} [params.listableMeta] listable Metadata for the new object. May be null for no listable metadata. + * @param {string} [params.form] the form element that contains the file(s) to upload. * NOTE: multipart forms must be supported by your Atmos version (check AtmosServiceInfo.browsercompat). - * @param {string|File} data the data for the new object (can be a String, Blob or File). Either form or data must be specified - * @param {string} mimeType the mimeType for the new object. If null, the object will be assigned application/octet-stream. - * Leave blank if form is present (mime type will be extracted from multipart data) - * @param {function} successCallback the callback for when the function completes. Should have the signature function(result) + * @param {string|Blob|File} [params.data] the data for the new object. If null, an empty object will be created. Use null for directories. + * @param {Checksum} [params.checksum] placeholder for checksum. Provide an instance if you wish to use a checksum. + * Be sure to use the same instance for updates (only appends are supported for checksummed objects). + * @param {string} [params.mimeType] the mimeType for the new object. If null, the object will be assigned application/octet-stream. + * Leave blank if form is present (mime type will be extracted from multipart data) + * @param {function} params.successCallback the callback for when the function completes. Should have the signature function(result) * where result will be an AtmosResult object. The created Object ID will be in the value field of the result object. - * @param {function=} progressCallback (optional) the callback for progress updates (i.e. status bar) + * @param {function} [params.progressCallback] the callback for progress updates (i.e. status bar) */ -AtmosRest.prototype.createObject = function( acl, meta, listableMeta, form, data, mimeType, successCallback, progressCallback ) { - var headers = {}; - var me = this; - - this._addAclHeaders( acl, headers ); - this._addMetadataHeaders( meta, headers, false ); - this._addMetadataHeaders( listableMeta, headers, true ); - - this._ajax( /** @type HttpRequest */ { - uri: this.context + '/objects', - method: 'POST', - headers: headers, - data: data, - mimeType: mimeType, - progress: progressCallback, - processResult: function( result, xhr ) { - if ( !result.successful ) return; - me._processCreateObjectResult( result, xhr ); - }, - complete: successCallback, - form: form - } ); -}; - -/** - * Creates an object in Atmos on the path provided. - * - * @param {string} path the namespace path in Atmos (must start with a slash) - * @param {Acl} acl an Acl for the new object. If null, the object will get the default Acl. - * @param {Object} meta regular Metadata for the new object. May be null for no regular metadata. - * @param {Object} listableMeta listable Metadata for the new object. May be null for no listable metadata. - * @param {Element} form the form element that contains the file(s) to upload. Either form or data must be specified - * NOTE: multipart forms must be supported by your Atmos version (check AtmosServiceInfo.browsercompat). - * @param {string|File} data the data for the new object (can be a String, Blob or File). Either form or data must be specified - * @param {string} mimeType the mimeType for the new object. If null, the object will be assigned application/octet-stream. - * Leave blank if form is present (mime type will be extracted from multipart data) - * @param {function} successCallback the callback for when the function completes. Should have the signature function(result) - * where result will be an AtmosResult object. The created Object ID will be in the value field of the result object. - * @param {function=} progressCallback the (optional) callback for progress updates (i.e. status bar) - */ -AtmosRest.prototype.createObjectOnPath = function( path, acl, meta, listableMeta, form, data, mimeType, successCallback, progressCallback ) { - if ( !AtmosRest.objectPathMatch.test( path ) ) { - throw "The path '" + path + "' is not valid"; +AtmosRest.prototype.createObject = function( params ) { + if ( params.path && !AtmosRest.objectPathMatch.test( params.path ) ) { + throw "The path '" + params.path + "' is not valid"; } var headers = {}; var me = this; - this._addAclHeaders( acl, headers ); - this._addMetadataHeaders( meta, headers, false ); - this._addMetadataHeaders( listableMeta, headers, true ); + this._addAclHeaders( params.acl, headers ); + this._addMetadataHeaders( params.meta, headers, false ); + this._addMetadataHeaders( params.listableMeta, headers, true ); + this._addChecksumHeader( params.data, headers ); this._ajax( /** @type HttpRequest */ { - uri: this._getPath( path ), + uri: params.path ? this._getPath( params.path ) : this.context + '/objects', method: 'POST', headers: headers, - data: data, - mimeType: mimeType, - progress: progressCallback, + data: params.data, + mimeType: params.mimeType, + progress: params.progressCallback, processResult: function( result, xhr ) { if ( !result.successful ) return; me._processCreateObjectResult( result, xhr ); }, - complete: successCallback, - form: form + complete: params.successCallback, + form: params.form } ); }; /** * Reads the contents of an object from Atmos * @param {string} id the object identifier (either an object path or an object id) - * @param {AtmosRange} range the range of the object to read, pass null to read the entire object. + * @param {AtmosRange} [range] the range of the object to read, pass null to read the entire object. * @param {function} callback the completion callback (both error and success). Upon success, * the object's content will be returned in the data property of the result object. */ AtmosRest.prototype.readObject = function( id, range, callback ) { + var me = this; + this._ajax( /** @type HttpRequest */ { uri: this._getPath( id ), method: 'GET', headers: {}, range: range, processResult: function( result, xhr ) { + if ( !range ) me._verifyChecksum(result, xhr); if ( !result.successful ) return; result.data = xhr.responseText; }, @@ -194,18 +165,18 @@ AtmosRest.prototype.readObject = function( id, range, callback ) { * Updates an object in Atmos with the given ID. * * @param {string} id the object ID or namespace path in Atmos. - * @param {Acl} acl an Acl for the object. May be null for no updates. - * @param {Object} meta regular Metadata for the object. May be null for no updates. - * @param {Object} listableMeta listable Metadata for the object. May be null for no updates. - * @param {string} form the form element that contains the file(s) to upload. Either form or data must be specified + * @param {Acl} [acl] an Acl for the object. May be null for no updates. + * @param {Object} [meta] regular Metadata for the object. May be null for no updates. + * @param {Object} [listableMeta] listable Metadata for the object. May be null for no updates. + * @param {string} [form] the form element that contains the file(s) to upload. * NOTE: multipart forms must be supported by your Atmos version (check AtmosServiceInfo.browsercompat). - * @param {string|File} data the data for the new object (can be a String, Blob or File). Either form or data must be specified - * @param {AtmosRange} range the range of the object to update, pass null to replace the entire object or if a form is used. - * @param {string} mimeType the mimeType for the new object. If null, the object will be assigned application/octet-stream. + * @param {string|Blob|File} [data] the data for the new object. Pass null to form and data to truncate the object. + * @param {AtmosRange} [range] the range of the object to update, pass null to replace the entire object or if a form is used. + * @param {string} [mimeType] the mimeType for the new object. If null, the object will be assigned application/octet-stream. * Leave blank if form is present (mime type will be extracted from multipart data) * @param {function} successCallback the callback for when the function completes. Should have the signature function(result) * where result will be an AtmosResult object. The result of this call will only contain status information. - * @param {function=} progressCallback the (optional) callback for progress updates (i.e. status bar) + * @param {function} [progressCallback] the callback for progress updates (i.e. status bar) */ AtmosRest.prototype.updateObject = function( id, acl, meta, listableMeta, form, data, range, mimeType, successCallback, progressCallback ) { var headers = {}; @@ -213,6 +184,7 @@ AtmosRest.prototype.updateObject = function( id, acl, meta, listableMeta, form, this._addAclHeaders( acl, headers ); this._addMetadataHeaders( meta, headers, false ); this._addMetadataHeaders( listableMeta, headers, true ); + this._addChecksumHeader( data, headers ); this._ajax( /** @type HttpRequest */ { uri: this._getPath( id ), @@ -1679,6 +1651,26 @@ AtmosRest.prototype._resolveUrl = function( path, query ) { return url; }; +AtmosRest.prototype._addChecksumHeader = function( data, headers ) { + headers["x-emc-wschecksum"] = 'SHA1/' + data.length + '/' + + Crypto.util.bytesToHex( Crypto.SHA1( data, { asBytes: true } ) ); +}; + +AtmosRest.prototype._verifyChecksum = function( result, xhr ) { + if ( !result.success ) return; + var checksumStr = xhr.getResponseHeader( 'x-emc-wschecksum' ); + if ( checksumStr ) { + var checksumParts = checksumStr.split( '/' ); + if ( checksumParts[0] == 'SHA1' ) { + var checksum = Crypto.util.bytesToHex( Crypto.SHA1( xhr.responseText, { asBytes: true } ) ); + if ( checksum != checksumParts[2] ) { + result.success = false; + result.errorMessage = 'checksum failed'; + } + } + } +}; + // // Logging functions: see if the console is available and log to it. // diff --git a/AtmosJS/src/Checksum.js b/AtmosJS/src/Checksum.js new file mode 100644 index 0000000..313a331 --- /dev/null +++ b/AtmosJS/src/Checksum.js @@ -0,0 +1,35 @@ +/* + + Copyright (c) 2011-2013, EMC Corporation + + All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of the EMC Corporation nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + */ +/** + * Constructs a new Checksum object. Used to hold checksum data about an Atmos object. Pass a new instance to + * createObject and use this same instance for updateObject. Note that if a checksum is provided on creation, the + * running checksum is required for all updates. When using a checksum, you can only append to an object or delete it. + * @constructor + */ +Checksum = function() { + +}; +//////////////////////// +// Exports for NodeJS // +//////////////////////// +if ( typeof(exports) != 'undefined' ) { + exports.Checksum = Checksum; +} diff --git a/AtmosJS/testcases-min.html b/AtmosJS/testcases-min.html index f6d7a16..a9ddfd2 100644 --- a/AtmosJS/testcases-min.html +++ b/AtmosJS/testcases-min.html @@ -25,7 +25,7 @@ AtmosJS Unit Tests - + diff --git a/AtmosJS/tests/atmos-config.js b/AtmosJS/tests/atmos-config.js index 2325110..37935ec 100644 --- a/AtmosJS/tests/atmos-config.js +++ b/AtmosJS/tests/atmos-config.js @@ -20,8 +20,8 @@ */ var atmosConfig = /** @type AtmosConfig */ { - uid: "15f02fc57acd4e17bb79c3e83822e2c9/A50231654471c3a4fa57", - secret: "MrJjsV+LdcpT+C6AWGe0Infntiw=", + uid: "{subtenant-id}/{uid}", + secret: "***********", enableUtf8: true, enableDebug: false }; @@ -29,8 +29,9 @@ var atmosConfig = /** @type AtmosConfig */ { if ( typeof(global) != 'undefined' ) { // We're running inside node.js - atmosConfig.host = 'api.atmosonline.com'; - atmosConfig.protocol = 'http:'; + atmosConfig.protocol = 'https:'; + atmosConfig.host = 'atmos.ecstestdrive.com'; +// atmosConfig.port = 9022; global.atmosConfig = atmosConfig; } \ No newline at end of file diff --git a/AtmosJS/tests/lowlevel.js b/AtmosJS/tests/lowlevel.js index 0dd227a..e72df91 100644 --- a/AtmosJS/tests/lowlevel.js +++ b/AtmosJS/tests/lowlevel.js @@ -201,6 +201,34 @@ atmosLowLevel = { sxml = AtmosUtil.serializeXml( policy.toDocument() ).trim(); test.ok( sxml == '' || sxml == '', 'minimal XML serialization is valid' ); + test.done(); + }, + + 'Checksum test': function( test ) { + var validHeaders = { + 'x-emc-wschecksum': 'SHA1/12/2ef7bde608ce5404e97d5f042f95f89f1c232871' + }; + var badAlgHeaders = { + 'x-emc-wschecksum': 'MD5/12/2ef7bde608ce5404e97d5f042f95f89f1c232872' + }; + var badSumHeaders = { + 'x-emc-wschecksum': 'SHA1/12/2ef7bde608ce5404e97d5f042f95f89f1c232872' + }; + var xhr = function(headers) { + this.responseText = 'Hello World!'; + this.getResponseHeader = function(key) { + return headers[key]; + }; + }; + var result = {success: true}; + atmos._verifyChecksum(result, new xhr(validHeaders)); + test.ok( result.success && Object.keys(result).length == 1 , "valid checksum" ); + result = {success: true}; + atmos._verifyChecksum(result, new xhr(badAlgHeaders)); + test.ok( result.success && Object.keys(result).length == 1 , "wrong algorithm" ); + result = {success: true}; + atmos._verifyChecksum(result, new xhr(badSumHeaders)); + test.ok( !result.success && result.errorMessage == 'checksum failed' && Object.keys(result).length == 2 , "bad checksum" ); test.done(); } };