capistrano-s3 2.4.0 → 3.0.0.pre2
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 +2 -1
- data/.github/workflows/build.yml +20 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +59 -0
- data/CHANGELOG.md +10 -0
- data/Gemfile +15 -2
- data/Guardfile +18 -0
- data/README.md +2 -2
- data/Rakefile +7 -5
- data/capistrano-s3.gemspec +32 -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 +7 -5
- data/lib/capistrano/s3/publisher.rb +105 -103
- data/lib/capistrano/s3/version.rb +3 -1
- data/lib/capistrano/s3.rb +5 -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/spec_helper.rb +5 -3
- data.tar.gz.sig +4 -1
- metadata +42 -93
- metadata.gz.sig +0 -0
- data/.travis.yml +0 -21
- data/certs/aleksandrs-ledovskis--2018-11-04-2020-11-03.pem +0 -26
- data/certs/j15e.pem +0 -21
- data/spec/publisher_spec.rb +0 -153
@@ -1,107 +1,111 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
4
|
-
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"
|
5
8
|
|
6
9
|
module Capistrano
|
7
10
|
module S3
|
8
11
|
module Publisher
|
9
|
-
LAST_PUBLISHED_FILE =
|
10
|
-
LAST_INVALIDATION_FILE =
|
12
|
+
LAST_PUBLISHED_FILE = ".last_published"
|
13
|
+
LAST_INVALIDATION_FILE = ".last_invalidation"
|
11
14
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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)
|
16
20
|
|
17
|
-
|
18
|
-
|
19
|
-
next if
|
20
|
-
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)
|
21
25
|
|
22
|
-
path =
|
23
|
-
path.gsub!(
|
26
|
+
path = base_file_path(deployment_path_absolute, file)
|
27
|
+
path.gsub!(%r{^/}, "") # Remove preceding slash for S3
|
24
28
|
|
25
|
-
|
29
|
+
put_object(s3_client, bucket, target_path, path, file, only_gzip, extra_options)
|
26
30
|
end
|
27
|
-
end
|
28
31
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
48
52
|
end
|
49
|
-
end
|
50
53
|
|
51
|
-
|
52
|
-
|
54
|
+
published_to!(bucket, stage)
|
55
|
+
end
|
53
56
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
+
def clear!(region, key, secret, bucket, stage = "default")
|
58
|
+
s3 = establish_s3_connection!(region, key, secret)
|
59
|
+
s3.buckets[bucket].clear!
|
57
60
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
+
clear_published!(bucket, stage)
|
62
|
+
FileUtils.rm(LAST_INVALIDATION_FILE)
|
63
|
+
end
|
61
64
|
|
62
|
-
|
63
|
-
|
65
|
+
def check_invalidation(region, key, secret, distribution_id, _stage = "default")
|
66
|
+
last_invalidation_id = File.read(LAST_INVALIDATION_FILE).strip
|
64
67
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
69
74
|
end
|
70
|
-
end
|
71
75
|
|
72
|
-
|
76
|
+
private
|
73
77
|
|
74
78
|
# Establishes the connection to Amazon S3
|
75
|
-
def
|
79
|
+
def establish_connection!(klass, region, key, secret)
|
76
80
|
# Send logging to STDOUT
|
77
81
|
Aws.config[:logger] = ::Logger.new(STDOUT)
|
78
82
|
Aws.config[:log_formatter] = Aws::Log::Formatter.colored
|
79
83
|
klass.new(
|
80
|
-
:
|
81
|
-
:
|
82
|
-
:
|
84
|
+
region: region,
|
85
|
+
access_key_id: key,
|
86
|
+
secret_access_key: secret
|
83
87
|
)
|
84
88
|
end
|
85
89
|
|
86
|
-
def
|
87
|
-
|
90
|
+
def establish_cf_client_connection!(region, key, secret)
|
91
|
+
establish_connection!(Aws::CloudFront::Client, region, key, secret)
|
88
92
|
end
|
89
93
|
|
90
|
-
def
|
91
|
-
|
94
|
+
def establish_s3_client_connection!(region, key, secret)
|
95
|
+
establish_connection!(Aws::S3::Client, region, key, secret)
|
92
96
|
end
|
93
97
|
|
94
|
-
def
|
95
|
-
|
98
|
+
def establish_s3_connection!(region, key, secret)
|
99
|
+
establish_connection!(Aws::S3, region, key, secret)
|
96
100
|
end
|
97
101
|
|
98
|
-
def
|
102
|
+
def base_file_path(root, file)
|
99
103
|
file.gsub(root, "")
|
100
104
|
end
|
101
105
|
|
102
|
-
def
|
106
|
+
def files(deployment_path, exclusions)
|
103
107
|
globbed_paths = Dir.glob(
|
104
|
-
File.join(deployment_path,
|
108
|
+
File.join(deployment_path, "**", "*"),
|
105
109
|
File::FNM_DOTMATCH # Else Unix-like hidden files will be ignored
|
106
110
|
)
|
107
111
|
|
@@ -112,41 +116,42 @@ module Capistrano
|
|
112
116
|
globbed_paths - excluded_paths
|
113
117
|
end
|
114
118
|
|
115
|
-
def
|
116
|
-
if File.
|
119
|
+
def last_published
|
120
|
+
if File.exist? LAST_PUBLISHED_FILE
|
117
121
|
YAML.load_file(LAST_PUBLISHED_FILE) || {}
|
118
122
|
else
|
119
123
|
{}
|
120
124
|
end
|
121
125
|
end
|
122
126
|
|
123
|
-
def
|
124
|
-
current_publish =
|
127
|
+
def published_to!(bucket, stage)
|
128
|
+
current_publish = last_published
|
125
129
|
current_publish["#{bucket}::#{stage}"] = Time.now.iso8601
|
126
130
|
File.write(LAST_PUBLISHED_FILE, current_publish.to_yaml)
|
127
131
|
end
|
128
132
|
|
129
|
-
def
|
130
|
-
current_publish =
|
133
|
+
def clear_published!(bucket, stage)
|
134
|
+
current_publish = last_published
|
131
135
|
current_publish["#{bucket}::#{stage}"] = nil
|
132
136
|
File.write(LAST_PUBLISHED_FILE, current_publish.to_yaml)
|
133
137
|
end
|
134
138
|
|
135
|
-
def
|
136
|
-
return false unless last_publish_time =
|
139
|
+
def published?(file, bucket, stage)
|
140
|
+
return false unless (last_publish_time = last_published["#{bucket}::#{stage}"])
|
141
|
+
|
137
142
|
File.mtime(file) < Time.parse(last_publish_time)
|
138
143
|
end
|
139
144
|
|
140
|
-
def
|
145
|
+
def put_object(s3_client, bucket, target_path, path, file, only_gzip, extra_options)
|
141
146
|
prefer_cf_mime_types = extra_options[:prefer_cf_mime_types] || false
|
142
147
|
|
143
148
|
base_name = File.basename(file)
|
144
149
|
mime_type = mime_type_for_file(base_name, prefer_cf_mime_types)
|
145
150
|
options = {
|
146
|
-
:
|
147
|
-
:
|
148
|
-
:
|
149
|
-
:
|
151
|
+
bucket: bucket,
|
152
|
+
key: add_prefix(path, prefix: target_path),
|
153
|
+
body: File.read(file),
|
154
|
+
acl: "public-read"
|
150
155
|
}
|
151
156
|
|
152
157
|
options.merge!(build_redirect_hash(path, extra_options[:redirect]))
|
@@ -154,9 +159,7 @@ module Capistrano
|
|
154
159
|
|
155
160
|
object_write_options = extra_options[:object_write] || {}
|
156
161
|
object_write_options.each do |pattern, object_options|
|
157
|
-
if File.fnmatch(pattern, options[:key])
|
158
|
-
options.merge!(object_options)
|
159
|
-
end
|
162
|
+
options.merge!(object_options) if File.fnmatch(pattern, options[:key])
|
160
163
|
end
|
161
164
|
|
162
165
|
if mime_type
|
@@ -167,69 +170,68 @@ module Capistrano
|
|
167
170
|
options.merge!(build_gzip_content_type_hash(file, mime_type, prefer_cf_mime_types))
|
168
171
|
|
169
172
|
# upload as original file name
|
170
|
-
options
|
173
|
+
options[:key] = add_prefix(orig_name(path), prefix: target_path) if only_gzip
|
171
174
|
end
|
172
175
|
end
|
173
176
|
|
174
|
-
|
177
|
+
s3_client.put_object(options)
|
175
178
|
end
|
176
179
|
|
177
|
-
def
|
180
|
+
def build_redirect_hash(path, redirect_options)
|
178
181
|
return {} unless redirect_options && redirect_options[path]
|
179
182
|
|
180
|
-
{ :
|
183
|
+
{ website_redirect_location: redirect_options[path] }
|
181
184
|
end
|
182
185
|
|
183
|
-
def
|
184
|
-
{ :
|
186
|
+
def build_content_type_hash(mime_type)
|
187
|
+
{ content_type: mime_type.content_type }
|
185
188
|
end
|
186
189
|
|
187
|
-
def
|
188
|
-
{ :
|
190
|
+
def build_gzip_content_encoding_hash
|
191
|
+
{ content_encoding: "gzip" }
|
189
192
|
end
|
190
193
|
|
191
|
-
def
|
192
|
-
File.exist?(
|
194
|
+
def gzipped_version?(file)
|
195
|
+
File.exist?(gzip_name(file))
|
193
196
|
end
|
194
197
|
|
195
|
-
def
|
196
|
-
orig_name =
|
198
|
+
def build_gzip_content_type_hash(file, _mime_type, prefer_cf_mime_types)
|
199
|
+
orig_name = orig_name(file)
|
197
200
|
orig_mime = mime_type_for_file(orig_name, prefer_cf_mime_types)
|
198
201
|
|
199
202
|
return {} unless orig_mime && File.exist?(orig_name)
|
200
203
|
|
201
|
-
{ :
|
204
|
+
{ content_type: orig_mime.content_type }
|
202
205
|
end
|
203
206
|
|
204
|
-
def
|
207
|
+
def mime_type_for_file(file, prefer_cf_mime_types)
|
205
208
|
types = MIME::Types.type_for(file)
|
206
209
|
|
207
210
|
if prefer_cf_mime_types
|
208
211
|
intersection = types & Capistrano::S3::MIMETypes::CF_MIME_TYPES
|
209
212
|
|
210
|
-
unless intersection.empty?
|
211
|
-
types = intersection
|
212
|
-
end
|
213
|
+
types = intersection unless intersection.empty?
|
213
214
|
end
|
214
215
|
|
215
216
|
types.first
|
216
217
|
end
|
217
218
|
|
218
|
-
def
|
219
|
+
def gzip_name(file)
|
219
220
|
"#{file}.gz"
|
220
221
|
end
|
221
222
|
|
222
|
-
def
|
223
|
+
def orig_name(file)
|
223
224
|
file.sub(/\.gz$/, "")
|
224
225
|
end
|
225
226
|
|
226
|
-
def
|
227
|
+
def add_prefix(path, prefix:)
|
227
228
|
if prefix.empty?
|
228
229
|
path
|
229
230
|
else
|
230
231
|
File.join(prefix, path)
|
231
232
|
end
|
232
233
|
end
|
234
|
+
end
|
233
235
|
end
|
234
236
|
end
|
235
237
|
end
|
data/lib/capistrano/s3.rb
CHANGED
@@ -1,11 +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"
|
5
7
|
require "capistrano/s3/mime_types"
|
6
8
|
|
7
|
-
if Gem::Specification.find_by_name(
|
8
|
-
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__)
|
9
11
|
else
|
10
|
-
require_relative
|
12
|
+
require_relative "tasks/capistrano_2"
|
11
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
|