capistrano-s3 2.3.0 → 3.0.0.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.github/workflows/build.yml +20 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +59 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +15 -2
- data/Guardfile +18 -0
- data/README.md +52 -3
- data/Rakefile +7 -5
- data/capistrano-s3.gemspec +31 -27
- data/certs/aleksandrs-ledovskis--2025-07-03-2035-07-01.pem +26 -0
- data/lib/capistrano/s3/defaults.rb +11 -9
- data/lib/capistrano/s3/mime_types.rb +55 -0
- data/lib/capistrano/s3/publisher.rb +122 -101
- data/lib/capistrano/s3/version.rb +3 -1
- data/lib/capistrano/s3.rb +6 -3
- data/lib/capistrano/tasks/capistrano_2.rb +18 -5
- data/lib/capistrano/tasks/capistrano_3.rb +22 -5
- data/spec/capistrano/s3/publisher_spec.rb +214 -0
- data/spec/sample-mime/script.js +1 -0
- data/spec/sample-write/assets/script.js +1 -0
- data/spec/sample-write/assets/style.css +3 -0
- data/spec/sample-write/index.html +4 -0
- data/spec/spec_helper.rb +8 -2
- data.tar.gz.sig +0 -0
- metadata +46 -108
- metadata.gz.sig +4 -3
- data/.travis.yml +0 -21
- data/certs/aleksandrs-ledovskis--2018-11-04-2020-11-03.pem +0 -26
- data/certs/j15e.pem +0 -22
- data/spec/publisher_spec.rb +0 -75
@@ -1,106 +1,111 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "aws-sdk"
|
4
|
+
require "mime/types"
|
5
|
+
require "fileutils"
|
6
|
+
require "capistrano/s3/mime_types"
|
7
|
+
require "yaml"
|
4
8
|
|
5
9
|
module Capistrano
|
6
10
|
module S3
|
7
11
|
module Publisher
|
8
|
-
LAST_PUBLISHED_FILE =
|
9
|
-
LAST_INVALIDATION_FILE =
|
12
|
+
LAST_PUBLISHED_FILE = ".last_published"
|
13
|
+
LAST_INVALIDATION_FILE = ".last_invalidation"
|
10
14
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
+
class << self
|
16
|
+
def publish!(region, key, secret, bucket, deployment_path, target_path, distribution_id,
|
17
|
+
invalidations, exclusions, only_gzip, extra_options, stage = "default")
|
18
|
+
deployment_path_absolute = File.expand_path(deployment_path, Dir.pwd)
|
19
|
+
s3_client = establish_s3_client_connection!(region, key, secret)
|
15
20
|
|
16
|
-
|
17
|
-
|
18
|
-
next if
|
19
|
-
next if only_gzip &&
|
21
|
+
files(deployment_path_absolute, exclusions).each do |file|
|
22
|
+
next if File.directory?(file)
|
23
|
+
next if published?(file, bucket, stage)
|
24
|
+
next if only_gzip && gzipped_version?(file)
|
20
25
|
|
21
|
-
path =
|
22
|
-
path.gsub!(
|
26
|
+
path = base_file_path(deployment_path_absolute, file)
|
27
|
+
path.gsub!(%r{^/}, "") # Remove preceding slash for S3
|
23
28
|
|
24
|
-
|
29
|
+
put_object(s3_client, bucket, target_path, path, file, only_gzip, extra_options)
|
25
30
|
end
|
26
|
-
end
|
27
31
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
32
|
+
# invalidate CloudFront distribution if needed
|
33
|
+
if distribution_id && !invalidations.empty?
|
34
|
+
cf = establish_cf_client_connection!(region, key, secret)
|
35
|
+
|
36
|
+
response = cf.create_invalidation(
|
37
|
+
distribution_id: distribution_id,
|
38
|
+
invalidation_batch: {
|
39
|
+
paths: {
|
40
|
+
quantity: invalidations.count,
|
41
|
+
items: invalidations.map do |path|
|
42
|
+
File.join("/", add_prefix(path, prefix: target_path))
|
43
|
+
end
|
44
|
+
},
|
45
|
+
caller_reference: SecureRandom.hex
|
46
|
+
}
|
47
|
+
)
|
48
|
+
|
49
|
+
if response&.successful?
|
50
|
+
File.write(LAST_INVALIDATION_FILE, response[:invalidation][:id])
|
51
|
+
end
|
47
52
|
end
|
48
|
-
end
|
49
53
|
|
50
|
-
|
51
|
-
|
54
|
+
published_to!(bucket, stage)
|
55
|
+
end
|
52
56
|
|
53
|
-
|
54
|
-
|
55
|
-
|
57
|
+
def clear!(region, key, secret, bucket, stage = "default")
|
58
|
+
s3 = establish_s3_connection!(region, key, secret)
|
59
|
+
s3.buckets[bucket].clear!
|
56
60
|
|
57
|
-
|
58
|
-
|
59
|
-
|
61
|
+
clear_published!(bucket, stage)
|
62
|
+
FileUtils.rm(LAST_INVALIDATION_FILE)
|
63
|
+
end
|
60
64
|
|
61
|
-
|
62
|
-
|
65
|
+
def check_invalidation(region, key, secret, distribution_id, _stage = "default")
|
66
|
+
last_invalidation_id = File.read(LAST_INVALIDATION_FILE).strip
|
63
67
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
+
cf = establish_cf_client_connection!(region, key, secret)
|
69
|
+
cf.wait_until(:invalidation_completed, distribution_id: distribution_id,
|
70
|
+
id: last_invalidation_id) do |w|
|
71
|
+
w.max_attempts = nil
|
72
|
+
w.delay = 30
|
73
|
+
end
|
68
74
|
end
|
69
|
-
end
|
70
75
|
|
71
|
-
|
76
|
+
private
|
72
77
|
|
73
78
|
# Establishes the connection to Amazon S3
|
74
|
-
def
|
79
|
+
def establish_connection!(klass, region, key, secret)
|
75
80
|
# Send logging to STDOUT
|
76
81
|
Aws.config[:logger] = ::Logger.new(STDOUT)
|
77
82
|
Aws.config[:log_formatter] = Aws::Log::Formatter.colored
|
78
83
|
klass.new(
|
79
|
-
:
|
80
|
-
:
|
81
|
-
:
|
84
|
+
region: region,
|
85
|
+
access_key_id: key,
|
86
|
+
secret_access_key: secret
|
82
87
|
)
|
83
88
|
end
|
84
89
|
|
85
|
-
def
|
86
|
-
|
90
|
+
def establish_cf_client_connection!(region, key, secret)
|
91
|
+
establish_connection!(Aws::CloudFront::Client, region, key, secret)
|
87
92
|
end
|
88
93
|
|
89
|
-
def
|
90
|
-
|
94
|
+
def establish_s3_client_connection!(region, key, secret)
|
95
|
+
establish_connection!(Aws::S3::Client, region, key, secret)
|
91
96
|
end
|
92
97
|
|
93
|
-
def
|
94
|
-
|
98
|
+
def establish_s3_connection!(region, key, secret)
|
99
|
+
establish_connection!(Aws::S3, region, key, secret)
|
95
100
|
end
|
96
101
|
|
97
|
-
def
|
102
|
+
def base_file_path(root, file)
|
98
103
|
file.gsub(root, "")
|
99
104
|
end
|
100
105
|
|
101
|
-
def
|
106
|
+
def files(deployment_path, exclusions)
|
102
107
|
globbed_paths = Dir.glob(
|
103
|
-
File.join(deployment_path,
|
108
|
+
File.join(deployment_path, "**", "*"),
|
104
109
|
File::FNM_DOTMATCH # Else Unix-like hidden files will be ignored
|
105
110
|
)
|
106
111
|
|
@@ -111,106 +116,122 @@ module Capistrano
|
|
111
116
|
globbed_paths - excluded_paths
|
112
117
|
end
|
113
118
|
|
114
|
-
def
|
115
|
-
if File.
|
119
|
+
def last_published
|
120
|
+
if File.exist? LAST_PUBLISHED_FILE
|
116
121
|
YAML.load_file(LAST_PUBLISHED_FILE) || {}
|
117
122
|
else
|
118
123
|
{}
|
119
124
|
end
|
120
125
|
end
|
121
126
|
|
122
|
-
def
|
123
|
-
current_publish =
|
127
|
+
def published_to!(bucket, stage)
|
128
|
+
current_publish = last_published
|
124
129
|
current_publish["#{bucket}::#{stage}"] = Time.now.iso8601
|
125
130
|
File.write(LAST_PUBLISHED_FILE, current_publish.to_yaml)
|
126
131
|
end
|
127
132
|
|
128
|
-
def
|
129
|
-
current_publish =
|
133
|
+
def clear_published!(bucket, stage)
|
134
|
+
current_publish = last_published
|
130
135
|
current_publish["#{bucket}::#{stage}"] = nil
|
131
136
|
File.write(LAST_PUBLISHED_FILE, current_publish.to_yaml)
|
132
137
|
end
|
133
138
|
|
134
|
-
def
|
135
|
-
return false unless last_publish_time =
|
139
|
+
def published?(file, bucket, stage)
|
140
|
+
return false unless (last_publish_time = last_published["#{bucket}::#{stage}"])
|
141
|
+
|
136
142
|
File.mtime(file) < Time.parse(last_publish_time)
|
137
143
|
end
|
138
144
|
|
139
|
-
def
|
145
|
+
def put_object(s3_client, bucket, target_path, path, file, only_gzip, extra_options)
|
146
|
+
prefer_cf_mime_types = extra_options[:prefer_cf_mime_types] || false
|
147
|
+
|
140
148
|
base_name = File.basename(file)
|
141
|
-
mime_type = mime_type_for_file(base_name)
|
149
|
+
mime_type = mime_type_for_file(base_name, prefer_cf_mime_types)
|
142
150
|
options = {
|
143
|
-
:
|
144
|
-
:
|
145
|
-
:
|
146
|
-
:
|
151
|
+
bucket: bucket,
|
152
|
+
key: add_prefix(path, prefix: target_path),
|
153
|
+
body: File.read(file),
|
154
|
+
acl: "public-read"
|
147
155
|
}
|
148
156
|
|
149
157
|
options.merge!(build_redirect_hash(path, extra_options[:redirect]))
|
150
158
|
options.merge!(extra_options[:write] || {})
|
151
159
|
|
160
|
+
object_write_options = extra_options[:object_write] || {}
|
161
|
+
object_write_options.each do |pattern, object_options|
|
162
|
+
options.merge!(object_options) if File.fnmatch(pattern, options[:key])
|
163
|
+
end
|
164
|
+
|
152
165
|
if mime_type
|
153
166
|
options.merge!(build_content_type_hash(mime_type))
|
154
167
|
|
155
168
|
if mime_type.sub_type == "gzip"
|
156
169
|
options.merge!(build_gzip_content_encoding_hash)
|
157
|
-
options.merge!(build_gzip_content_type_hash(file, mime_type))
|
170
|
+
options.merge!(build_gzip_content_type_hash(file, mime_type, prefer_cf_mime_types))
|
158
171
|
|
159
172
|
# upload as original file name
|
160
|
-
options
|
173
|
+
options[:key] = add_prefix(orig_name(path), prefix: target_path) if only_gzip
|
161
174
|
end
|
162
175
|
end
|
163
176
|
|
164
|
-
|
177
|
+
s3_client.put_object(options)
|
165
178
|
end
|
166
179
|
|
167
|
-
def
|
180
|
+
def build_redirect_hash(path, redirect_options)
|
168
181
|
return {} unless redirect_options && redirect_options[path]
|
169
182
|
|
170
|
-
{ :
|
183
|
+
{ website_redirect_location: redirect_options[path] }
|
171
184
|
end
|
172
185
|
|
173
|
-
def
|
174
|
-
{ :
|
186
|
+
def build_content_type_hash(mime_type)
|
187
|
+
{ content_type: mime_type.content_type }
|
175
188
|
end
|
176
189
|
|
177
|
-
def
|
178
|
-
{ :
|
190
|
+
def build_gzip_content_encoding_hash
|
191
|
+
{ content_encoding: "gzip" }
|
179
192
|
end
|
180
193
|
|
181
|
-
def
|
182
|
-
File.exist?(
|
194
|
+
def gzipped_version?(file)
|
195
|
+
File.exist?(gzip_name(file))
|
183
196
|
end
|
184
197
|
|
185
|
-
def
|
186
|
-
orig_name =
|
187
|
-
orig_mime = mime_type_for_file(orig_name)
|
198
|
+
def build_gzip_content_type_hash(file, _mime_type, prefer_cf_mime_types)
|
199
|
+
orig_name = orig_name(file)
|
200
|
+
orig_mime = mime_type_for_file(orig_name, prefer_cf_mime_types)
|
188
201
|
|
189
202
|
return {} unless orig_mime && File.exist?(orig_name)
|
190
203
|
|
191
|
-
{ :
|
204
|
+
{ content_type: orig_mime.content_type }
|
192
205
|
end
|
193
206
|
|
194
|
-
def
|
195
|
-
|
196
|
-
|
207
|
+
def mime_type_for_file(file, prefer_cf_mime_types)
|
208
|
+
types = MIME::Types.type_for(file)
|
209
|
+
|
210
|
+
if prefer_cf_mime_types
|
211
|
+
intersection = types & Capistrano::S3::MIMETypes::CF_MIME_TYPES
|
212
|
+
|
213
|
+
types = intersection unless intersection.empty?
|
214
|
+
end
|
215
|
+
|
216
|
+
types.first
|
197
217
|
end
|
198
218
|
|
199
|
-
def
|
219
|
+
def gzip_name(file)
|
200
220
|
"#{file}.gz"
|
201
221
|
end
|
202
222
|
|
203
|
-
def
|
223
|
+
def orig_name(file)
|
204
224
|
file.sub(/\.gz$/, "")
|
205
225
|
end
|
206
226
|
|
207
|
-
def
|
227
|
+
def add_prefix(path, prefix:)
|
208
228
|
if prefix.empty?
|
209
229
|
path
|
210
230
|
else
|
211
231
|
File.join(prefix, path)
|
212
232
|
end
|
213
233
|
end
|
234
|
+
end
|
214
235
|
end
|
215
236
|
end
|
216
237
|
end
|
data/lib/capistrano/s3.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "capistrano"
|
2
4
|
require "capistrano/s3/publisher"
|
3
5
|
require "capistrano/s3/version"
|
4
6
|
require "capistrano/s3/defaults"
|
7
|
+
require "capistrano/s3/mime_types"
|
5
8
|
|
6
|
-
if Gem::Specification.find_by_name(
|
7
|
-
load File.expand_path(
|
9
|
+
if Gem::Specification.find_by_name("capistrano").version >= Gem::Version.new("3.0.0")
|
10
|
+
load File.expand_path("tasks/capistrano_3.rb", __dir__)
|
8
11
|
else
|
9
|
-
require_relative
|
12
|
+
require_relative "tasks/capistrano_2"
|
10
13
|
end
|
@@ -1,7 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Capistrano
|
4
|
+
# rubocop:disable Metrics/BlockLength
|
2
5
|
Configuration.instance(true).load do
|
3
6
|
def _cset(name, *args, &block)
|
4
|
-
set(name, *args, &block)
|
7
|
+
set(name, *args, &block) unless exists?(name)
|
5
8
|
end
|
6
9
|
|
7
10
|
Capistrano::S3::Defaults.populate(self, :_cset)
|
@@ -16,14 +19,21 @@ module Capistrano
|
|
16
19
|
|
17
20
|
desc "Waits until the last CloudFront invalidation batch is completed"
|
18
21
|
task :wait_for_invalidation do
|
19
|
-
S3::Publisher.check_invalidation(region, access_key_id, secret_access_key,
|
22
|
+
S3::Publisher.check_invalidation(region, access_key_id, secret_access_key,
|
23
|
+
distribution_id)
|
20
24
|
end
|
21
25
|
|
22
26
|
desc "Upload files to the bucket in the current state"
|
23
27
|
task :upload_files do
|
24
|
-
extra_options = {
|
28
|
+
extra_options = {
|
29
|
+
write: bucket_write_options,
|
30
|
+
redirect: redirect_options,
|
31
|
+
object_write: object_write_options,
|
32
|
+
prefer_cf_mime_types: prefer_cf_mime_types
|
33
|
+
}
|
25
34
|
S3::Publisher.publish!(region, access_key_id, secret_access_key,
|
26
|
-
|
35
|
+
bucket, deployment_path, target_path, distribution_id,
|
36
|
+
invalidations, exclusions, only_gzip, extra_options)
|
27
37
|
end
|
28
38
|
end
|
29
39
|
|
@@ -31,7 +41,10 @@ module Capistrano
|
|
31
41
|
s3.upload_files
|
32
42
|
end
|
33
43
|
|
34
|
-
task :restart do
|
44
|
+
task :restart do
|
45
|
+
# @todo remove?
|
46
|
+
end
|
35
47
|
end
|
36
48
|
end
|
49
|
+
# rubocop:enable Metrics/BlockLength
|
37
50
|
end
|
@@ -1,26 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
namespace :load do
|
2
4
|
task :defaults do
|
3
5
|
Capistrano::S3::Defaults.populate(self, :set)
|
4
6
|
end
|
5
7
|
end
|
6
8
|
|
9
|
+
# rubocop:disable Metrics/BlockLength
|
7
10
|
namespace :deploy do
|
8
11
|
namespace :s3 do
|
9
12
|
desc "Empties bucket of all files. Caution when using this command, as it cannot be undone!"
|
10
13
|
task :empty do
|
11
|
-
Capistrano::S3::Publisher.clear!(fetch(:region), fetch(:access_key_id),
|
14
|
+
Capistrano::S3::Publisher.clear!(fetch(:region), fetch(:access_key_id),
|
15
|
+
fetch(:secret_access_key), fetch(:bucket), fetch(:stage))
|
12
16
|
end
|
13
17
|
|
14
18
|
desc "Waits until the last CloudFront invalidation batch is completed"
|
15
19
|
task :wait_for_invalidation do
|
16
|
-
Capistrano::S3::Publisher.check_invalidation(fetch(:region), fetch(:access_key_id),
|
20
|
+
Capistrano::S3::Publisher.check_invalidation(fetch(:region), fetch(:access_key_id),
|
21
|
+
fetch(:secret_access_key),
|
22
|
+
fetch(:distribution_id),
|
23
|
+
fetch(:stage))
|
17
24
|
end
|
18
25
|
|
19
26
|
desc "Upload files to the bucket in the current state"
|
20
27
|
task :upload_files do
|
21
|
-
extra_options = {
|
22
|
-
|
23
|
-
|
28
|
+
extra_options = {
|
29
|
+
write: fetch(:bucket_write_options),
|
30
|
+
redirect: fetch(:redirect_options),
|
31
|
+
object_write: fetch(:object_write_options),
|
32
|
+
prefer_cf_mime_types: fetch(:prefer_cf_mime_types)
|
33
|
+
}
|
34
|
+
Capistrano::S3::Publisher.publish!(fetch(:region), fetch(:access_key_id),
|
35
|
+
fetch(:secret_access_key), fetch(:bucket),
|
36
|
+
fetch(:deployment_path), fetch(:target_path),
|
37
|
+
fetch(:distribution_id), fetch(:invalidations),
|
38
|
+
fetch(:exclusions), fetch(:only_gzip), extra_options,
|
39
|
+
fetch(:stage))
|
24
40
|
end
|
25
41
|
end
|
26
42
|
|
@@ -28,3 +44,4 @@ namespace :deploy do
|
|
28
44
|
invoke("deploy:s3:upload_files")
|
29
45
|
end
|
30
46
|
end
|
47
|
+
# rubocop:enable Metrics/BlockLength
|