s3_asset_deploy 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +14 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +1 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +72 -0
- data/LICENSE.txt +21 -0
- data/README.md +180 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/s3_asset_deploy.rb +7 -0
- data/lib/s3_asset_deploy/asset_helper.rb +19 -0
- data/lib/s3_asset_deploy/errors.rb +10 -0
- data/lib/s3_asset_deploy/local_asset.rb +37 -0
- data/lib/s3_asset_deploy/local_asset_collector.rb +29 -0
- data/lib/s3_asset_deploy/manager.rb +206 -0
- data/lib/s3_asset_deploy/rails_local_asset.rb +17 -0
- data/lib/s3_asset_deploy/rails_local_asset_collector.rb +46 -0
- data/lib/s3_asset_deploy/remote_asset.rb +37 -0
- data/lib/s3_asset_deploy/remote_asset_collector.rb +55 -0
- data/lib/s3_asset_deploy/removal_manifest.rb +99 -0
- data/lib/s3_asset_deploy/version.rb +5 -0
- data/s3_asset_deploy.gemspec +41 -0
- metadata +183 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 72c4c1220b3051d9189389b3b956d509506380ae4bef112212480e1da9ac75e4
|
4
|
+
data.tar.gz: b9fbd6e07c1cced6e5a20d1cb72338093eec260a540a434ee3cc60012e680549
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cb3a963f7a47375414e9b51b1b3f525a890924995b380d193151e9a77193a7ddec8f1f50864fc60b37c5c768586fefa76b1335d3e849918572ec4b7be3af866a
|
7
|
+
data.tar.gz: d240fe42ae5578cf136f1dac4b637165603836f8cdceb7bf8d820cd6808048d78b50b4dbbe235f3e1678aedbe8f36a6b76f1310a5d64f0605622650cca2d01b1
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Changelog
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
6
|
+
|
7
|
+
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
8
|
+
|
9
|
+
## Our Standards
|
10
|
+
|
11
|
+
Examples of behavior that contributes to a positive environment for our community include:
|
12
|
+
|
13
|
+
* Demonstrating empathy and kindness toward other people
|
14
|
+
* Being respectful of differing opinions, viewpoints, and experiences
|
15
|
+
* Giving and gracefully accepting constructive feedback
|
16
|
+
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
17
|
+
* Focusing on what is best not just for us as individuals, but for the overall community
|
18
|
+
|
19
|
+
Examples of unacceptable behavior include:
|
20
|
+
|
21
|
+
* The use of sexualized language or imagery, and sexual attention or
|
22
|
+
advances of any kind
|
23
|
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
24
|
+
* Public or private harassment
|
25
|
+
* Publishing others' private information, such as a physical or email
|
26
|
+
address, without their explicit permission
|
27
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
28
|
+
professional setting
|
29
|
+
|
30
|
+
## Enforcement Responsibilities
|
31
|
+
|
32
|
+
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
33
|
+
|
34
|
+
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
35
|
+
|
36
|
+
## Scope
|
37
|
+
|
38
|
+
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
39
|
+
|
40
|
+
## Enforcement
|
41
|
+
|
42
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at contact@loomly.com. All complaints will be reviewed and investigated promptly and fairly.
|
43
|
+
|
44
|
+
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
45
|
+
|
46
|
+
## Enforcement Guidelines
|
47
|
+
|
48
|
+
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
49
|
+
|
50
|
+
### 1. Correction
|
51
|
+
|
52
|
+
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
53
|
+
|
54
|
+
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
55
|
+
|
56
|
+
### 2. Warning
|
57
|
+
|
58
|
+
**Community Impact**: A violation through a single incident or series of actions.
|
59
|
+
|
60
|
+
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
61
|
+
|
62
|
+
### 3. Temporary Ban
|
63
|
+
|
64
|
+
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
65
|
+
|
66
|
+
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
67
|
+
|
68
|
+
### 4. Permanent Ban
|
69
|
+
|
70
|
+
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
71
|
+
|
72
|
+
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
73
|
+
|
74
|
+
## Attribution
|
75
|
+
|
76
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
|
77
|
+
available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
78
|
+
|
79
|
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
80
|
+
|
81
|
+
[homepage]: https://www.contributor-covenant.org
|
82
|
+
|
83
|
+
For answers to common questions about this code of conduct, see the FAQ at
|
84
|
+
https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
s3_asset_deploy (0.1.0)
|
5
|
+
aws-sdk-s3 (~> 1.0)
|
6
|
+
mime-types (~> 3.0)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
aws-eventstream (1.1.0)
|
12
|
+
aws-partitions (1.422.0)
|
13
|
+
aws-sdk-core (3.111.2)
|
14
|
+
aws-eventstream (~> 1, >= 1.0.2)
|
15
|
+
aws-partitions (~> 1, >= 1.239.0)
|
16
|
+
aws-sigv4 (~> 1.1)
|
17
|
+
jmespath (~> 1.0)
|
18
|
+
aws-sdk-kms (1.41.0)
|
19
|
+
aws-sdk-core (~> 3, >= 3.109.0)
|
20
|
+
aws-sigv4 (~> 1.1)
|
21
|
+
aws-sdk-s3 (1.87.0)
|
22
|
+
aws-sdk-core (~> 3, >= 3.109.0)
|
23
|
+
aws-sdk-kms (~> 1)
|
24
|
+
aws-sigv4 (~> 1.1)
|
25
|
+
aws-sigv4 (1.2.2)
|
26
|
+
aws-eventstream (~> 1, >= 1.0.2)
|
27
|
+
byebug (11.1.3)
|
28
|
+
coderay (1.1.3)
|
29
|
+
diff-lcs (1.4.4)
|
30
|
+
jmespath (1.4.0)
|
31
|
+
method_source (1.0.0)
|
32
|
+
mime-types (3.3.1)
|
33
|
+
mime-types-data (~> 3.2015)
|
34
|
+
mime-types-data (3.2020.1104)
|
35
|
+
pry (0.13.1)
|
36
|
+
coderay (~> 1.1)
|
37
|
+
method_source (~> 1.0)
|
38
|
+
pry-byebug (3.9.0)
|
39
|
+
byebug (~> 11.0)
|
40
|
+
pry (~> 0.13.0)
|
41
|
+
rake (13.0.3)
|
42
|
+
rspec (3.10.0)
|
43
|
+
rspec-core (~> 3.10.0)
|
44
|
+
rspec-expectations (~> 3.10.0)
|
45
|
+
rspec-mocks (~> 3.10.0)
|
46
|
+
rspec-core (3.10.1)
|
47
|
+
rspec-support (~> 3.10.0)
|
48
|
+
rspec-expectations (3.10.1)
|
49
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
50
|
+
rspec-support (~> 3.10.0)
|
51
|
+
rspec-mocks (3.10.2)
|
52
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
53
|
+
rspec-support (~> 3.10.0)
|
54
|
+
rspec-support (3.10.2)
|
55
|
+
rspec_junit_formatter (0.4.1)
|
56
|
+
rspec-core (>= 2, < 4, != 2.12.0)
|
57
|
+
timecop (0.9.2)
|
58
|
+
|
59
|
+
PLATFORMS
|
60
|
+
ruby
|
61
|
+
|
62
|
+
DEPENDENCIES
|
63
|
+
pry (~> 0.13)
|
64
|
+
pry-byebug (~> 3.9)
|
65
|
+
rake (~> 13.0)
|
66
|
+
rspec (~> 3.0)
|
67
|
+
rspec_junit_formatter (~> 0.4)
|
68
|
+
s3_asset_deploy!
|
69
|
+
timecop (~> 0.9)
|
70
|
+
|
71
|
+
BUNDLED WITH
|
72
|
+
2.2.7
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 Loomly
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
# S3AssetDeploy
|
2
|
+
|
3
|
+
[![CircleCI](https://circleci.com/gh/Loomly/s3_asset_deploy.svg?style=shield)](https://circleci.com/gh/Loomly/s3_asset_deploy)
|
4
|
+
|
5
|
+
During rolling deploys to our web instances, this is what we use at
|
6
|
+
[Loomly](https://www.loomly.com) to safely deploy our web assets to S3 to be served via Cloudfront.
|
7
|
+
This gem is designed to upload and clean unneeded assets from S3 in a safe manner such that older
|
8
|
+
versions or recently removed assets are kept on S3 during the rolling deploy process.
|
9
|
+
It also maintains a version limit and TTL (time-to-live) on assets to avoid deleting
|
10
|
+
recent and outdated versions (up to a limit) or those that have been recently removed.
|
11
|
+
|
12
|
+
## Background
|
13
|
+
|
14
|
+
At the very beginning, we were serving our assets from our webservers. This isn't ideal for many reasons, but one big one is that it's problematic during rolling deploys where you temporarily have some web servers with new assets and some with old assets during the deploy. When round-robbining requests to instances behind a load balancer, this can result in requests for assets hitting web servers that don't have the asset being requested (either the new or the old depending on what web server and what's being requested). One way to fix this problem is to serve your assets from a CDN and keep both old and new versions of assets available on the CDN during the deploy process. So we decided to serve our assets from Cloudfront, backed by S3. In order to upload our assets to S3 during our deploy process, we started using [`asset_sync`](https://github.com/AssetSync/asset_sync). `asset_sync` served us well for quite some time, but our needs started to diverge a bit. Namely, `asset_sync`:
|
15
|
+
|
16
|
+
- Depends on the [`fog`](https://github.com/fog/fog) gem which was an extra dependency we didn't need since we already had the [`aws-sdk-s3`](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3.html) gem as a dependency.
|
17
|
+
- Uses a global configuration, which made it difficult to deploy to different S3 buckets depending on the environment (development, staging, production, etc.).
|
18
|
+
- Didn't have a way to remove or retire outdated or old assets from storage (in this case S3).
|
19
|
+
|
20
|
+
We took inspiration from `asset_sync` and ended up writing our own library inside our Rails app. We figured this could be useful to others, so we then moved it to an open source gem. While Rails is a "first-class citizen", this gem can be used with other frameworks by writing your own `S3AssetDeploy::LocalAssetCollector`. See the `Usage` section below for more details.
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
Add this line to your application's Gemfile:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
gem "s3_asset_deploy"
|
28
|
+
```
|
29
|
+
|
30
|
+
And then execute:
|
31
|
+
|
32
|
+
$ bundle install
|
33
|
+
|
34
|
+
Or install it yourself as:
|
35
|
+
|
36
|
+
$ gem install s3_asset_deploy
|
37
|
+
|
38
|
+
## Usage
|
39
|
+
|
40
|
+
Before using `S3AssetDeploy` you want to make sure to compile your assets. Assets must also be compiled using [fingerprinting](https://guides.rubyonrails.org/asset_pipeline.html#what-is-fingerprinting-and-why-should-i-care-questionmark) for things to work correctly. By default, `S3AssetDeploy` works with Rails and will find your locally compiled assets after running `rake assets:precompile`. Once you've compiled your assets, you can deploy them with:
|
41
|
+
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
manager = S3AssetDeploy::Manager("my-s3-bucket")
|
45
|
+
manager.deploy do
|
46
|
+
# Perform deploy to web instances in this block
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
`S3AssetDeploy::Manager#deploy` will perform the following steps:
|
51
|
+
- Upload your assets to the S3 bucket you specify
|
52
|
+
- Yield to the block
|
53
|
+
- Clean old versions assets or removed assets
|
54
|
+
|
55
|
+
Since it's yielding to the block after uploading, but before cleaning, the block is an ideal place to perform a deploy, especially if it's a rolling deploy across multiple servers. If you want to perform an upload or a clean without using `#deploy`, you can call `#upload` or `#clean` directly. For more configuration options, see below.
|
56
|
+
|
57
|
+
### Initializing [`S3AssetDeploy::Manager`](https://github.com/Loomly/s3_asset_deploy/blob/main/lib/s3_asset_deploy/manager.rb)
|
58
|
+
You'll need to initialize `S3AssetDeploy::Manager` with an S3 bucket name and **optionally**:
|
59
|
+
|
60
|
+
- **s3_client_options** (Hash) -> A hash that is passed directly to [`Aws::S3::Client#initialize`](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#initialize-instance_method) to configure the S3 client. By default the region is set to `us-east-1`.
|
61
|
+
- **logger** (Logger) -> A custom logger. By default things are logged to `STDOUT`.
|
62
|
+
- **local_asset_collector** (S3AssetDeploy::LocalAssetCollector) -> A custom instance of `S3AssetDeploy::LocalAssetCollector`. This allows you to customize how locally compiled assets are collected.
|
63
|
+
- **upload_options** (Hash) -> A hash consisting of options that are passed directly to [`Aws::S3::Client#put_object`](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#put_object-instance_method) when each asset is uploaded. By default `acl` is set to `public-read` and `cache_control` is set to `public, max-age=31536000`.
|
64
|
+
- **remove_fingerprint** (Lambda) -> Lambda for overriding how fingerprints are removed from asset paths. Fingerprints need to be removed during the cleaning process in order to group versions of the same file. If no Lambda is provided, [`S3AssetDeploy::AssetHelper.remove_fingerprint`](https://github.com/Loomly/s3_asset_deploy/blob/main/lib/s3_asset_deploy/asset_helper.rb#L8) is used by default.
|
65
|
+
|
66
|
+
Here's an example:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
manager = S3AssetDeploy::Manager.new(
|
70
|
+
"mybucket",
|
71
|
+
s3_client_options: { region: "us-west-1", profile: "my-aws-profile" },
|
72
|
+
logger: Logger.new(STDOUT),
|
73
|
+
remove_fingerprint: ->(path) { path.gsub("-myfingerprint", "") }
|
74
|
+
)
|
75
|
+
```
|
76
|
+
|
77
|
+
### Deploying Assets
|
78
|
+
Once you have an instance of `S3AssetDeploy::Manager`, you can deploy your precompiled assets with `S3AssetDeploy::Manager#deploy`:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
manager.deploy(version_limit: 2, version_ttl: 3600, removed_ttl: 172800) do
|
82
|
+
# Perform deploy to web instances in this block
|
83
|
+
end
|
84
|
+
```
|
85
|
+
|
86
|
+
This will upload new assets and perform a clean, which deletes removed assets and old versions from your bucket after the block is executed.
|
87
|
+
With the arguments used above, the clean process will keep the latest version on S3, two of the most recent older versions (`version_limit`), and any versions created within the last hour (`version_ttl`).
|
88
|
+
If you there are assets that are in your S3 bucket but no longer included in your locally compiled bundle, they will be deleted from S3 using the `removed_ttl` (after two days in the case above). This process uses [S3 object tagging](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#put_object_tagging-instance_method) to track `removed_at` timestamps. Here are a list of all the options you can pass to `#deploy`:
|
89
|
+
|
90
|
+
- **version_limit** (Integer) -> Max number of older versions of an asset to keep around. Note that this limit does **not** include the current version. Therefore, setting this to 0 will keep the current version and delete any older versions. Default is `2`.
|
91
|
+
- **version_ttl** (Integer) -> Number of seconds to keep newly uploaded versions before deleting according to `version_limit`. If an older version is still within the `version_ttl`, it will be kept on S3 even if the total number of older versions is beyond `version_limit`. Default is `3600`.
|
92
|
+
- **removed_ttl** (Integer) -> Number of seconds to keep assets on S3 that have been removed from your compiled set of assets. If the age of a removed asset expires according to `removed_ttl`, it will be deleted on the next deploy. Default is `172800`.
|
93
|
+
- **clean** (Boolean) -> Skip the clean process during a deploy. Default is `true`.
|
94
|
+
- **dry_run** (Boolean) -> Run deploy in read-only mode. This is helpful for debugging purposes and seeing plan of what would happen without performing any writes or deletes. Default is `false`.
|
95
|
+
|
96
|
+
`S3AssetDeploy::Manager#deploy` performs its work by delegating to `S3AssetDeploy#upload` and `S3AssetDeploy#clean`, which you can call yourself if you need some more control.
|
97
|
+
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
# Upload new assets
|
101
|
+
manager.upload
|
102
|
+
|
103
|
+
# Delete old versions and removed assets from S3
|
104
|
+
manager.clean
|
105
|
+
```
|
106
|
+
|
107
|
+
`S3AssetDeploy::Manager#deploy` and `S3AssetDeploy::Manager#clean` both accept `dry_run` as a keyword argument.
|
108
|
+
`S3AssetDeploy::Manager#clean` also accepts `version_limit`, `version_ttl`, and `removed_ttl` just like `S3AssetDeploy::Manager#deploy`.
|
109
|
+
|
110
|
+
|
111
|
+
## Customizing local asset collection
|
112
|
+
By default, `S3AssetDeploy::Manager` will use [`S3AssetDeploy::RailsLocalAssetCollector`](https://github.com/Loomly/s3_asset_deploy/blob/main/lib/s3_asset_deploy/rails_local_asset_collector.rb) to collect locally compiled assets. This will use the `Sprockets::Manifest` and `Webpacker` config (if `Webpacker` is installed) to locate the compiled assets. `S3AssetDeploy::RailsLocalAssetCollector` inherits from the [`S3AssetDeploy::LocalAssetCollector`](https://github.com/Loomly/s3_asset_deploy/blob/main/lib/s3_asset_deploy/local_asset_collector.rb) base class. You can completely customize how your local assets are collected for deploys by creating your own class that inherits from `S3AssetDeploy::LocalAssetCollector` and passing it into the manager. You'll want override `S3AssetDeploy::LocalAssetCollector#assets` in your custom collector such that it returns an array of `S3AssetDeploy::LocalAsset` instances. Here's a basic example:
|
113
|
+
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
class MyCustomLocalAssetCollector < S3AssetDeploy::LocalAssetCollector
|
117
|
+
def assets
|
118
|
+
# Override this method to return an array of your locally compiled assets
|
119
|
+
# as instances of S3AssetDeploy::LocalAsset
|
120
|
+
[S3AssetDeploy::LocalAsset.new("path-to-my-asset.jpg")]
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
manager = S3AssetDeploy::Manager.new(
|
125
|
+
"mybucket",
|
126
|
+
local_asset_collector: MyCustomLocalAssetCollector.new
|
127
|
+
)
|
128
|
+
```
|
129
|
+
|
130
|
+
## Dry run
|
131
|
+
As mentioned above, you can run operations in "dry" mode by passing `dry_run: true`.
|
132
|
+
This will skip any write or delete operations and only perform read opeartions with log output.
|
133
|
+
This is helpful for debugging or planning purposes.
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
> manager = S3AssetDeploy::Manager("my-s3-bucket")
|
137
|
+
> manager.deploy(dry_run: true)
|
138
|
+
|
139
|
+
I, [2021-02-17T16:12:23.703677 #65335] INFO -- : S3AssetDeploy::Manager: Cleaning assets from test-bucket S3 bucket. Dry run: true
|
140
|
+
I, [2021-02-17T16:12:23.703677 #65335] INFO -- : S3AssetDeploy::Manager: Determining how long ago assets/file-2-34567.jpg was removed - removed on 2021-02-15 23:12:22 UTC (172801.703677 seconds ago)
|
141
|
+
I, [2021-02-17T16:12:23.703677 #65335] INFO -- : S3AssetDeploy::Manager: Determining how long ago assets/file-3-9876666.jpg was removed - removed on 2021-02-15 23:12:24 UTC (172799.703677 seconds ago)
|
142
|
+
```
|
143
|
+
|
144
|
+
## AWS IAM Permissions
|
145
|
+
`S3AsetDeploy` requires the following AWS IAM permissions:
|
146
|
+
|
147
|
+
```json
|
148
|
+
"Statement": [
|
149
|
+
{
|
150
|
+
"Action": [
|
151
|
+
"s3:ListBucket",
|
152
|
+
"s3:PutObject*",
|
153
|
+
"s3:DeleteObject"
|
154
|
+
],
|
155
|
+
"Effect": "Allow",
|
156
|
+
"Resource": [
|
157
|
+
"arn:aws:s3:::#{YOUR_BUCKET}",
|
158
|
+
"arn:aws:s3:::#{YOUR_BUCKET}/*"
|
159
|
+
]
|
160
|
+
}
|
161
|
+
]
|
162
|
+
```
|
163
|
+
|
164
|
+
## Development
|
165
|
+
|
166
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
167
|
+
|
168
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
169
|
+
|
170
|
+
## Contributing
|
171
|
+
|
172
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/Loomly/s3_asset_deploy. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/Loomly/s3_asset_deploy/blob/main/CODE_OF_CONDUCT.md).
|
173
|
+
|
174
|
+
## License
|
175
|
+
|
176
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
177
|
+
|
178
|
+
## Code of Conduct
|
179
|
+
|
180
|
+
Everyone interacting in the S3AssetDeploy project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/Loomly/s3_asset_deploy/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "s3_asset_deploy"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require "irb"
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "mime/types"
|
4
|
+
|
5
|
+
class S3AssetDeploy::AssetHelper
|
6
|
+
FINGERPRINTED_ASSET_REGEX = /\A(.*)-([[:alnum:]]+)((?:(?:\.[[:alnum:]]+))+)\z/.freeze
|
7
|
+
|
8
|
+
def self.remove_fingerprint(path)
|
9
|
+
match_data = path.match(FINGERPRINTED_ASSET_REGEX)
|
10
|
+
return asset_path unless match_data
|
11
|
+
"#{match_data[1]}#{match_data[3]}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.mime_type_for_path(path)
|
15
|
+
extension = File.extname(path)[1..-1]
|
16
|
+
return "application/json" if extension == "map"
|
17
|
+
MIME::Types.type_for(extension).first
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class S3AssetDeploy::Error < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
class S3AssetDeploy::DuplicateAssetsError < S3AssetDeploy::Error
|
7
|
+
def initialize(msg = "Duplicate precompiled assets detected. Please make sure there are no duplicate precompiled assets in the public dir.")
|
8
|
+
super
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "s3_asset_deploy/asset_helper"
|
4
|
+
|
5
|
+
class S3AssetDeploy::LocalAsset
|
6
|
+
attr_reader :path
|
7
|
+
|
8
|
+
def initialize(path, remove_fingerprint: nil)
|
9
|
+
@path = path
|
10
|
+
@remove_fingerprint = remove_fingerprint
|
11
|
+
end
|
12
|
+
|
13
|
+
def original_path
|
14
|
+
@original_path ||=
|
15
|
+
if @remove_fingerprint
|
16
|
+
@remove_fingerprint.call(path)
|
17
|
+
else
|
18
|
+
S3AssetDeploy::AssetHelper.remove_fingerprint(path)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def full_path
|
23
|
+
File.join(ENV["PWD"], "public", path)
|
24
|
+
end
|
25
|
+
|
26
|
+
def mime_type
|
27
|
+
S3AssetDeploy::AssetHelper.mime_type_for_path(path).to_s
|
28
|
+
end
|
29
|
+
|
30
|
+
def ==(other_asset)
|
31
|
+
path == other_asset.path
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
path
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class S3AssetDeploy::LocalAssetCollector
|
4
|
+
def initialize(remove_fingerprint: nil)
|
5
|
+
@remove_fingerprint = remove_fingerprint
|
6
|
+
end
|
7
|
+
|
8
|
+
def assets
|
9
|
+
[]
|
10
|
+
end
|
11
|
+
|
12
|
+
def asset_paths
|
13
|
+
assets.map(&:path)
|
14
|
+
end
|
15
|
+
|
16
|
+
def asset_map
|
17
|
+
assets.map do |asset|
|
18
|
+
[asset.original_path, asset]
|
19
|
+
end.to_h
|
20
|
+
end
|
21
|
+
|
22
|
+
def original_asset_paths
|
23
|
+
assets.map(&:original_path)
|
24
|
+
end
|
25
|
+
|
26
|
+
def full_file_path(asset_path)
|
27
|
+
asset_path
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
require "time"
|
5
|
+
require "aws-sdk-s3"
|
6
|
+
require "s3_asset_deploy/errors"
|
7
|
+
require "s3_asset_deploy/removal_manifest"
|
8
|
+
require "s3_asset_deploy/rails_local_asset_collector"
|
9
|
+
require "s3_asset_deploy/remote_asset_collector"
|
10
|
+
|
11
|
+
class S3AssetDeploy::Manager
|
12
|
+
attr_reader :bucket_name, :logger, :local_asset_collector, :remote_asset_collector
|
13
|
+
|
14
|
+
def initialize(bucket_name, s3_client_options: {}, logger: nil, local_asset_collector: nil, upload_options: {}, remove_fingerprint: nil)
|
15
|
+
@bucket_name = bucket_name.to_s
|
16
|
+
@logger = logger || Logger.new(STDOUT)
|
17
|
+
|
18
|
+
@s3_client_options = {
|
19
|
+
region: "us-east-1",
|
20
|
+
logger: @logger
|
21
|
+
}.merge(s3_client_options)
|
22
|
+
@upload_options = upload_options
|
23
|
+
|
24
|
+
@local_asset_collector = local_asset_collector || S3AssetDeploy::RailsLocalAssetCollector.new(remove_fingerprint: remove_fingerprint)
|
25
|
+
@remote_asset_collector = S3AssetDeploy::RemoteAssetCollector.new(
|
26
|
+
bucket_name,
|
27
|
+
s3_client_options: @s3_client_options,
|
28
|
+
remove_fingerprint: remove_fingerprint
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
def removal_manifest
|
33
|
+
@removal_manifest ||= S3AssetDeploy::RemovalManifest.new(
|
34
|
+
bucket_name,
|
35
|
+
s3_client_options: @s3_client_options
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
def local_assets_to_upload
|
40
|
+
remote_asset_paths = remote_asset_collector.asset_paths
|
41
|
+
local_asset_collector.assets.reject { |asset| remote_asset_paths.include?(asset.path) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def upload(dry_run: false)
|
45
|
+
verify_no_duplicate_assets!
|
46
|
+
|
47
|
+
removal_manifest.load
|
48
|
+
assets_to_upload = local_assets_to_upload
|
49
|
+
|
50
|
+
(removal_manifest.keys & local_asset_collector.asset_paths).each do |path|
|
51
|
+
log "#{path} has been re-added. Deleting from removal manifest."
|
52
|
+
removal_manifest.delete(path) unless dry_run
|
53
|
+
end
|
54
|
+
|
55
|
+
uploaded_assets = []
|
56
|
+
assets_to_upload.each do |asset|
|
57
|
+
next unless File.file?(asset.full_path)
|
58
|
+
log "Uploading #{asset.path}..."
|
59
|
+
upload_asset(asset) unless dry_run
|
60
|
+
uploaded_assets << asset.path
|
61
|
+
end
|
62
|
+
|
63
|
+
removal_manifest.save unless dry_run
|
64
|
+
remote_asset_collector.clear_cache
|
65
|
+
uploaded_assets
|
66
|
+
end
|
67
|
+
|
68
|
+
# Cleanup old assets on S3. By default it will
|
69
|
+
# keep the latest version, 2 older versions (version_limit) and any created within the past hour (version_ttl).
|
70
|
+
# When assets are removed completely, they are tagged with a removed_at timestamp
|
71
|
+
# and eventually deleted based on the removed_ttl.
|
72
|
+
def clean(version_limit: 2, version_ttl: 3600, removed_ttl: 172800, dry_run: false)
|
73
|
+
verify_no_duplicate_assets!
|
74
|
+
|
75
|
+
version_ttl = version_ttl.to_i
|
76
|
+
removed_ttl = removed_ttl.to_i
|
77
|
+
|
78
|
+
log "Cleaning assets from #{bucket_name} S3 bucket. Dry run: #{dry_run}."
|
79
|
+
s3_keys_to_delete = []
|
80
|
+
|
81
|
+
unless local_assets_to_upload.empty?
|
82
|
+
log "WARNING: Please upload latest asset versions to remote host before cleaning."
|
83
|
+
return s3_keys_to_delete
|
84
|
+
end
|
85
|
+
|
86
|
+
removal_manifest.load
|
87
|
+
local_asset_map = local_asset_collector.asset_map
|
88
|
+
|
89
|
+
remote_asset_collector.grouped_assets.each do |original_path, versions|
|
90
|
+
current_asset = local_asset_map[original_path]
|
91
|
+
|
92
|
+
# Remove current asset version from the list
|
93
|
+
versions_to_delete = versions.reject do |version|
|
94
|
+
version.path == current_asset.path if current_asset
|
95
|
+
end
|
96
|
+
|
97
|
+
# Sort remaining versions from newest to oldest
|
98
|
+
versions_to_delete = versions_to_delete.sort_by(&:last_modified).reverse
|
99
|
+
|
100
|
+
# If the asset has been completely removed from our set of locally compiled assets
|
101
|
+
# then use removed_at timestamp from manifest and removed_ttl to determine if it
|
102
|
+
# should be deleted from remote host.
|
103
|
+
# Otherwise, use version_ttl and version_limit to dermine whether version should be kept.
|
104
|
+
versions_to_delete = versions_to_delete.each_with_index.drop_while do |version, index|
|
105
|
+
if !current_asset
|
106
|
+
if (removed_at = removal_manifest[version.path])
|
107
|
+
removed_at = Time.parse(removed_at)
|
108
|
+
removed_age = Time.now.utc - removed_at
|
109
|
+
log "Determining how long ago #{version.path} was removed - removed on #{removed_at} (#{removed_age} seconds ago)."
|
110
|
+
drop = removed_age < removed_ttl
|
111
|
+
log "Marking removed asset #{version.path} for deletion." unless drop
|
112
|
+
removal_manifest.delete(version.path) unless drop || dry_run
|
113
|
+
drop
|
114
|
+
else
|
115
|
+
log "Adding #{version.path} to removal manifest."
|
116
|
+
removed_at = Time.now.utc
|
117
|
+
removal_manifest[version.path] = removed_at.iso8601 unless dry_run
|
118
|
+
true
|
119
|
+
end
|
120
|
+
else
|
121
|
+
# Keep if under age or within the version_limit
|
122
|
+
version_age = [0, Time.now - version.last_modified].max
|
123
|
+
drop = version_age < version_ttl || index < version_limit
|
124
|
+
log "Marking #{version.path} for deletion. Version age: #{version_age}. Version: #{index + 1}." unless drop
|
125
|
+
drop
|
126
|
+
end
|
127
|
+
end.map(&:first)
|
128
|
+
|
129
|
+
s3_keys_to_delete += versions_to_delete.map(&:path)
|
130
|
+
end
|
131
|
+
|
132
|
+
unless dry_run
|
133
|
+
delete_objects(s3_keys_to_delete)
|
134
|
+
removal_manifest.save
|
135
|
+
end
|
136
|
+
|
137
|
+
remote_asset_collector.clear_cache
|
138
|
+
s3_keys_to_delete
|
139
|
+
end
|
140
|
+
|
141
|
+
def deploy(version_limit: 2, version_ttl: 3600, removed_ttl: 172800, clean: true, dry_run: false)
|
142
|
+
upload(dry_run: dry_run)
|
143
|
+
yield if block_given?
|
144
|
+
if clean
|
145
|
+
clean(
|
146
|
+
dry_run: dry_run,
|
147
|
+
version_limit: version_limit,
|
148
|
+
version_ttl: version_ttl,
|
149
|
+
removed_ttl: removed_ttl
|
150
|
+
)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def to_s
|
155
|
+
"#<#{self.class.name}:#{"0x0000%x" % (object_id << 1)} @bucket_name='#{bucket_name}'>"
|
156
|
+
end
|
157
|
+
|
158
|
+
def inspect
|
159
|
+
to_s
|
160
|
+
end
|
161
|
+
|
162
|
+
protected
|
163
|
+
|
164
|
+
def upload_asset(asset)
|
165
|
+
file_handle = File.open(asset.full_path)
|
166
|
+
|
167
|
+
params = {
|
168
|
+
bucket: bucket_name,
|
169
|
+
key: asset.path,
|
170
|
+
body: file_handle,
|
171
|
+
acl: "public-read",
|
172
|
+
content_type: asset.mime_type,
|
173
|
+
cache_control: "public, max-age=31536000"
|
174
|
+
}.merge(@upload_options)
|
175
|
+
|
176
|
+
put_object(params)
|
177
|
+
ensure
|
178
|
+
file_handle.close
|
179
|
+
end
|
180
|
+
|
181
|
+
def s3
|
182
|
+
@s3 ||= Aws::S3::Client.new(@s3_client_options)
|
183
|
+
end
|
184
|
+
|
185
|
+
def verify_no_duplicate_assets!
|
186
|
+
if local_asset_collector.original_asset_paths.uniq.length != local_asset_collector.asset_paths.length
|
187
|
+
raise S3AssetDeploy::DuplicateAssetsError
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def put_object(object)
|
192
|
+
s3.put_object(object)
|
193
|
+
end
|
194
|
+
|
195
|
+
def delete_objects(keys = [])
|
196
|
+
return if keys.empty?
|
197
|
+
s3.delete_objects(
|
198
|
+
bucket: bucket_name,
|
199
|
+
delete: { objects: keys.map { |key| { key: key }} }
|
200
|
+
)
|
201
|
+
end
|
202
|
+
|
203
|
+
def log(msg)
|
204
|
+
logger.info("#{self.class.name}: #{msg}")
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "s3_asset_deploy/local_asset"
|
4
|
+
|
5
|
+
class S3AssetDeploy::RailsLocalAsset < S3AssetDeploy::LocalAsset
|
6
|
+
attr_reader :path
|
7
|
+
|
8
|
+
def full_path
|
9
|
+
File.join(public_path, path)
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def public_path
|
15
|
+
::Rails.public_path
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "s3_asset_deploy/rails_local_asset"
|
4
|
+
require "s3_asset_deploy/local_asset_collector"
|
5
|
+
|
6
|
+
class S3AssetDeploy::RailsLocalAssetCollector < S3AssetDeploy::LocalAssetCollector
|
7
|
+
def assets
|
8
|
+
assets_from_manifest + pack_assets
|
9
|
+
end
|
10
|
+
|
11
|
+
def assets_from_manifest
|
12
|
+
manifest = ::Sprockets::Manifest.new(
|
13
|
+
::ActionView::Base.assets_manifest.environment,
|
14
|
+
::ActionView::Base.assets_manifest.dir
|
15
|
+
)
|
16
|
+
manifest.assets.values.map do |f|
|
17
|
+
S3AssetDeploy::RailsLocalAsset.new(
|
18
|
+
File.join(assets_prefix, f),
|
19
|
+
remove_fingerprint: @remove_fingerprint
|
20
|
+
)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def pack_assets
|
25
|
+
return [] unless defined?(::Webpacker)
|
26
|
+
|
27
|
+
Dir.chdir(public_path) do
|
28
|
+
packs_dir = ::Webpacker.config.public_output_path.relative_path_from(public_path)
|
29
|
+
|
30
|
+
Dir[File.join(packs_dir, "/**/**")]
|
31
|
+
.select { |path| File.file?(path) }
|
32
|
+
.reject { |path| path.ends_with?(".gz") || path.ends_with?("manifest.json") }
|
33
|
+
.map { |path| S3AssetDeploy::RailsLocalAsset.new(path, remove_fingerprint: @remove_fingerprint) }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def public_path
|
40
|
+
::Rails.public_path
|
41
|
+
end
|
42
|
+
|
43
|
+
def assets_prefix
|
44
|
+
::Rails.application.config.assets.prefix.sub(/^\//, "")
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "s3_asset_deploy/asset_helper"
|
4
|
+
|
5
|
+
class S3AssetDeploy::RemoteAsset
|
6
|
+
attr_reader :s3_object
|
7
|
+
|
8
|
+
def initialize(s3_object, remove_fingerprint: nil)
|
9
|
+
@s3_object = s3_object
|
10
|
+
@remove_fingerprint = remove_fingerprint
|
11
|
+
end
|
12
|
+
|
13
|
+
def original_path
|
14
|
+
@original_path ||=
|
15
|
+
if @remove_fingerprint
|
16
|
+
@remove_fingerprint.call(path)
|
17
|
+
else
|
18
|
+
S3AssetDeploy::AssetHelper.remove_fingerprint(path)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def last_modified
|
23
|
+
s3_object.last_modified
|
24
|
+
end
|
25
|
+
|
26
|
+
def path
|
27
|
+
s3_object.key
|
28
|
+
end
|
29
|
+
|
30
|
+
def ==(other_asset)
|
31
|
+
path == other_asset.path
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
path
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "aws-sdk-s3"
|
4
|
+
require "s3_asset_deploy/removal_manifest"
|
5
|
+
require "s3_asset_deploy/remote_asset"
|
6
|
+
|
7
|
+
class S3AssetDeploy::RemoteAssetCollector
|
8
|
+
attr_reader :bucket_name
|
9
|
+
|
10
|
+
def initialize(bucket_name, s3_client_options: {}, remove_fingerprint: nil)
|
11
|
+
@bucket_name = bucket_name
|
12
|
+
@remove_fingerprint = remove_fingerprint
|
13
|
+
@s3_client_options = {
|
14
|
+
region: "us-east-1",
|
15
|
+
logger: @logger
|
16
|
+
}.merge(s3_client_options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def s3
|
20
|
+
@s3 ||= Aws::S3::Client.new(@s3_client_options)
|
21
|
+
end
|
22
|
+
|
23
|
+
def assets
|
24
|
+
@assets ||= s3.list_objects_v2(bucket: bucket_name).each_with_object([]) do |response, array|
|
25
|
+
remote_assets = response
|
26
|
+
.contents
|
27
|
+
.reject { |obj| obj.key == S3AssetDeploy::RemovalManifest::PATH }
|
28
|
+
.map do |obj|
|
29
|
+
S3AssetDeploy::RemoteAsset.new(obj, remove_fingerprint: @remove_fingerprint)
|
30
|
+
end
|
31
|
+
|
32
|
+
array.concat(remote_assets)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def clear_cache
|
37
|
+
@assets = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def asset_paths
|
41
|
+
assets.map(&:path)
|
42
|
+
end
|
43
|
+
|
44
|
+
def grouped_assets
|
45
|
+
assets.group_by(&:original_path)
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_s
|
49
|
+
"#<#{self.class.name}:#{"0x0000%x" % (object_id << 1)} @bucket_name='#{bucket_name}'>"
|
50
|
+
end
|
51
|
+
|
52
|
+
def inspect
|
53
|
+
to_s
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
class S3AssetDeploy::RemovalManifest
|
4
|
+
attr_reader :bucket_name
|
5
|
+
|
6
|
+
PATH = "s3-asset-deploy-removal-manifest.json".freeze
|
7
|
+
|
8
|
+
def initialize(bucket_name, s3_client_options: {})
|
9
|
+
@bucket_name = bucket_name
|
10
|
+
@loaded = false
|
11
|
+
@changed = false
|
12
|
+
@manifest = {}
|
13
|
+
@s3_client_options = {
|
14
|
+
region: "us-east-1",
|
15
|
+
logger: @logger
|
16
|
+
}.merge(s3_client_options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def s3
|
20
|
+
@s3 ||= Aws::S3::Client.new(@s3_client_options)
|
21
|
+
end
|
22
|
+
|
23
|
+
def load
|
24
|
+
return true if loaded?
|
25
|
+
@manifest = fetch_manifest
|
26
|
+
@loaded = true
|
27
|
+
rescue Aws::S3::Errors::NoSuchKey
|
28
|
+
@manifest = {}
|
29
|
+
@loaded = true
|
30
|
+
end
|
31
|
+
|
32
|
+
def loaded?
|
33
|
+
@loaded
|
34
|
+
end
|
35
|
+
|
36
|
+
def changed?
|
37
|
+
@changed
|
38
|
+
end
|
39
|
+
|
40
|
+
def save
|
41
|
+
return false unless loaded?
|
42
|
+
return true unless changed?
|
43
|
+
|
44
|
+
s3.put_object({
|
45
|
+
bucket: bucket_name,
|
46
|
+
key: PATH,
|
47
|
+
body: @manifest.to_json,
|
48
|
+
acl: "private",
|
49
|
+
content_type: "application/json"
|
50
|
+
})
|
51
|
+
|
52
|
+
@changed = false
|
53
|
+
|
54
|
+
true
|
55
|
+
end
|
56
|
+
|
57
|
+
def keys
|
58
|
+
@manifest.keys
|
59
|
+
end
|
60
|
+
|
61
|
+
def delete(key)
|
62
|
+
return unless loaded?
|
63
|
+
@changed = true
|
64
|
+
@manifest.delete(key)
|
65
|
+
end
|
66
|
+
|
67
|
+
def [](key)
|
68
|
+
@manifest[key]
|
69
|
+
end
|
70
|
+
|
71
|
+
def []=(key, value)
|
72
|
+
return unless loaded?
|
73
|
+
@changed = true
|
74
|
+
@manifest[key] = value
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_h
|
78
|
+
@manifest
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_s
|
82
|
+
@manifest.to_s
|
83
|
+
end
|
84
|
+
|
85
|
+
def inspect
|
86
|
+
"#<#{self.class.name}:#{"0x0000%x" % (object_id << 1)} @bucket_name='#{bucket_name}'>"
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def fetch_manifest
|
92
|
+
resp = s3.get_object({
|
93
|
+
bucket: bucket_name,
|
94
|
+
key: PATH
|
95
|
+
})
|
96
|
+
|
97
|
+
JSON.parse(resp.body.read)
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/s3_asset_deploy/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "s3_asset_deploy"
|
7
|
+
spec.version = S3AssetDeploy::VERSION
|
8
|
+
spec.authors = ["Loomly"]
|
9
|
+
spec.email = ["contact@loomly.com"]
|
10
|
+
|
11
|
+
spec.summary = "Safely deploy web app assets to S3 during rolling or multi-step deploys."
|
12
|
+
spec.homepage = "https://www.loomly.com"
|
13
|
+
spec.license = "MIT"
|
14
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
|
15
|
+
|
16
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
17
|
+
spec.metadata["source_code_uri"] = "https://github.com/Loomly/s3_asset_deploy"
|
18
|
+
spec.metadata["changelog_uri"] = "https://github.com/Loomly/s3_asset_deploy/CHANGELOG.md"
|
19
|
+
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
21
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
22
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
23
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
24
|
+
end
|
25
|
+
spec.bindir = "exe"
|
26
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
27
|
+
spec.require_paths = ["lib"]
|
28
|
+
|
29
|
+
spec.add_dependency "aws-sdk-s3", "~> 1.0"
|
30
|
+
spec.add_dependency "mime-types", "~> 3.0"
|
31
|
+
|
32
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
33
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
34
|
+
spec.add_development_dependency "timecop", "~> 0.9"
|
35
|
+
spec.add_development_dependency "pry", "~> 0.13"
|
36
|
+
spec.add_development_dependency "pry-byebug", "~> 3.9"
|
37
|
+
spec.add_development_dependency "rspec_junit_formatter", "~> 0.4"
|
38
|
+
|
39
|
+
# For more information and examples about making a new gem, checkout our
|
40
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
41
|
+
end
|
metadata
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: s3_asset_deploy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Loomly
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-02-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: aws-sdk-s3
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: mime-types
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '13.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '13.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: timecop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.9'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.9'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pry
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.13'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.13'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: pry-byebug
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '3.9'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '3.9'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rspec_junit_formatter
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0.4'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0.4'
|
125
|
+
description:
|
126
|
+
email:
|
127
|
+
- contact@loomly.com
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- ".circleci/config.yml"
|
133
|
+
- ".gitignore"
|
134
|
+
- ".rspec"
|
135
|
+
- CHANGELOG.md
|
136
|
+
- CODE_OF_CONDUCT.md
|
137
|
+
- Gemfile
|
138
|
+
- Gemfile.lock
|
139
|
+
- LICENSE.txt
|
140
|
+
- README.md
|
141
|
+
- Rakefile
|
142
|
+
- bin/console
|
143
|
+
- bin/setup
|
144
|
+
- lib/s3_asset_deploy.rb
|
145
|
+
- lib/s3_asset_deploy/asset_helper.rb
|
146
|
+
- lib/s3_asset_deploy/errors.rb
|
147
|
+
- lib/s3_asset_deploy/local_asset.rb
|
148
|
+
- lib/s3_asset_deploy/local_asset_collector.rb
|
149
|
+
- lib/s3_asset_deploy/manager.rb
|
150
|
+
- lib/s3_asset_deploy/rails_local_asset.rb
|
151
|
+
- lib/s3_asset_deploy/rails_local_asset_collector.rb
|
152
|
+
- lib/s3_asset_deploy/remote_asset.rb
|
153
|
+
- lib/s3_asset_deploy/remote_asset_collector.rb
|
154
|
+
- lib/s3_asset_deploy/removal_manifest.rb
|
155
|
+
- lib/s3_asset_deploy/version.rb
|
156
|
+
- s3_asset_deploy.gemspec
|
157
|
+
homepage: https://www.loomly.com
|
158
|
+
licenses:
|
159
|
+
- MIT
|
160
|
+
metadata:
|
161
|
+
homepage_uri: https://www.loomly.com
|
162
|
+
source_code_uri: https://github.com/Loomly/s3_asset_deploy
|
163
|
+
changelog_uri: https://github.com/Loomly/s3_asset_deploy/CHANGELOG.md
|
164
|
+
post_install_message:
|
165
|
+
rdoc_options: []
|
166
|
+
require_paths:
|
167
|
+
- lib
|
168
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - ">="
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: 2.4.0
|
173
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
174
|
+
requirements:
|
175
|
+
- - ">="
|
176
|
+
- !ruby/object:Gem::Version
|
177
|
+
version: '0'
|
178
|
+
requirements: []
|
179
|
+
rubygems_version: 3.0.3
|
180
|
+
signing_key:
|
181
|
+
specification_version: 4
|
182
|
+
summary: Safely deploy web app assets to S3 during rolling or multi-step deploys.
|
183
|
+
test_files: []
|