├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json └── test └── postman_collection.s3g.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Kevin McGinty 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | express-s3-router 2 | ========= 3 | 4 | An attempt at a RESTful JSON service for working with AWS S3. 5 | 6 | express-s3-router is designed to be a drop in router for use in an Express app. This means you need to setup an Express app to consume the router. Abstracting the API to a router allows the flexibilty to configure the rest of the Express app freely. 7 | 8 | The router provides basic CRUD operations with JSON responses for operations such such as listing all buckets, creating a bucket, deleting a bucket, getting bucket contents, creating a file, getting a file, and deleting a file. 9 | 10 | ## Installation 11 | 12 | Install express-s3-router to your project via npm like a typical dependency. Please note that Express is a peerDependecy so if your app will have to have that installed as well. 13 | 14 | npm install express-s3-router --save 15 | 16 | ## Usage 17 | 18 | // Setup an Express app 19 | var express = require('express'); 20 | var app = express(); 21 | 22 | // Load AWS config object from JSON file 23 | var awsConfig = require('./awsConfig.json'); 24 | 25 | // Create a new router using the config 26 | var s3ExpressRouter = require('express-s3-router')(awsConfig); 27 | 28 | // Add the router to our app using the root url of '/buckets' 29 | app.use('/buckets', s3ExpressRouter); 30 | 31 | // Basic Express example stuff below 32 | app.get('/', function (req, res) { 33 | res.send('Hello Index!'); 34 | }); 35 | 36 | var server = app.listen(3000, function () { 37 | var host = server.address().address; 38 | var port = server.address().port; 39 | console.log('Example app listening at http://%s:%s', host, port); 40 | }); 41 | 42 | 43 | ##### Notes: 44 | 45 | 1) The configuration using a JSON file is optional. There are various ways to load configurations for AWS such using environment variables. http://docs.aws.amazon.com/AWSJavaScriptSDK/guide/node-configuring.html 46 | 47 | 48 | ## Routes 49 | 50 | | URL | Method | Operation | 51 | |-------------|--------------|------------------------------------| 52 | | / | GET | List all Buckets | 53 | | /:name | GET | Get named bucket's contents | 54 | | /:name | PUT | Create named bucket | 55 | | /:name | DELETE | Delete named bucket | 56 | | /:name/:key | GET | Get file(key) from bucket(name) | 57 | | /:name/:key | PUT | Create file(key) in bucket(name) | 58 | | /:name/:key | DELETE | Delete file(key) from bucket(name) | 59 | 60 | 61 | ##### Notes: 62 | 63 | 1) Strict routing is enabled for the purpose of manipulating files keys that end in slash "/". For example a perfectly valid key is foo/ and an example request to get the object would be http://localhost:3000/buckets/foo/ Note the request also includes the trailing slash. 64 | 65 | 2) If no root url is provided the router will simply operate from /. Adding a root url means that s3Express URL endpoints will only operate on that url. For example if you used the example above you could get all the buckets by using the url http://localhost:3000/buckets. If instead you provided no root url then the same operation of getting all buckets would instead just be http://localhost:3000/. 66 | 67 | 68 | ## Contributing 69 | 70 | In lieu of a formal styleguide, take care to maintain the existing coding style. 71 | Add unit tests for any new or changed functionality. Lint and test your code. 72 | 73 | 74 | ## Tests 75 | There is a Postman collection located in /test that can be used to test the API. 76 | 77 | 78 | ## Release History 79 | 80 | * 0.1.0 Initial release -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*jslint es5: true */ 2 | /*global 3 | require, module, console 4 | */ 5 | (function () { 6 | 'use strict'; 7 | 8 | module.exports = function (config) { 9 | 10 | // Set strict routing that /buckets/test/abc is not the same as /buckets/test/abc/ 11 | // adding the slash at the end denotes creating a folder and not a file 12 | var router = require('express').Router({strict: true}), 13 | AWS = require('aws-sdk'), 14 | extend = require('util')._extend, 15 | mime = require('mime-types'), 16 | url = require('url'), 17 | DEFAULT_MAX_KEYS = 1000, 18 | 19 | // Return a location like object representing the request url 20 | getLocation = function (req) { 21 | var protocol = req.secure ? 'https://' : 'http://', 22 | hostname = req.headers.host, 23 | originalUrl = req.originalUrl !== '/' ? req.originalUrl : '', 24 | fullUrl = protocol + hostname + originalUrl, 25 | // Split out the query string 26 | urlParts = fullUrl.split('?'), 27 | origin = urlParts[0], 28 | search = urlParts[1] ? '?' + urlParts[1] : ''; 29 | return { 30 | // Protocol, hostname and port 31 | origin: origin, 32 | // Query string 33 | search: search, 34 | protocol: protocol, 35 | hostname: hostname, 36 | href: originalUrl 37 | }; 38 | }, 39 | // Merge the two objects and create search query string with them 40 | getMergedSearch = function (searchParams, otherParams) { 41 | var mergedParams = extend(searchParams, otherParams), 42 | paramKey, 43 | searchString = ''; 44 | 45 | for (paramKey in mergedParams) { 46 | if (mergedParams.hasOwnProperty(paramKey)) { 47 | if (!searchString) { 48 | searchString += '?' + paramKey + '=' + mergedParams[paramKey]; 49 | } else { 50 | searchString += '&' + paramKey + '=' + mergedParams[paramKey]; 51 | } 52 | } 53 | } 54 | 55 | return searchString; 56 | }, 57 | getNextMarkerLink = function (req, nextMarker) { 58 | var location = getLocation(req); 59 | 60 | // Merge marker property with existing queries 61 | location.search = getMergedSearch(req.query, { 62 | marker: nextMarker 63 | }); 64 | 65 | return location.origin + location.search; 66 | }, 67 | getS3ObjectLink = function (req, bucket, content) { 68 | var location = getLocation(req), 69 | href = location.origin; 70 | 71 | // If the objectKey is a folder we want to get a url to the bucket with a prefix param 72 | if (content.Key.substr(-1) === '/' && content.Size === 0) { 73 | // Merge marker property with existing queries 74 | location.search = getMergedSearch(req.query, { 75 | prefix: encodeURI(content.Key) 76 | }); 77 | href += location.search; 78 | } else { 79 | // Since we are getting a link for an item the href must end with slash 80 | if (href.substr(-1) !== '/') { 81 | href += '/'; 82 | } 83 | href += encodeURI(content.Key); 84 | } 85 | 86 | return href; 87 | }, 88 | getBucketLink = function (req, bucket) { 89 | var location = getLocation(req), 90 | href = location.origin; 91 | 92 | // Since we are getting a link for an item the href must end with slash 93 | if (href.substr(-1) !== '/') { 94 | href += '/'; 95 | } 96 | 97 | // Add the bucket and search 98 | href += bucket + location.search; 99 | 100 | return href; 101 | }, 102 | getOriginalink = function (req) { 103 | var location = getLocation(req), 104 | href = location.origin + location.search; 105 | return href; 106 | }, 107 | getFullKeyParam = function (params, route) { 108 | // Due to how our routing accepts arbitrary length folders 109 | // The key param could be split between params.key and params.'0' 110 | // Return the combined keys 111 | var restOfKey = params['0'] || ''; 112 | 113 | var key = params.key + restOfKey; 114 | 115 | // Check if this route ends with a / ensure the key also does (similar to strict routing) 116 | if (route.path.slice(-1) === '/' && key.slice(-1) !== '/') { 117 | key = key + '/'; 118 | } 119 | 120 | return key; 121 | }, 122 | // Get all buckets 123 | listBuckets = function (req, res) { 124 | var s3 = new AWS.S3(), 125 | handleListBuckets = function (err, data) { 126 | if (err) { 127 | res.status(500).send(err); 128 | } else { 129 | // Add link 130 | data.Buckets.forEach(function (bucket) { 131 | bucket.links = [{ 132 | rel: 'self', 133 | href: getBucketLink(req, bucket.Name) 134 | }]; 135 | }); 136 | 137 | res.json(data.Buckets); 138 | } 139 | }; 140 | 141 | s3.listBuckets(handleListBuckets); 142 | }, 143 | // Get a bucket 144 | getBucket = function (req, res) { 145 | var s3 = new AWS.S3(), 146 | bucket = req.params.name, 147 | location = getLocation(req), 148 | params = { 149 | Bucket: bucket, 150 | Delimiter: req.query.delimiter, 151 | Marker: req.query.marker, 152 | MaxKeys: req.query.maxKeys || DEFAULT_MAX_KEYS, 153 | Prefix: req.query.prefix 154 | }, 155 | handleListObjects = function (err, data) { 156 | if (err) { 157 | res.status(500).send(err); 158 | } else { 159 | 160 | // Get link for this set 161 | data.links = [{ 162 | rel: 'self', 163 | href: getOriginalink(req) 164 | }]; 165 | 166 | // Map through bucket contents to filter out self reference and add links 167 | data.Contents = data.Contents.map(function (content) { 168 | // Add s3g link to reference the resource 169 | content.links = [{ 170 | ref: 'self', 171 | href: getS3ObjectLink(req, bucket, content) 172 | }]; 173 | 174 | // Filter out any references to the parent file 175 | if (location.origin + location.search !== content.links[0].href) { 176 | return content; 177 | } 178 | 179 | }).filter(Boolean); 180 | 181 | 182 | // If this set of objects has a next marker that means there are > 1000 so provide the link to the next set of data 183 | if (data.IsTruncated) { 184 | // Get link for next set using next marker if available or the last key in the content set 185 | data.links.push({ 186 | rel: 'next', 187 | href: getNextMarkerLink(req, data.NextMarker || data.Contents[data.Contents.length - 1].Key) 188 | }); 189 | } 190 | res.json(data); 191 | } 192 | }; 193 | 194 | s3.listObjects(params, handleListObjects); 195 | }, 196 | // Create a new bucket 197 | createBucket = function (req, res) { 198 | var s3 = new AWS.S3(), 199 | bucket = req.params.name, 200 | params = { 201 | Bucket: bucket 202 | }, 203 | handleCreateBucket = function (err, data) { 204 | if (err) { 205 | res.status(500).send(err); 206 | } else { 207 | // Add s3g link to reference the resource 208 | data.links = [{ 209 | rel: 'self', 210 | href: getOriginalink(req, bucket) 211 | }]; 212 | 213 | res.json(data); 214 | } 215 | }; 216 | 217 | s3.createBucket(params, handleCreateBucket); 218 | }, 219 | // Delete a bucket 220 | deleteBucket = function (req, res) { 221 | var s3 = new AWS.S3(), 222 | params = { 223 | Bucket: req.params.name 224 | }, 225 | handleDeleteBucket = function (err, data) { 226 | if (err) { 227 | res.status(500).send(err); 228 | } else { 229 | res.json(data); 230 | } 231 | }; 232 | 233 | s3.deleteBucket(params, handleDeleteBucket); 234 | }, 235 | // Get an object key from the bucket 236 | getObjectInBucket = function (req, res) { 237 | var s3 = new AWS.S3(), 238 | bucket = req.params.name, 239 | key = getFullKeyParam(req.params, req.route), 240 | params = { 241 | Bucket: bucket, 242 | Key: key 243 | }, 244 | handleError = function (err) { 245 | res.status(500).send(err); 246 | }; 247 | 248 | s3.getObject(params).createReadStream().on('error', handleError).pipe(res); 249 | 250 | }, 251 | // Upload a file to the bucket 252 | createObjectInBucket = function (req, res) { 253 | var s3 = new AWS.S3(), 254 | bucket = req.params.name, 255 | key = getFullKeyParam(req.params, req.route), 256 | contentType = mime.lookup(key), 257 | params = { 258 | Bucket: bucket, 259 | Key: key, 260 | // Pipe the file through the request body 261 | Body: req 262 | }, 263 | trackUploadProgress = function (evt) { 264 | // This function is called each time a chunk of the file is uploaded 265 | //console.log(evt); 266 | }, 267 | handleUploadComplete = function (err, data) { 268 | if (err) { 269 | res.status(500).send(err); 270 | } else { 271 | // Add s3g link to reference the resource 272 | data.links = [{ 273 | rel: 'self', 274 | href: getOriginalink(req) 275 | }]; 276 | 277 | res.json(data); 278 | } 279 | }; 280 | 281 | // Set content type for object if we have one 282 | if (contentType) { 283 | params.ContentType = contentType; 284 | } 285 | 286 | s3.upload(params).on('httpUploadProgress', trackUploadProgress).send(handleUploadComplete); 287 | }, 288 | // Delete objects from a bucket 289 | deleteObjectsInBucket = function (req, res) { 290 | var s3 = new AWS.S3(), 291 | bucket = req.params.name, 292 | key = getFullKeyParam(req.params, req.route), 293 | params = { 294 | Bucket: bucket, 295 | Delete: { 296 | Objects: [{ 297 | Key: key 298 | }] 299 | } 300 | }, 301 | 302 | handleDeleteObjects = function (err, data) { 303 | if (err) { 304 | res.status(500).send(err); 305 | } else { 306 | res.json(data); 307 | } 308 | }; 309 | s3.deleteObjects(params, handleDeleteObjects); 310 | }; 311 | 312 | // If there are config options specified update the config with them 313 | if (config) { 314 | AWS.config.update(config); 315 | } 316 | 317 | // Routes 318 | // Get all buckets on root url 319 | router.get('/', listBuckets); 320 | 321 | // Bucket CRUD 322 | // Get a bucket by name 323 | router.get('/:name', getBucket); 324 | router.get('/:name/', getBucket); 325 | // Create a bucket by name 326 | router.put('/:name', createBucket); 327 | router.put('/:name/', createBucket); 328 | // Delete a bucket by name 329 | router.delete('/:name', deleteBucket); 330 | router.delete('/:name/', deleteBucket); 331 | 332 | // Object CRUD 333 | // Get an object(key) from bucket(name) 334 | router.get('/:name/:key*', getObjectInBucket); 335 | router.get('/:name/:key/', getObjectInBucket); 336 | // Create an object(key) from bucket(name) 337 | router.put('/:name/:key*', createObjectInBucket); 338 | router.put('/:name/:key/', createObjectInBucket); 339 | // Delete an object(key) from bucket(name) 340 | router.delete('/:name/:key*', deleteObjectsInBucket); 341 | router.delete('/:name/:key/', deleteObjectsInBucket); 342 | 343 | return router; 344 | }; 345 | }()); -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-s3-router", 3 | "version": "0.1.2", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "mime-db": { 8 | "version": "1.36.0", 9 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", 10 | "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" 11 | }, 12 | "mime-types": { 13 | "version": "2.1.20", 14 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", 15 | "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", 16 | "requires": { 17 | "mime-db": "1.36.0" 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-s3-router", 3 | "version": "0.1.3", 4 | "description": "Amazon S3 Gateway via an Express Router", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": { 10 | "name": "Kevin McGinty", 11 | "email": "atomic.frameworks@gmail.com", 12 | "url": "http://www.atomicframeworks.com/" 13 | }, 14 | "licenses": [ 15 | { 16 | "type": "MIT", 17 | "url": "https://github.com/atomicframeworks/express-s3-router/blob/master/LICENSE" 18 | } 19 | ], 20 | "bugs": { 21 | "url": "https://github.com/atomicframeworks/express-s3-router/issues" 22 | }, 23 | "homepage": "https://github.com/atomicframeworks/express-s3-router", 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/atomicframeworks/express-s3-router.git" 27 | }, 28 | "dependencies": { 29 | "aws-sdk": "^2.1.46", 30 | "mime-types": "^2.1.20" 31 | }, 32 | "peerDependencies": { 33 | "express": "^4.13.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/postman_collection.s3g.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "7a9446d6-15ad-b1d3-20a0-f5aca0e1ea44", 3 | "name": "s3g", 4 | "description": "AWS S3 Gateway", 5 | "order": [ 6 | "155170ce-d261-0ab6-9815-19f82267f967", 7 | "ceb15bd9-061b-1adf-8780-4064283f96c9", 8 | "448b983c-e4a3-f9eb-e3e0-6f01371f47dc", 9 | "5372367e-f519-35bb-7bc8-eeb8051d6223", 10 | "776a8fcf-85cc-6d38-62df-93de58978390", 11 | "dd318da5-37c0-9b78-ace6-5999854ffa98", 12 | "c87a88ec-948f-151d-2955-065501e57024", 13 | "b5d84199-37c5-e6b2-dbd3-662124a8cc49", 14 | "1af59390-cf4a-7664-6fff-fa247247c953" 15 | ], 16 | "folders": [], 17 | "timestamp": 1440223096446, 18 | "owner": 0, 19 | "remoteLink": "", 20 | "public": false, 21 | "requests": [ 22 | { 23 | "id": "155170ce-d261-0ab6-9815-19f82267f967", 24 | "headers": "", 25 | "url": "http://localhost:3000/buckets", 26 | "preRequestScript": "", 27 | "pathVariables": {}, 28 | "method": "GET", 29 | "data": [], 30 | "dataMode": "params", 31 | "version": 2, 32 | "tests": "", 33 | "currentHelper": "normal", 34 | "helperAttributes": {}, 35 | "time": 1440233093702, 36 | "name": "GET Buckets", 37 | "description": "", 38 | "collectionId": "7a9446d6-15ad-b1d3-20a0-f5aca0e1ea44", 39 | "responses": [] 40 | }, 41 | { 42 | "id": "1af59390-cf4a-7664-6fff-fa247247c953", 43 | "headers": "", 44 | "url": "http://localhost:3000/buckets/postman-this-must-be-random/nested-bucket/even-further/file.png", 45 | "preRequestScript": "", 46 | "pathVariables": {}, 47 | "method": "DELETE", 48 | "dataMode": "binary", 49 | "version": 2, 50 | "tests": "", 51 | "currentHelper": "normal", 52 | "helperAttributes": {}, 53 | "time": 1440233055016, 54 | "name": "DELETE Nested Folder File", 55 | "description": "", 56 | "collectionId": "7a9446d6-15ad-b1d3-20a0-f5aca0e1ea44", 57 | "responses": [] 58 | }, 59 | { 60 | "id": "448b983c-e4a3-f9eb-e3e0-6f01371f47dc", 61 | "headers": "", 62 | "url": "http://localhost:3000/buckets/postman-this-must-be-random", 63 | "preRequestScript": "", 64 | "pathVariables": {}, 65 | "method": "GET", 66 | "data": [], 67 | "dataMode": "params", 68 | "version": 2, 69 | "tests": "", 70 | "currentHelper": "normal", 71 | "helperAttributes": {}, 72 | "time": 1440233139347, 73 | "name": "GET Postman Bucket", 74 | "description": "", 75 | "collectionId": "7a9446d6-15ad-b1d3-20a0-f5aca0e1ea44", 76 | "responses": [] 77 | }, 78 | { 79 | "id": "5372367e-f519-35bb-7bc8-eeb8051d6223", 80 | "headers": "", 81 | "url": "http://localhost:3000/buckets/postman-this-must-be-random", 82 | "preRequestScript": "", 83 | "pathVariables": {}, 84 | "method": "DELETE", 85 | "data": [], 86 | "dataMode": "params", 87 | "version": 2, 88 | "tests": "", 89 | "currentHelper": "normal", 90 | "helperAttributes": {}, 91 | "time": 1440223525751, 92 | "name": "DELETE Postman Bucket", 93 | "description": "", 94 | "collectionId": "7a9446d6-15ad-b1d3-20a0-f5aca0e1ea44", 95 | "responses": [] 96 | }, 97 | { 98 | "id": "776a8fcf-85cc-6d38-62df-93de58978390", 99 | "headers": "", 100 | "url": "http://localhost:3000/buckets/postman-this-must-be-random/nested-bucket/even-further/", 101 | "preRequestScript": "", 102 | "pathVariables": {}, 103 | "method": "PUT", 104 | "data": [], 105 | "dataMode": "params", 106 | "version": 2, 107 | "tests": "", 108 | "currentHelper": "normal", 109 | "helperAttributes": {}, 110 | "time": 1440231185219, 111 | "name": "PUT Nested Folder", 112 | "description": "", 113 | "collectionId": "7a9446d6-15ad-b1d3-20a0-f5aca0e1ea44", 114 | "responses": [] 115 | }, 116 | { 117 | "id": "b5d84199-37c5-e6b2-dbd3-662124a8cc49", 118 | "headers": "", 119 | "url": "http://localhost:3000/buckets/postman-this-must-be-random/nested-bucket/even-further/file.png", 120 | "preRequestScript": "", 121 | "pathVariables": {}, 122 | "method": "PUT", 123 | "dataMode": "binary", 124 | "version": 2, 125 | "tests": "", 126 | "currentHelper": "normal", 127 | "helperAttributes": {}, 128 | "time": 1440232397767, 129 | "name": "PUT Nested Folder File", 130 | "description": "", 131 | "collectionId": "7a9446d6-15ad-b1d3-20a0-f5aca0e1ea44", 132 | "responses": [] 133 | }, 134 | { 135 | "id": "c87a88ec-948f-151d-2955-065501e57024", 136 | "headers": "", 137 | "url": "http://localhost:3000/buckets/postman-this-must-be-random/nested-bucket/even-further/", 138 | "preRequestScript": "", 139 | "pathVariables": {}, 140 | "method": "DELETE", 141 | "data": [], 142 | "dataMode": "params", 143 | "version": 2, 144 | "tests": "", 145 | "currentHelper": "normal", 146 | "helperAttributes": {}, 147 | "time": 1440233133751, 148 | "name": "DELETE Nested Folder", 149 | "description": "", 150 | "collectionId": "7a9446d6-15ad-b1d3-20a0-f5aca0e1ea44", 151 | "responses": [] 152 | }, 153 | { 154 | "id": "ceb15bd9-061b-1adf-8780-4064283f96c9", 155 | "headers": "", 156 | "url": "http://localhost:3000/buckets/postman-this-must-be-random", 157 | "preRequestScript": "", 158 | "pathVariables": {}, 159 | "method": "PUT", 160 | "data": [], 161 | "dataMode": "params", 162 | "version": 2, 163 | "tests": "", 164 | "currentHelper": "normal", 165 | "helperAttributes": {}, 166 | "time": 1440231206615, 167 | "name": "PUT Postman Bucket", 168 | "description": "", 169 | "collectionId": "7a9446d6-15ad-b1d3-20a0-f5aca0e1ea44", 170 | "responses": [] 171 | }, 172 | { 173 | "id": "dd318da5-37c0-9b78-ace6-5999854ffa98", 174 | "headers": "", 175 | "url": "http://localhost:3000/buckets/postman-this-must-be-random/nested-bucket/even-further/", 176 | "preRequestScript": "", 177 | "pathVariables": {}, 178 | "method": "GET", 179 | "data": [], 180 | "dataMode": "params", 181 | "version": 2, 182 | "tests": "", 183 | "currentHelper": "normal", 184 | "helperAttributes": {}, 185 | "time": 1440231301762, 186 | "name": "GET Nested Folder", 187 | "description": "", 188 | "collectionId": "7a9446d6-15ad-b1d3-20a0-f5aca0e1ea44", 189 | "responses": [] 190 | } 191 | ] 192 | } --------------------------------------------------------------------------------