middleman-aws-deploy 0.5.0
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.
- data/Gemfile +2 -0
- data/README.md +38 -0
- data/lib/middleman-aws-deploy.rb +6 -0
- data/lib/middleman-aws-deploy/cloudfront-invalidate.rb +98 -0
- data/lib/middleman-aws-deploy/s3deploy.rb +98 -0
- data/lib/middleman-aws-deploy/version.rb +5 -0
- data/middleman-aws-deploy.gemspec +26 -0
- metadata +133 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# Middleman AWS Deploy
|
2
|
+
|
3
|
+
Deploys build files to S3 at the end of the Middleman build toolchain, and/or invalidates Cloudfront.
|
4
|
+
|
5
|
+
In your `config.rb` file, add.
|
6
|
+
|
7
|
+
```
|
8
|
+
configure :build do
|
9
|
+
# ...
|
10
|
+
if ENV.include?('DEPLOY')
|
11
|
+
activate :s3_deploy do |s3|
|
12
|
+
s3.access_key_id = ENV['AWS_ACCESS_KEY_ID']
|
13
|
+
s3.secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
|
14
|
+
s3.bucket = ENV['AWS_S3_BUCKET']
|
15
|
+
end
|
16
|
+
activate :invalidate_cloudfront do |cf|
|
17
|
+
cf.access_key_id = ENV['AWS_ACCESS_KEY_ID']
|
18
|
+
cf.secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
|
19
|
+
cf.distribution_id = ENV['AWS_CLOUDFRONT_DIST_ID']
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
```
|
24
|
+
|
25
|
+
With valid AWS creds, an s3 bucket, and cloudfront distribution id, you can deploy
|
26
|
+
with the env var DEPLOY as a trigger (you can use whichever kind of trigger you like):
|
27
|
+
|
28
|
+
```
|
29
|
+
DEPLOY=true middleman build
|
30
|
+
```
|
31
|
+
|
32
|
+
### S3 Deployment Note
|
33
|
+
|
34
|
+
The S3 deployer will check local file's md5 hash against the remote s3 etag. If
|
35
|
+
they are the same, it will not upload the identical file. However, if
|
36
|
+
you have `:cachebuster` set to active the hashes will always end up different
|
37
|
+
on every build, triggering an upload. There's little such danger however for
|
38
|
+
assets (css, images, etc).
|
@@ -0,0 +1,6 @@
|
|
1
|
+
require "middleman-core"
|
2
|
+
require "middleman-aws-deploy/s3deploy"
|
3
|
+
require "middleman-aws-deploy/cloudfront-invalidate"
|
4
|
+
|
5
|
+
::Middleman::Extensions.register(:s3_deploy, Middleman::AWSDeploy::S3Deploy)
|
6
|
+
::Middleman::Extensions.register(:invalidate_cloudfront, Middleman::AWSDeploy::InvalidateCloudfront)
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'uri'
|
3
|
+
require 'hmac'
|
4
|
+
require 'hmac-sha1'
|
5
|
+
require 'net/https'
|
6
|
+
require 'base64'
|
7
|
+
|
8
|
+
CF_BATCH_SIZE = 1000
|
9
|
+
|
10
|
+
module Middleman
|
11
|
+
module AWSDeploy
|
12
|
+
module InvalidateCloudfront
|
13
|
+
Opts = Struct.new(:access_key_id, :secret_access_key, :distribution_id)
|
14
|
+
|
15
|
+
class << self
|
16
|
+
module Options
|
17
|
+
def aws_cfi_opts
|
18
|
+
::Middleman::AWSDeploy::InvalidateCloudfront.options
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def options
|
23
|
+
@@options
|
24
|
+
end
|
25
|
+
|
26
|
+
def registered(app, opts_data = {}, &block)
|
27
|
+
@@options = Opts.new(opts_data)
|
28
|
+
yield @@options if block_given?
|
29
|
+
|
30
|
+
app.send :include, Options
|
31
|
+
app.after_build do
|
32
|
+
puts "== Invalidating CloudFront"
|
33
|
+
|
34
|
+
# you can only invalidate in batches of 1000
|
35
|
+
def invalidate(files, pos=0)
|
36
|
+
paths, count = "", 0
|
37
|
+
|
38
|
+
escaped_files = []
|
39
|
+
files[pos...(pos+CF_BATCH_SIZE)].each do |key|
|
40
|
+
count += 1
|
41
|
+
escaped_files << (escaped_file = URI.escape(key.to_s))
|
42
|
+
paths += "<Path>#{escaped_file}</Path>"
|
43
|
+
end
|
44
|
+
|
45
|
+
return if count < 1
|
46
|
+
|
47
|
+
paths = "<Paths><Quantity>#{count}</Quantity><Items>#{paths}</Items></Paths>"
|
48
|
+
|
49
|
+
digest = HMAC::SHA1.new(aws_cfi_opts.secret_access_key)
|
50
|
+
digest << date = Time.now.utc.strftime("%a, %d %b %Y %H:%M:%S %Z")
|
51
|
+
|
52
|
+
uri = URI.parse("https://cloudfront.amazonaws.com/2012-07-01/distribution/#{aws_cfi_opts.distribution_id}/invalidation")
|
53
|
+
header = {
|
54
|
+
'x-amz-date' => date,
|
55
|
+
'Content-Type' => 'text/xml',
|
56
|
+
'Authorization' => "AWS %s:%s" %
|
57
|
+
[aws_cfi_opts.access_key_id, Base64.encode64(digest.digest)]
|
58
|
+
}
|
59
|
+
req = Net::HTTP::Post.new(uri.path)
|
60
|
+
req.initialize_http_header(header)
|
61
|
+
body = "<InvalidationBatch xmlns=\"http://cloudfront.amazonaws.com/doc/2012-07-01/\">#{paths}<CallerReference>ref_#{Time.now.utc.to_i}</CallerReference></InvalidationBatch>"
|
62
|
+
req.body = body
|
63
|
+
|
64
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
65
|
+
http.use_ssl = true
|
66
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
67
|
+
res = http.request(req)
|
68
|
+
if res.code == '201'
|
69
|
+
puts "CloudFront reloading #{count} paths"
|
70
|
+
else
|
71
|
+
$stderr.puts "CloudFront Invalidate failed with error #{res.code}"
|
72
|
+
$stderr.puts res.body
|
73
|
+
$stderr.puts escaped_files.join("\n")
|
74
|
+
end
|
75
|
+
return if res.code == 400
|
76
|
+
|
77
|
+
if count == CF_BATCH_SIZE
|
78
|
+
invalidate(files, pos+count)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def cf_files
|
83
|
+
files = Dir["./build/**/.*", "./build/**/*"]
|
84
|
+
.reject{|f| File.directory?(f) }.map{|f| f.sub(/^(?:\.\/)?build\//, '/') }
|
85
|
+
# if :directory_indexes is active, we must invalidate both files and dirs
|
86
|
+
# doing this 100% of the time, it would be nice if we could find of
|
87
|
+
files += files.map{|f| f.gsub(/\/index\.html$/, '/') }
|
88
|
+
files.uniq!
|
89
|
+
end
|
90
|
+
|
91
|
+
invalidate(cf_files)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
alias :included :registered
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'fog'
|
3
|
+
require 'parallel'
|
4
|
+
require 'progressbar'
|
5
|
+
require 'digest/md5'
|
6
|
+
|
7
|
+
module Middleman
|
8
|
+
module AWSDeploy
|
9
|
+
module S3Deploy
|
10
|
+
Opts = Struct.new(:access_key_id, :secret_access_key, :threads, :bucket, :region)
|
11
|
+
|
12
|
+
class << self
|
13
|
+
module Options
|
14
|
+
def aws_s3_opts
|
15
|
+
::Middleman::AWSDeploy::S3Deploy.options
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def options
|
20
|
+
@@options
|
21
|
+
end
|
22
|
+
|
23
|
+
def registered(app, opts_data = {}, &block)
|
24
|
+
@@options = Opts.new(opts_data)
|
25
|
+
yield @@options if block_given?
|
26
|
+
|
27
|
+
@@options.threads ||= 8
|
28
|
+
app.send :include, Options
|
29
|
+
app.after_build do
|
30
|
+
puts "== Uploading to S3"
|
31
|
+
|
32
|
+
def storage
|
33
|
+
@storage ||= Fog::Storage.new({
|
34
|
+
:provider => 'AWS',
|
35
|
+
:aws_access_key_id => aws_s3_opts.access_key_id,
|
36
|
+
:aws_secret_access_key => aws_s3_opts.secret_access_key,
|
37
|
+
:region => aws_s3_opts.region
|
38
|
+
})
|
39
|
+
end
|
40
|
+
|
41
|
+
def remote_files
|
42
|
+
@directory ||= storage.directories.get(aws_s3_opts.bucket)
|
43
|
+
@remote_files ||= @directory.files
|
44
|
+
end
|
45
|
+
|
46
|
+
def etag(key)
|
47
|
+
object = remote_files.head(key)
|
48
|
+
object && object.etag
|
49
|
+
end
|
50
|
+
|
51
|
+
def upload(key)
|
52
|
+
begin
|
53
|
+
remote_files.new({
|
54
|
+
:key => key,
|
55
|
+
:body => File.open("./build/#{key}"),
|
56
|
+
:public => true,
|
57
|
+
:acl => 'public-read'
|
58
|
+
}).save
|
59
|
+
rescue
|
60
|
+
$stderr.puts "Failed to upload #{key}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def files
|
65
|
+
@files ||= Dir["./build/**/.*", "./build/**/*"]
|
66
|
+
.reject{|f| File.directory?(f) }.map{|f| f.sub(/^(?:\.\/)?build\//, '') }
|
67
|
+
end
|
68
|
+
|
69
|
+
# no need to upload an exact copy of an existing remote file
|
70
|
+
replace = []
|
71
|
+
progress = ProgressBar.new('Hash check', files.count)
|
72
|
+
Parallel.each(files,
|
73
|
+
:in_threads => aws_s3_opts.threads,
|
74
|
+
:finish => lambda {|i, item| progress.inc }) do |f|
|
75
|
+
md5 = Digest::MD5.hexdigest(File.read("./build/#{f}"))
|
76
|
+
replace << f if md5 != etag(f)
|
77
|
+
end
|
78
|
+
progress.finish
|
79
|
+
|
80
|
+
if replace.empty?
|
81
|
+
puts "Nothing to upload"
|
82
|
+
else
|
83
|
+
# upload necessary files
|
84
|
+
progress = ProgressBar.new('Uploading', replace.count)
|
85
|
+
Parallel.each(replace,
|
86
|
+
:in_threads => aws_s3_opts.threads,
|
87
|
+
:finish => lambda {|i, item| progress.inc }) do |f|
|
88
|
+
upload(f)
|
89
|
+
end
|
90
|
+
progress.finish
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
alias :included :registered
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "middleman-aws-deploy/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "middleman-aws-deploy"
|
7
|
+
s.version = Middleman::AWSDeploy::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Eric Redmond"]
|
10
|
+
s.email = ["eric.redmond@gmail.com"]
|
11
|
+
s.homepage = "https://github.com/coderoshi/middleman-aws-deploy"
|
12
|
+
s.summary = %q{Deploy to AWS with Middleman}
|
13
|
+
s.description = %q{Adds S3 deploy and Cloudfront invalidation support to the Middleman build toolchain}
|
14
|
+
|
15
|
+
s.rubyforge_project = "middleman-aws-deploy"
|
16
|
+
|
17
|
+
s.files = `git ls-files -z`.split("\0")
|
18
|
+
s.test_files = `git ls-files -z -- {fixtures,features}/*`.split("\0")
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_dependency("middleman-core", ["~> 3.0.0"])
|
22
|
+
s.add_dependency("ruby-hmac", ["~> 0.4.0"])
|
23
|
+
s.add_dependency("parallel", ["~> 0.6.1"])
|
24
|
+
s.add_dependency("fog", ["~> 1.8.0"])
|
25
|
+
s.add_dependency("progressbar", ["~> 0.12.0"])
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: middleman-aws-deploy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Eric Redmond
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-01-09 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: middleman-core
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.0.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 3.0.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: ruby-hmac
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 0.4.0
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 0.4.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: parallel
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.6.1
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.6.1
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: fog
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 1.8.0
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 1.8.0
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: progressbar
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 0.12.0
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 0.12.0
|
94
|
+
description: Adds S3 deploy and Cloudfront invalidation support to the Middleman build
|
95
|
+
toolchain
|
96
|
+
email:
|
97
|
+
- eric.redmond@gmail.com
|
98
|
+
executables: []
|
99
|
+
extensions: []
|
100
|
+
extra_rdoc_files: []
|
101
|
+
files:
|
102
|
+
- Gemfile
|
103
|
+
- README.md
|
104
|
+
- lib/middleman-aws-deploy.rb
|
105
|
+
- lib/middleman-aws-deploy/cloudfront-invalidate.rb
|
106
|
+
- lib/middleman-aws-deploy/s3deploy.rb
|
107
|
+
- lib/middleman-aws-deploy/version.rb
|
108
|
+
- middleman-aws-deploy.gemspec
|
109
|
+
homepage: https://github.com/coderoshi/middleman-aws-deploy
|
110
|
+
licenses: []
|
111
|
+
post_install_message:
|
112
|
+
rdoc_options: []
|
113
|
+
require_paths:
|
114
|
+
- lib
|
115
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
116
|
+
none: false
|
117
|
+
requirements:
|
118
|
+
- - ! '>='
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
122
|
+
none: false
|
123
|
+
requirements:
|
124
|
+
- - ! '>='
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
requirements: []
|
128
|
+
rubyforge_project: middleman-aws-deploy
|
129
|
+
rubygems_version: 1.8.24
|
130
|
+
signing_key:
|
131
|
+
specification_version: 3
|
132
|
+
summary: Deploy to AWS with Middleman
|
133
|
+
test_files: []
|