├── .bashrc ├── .circleci └── config.yml ├── .dockerignore ├── .gitignore ├── .travis.yml ├── Dockerfile ├── Jenkinsfile ├── LICENSE.md ├── README.md ├── bats └── general-options.bats ├── bin └── akamaiNetstorage ├── cli.json ├── index.js ├── package.json ├── release.js ├── src ├── auth.js ├── helpers.js ├── logger.js ├── netstorage.js ├── netstorage_auth.js └── netstorage_rc.js └── test ├── README ├── mocha.opts └── nstest.js /.bashrc: -------------------------------------------------------------------------------- 1 | # ~/.bashrc: executed by bash(1) for non-login shells. 2 | 3 | export PATH=${PATH}:/opt/bin 4 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:7.10 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: yarn install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run tests! 37 | - run: yarn test 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | hostlist.json 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 7.0 4 | script: npm test 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:7 2 | MAINTAINER Kirsten Hunter (khunter@akamai.com) 3 | RUN apt-get update 4 | RUN apt-get install -y curl patch gawk g++ gcc make libc6-dev patch libreadline6-dev zlib1g-dev libssl-dev libyaml-dev autoconf libgdbm-dev libncurses5-dev automake libtool bison pkg-config libffi-dev 5 | RUN apt-get install -y -q libssl-dev python-all wget vim 6 | ADD . /opt 7 | WORKDIR /opt 8 | RUN /usr/local/bin/npm install 9 | RUN export PATH=${PATH}:/opt/bin 10 | RUN git clone https://github.com/akamai-open/api-kickstart 11 | RUN git clone https://github.com/stedolan/jq.git 12 | RUN cp /opt/.bashrc /root/.bashrc 13 | ENTRYPOINT ["/bin/bash"] 14 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent any 3 | stages { 4 | stage('Initialize') { 5 | steps { 6 | sh '''git pull origin master 7 | npm install''' 8 | echo 'Initialization complete' 9 | } 10 | } 11 | stage('Create') { 12 | steps { 13 | sh 'bin/akamaiProperty create jenkins.$BUILD_NUMBER.com --clone jenkins.base.property' 14 | echo 'Property created' 15 | } 16 | } 17 | stage('Modify') { 18 | steps { 19 | sh 'bin/akamaiProperty modify jenkins.$BUILD_NUMBER.com --origin jenkins.origin.com' 20 | sh 'bin/akamaiProperty modify jenkins.$BUILD_NUMBER.com --origin origin.jenkins.com' 21 | } 22 | } 23 | stage('Retrieve') { 24 | steps { 25 | sh 'bin/akamaiProperty retrieve jenkins.$BUILD_NUMBER.com --file rules.json' 26 | } 27 | } 28 | stage('Update') { 29 | steps { 30 | sh 'bin/akamaiProperty update jenkins.$BUILD_NUMBER.com --file rules.json' 31 | } 32 | } 33 | stage('Activate') { 34 | steps { 35 | sh 'bin/akamaiProperty activate jenkins.$BUILD_NUMBER.com --network BOTH' 36 | } 37 | } 38 | stage('Deactivate') { 39 | steps { 40 | sh 'bin/akamaiProperty deactivate jenkins.$BUILD_NUMBER.com --network BOTH' 41 | } 42 | } 43 | stage('Delete') { 44 | steps { 45 | sh 'bin/akamaiProperty delete jenkins.$BUILD_NUMBER.com' 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Akamai CLI for Netstorage 2 | 3 | *NOTE REGARDING MAINTAINING THIS PACKAGE:* The CLI-NetStorage package is currently no longer officially maintained by Akamai's engineering teams. The package still works but there will be no ongoing maintenance or features added. This package will receive occasional vulnerability scanning to upgrade third-party libraries to newer versions with minimal testing. Use at your own risk. We recommend the use of the NetStorage Usage API instead: https://techdocs.akamai.com/netstorage-usage/reference/api - Last Update: October 2023. 4 | 5 | *NOTE:* This tool is intended to be installed via the Akamai CLI package manager, which can be retrieved from the releases page of the [Akamai CLI](https://github.com/akamai/cli) tool. 6 | 7 | *NOTE:* This tool is only supported for use with ObjectStore, not with the older FileStore system. Any use of the tool to work with FileStore directories is unsupported. 8 | 9 | ### Credentials 10 | * Various values associated with the Storage Group and Upload Account are required for use in calls made to the API. Once the Upload Account has fully propagated (after enabling the HTTP API), you can view it in the NetStorage Groups UI to gather this information. Select the Upload Account entity. 11 | * Locate the Upload Account in which you've enabled the NetStorage HTTP API, and click it to open Detail View. (You can type the name of the target account in the Filter field to limit results in this table.) 12 | * Click the Edit button. 13 | 14 | Make note of the following values: 15 | * The "*Key*" Value - 16 | * Under "Upload Accounts" select the account 17 | * Click the "edit" icon in the upper right 18 | * Under "Access methods" click the Netstorage HTTP CMS API tab 19 | * The key is in the resulting table 20 | * The "*Id*" in the Upload Account Details content panel. 21 | * The Storage Group Name (*group*) - This is revealed in the second column of the table in the Upload Directory Association content panel. 22 | 23 | Select the Storage Groups entity. 24 | * Input the Storage Group Name you noted in the Filter field. 25 | * Click its entry in the table to open Detail View. 26 | * In the Storage Group Details, make note of the NetStorage HTTP API entry for the *host* entry 27 | 28 | Once you have gathered all the values, run 'akamai netstorage setup' to save them to your system. 29 | 30 | ## Overview 31 | The Akamai Netstorage CLI is a utility for interacting with Akamai's NetStorage platform from the command line. 32 | 33 | ``` 34 | Usage: akamai netstorage [options] 35 | 36 | Commands: 37 | setup Setup authentication for Netstorage 38 | du disk usage stats 39 | mkdir create a new directory 40 | rmdir delete a directory 41 | dir view a directory structure 42 | quick-delete recursively delete a directory 43 | list view a directory listing 44 | upload upload a file 45 | download download a file 46 | delete delete a file 47 | rename rename/move a file 48 | symlink create a symlink 49 | mtime update modification time for a file 50 | stat see file information 51 | 52 | Command options: 53 | --config Config file 54 | [file] [default: ~/.akamai-cli/.netstorage/auth] 55 | 56 | --section
Section for config file 57 | [string] [default: default] 58 | 59 | --cpcode Default CPCode 60 | [string] 61 | 62 | --help Show help 63 | [commands: help] [boolean] 64 | 65 | --version Show version number 66 | [commands: version] [boolean] 67 | ``` 68 | Copyright (C) Akamai Technologies, Inc 69 | Visit http://github.com/akamai/cli-netstorage for detailed documentation 70 | -------------------------------------------------------------------------------- /bats/general-options.bats: -------------------------------------------------------------------------------- 1 | @test "Bare bin/akamaiNetStorage shows usage" { 2 | run bin/akamaiNetStorage 3 | [ $status -eq 0 ] 4 | [ $(expr "${lines[0]}" : "Usage:") -ne 0 ] 5 | [ "${#lines[@]}" -gt 10 ] 6 | [ $(expr "$output" : ".*config.*") -ne 0 ] 7 | [ $(expr "$output" : ".*section.*") -ne 0 ] 8 | } 9 | @test "Akamai subcommands work with pre help" { 10 | run bin/akamaiNetStorage help modify 11 | [ $status -eq 0 ] 12 | [ $(expr "${lines[0]}" : "Usage:") -ne 0 ] 13 | [ "${#lines[@]}" -gt 10 ] 14 | [ $(expr "$output" : ".*config.*") -ne 0 ] 15 | [ $(expr "$output" : ".*section.*") -ne 0 ] 16 | } 17 | 18 | @test "Akamai subcommands work with help flag (--help)" { 19 | run bin/akamaiNetStorage modify --help 20 | [ $status -eq 0 ] 21 | [ $(expr "${lines[0]}" : "Usage:") -ne 0 ] 22 | [ "${#lines[@]}" -gt 10 ] 23 | [ $(expr "$output" : ".*config.*") -ne 0 ] 24 | [ $(expr "$output" : ".*section.*") -ne 0 ] 25 | } 26 | 27 | @test "Create a new test property $PROPERTYNAME" { 28 | run bin/akamaiNetStorage dir 29 | echo "status = ${status}" 30 | echo "output = ${output}" 31 | [ $status -eq 0 ] 32 | } 33 | 34 | -------------------------------------------------------------------------------- /bin/akamaiNetstorage: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Copyright 2017 Akamai Technologies, Inc. All Rights Reserved 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | "use strict"; 18 | 19 | /** 20 | * Command line tool to interface with the WebSite library for manipulating the CDN activities on Akamai 21 | * 22 | * @author Kirsten Hunter 23 | */ 24 | 25 | var path = require('path'); 26 | let untildify = require('untildify'); 27 | let NetStorageAuth = require('../index').NetStorageAuth; 28 | let NetStorage = require('../index').NetStorage; 29 | 30 | if ((process.versions["node"]).split('.')[0] < 7) { 31 | console.log("The Akamai CLI for NetStorage requires Node 7 or later.") 32 | process.exit() 33 | } 34 | 35 | function appName() { 36 | return process.argv[1].replace(/.*\//, ""); 37 | } 38 | let cpcode = process.env.AKAMAI_NS_DEFAULT_CPCODE ? process.env.AKAMAI_NS_DEFAULT_CPCODE : "" 39 | // I could probably put a check of the config file here but it's a lot of work for just 40 | 41 | let app = "akamai netstorage"; 42 | 43 | let config = untildify("~/.akamai-cli/.netstorage/auth"); 44 | let debug = false; 45 | let targetProperty; 46 | let searchString; 47 | 48 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; 49 | 50 | function setup(app, options) { 51 | return app.setup(options) 52 | // We need to set up the authentication for NetStorage 53 | // Assume they have all the pieces needed 54 | // Store them in creds file in the .akamai_cli directory 55 | } 56 | 57 | function diskusage(app, options) { 58 | return app.diskusage(options) 59 | // Return disk usage for the NetStorage cpcode 60 | } 61 | 62 | function stat(app, options) { 63 | return app.stat(options) 64 | // Return status for the specified file 65 | } 66 | 67 | function mtime(app, options) { 68 | return app.mtime(options) 69 | // Update timestamp for the selected file 70 | } 71 | 72 | function dir(app, options) { 73 | return app.dir(options) 74 | // Show directory structure 75 | } 76 | 77 | function list(app, options) { 78 | return app.list(options) 79 | // Show directory structure 80 | } 81 | 82 | function del(app, options) { 83 | return app.delete(options) 84 | // List items in directory 85 | } 86 | 87 | function mkdir(app, options) { 88 | return app.mkdir(options) 89 | // Create a new directory 90 | } 91 | 92 | function rmdir(app, options) { 93 | return app.rmdir(options) 94 | // Create a new directory 95 | } 96 | 97 | function quickdelete(app, options) { 98 | return app.quickDelete(options) 99 | // Create a new directory 100 | } 101 | 102 | 103 | function upload(app, options) { 104 | return app.upload(options) 105 | // Upload a file 106 | } 107 | 108 | function download(app, options) { 109 | return app.download(options) 110 | // Download a file 111 | } 112 | 113 | function symlink(app, options) { 114 | return app.symlink(options) 115 | // Rename a file 116 | } 117 | 118 | function rename(app, options) { 119 | return app.rename(options) 120 | // Rename a file 121 | } 122 | 123 | function main () { 124 | process.argv[1] = 'akamai netstorage' 125 | const chalk = require('chalk') 126 | return require('sywac') 127 | .file('--config ', { 128 | desc: 'Config file', 129 | defaultValue: untildify("~/.akamai-cli/.netstorage/auth"), 130 | hints: ``, 131 | group: "Command options:" 132 | }) 133 | .string('--section
', { 134 | desc: 'Section for config file', 135 | defaultValue: "default", 136 | group: "Command options:" 137 | }) 138 | 139 | .string('--cpcode ', { 140 | desc: 'CPCode', 141 | defaultValue: cpcode, 142 | group: "Command options:" 143 | }) 144 | 145 | .command('setup', { 146 | desc: 'Setup authentication for Netstorage', 147 | group: 'System commands:', 148 | run: options => { 149 | options["setup"] = true 150 | let app = new NetStorage(options); 151 | return setup(app, options); 152 | } 153 | }) 154 | .command('du', { 155 | desc: 'disk usage stats', 156 | group: 'System commands:', 157 | run: options => { 158 | let app = new NetStorage(options); 159 | return diskusage(app, options) 160 | } 161 | }) 162 | 163 | .command('mkdir ', { 164 | desc: 'create a new directory', 165 | group: 'Directory commands:', 166 | setup: sywac => { 167 | sywac 168 | .string('--path ', { 169 | desc: 'Path for new directory' 170 | }) 171 | }, 172 | run: options => { 173 | let app = new NetStorage(options); 174 | return mkdir(app, options) 175 | } 176 | }) 177 | .command('rmdir ', { 178 | group: 'Directory commands:', 179 | desc: 'delete an empty directory', 180 | run: options => { 181 | let app = new NetStorage(options); 182 | return rmdir(app, options) 183 | } 184 | }) 185 | .command('dir [directory]', { 186 | desc: 'view a directory structure', 187 | group: 'Directory commands:', 188 | defaultValue: "", 189 | setup: sywac => { 190 | sywac 191 | .string('--prefix ', { 192 | desc: 'List files beginning with this prefix only' 193 | }) 194 | .string('--start ', { 195 | desc: 'Start listing at this value' 196 | }) 197 | .string('--end ', { 198 | desc: 'Stop listing files at this value' 199 | }) 200 | .string('--max_entries ', { 201 | desc: 'List only this number of items' 202 | }) 203 | .string('--encoding ', { 204 | desc: 'Use this XML encoding' 205 | }) 206 | }, 207 | 208 | run: options => { 209 | let app = new NetStorage(options); 210 | return dir(app, options) 211 | } 212 | }) 213 | .command('quick-delete ', { 214 | desc: 'recursively delete a directory', 215 | group: 'Directory commands:', 216 | hints: 'Disabled by default', 217 | run: options => { 218 | let app = new NetStorage(options); 219 | return quickdelete(app, options) 220 | } 221 | }) 222 | 223 | .command('list ', { 224 | desc: 'view a directory listing', 225 | group: 'Directory commands:', 226 | default: "", 227 | setup: sywac => { 228 | sywac 229 | .string('--end ', { 230 | desc: 'Stop listing files at this value' 231 | }) 232 | .string('--max_entries ', { 233 | desc: 'List only this number of items' 234 | }) 235 | .string('--encoding ', { 236 | desc: 'Use this XML encoding' 237 | }) 238 | }, 239 | 240 | run: options => { 241 | let app = new NetStorage(options); 242 | return list(app, options) 243 | } 244 | }) 245 | 246 | .command('upload ', { 247 | group: 'File commands:', 248 | desc: 'upload a file', 249 | setup: sywac => { 250 | sywac 251 | .string('--directory ', { 252 | desc: 'Directory for the file upload', 253 | default: "" 254 | }) 255 | }, 256 | run: options => { 257 | let app = new NetStorage(options); 258 | return upload(app, options) 259 | } 260 | }) 261 | 262 | 263 | .command('download ', { 264 | group: 'File commands:', 265 | desc: 'download a file', 266 | setup: sywac => { 267 | sywac 268 | .string('--directory ', { 269 | desc: 'Directory for the file download', 270 | default: "" 271 | }) 272 | .string('--outfile ', { 273 | desc: 'Local file for the download' 274 | }) 275 | }, 276 | 277 | run: options => { 278 | let app = new NetStorage(options); 279 | return download(app, options) 280 | } 281 | }) 282 | 283 | .command('delete ', { 284 | group: 'File commands:', 285 | desc: 'remove a file', 286 | alias: 'rm', 287 | run: options => { 288 | let app = new NetStorage(options); 289 | return del(app, options) 290 | } 291 | }) 292 | 293 | 294 | .command('rename ', { 295 | group: 'File commands:', 296 | desc: 'rename/move a file', 297 | run: options => { 298 | let app = new NetStorage(options); 299 | return rename(app, options) 300 | } 301 | }) 302 | 303 | 304 | .command('symlink ', { 305 | group: 'File commands:', 306 | desc: 'create a symlink', 307 | run: options => { 308 | let app = new NetStorage(options); 309 | return symlink(app, options) 310 | } 311 | }) 312 | 313 | 314 | .command('mtime ', { 315 | desc: 'update modification time for a file', 316 | group: 'File commands:', 317 | setup: sywac => { 318 | sywac 319 | .string('--timestamp ', { 320 | desc: 'Timestamp to use for the command' 321 | }) 322 | }, 323 | run: options => { 324 | let app = new NetStorage(options); 325 | return mtime(app, options) 326 | } 327 | }) 328 | 329 | 330 | .command('stat ', { 331 | group: 'File commands:', 332 | desc: 'see file information', 333 | run: options => { 334 | let app = new NetStorage(options); 335 | return stat(app, options) 336 | } 337 | }) 338 | 339 | .showHelpByDefault() 340 | .help('--help', { group: "Command options:"}) 341 | .version('--version', { group: "Command options:"}) 342 | .epilogue('Copyright (C) Akamai Technologies, Inc\nVisit http://github.com/akamai/cli-netstorage for detailed documentation\n') 343 | .style({ 344 | // you can style several other things too 345 | // and even style things differently when validation fails for them 346 | // see http://sywac.io/docs/sync-config.html#style 347 | group: str => chalk.blue.bold(str), 348 | }) 349 | .style({ 350 | group: str => chalk.bold.blue(str === 'Options:' ? 'General options:' : str) 351 | }) 352 | .outputSettings({ maxWidth: 90 }) 353 | .parse() 354 | .then(result => { 355 | // let errors bubble up 356 | if (result.errors.length) { 357 | console.error(chalk.red.bold("\nERROR: \n\t"), result.output || JSON.stringify(result.output[0])); 358 | console.output = ""; 359 | } else if (result.output) { 360 | console.log(result.output) 361 | process.exit(result.code) 362 | } 363 | }) 364 | } 365 | console.time(app); 366 | var begin=Date.now(); 367 | 368 | main().then(options => { 369 | var end=Date.now(); 370 | var timeSpent=((end-begin)/1000/60).toPrecision(2) +" mins"; 371 | }) 372 | -------------------------------------------------------------------------------- /cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "requirements": { 3 | "node": "7.0.0" 4 | }, 5 | "commands": [ 6 | { 7 | "name": "netstorage", 8 | "version": "1.0.1", 9 | "description": "Interface for Akamai NetStorage", 10 | "bin": "https://github.com/akamai/cli-netstorage/releases/download/{{.Version}}/akamai-{{.Name}}-{{.Version}}-{{.OS}}-{{.Arch}}{{.BinSuffix}}" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | NetStorage: require('./src/netstorage.js'), 4 | NetStorageAuth: require('./src/netstorage_auth.js') 5 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "akamaiclinetstorage", 3 | "version": "0.1.1", 4 | "description": "A CLI to interface with Akamai's NetStorage platform.", 5 | "repository": "https://github.com/akamai/cli-netstorage", 6 | "license": "Apache-2.0", 7 | "engines": { 8 | "node": ">=7.0.0" 9 | }, 10 | "scripts": { 11 | "start": "node bin/akamaiNetStorage", 12 | "test": "mocha test --timeout 0", 13 | "build": "pkg bin/akamaiProperty --target node8-linux-x86,node8-linux-x64,node8-win-x86,node8-win-x64,node8-macos-x64 --output akamai-netstorage-1.0.0" 14 | }, 15 | "main": "./index.js", 16 | "files": [ 17 | "index.js", 18 | "src" 19 | ], 20 | "dependencies": { 21 | "chalk": "^2.3.0", 22 | "child-process-promise": "^2.2.1", 23 | "ini": "^1.3.5", 24 | "inquirer": "^4.0.2", 25 | "log4js": "^2.4.1", 26 | "md5": "^2.2.1", 27 | "merge": "^1.2.0", 28 | "mocha": "^3.2.0", 29 | "moment": "^2.20.1", 30 | "mz": "^2.7.0", 31 | "options": "0.0.6", 32 | "pkg": "^4.3.0-beta.5", 33 | "request": "^2.74.0", 34 | "safe-buffer": "^5.1.1", 35 | "sywac": "^1.2.0", 36 | "untildify": "^4.0.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /release.js: -------------------------------------------------------------------------------- 1 | // build.js 2 | // Very simple build script to run angular build 3 | // Needed because all the cross platform things I tried didn't work 4 | 5 | // No command line arguments, we're just going to use the env vars 6 | let fs = require('fs') 7 | 8 | let source = "bin/akamaiNetStorage" 9 | let target = "akamai-netstorage-1.0.1" 10 | 11 | var exec = require('child-process-promise').exec; 12 | 13 | exec(`pkg ${source} --target node8-linux-x86,node8-linux-x64,node8-win-x86,node8-win-x64,node8-macos-x64 --output ${target}`) 14 | .then(function (result) { 15 | let stdout = result.stdout; 16 | let stderr = result.stderr; 17 | console.log('stdout: ', stdout); 18 | console.log('stderr: ', stderr); 19 | }) 20 | .then(() => { 21 | exec(`ls ${target}\*`) 22 | .then(result => { 23 | for (let filename of result.stdout.split('\n')) { 24 | if (!filename) {continue} 25 | let oldname = filename 26 | filename =filename.replace('-win-','-windows-') 27 | filename =filename.replace('-x64','-amd64') 28 | filename =filename.replace('macos','mac') 29 | filename =filename.replace('x86','386') 30 | require('child_process').execSync(`shasum -a 256 release/${filename} | awk '{print $1}' > release/${filename}.sig`) 31 | } 32 | }) 33 | .catch(function (err) { 34 | console.error('ERROR: ', err); 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/auth.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Akamai Technologies, Inc. All Rights Reserved 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | var uuid = require('uuid'), 16 | helpers = require('./helpers'), 17 | logger = require('./logger'), 18 | url = require('url'); 19 | 20 | function makeAuthHeader(key, authData, path, action, timestamp) { 21 | var authSign = helpers.signRequest(authData, key, path, action, timestamp); 22 | 23 | logger.info('Signed authorization header: ' + authSign + '\n'); 24 | 25 | return authSign; 26 | } 27 | 28 | function makeAuthData(request, key, id, group, timestamp, nonce) { 29 | var authData = ["5","0.0.0.0","0.0.0.0", timestamp, nonce, id].join(', ') 30 | 31 | return authData 32 | } 33 | 34 | function makeURL(host, path, queryStringObj) { 35 | var parsed = url.parse("http://" + host + path, true); 36 | if (queryStringObj) parsed.query = queryStringObj; 37 | return url.format( parsed); 38 | } 39 | 40 | module.exports = { 41 | generateAuth: function(request, key, id, group, path, host, action, timestamp) { 42 | timestamp = timestamp || helpers.createTimestamp(); 43 | nonce = helpers.createNonce(); 44 | 45 | if (!request.hasOwnProperty('headers')) { 46 | request.headers = {}; 47 | } 48 | 49 | request.url = makeURL(host, path); 50 | authData = makeAuthData(request, key, id, group, timestamp, nonce); 51 | request.headers["X-Akamai-ACS-Auth-Data"] = authData; 52 | request.headers["X-Akamai-ACS-Auth-Sign"] = makeAuthHeader(key, authData, path, action, timestamp); 53 | request.headers["X-Akamai-ACS-Action"] = action 54 | return request; 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Akamai Technologies, Inc. All Rights Reserved 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | var crypto = require('crypto'), 16 | moment = require('moment'), 17 | url = require('url'), 18 | ini = require('ini'), 19 | fs = require('fs'), 20 | logger = require('./logger'); 21 | 22 | module.exports = { 23 | createTimestamp: function() { 24 | return moment().utc().unix(); 25 | }, 26 | 27 | createNonce: function() { 28 | return crypto.randomBytes(10).toString('hex'); 29 | }, 30 | 31 | contentHash: function(request, maxBody) { 32 | var contentHash = '', 33 | preparedBody = request.body || ''; 34 | 35 | if (typeof preparedBody === 'object') { 36 | var postDataNew = '', 37 | key; 38 | 39 | logger.info('Body content is type Object, transforming to POST data'); 40 | 41 | for (key in preparedBody) { 42 | postDataNew += key + '=' + encodeURIComponent(JSON.stringify(preparedBody[key])) + '&'; 43 | } 44 | 45 | // Strip trailing ampersand 46 | postDataNew = postDataNew.replace(/&+$/, ""); 47 | 48 | preparedBody = postDataNew; 49 | request.body = preparedBody; // Is this required or being used? 50 | } 51 | 52 | logger.info('Body is \"' + preparedBody + '\"'); 53 | logger.debug('PREPARED BODY LENGTH', preparedBody.length); 54 | 55 | if (request.method === 'POST' && preparedBody.length > 0) { 56 | logger.info('Signing content: \"' + preparedBody + '\"'); 57 | 58 | // If body data is too large, cut down to max-body size 59 | if (preparedBody.length > maxBody) { 60 | logger.warn('Data length (' + preparedBody.length + ') is larger than maximum ' + maxBody); 61 | preparedBody = preparedBody.substring(0, maxBody); 62 | logger.info('Body truncated. New value \"' + preparedBody + '\"'); 63 | } 64 | 65 | logger.debug('PREPARED BODY', preparedBody); 66 | 67 | contentHash = this.base64Sha256(preparedBody); 68 | logger.info('Content hash is \"' + contentHash + '\"'); 69 | } 70 | 71 | return contentHash; 72 | }, 73 | 74 | dataToSign: function(authData, path, action, timestamp) { 75 | dataToSign = [ 76 | authData, 77 | path+"\n", 78 | "x-akamai-acs-action:"+action+"\n" 79 | ]; 80 | 81 | dataToSign = dataToSign.join('').toString(); 82 | 83 | logger.info('Data to sign: "' + dataToSign + '" \n'); 84 | 85 | return dataToSign; 86 | }, 87 | 88 | extend: function(a, b) { 89 | var key; 90 | 91 | for (key in b) { 92 | if (!a.hasOwnProperty(key)) { 93 | a[key] = b[key]; 94 | } 95 | } 96 | 97 | return a; 98 | }, 99 | 100 | readConfigFile(filename, section) { 101 | let result; 102 | try { 103 | result = fs.readFileSync(filename) 104 | } catch(error) { 105 | throw new Error ("\n\n \tNo configuration file found. \n\tRun akamai netstorage setup to configure credentials\n\n" ) 106 | } 107 | let configObject = ini.parse(result.toString()) 108 | return (configObject[section]) 109 | }, 110 | 111 | 112 | isRedirect: function(statusCode) { 113 | return [ 114 | 300, 301, 302, 303, 307 115 | ].indexOf(statusCode) !== -1; 116 | }, 117 | 118 | base64Sha256: function(data) { 119 | var shasum = crypto.createHash('sha256').update(data); 120 | 121 | return shasum.digest('base64'); 122 | }, 123 | 124 | base64HmacSha256: function(data, key) { 125 | var encrypt = crypto.createHmac('sha256', key); 126 | 127 | encrypt.update(data); 128 | 129 | return encrypt.digest('base64'); 130 | }, 131 | 132 | /** 133 | * Creates a String containing a tab delimited set of headers. 134 | * @param {Object} headers Object containing the headers to add to the set. 135 | * @return {String} String containing a tab delimited set of headers. 136 | */ 137 | canonicalizeHeaders: function(headers) { 138 | var formattedHeaders = [], 139 | key; 140 | 141 | for (key in headers) { 142 | formattedHeaders.push(key.toLowerCase() + ':' + headers[key].trim().replace(/\s+/g, ' ')); 143 | } 144 | 145 | return formattedHeaders.join('\t'); 146 | }, 147 | 148 | signRequest: function(authData, key, path, action, timestamp) { 149 | return this.base64HmacSha256(this.dataToSign(authData, path, action, timestamp), key); 150 | } 151 | }; 152 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Akamai Technologies, Inc. All Rights Reserved 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | var log4js = require('log4js'), 16 | logger = log4js.getLogger(); 17 | 18 | if (!process.env.LOG4JS_CONFIG) { 19 | logger.level = log4js.levels.ERROR; 20 | } 21 | 22 | module.exports = logger; 23 | -------------------------------------------------------------------------------- /src/netstorage.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Akamai Technologies, Inc. All Rights Reserved 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 'use strict'; 15 | 16 | let untildify = require('untildify'); 17 | let inquirer = require('inquirer') 18 | let md5 = require('md5'); 19 | let fs = require('fs'); 20 | let tmpDir = require('os').tmpdir(); 21 | let ini = require('ini'); 22 | let merge = require ('merge'); 23 | let path = require ('path') 24 | let NetStorageAuth = require ('../src/netstorage_auth'); 25 | let moment = require('moment') 26 | 27 | function sleep(time) { 28 | return new Promise((resolve) => setTimeout(resolve, time)); 29 | } 30 | 31 | function createConfigDir(config) { 32 | return new Promise(function(resolve, reject) { 33 | let createDir = path.dirname(config); 34 | fs.mkdir(createDir, function (error) { 35 | if (error && error.code != "EEXIST") { 36 | reject(error) 37 | } else { 38 | resolve() 39 | } 40 | }) 41 | }) 42 | } 43 | 44 | function readConfigFile(filename, section) { 45 | return new Promise(function(resolve, reject) { 46 | fs.readFile(filename, section, function (error, result) { 47 | if (error) { 48 | resolve() 49 | } else { 50 | let configObject = ini.parse(result.toString()) 51 | if (section) { 52 | resolve(configObject[section]) 53 | } else { 54 | resolve(configObject) 55 | } 56 | 57 | } 58 | }) 59 | }) 60 | } 61 | 62 | 63 | function readUploadFile(filename, section) { 64 | return new Promise(function(resolve, reject) { 65 | fs.readFile(filename, function (error, result) { 66 | if (error) { 67 | reject(error) 68 | } else { 69 | resolve(result); 70 | } 71 | }) 72 | }) 73 | } 74 | 75 | function writeConfigFile(filename, contents) { 76 | return new Promise(function(resolve, reject) { 77 | fs.writeFile(filename, contents, function (error, result) { 78 | if (error) { 79 | reject(error) 80 | } else { 81 | resolve(result); 82 | } 83 | }) 84 | }) 85 | } 86 | 87 | function writeDownloadFile(filename, contents) { 88 | return new Promise(function(resolve, reject) { 89 | fs.writeFile(filename, contents, function (error, result) { 90 | if (error) { 91 | reject(error) 92 | } else { 93 | resolve(result); 94 | } 95 | }) 96 | }) 97 | } 98 | 99 | 100 | //export default class WebSite { 101 | class NetStorage { 102 | 103 | constructor(auth = { config: "~/.akamai-cli/.netstorage/auth", section: "default", debug: false, default: true, setup:false}) { 104 | if (auth.setup) { return } 105 | if (auth.key && auth.id && auth.group && auth.host) 106 | this._nsClient = new NetStorageAuth(auth.key, auth.id, auth.group, auth.host, auth.debug); 107 | else 108 | this._nsClient = new NetStorageAuth({ 109 | path: untildify(auth.config), 110 | section: auth.section, 111 | debug: auth.debug 112 | }); 113 | if (!this._nsClient.config.host && !auth.setup) { 114 | throw new Error("Configuration has not been set up. Please run akamai netstorage setup.") 115 | } 116 | } 117 | 118 | 119 | setup(options) { 120 | let questions = [] 121 | let list = ["key","id","group","host","cpcode"] 122 | 123 | let newList = {"key": "The key field is displayed in the NetStorage HTTP API tab under Upload Accounts", 124 | "id": "On the Upload Account Details page, this is the ID", 125 | "group":"On the Upload Directory Association screen, this is in the second column", 126 | "host": "In the Storage Group Details, you will find this value under NetStorage HTTP API. \nExample: test111-nsu.akamaihd.net", 127 | "cpcode":"Default CPCode to use for your CLI commands. This is optional." 128 | } 129 | 130 | let currentConfig 131 | return new Promise((resolve, reject) => { 132 | console.log("You will need to use the credential information from Luna.") 133 | for (let field of Object.keys(newList)) { 134 | if (!options[field]) { 135 | let question = { 136 | type: "input", 137 | name: field, 138 | message: "\n" + newList[field] + "\nPlease input the following information: " + field +": ", 139 | } 140 | questions.push(question) 141 | } 142 | } 143 | return resolve() 144 | }) 145 | .then(() => { 146 | return readConfigFile(options.config) 147 | }) 148 | .then(config => { 149 | currentConfig = config || {} 150 | return createConfigDir(options.config) 151 | }) 152 | .then(() => { 153 | return inquirer.prompt(questions) 154 | }) 155 | .then(answers => { 156 | options = merge(options, answers) 157 | let filename = options.config 158 | let section = options.section 159 | let config = currentConfig 160 | config[section] = {} 161 | for (let field of list) { 162 | config[section][field] = options[field] 163 | } 164 | return writeConfigFile(filename, ini.stringify(config, {whitespace:true})) 165 | }) 166 | } 167 | diskusage(options) { 168 | return this.parseFileCpCode(options) 169 | .then(options => { 170 | return new Promise ((resolve, reject) => { 171 | console.info("Getting disk usage information") 172 | let request = { 173 | action: "version=1&action=du&format=xml", 174 | path: "/" + options.cpcode 175 | } 176 | return resolve(request) 177 | }) 178 | }) 179 | .then(request => { 180 | return this.makeRequest(request) 181 | }) 182 | .then(response => { 183 | console.log(response.body) 184 | Promise.resolve(response.body) 185 | }) 186 | } 187 | mtime(options) { 188 | return this.parseFileCpCode(options) 189 | .then(options => { 190 | return new Promise ((resolve, reject) => { 191 | if (!options.timestamp) { 192 | options.timestamp=moment().utc().unix(); 193 | } 194 | let path=this.buildPath([options.cpcode, options.file]) 195 | 196 | console.info("Updating modification time for file") 197 | let request = { 198 | action: "version=1&action=mtime&mtime=" + options.timestamp, 199 | method: "POST", 200 | path: path, 201 | } 202 | resolve(request); 203 | }) 204 | }) 205 | .then(request => { 206 | return this.makeRequest(request) 207 | }) 208 | .then(response => { 209 | console.log(response.body) 210 | Promise.resolve(response.body) 211 | }) 212 | } 213 | stat(options) { 214 | return this.parseFileCpCode(options) 215 | .then(options => { 216 | 217 | return new Promise ((resolve, reject) => { 218 | let path=this.buildPath([options.cpcode, options.directory, options.file]) 219 | console.info("Getting stat for file") 220 | let request = { 221 | action: "version=1&action=stat&format=xml", 222 | path: path 223 | } 224 | resolve(request) 225 | }) 226 | .then(request => { 227 | return this.makeRequest(request) 228 | }) 229 | .then(response => { 230 | console.log(response.body) 231 | Promise.resolve(response.body) 232 | }) 233 | }) 234 | } 235 | 236 | upload(options) { 237 | console.info("Uploading file") 238 | 239 | return this.parseFileCpCode(options) 240 | .then(options => { 241 | let path = this.buildPath([options.cpcode, options.directory, options.file]) 242 | var file = fs.createReadStream(options.file) 243 | let request = { 244 | action: "version=1&action=upload", 245 | method: "PUT", 246 | path: path, 247 | body: file 248 | } 249 | return this.makeRequest(request) 250 | .then(response => { 251 | Promise.resolve(response.body) 252 | }) 253 | }).catch(error => { 254 | console.log("ERROR from upload: " + error) 255 | }) 256 | } 257 | 258 | download(options) { 259 | console.info("Downloading file") 260 | 261 | return this.parseFileCpCode(options) 262 | .then(options => { 263 | let path = this.buildPath([options.cpcode, options.path, options.file]) 264 | 265 | let request = { 266 | action: "version=1&action=download", 267 | method: "GET", 268 | path: path 269 | } 270 | return this.makeRequest(request) 271 | }).then(response => { 272 | if (!options.outfile) {options.outfile=options.file} 273 | return writeDownloadFile(options.outfile, response.body) 274 | }).catch(error => { 275 | console.log("ERROR from download: " + error) 276 | }) 277 | } 278 | 279 | delete(options) { 280 | return this.parseFileCpCode(options) 281 | .then(options => { 282 | 283 | return new Promise ((resolve, reject) => { 284 | let path=this.buildPath([options.cpcode, options.directory, options.file]) 285 | console.info("Deleting " + path) 286 | let request = { 287 | method:"PUT", 288 | action: "version=1&action=delete", 289 | path: path, 290 | body: "" 291 | } 292 | resolve(request) 293 | }) 294 | .then(request => { 295 | return this.makeRequest(request) 296 | }) 297 | .then(response => { 298 | console.log(response.body) 299 | Promise.resolve(response.body) 300 | }) 301 | }) 302 | } 303 | 304 | rename(options) { 305 | return this.parseFileCpCode(options) 306 | .then(options => { 307 | 308 | return new Promise ((resolve, reject) => { 309 | let path=this.buildPath([options.cpcode, options.file]) 310 | console.info("Moving " + path) 311 | let newpath = this.buildPath([options.cpcode, options.location]) 312 | 313 | let request = { 314 | method:"POST", 315 | action: "version=1&action=rename&destination=" + newpath, 316 | path: path, 317 | body: "" 318 | } 319 | resolve(request) 320 | }) 321 | .then(request => { 322 | console.log(request); 323 | return this.makeRequest(request) 324 | }) 325 | .then(response => { 326 | console.log(response.body) 327 | Promise.resolve(response.body) 328 | }) 329 | }) 330 | } 331 | 332 | symlink(options) { 333 | return this.parseFileCpCode(options) 334 | .then(options => { 335 | 336 | return new Promise ((resolve, reject) => { 337 | let path=this.buildPath([options.cpcode, options.file]) 338 | console.info("Creating Symlink for " + path) 339 | let newpath = this.buildPath([options.cpcode, options.target]) 340 | 341 | let request = { 342 | method:"POST", 343 | action: "version=1&action=symlink&target=" + newpath, 344 | path: path, 345 | body: "" 346 | } 347 | resolve(request) 348 | }) 349 | .then(request => { 350 | console.log(request); 351 | return this.makeRequest(request) 352 | }) 353 | .then(response => { 354 | console.log(response.body) 355 | Promise.resolve(response.body) 356 | }) 357 | }) 358 | } 359 | 360 | 361 | mkdir(options) { 362 | return this.parseFileCpCode(options) 363 | .then(options => { 364 | return new Promise ((resolve, reject) => { 365 | let path = this.buildPath([options.cpcode, options.path, options.directory]); 366 | 367 | console.info("Creating directory") 368 | let request = { 369 | action: "version=1&action=mkdir", 370 | method: "PUT", 371 | path: path, 372 | } 373 | resolve(request); 374 | }) 375 | }) 376 | .then(request => { 377 | return this.makeRequest(request) 378 | }) 379 | .then(response => { 380 | console.log(response.body) 381 | Promise.resolve(response.body) 382 | }) 383 | } 384 | 385 | quickDelete(options) { 386 | return this.parseFileCpCode(options) 387 | .then(options => { 388 | 389 | return new Promise ((resolve, reject) => { 390 | let path=this.buildPath([options.cpcode, options.directory]) 391 | console.info("Quick deleting " + path) 392 | let request = { 393 | method:"POST", 394 | action: "version=1&action=quick-delete&quick-delete=imreallyreallysure", 395 | path: path, 396 | body: "" 397 | } 398 | resolve(request) 399 | }) 400 | .then(request => { 401 | console.log(request) 402 | return this.makeRequest(request) 403 | }) 404 | .then(response => { 405 | console.log(response.body) 406 | Promise.resolve(response.body) 407 | }) 408 | }) 409 | } 410 | 411 | rmdir(options) { 412 | return this.parseFileCpCode(options) 413 | .then(options => { 414 | 415 | return new Promise ((resolve, reject) => { 416 | let path=this.buildPath([options.cpcode, options.directory]) 417 | console.info("Deleting " + path) 418 | let request = { 419 | method:"PUT", 420 | action: "version=1&action=rmdir", 421 | path: path, 422 | body: "" 423 | } 424 | resolve(request) 425 | }) 426 | .then(request => { 427 | console.log(request) 428 | return this.makeRequest(request) 429 | }) 430 | .then(response => { 431 | console.log(response.body) 432 | Promise.resolve(response.body) 433 | }) 434 | }) 435 | } 436 | 437 | list(options) { 438 | return this.parseFileCpCode(options) 439 | .then(options => { 440 | return new Promise ((resolve, reject) => { 441 | let path=this.buildPath([options.cpcode, options.directory]) 442 | let query=this.buildQuery(options, ["end","max_entries","encoding"],"&") 443 | 444 | console.info("Getting directory listing") 445 | let request = { 446 | action: "version=1&action=list&format=xml", 447 | path: path, 448 | } 449 | resolve(request); 450 | }) 451 | }) 452 | .then(request => { 453 | return this.makeRequest(request) 454 | }) 455 | .then(response => { 456 | console.log(response.body) 457 | Promise.resolve(response.body) 458 | }) 459 | } 460 | 461 | dir(options) { 462 | return this.parseFileCpCode(options) 463 | .then(options => { 464 | return new Promise ((resolve, reject) => { 465 | let path=this.buildPath([options.cpcode, options.directory]) 466 | let query=this.buildQuery(options, ["prefix","start","end","max_entries","encoding"],"&") 467 | console.info("Getting directory listing") 468 | let request = { 469 | action: "version=1&action=dir&format=xml" + query, 470 | path: path, 471 | } 472 | resolve(request); 473 | }) 474 | }) 475 | .then(request => { 476 | return this.makeRequest(request) 477 | }) 478 | .then(response => { 479 | console.log(response.body) 480 | return(response.body) 481 | }) 482 | } 483 | 484 | ///////////////////////////////////////////////////// 485 | // Helper methods 486 | ///////////////////////////////////////////////////// 487 | 488 | // Build a path from components, skip empty values 489 | // To avoid the issue NS has with // in paths 490 | buildPath(components) { 491 | if (components == []) { 492 | return; 493 | } 494 | let comparray = [""] 495 | components.map(element => { 496 | if (element) { 497 | comparray.push(element) 498 | } 499 | }) 500 | return comparray.join('/').toString(); 501 | } 502 | 503 | // Build action query string from variables 504 | buildQuery(object, components) { 505 | let comparray = []; 506 | components.map(element => { 507 | if (object[element] != null) { 508 | comparray.push(element + "=" + object[element]) 509 | } 510 | }) 511 | if (comparray.length == 0) {return} 512 | comparray.unshift("") 513 | return comparray.join('&').toString(); 514 | } 515 | 516 | // Get the CPCode from flag, from environment or from the file path 517 | // in the command 518 | parseFileCpCode(options){ 519 | return new Promise ((resolve, reject) => { 520 | options.cpcode = options.cpcode || this._nsClient.config.cpcode 521 | 522 | if (!options.cpcode && options.file) { 523 | var re = /\/(\d+)\/(.*)$/i; 524 | var match = options.file.match(re); 525 | if (!match && options.directory) { 526 | match = options.directory.match(re); 527 | } 528 | if (match && match[1]) { 529 | options.cpcode = match[1]; 530 | options.file = match[2]; 531 | return resolve (options); 532 | } 533 | } 534 | if (!options.cpcode) { 535 | return reject("No CPCode found in environment or config file.") 536 | } 537 | resolve (options); 538 | }) 539 | } 540 | 541 | 542 | makeRequest(request) { 543 | return new Promise ((resolve, reject) => { 544 | this._nsClient.auth(request) 545 | this._nsClient.send((data, response) => { 546 | if (response.statusCode != 200) { 547 | reject("Unable to complete action. Status code " + response.statusCode) 548 | } else { 549 | resolve(response) 550 | } 551 | }) 552 | }) 553 | } 554 | 555 | } 556 | module.exports = NetStorage; 557 | /* 558 | Id (Key-name): kirsten 559 | Key: NUq2a5N8C14uWCs8k3aq7l003J40ymIS7s45v5Jn9LHhl5QRIz 560 | Storage group name: kirsten 561 | Connection Hostname: kirsten-nsu.akamaihd.net 562 | */ 563 | -------------------------------------------------------------------------------- /src/netstorage_auth.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Akamai Technologies, Inc. All Rights Reserved 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | var request = require('request'), 16 | url = require('url'), 17 | auth = require('./auth'), 18 | netstorage_rc = require('./netstorage_rc'), 19 | helpers = require('./helpers'), 20 | logger = require('./logger'); 21 | 22 | var NetStorageAuth = function(key, id, group, host, debug) { 23 | request.debug = process.env.EG_VERBOSE || false; 24 | if (typeof arguments[0] === 'object') { 25 | request.debug = request.debug || arguments[0].debug ? true : false; 26 | this._setConfigFromObj(arguments[0]); 27 | } else { 28 | request.debug = request.debug || debug ? true : false; 29 | this._setConfigFromStrings(key, id, group, host, debug); 30 | } 31 | }; 32 | 33 | /** 34 | * Builds the request using the properties of the local config Object. 35 | * 36 | * @param {Object} req The request Object. Can optionally contain a 37 | * 'headersToSign' property: An ordered list header names 38 | * that will be included in the signature. This will be 39 | * provided by specific APIs. 40 | */ 41 | NetStorageAuth.prototype.auth = function(req) { 42 | req = helpers.extend(req, { 43 | url: this.config.host + req.path, 44 | method: 'GET', 45 | headers: { 46 | 'Content-Type': "application/json" 47 | }, 48 | followRedirect: false, 49 | body: '' 50 | }); 51 | 52 | this.request = auth.generateAuth( 53 | req, 54 | this.config.key, 55 | this.config.id, 56 | this.config.group, 57 | req.path, 58 | this.config.host, 59 | req.action 60 | ); 61 | return this; 62 | }; 63 | 64 | NetStorageAuth.prototype.send = function(callback) { 65 | request(this.request, function(error, response, body) { 66 | 67 | if (error) { 68 | callback(error); 69 | return; 70 | } 71 | if (helpers.isRedirect(response.statusCode)) { 72 | this._handleRedirect(response, callback); 73 | return; 74 | } 75 | callback(null, response, body); 76 | }.bind(this)); 77 | 78 | return this; 79 | }; 80 | 81 | NetStorageAuth.prototype._handleRedirect = function(resp, callback) { 82 | var parsedUrl = url.parse(resp.headers['location']); 83 | 84 | resp.headers['authorization'] = undefined; 85 | this.request.url = undefined; 86 | this.request.path = parsedUrl.path; 87 | 88 | this.auth(this.request); 89 | this.send(callback); 90 | }; 91 | 92 | /** 93 | * Creates a config object from a set of parameters. 94 | * 95 | * @param {String} key 96 | * @param {String} id 97 | * @param {String} group 98 | * @param {String} host 99 | */ 100 | NetStorageAuth.prototype._setConfigFromStrings = function(key, id, group, host) { 101 | if (!validatedArgs([key, id, group, host])) { 102 | throw new Error('Insufficient Akamai credentials'); 103 | } 104 | 105 | this.config = { 106 | key: key, 107 | id: id, 108 | group: group, 109 | host: host 110 | }; 111 | }; 112 | 113 | function validatedArgs(args) { 114 | var expected = [ 115 | 'key', 'id', 'group', 'host' 116 | ], 117 | valid = true; 118 | 119 | expected.forEach(function(arg, i) { 120 | if (!args[i]) { 121 | if (process.env.EDGEGRID_ENV !== 'test') { 122 | logger.error('No defined ' + arg); 123 | } 124 | 125 | valid = false; 126 | } 127 | }); 128 | 129 | return valid; 130 | } 131 | 132 | /** 133 | * Creates a config Object from the section of a defined .edgerc file. 134 | * 135 | * @param {Object} obj An Object containing a path and section property that 136 | * define the .edgerc section to use to create the Object. 137 | */ 138 | NetStorageAuth.prototype._setConfigFromObj = function(obj) { 139 | if (!obj.path) { 140 | if (process.env.EDGEGRID_ENV !== 'test') { 141 | logger.error('No .netstorage path'); 142 | } 143 | 144 | throw new Error('No netstorage auth path'); 145 | } 146 | 147 | this.config = netstorage_rc(obj.path, obj.section); 148 | }; 149 | 150 | module.exports = NetStorageAuth; 151 | -------------------------------------------------------------------------------- /src/netstorage_rc.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Akamai Technologies, Inc. All Rights Reserved 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | var fs = require('fs'); 16 | helpers = require('./helpers'); 17 | 18 | function readEnv(section) { 19 | // If any are set, we're committed 20 | var envConf = {}; 21 | var envPrefix = "AKAMAI_NS_" + section.toUpperCase() 22 | var tokens = 23 | ['key', 'id','group','host']; 24 | 25 | tokens.forEach(function(token){ 26 | var envcheck = envPrefix + "_" + token.toUpperCase() 27 | if (process.env[envcheck]) { 28 | envConf[token] = process.env[envcheck]; 29 | } 30 | }) 31 | 32 | if (Object.keys(envConf).length > 0) { 33 | console.log("Using configuration from environment variables") 34 | return envConf; 35 | } 36 | return; 37 | } 38 | 39 | module.exports = function(path, conf) { 40 | var confSection = conf || 'default' 41 | var envConf = readEnv(confSection); 42 | if (envConf && Object.keys(envConf).length > 0) { 43 | return envConf; 44 | } 45 | 46 | var config = helpers.readConfigFile(path, confSection) 47 | 48 | if (!config) { 49 | throw new Error('An error occurred parsing the .netstorage/auth file. You probably specified an invalid section name.'); 50 | } 51 | 52 | return (config); 53 | }; 54 | -------------------------------------------------------------------------------- /test/README: -------------------------------------------------------------------------------- 1 | Test directory 2 | 3 | To run the tests, first set the following environment variables: 4 | * AKAMAI_TEST_HOST = host you want to work with 5 | * AKAMAI_TEST_PROPID = property ID for that host 6 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --timeout 50000000 2 | -------------------------------------------------------------------------------- /test/nstest.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | var NetStorage = require('../index').NetStorage; 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | 6 | 7 | var netstorage = new NetStorage() 8 | 9 | let temp_directory = "circle-" + Date.now(); 10 | 11 | describe('Retrieve directory listing', function () { 12 | it ('should retrieve the directory listing', function() { 13 | return netstorage.dir({}) 14 | .then(response => { 15 | assert(response) 16 | }) 17 | .catch((error) => { 18 | assert(error); 19 | }) 20 | }) 21 | 22 | it ('should retrieve directory listing with options', function() { 23 | return netstorage.dir({prefix:"test"}) 24 | .then(response => { 25 | assert(response) 26 | }) 27 | .catch((error) => { 28 | assert(error); 29 | }) 30 | }) 31 | }) 32 | describe('Retrieve disk usage', function () { 33 | it ('should display the disk usage', function() { 34 | return netstorage.diskusage({}) 35 | .then(response => { 36 | assert(response) 37 | }) 38 | .catch((error) => { 39 | assert(error); 40 | }) 41 | }) 42 | }) 43 | 44 | describe('Should create, upload, list, delete and rmdir', function () { 45 | it ('should create a new directory', function() { 46 | return netstorage.mkdir({directory:temp_directory}) 47 | .then(response => { 48 | assert(response) 49 | }) 50 | .catch((error) => { 51 | assert(error); 52 | }) 53 | }) 54 | 55 | it ('should upload a file to the directory', function() { 56 | return netstorage.upload({file:"test/nstest.js", directory:temp_directory}) 57 | .then(response => { 58 | assert(response) 59 | }) 60 | .catch((error) => { 61 | assert(error); 62 | }) 63 | }) 64 | it ('should do a directory listing on the directory', function() { 65 | return netstorage.dir({directory:temp_directory}) 66 | .then(response => { 67 | assert(response) 68 | }) 69 | .catch((error) => { 70 | assert(error); 71 | }) 72 | }) 73 | it ('should delete the file from the directory', function() { 74 | return netstorage.delete({file:temp_directory + "/test/nstest.js"}) 75 | .then(response => { 76 | assert(response) 77 | }) 78 | .catch((error) => { 79 | assert(error); 80 | }) 81 | }) 82 | it ('should remove the directory', function() { 83 | return netstorage.delete({directory:temp_directory}) 84 | .then(response => { 85 | console.log(response) 86 | }) 87 | .then(response => { 88 | assert(response) 89 | }) 90 | .catch((error) => { 91 | assert(error); 92 | }) 93 | }) 94 | 95 | }) 96 | 97 | --------------------------------------------------------------------------------