diff --git a/src/bos_client.js b/src/bos_client.js index b667c81..b33db25 100644 --- a/src/bos_client.js +++ b/src/bos_client.js @@ -33,6 +33,7 @@ var HttpClient = require('./http_client'); var BceBaseClient = require('./bce_base_client'); var MimeType = require('./mime.types'); var WMStream = require('./wm_stream'); +var Multipart = require('./multipart'); // var MIN_PART_SIZE = 1048576; // 1M // var THREAD = 2; @@ -627,6 +628,84 @@ BosClient.prototype.listMultipartUploads = function (bucketName, options) { }); }; +/** + * Generate PostObject policy signature. + * + * @param {Object} policy The policy object. + * @return {string} + */ +BosClient.prototype.signPostObjectPolicy = function (policy) { + var credentials = this.config.credentials; + var auth = new Auth(credentials.ak, credentials.sk); + + policy = new Buffer(JSON.stringify(policy)).toString('base64'); + var signature = auth.hash(policy, credentials.sk); + + return { + policy: policy, + signature: signature + }; +}; + +/** + * Post an object. + * + * @see {http://wiki.baidu.com/pages/viewpage.action?pageId=161461681} + * + * @param {string} bucketName The bucket name. + * @param {string} key The object name. + * @param {string|Buffer} data The file raw data or file path. + * @param {Object} options The form fields. + * @return {Promise} + */ +BosClient.prototype.postObject = function (bucketName, key, data, options) { + var boundary = 'MM8964' + (Math.random() * Math.pow(2, 63)).toString(36); + var contentType = 'multipart/form-data; boundary=' + boundary; + + if (u.isString(data)) { + data = fs.readFileSync(data); + } + else if (!Buffer.isBuffer(data)) { + throw new Error('Invalid data type.'); + } + + var credentials = this.config.credentials; + var ak = credentials.ak; + + var blacklist = ['signature', 'accessKey', 'key', 'file']; + options = u.omit(options || {}, blacklist); + + var multipart = new Multipart(boundary); + for (var k in options) { + if (options.hasOwnProperty(k)) { + if (k !== 'policy') { + multipart.addPart(k, options[k]); + } + } + } + + if (options.policy) { + var rv = this.signPostObjectPolicy(options.policy); + multipart.addPart('policy', rv.policy); + multipart.addPart('signature', rv.signature); + } + + multipart.addPart('accessKey', ak); + multipart.addPart('key', key); + multipart.addPart('file', data); + + var body = multipart.encode(); + + var headers = {}; + headers[H.CONTENT_TYPE] = contentType; + + return this.sendRequest('POST', { + bucketName: bucketName, + body: body, + headers: headers + }); +}; + // --- E N D --- BosClient.prototype.sendRequest = function (httpMethod, varArgs) { diff --git a/src/multipart.js b/src/multipart.js new file mode 100644 index 0000000..97ac4bb --- /dev/null +++ b/src/multipart.js @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2014 Baidu.com, Inc. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * @file src/multipart.js + * @author leeight + */ + +var util = require('util'); + +var u = require('underscore'); + +/** + * Multipart Encoder + * + * @param {string} boundary The multipart boundary. + */ +function Multipart(boundary) { + this._boundary = boundary; + + /** + * @type {Array.} + */ + this._parts = []; +} + +/** + * Add a part + * + * @param {string} name The part name. + * @param {string|Buffer} data The part data. + */ +Multipart.prototype.addPart = function (name, data) { + var part = []; + + var header = util.format( + '--%s\r\nContent-Disposition: form-data; name="%s"%s\r\n\r\n', + this._boundary, name, ''); + part.push(new Buffer(header)); + + if (Buffer.isBuffer(data)) { + part.push(data); + part.push(new Buffer('\r\n')); + } + else if (u.isString(data)) { + part.push(new Buffer(data + '\r\n')); + } + else { + throw new Error('Invalid data type.'); + } + + this._parts.push(Buffer.concat(part)); +}; + +Multipart.prototype.encode = function () { + return Buffer.concat( + [ + Buffer.concat(this._parts), + new Buffer(util.format('--%s--', this._boundary)) + ] + ); +}; + +module.exports = Multipart; + + + + + + + + + + +/* vim: set ts=4 sw=4 sts=4 tw=120: */ diff --git a/test/run-all.sh b/test/run-all.sh index 32b22c1..dfd1e60 100755 --- a/test/run-all.sh +++ b/test/run-all.sh @@ -55,6 +55,7 @@ SPECS=( test/sdk/sts.spec.js test/sdk/crypto.spec.js test/sdk/auth.spec.js + test/sdk/multipart.spec.js test/sdk/http_client.spec.js test/sdk/mime.types.spec.js test/sdk/bos_client.spec.js diff --git a/test/sdk/multipart.spec.js b/test/sdk/multipart.spec.js new file mode 100644 index 0000000..f4dff01 --- /dev/null +++ b/test/sdk/multipart.spec.js @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2014 Baidu.com, Inc. All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +var expect = require('expect.js'); + +var Multipart = require('../../src/multipart'); + +describe('Multipart', function () { + it('invalid data type', function () { + var multipart = new Multipart('hahaha'); + try { + multipart.addPart('accessKey', null); + expect().fail('SHOULD NOT REACH HERE'); + } + catch (ex) { + expect(ex).to.be.an(Error); + } + }); + + it('encode', function () { + var multipart = new Multipart('hahaha'); + multipart.addPart('accessKey', '499d0610679c4da2a69b64086a4cc3bc'); + multipart.addPart('policy', 'eyJleHBpcmF0aW9uIjoiMjA='); + multipart.addPart('signature', 'd1a617a725122c20319'); + multipart.addPart('key', new Buffer('world.txt')); + multipart.addPart('Content-Disposition', 'attachment;filename="download/object"'); + multipart.addPart('x-bce-meta-object-tag', new Buffer('test1')); + + var encoded = multipart.encode().toString(); + expect(encoded).to.eql( + '--hahaha\r\n' + + 'Content-Disposition: form-data; name="accessKey"\r\n\r\n' + + '499d0610679c4da2a69b64086a4cc3bc\r\n' + + '--hahaha\r\n' + + 'Content-Disposition: form-data; name="policy"\r\n\r\n' + + 'eyJleHBpcmF0aW9uIjoiMjA=\r\n' + + '--hahaha\r\n' + + 'Content-Disposition: form-data; name="signature"\r\n\r\n' + + 'd1a617a725122c20319\r\n' + + '--hahaha\r\n' + + 'Content-Disposition: form-data; name="key"\r\n\r\n' + + 'world.txt\r\n' + + '--hahaha\r\n' + + 'Content-Disposition: form-data; name="Content-Disposition"\r\n\r\n' + + 'attachment;filename="download/object"\r\n' + + '--hahaha\r\n' + + 'Content-Disposition: form-data; name="x-bce-meta-object-tag"\r\n\r\n' + + 'test1\r\n' + + '--hahaha--' + ); + }); +}); + + + + + + + + + + +/* vim: set ts=4 sw=4 sts=4 tw=120: */ diff --git a/test/sdk/post_object.spec.js b/test/sdk/post_object.spec.js new file mode 100644 index 0000000..85ae0d0 --- /dev/null +++ b/test/sdk/post_object.spec.js @@ -0,0 +1,165 @@ +/* +* Copyright (c) 2014 Baidu.com, Inc. All Rights Reserved +* +* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +* the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +* specific language governing permissions and limitations under the License. +*/ + +var util = require('util'); +var path = require('path'); +var fs = require('fs'); + +var Q = require('q'); +var u = require('underscore'); +var expect = require('expect.js'); +var debug = require('debug')('bos_client.spec'); + +var config = require('../config'); +var helper = require('./helper'); +var BosClient = require('../..').BosClient; +var crypto = require('../../src/crypto'); + +describe('XBosClient', function () { + var client; + var fail; + + beforeEach(function () { + fail = helper.fail(this); + + client = new BosClient({ + endpoint: 'https://bos.qasandbox.bcetest.baidu.com', + credentials: { + ak: 'f932edbccdb04cec8e3307b1798f16e6', + sk: 'cdeb07159a884b80aac08ff474b6e71f' + } + }); + }); + + it('postObject to public-read bucket', function (done) { + client.postObject('bcecdn', 'world.txt', new Buffer('hello world')) + .then(function (response) { + expect().fail('SHOULD NOT REACH HERE.'); + }) + .catch(function (error) { + debug(error); + expect(error.status_code).to.eql(403); + }) + .fin(done); + }); + + it('postObject with expired policy', function (done) { + client.postObject('bcecdn', 'world.txt', new Buffer('hello world'), { + 'policy': { + 'expiration': '2015-04-26T13:29:46Z', + 'conditions': [ + {'bucket': 'bcecdn'} + ] + } + }) + .then(function () { + expect().fail('SHOULD NOT REACH HERE.'); + }) + .catch(function (error) { + debug(error); + expect(error.status_code).to.eql(403); + expect(error.code).to.eql('RequestExpired'); + }) + .fin(done); + }); + + it('postObject with success-action-status-200', function (done) { + client.postObject('bcecdn', 'world.txt', new Buffer('hello world'), { + 'success-action-status': '204', + 'policy': { + 'expiration': '2016-04-26T13:29:46Z', + 'conditions': [ + {'bucket': 'bcecdn'} + ] + } + }) + .then(function (response) { + debug(response); + expect(response.http_headers['etag']).to.eql('5eb63bbbe01eeed093cb22bb8f5acdc3'); + }) + .catch(fail) + .fin(done); + }); + + it('postObject with invalid content-length', function (done) { + client.postObject('bcecdn', 'world.txt', new Buffer('hello world'), { + 'policy': { + 'expiration': '2016-04-26T13:29:46Z', + 'conditions': [ + {'bucket': 'bcecdn'}, + ["content-length-range", 0, 10] + ] + } + }) + .then(function () { + expect().fail('SHOULD NOT REACH HERE.'); + }) + .catch(function (error) { + debug(error); + expect(error.status_code).to.eql(400); + expect(error.code).to.eql('MaxMessageLengthExceeded'); + }) + .fin(done); + }); + + it('postObject with invalid key name', function (done) { + client.postObject('bcecdn', 'world.txt', new Buffer('hello world'), { + 'policy': { + 'expiration': '2016-04-26T13:29:46Z', + 'conditions': [ + {'bucket': 'bcecdn'}, + {'key': 'abc*'} + ] + } + }) + .then(function (response) { + expect().fail('SHOULD NOT REACH HERE.'); + }) + .catch(function (error) { + debug(error); + // expect(1).to.eql(2); + expect(error.status_code).to.eql(403); + expect(error.code).to.eql('AccessDenied'); + }) + .fin(done); + }); + + it('postObject with success-action-status-201', function (done) { + client.postObject('bcecdn', 'world.txt', new Buffer('hello world'), { + 'Content-Type': 'foo/bar', + 'x-bce-meta-foo': 'bar', + 'success-action-redirect': 'https://www.baidu.com', + 'success-action-status': '201', + 'policy': { + 'expiration': '2016-04-26T13:29:46Z', + 'conditions': [ + {'bucket': 'bcecdn'} + ] + } + }) + .then(function (response) { + expect().fail('SHOULD NOT REACH HERE.'); + }) + .catch(function (error) { + debug(error); + expect(error.status_code).to.eql(302); + return client.getObjectMetadata('bcecdn', 'world.txt'); + }) + .then(function (response) { + debug(response); + expect(response.http_headers['content-type']).to.eql('foo/bar'); + expect(response.http_headers['x-bce-meta-foo']).to.eql('bar'); + }) + .fin(done); + }); +});