diff --git a/.gitignore b/.gitignore
index d09439de..7fe34d94 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,5 @@ logs
node_modules
/dist/
-yarn.lock
\ No newline at end of file
+yarn.lock
+package-lock.json
diff --git a/docs/API.md b/docs/API.md
index 37c337c7..1256d453 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -29,10 +29,10 @@ var s3Client = new Minio.Client({
```
| Bucket operations | Object operations | Presigned operations | Bucket Policy & Notification operations |
| ------------- |-------------| -----| ----- |
-| [`makeBucket`](#makeBucket) | [`getObject`](#getObject) | [`presignedGetObject`](#presignedGetObject) | [`getBucketNotification`](#getBucketNotification) |
-| [`listBuckets`](#listBuckets) | [`getPartialObject`](#getPartialObject) | [`presignedPutObject`](#presignedPutObject) | [`setBucketNotification`](#setBucketNotification) |
-| [`bucketExists`](#bucketExists) | [`fGetObject`](#fGetObject) | [`presignedPostPolicy`](#presignedPostPolicy) | [`removeAllBucketNotification`](#removeAllBucketNotification) |
-| [`removeBucket`](#removeBucket) | [`putObject`](#putObject) | | [`getBucketPolicy`](#getBucketPolicy) | |
+| [`makeBucket`](#makeBucket) | [`getObject`](#getObject) | [`presignedUrl`](#presignedUrl) | [`getBucketNotification`](#getBucketNotification) |
+| [`listBuckets`](#listBuckets) | [`getPartialObject`](#getPartialObject) | [`presignedGetObject`](#presignedGetObject) | [`setBucketNotification`](#setBucketNotification) |
+| [`bucketExists`](#bucketExists) | [`fGetObject`](#fGetObject) | [`presignedPutObject`](#presignedPutObject) | [`removeAllBucketNotification`](#removeAllBucketNotification) |
+| [`removeBucket`](#removeBucket) | [`putObject`](#putObject) | [`presignedPostPolicy`](#presignedPostPolicy) | [`getBucketPolicy`](#getBucketPolicy) | |
| [`listObjects`](#listObjects) | [`fPutObject`](#fPutObject) | | [`setBucketPolicy`](#setBucketPolicy)
| [`listObjectsV2`](#listObjectsV2) | [`copyObject`](#copyObject) | | [`listenBucketNotification`](#listenBucketNotification)|
| [`listIncompleteUploads`](#listIncompleteUploads) | [`statObject`](#statObject) |
@@ -349,9 +349,9 @@ __Parameters__
| Param | Type | Description |
|---|---|---|
-| `bucketName` | _string_ | Name of the bucket. |
-| `objectName` | _string_ | Name of the object. |
-| `callback(err, stream)` | _function_ | Callback is called with `err` in case of error. `stream` is the object content stream. If no callback is passed, a `Promise` is returned. |
+|`bucketName` | _string_ | Name of the bucket. |
+|`objectName` | _string_ | Name of the object. |
+|`callback(err, stream)` | _function_ | Callback is called with `err` in case of error. `stream` is the object content stream. If no callback is passed, a `Promise` is returned. |
__Example__
@@ -653,10 +653,55 @@ minioClient.removeIncompleteUpload('mybucket', 'photo.jpg', function(err) {
Presigned URLs are generated for temporary download/upload access to private objects.
+
+### presignedUrl(httpMethod, bucketName, objectName, expiry[, reqParams, cb])
+
+Generates a presigned URL for the provided HTTP method, 'httpMethod'. Browsers/Mobile clients may point to this URL to directly download objects even if the bucket is private. This presigned URL can have an associated expiration time in seconds after which the URL is no longer valid. The default value is 7 days.
+
+
+__Parameters__
+
+
+
+| Param | Type | Description |
+|---|---|---|
+|`bucketName` | _string_ | Name of the bucket. |
+|`objectName` | _string_ | Name of the object. |
+|`expiry` | _number_ | Expiry time in seconds. Default value is 7 days. |
+|`reqParams` | _object_ | request parameters. |
+|`callback(err, presignedUrl)` | _function_ | Callback function is called with non `null` err value in case of error. `presignedUrl` will be the URL using which the object can be downloaded using GET request. If no callback is passed, a `Promise` is returned. |
+
+
+__Example 1__
+
+
+```js
+// presigned url for 'getObject' method.
+// expires in a day.
+minioClient.presignedUrl('GET', 'mybucket', 'hello.txt', 24*60*60, function(err, presignedUrl) {
+ if (err) return console.log(err)
+ console.log(presignedUrl)
+})
+```
+
+
+__Example 2__
+
+
+```js
+// presigned url for 'listObject' method.
+// Lists objects in 'myBucket' with prefix 'data'.
+// Lists max 1000 of them.
+minioClient.presignedUrl('GET', 'mybucket', '', 1000, {'prefix': 'data', 'max-keys': 1000}, function(err, presignedUrl) {
+ if (err) return console.log(err)
+ console.log(presignedUrl)
+})
+```
+
### presignedGetObject(bucketName, objectName, expiry[, cb])
-Generates a presigned URL for HTTP GET operations. Browsers/Mobile clients may point to this URL to directly download objects even if the bucket is private. This presigned URL can have an associated expiration time in seconds after which the URL is no longer valid. The default expiry is set to 7 days.
+Generates a presigned URL for HTTP GET operations. Browsers/Mobile clients may point to this URL to directly download objects even if the bucket is private. This presigned URL can have an associated expiration time in seconds after which the URL is no longer valid. The default value is 7 days.
__Parameters__
@@ -665,10 +710,10 @@ __Parameters__
| Param | Type | Description |
|---|---|---|
-| `bucketName` | _string_ | Name of the bucket. |
-|`objectName` | _string_ | Name of the object. |
-| `expiry` |_number_ | Expiry in seconds. Default expiry is set to 7 days. |
-| `callback(err, presignedUrl)` | _function_ | Callback function is called with non `null` err value in case of error. `presignedUrl` will be the URL using which the object can be downloaded using GET request. If no callback is passed, a `Promise` is returned. |
+|`bucketName` | _string_ | Name of the bucket. |
+|`objectName` | _string_ | Name of the object. |
+|`expiry` | _number_ | Expiry time in seconds. Default value is 7 days. |
+|`callback(err, presignedUrl)` | _function_ | Callback function is called with non `null` err value in case of error. `presignedUrl` will be the URL using which the object can be downloaded using GET request. If no callback is passed, a `Promise` is returned. |
__Example__
@@ -685,7 +730,7 @@ minioClient.presignedGetObject('mybucket', 'hello.txt', 24*60*60, function(err,
### presignedPutObject(bucketName, objectName, expiry[, callback])
-Generates a presigned URL for HTTP PUT operations. Browsers/Mobile clients may point to this URL to upload objects directly to a bucket even if it is private. This presigned URL can have an associated expiration time in seconds after which the URL is no longer valid. The default expiry is set to 7 days.
+Generates a presigned URL for HTTP PUT operations. Browsers/Mobile clients may point to this URL to upload objects directly to a bucket even if it is private. This presigned URL can have an associated expiration time in seconds after which the URL is no longer valid. The default value is 7 days.
__Parameters__
@@ -693,10 +738,10 @@ __Parameters__
| Param | Type | Description |
|---|---|---|
-| `bucketName` | _string_ | Name of the bucket. |
-| `objectName` | _string_ | Name of the object. |
-| `expiry` | _number_ | Expiry in seconds. Default expiry is set to 7 days. |
-| `callback(err, presignedUrl)` | _function_ | Callback function is called with non `null` err value in case of error. `presignedUrl` will be the URL using which the object can be uploaded using PUT request. If no callback is passed, a `Promise` is returned. |
+|`bucketName` | _string_ | Name of the bucket. |
+|`objectName` | _string_ | Name of the object. |
+|`expiry` | _number_ | Expiry time in seconds. Default value is 7 days. |
+|`callback(err, presignedUrl)` | _function_ | Callback function is called with non `null` err value in case of error. `presignedUrl` will be the URL using which the object can be uploaded using PUT request. If no callback is passed, a `Promise` is returned. |
__Example__
diff --git a/package.json b/package.json
index 549cd12f..576732a5 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,8 @@
"through2": "^0.6.5",
"uuid": "^3.1.0",
"xml": "^1.0.0",
- "xml2js": "^0.4.15"
+ "xml2js": "^0.4.15",
+ "querystring": "0.2.0"
},
"devDependencies": {
"browserify": "^12.0.1",
diff --git a/src/main/minio.js b/src/main/minio.js
index 0e5974e5..44de8bf4 100644
--- a/src/main/minio.js
+++ b/src/main/minio.js
@@ -25,6 +25,7 @@ import BlockStream2 from 'block-stream2'
import Xml from 'xml'
import xml2js from 'xml2js'
import async from 'async'
+import querystring from 'querystring'
import mkdirp from 'mkdirp'
import path from 'path'
import _ from 'lodash'
@@ -1158,6 +1159,7 @@ export class Client {
if (queries.length > 0) {
query = `${queries.join('&')}`
}
+
var method = 'GET'
var transformer = transformers.getListObjectsTransformer()
this.makeRequest({method, bucketName, query}, '', 200, '', (e, response) => {
@@ -1471,31 +1473,39 @@ export class Client {
this.makeRequest({method, bucketName, query}, policyPayload, 204, '', cb)
}
- // Generate a presigned URL for PUT. Using this URL, the browser can upload to S3 only with the specified object name.
+ // Generate a generic presigned URL which can be
+ // used for HTTP methods GET, PUT, HEAD and DELETE
//
// __Arguments__
+ // * `method` _string_: name of the HTTP method
// * `bucketName` _string_: name of the bucket
// * `objectName` _string_: name of the object
// * `expiry` _number_: expiry in seconds (optional, default 7 days)
- presignedPutObject(bucketName, objectName, expires, cb) {
+ // * `reqParams` _object_: request parameters (optional)
+ presignedUrl(method, bucketName, objectName, expires, reqParams, cb) {
if (this.anonymous) {
- throw new errors.AnonymousRequestError('Presigned PUT url cannot be generated for anonymous requests')
+ throw new errors.AnonymousRequestError('Presigned ' + method + ' url cannot be generated for anonymous requests')
+ }
+ if (isFunction(reqParams)) {
+ cb = reqParams
+ reqParams = {}
}
if (isFunction(expires)) {
cb = expires
- expires = 24 * 60 * 60 * 7
- }
- if (!isValidBucketName(bucketName)) {
- throw new errors.InvalidBucketNameError('Invalid bucket name: ' + bucketName)
- }
- if (!isValidObjectName(objectName)) {
- throw new errors.InvalidObjectNameError(`Invalid object name: ${objectName}`)
+ reqParams = {}
+ expires = 24 * 60 * 60 * 7 // 7 days in seconds
}
if (!isNumber(expires)) {
throw new TypeError('expires should be of type "number"')
}
- var method = 'PUT'
+ if (!isObject(reqParams)) {
+ throw new TypeError('reqParams should be of type "object"')
+ }
+ if (!isFunction(cb)) {
+ throw new TypeError('callback should be of type "function"')
+ }
var requestDate = new Date()
+ var query = querystring.stringify(reqParams)
this.getBucketRegion(bucketName, (e, region) => {
if (e) return cb(e)
// This statement is added to ensure that we send error through
@@ -1504,7 +1514,8 @@ export class Client {
var reqOptions = this.getRequestOptions({method,
region,
bucketName,
- objectName})
+ objectName,
+ query})
try {
url = presignSignatureV4(reqOptions, this.accessKey, this.secretKey,
region, requestDate, expires)
@@ -1523,61 +1534,36 @@ export class Client {
// * `expiry` _number_: expiry in seconds (optional, default 7 days)
// * `respHeaders` _object_: response headers to override (optional)
presignedGetObject(bucketName, objectName, expires, respHeaders, cb) {
- if (this.anonymous) {
- throw new errors.AnonymousRequestError('Presigned GET url cannot be generated for anonymous requests')
- }
- if (isFunction(respHeaders)) {
- cb = respHeaders
- respHeaders = {}
- }
- if (isFunction(expires)) {
- cb = expires
- respHeaders = {}
- expires = 24 * 60 * 60 * 7
- }
if (!isValidBucketName(bucketName)) {
throw new errors.InvalidBucketNameError('Invalid bucket name: ' + bucketName)
}
if (!isValidObjectName(objectName)) {
throw new errors.InvalidObjectNameError(`Invalid object name: ${objectName}`)
}
- if (!isNumber(expires)) {
- throw new TypeError('expires should be of type "number"')
- }
- if (!isObject(respHeaders)) {
- throw new TypeError('respHeaders should be of type "object"')
- }
- if (!isFunction(cb)) {
- throw new TypeError('callback should be of type "function"')
- }
var validRespHeaders = ['response-content-type', 'response-content-language', 'response-expires', 'response-cache-control',
'response-content-disposition', 'response-content-encoding']
validRespHeaders.forEach(header => {
- if (respHeaders[header] !== undefined && !isString(respHeaders[header])) {
+ if (respHeaders !== undefined && respHeaders[header] !== undefined && !isString(respHeaders[header])) {
throw new TypeError(`response header ${header} should be of type "string"`)
}
})
- var method = 'GET'
- var requestDate = new Date()
- var query = _.map(respHeaders, (value, key) => `${key}=${uriEscape(value)}`).join('&')
- this.getBucketRegion(bucketName, (e, region) => {
- if (e) return cb(e)
- // This statement is added to ensure that we send error through
- // callback on presign failure.
- var url
- var reqOptions = this.getRequestOptions({method,
- region,
- bucketName,
- objectName,
- query})
- try {
- url = presignSignatureV4(reqOptions, this.accessKey, this.secretKey,
- region, requestDate, expires)
- } catch (pe) {
- return cb(pe)
- }
- cb(null, url)
- })
+ return this.presignedUrl('GET', bucketName, objectName, expires, respHeaders, cb)
+ }
+
+ // Generate a presigned URL for PUT. Using this URL, the browser can upload to S3 only with the specified object name.
+ //
+ // __Arguments__
+ // * `bucketName` _string_: name of the bucket
+ // * `objectName` _string_: name of the object
+ // * `expiry` _number_: expiry in seconds (optional, default 7 days)
+ presignedPutObject(bucketName, objectName, expires, cb) {
+ if (!isValidBucketName(bucketName)) {
+ throw new errors.InvalidBucketNameError('Invalid bucket name: ${bucketName}')
+ }
+ if (!isValidObjectName(objectName)) {
+ throw new errors.InvalidObjectNameError('Invalid object name: ${objectName}')
+ }
+ return this.presignedUrl('PUT', bucketName, objectName, expires, cb)
}
// return PostPolicy object
diff --git a/src/test/functional/functional-tests.js b/src/test/functional/functional-tests.js
index 7e9ae282..a679cd8d 100644
--- a/src/test/functional/functional-tests.js
+++ b/src/test/functional/functional-tests.js
@@ -742,7 +742,7 @@ describe('functional tests', function() {
})
step('presignedPutObject(bucketName, objectName)__', done => {
- // negative values should trigger an error
+ // Putting the same object should not cause any error
client.presignedPutObject(bucketName, _1byteObjectName)
.then(() => done())
.catch(done)
@@ -771,6 +771,29 @@ describe('functional tests', function() {
})
})
+ step('presignedUrl(getMethod, bucketName, objectName, expires, cb)__', done => {
+ client.presignedUrl('GET', bucketName, _1byteObjectName, 1000, (e, presignedUrl) => {
+ if (e) return done(e)
+ var transport = http
+ var options = _.pick(url.parse(presignedUrl), ['hostname', 'port', 'path', 'protocol'])
+ options.method = 'GET'
+ if (options.protocol === 'https:') transport = https
+ var request = transport.request(options, (response) => {
+ if (response.statusCode !== 200) return done(new Error(`error on put : ${response.statusCode}`))
+ var error = null
+ response.on('error', e => done(e))
+ response.on('end', () => done(error))
+ response.on('data', (data) => {
+ if (data.toString() !== _1byte.toString()) {
+ error = new Error('content mismatch')
+ }
+ })
+ })
+ request.on('error', e => done(e))
+ request.end()
+ })
+ })
+
step('presignedGetObject(bucketName, objectName, cb)__', done => {
client.presignedGetObject(bucketName, _1byteObjectName, (e, presignedUrl) => {
if (e) return done(e)
@@ -872,8 +895,52 @@ describe('functional tests', function() {
.catch(() => done())
})
- step('removeObject(bucketName, objectName, done)__', done => {
- client.removeObject(bucketName, _1byteObjectName, done)
+ step('presignedUrl(listObjectMethod, bucketName, \'\', expires, reqParams, cb)__', done => {
+ client.presignedUrl('GET', bucketName, '', 1000, {'prefix': 'data', 'max-keys': 1000}, (e, presignedUrl) => {
+ if (e) return done(e)
+ var transport = http
+ var options = _.pick(url.parse(presignedUrl), ['hostname', 'port', 'path', 'protocol'])
+ options.method = 'GET'
+ options.headers = {
+ }
+ var str = ''
+ if (options.protocol === 'https:') transport = https
+ var callback = function (response) {
+ if (response.statusCode !== 200) return done(new Error(`error on put : ${response.statusCode}`))
+ response.on('error', e => done(e))
+ response.on('end', function () {
+ if (!str.match(`${_1byteObjectName}`)) {
+ return done(new Error('Listed object does not match the object in the bucket!'))
+ }
+ done()
+ })
+ response.on('data', function (chunk) {
+ str += chunk
+ })
+ }
+ var request = transport.request(options, callback)
+ request.end()
+ })
+ })
+
+ step('presignedUrl(deleteMethod, bucketName, objectName, expires, cb)__', done => {
+ client.presignedUrl('DELETE', bucketName, _1byteObjectName, 1000, (e, presignedUrl) => {
+ if (e) return done(e)
+ var transport = http
+ var options = _.pick(url.parse(presignedUrl), ['hostname', 'port', 'path', 'protocol'])
+ options.method = 'DELETE'
+ options.headers = {
+ }
+ if (options.protocol === 'https:') transport = https
+ var request = transport.request(options, (response) => {
+ if (response.statusCode !== 204) return done(new Error(`error on put : ${response.statusCode}`))
+ response.on('error', e => done(e))
+ response.on('end', () => done())
+ response.on('data', () => {})
+ })
+ request.on('error', e => done(e))
+ request.end()
+ })
})
})