├── .editorconfig ├── .ember-cli ├── .eslintrc.js ├── .github ├── PULL_REQUEST_TEMPLATE └── workflows │ └── ci.yaml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── RELEASE.md ├── appveyor.yml ├── index.js ├── lib ├── s3.js └── util │ └── join-uri-segments.js ├── package.json ├── tests ├── .eslintrc.js ├── helpers │ └── assert.js ├── runner.js └── unit │ ├── .gitkeep │ ├── fixtures │ ├── test.html │ └── test.tar │ ├── index-test.js │ └── lib │ └── s3-test.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.js] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.hbs] 21 | insert_final_newline = false 22 | indent_style = space 23 | indent_size = 2 24 | 25 | [*.css] 26 | indent_style = space 27 | indent_size = 2 28 | 29 | [*.html] 30 | indent_style = space 31 | indent_size = 2 32 | 33 | [*.{diff,md}] 34 | trim_trailing_whitespace = false 35 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | ecmaVersion: 'latest', 5 | sourceType: 'module' 6 | }, 7 | extends: 'eslint:recommended', 8 | env: { 9 | node: true 10 | }, 11 | rules: { 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | ## What Changed & Why 2 | Explain what changed and why. 3 | 4 | ## Related issues 5 | Link to related issues in this or other repositories (if any) 6 | 7 | ## PR Checklist 8 | - [ ] Add tests 9 | - [ ] Add documentation 10 | - [ ] Prefix documentation-only commits with [DOC] 11 | 12 | ## People 13 | Mention people who would be interested in the changeset (if any) 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | name: Test 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node-version: [14.x, 16.x, 18.x, 20.x] 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | cache: 'yarn' 21 | - run: yarn install 22 | - run: yarn test 23 | 24 | test-floating: 25 | name: Floating Dependencies 26 | runs-on: ubuntu-latest 27 | strategy: 28 | matrix: 29 | node-version: [14.x, 16.x, 18.x, 20.x] 30 | steps: 31 | - uses: actions/checkout@v2 32 | - name: Use Node.js ${{ matrix.node-version }} 33 | uses: actions/setup-node@v2 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | cache: 'yarn' 37 | - name: install dependencies 38 | run: yarn install --no-lockfile 39 | - name: test 40 | run: yarn test 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/* 15 | /libpeerconnection.log 16 | npm-debug.log 17 | testem.log 18 | 19 | .tool-versions -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | tests/ 3 | tmp/ 4 | dist/ 5 | 6 | .bowerrc 7 | .editorconfig 8 | .ember-cli 9 | .travis.yml 10 | .npmignore 11 | **/.gitkeep 12 | bower.json 13 | ember-cli-build.js 14 | Brocfile.js 15 | testem.json 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | 4 | 5 | 6 | 7 | ## 4.0.3 (2024-05-02) 8 | 9 | #### :bug: Bug Fix 10 | * [#143](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/143) Handle NotFound S3 Response ([@jrjohnson](https://github.com/jrjohnson)) 11 | 12 | #### Committers: 1 13 | - Jon Johnson ([@jrjohnson](https://github.com/jrjohnson)) 14 | 15 | ## 4.0.2 (2024-04-05) 16 | 17 | #### :bug: Bug Fix 18 | * [#142](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/142) Handle empty s3 response ([@jrjohnson](https://github.com/jrjohnson)) 19 | 20 | #### Committers: 1 21 | - Jon Johnson ([@jrjohnson](https://github.com/jrjohnson)) 22 | 23 | ## 4.0.1 (2024-04-03) 24 | 25 | #### :rocket: Enhancement 26 | * Release changes in 4.0.0-0 prerelease. 27 | * [#134](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/134) Optionally ignore ACLs ([@khornberg](https://github.com/khornberg)) 28 | 29 | #### Committers: 1 30 | - Kyle Hornberg ([@khornberg](https://github.com/khornberg)) 31 | 32 | 33 | ## 4.0.0-0 (2023-08-28) 34 | 35 | Pre-release to verify aws sdk update 36 | 37 | ## 3.0.0 (2023-05-16) 38 | 39 | #### :boom: Breaking Change 40 | * [#133](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/133) Update node version requirements and dependencies ([@lukemelia](https://github.com/lukemelia)) 41 | * [#113](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/113) [breaking] Switch to Github Actions for CI ([@jrjohnson](https://github.com/jrjohnson)) 42 | 43 | #### :rocket: Enhancement 44 | * [#130](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/130) Add support for didDeployMessage ([@jkeen](https://github.com/jkeen)) 45 | * [#119](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/119) Optimise activation speed ([@vitch](https://github.com/vitch)) 46 | 47 | #### Committers: 4 48 | - Jeff Keen ([@jkeen](https://github.com/jkeen)) 49 | - Jon Johnson ([@jrjohnson](https://github.com/jrjohnson)) 50 | - Kelvin Luck ([@vitch](https://github.com/vitch)) 51 | - Luke Melia ([@lukemelia](https://github.com/lukemelia)) 52 | 53 | ## 2.0.0 There was no 2.0 54 | 55 | ## [v1.0.1](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/tree/v1.0.1) 56 | [Full Changelog](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/compare/v1.0.0...v1.0.1) 57 | 58 | - [#74](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/74) add support for AWS credential profile [@VertekCorp](https://github.com/VertekCorp) 59 | 60 | Thank you to all who took the time to contribute! 61 | 62 | ## [v1.0.0](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/tree/v1.0.0) (2017-03-31) 63 | [Full Changelog](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/compare/v1.0.0-beta.1...v1.0.0) 64 | 65 | No changes from beta.1 66 | 67 | ## [v1.0.0-beta.1](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/tree/v1.0.0-beta.1) (2017-03-31) 68 | [Full Changelog](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/compare/v1.0.0-beta.0...v1.0.0-beta.1) 69 | 70 | **Merged pull requests:** 71 | 72 | - Add support for recusively listing all revisions in a bucket [\#71](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/71) ([vitch](https://github.com/vitch)) 73 | 74 | ## [v1.0.0-beta.0](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/tree/v1.0.0-beta.0) (2017-03-25) 75 | [Full Changelog](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/compare/v0.5.0...v1.0.0-beta.0) 76 | 77 | **Merged pull requests:** 78 | 79 | - Upgrade ember-cli & embrace being a node-only ember-cli addon [\#72](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/72) ([lukemelia](https://github.com/lukemelia)) 80 | - \[DOC\] Link to previewing revisions article [\#69](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/69) ([blimmer](https://github.com/blimmer)) 81 | - Add Server Side Encryption [\#67](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/67) ([sethpollack](https://github.com/sethpollack)) 82 | - Update config example to reflect required parameters [\#66](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/66) ([crhayes](https://github.com/crhayes)) 83 | 84 | ## [v0.5.0](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/tree/v0.5.0) (2016-05-12) 85 | [Full Changelog](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/compare/v0.4.0...v0.5.0) 86 | 87 | **Merged pull requests:** 88 | 89 | - Allow cache-control headers to be set by options [\#63](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/63) ([LevelbossMike](https://github.com/LevelbossMike)) 90 | - Upgrade ember-cli to 2.5.0 [\#62](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/62) ([LevelbossMike](https://github.com/LevelbossMike)) 91 | 92 | ## [v0.4.0](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/tree/v0.4.0) (2016-04-01) 93 | [Full Changelog](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/compare/v0.3.1...v0.4.0) 94 | 95 | **Merged pull requests:** 96 | 97 | - Take `filePattern` into account when determining `isGzipped` status [\#59](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/59) ([elidupuis](https://github.com/elidupuis)) 98 | 99 | ## [v0.3.1](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/tree/v0.3.1) (2016-02-21) 100 | [Full Changelog](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/compare/v0.3.0...v0.3.1) 101 | 102 | **Merged pull requests:** 103 | 104 | - Release v0.3.1 [\#57](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/57) ([LevelbossMike](https://github.com/LevelbossMike)) 105 | - Remove remaining `path.join` for win-compatibility [\#56](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/56) ([LevelbossMike](https://github.com/LevelbossMike)) 106 | - Add appveyor badge [\#55](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/55) ([LevelbossMike](https://github.com/LevelbossMike)) 107 | - Add appveyor for windows builds [\#53](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/53) ([LevelbossMike](https://github.com/LevelbossMike)) 108 | - Add tests for untested hooks [\#51](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/51) ([LevelbossMike](https://github.com/LevelbossMike)) 109 | - Update README to include Cloudfont pretty url config [\#50](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/50) ([jarredkenny](https://github.com/jarredkenny)) 110 | - Remove path.join on file upload key [\#43](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/43) ([twaggs](https://github.com/twaggs)) 111 | - Fix \#27: Conditionally add ContentEncoding:gzip header when index.html is gzipped [\#42](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/42) ([taylon](https://github.com/taylon)) 112 | 113 | ## [v0.3.0](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/tree/v0.3.0) (2016-02-06) 114 | [Full Changelog](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/compare/v0.2.0...v0.3.0) 115 | 116 | **Merged pull requests:** 117 | 118 | - add fetchInitialRevisions [\#47](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/47) ([ghedamat](https://github.com/ghedamat)) 119 | - Add mimetype detection based on `filePattern`/`filePath` [\#46](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/46) ([elidupuis](https://github.com/elidupuis)) 120 | - Ensure ACL configuration option is set during activation [\#44](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/44) ([elidupuis](https://github.com/elidupuis)) 121 | - update ember-cli-deploy-plugin [\#41](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/41) ([ghedamat](https://github.com/ghedamat)) 122 | - \[Doc\] Fix documentation passing revision via cli [\#39](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/39) ([LevelbossMike](https://github.com/LevelbossMike)) 123 | - Readme: Missing `=` in snippet [\#38](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/38) ([jthiller](https://github.com/jthiller)) 124 | - Useful info for AWS admins [\#37](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/37) ([pablobm](https://github.com/pablobm)) 125 | - Don't use path.join for URLs [\#36](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/36) ([LevelbossMike](https://github.com/LevelbossMike)) 126 | 127 | ## [v0.2.0](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/tree/v0.2.0) (2015-12-31) 128 | [Full Changelog](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/compare/v0.1.1...v0.2.0) 129 | 130 | **Merged pull requests:** 131 | 132 | - Add missing assignment [\#34](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/34) ([backspace](https://github.com/backspace)) 133 | - Loosen AWS option requirements [\#33](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/33) ([quiddle](https://github.com/quiddle)) 134 | - \[BREAKING\] Make `region` a required configuration. [\#31](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/31) ([LevelbossMike](https://github.com/LevelbossMike)) 135 | - Update to revision plugin information on readme [\#29](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/29) ([Jordan4jc](https://github.com/Jordan4jc)) 136 | 137 | ## [v0.1.1](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/tree/v0.1.1) (2015-12-13) 138 | [Full Changelog](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/compare/v0.1.0...v0.1.1) 139 | 140 | **Merged pull requests:** 141 | 142 | - update link to ember-cli-deploy-build [\#26](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/26) ([csantero](https://github.com/csantero)) 143 | - Add support for ACL on objects [\#24](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/24) ([flecno](https://github.com/flecno)) 144 | - Warn about configuration sharing [\#22](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/22) ([LevelbossMike](https://github.com/LevelbossMike)) 145 | - Fix quickstart instructions in README.md [\#17](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/17) ([LevelbossMike](https://github.com/LevelbossMike)) 146 | 147 | ## [v0.1.0](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/tree/v0.1.0) (2015-10-25) 148 | [Full Changelog](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/compare/v0.1.0-beta.1...v0.1.0) 149 | 150 | **Merged pull requests:** 151 | 152 | - Release 0.1.0 [\#14](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/14) ([LevelbossMike](https://github.com/LevelbossMike)) 153 | - Update to use new verbose option for logging [\#13](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/13) ([lukemelia](https://github.com/lukemelia)) 154 | 155 | ## [v0.1.0-beta.1](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/tree/v0.1.0-beta.1) (2015-10-19) 156 | **Merged pull requests:** 157 | 158 | - Release v0.1.0-beta.1 [\#12](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/12) ([LevelbossMike](https://github.com/LevelbossMike)) 159 | - Add `allowOverwrite` option [\#11](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/11) ([LevelbossMike](https://github.com/LevelbossMike)) 160 | - Updating docs for new options [\#10](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/10) ([lpetre](https://github.com/lpetre)) 161 | - Add Travis badge [\#9](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/9) ([LevelbossMike](https://github.com/LevelbossMike)) 162 | - Add tests for lib/s3 [\#8](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/8) ([LevelbossMike](https://github.com/LevelbossMike)) 163 | - Large restructure of s3-index plugin [\#7](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/7) ([lpetre](https://github.com/lpetre)) 164 | - Handle the case where there is no current.json [\#5](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/5) ([lpetre](https://github.com/lpetre)) 165 | - Support passing a valid s3client via config [\#4](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/4) ([lpetre](https://github.com/lpetre)) 166 | - Added missing dependency [\#3](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/3) ([vitch](https://github.com/vitch)) 167 | - Added function keyword [\#2](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/2) ([vitch](https://github.com/vitch)) 168 | - \[WIP\] Retrieve revisionKey from revisionData [\#1](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/pull/1) ([achambers](https://github.com/achambers)) 169 | 170 | 171 | 172 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* 173 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ember-cli-deploy-s3-index [![Build Status](https://travis-ci.org/ember-cli-deploy/ember-cli-deploy-s3-index.svg?branch=master)](https://travis-ci.org/ember-cli-deploy/ember-cli-deploy-s3-index) [![Build status](https://ci.appveyor.com/api/projects/status/iogkx02goxx6jsf3/branch/master?svg=true)](https://ci.appveyor.com/project/LevelbossMike/ember-cli-deploy-s3-index/branch/master) 2 | 3 | 4 | > An ember-cli-deploy plugin to deploy ember-cli's bootstrap index file to [S3](https://aws.amazon.com/de/s3). 5 | 6 | [![](https://ember-cli-deploy.github.io/ember-cli-deploy-version-badges/plugins/ember-cli-deploy-s3-index.svg)](http://ember-cli-deploy.github.io/ember-cli-deploy-version-badges/) 7 | 8 | This plugin uploads a file, presumably index.html, to a specified S3-bucket. 9 | 10 | More often than not this plugin will be used in conjunction with the [lightning method of deployment][1] where the ember application assets will be served from S3 and the index.html file will also be served from S3. 11 | 12 | ## What is an ember-cli-deploy plugin? 13 | 14 | A plugin is an addon that can be executed as a part of the ember-cli-deploy pipeline. A plugin will implement one or more of the ember-cli-deploy's pipeline hooks. 15 | 16 | For more information on what plugins are and how they work, please refer to the [Plugin Documentation][2]. 17 | 18 | ## Quick Start 19 | To get up and running quickly, do the following: 20 | 21 | - Ensure [ember-cli-deploy-build][4] is installed and configured. 22 | - Ensure [ember-cli-deploy-revision-data][6] is installed and configured. 23 | - Ensure [ember-cli-deploy-display-revisions](https://github.com/duizendnegen/ember-cli-deploy-display-revisions) is installed and configured. 24 | 25 | - Install this plugin 26 | 27 | ```bash 28 | $ ember install ember-cli-deploy-s3-index 29 | ``` 30 | 31 | - Place the following configuration into `config/deploy.js` 32 | 33 | ```javascript 34 | ENV['s3-index'] = { 35 | accessKeyId: '', 36 | secretAccessKey: '', 37 | bucket: '', 38 | region: '' 39 | } 40 | ``` 41 | 42 | - Run the pipeline 43 | 44 | ```bash 45 | $ ember deploy 46 | ``` 47 | 48 | ## ember-cli-deploy Hooks Implemented 49 | 50 | For detailed information on what plugin hooks are and how they work, please refer to the [Plugin Documentation][2]. 51 | 52 | - `configure` 53 | - `upload` 54 | - `activate` 55 | - `fetchRevisions` 56 | 57 | ## Configuration Options 58 | 59 | 60 | For detailed information on how configuration of plugins works, please refer to the [Plugin Documentation][2]. 61 | 62 | 63 |
64 | **WARNING: Don't share a configuration object between [ember-cli-deploy-s3](https://github.com/ember-cli-deploy/ember-cli-deploy-s3) and this plugin. The way these two plugins read their configuration has sideeffects which will unfortunately break your deploy if you share one configuration object between the two** (we are already working on a fix) 65 |
66 | 67 | ### accessKeyId 68 | 69 | The AWS access key for the user that has the ability to upload to the `bucket`. If this is left undefined, the normal [AWS SDK credential resolution][7] will take place. 70 | 71 | *Default:* `undefined` 72 | 73 | ### secretAccessKey 74 | 75 | The AWS secret for the user that has the ability to upload to the `bucket`. This must be defined when `accessKeyId` is defined. 76 | 77 | *Default:* `undefined` 78 | 79 | ### profile 80 | 81 | The AWS profile as definied in ~/.aws/credentials. If this is left undefined, the normal [AWS SDK credential resolution][7] will take place. 82 | 83 | *Default:* `undefined` 84 | 85 | ### bucket (`required`) 86 | 87 | The AWS bucket that the files will be uploaded to. 88 | 89 | *Default:* `undefined` 90 | 91 | ### region (`required`) 92 | 93 | The region your bucket is located in. (e.g. set this to `eu-west-1` if your bucket is located in the 'Ireland' region) 94 | 95 | *Default:* `undefined` 96 | 97 | ### prefix 98 | 99 | A directory within the `bucket` that the files should be uploaded in to. 100 | 101 | *Default:* `''` 102 | 103 | ### filePattern 104 | 105 | A file matching this pattern will be uploaded to S3. The active key in S3 will be a combination of the `bucket`, `prefix`, `filePattern`. The versioned keys will have `revisionKey` appended. 106 | 107 | *Default:* `'index.html'` 108 | 109 | ### acl 110 | 111 | The ACL to apply to the objects. 112 | 113 | Set to `false` to not apply any ACLs. 114 | 115 | *Default:* `'public-read'` 116 | 117 | ### cacheControl 118 | 119 | Sets the `Cache-Control` header on uploaded files. 120 | 121 | *Default:* `'max-age=0, no-cache'` 122 | 123 | ### distDir 124 | 125 | The root directory where the file matching `filePattern` will be searched for. By default, this option will use the `distDir` property of the deployment context. 126 | 127 | *Default:* `context.distDir` 128 | 129 | ### revisionKey 130 | 131 | The unique revision number for the version of the file being uploaded to S3. The key will be a combination of the `keyPrefix` and the `revisionKey`. By default this option will use either the `revision` passed in from the command line or the `revisionData.revisionKey` property from the deployment context. 132 | 133 | *Default:* `context.commandOptions.revision || context.revisionData.revisionKey` 134 | 135 | ### allowOverwrite 136 | 137 | A flag to specify whether the revision should be overwritten if it already exists in S3. 138 | 139 | *Default:* `false` 140 | 141 | ### s3Client 142 | 143 | The underlying S3 library used to upload the files to S3. This allows the user to use the default upload client provided by this plugin but switch out the underlying library that is used to actually send the files. 144 | 145 | The client specified MUST implement functions called `getObject` and `putObject`. See [Using V2 API] (https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/welcome.html#using_v2_commands). 146 | 147 | *Default:* the default S3 library is `@aws-sdk/s3-client` (AWS JS SDK v3). Please use this addon version < 3.0.0 if you want to use AWS JS SDK 2.0 148 | 149 | ### endpoint 150 | 151 | AWS (or AWS compatible endpoint) to use. E.g. with DigitalOcean Spaces, Microsoft Azure Blob Storage, 152 | or Openstack Swift 153 | 154 | If `endpoint` set the `region` option will be ignored. 155 | 156 | *Default:* `[region].s3.amazonaws.com` 157 | 158 | 159 | ### serverSideEncryption 160 | 161 | The Server-side encryption algorithm used when storing this object in S3 (e.g., AES256, aws:kms). Possible values include: 162 | - "AES256" 163 | - "aws:kms" 164 | 165 | ### urlEncodeSourceObject 166 | 167 | Controls if the `x-amz-copy-source` header is going to be be URL encoded. 168 | There is a known issue with DigitalOcean Spaces and older versions of CEPH 169 | that don't accept URL encoded copy sources. 170 | 171 | If you are using DigitalOcean spaces you need to set this setting to `false`. 172 | 173 | *Default:* `true` 174 | 175 | ### didDeployMessage 176 | 177 | A message that will be displayed after the index file has been successfully uploaded to S3. By default this message will only display if the revision for `revisionData.revisionKey` of the deployment context has been activated. 178 | 179 | *Default:* 180 | 181 | ```javascript 182 | if (context.revisionData.revisionKey && !context.revisionData.activatedRevisionKey) { 183 | return "Deployed but did not activate revision " + context.revisionData.revisionKey + ". " 184 | + "To activate, run: " 185 | + "ember deploy:activate " + context.revisionData.revisionKey + " --environment=" + context.deployEnvironment + "\n"; 186 | } 187 | ``` 188 | 189 | 190 | 191 | ### How do I activate a revision? 192 | 193 | A user can activate a revision by either: 194 | 195 | - Passing a command line argument to the `deploy` command: 196 | 197 | ```bash 198 | $ ember deploy --activate=true 199 | ``` 200 | 201 | - Running the `deploy:activate` command: 202 | 203 | ```bash 204 | $ ember deploy:activate --revision 205 | ``` 206 | 207 | - Setting the `activateOnDeploy` flag in `deploy.js` 208 | 209 | ```javascript 210 | ENV.pipeline = { 211 | activateOnDeploy: true 212 | } 213 | ``` 214 | 215 | ### What does activation do? 216 | 217 | When *ember-cli-deploy-s3-index* uploads a file to S3, it uploads it under the key defined by a combination of the four config properties `bucket`, `prefix`, `filePattern` and `revisionKey`. 218 | 219 | So, if the `filePattern` was configured to be `index.html` and there had been a few revisons deployed, then your bucket might look something like this: 220 | 221 | ```bash 222 | $ aws s3 ls s3:/// 223 | PRE assets/ 224 | 2015-09-27 07:25:26 585 crossdomain.xml 225 | 2015-09-27 07:47:42 1207 index.html 226 | 2015-09-27 07:25:51 1207 index.html:a644ba43cdb987288d646c5a97b1c8a9 227 | 2015-09-27 07:20:27 1207 index.html:61cfff627b79058277e604686197bbbd 228 | 2015-09-27 07:19:11 1207 index.html:9dd26dbc8f3f9a8a342d067335315a63 229 | ``` 230 | 231 | Activating a revision would copy the content of the passed revision to `index.html` which is used to host your ember application via the [static web hosting](https://docs.aws.amazon.com/AmazonS3/latest/dev/WebsiteHosting.html) feature built into S3. 232 | 233 | ```bash 234 | $ ember deploy:activate --revision a644ba43cdb987288d646c5a97b1c8a9 235 | ``` 236 | 237 | ### When does activation occur? 238 | 239 | Activation occurs during the `activate` hook of the pipeline. By default, activation is turned off and must be explicitly enabled by one of the 3 methods above. 240 | 241 | ## Prerequisites 242 | 243 | The following properties are expected to be present on the deployment `context` object: 244 | 245 | - `distDir` (provided by [ember-cli-deploy-build][4]) 246 | - `project.name()` (provided by [ember-cli-deploy][5]) 247 | - `revisionKey` (provided by [ember-cli-deploy-revision-data][6]) 248 | - `commandLineArgs.revisionKey` (provided by [ember-cli-deploy][5]) 249 | - `deployEnvironment` (provided by [ember-cli-deploy][5]) 250 | 251 | # Configuring Amazon S3 252 | 253 | ## Minimum S3 Permissions 254 | 255 | This plugin requires the following permissions on your Amazon S3 access policy: 256 | 257 | * For the bucket: 258 | * s3:ListBucket 259 | * For the files on the bucket: 260 | * s3:GetObject 261 | * s3:PutObject 262 | * s3:PutObjectAcl 263 | 264 | The following is an example policy that meets these requirements: 265 | 266 | { 267 | "Statement": [ 268 | { 269 | "Sid": "Stmt1EmberCLIS3IndexDeployPolicy", 270 | "Effect": "Allow", 271 | "Action": [ 272 | "s3:GetObject", 273 | "s3:PutObject", 274 | "s3:PutObjectAcl", 275 | "s3:ListBucket" 276 | ], 277 | "Resource": [ 278 | "arn:aws:s3:::", 279 | "arn:aws:s3:::/*" 280 | ] 281 | } 282 | ] 283 | } 284 | 285 | 286 | ## Using History-Location 287 | You can deploy your Ember application to S3 and still use the history-api for pretty URLs. This needs some configuration and the exact process depends on whether or not you are using Cloudfront to serve cached content from your S3 bucket or if you are serving from an S3 bucket directly using S3's Static Website Hosting option. Both options work, however, the Cloudfront method allows the process to occur without flashing a non-pretty URL in the browser before the application loads. 288 | 289 | ### With Cloudfront 290 | A Cloudfront Custom Error Response can handle catching the 404 error that occurs when a request is made to a pretty URL and can allow that request to be handled by index.html and in turn Ember. 291 | 292 | A Custom Error Response can be created for your CloudFront distrubution in the AWS console by navigating to: 293 | 294 | Cloudfront > `Distribution ID` > Error Pages > Create Custom Error Response. 295 | 296 | You will want to use the following values. 297 | 298 | * HTTP Error Code: `404: Not Found` 299 | * Customized Error Response: `Yes` 300 | * Response Page Path: `/index.html` 301 | * HTTP Response Code: `200: OK` 302 | 303 | ### Without Cloudfront 304 | 305 | 306 |
307 | **WARNING: ⚠️ This solution might not work in all browsers. See [this issue](https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index/issues/68) for details. 308 |
309 | 310 | From within the Static Website Hosting options for your S3 bucket, you can use S3's `Redirection Rules`-feature to redirect user's to the correct route based on the URL they are requesting from your app: 311 | 312 | ``` 313 | 314 | 315 | 316 | 403 317 | 318 | 319 | 320 | #/ 321 | 322 | 323 | 324 | ``` 325 | 326 | ## Previewing Revisions 327 | If you'd like to be able to preview a deployed revision before activation, you'll need some additional setup. See [this article](http://blog.firstiwaslike.com/previewing-revisions-with-ember-cli-deploy-s3-index/) for more information. 328 | 329 | ## Running Tests 330 | 331 | - `npm test` 332 | 333 | [1]: https://github.com/lukemelia/ember-cli-deploy-lightning-pack "ember-cli-deploy-lightning-pack" 334 | [2]: http://ember-cli.github.io/ember-cli-deploy/plugins "Plugin Documentation" 335 | [3]: https://www.npmjs.com/package/redis "Redis Client" 336 | [4]: https://github.com/ember-cli-deploy/ember-cli-deploy-build "ember-cli-deploy-build" 337 | [5]: https://github.com/ember-cli/ember-cli-deploy "ember-cli-deploy" 338 | [6]: https://github.com/ember-cli-deploy/ember-cli-deploy-revision-data "ember-cli-deploy-revision-data" 339 | [7]: https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-shared.html "Setting AWS Credentials" 340 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | Releases are mostly automated using 4 | [release-it](https://github.com/release-it/release-it/) and 5 | [lerna-changelog](https://github.com/lerna/lerna-changelog/). 6 | 7 | ## Preparation 8 | 9 | Since the majority of the actual release process is automated, the primary 10 | remaining task prior to releasing is confirming that all pull requests that 11 | have been merged since the last release have been labeled with the appropriate 12 | `lerna-changelog` labels and the titles have been updated to ensure they 13 | represent something that would make sense to our users. Some great information 14 | on why this is important can be found at 15 | [keepachangelog.com](https://keepachangelog.com/en/1.0.0/), but the overall 16 | guiding principle here is that changelogs are for humans, not machines. 17 | 18 | When reviewing merged PR's the labels to be used are: 19 | 20 | * breaking - Used when the PR is considered a breaking change. 21 | * enhancement - Used when the PR adds a new feature or enhancement. 22 | * bug - Used when the PR fixes a bug included in a previous release. 23 | * documentation - Used when the PR adds or updates documentation. 24 | * internal - Used for internal changes that still require a mention in the 25 | changelog/release notes. 26 | 27 | ## Release 28 | 29 | Once the prep work is completed, the actual release is straight forward: 30 | 31 | * First, ensure that you have installed your projects dependencies: 32 | 33 | ```sh 34 | yarn install 35 | ``` 36 | 37 | * Second, ensure that you have obtained a 38 | [GitHub personal access token][generate-token] with the `repo` scope (no 39 | other permissions are needed). Make sure the token is available as the 40 | `GITHUB_AUTH` environment variable. 41 | 42 | For instance: 43 | 44 | ```bash 45 | export GITHUB_AUTH=abc123def456 46 | ``` 47 | 48 | [generate-token]: https://github.com/settings/tokens/new?scopes=repo&description=GITHUB_AUTH+env+variable 49 | 50 | * And last (but not least 😁) do your release. 51 | 52 | ```sh 53 | npx release-it 54 | ``` 55 | 56 | [release-it](https://github.com/release-it/release-it/) manages the actual 57 | release process. It will prompt you to to choose the version number after which 58 | you will have the chance to hand tweak the changelog to be used (for the 59 | `CHANGELOG.md` and GitHub release), then `release-it` continues on to tagging, 60 | pushing the tag and commits, etc. 61 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # http://www.appveyor.com/docs/appveyor-yml 2 | 3 | # Fix line endings in Windows. (runs before repo cloning) 4 | init: 5 | - git config --global core.autorclf true 6 | 7 | # Test against these versions of Node.js. 8 | environment: 9 | matrix: 10 | - nodejs_version: "4.5" 11 | - nodejs_version: "6.10" 12 | 13 | cache: 14 | - "%LOCALAPPDATA%\\Yarn" 15 | 16 | # Install scripts. (runs after repo cloning) 17 | install: 18 | # Get the latest stable version of Node.js or io.js 19 | - ps: Install-Product node $env:nodejs_version 20 | # install modules 21 | - md C:\nc 22 | - yarn install 23 | 24 | # Post-install test scripts. 25 | test_script: 26 | # Output useful info for debugging. 27 | - node --version 28 | - yarn --version 29 | # run tests 30 | - yarn test 31 | 32 | # Don't actually build. 33 | build: off 34 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | var DeployPluginBase = require('ember-cli-deploy-plugin'); 4 | var S3 = require('./lib/s3'); 5 | var joinUriSegments = require('./lib/util/join-uri-segments'); 6 | 7 | module.exports = { 8 | name: 'ember-cli-deploy-s3-index', 9 | 10 | createDeployPlugin: function(options) { 11 | var DeployPlugin = DeployPluginBase.extend({ 12 | name: options.name, 13 | S3: options.S3 || S3, 14 | 15 | defaultConfig: { 16 | filePattern: 'index.html', 17 | prefix: '', 18 | acl: 'public-read', 19 | cacheControl: 'max-age=0, no-cache', 20 | urlEncodeSourceObject: true, 21 | distDir: function(context) { 22 | return context.distDir; 23 | }, 24 | revisionKey: function(context) { 25 | var revisionKey = context.revisionData && context.revisionData.revisionKey; 26 | return context.commandOptions.revision || revisionKey; 27 | }, 28 | s3Client: function(context) { 29 | return context.s3Client; // if you want to provide your own S3 client to be used instead of one from aws-sdk 30 | }, 31 | gzippedFiles: function(context) { 32 | return context.gzippedFiles || []; 33 | }, 34 | brotliCompressedFiles: function(context) { 35 | return context.brotliCompressedFiles || []; 36 | }, 37 | didDeployMessage: function(context){ 38 | var revisionKey = context.revisionData && context.revisionData.revisionKey; 39 | var activatedRevisionKey = context.revisionData && context.revisionData.activatedRevisionKey; 40 | if (revisionKey && !activatedRevisionKey) { 41 | return "Deployed but did not activate revision " + revisionKey + ". " 42 | + "To activate, run: " 43 | + "ember deploy:activate " + context.deployTarget + " --revision=" + revisionKey + "\n"; 44 | } 45 | }, 46 | allowOverwrite: false 47 | }, 48 | 49 | requiredConfig: ['bucket', 'region'], 50 | 51 | upload: function(/* context */) { 52 | var bucket = this.readConfig('bucket'); 53 | var prefix = this.readConfig('prefix'); 54 | var acl = this.readConfig('acl'); 55 | var cacheControl = this.readConfig('cacheControl'); 56 | var revisionKey = this.readConfig('revisionKey'); 57 | var distDir = this.readConfig('distDir'); 58 | var filePattern = this.readConfig('filePattern'); 59 | var gzippedFiles = this.readConfig('gzippedFiles'); 60 | var brotliCompressedFiles = this.readConfig('brotliCompressedFiles'); 61 | var allowOverwrite = this.readConfig('allowOverwrite'); 62 | var serverSideEncryption = this.readConfig('serverSideEncryption'); 63 | var urlEncodeSourceObject = this.readConfig('urlEncodeSourceObject'); 64 | var filePath = joinUriSegments(distDir, filePattern); 65 | 66 | var options = { 67 | bucket: bucket, 68 | prefix: prefix, 69 | cacheControl: cacheControl, 70 | filePattern: filePattern, 71 | filePath: filePath, 72 | revisionKey: revisionKey, 73 | gzippedFilePaths: gzippedFiles, 74 | brotliCompressedFilePaths: brotliCompressedFiles, 75 | allowOverwrite: allowOverwrite, 76 | urlEncodeSourceObject: urlEncodeSourceObject 77 | }; 78 | 79 | if (acl) { 80 | options.acl = acl; 81 | } 82 | 83 | if (serverSideEncryption) { 84 | options.serverSideEncryption = serverSideEncryption; 85 | } 86 | 87 | this.log('preparing to upload revision to S3 bucket `' + bucket + '`', { verbose: true }); 88 | 89 | var s3 = new this.S3({ plugin: this }); 90 | return s3.upload(options); 91 | }, 92 | 93 | activate: function(/* context */) { 94 | var bucket = this.readConfig('bucket'); 95 | var prefix = this.readConfig('prefix'); 96 | var acl = this.readConfig('acl'); 97 | var revisionKey = this.readConfig('revisionKey'); 98 | var filePattern = this.readConfig('filePattern'); 99 | var serverSideEncryption = this.readConfig('serverSideEncryption'); 100 | var urlEncodeSourceObject = this.readConfig('urlEncodeSourceObject'); 101 | 102 | var options = { 103 | bucket: bucket, 104 | prefix: prefix, 105 | filePattern: filePattern, 106 | revisionKey: revisionKey, 107 | urlEncodeSourceObject: urlEncodeSourceObject, 108 | }; 109 | 110 | if (acl) { 111 | options.acl = acl; 112 | } 113 | 114 | if (serverSideEncryption) { 115 | options.serverSideEncryption = serverSideEncryption; 116 | } 117 | 118 | this.log('preparing to activate `' + revisionKey + '`', { verbose: true }); 119 | 120 | var s3 = new this.S3({ plugin: this }); 121 | return s3.activate(options).then(() => { 122 | this.log(`✔ Activated revision \`${revisionKey}\``, {}); 123 | 124 | return { 125 | revisionData: { 126 | activatedRevisionKey: revisionKey 127 | } 128 | } 129 | }); 130 | }, 131 | 132 | didDeploy: function(/* context */){ 133 | var didDeployMessage = this.readConfig('didDeployMessage'); 134 | if (didDeployMessage) { 135 | this.log(didDeployMessage); 136 | } 137 | }, 138 | 139 | fetchRevisions: function(context) { 140 | return this._list(context) 141 | .then(function(revisions) { 142 | return { 143 | revisions: revisions 144 | }; 145 | }); 146 | }, 147 | 148 | fetchInitialRevisions: function(context) { 149 | return this._list(context) 150 | .then(function(revisions) { 151 | return { 152 | initialRevisions: revisions 153 | }; 154 | }); 155 | }, 156 | 157 | _list: function(/* context */) { 158 | var bucket = this.readConfig('bucket'); 159 | var prefix = this.readConfig('prefix'); 160 | var filePattern = this.readConfig('filePattern'); 161 | 162 | var options = { 163 | bucket: bucket, 164 | prefix: prefix, 165 | filePattern: filePattern, 166 | }; 167 | 168 | var s3 = new this.S3({ plugin: this }); 169 | return s3.fetchRevisions(options); 170 | } 171 | }); 172 | 173 | return new DeployPlugin(); 174 | } 175 | }; 176 | -------------------------------------------------------------------------------- /lib/s3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var { fromIni } = require('@aws-sdk/credential-providers'); 4 | var { S3 } = require('@aws-sdk/client-s3'); 5 | var CoreObject = require('core-object'); 6 | var RSVP = require('rsvp'); 7 | var fs = require('fs'); 8 | var readFile = RSVP.denodeify(fs.readFile); 9 | var mime = require('mime-types'); 10 | var joinUriSegments = require('./util/join-uri-segments'); 11 | 12 | async function headObject(client, params) { 13 | try { 14 | return await client.headObject(params); 15 | } catch (err) { 16 | if (err.name === 'NotFound') { 17 | return; 18 | } 19 | throw err; 20 | } 21 | } 22 | 23 | module.exports = CoreObject.extend({ 24 | init: function(options) { 25 | this._super(); 26 | var plugin = options.plugin; 27 | var config = plugin.pluginConfig; 28 | var profile = plugin.readConfig('profile'); 29 | var endpoint = plugin.readConfig('endpoint'); 30 | var credentials; 31 | 32 | this._plugin = plugin; 33 | 34 | var providedS3Client = plugin.readConfig("s3Client"); 35 | 36 | 37 | if (profile && !providedS3Client) { 38 | this._plugin.log("Using AWS profile from config", { verbose: true }); 39 | credentials = fromIni({ profile: profile }); 40 | } 41 | 42 | if (endpoint) { 43 | this._plugin.log('Using endpoint from config', { verbose: true }); 44 | } 45 | 46 | this._client = providedS3Client || new S3(config); 47 | 48 | if (endpoint) { 49 | this._client.config.endpoint = endpoint; 50 | } 51 | if (credentials) { 52 | this._client.config.credentials = credentials; 53 | } 54 | 55 | }, 56 | 57 | upload: function(options) { 58 | var client = this._client; 59 | var plugin = this._plugin; 60 | var bucket = options.bucket; 61 | var acl = options.acl; 62 | var cacheControl = options.cacheControl; 63 | var allowOverwrite = options.allowOverwrite; 64 | var key = options.filePattern + ":" + options.revisionKey; 65 | var revisionKey = joinUriSegments(options.prefix, key); 66 | var putObject = RSVP.denodeify(client.putObject.bind(client)); 67 | var gzippedFilePaths = options.gzippedFilePaths || []; 68 | var brotliCompressedFilePaths = options.brotliCompressedFilePaths || []; 69 | var isGzipped = gzippedFilePaths.indexOf(options.filePattern) !== -1; 70 | var isBrotliCompressed = brotliCompressedFilePaths.indexOf(options.filePattern) !== -1; 71 | var serverSideEncryption = options.serverSideEncryption; 72 | var checkForOverwrite = RSVP.resolve(); 73 | 74 | var params = { 75 | Bucket: bucket, 76 | Key: revisionKey, 77 | ACL: acl, 78 | ContentType: mime.lookup(options.filePath) || 'text/html', 79 | CacheControl: cacheControl 80 | }; 81 | 82 | if (serverSideEncryption) { 83 | params.ServerSideEncryption = serverSideEncryption; 84 | } 85 | 86 | if (isGzipped) { 87 | params.ContentEncoding = 'gzip'; 88 | } 89 | 90 | if (isBrotliCompressed) { 91 | params.ContentEncoding = 'br'; 92 | } 93 | 94 | if (!allowOverwrite) { 95 | checkForOverwrite = this.findRevision(options) 96 | .then(function(found) { 97 | if (found !== undefined) { 98 | return RSVP.reject("REVISION ALREADY UPLOADED! (set `allowOverwrite: true` if you want to support overwriting revisions)"); 99 | } 100 | return RSVP.resolve(); 101 | }) 102 | } 103 | 104 | return checkForOverwrite 105 | .then(readFile.bind(this, options.filePath)) 106 | .then(function(fileContents) { 107 | params.Body = fileContents; 108 | return putObject(params).then(function() { 109 | plugin.log('✔ ' + revisionKey, { verbose: true }); 110 | }); 111 | }); 112 | }, 113 | 114 | activate: function(options) { 115 | var plugin = this._plugin; 116 | var client = this._client; 117 | var bucket = options.bucket; 118 | var acl = options.acl; 119 | var prefix = options.prefix; 120 | var filePattern = options.filePattern; 121 | var key = filePattern + ":" + options.revisionKey; 122 | var serverSideEncryption = options.serverSideEncryption; 123 | var urlEncodeSourceObject = options.urlEncodeSourceObject; 124 | 125 | var revisionKey = joinUriSegments(prefix, key); 126 | var indexKey = joinUriSegments(prefix, filePattern); 127 | var copyObject = RSVP.denodeify(client.copyObject.bind(client)); 128 | 129 | var params = { 130 | Bucket: bucket, 131 | Key: indexKey, 132 | ACL: acl, 133 | }; 134 | 135 | if (urlEncodeSourceObject) { 136 | params.CopySource = encodeURIComponent([bucket, revisionKey].join('/')); 137 | } else { 138 | params.CopySource = `${bucket}/${revisionKey}` 139 | } 140 | 141 | if (serverSideEncryption) { 142 | params.ServerSideEncryption = serverSideEncryption; 143 | } 144 | 145 | return this.findRevision(options).then(function(found) { 146 | if (found !== undefined) { 147 | return copyObject(params).then(function() { 148 | plugin.log('✔ ' + revisionKey + " => " + indexKey); 149 | }); 150 | } else { 151 | return RSVP.reject("REVISION NOT FOUND!"); // see how we should handle a pipeline failure 152 | } 153 | }); 154 | }, 155 | 156 | findRevision: function(options) { 157 | var client = this._client; 158 | var listObjects = RSVP.denodeify(client.listObjects.bind(client)); 159 | var bucket = options.bucket; 160 | var prefix = options.prefix; 161 | var revisionPrefix = joinUriSegments(prefix, options.filePattern + ":" + options.revisionKey); 162 | 163 | return listObjects({ Bucket: bucket, Prefix: revisionPrefix }) 164 | .then((response) => response.Contents?.find((element) => element.Key === revisionPrefix)); 165 | }, 166 | 167 | fetchRevisions: function(options) { 168 | var client = this._client; 169 | var bucket = options.bucket; 170 | var prefix = options.prefix; 171 | var revisionPrefix = joinUriSegments(prefix, options.filePattern + ":"); 172 | var indexKey = joinUriSegments(prefix, options.filePattern); 173 | 174 | return RSVP.hash({ 175 | revisions: this.listAllObjects({ Bucket: bucket, Prefix: revisionPrefix }), 176 | current: headObject(client, { Bucket: bucket, Key: indexKey }), 177 | }) 178 | .then(function(data) { 179 | return data.revisions.sort(function(a, b) { 180 | return new Date(b.LastModified) - new Date(a.LastModified); 181 | }).map(function(d) { 182 | var revision = d.Key.substr(revisionPrefix.length); 183 | var active = data.current && d.ETag === data.current.ETag; 184 | return { revision: revision, timestamp: d.LastModified, active: active }; 185 | }); 186 | }); 187 | }, 188 | 189 | listAllObjects: function(options) { 190 | var client = this._client; 191 | var listObjects = RSVP.denodeify(client.listObjects.bind(client)); 192 | var allRevisions = []; 193 | 194 | function listObjectRecursively(options) { 195 | return listObjects(options).then(function(response) { 196 | [].push.apply(allRevisions, response.Contents); 197 | 198 | if (response.IsTruncated) { 199 | var nextMarker = response.Contents[response.Contents.length - 1].Key; 200 | options.Marker = nextMarker; 201 | return listObjectRecursively(options); 202 | } else { 203 | return allRevisions; 204 | } 205 | }); 206 | } 207 | 208 | return listObjectRecursively(options); 209 | 210 | } 211 | }); 212 | -------------------------------------------------------------------------------- /lib/util/join-uri-segments.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | function joinUriSegments(prefix, uri) { 3 | return prefix === '' ? uri : [prefix, uri].join('/'); 4 | } 5 | 6 | module.exports = joinUriSegments; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-cli-deploy-s3-index", 3 | "version": "4.0.3", 4 | "description": "Ember CLI Deploy plugin to deploy ember-cli's bootstrap index file to S3.", 5 | "keywords": [ 6 | "ember-addon", 7 | "ember-cli-deploy-plugin" 8 | ], 9 | "repository": "https://github.com/ember-cli-deploy/ember-cli-deploy-s3-index", 10 | "license": "MIT", 11 | "author": "", 12 | "directories": { 13 | "doc": "doc", 14 | "test": "tests" 15 | }, 16 | "scripts": { 17 | "release": "release-it", 18 | "test": "node tests/runner.js && ./node_modules/.bin/eslint index.js lib/* tests/**/*.js" 19 | }, 20 | "dependencies": { 21 | "@aws-sdk/client-s3": "^3.385.0", 22 | "@aws-sdk/credential-providers": "^3.391.0", 23 | "core-object": "^3.0.0", 24 | "ember-cli-deploy-plugin": "^0.2.9", 25 | "mime-types": "^2.1.27", 26 | "rsvp": "^4.8.5" 27 | }, 28 | "devDependencies": { 29 | "chai": "^4.2.0", 30 | "chai-as-promised": "^7.1.1", 31 | "ember-cli": "~3.18.0", 32 | "eslint": "^7.0.0", 33 | "glob": "^7.1.6", 34 | "mocha": "^7.1.2", 35 | "release-it": "^14.2.1", 36 | "release-it-lerna-changelog": "^3.1.0" 37 | }, 38 | "engines": { 39 | "node": "14.* || 16.* || 18.* || >= 20" 40 | }, 41 | "publishConfig": { 42 | "registry": "https://registry.npmjs.org" 43 | }, 44 | "release-it": { 45 | "git": { 46 | "requireCleanWorkingDir": false 47 | }, 48 | "github": { 49 | "release": true, 50 | "tokenRef": "GITHUB_AUTH" 51 | }, 52 | "plugins": { 53 | "release-it-lerna-changelog": { 54 | "infile": "CHANGELOG.md", 55 | "launchEditor": true 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | "describe": true, 4 | "beforeEach": true, 5 | "it": true 6 | }, 7 | env: { 8 | mocha: true 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /tests/helpers/assert.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | var chaiAsPromised = require('chai-as-promised'); 5 | 6 | chai.use(chaiAsPromised); 7 | 8 | module.exports = chai.assert; 9 | -------------------------------------------------------------------------------- /tests/runner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var glob = require('glob'); 4 | var Mocha = require('mocha'); 5 | 6 | var mocha = new Mocha({ 7 | reporter: 'spec' 8 | }); 9 | 10 | var arg = process.argv[2]; 11 | var root = 'tests/'; 12 | 13 | function addFiles(mocha, files) { 14 | glob.sync(root + files).forEach(mocha.addFile.bind(mocha)); 15 | } 16 | 17 | addFiles(mocha, '/**/*-test.js'); 18 | 19 | if (arg === 'all') { 20 | addFiles(mocha, '/**/*-test-slow.js'); 21 | } 22 | 23 | mocha.run(function(failures) { 24 | process.on('exit', function() { 25 | process.exit(failures); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-cli-deploy/ember-cli-deploy-s3-index/e32e3e3df644efc3f3ef31623b37af16d0f1cc34/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tests/unit/fixtures/test.html: -------------------------------------------------------------------------------- 1 |

Welcome to Ember!

2 | -------------------------------------------------------------------------------- /tests/unit/fixtures/test.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-cli-deploy/ember-cli-deploy-s3-index/e32e3e3df644efc3f3ef31623b37af16d0f1cc34/tests/unit/fixtures/test.tar -------------------------------------------------------------------------------- /tests/unit/index-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var CoreObject = require('core-object'); 4 | var RSVP = require('rsvp'); 5 | var assert = require('./../helpers/assert'); 6 | 7 | var stubProject = { 8 | name: function(){ 9 | return 'my-project'; 10 | } 11 | }; 12 | 13 | describe('s3-index plugin', function() { 14 | var subject, mockUi, context, MockS3, plugin, s3Options, REVISIONS_DATA; 15 | 16 | var DIST_DIR = 'tmp/dist'; 17 | var BUCKET = 'bucket'; 18 | var REGION = 'eu-west-1'; 19 | var REVISION_KEY = 'revision-key'; 20 | var DEFAULT_PREFIX = ''; 21 | var DEFAULT_FILE_PATTERN = 'index.html'; 22 | var DEFAULT_ACL = 'public-read'; 23 | var DEFAULT_CACHE_CONTROL = 'max-age=0, no-cache'; 24 | 25 | function s3Stub(returnValue) { 26 | return function(options) { 27 | s3Options = options; 28 | return RSVP.resolve(returnValue); 29 | }; 30 | } 31 | 32 | before(function() { 33 | subject = require('../../index'); 34 | }); 35 | 36 | beforeEach(function() { 37 | mockUi = { 38 | verbose: true, 39 | messages: [], 40 | write: function() { }, 41 | writeLine: function(message) { 42 | this.messages.push(message); 43 | } 44 | }; 45 | 46 | REVISIONS_DATA = [{ 47 | revision: 'a', 48 | active: false 49 | }]; 50 | 51 | MockS3 = CoreObject.extend({ 52 | upload: s3Stub(), 53 | activate: s3Stub(), 54 | fetchRevisions: s3Stub(REVISIONS_DATA) 55 | }); 56 | 57 | 58 | plugin = subject.createDeployPlugin({ 59 | name: 's3-index', 60 | S3: MockS3 61 | }); 62 | 63 | context = { 64 | deployTarget: "qa", 65 | ui: mockUi, 66 | 67 | project: stubProject, 68 | 69 | commandOptions: {}, 70 | 71 | distDir: DIST_DIR, 72 | 73 | revisionData: { 74 | revisionKey: REVISION_KEY 75 | }, 76 | 77 | config: { 78 | 's3-index': { 79 | prefix: DEFAULT_PREFIX, 80 | filePattern: DEFAULT_FILE_PATTERN, 81 | bucket: BUCKET, 82 | region: REGION 83 | } 84 | } 85 | }; 86 | }); 87 | 88 | it('has a name', function() { 89 | assert.equal(plugin.name, 's3-index'); 90 | }); 91 | 92 | 93 | describe('hooks', function() { 94 | beforeEach(function() { 95 | plugin.beforeHook(context); 96 | plugin.configure(context); 97 | }); 98 | 99 | it('implements the correct hooks', function() { 100 | assert.ok(plugin.configure); 101 | assert.ok(plugin.upload); 102 | assert.ok(plugin.activate); 103 | assert.ok(plugin.fetchRevisions); 104 | assert.ok(plugin.fetchInitialRevisions); 105 | }); 106 | 107 | describe('#upload', function() { 108 | it('passes the correct options to the S3-abstraction', function() { 109 | var promise = plugin.upload(context); 110 | 111 | return assert.isFulfilled(promise) 112 | .then(function() { 113 | var expected = { 114 | acl: DEFAULT_ACL, 115 | cacheControl: DEFAULT_CACHE_CONTROL, 116 | bucket: BUCKET, 117 | prefix: DEFAULT_PREFIX, 118 | filePattern: DEFAULT_FILE_PATTERN, 119 | filePath: DIST_DIR+'/'+DEFAULT_FILE_PATTERN, 120 | gzippedFilePaths: [], 121 | brotliCompressedFilePaths: [], 122 | revisionKey: REVISION_KEY, 123 | allowOverwrite: false, 124 | urlEncodeSourceObject: true 125 | }; 126 | 127 | assert.deepEqual(s3Options, expected); 128 | }); 129 | }); 130 | 131 | it('detects serverSideEncryption when defined', function() { 132 | context.config['s3-index'].serverSideEncryption = 'AES256'; 133 | var promise = plugin.upload(context); 134 | 135 | return assert.isFulfilled(promise) 136 | .then(function() { 137 | assert.equal(s3Options.serverSideEncryption, 'AES256', 'serverSideEncryption passed correctly'); 138 | }); 139 | }); 140 | 141 | it('filters serverSideEncryption when not defined', function() { 142 | var promise = plugin.upload(context); 143 | 144 | return assert.isFulfilled(promise) 145 | .then(function() { 146 | assert.equal(Object.prototype.hasOwnProperty.call(s3Options, 'serverSideEncryption'), false, 'serverSideEncryption filtered correctly'); 147 | }); 148 | }); 149 | 150 | it('filters acl when not defined', function() { 151 | context.config['s3-index'].acl = false; 152 | var promise = plugin.upload(context); 153 | 154 | return assert.isFulfilled(promise) 155 | .then(function() { 156 | assert.equal(Object.prototype.hasOwnProperty.call(s3Options, 'acl'), false, 'acl filtered correctly'); 157 | }); 158 | }); 159 | 160 | it('passes cacheControl options based on the cacheControl option to the s3-abstraction', function() { 161 | var cacheControl = 'max-age=3600'; 162 | context.config['s3-index'].cacheControl = cacheControl; 163 | 164 | var promise = plugin.upload(context); 165 | 166 | return assert.isFulfilled(promise) 167 | .then(function() { 168 | var expected = { 169 | acl: DEFAULT_ACL, 170 | cacheControl: cacheControl, 171 | bucket: BUCKET, 172 | prefix: DEFAULT_PREFIX, 173 | filePattern: DEFAULT_FILE_PATTERN, 174 | filePath: DIST_DIR+'/'+DEFAULT_FILE_PATTERN, 175 | gzippedFilePaths: [], 176 | brotliCompressedFilePaths: [], 177 | revisionKey: REVISION_KEY, 178 | allowOverwrite: false, 179 | urlEncodeSourceObject: true 180 | }; 181 | 182 | assert.deepEqual(s3Options, expected); 183 | }); 184 | }); 185 | 186 | it('passes gzippedFilePaths to S3 based on the `context.gzippedFiles` that ember-cli-deploy-gzip provides', function() { 187 | context.gzippedFiles = ['index.html']; 188 | 189 | var promise = plugin.upload(context); 190 | 191 | return assert.isFulfilled(promise) 192 | .then(function() { 193 | var expected = { 194 | acl: DEFAULT_ACL, 195 | cacheControl: DEFAULT_CACHE_CONTROL, 196 | bucket: BUCKET, 197 | prefix: DEFAULT_PREFIX, 198 | filePattern: DEFAULT_FILE_PATTERN, 199 | filePath: DIST_DIR+'/'+DEFAULT_FILE_PATTERN, 200 | gzippedFilePaths: ['index.html'], 201 | brotliCompressedFilePaths: [], 202 | revisionKey: REVISION_KEY, 203 | allowOverwrite: false, 204 | urlEncodeSourceObject: true 205 | }; 206 | 207 | assert.deepEqual(s3Options, expected); 208 | }); 209 | }); 210 | 211 | it('passes brotliCompressedFilePaths to S3 based on the `context.brotliCompressedFiles` that ember-cli-deploy-compress provides', function() { 212 | context.brotliCompressedFiles = ['index.html']; 213 | 214 | var promise = plugin.upload(context); 215 | 216 | return assert.isFulfilled(promise) 217 | .then(function() { 218 | var expected = { 219 | acl: DEFAULT_ACL, 220 | cacheControl: DEFAULT_CACHE_CONTROL, 221 | bucket: BUCKET, 222 | prefix: DEFAULT_PREFIX, 223 | filePattern: DEFAULT_FILE_PATTERN, 224 | filePath: DIST_DIR+'/'+DEFAULT_FILE_PATTERN, 225 | gzippedFilePaths: [], 226 | brotliCompressedFilePaths: ['index.html'], 227 | revisionKey: REVISION_KEY, 228 | allowOverwrite: false, 229 | urlEncodeSourceObject: true 230 | }; 231 | 232 | assert.deepEqual(s3Options, expected); 233 | }); 234 | }); 235 | }); 236 | 237 | describe('#activate', function() { 238 | it('passes the correct options to the S3-abstraction', function() { 239 | context.commandOptions.revision = '1234'; 240 | 241 | var promise = plugin.activate(context); 242 | 243 | return assert.isFulfilled(promise) 244 | .then(function() { 245 | var expected = { 246 | acl: DEFAULT_ACL, 247 | bucket: BUCKET, 248 | prefix: DEFAULT_PREFIX, 249 | filePattern: DEFAULT_FILE_PATTERN, 250 | revisionKey: '1234', 251 | urlEncodeSourceObject: true, 252 | }; 253 | 254 | assert.deepEqual(s3Options, expected); 255 | }); 256 | }); 257 | 258 | it('detects serverSideEncryption when defined', function() { 259 | context.config['s3-index'].serverSideEncryption = 'AES256'; 260 | var promise = plugin.activate(context); 261 | 262 | return assert.isFulfilled(promise) 263 | .then(function() { 264 | assert.equal(s3Options.serverSideEncryption, 'AES256', 'serverSideEncryption passed correctly'); 265 | }); 266 | }); 267 | 268 | it('filters serverSideEncryption when not defined', function() { 269 | var promise = plugin.activate(context); 270 | 271 | return assert.isFulfilled(promise) 272 | .then(function() { 273 | assert.equal(Object.prototype.hasOwnProperty.call(s3Options, 'serverSideEncryption'), false, 'serverSideEncryption filtered correctly'); 274 | }); 275 | }); 276 | 277 | it('filters acl when not defined', function() { 278 | context.config['s3-index'].acl = false; 279 | var promise = plugin.upload(context); 280 | 281 | return assert.isFulfilled(promise) 282 | .then(function() { 283 | assert.equal(Object.prototype.hasOwnProperty.call(s3Options, 'acl'), false, 'acl filtered correctly'); 284 | }); 285 | }); 286 | 287 | it('displays activation message when revision is activated', function() { 288 | var promise = plugin.activate(context); 289 | 290 | return assert.isFulfilled(promise) 291 | .then(function() { 292 | assert.match(mockUi.messages.join("\n"), new RegExp(`✔ Activated revision \`${REVISION_KEY}\``)); 293 | }); 294 | }) 295 | }); 296 | 297 | describe('#fetchInitialRevisions', function() { 298 | it('fills the `initialRevisions`-variable on context', function() { 299 | return assert.isFulfilled(plugin.fetchInitialRevisions(context)) 300 | .then(function(result) { 301 | assert.deepEqual(result, { 302 | initialRevisions: REVISIONS_DATA 303 | }); 304 | }); 305 | }); 306 | 307 | it('passes the correct options to the S3-abstraction', function() { 308 | var promise = plugin.fetchRevisions(context); 309 | 310 | return assert.isFulfilled(promise) 311 | .then(function() { 312 | var expected = { 313 | bucket: BUCKET, 314 | prefix: DEFAULT_PREFIX, 315 | filePattern: DEFAULT_FILE_PATTERN 316 | }; 317 | 318 | assert.deepEqual(s3Options, expected); 319 | }); 320 | }); 321 | }); 322 | 323 | describe('#fetchRevisions', function() { 324 | it('fills the `revisions`-variable on context', function() { 325 | return assert.isFulfilled(plugin.fetchRevisions(context)) 326 | .then(function(result) { 327 | assert.deepEqual(result, { 328 | revisions: REVISIONS_DATA 329 | }); 330 | }); 331 | }); 332 | 333 | it('passes the correct options to the S3-abstraction', function() { 334 | var promise = plugin.fetchRevisions(context); 335 | 336 | return assert.isFulfilled(promise) 337 | .then(function() { 338 | var expected = { 339 | bucket: BUCKET, 340 | prefix: DEFAULT_PREFIX, 341 | filePattern: DEFAULT_FILE_PATTERN 342 | }; 343 | 344 | assert.deepEqual(s3Options, expected); 345 | }); 346 | }); 347 | }); 348 | 349 | describe('#didDeploy', function() { 350 | it("prints default message about lack of activation when revision has not been activated", function() { 351 | plugin.upload = function() {}; 352 | plugin.activate = function() {}; 353 | plugin.beforeHook(context); 354 | plugin.configure(context); 355 | plugin.beforeHook(context); 356 | plugin.didDeploy(context); 357 | 358 | let message = mockUi.messages.join("\n") 359 | assert.match(message, new RegExp(`Deployed but did not activate revision ${REVISION_KEY}`)); 360 | assert.match(message, /To activate, run/); 361 | assert.match(message, new RegExp(`ember deploy:activate qa --revision=${REVISION_KEY}`)); 362 | }); 363 | }); 364 | }); 365 | }); 366 | -------------------------------------------------------------------------------- /tests/unit/lib/s3-test.js: -------------------------------------------------------------------------------- 1 | var assert = require('./../../helpers/assert'); 2 | 3 | describe('s3', function() { 4 | var S3, mockUi, s3Client, plugin, subject, options, listParams, headParams, copyParams, revisionsData, currentData; 5 | 6 | before(function() { 7 | S3 = require('../../../lib/s3'); 8 | }); 9 | 10 | beforeEach(function() { 11 | revisionsData = { 12 | Contents: [ 13 | { Key: 'test.html:123', LastModified: new Date('September 27, 2015 01:00:00'), ETag: '123' }, 14 | { Key: 'test.html:456', LastModified: new Date('September 27, 2015 02:00:00') , ETag: '456' } 15 | ] 16 | }; 17 | 18 | currentData = { Key: 'test.html', lastModified: new Date('September 27, 2015 01:30:00'), ETag: '123' }; 19 | 20 | s3Client = { 21 | putObject: function(params, cb) { 22 | cb(); 23 | }, 24 | 25 | listObjects: function(params, cb) { 26 | listParams = params; 27 | cb(undefined, revisionsData); 28 | }, 29 | 30 | headObject: async function(params, cb) { 31 | headParams = params; 32 | return currentData; 33 | }, 34 | 35 | copyObject: function(params, cb) { 36 | copyParams = params; 37 | cb(); 38 | }, 39 | config: {} 40 | }; 41 | mockUi = { 42 | messages: [], 43 | write: function() {}, 44 | writeLine: function(message) { 45 | this.messages.push(message); 46 | } 47 | }; 48 | plugin = { 49 | ui: mockUi, 50 | readConfig: function(propertyName) { 51 | if (propertyName === 's3Client') { 52 | return s3Client; 53 | } 54 | }, 55 | log: function(message/*, opts */) { 56 | this.ui.write('| '); 57 | this.ui.writeLine('- ' + message); 58 | } 59 | }; 60 | subject = new S3({ 61 | plugin: plugin 62 | }); 63 | }); 64 | 65 | describe('#upload', function() { 66 | var s3Params; 67 | var filePattern = 'test.html'; 68 | var revisionKey = 'some-revision-key'; 69 | var bucket = 'some-bucket'; 70 | 71 | beforeEach(function() { 72 | options = { 73 | bucket: bucket, 74 | prefix: '', 75 | acl: 'public-read', 76 | cacheControl: 'max-age=0, no-cache', 77 | gzippedFilePaths: [], 78 | filePattern: filePattern, 79 | revisionKey: revisionKey, 80 | filePath: 'tests/unit/fixtures/test.html', 81 | allowOverwrite: false, 82 | urlEncodeSourceObject: true 83 | }; 84 | 85 | s3Client.putObject = function(params, cb) { 86 | s3Params = params; 87 | cb(); 88 | }; 89 | }); 90 | 91 | it('resolves if upload succeeds', function() { 92 | var promise = subject.upload(options); 93 | 94 | return assert.isFulfilled(promise) 95 | .then(function() { 96 | var expectLogOutput = '- ✔ '+filePattern+':'+revisionKey; 97 | 98 | assert.equal(mockUi.messages.length, 1, 'Logs one line'); 99 | assert.equal(mockUi.messages[0], expectLogOutput, 'Upload log output correct'); 100 | }); 101 | }); 102 | 103 | it('rejects if upload fails', function() { 104 | s3Client.putObject = function(params, cb) { 105 | cb('error uploading'); 106 | }; 107 | 108 | var promise = subject.upload(options); 109 | 110 | return assert.isRejected(promise); 111 | }); 112 | 113 | it('passes expected parameters to the used s3-client', function() { 114 | 115 | var promise = subject.upload(options); 116 | 117 | return assert.isFulfilled(promise) 118 | .then(function() { 119 | var expectedKey = filePattern+':'+revisionKey; 120 | var defaultACL = 'public-read'; 121 | 122 | assert.equal(s3Params.Bucket, bucket, 'Bucket passed correctly'); 123 | assert.equal(s3Params.Key, expectedKey, 'Key passed correctly'); 124 | assert.equal(s3Params.ACL, defaultACL, 'ACL defaults to `public-read`'); 125 | assert.equal(s3Params.ContentType, 'text/html', 'contentType is set to `text/html`'); 126 | assert.equal(s3Params.CacheControl, 'max-age=0, no-cache', 'cacheControl set correctly'); 127 | }); 128 | }); 129 | 130 | it('detects serverSideEncryption when defined', function() { 131 | options.serverSideEncryption = 'AES256'; 132 | var promise = subject.upload(options); 133 | 134 | return assert.isFulfilled(promise) 135 | .then(function() { 136 | assert.equal(s3Params.ServerSideEncryption, 'AES256', 'serverSideEncryption passed correctly'); 137 | }); 138 | }); 139 | 140 | it('filters serverSideEncryption when not defined', function() { 141 | var promise = subject.upload(options); 142 | 143 | return assert.isFulfilled(promise) 144 | .then(function() { 145 | assert.equal(s3Params.hasOwnProperty('serverSideEncryption'), false, 'serverSideEncryption filtered correctly'); 146 | }); 147 | }); 148 | 149 | it('detects `filePattern` other than `index.html` in order to customize ContentType', function() { 150 | var filePath = 'tests/unit/fixtures/test.tar'; 151 | 152 | options.filePath = filePath; 153 | var promise = subject.upload(options); 154 | 155 | return assert.isFulfilled(promise) 156 | .then(function() { 157 | var expectedContentType = 'application/x-tar'; 158 | assert.equal(s3Params.ContentType, expectedContentType, 'contentType is set to `application/x-tar'); 159 | }); 160 | }); 161 | 162 | it('sets the Content-Encoding header to gzip when the index file is gzipped', function() { 163 | options.gzippedFilePaths = [filePattern]; 164 | var promise = subject.upload(options); 165 | 166 | return assert.isFulfilled(promise) 167 | .then(function() { 168 | assert.equal(s3Params.ContentEncoding, 'gzip', 'contentEncoding is set to gzip'); 169 | }); 170 | }); 171 | 172 | it('sets the Content-Encoding header to br when the index file is brotli compressed', function() { 173 | options.brotliCompressedFilePaths = [filePattern]; 174 | var promise = subject.upload(options); 175 | 176 | return assert.isFulfilled(promise) 177 | .then(function() { 178 | assert.equal(s3Params.ContentEncoding, 'br', 'contentEncoding is set to br'); 179 | }); 180 | }); 181 | 182 | it('allows `prefix` option to be passed to customize upload-path', function() { 183 | var prefix = 'my-app'; 184 | 185 | options.prefix = prefix; 186 | var promise = subject.upload(options); 187 | 188 | return assert.isFulfilled(promise) 189 | .then(function() { 190 | var expectLogOutput = '- ✔ '+prefix+'/'+filePattern+':'+revisionKey; 191 | var expectedKey = prefix+'/'+filePattern+':'+revisionKey; 192 | 193 | assert.equal(mockUi.messages[0], expectLogOutput, 'Prefix is included in log output'); 194 | assert.equal(s3Params.Key, expectedKey, 'Key (including prefix) passed correctly'); 195 | }); 196 | }); 197 | 198 | it('allows `acl` option to be passed to customize the used ACL', function() { 199 | var acl = 'authenticated-read'; 200 | 201 | options.acl = acl; 202 | var promise = subject.upload(options); 203 | 204 | return assert.isFulfilled(promise) 205 | .then(function() { 206 | assert.equal(s3Params.ACL, acl, 'acl passed correctly'); 207 | }); 208 | }); 209 | 210 | it('allows `endpoint` option to be passed to customize storage', function() { 211 | var endpoint = 'foo.bar.baz'; 212 | subject = new S3({ 213 | plugin: Object.assign(plugin, { 214 | readConfig: function(propertyName) { 215 | if (propertyName === 's3Client') { 216 | return s3Client; 217 | } else if (propertyName === 'endpoint') { 218 | return endpoint; 219 | } 220 | } 221 | }) 222 | }); 223 | var promise = subject.upload(options); 224 | return assert.isFulfilled(promise) 225 | .then(function() { 226 | assert.equal(s3Client.config.endpoint, endpoint, 'Endpoint in SDK is correct'); 227 | assert.equal(mockUi.messages[0], '- Using endpoint from config', 'Prefix is included in log output'); 228 | }); 229 | }); 230 | 231 | it('allows `cacheControl` option to be passed to customize the used cache-control', function() { 232 | var cacheControl = 'max-age=3600'; 233 | 234 | options.cacheControl = cacheControl; 235 | 236 | var promise = subject.upload(options); 237 | 238 | return assert.isFulfilled(promise) 239 | .then(function() { 240 | assert.equal(s3Params.CacheControl, cacheControl, 'cache-control passed correctly'); 241 | 242 | }); 243 | }); 244 | 245 | it('succeeds when revision key search returns no values', function() { 246 | s3Client.listObjects = function(params, cb) { 247 | cb(undefined, {}); 248 | }; 249 | var promise = subject.upload(options); 250 | 251 | return assert.isFulfilled(promise); 252 | }); 253 | 254 | describe("when revisionKey was already uploaded", function() { 255 | beforeEach(function() { 256 | options.revisionKey = "123"; 257 | }); 258 | 259 | it('rejects when trying to upload an already uploaded revision', function() { 260 | var promise = subject.upload(options); 261 | 262 | return assert.isRejected(promise); 263 | }); 264 | 265 | it('does not reject when allowOverwrite option is set to true', function() { 266 | options.allowOverwrite = true; 267 | 268 | var promise = subject.upload(options); 269 | 270 | return assert.isFulfilled(promise); 271 | }); 272 | }); 273 | }); 274 | 275 | describe('#fetchRevisions', function() { 276 | var bucket = 'some-bucket'; 277 | var prefix = ''; 278 | var filePattern = 'test.html'; 279 | 280 | beforeEach(function() { 281 | options = { 282 | bucket: bucket, 283 | prefix: prefix, 284 | filePattern: filePattern 285 | }; 286 | }); 287 | 288 | it('returns an array of uploaded revisions in `{ revision: revisionKey, timestamp: timestamp, active: active }` format sorted by date in descending order', function() { 289 | var promise = subject.fetchRevisions(options); 290 | var expected = [ 291 | { revision: '456', timestamp: new Date('September 27, 2015 02:00:00') , active: false }, 292 | { revision: '123', timestamp: new Date('September 27, 2015 01:00:00'), active: true } 293 | ]; 294 | 295 | return assert.isFulfilled(promise) 296 | .then(function(revisionsData) { 297 | return assert.deepEqual(revisionsData, expected, 'Revisions data correct'); 298 | }); 299 | }); 300 | 301 | it('sends correct parameters for s3#listObjects and s3#headObject', function() { 302 | var expectePrefix = filePattern+':'; 303 | 304 | var promise = subject.fetchRevisions(options); 305 | 306 | return assert.isFulfilled(promise) 307 | .then(function() { 308 | assert.equal(listParams.Bucket, bucket, 'list Bucket set correctly'); 309 | assert.equal(listParams.Prefix, expectePrefix, 'list Prefix set correctly'); 310 | assert.equal(headParams.Bucket, bucket, 'head Bucket set correctly'); 311 | assert.equal(headParams.Key, filePattern, 'head Key set correctly'); 312 | }); 313 | }); 314 | 315 | it('changes parameters sent to s3#listObjects and s3#headObject when setting the `prefix`-option', function() { 316 | var prefix = 'my-app'; 317 | var expectedPrefix = 'my-app/'+filePattern+':'; 318 | var expectedKey = 'my-app/'+filePattern; 319 | 320 | options.prefix = prefix; 321 | var promise = subject.fetchRevisions(options); 322 | 323 | return assert.isFulfilled(promise) 324 | .then(function() { 325 | assert.equal(listParams.Prefix, expectedPrefix); 326 | assert.equal(headParams.Key, expectedKey); 327 | }); 328 | }); 329 | 330 | it('correctly pages s3#listObjects calls when necessary', function() { 331 | 332 | var revisions = [ 333 | { Key: 'test.html:111', LastModified: new Date('September 27, 2015 01:00:00'), ETag: '111' }, 334 | { Key: 'test.html:222', LastModified: new Date('September 27, 2015 02:00:00') , ETag: '222' }, 335 | { Key: 'test.html:333', LastModified: new Date('September 27, 2015 03:00:00') , ETag: '333' }, 336 | { Key: 'test.html:444', LastModified: new Date('September 27, 2015 04:00:00') , ETag: '444' }, 337 | { Key: 'test.html:555', LastModified: new Date('September 27, 2015 05:00:00') , ETag: '555' }, 338 | { Key: 'test.html:666', LastModified: new Date('September 27, 2015 06:00:00') , ETag: '666' }, 339 | { Key: 'test.html:777', LastModified: new Date('September 27, 2015 07:00:00') , ETag: '777' }, 340 | { Key: 'test.html:888', LastModified: new Date('September 27, 2015 08:00:00') , ETag: '888' }, 341 | { Key: 'test.html:999', LastModified: new Date('September 27, 2015 09:00:00') , ETag: '999' }, 342 | { Key: 'test.html:000', LastModified: new Date('September 27, 2015 10:00:00') , ETag: '000' }, 343 | ]; 344 | 345 | var resultsPerPage = 4; 346 | var listObjectCalls = []; 347 | 348 | s3Client.listObjects = function(params, cb) { 349 | listObjectCalls.push(params.Marker); 350 | var offset = 0; 351 | offset = revisions.map(function(revision) { 352 | return revision.Key; 353 | }).indexOf(params.Marker) + 1; 354 | var lastResult = offset + resultsPerPage; 355 | 356 | cb(undefined, { 357 | Contents: revisions.slice(offset, lastResult), 358 | IsTruncated: lastResult < revisions.length, 359 | }); 360 | }; 361 | 362 | var promise = subject.fetchRevisions(options); 363 | 364 | return assert.isFulfilled(promise) 365 | .then(function(revisionsData) { 366 | assert.equal(revisionsData.length, revisions.length, 'All revisions are available'); 367 | assert.equal(listObjectCalls.length, 3, 'listObjects was called 3 times'); 368 | assert.equal(listObjectCalls[0], undefined, 'the first call to listObjects had the expected marker'); 369 | assert.equal(listObjectCalls[1], 'test.html:444', 'the second call to listObjects had the expected marker'); 370 | assert.equal(listObjectCalls[2], 'test.html:888', 'the third call to listObjects had the expected marker'); 371 | }); 372 | }); 373 | }); 374 | 375 | describe('#activate', function() { 376 | var bucket = 'some-bucket'; 377 | var prefix = ''; 378 | var acl = 'public-read'; 379 | var filePattern = 'test.html'; 380 | 381 | beforeEach(function() { 382 | options = { 383 | bucket: bucket, 384 | prefix: prefix, 385 | acl: acl, 386 | filePattern: filePattern, 387 | urlEncodeSourceObject: true, 388 | }; 389 | }); 390 | 391 | describe('with a valid revisionKey', function() { 392 | beforeEach(function() { 393 | options.revisionKey = '456'; 394 | }); 395 | 396 | it('resolves when passing an existing revisionKey', function() { 397 | var promise = subject.activate(options); 398 | 399 | return assert.isFulfilled(promise); 400 | }); 401 | 402 | it('logs to the console when activation was successful', function() { 403 | var promise = subject.activate(options); 404 | var expectedOutput = '- ✔ '+filePattern+':456 => '+filePattern; 405 | 406 | return assert.isFulfilled(promise) 407 | .then(function() { 408 | assert.equal(mockUi.messages.length, 1, 'Logs one line'); 409 | assert.equal(mockUi.messages[0], expectedOutput, 'Activation output correct'); 410 | }); 411 | }); 412 | 413 | it('passes correct parameters to s3#copyObject', function() { 414 | var promise = subject.activate(options); 415 | 416 | return assert.isFulfilled(promise) 417 | .then(function() { 418 | assert.equal(copyParams.Bucket, bucket); 419 | assert.equal(copyParams.ACL, acl); 420 | assert.equal(copyParams.CopySource, bucket+'%2F'+filePattern+'%3A456'); 421 | assert.equal(copyParams.Key, filePattern); 422 | }); 423 | }); 424 | 425 | it('allows `prefix` option to be passed to customize Key and CopySource passed to s3#copyObject', function() { 426 | var prefix = 'my-app'; 427 | revisionsData = { 428 | Contents: [ 429 | { Key: prefix+'/test.html:123', LastModified: new Date('September 27, 2015 01:00:00'), ETag: '123' }, 430 | { Key: prefix+'/test.html:456', LastModified: new Date('September 27, 2015 02:00:00') , ETag: '456' } 431 | ] 432 | }; 433 | 434 | options.prefix = prefix; 435 | var promise = subject.activate(options); 436 | 437 | return assert.isFulfilled(promise) 438 | .then(function() { 439 | assert.equal(copyParams.Key, prefix+'/'+filePattern); 440 | assert.equal(copyParams.CopySource, bucket+'%2F'+prefix+'%2F'+filePattern+'%3A456'); 441 | }); 442 | }); 443 | 444 | it ('does not url encode the CopySource when urlEncodeSourceObject is disabled', function() { 445 | options.urlEncodeSourceObject = false; 446 | var prefix = 'my-app'; 447 | revisionsData = { 448 | Contents: [ 449 | { Key: prefix+'/test.html:123', LastModified: new Date('September 27, 2015 01:00:00'), ETag: '123' }, 450 | { Key: prefix+'/test.html:456', LastModified: new Date('September 27, 2015 02:00:00') , ETag: '456' } 451 | ] 452 | }; 453 | 454 | options.prefix = prefix; 455 | var promise = subject.activate(options); 456 | 457 | return assert.isFulfilled(promise) 458 | .then(function() { 459 | assert.equal(copyParams.Key, prefix+'/'+filePattern); 460 | assert.equal(copyParams.CopySource, bucket+'/'+prefix+'/'+filePattern+':456'); 461 | }); 462 | }); 463 | 464 | it('allows `acl` option to be passed to customize the used ACL', function() { 465 | var acl = 'authenticated-read'; 466 | 467 | options.acl = acl; 468 | var promise = subject.activate(options); 469 | 470 | return assert.isFulfilled(promise) 471 | .then(function() { 472 | assert.equal(copyParams.ACL, acl); 473 | }); 474 | }); 475 | 476 | it('detects serverSideEncryption when defined', function() { 477 | options.serverSideEncryption = 'AES256'; 478 | var promise = subject.activate(options); 479 | 480 | return assert.isFulfilled(promise) 481 | .then(function() { 482 | assert.equal(copyParams.ServerSideEncryption, 'AES256', 'serverSideEncryption passed correctly'); 483 | }); 484 | }); 485 | 486 | it('filters serverSideEncryption when not defined', function() { 487 | var promise = subject.activate(options); 488 | 489 | return assert.isFulfilled(promise) 490 | .then(function() { 491 | assert.equal(copyParams.hasOwnProperty('serverSideEncryption'), false, 'serverSideEncryption filtered correctly'); 492 | }); 493 | }); 494 | }); 495 | 496 | describe('with an invalid revision key', function() { 497 | beforeEach(function() { 498 | options.revisionKey = '457'; 499 | }); 500 | 501 | it('rejects when passing an non-existing revisionKey', function() { 502 | var promise = subject.activate(options); 503 | 504 | return assert.isRejected(promise); 505 | }); 506 | }); 507 | }); 508 | }); 509 | --------------------------------------------------------------------------------