middleman-cdn 0.1.9 → 0.1.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +69 -34
- data/lib/middleman-cdn/cdns/base.rb +2 -2
- data/lib/middleman-cdn/cdns/rackspace.rb +64 -0
- data/lib/middleman-cdn/clients/rackspace.rb +82 -0
- data/lib/middleman-cdn/commands.rb +31 -27
- data/lib/middleman-cdn/extension.rb +1 -0
- data/lib/middleman-cdn/version.rb +1 -1
- data/middleman-cdn.gemspec +2 -0
- data/spec/lib/middleman-cdn/cdns/base_protocol.rb +2 -2
- data/spec/lib/middleman-cdn/cdns/rackspace_spec.rb +216 -0
- data/spec/lib/middleman-cdn/clients/rackspace_response_doubles.rb +65 -0
- data/spec/lib/middleman-cdn/clients/rackspace_spec.rb +122 -0
- data/spec/spec_helper.rb +3 -0
- metadata +35 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f1126fd1f909909f010c506e2239341214a4ef80
|
4
|
+
data.tar.gz: 6ee82a56f22971322621659548ff8430e9259f8a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9dbf7a85e93f6ed5caba2d3264379114fd318d04449c2b2487127095ee8ab0534ff767e39a7a6d5c7c745341a684093b3ea4b4daa4a96d33dd4f9be947b2f2cf
|
7
|
+
data.tar.gz: 2540c95e40446f4ac123da08e4929df6e2926481ff287c402824a4f8177256b7df42241f68f5ae29a82e1f8bc788d45a4164e6273a39f790f95ab01e4b57850e
|
data/README.md
CHANGED
@@ -1,21 +1,21 @@
|
|
1
1
|
# Middleman CDN
|
2
|
-
[![Gem Version](https://badge.fury.io/rb/middleman-cdn.svg)](http://badge.fury.io/rb/middleman-cdn) [![Build Status](https://travis-ci.org/leighmcculloch/middleman-cdn.svg)](https://travis-ci.org/leighmcculloch/middleman-cdn) [![Dependency Status](https://gemnasium.com/leighmcculloch/middleman-cdn.png)](https://gemnasium.com/leighmcculloch/middleman-cdn)
|
2
|
+
[![Gem Version](https://badge.fury.io/rb/middleman-cdn.svg)](http://badge.fury.io/rb/middleman-cdn) [![Build Status](https://travis-ci.org/leighmcculloch/middleman-cdn.svg)](https://travis-ci.org/leighmcculloch/middleman-cdn) [![Coverage Status](https://img.shields.io/coveralls/leighmcculloch/middleman-cdn.svg)](https://coveralls.io/r/leighmcculloch/middleman-cdn) [![Dependency Status](https://gemnasium.com/leighmcculloch/middleman-cdn.png)](https://gemnasium.com/leighmcculloch/middleman-cdn)
|
3
3
|
|
4
4
|
A [middleman](http://middlemanapp.com/) deploy tool for invalidating resources cached
|
5
5
|
on common Content Delivery Networks (CDNs).
|
6
6
|
|
7
7
|
* Cache invalidation of files on:
|
8
8
|
* [CloudFlare](https://cloudflare.com)
|
9
|
-
* [Fastly](https://fastly.com)
|
10
9
|
* [MaxCDN](https://maxcdn.com)
|
10
|
+
* [Fastly](https://fastly.com)
|
11
11
|
* [Amazon CloudFront](https://aws.amazon.com/cloudfront/)
|
12
|
+
* [Rackspace CloudFiles](http://www.rackspace.com/cloud/files/)
|
12
13
|
* Select files for invalidation with regex.
|
13
14
|
* Automatically invalidate after build.
|
14
|
-
* Manually trigger invalidation with single command.
|
15
|
+
* Manually trigger invalidation with a single command.
|
15
16
|
|
16
17
|
What's next?
|
17
|
-
|
18
|
-
* Add support for RackspaceCDN (Akamai).
|
18
|
+
* Invalidating files only when they've changed.
|
19
19
|
* [Open an issue](../../issues/new) if you'd like your CDN provider added.
|
20
20
|
|
21
21
|
# Usage
|
@@ -46,6 +46,12 @@ activate :cdn do |cdn|
|
|
46
46
|
'https://example.com',
|
47
47
|
]
|
48
48
|
}
|
49
|
+
cdn.maxcdn = {
|
50
|
+
alias: "...", # default ENV['MAXCDN_ALIAS']
|
51
|
+
consumer_key: "...", # default ENV['MAXCDN_CONSUMER_KEY']
|
52
|
+
consumer_secret: "...", # default ENV['MAXCDN_CONSUMER_SECRET']
|
53
|
+
zone_id: "...",
|
54
|
+
}
|
49
55
|
cdn.fastly = {
|
50
56
|
api_key: '...', # default ENV['FASTLY_API_KEY']
|
51
57
|
base_urls: [
|
@@ -53,17 +59,18 @@ activate :cdn do |cdn|
|
|
53
59
|
'https://www.example.com'
|
54
60
|
],
|
55
61
|
}
|
56
|
-
cdn.maxcdn = {
|
57
|
-
alias: "...", # default ENV['MAXCDN_ALIAS']
|
58
|
-
consumer_key: "...", # default ENV['MAXCDN_CONSUMER_KEY']
|
59
|
-
consumer_secret: "...", # default ENV['MAXCDN_CONSUMER_SECRET']
|
60
|
-
zone_id: "...",
|
61
|
-
}
|
62
62
|
cdn.cloudfront = {
|
63
63
|
access_key_id: '...', # default ENV['AWS_ACCESS_KEY_ID']
|
64
64
|
secret_access_key: '...', # default ENV['AWS_SECRET_ACCESS_KEY']
|
65
65
|
distribution_id: '...'
|
66
66
|
}
|
67
|
+
cdn.rackspace = {
|
68
|
+
username: "...", # default ENV['RACKSPACE_USERNAME']
|
69
|
+
api_key: "...", # default ENV['RACKSPACE_API_KEY']
|
70
|
+
region: "DFW", # DFW, SYD, IAD, ORD, HKG, etc
|
71
|
+
container: "...",
|
72
|
+
notification_email: "you@example.com" # optional
|
73
|
+
}
|
67
74
|
cdn.filter = /\.html/i # default /.*/
|
68
75
|
cdn.after_build = true # default is false
|
69
76
|
end
|
@@ -107,6 +114,23 @@ at.
|
|
107
114
|
|
108
115
|
CloudFlare invalidations often take a few seconds.
|
109
116
|
|
117
|
+
### Configuration: MaxCDN
|
118
|
+
|
119
|
+
The `maxcdn` parameter contains the information specific to your MaxCDN
|
120
|
+
account. Your `alias` can be found on the API tab of your MaxCDN account page,
|
121
|
+
and you'll need to create an `application` in your MaxCDN account which
|
122
|
+
will provide you with API keys. The extension works by invalidating files
|
123
|
+
in pull zones. Make sure you add your website as a pull zone.
|
124
|
+
|
125
|
+
| Parameter | Description |
|
126
|
+
|:--------- |:----------- |
|
127
|
+
| `alias` | You can find this by logging into MaxCDN, going to your account page, and then going to the API tab and it will be down the bottom right. |
|
128
|
+
| `consumer_key` | You can find this by logging into MaxCDN, going to your account page, and then going to the API tab and creating an application which will give you a key and secret. |
|
129
|
+
| `secret_key` | You can find this by logging into MaxCDN, going to your account page, and then going to the API tab and creating an application which will give you a key and secret. |
|
130
|
+
| `zone_id` | Each pull zone has a zone_id, you'll find this in your account. |
|
131
|
+
|
132
|
+
MaxCDN invalidations often take a few seconds.
|
133
|
+
|
110
134
|
### Configuration: Fastly
|
111
135
|
|
112
136
|
The `fastly` parameter contains the information specific to your Fastly
|
@@ -121,23 +145,6 @@ at.
|
|
121
145
|
|
122
146
|
Fastly invalidations often take a few seconds.
|
123
147
|
|
124
|
-
### Configuration: MaxCDN
|
125
|
-
|
126
|
-
The `maxcdn` parameter contains the information specific to your MaxCDN
|
127
|
-
account. You'll need to create an `application` in your MaxCDN account which
|
128
|
-
will provide you with API keys, and your alias can be found on the API tab of
|
129
|
-
your account page. The extension works by invalidating files in pull zones.
|
130
|
-
Make sure you add your website as a pull zone.
|
131
|
-
|
132
|
-
| Parameter | Description |
|
133
|
-
|:--------- |:----------- |
|
134
|
-
| `alias` | You can find this by logging into MaxCDN, going to your account page, and then going to the API tab and it will be down the bottom right. |
|
135
|
-
| `consumer_key` | You can find this by logging into MaxCDN, going to your account page, and then going to the API tab and creating an application which will give you a key and secret. |
|
136
|
-
| `secret_key` | You can find this by logging into MaxCDN, going to your account page, and then going to the API tab and creating an application which will give you a key and secret. |
|
137
|
-
| `zone_id` | Each pull zone has a zone_id, you'll find this in your account. |
|
138
|
-
|
139
|
-
MaxCDN invalidations often take a few seconds.
|
140
|
-
|
141
148
|
### Configuration: CloudFront
|
142
149
|
|
143
150
|
The `cloudfront` parameter contains the information specific to your AWS CloudFront
|
@@ -152,16 +159,40 @@ account and which distribution files should be invalidated for.
|
|
152
159
|
CloudFront invalidations take up to 15 minutes. You can monitor the progress of
|
153
160
|
the invalidation in your AWS Console.
|
154
161
|
|
162
|
+
### Configuration: Rackspace CloudFiles
|
163
|
+
|
164
|
+
The `rackspace` parameter contains the information specific to your Rackspace
|
165
|
+
account. The extension works by invalidating files stored at the edge on
|
166
|
+
Rackspace's CDN (Akaimi) that mirrors Rackspace's CloudFiles. If you specify a
|
167
|
+
notification email, you will receive an email from Akaimi when the invalidation
|
168
|
+
has been completed.
|
169
|
+
|
170
|
+
| Parameter | Description |
|
171
|
+
|:--------- |:----------- |
|
172
|
+
| `username` | Your Rackspace username. |
|
173
|
+
| `api_key` | Your Rackspace API key. |
|
174
|
+
| `region` | The region the CloudFiles container is stored in. Typically this is `DFW` unless you specified an alternative region for your container. Examples: `DFW`, `SYD`, `IAD`, `ORD`, `HKG`. |
|
175
|
+
| `container` | The CloudFiles container. |
|
176
|
+
| `notification_email` | An email will be sent to this address after the invalidation has completed. |
|
177
|
+
|
178
|
+
Note:
|
179
|
+
1. At the time of writing Rackspace allows 25 files to be invalidated per
|
180
|
+
day.
|
181
|
+
2. This extension will only invalidate files on the CDN and will not synchronise
|
182
|
+
or upload files to CloudFiles. Use [middleman-sync](https://github.com/karlfreeman/middleman-sync) to do that.
|
183
|
+
|
155
184
|
### Credentials via Environment Variables
|
156
185
|
|
157
186
|
Instead of storing your CDN credentials in config.rb where they may be public
|
158
187
|
on github, store them in environment variables, or execute on the
|
159
|
-
commandline as
|
188
|
+
commandline as. Any parameters with a default of `ENV[...]` (see above) can be stored
|
189
|
+
as environment variables or provided on the commandline like this example.
|
160
190
|
|
161
191
|
```bash
|
162
|
-
CLOUDFLARE_CLIENT_API_KEY= CLOUDFLARE_EMAIL=
|
192
|
+
CLOUDFLARE_CLIENT_API_KEY= CLOUDFLARE_EMAIL= bundle exec middleman invalidate
|
163
193
|
```
|
164
194
|
|
195
|
+
|
165
196
|
## Invalidating
|
166
197
|
|
167
198
|
Set `after_build` to `true` and the cache will be invalidated after build:
|
@@ -181,17 +212,21 @@ bundle exec middleman cdn
|
|
181
212
|
|
182
213
|
## Example Usage
|
183
214
|
|
184
|
-
I'm using middleman-cdn on my personal website [leighmcculloch.com](http://leighmcculloch.com) which is on [github](https://github.com/leighmcculloch/leighmcculloch.com) if you want to checkout how I deploy.
|
215
|
+
I'm using middleman-cdn on my personal website [leighmcculloch.com](http://leighmcculloch.com) which is on [github](https://github.com/leighmcculloch/leighmcculloch.com) if you want to checkout how I deploy. It's configuration has all of the above CDNs in use for demonstration. I primarily use CloudFlare, and unlike the other CDNs, CloudFlare doesn't default to caching HTML. To make the most of CloudFlare, configure a PageRule that looks like this to tell CloudFlare to cache everything.
|
185
216
|
![CloudFlare PageRule Example](README-cloudflare-pagerule-example.png)
|
186
217
|
|
187
218
|
## Thanks
|
188
219
|
|
189
|
-
Middleman CDN is a fork off [Middleman CloudFront](https://github.com/andrusha/middleman-cloudfront) and I used it as the base for building this extension. The code was well structured and easy to understand. It was easy to break out the CloudFront specific logic and to add support for CloudFlare. My gratitude goes to @andrusha and
|
220
|
+
Middleman CDN is a fork off [Middleman CloudFront](https://github.com/andrusha/middleman-cloudfront) and I used it as the base for building this extension. The code was well structured and easy to understand. It was easy to break out the CloudFront specific logic and to add support for CloudFlare and the other CDNs. My gratitude goes to @andrusha and @manuelmeurer for their work on Middleman CloudFront.
|
190
221
|
|
191
222
|
Thanks to @b4k3r for the [Cloudflare gem](https://github.com/b4k3r/cloudflare) that made invalidating CloudFlare files a breeze.
|
192
223
|
|
224
|
+
Thanks to @geemus and the many contributors to the [fog gem](https://github.com/fog/fog) that made invalidating CloudFront easy.
|
225
|
+
|
226
|
+
Official gems from [Fastly](https://github.com/fastly/fastly-ruby) and [MaxCDN](https://github.com/MaxCDN/ruby-maxcdn) are used for interacting with their services.
|
227
|
+
|
193
228
|
## Why Middleman CDN
|
194
229
|
|
195
|
-
|
230
|
+
It's becoming increasingly common for static websites to be hosted across multiple CDNs. [jsDelivr](http://jsdelivr.com/) is a well known promoter of this strategy and it's a strategy I want my toolset (middleman) to support for my next side project.
|
196
231
|
|
197
|
-
|
232
|
+
I've created this extension so that it can grow to support the CDNs we (you and me) are using.
|
@@ -14,8 +14,8 @@ module Middleman
|
|
14
14
|
TEXT
|
15
15
|
end
|
16
16
|
|
17
|
-
def say_status(status, newline: true, header: true)
|
18
|
-
::Middleman::Cli::CDN.say_status(self.class.key, status, newline: newline, header: header)
|
17
|
+
def say_status(status, newline: true, header: true, wait_enter: false)
|
18
|
+
::Middleman::Cli::CDN.say_status(self.class.key, status, newline: newline, header: header, wait_enter: wait_enter)
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
@@ -0,0 +1,64 @@
|
|
1
|
+
#Encoding: UTF-8
|
2
|
+
require "httparty"
|
3
|
+
require "active_support/core_ext/string"
|
4
|
+
require "middleman-cdn/clients/rackspace.rb"
|
5
|
+
|
6
|
+
module Middleman
|
7
|
+
module Cli
|
8
|
+
class RackspaceCDN < BaseCDN
|
9
|
+
DAILY_LIMIT = 25
|
10
|
+
|
11
|
+
def self.key
|
12
|
+
"rackspace"
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.example_configuration_elements
|
16
|
+
{
|
17
|
+
username: ['"..."', "# default ENV['RACKSPACE_USERNAME']"],
|
18
|
+
api_key: ['"..."', "# default ENV['RACKSPACE_API_KEY']"],
|
19
|
+
region: ['"DFW"', "# DFW, SYD, IAD, ORD, HKG, etc"],
|
20
|
+
container: ['"..."', ""],
|
21
|
+
notification_email: ['"..."', "# optional"],
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def invalidate(options, files)
|
26
|
+
options[:username] ||= ENV['RACKSPACE_USERNAME']
|
27
|
+
options[:api_key] ||= ENV['RACKSPACE_API_KEY']
|
28
|
+
|
29
|
+
[:username, :api_key, :region, :container].each do |key|
|
30
|
+
if options[key].blank?
|
31
|
+
say_status("Error: Configuration key rackspace[:#{key}] is missing.".light_red)
|
32
|
+
raise
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
files = files.reject { |file| file.end_with?("/") }
|
37
|
+
|
38
|
+
if files.count > DAILY_LIMIT
|
39
|
+
say_status("Warning: You are invalidating more files than Rackspace's daily limit (25).")
|
40
|
+
say_status("Press ENTER to continue, or CTRL-C to exit.", wait_enter: true)
|
41
|
+
end
|
42
|
+
|
43
|
+
rackspace_client = RackspaceClient.new(options[:username], options[:api_key])
|
44
|
+
|
45
|
+
files.each do |file|
|
46
|
+
invalidate_file(rackspace_client, options[:region], options[:container], file, notification_email: options[:notification_email])
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def invalidate_file(rackspace_client, region, container, file, notification_email: nil)
|
53
|
+
begin
|
54
|
+
say_status("Invalidating #{file}...", newline: false)
|
55
|
+
rackspace_client.invalidate(region, container, file, notification_email: notification_email)
|
56
|
+
rescue => e
|
57
|
+
say_status(" error: #{e.message}".light_red, header: false)
|
58
|
+
else
|
59
|
+
say_status("✔".light_green, header: false)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
#Encoding: UTF-8
|
2
|
+
require "httparty"
|
3
|
+
require "active_support/core_ext/string"
|
4
|
+
|
5
|
+
module Middleman
|
6
|
+
module Cli
|
7
|
+
class RackspaceClient
|
8
|
+
def initialize(username, api_key)
|
9
|
+
@username = username
|
10
|
+
@api_key = api_key
|
11
|
+
@auth_data = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def invalidate(region, container, file, notification_email: nil)
|
15
|
+
headers = { "x-auth-token" => get_auth_token }
|
16
|
+
headers.merge!({ "x-purge-email" => notification_email }) if notification_email.present?
|
17
|
+
response = HTTParty.delete("#{get_cdn_endpoint(region)}/#{container}#{URI.escape(file)}", { :headers => headers })
|
18
|
+
case response.header.code
|
19
|
+
when "204"
|
20
|
+
# success
|
21
|
+
when "400"
|
22
|
+
error_message = response.headers["x-purge-failed-reason"]
|
23
|
+
raise "400, #{error_message}" if error_message.present?
|
24
|
+
raise "400, an error occurred."
|
25
|
+
when "403"
|
26
|
+
raise "403, the server refused to respond to the request. Check your credentials."
|
27
|
+
when "404"
|
28
|
+
raise "404, the requested resource could not be found."
|
29
|
+
else
|
30
|
+
error_message = response.body
|
31
|
+
raise "#{response.header.code}, an error occurred. #{error_message}".rstrip
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def perform_auth
|
38
|
+
return if @auth_data.present?
|
39
|
+
response = HTTParty.post("https://identity.api.rackspacecloud.com/v2.0/tokens", {
|
40
|
+
:body => {
|
41
|
+
"auth" => {
|
42
|
+
"RAX-KSKEY:apiKeyCredentials" => {
|
43
|
+
"username" => @username,
|
44
|
+
"apiKey" => @api_key
|
45
|
+
}
|
46
|
+
}
|
47
|
+
}.to_json,
|
48
|
+
:headers => {
|
49
|
+
"Content-Type" => "application/json"
|
50
|
+
}
|
51
|
+
})
|
52
|
+
case response.header.code
|
53
|
+
when "200"
|
54
|
+
@auth_data = JSON.parse(response.body)
|
55
|
+
else
|
56
|
+
error_message = response.body
|
57
|
+
raise "#{response.header.code}, an error occurred. #{error_message}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def get_auth_token
|
62
|
+
perform_auth
|
63
|
+
@auth_data["access"]["token"]["id"]
|
64
|
+
end
|
65
|
+
|
66
|
+
def get_service_endpoint(service_type, region)
|
67
|
+
perform_auth
|
68
|
+
access = @auth_data["access"] if @auth_data
|
69
|
+
serviceCatalog = access["serviceCatalog"] if access
|
70
|
+
service = serviceCatalog.find { |service| service["type"] == service_type } if serviceCatalog
|
71
|
+
endpoints = service["endpoints"] if service
|
72
|
+
endpoint = endpoints.find { |endpoint| endpoint["region"] == region } if endpoints
|
73
|
+
return public_url = endpoint["publicURL"] if endpoint
|
74
|
+
nil
|
75
|
+
end
|
76
|
+
|
77
|
+
def get_cdn_endpoint(region)
|
78
|
+
get_service_endpoint("rax:object-cdn", region)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -5,6 +5,7 @@ require "middleman-cdn/cdns/cloudflare.rb"
|
|
5
5
|
require "middleman-cdn/cdns/cloudfront.rb"
|
6
6
|
require "middleman-cdn/cdns/fastly.rb"
|
7
7
|
require "middleman-cdn/cdns/maxcdn.rb"
|
8
|
+
require "middleman-cdn/cdns/rackspace.rb"
|
8
9
|
require "colorize"
|
9
10
|
|
10
11
|
module Middleman
|
@@ -23,42 +24,44 @@ module Middleman
|
|
23
24
|
|
24
25
|
desc "cdn:cdn_invalidate", "Invalidate your CloudFlare or CloudFront cache"
|
25
26
|
def cdn_invalidate(options = nil)
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
27
|
+
begin
|
28
|
+
if options.nil?
|
29
|
+
app_instance = ::Middleman::Application.server.inst
|
30
|
+
unless app_instance.respond_to?(:cdn_options)
|
31
|
+
self.class.say_status(nil, "Error: You need to activate the cdn extension in config.rb.\n#{example_configuration}".light_red)
|
32
|
+
raise
|
33
|
+
end
|
34
|
+
options = app_instance.cdn_options
|
31
35
|
end
|
32
|
-
options
|
33
|
-
end
|
34
|
-
options.filter ||= /.*/
|
36
|
+
options.filter ||= /.*/
|
35
37
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
38
|
+
if cdns.all? { |cdn| options.public_send(cdn.key.to_sym).nil? }
|
39
|
+
self.class.say_status(nil, "Error: You must specify a config for one of the supported CDNs.\n#{example_configuration}".light_red)
|
40
|
+
raise
|
41
|
+
end
|
40
42
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
43
|
+
files = list_files(options.filter)
|
44
|
+
self.class.say_status(nil, "Invalidating #{files.count} files with filter: " + "#{options.filter.source}".magenta.bold)
|
45
|
+
files.each { |file| self.class.say_status(nil, " • #{file}") }
|
46
|
+
return if files.empty?
|
45
47
|
|
46
|
-
|
47
|
-
|
48
|
-
|
48
|
+
cdns_keyed.each do |cdn_key, cdn|
|
49
|
+
cdn_options = options.public_send(cdn_key.to_sym)
|
50
|
+
cdn.new.invalidate(cdn_options, files) unless cdn_options.nil?
|
51
|
+
end
|
52
|
+
rescue SystemExit, Interrupt
|
53
|
+
self.class.say_status(nil, nil, header: false)
|
49
54
|
end
|
50
55
|
end
|
51
56
|
|
52
|
-
def self.say_status(cdn, status, newline: true, header: true)
|
57
|
+
def self.say_status(cdn, status, newline: true, header: true, wait_enter: false)
|
53
58
|
message = ""
|
54
59
|
message << "#{:cdn.to_s.rjust(12).light_green.bold} #{cdn.try(:yellow).try(:bold)}" if header
|
55
60
|
message << " " if header && cdn
|
56
|
-
message << status
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
print message
|
61
|
-
end
|
61
|
+
message << status if status
|
62
|
+
print message
|
63
|
+
STDIN.noecho(&:gets) if wait_enter
|
64
|
+
puts "" if newline
|
62
65
|
end
|
63
66
|
|
64
67
|
private
|
@@ -68,7 +71,8 @@ module Middleman
|
|
68
71
|
CloudFlareCDN,
|
69
72
|
CloudFrontCDN,
|
70
73
|
FastlyCDN,
|
71
|
-
MaxCDN
|
74
|
+
MaxCDN,
|
75
|
+
RackspaceCDN
|
72
76
|
]
|
73
77
|
end
|
74
78
|
|
@@ -13,6 +13,7 @@ module Middleman
|
|
13
13
|
option :cloudfront, nil, 'CloudFront options'
|
14
14
|
option :fastly, nil, 'Fastly options'
|
15
15
|
option :maxcdn, nil, 'MaxCDN options'
|
16
|
+
option :rackspace, nil, 'Rackspace options'
|
16
17
|
option :filter, nil, 'Cloudflare options'
|
17
18
|
option :after_build, false, 'Cloudflare options'
|
18
19
|
|
data/middleman-cdn.gemspec
CHANGED
@@ -23,9 +23,11 @@ Gem::Specification.new do |s|
|
|
23
23
|
s.add_dependency 'maxcdn', '~> 0.1'
|
24
24
|
s.add_dependency 'colorize', '~> 0.7'
|
25
25
|
s.add_dependency 'activesupport', '~> 4.1'
|
26
|
+
s.add_dependency 'httparty', '~> 0.13'
|
26
27
|
|
27
28
|
s.add_development_dependency 'rake', '~> 0.9'
|
28
29
|
s.add_development_dependency 'rspec', '~> 3.0'
|
30
|
+
s.add_development_dependency 'coveralls', '~> 0.7'
|
29
31
|
|
30
32
|
s.add_dependency 'middleman-core', '~> 3.0'
|
31
33
|
end
|
@@ -40,12 +40,12 @@ TEXT
|
|
40
40
|
|
41
41
|
describe "#say_status" do
|
42
42
|
it "should use the Cli CDN class to say status" do
|
43
|
-
expect(::Middleman::Cli::CDN).to receive(:say_status).with(described_class.key, "status text", newline: false, header: false)
|
43
|
+
expect(::Middleman::Cli::CDN).to receive(:say_status).with(described_class.key, "status text", newline: false, header: false, wait_enter: false)
|
44
44
|
subject.say_status("status text", newline: false, header: false)
|
45
45
|
end
|
46
46
|
|
47
47
|
it "should use the Cli CDN class to say status with defaults" do
|
48
|
-
expect(::Middleman::Cli::CDN).to receive(:say_status).with(described_class.key, "status text", newline: true, header: true)
|
48
|
+
expect(::Middleman::Cli::CDN).to receive(:say_status).with(described_class.key, "status text", newline: true, header: true, wait_enter: false)
|
49
49
|
subject.say_status("status text")
|
50
50
|
end
|
51
51
|
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
#Encoding: UTF-8
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'lib/middleman-cdn/cdns/base_protocol'
|
4
|
+
|
5
|
+
describe Middleman::Cli::RackspaceCDN do
|
6
|
+
it_behaves_like "BaseCDN"
|
7
|
+
|
8
|
+
describe '.key' do
|
9
|
+
it "should be 'rackspace'" do
|
10
|
+
expect(described_class.key).to eq("rackspace")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '.example_configuration_elements' do
|
15
|
+
it "should contain these keys" do
|
16
|
+
required_keys = [:username, :api_key, :region, :container, :notification_email]
|
17
|
+
expect(described_class.example_configuration_elements.keys).to eq(required_keys)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#invalidate' do
|
22
|
+
let(:double_rackspace) { double("RackspaceClient") }
|
23
|
+
|
24
|
+
before do
|
25
|
+
allow(double_rackspace).to receive(:invalidate)
|
26
|
+
allow(Middleman::Cli::RackspaceClient).to receive(:new).and_return(double_rackspace)
|
27
|
+
end
|
28
|
+
|
29
|
+
let(:files) { [ "/index.html", "/", "/test/index.html", "/test/image.png" ] }
|
30
|
+
|
31
|
+
let(:files_no_dirs) { files.reject { |file| file.end_with?("/") } }
|
32
|
+
|
33
|
+
context "all options provided" do
|
34
|
+
let(:options) do
|
35
|
+
{
|
36
|
+
username: "00000000000000000000",
|
37
|
+
api_key: "11111111111111111111",
|
38
|
+
region: "222222",
|
39
|
+
container: "333333",
|
40
|
+
notification_email: "test@example.com",
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should instantiate rackspace client with credentails" do
|
45
|
+
expect(Middleman::Cli::RackspaceClient).to receive(:new).with("00000000000000000000", "11111111111111111111")
|
46
|
+
subject.invalidate(options, files)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should not raise errors" do
|
50
|
+
subject.invalidate(options, files)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should invalidate each files one at a time" do
|
54
|
+
expect(double_rackspace).to receive(:invalidate).once.ordered.with("222222", "333333", "/index.html", notification_email: "test@example.com")
|
55
|
+
expect(double_rackspace).to receive(:invalidate).once.ordered.with("222222", "333333", "/test/index.html", notification_email: "test@example.com")
|
56
|
+
expect(double_rackspace).to receive(:invalidate).once.ordered.with("222222", "333333", "/test/image.png", notification_email: "test@example.com")
|
57
|
+
expect(double_rackspace).to_not receive(:invalidate).with(anything, anything, "/", anything)
|
58
|
+
subject.invalidate(options, files)
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should output saying invalidating each file" do
|
62
|
+
files_escaped = files_no_dirs.map { |file| Regexp.escape(file) }
|
63
|
+
expect { subject.invalidate(options, files) }.to output(/#{files_escaped.join(".+")}/m).to_stdout
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should output saying success checkmarks" do
|
67
|
+
expect { subject.invalidate(options, files) }.to output(/✔/).to_stdout
|
68
|
+
end
|
69
|
+
|
70
|
+
context "and errors occurs when purging" do
|
71
|
+
before do
|
72
|
+
allow(double_rackspace).to receive(:invalidate).and_raise(StandardError)
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should output saying error information" do
|
76
|
+
expect { subject.invalidate(options, files) }.to output(/error: StandardError/).to_stdout
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context "environment variables used for credentials" do
|
82
|
+
before do
|
83
|
+
allow(ENV).to receive(:[])
|
84
|
+
allow(ENV).to receive(:[]).with("RACKSPACE_USERNAME").and_return("00000000000000000000")
|
85
|
+
allow(ENV).to receive(:[]).with("RACKSPACE_API_KEY").and_return("11111111111111111111")
|
86
|
+
end
|
87
|
+
|
88
|
+
let(:options) do
|
89
|
+
{
|
90
|
+
region: "222222",
|
91
|
+
container: "333333",
|
92
|
+
notification_email: "test@example.com",
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should instantiate with environment variable credentails" do
|
97
|
+
expect(Middleman::Cli::RackspaceClient).to receive(:new).with("00000000000000000000", "11111111111111111111")
|
98
|
+
subject.invalidate(options, files)
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should not raise errors" do
|
102
|
+
subject.invalidate(options, files)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context "if username not provided" do
|
107
|
+
before do
|
108
|
+
allow(ENV).to receive(:[])
|
109
|
+
allow(ENV).to receive(:[]).with("RACKSPACE_USERNAME").and_return(nil)
|
110
|
+
end
|
111
|
+
|
112
|
+
let(:options) do
|
113
|
+
{
|
114
|
+
api_key: "11111111111111111111",
|
115
|
+
region: "222222",
|
116
|
+
container: "333333",
|
117
|
+
notification_email: "test@example.com",
|
118
|
+
}
|
119
|
+
end
|
120
|
+
|
121
|
+
it "should raise error" do
|
122
|
+
expect { subject.invalidate(options, files) }.to raise_error(RuntimeError)
|
123
|
+
end
|
124
|
+
|
125
|
+
it "should output saying error" do
|
126
|
+
expect { subject.invalidate(options, files) rescue nil }.to output(/Error: Configuration key rackspace\[:username\] is missing\./).to_stdout
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
context "if api key not provided" do
|
131
|
+
before do
|
132
|
+
allow(ENV).to receive(:[])
|
133
|
+
allow(ENV).to receive(:[]).with("RACKSPACE_API_KEY").and_return(nil)
|
134
|
+
end
|
135
|
+
|
136
|
+
let(:options) do
|
137
|
+
{
|
138
|
+
username: "00000000000000000000",
|
139
|
+
region: "222222",
|
140
|
+
container: "333333",
|
141
|
+
notification_email: "test@example.com",
|
142
|
+
}
|
143
|
+
end
|
144
|
+
|
145
|
+
it "should raise error" do
|
146
|
+
expect { subject.invalidate(options, files) }.to raise_error(RuntimeError)
|
147
|
+
end
|
148
|
+
|
149
|
+
it "should output saying error" do
|
150
|
+
expect { subject.invalidate(options, files) rescue nil }.to output(/Error: Configuration key rackspace\[:api_key\] is missing\./).to_stdout
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
context "if region not provided" do
|
155
|
+
let(:options) do
|
156
|
+
{
|
157
|
+
username: "00000000000000000000",
|
158
|
+
api_key: "11111111111111111111",
|
159
|
+
container: "333333",
|
160
|
+
notification_email: "test@example.com",
|
161
|
+
}
|
162
|
+
end
|
163
|
+
|
164
|
+
it "should raise error" do
|
165
|
+
expect { subject.invalidate(options, files) }.to raise_error(RuntimeError)
|
166
|
+
end
|
167
|
+
|
168
|
+
it "should output saying error" do
|
169
|
+
expect { subject.invalidate(options, files) rescue nil }.to output(/Error: Configuration key rackspace\[:region\] is missing\./).to_stdout
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
context "if container not provided" do
|
174
|
+
let(:options) do
|
175
|
+
{
|
176
|
+
username: "00000000000000000000",
|
177
|
+
api_key: "11111111111111111111",
|
178
|
+
region: "222222",
|
179
|
+
notification_email: "test@example.com",
|
180
|
+
}
|
181
|
+
end
|
182
|
+
|
183
|
+
it "should raise error" do
|
184
|
+
expect { subject.invalidate(options, files) }.to raise_error(RuntimeError)
|
185
|
+
end
|
186
|
+
|
187
|
+
it "should output saying error" do
|
188
|
+
expect { subject.invalidate(options, files) rescue nil }.to output(/Error: Configuration key rackspace\[:container\] is missing\./).to_stdout
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
context "if notification email not provided" do
|
193
|
+
let(:options) do
|
194
|
+
{
|
195
|
+
username: "00000000000000000000",
|
196
|
+
api_key: "11111111111111111111",
|
197
|
+
region: "222222",
|
198
|
+
container: "333333"
|
199
|
+
}
|
200
|
+
end
|
201
|
+
|
202
|
+
it "should not raise error" do
|
203
|
+
expect { subject.invalidate(options, files) }.to_not raise_error
|
204
|
+
end
|
205
|
+
|
206
|
+
it "should invalidate each file with a nil notification email" do
|
207
|
+
expect(double_rackspace).to receive(:invalidate).once.ordered.with("222222", "333333", "/index.html", notification_email: nil)
|
208
|
+
expect(double_rackspace).to receive(:invalidate).once.ordered.with("222222", "333333", "/test/index.html", notification_email: nil)
|
209
|
+
expect(double_rackspace).to receive(:invalidate).once.ordered.with("222222", "333333", "/test/image.png", notification_email: nil)
|
210
|
+
expect(double_rackspace).to_not receive(:invalidate).with(anything, anything, "/", notification_email: nil)
|
211
|
+
subject.invalidate(options, files)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module RackspaceResponseDoubles
|
4
|
+
def double_for_response_auth_success
|
5
|
+
double({
|
6
|
+
:header => double({
|
7
|
+
:code => "200"
|
8
|
+
}),
|
9
|
+
:headers => {},
|
10
|
+
:body => {
|
11
|
+
"access" => { "token" => { "id" => "<auth-token>" } },
|
12
|
+
"serviceCatalog" => [
|
13
|
+
{
|
14
|
+
"type" => "rax:object-cdn",
|
15
|
+
"endpoints" => [
|
16
|
+
{
|
17
|
+
"region" => "DFW",
|
18
|
+
"publicURL" => "http://example.com/"
|
19
|
+
}
|
20
|
+
]
|
21
|
+
}
|
22
|
+
]
|
23
|
+
}.to_json
|
24
|
+
})
|
25
|
+
end
|
26
|
+
|
27
|
+
def double_for_response_auth_fail
|
28
|
+
double({
|
29
|
+
:header => double({
|
30
|
+
:code => "401"
|
31
|
+
}),
|
32
|
+
:headers => {},
|
33
|
+
:body => {
|
34
|
+
"unauthorized" => {
|
35
|
+
"code" => "401",
|
36
|
+
"message" => "Username or api key is invalid."
|
37
|
+
}
|
38
|
+
}.to_json
|
39
|
+
})
|
40
|
+
end
|
41
|
+
|
42
|
+
def double_for_response_delete(status_code: 204, fail_message: nil)
|
43
|
+
d = double({
|
44
|
+
:header => double({
|
45
|
+
:code => "#{status_code}"
|
46
|
+
}),
|
47
|
+
:headers => {},
|
48
|
+
:body => ""
|
49
|
+
})
|
50
|
+
if fail_message
|
51
|
+
case status_code
|
52
|
+
when 204, 403, 404
|
53
|
+
when 400
|
54
|
+
allow(d).to receive(:headers).and_return({ "x-purge-failed-reason" => fail_message })
|
55
|
+
else
|
56
|
+
allow(d).to receive(:body).and_return(fail_message) if status_code
|
57
|
+
end
|
58
|
+
end
|
59
|
+
d
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
RSpec.configure do |c|
|
64
|
+
c.include RackspaceResponseDoubles, :include_rackspace_response_doubles
|
65
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
#Encoding: UTF-8
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'lib/middleman-cdn/clients/rackspace_response_doubles.rb'
|
4
|
+
|
5
|
+
describe Middleman::Cli::RackspaceClient, :include_rackspace_response_doubles do
|
6
|
+
|
7
|
+
describe '#invalidate', :vcr do
|
8
|
+
|
9
|
+
let(:username) { "111111" }
|
10
|
+
let(:api_key) { "000000" }
|
11
|
+
let(:region) { "DFW" }
|
12
|
+
let(:container) { "container" }
|
13
|
+
let(:notification_email) { "test@example.com" }
|
14
|
+
|
15
|
+
let(:subject) { described_class.new(username, api_key) }
|
16
|
+
|
17
|
+
before do
|
18
|
+
allow(HTTParty).to receive(:post).and_return(double_for_response_auth_success)
|
19
|
+
allow(HTTParty).to receive(:delete).and_return(double_for_response_delete)
|
20
|
+
end
|
21
|
+
|
22
|
+
context "authentication" do
|
23
|
+
context "valid credentials" do
|
24
|
+
before do
|
25
|
+
expect(HTTParty).to receive(:post).with("https://identity.api.rackspacecloud.com/v2.0/tokens", {
|
26
|
+
:body => {
|
27
|
+
"auth" => {
|
28
|
+
"RAX-KSKEY:apiKeyCredentials" => {
|
29
|
+
"username" => username,
|
30
|
+
"apiKey" => api_key
|
31
|
+
}
|
32
|
+
}
|
33
|
+
}.to_json,
|
34
|
+
:headers => {
|
35
|
+
"Content-Type" => "application/json"
|
36
|
+
}
|
37
|
+
}).once.and_return(double_for_response_auth_success)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should authenticate using the given credentials" do
|
41
|
+
subject.invalidate(region, container, "/index.html")
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should authenticate once only" do
|
45
|
+
subject.invalidate(region, container, "/index.html")
|
46
|
+
subject.invalidate(region, container, "/dir/index.html")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "invalid credentials" do
|
51
|
+
it "should raise error" do
|
52
|
+
expect(HTTParty).to receive(:post).and_return(double_for_response_auth_fail)
|
53
|
+
expect{ subject.invalidate(region, container, "/index.html") }.to raise_error(RuntimeError)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should retry authentication when needed" do
|
57
|
+
expect(HTTParty).to receive(:post).with("https://identity.api.rackspacecloud.com/v2.0/tokens", anything).once.and_return(double_for_response_auth_fail)
|
58
|
+
expect(HTTParty).to receive(:post).with("https://identity.api.rackspacecloud.com/v2.0/tokens", anything).once.and_return(double_for_response_auth_success)
|
59
|
+
subject.invalidate(region, container, "/index.html") rescue nil
|
60
|
+
subject.invalidate(region, container, "/index.html")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context "successful invalidate (204)" do
|
66
|
+
it "should not raise error" do
|
67
|
+
expect{ subject.invalidate(region, container, "/index.html") }.to_not raise_error
|
68
|
+
end
|
69
|
+
|
70
|
+
context "with notification email" do
|
71
|
+
it "should include notification email in invalidation request" do
|
72
|
+
expect(HTTParty).to receive(:delete).with(anything, hash_including({
|
73
|
+
:headers => hash_including({
|
74
|
+
"x-purge-email" => "test@example.com"
|
75
|
+
})
|
76
|
+
})).once.and_return(double_for_response_delete)
|
77
|
+
subject.invalidate(region, container, "/index.html", notification_email: "test@example.com")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context "with file names that contain characters not allowed in a URL" do
|
82
|
+
it "should escape the file path" do
|
83
|
+
expect(HTTParty).to receive(:delete).with(match(/\/dir\/index%20file.html$/), anything).once.and_return(double_for_response_delete)
|
84
|
+
subject.invalidate(region, container, "/dir/index file.html")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context "unsuccessful invalidation" do
|
90
|
+
it "should raise error on 400 with fail message" do
|
91
|
+
expect(HTTParty).to receive(:delete).and_return(double_for_response_delete(status_code: 400, fail_message: "the fail message"))
|
92
|
+
expect{ subject.invalidate(region, container, "/index.html") }.to raise_error(RuntimeError, "400, the fail message")
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should raise error on 400 without fail message" do
|
96
|
+
expect(HTTParty).to receive(:delete).and_return(double_for_response_delete(status_code: 400))
|
97
|
+
expect{ subject.invalidate(region, container, "/index.html") }.to raise_error(RuntimeError, "400, an error occurred.")
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should raise error on 403" do
|
101
|
+
expect(HTTParty).to receive(:delete).and_return(double_for_response_delete(status_code: 403))
|
102
|
+
expect{ subject.invalidate(region, container, "/index.html") }.to raise_error(RuntimeError, "403, the server refused to respond to the request. Check your credentials.")
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should raise error on 404" do
|
106
|
+
expect(HTTParty).to receive(:delete).and_return(double_for_response_delete(status_code: 404))
|
107
|
+
expect{ subject.invalidate(region, container, "/index.html") }.to raise_error(RuntimeError, "404, the requested resource could not be found.")
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should raise error on any unspecified response codes" do
|
111
|
+
expect(HTTParty).to receive(:delete).and_return(double_for_response_delete(status_code: 999))
|
112
|
+
expect{ subject.invalidate(region, container, "/index.html") }.to raise_error(RuntimeError, "999, an error occurred.")
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should raise error on any unspecified response codes with fail message in body" do
|
116
|
+
expect(HTTParty).to receive(:delete).and_return(double_for_response_delete(status_code: 999, fail_message: "the error has arrived"))
|
117
|
+
expect{ subject.invalidate(region, container, "/index.html") }.to raise_error(RuntimeError, "999, an error occurred. the error has arrived")
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: middleman-cdn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Leigh McCulloch
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-07-
|
11
|
+
date: 2014-07-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: fog
|
@@ -94,6 +94,20 @@ dependencies:
|
|
94
94
|
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '4.1'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: httparty
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.13'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.13'
|
97
111
|
- !ruby/object:Gem::Dependency
|
98
112
|
name: rake
|
99
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -122,6 +136,20 @@ dependencies:
|
|
122
136
|
- - "~>"
|
123
137
|
- !ruby/object:Gem::Version
|
124
138
|
version: '3.0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: coveralls
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0.7'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0.7'
|
125
153
|
- !ruby/object:Gem::Dependency
|
126
154
|
name: middleman-core
|
127
155
|
requirement: !ruby/object:Gem::Requirement
|
@@ -155,6 +183,8 @@ files:
|
|
155
183
|
- lib/middleman-cdn/cdns/cloudfront.rb
|
156
184
|
- lib/middleman-cdn/cdns/fastly.rb
|
157
185
|
- lib/middleman-cdn/cdns/maxcdn.rb
|
186
|
+
- lib/middleman-cdn/cdns/rackspace.rb
|
187
|
+
- lib/middleman-cdn/clients/rackspace.rb
|
158
188
|
- lib/middleman-cdn/commands.rb
|
159
189
|
- lib/middleman-cdn/extension.rb
|
160
190
|
- lib/middleman-cdn/version.rb
|
@@ -165,6 +195,9 @@ files:
|
|
165
195
|
- spec/lib/middleman-cdn/cdns/cloudfront_spec.rb
|
166
196
|
- spec/lib/middleman-cdn/cdns/fastly_spec.rb
|
167
197
|
- spec/lib/middleman-cdn/cdns/maxcdn_spec.rb
|
198
|
+
- spec/lib/middleman-cdn/cdns/rackspace_spec.rb
|
199
|
+
- spec/lib/middleman-cdn/clients/rackspace_response_doubles.rb
|
200
|
+
- spec/lib/middleman-cdn/clients/rackspace_spec.rb
|
168
201
|
- spec/lib/middleman-cdn/commands_spec.rb
|
169
202
|
- spec/spec_helper.rb
|
170
203
|
homepage: https://github.com/leighmcculloch/middleman-cdn
|