middleman-s3_sync 3.0.47 → 3.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +3 -2
- data/lib/middleman-s3_sync.rb +4 -3
- data/lib/middleman-s3_sync/commands.rb +16 -8
- data/lib/middleman-s3_sync/extension.rb +74 -37
- data/lib/middleman/s3_sync.rb +49 -40
- data/lib/middleman/s3_sync/caching_policy.rb +58 -0
- data/lib/middleman/s3_sync/options.rb +2 -84
- data/lib/middleman/s3_sync/resource.rb +28 -15
- data/lib/middleman/s3_sync/version.rb +1 -1
- data/middleman-s3_sync.gemspec +4 -3
- data/spec/caching_policy_spec.rb +80 -0
- data/spec/resource_spec.rb +16 -8
- data/spec/spec_helper.rb +0 -1
- metadata +25 -10
- data/spec/options_spec.rb +0 -130
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0ea0d9098ccfca7a420089e791da7f7881298c5c
|
4
|
+
data.tar.gz: 93b501fb586db5be0cd329c448ac2edb5da3c044
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1d4b328645fe0171b6b5f01c0b0f2f19428c17a06b0e0af8bfa9b908763982e5a12b52b2c33e7b1a57b4e7e6bdd424ae27e302d97b17db7a8f08654868239615
|
7
|
+
data.tar.gz: 9d427de8aca80db2ff476dc42da6a2fc11bb769651c8f16a24780dbc0b9a9ae51459a038ae8c85abff8a45c5728d511791254bdc8566368f8d066a1d0089453a
|
data/.travis.yml
CHANGED
data/lib/middleman-s3_sync.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
require 'middleman-core'
|
2
|
+
require 'middleman-s3_sync/commands'
|
2
3
|
require 'middleman/s3_sync'
|
3
4
|
|
4
|
-
::Middleman::Extensions.register(:s3_sync
|
5
|
-
|
5
|
+
::Middleman::Extensions.register(:s3_sync) do
|
6
|
+
require 'middleman-s3_sync/extension'
|
7
|
+
::Middleman::S3SyncExtension
|
6
8
|
end
|
7
|
-
|
@@ -2,6 +2,8 @@ require 'middleman-core/cli'
|
|
2
2
|
|
3
3
|
module Middleman
|
4
4
|
module Cli
|
5
|
+
Base.map("sync" => "s3_sync")
|
6
|
+
|
5
7
|
class S3Sync < Thor
|
6
8
|
include Thor::Actions
|
7
9
|
|
@@ -13,11 +15,11 @@ module Middleman
|
|
13
15
|
true
|
14
16
|
end
|
15
17
|
|
16
|
-
desc "s3_sync", "
|
17
|
-
|
18
|
+
desc "s3_sync [options]", "Synchronizes a middleman site to an AWS S3 bucket"
|
19
|
+
class_option :force, type: :boolean,
|
18
20
|
desc: "Push all local files to the server",
|
19
21
|
aliases: '-f'
|
20
|
-
|
22
|
+
class_option :bucket, type: :string,
|
21
23
|
desc: "Specify which bucket to use, overrides the configured bucket.",
|
22
24
|
aliases: '-b'
|
23
25
|
method_option :prefix, type: :string,
|
@@ -26,25 +28,31 @@ module Middleman
|
|
26
28
|
method_option :verbose, type: :boolean,
|
27
29
|
desc: "Adds more verbosity...",
|
28
30
|
aliases: '-v'
|
31
|
+
class_option :dry_run, type: :boolean,
|
32
|
+
desc: "Performs a dry run of the sync",
|
33
|
+
aliases: '-n'
|
29
34
|
|
30
35
|
def s3_sync
|
31
|
-
|
32
|
-
|
36
|
+
::Middleman::S3Sync.app = ::Middleman::Application.server.inst
|
37
|
+
|
38
|
+
s3_sync_options = ::Middleman::S3Sync.s3_sync_options
|
39
|
+
|
40
|
+
bucket = s3_sync_options.bucket rescue nil
|
41
|
+
|
33
42
|
unless bucket
|
34
43
|
raise Thor::Error.new "You need to activate the s3_sync extension and at least provide the bucket name."
|
35
44
|
end
|
36
45
|
|
37
|
-
s3_sync_options = shared_inst.s3_sync_options
|
38
46
|
|
39
47
|
# Override options based on what was passed on the command line...
|
40
48
|
s3_sync_options.force = options[:force] if options[:force]
|
41
49
|
s3_sync_options.bucket = options[:bucket] if options[:bucket]
|
42
50
|
s3_sync_options.verbose = options[:verbose] if options[:verbose]
|
43
51
|
s3_sync_options.prefix = options[:prefix] if options[:prefix]
|
52
|
+
s3_sync_options.dry_run = options[:dry_run] if options[:dry_run]
|
44
53
|
|
45
|
-
::Middleman::S3Sync.sync(
|
54
|
+
::Middleman::S3Sync.sync()
|
46
55
|
end
|
47
56
|
end
|
48
|
-
Base.map('sync' => 's3_sync')
|
49
57
|
end
|
50
58
|
end
|
@@ -1,59 +1,96 @@
|
|
1
1
|
require 'middleman-core'
|
2
|
+
require 'middleman/s3_sync'
|
2
3
|
require 'map'
|
3
4
|
|
4
5
|
module Middleman
|
5
|
-
|
6
|
-
|
7
|
-
|
6
|
+
class S3SyncExtension < Extension
|
7
|
+
# Options supported by the extension...
|
8
|
+
option :prefix, nil, 'Path prefix of the resource we are looking for on the server.'
|
9
|
+
option :http_prefix, nil, 'Path prefix of the resources'
|
10
|
+
option :acl, 'public-read', 'ACL for the resources being pushed to S3'
|
11
|
+
option :bucket, 'nil', 'The name of the bucket we are pushing to.'
|
12
|
+
option :region, 'us-east-1', 'The name of the AWS region hosting the S3 bucket'
|
13
|
+
option :aws_access_key_id, ENV['AWS_ACCESS_KEY_ID'] , 'The AWS access key id'
|
14
|
+
option :aws_secret_access_key, ENV['AWS_SECRET_ACCESS_KEY'], 'The AWS secret access key'
|
15
|
+
option :after_build, false, 'Whether to synchronize right after the build'
|
16
|
+
option :build_dir, nil, 'Where the built site is stored'
|
17
|
+
option :delete, true, 'Whether to delete resources that do not have a local equivalent'
|
18
|
+
option :encryption, false, 'Whether to encrypt the content on the S3 bucket'
|
19
|
+
option :force, false, 'Whether to push all current resources to S3'
|
20
|
+
option :prefer_gzip, true, 'Whether to push the compressed version of the resource to S3'
|
21
|
+
option :reduced_redundancy_storage, nil, 'Whether to use the reduced redundancy storage option'
|
22
|
+
option :path_style, true, 'Whether to use path_style URLs to communiated with S3'
|
23
|
+
option :version_bucket, false, 'Whether to enable versionning on the S3 bucket content'
|
24
|
+
option :verbose, false, 'Whether to provide more verbose output'
|
25
|
+
option :dry_run, false, 'Whether to perform a dry-run'
|
8
26
|
|
9
|
-
|
10
|
-
|
11
|
-
|
27
|
+
def initialize(app, options_hash = {}, &block)
|
28
|
+
super
|
29
|
+
app.define_hook :after_s3_sync
|
30
|
+
# Temporary workaround for 3.3 and 3.4.
|
31
|
+
app.send :include, ClassMethods
|
32
|
+
end
|
33
|
+
|
34
|
+
def after_configuration
|
35
|
+
read_config
|
36
|
+
options.aws_access_key_id ||= ENV['AWS_ACCESS_KEY_ID']
|
37
|
+
options.aws_secret_access_key ||= ENV['AWS_SECRET_ACCESS_KEY']
|
38
|
+
options.http_prefix = app.http_prefix if app.respond_to? :http_prefix
|
39
|
+
options.build_dir ||= app.build_dir if app.respond_to? :build_dir
|
40
|
+
::Middleman::S3Sync.s3_sync_options = s3_sync_options
|
41
|
+
end
|
12
42
|
|
13
|
-
|
43
|
+
def after_build
|
44
|
+
::Middleman::S3Sync.sync() if options.after_build
|
45
|
+
end
|
14
46
|
|
15
|
-
|
47
|
+
def manipulate_resource_list(mm_resources)
|
48
|
+
::Middleman::S3Sync.mm_resources = mm_resources
|
49
|
+
end
|
16
50
|
|
17
|
-
|
51
|
+
def s3_sync_options
|
52
|
+
options
|
53
|
+
end
|
18
54
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
55
|
+
# Read config options from an IO stream and set them on `self`. Defaults
|
56
|
+
# to reading from the `.s3_sync` file in the MM project root if it exists.
|
57
|
+
#
|
58
|
+
# @param io [IO] an IO stream to read from
|
59
|
+
# @return [void]
|
60
|
+
def read_config(io = nil)
|
61
|
+
unless io
|
62
|
+
root_path = ::Middleman::Application.root
|
63
|
+
config_file_path = File.join(root_path, ".s3_sync")
|
24
64
|
|
25
|
-
|
26
|
-
|
27
|
-
app.after_build do |builder|
|
28
|
-
::Middleman::S3Sync.sync(options) if options.after_build
|
29
|
-
end
|
65
|
+
# skip if config file does not exist
|
66
|
+
return unless File.exists?(config_file_path)
|
30
67
|
|
31
|
-
|
32
|
-
end
|
68
|
+
io = File.open(config_file_path, "r")
|
33
69
|
end
|
34
|
-
alias :included :registered
|
35
70
|
|
36
|
-
|
37
|
-
@options
|
38
|
-
end
|
71
|
+
config = YAML.load(io).symbolize_keys
|
39
72
|
|
40
|
-
|
41
|
-
|
73
|
+
OPTIONS.each do |config_option|
|
74
|
+
self.send("#{config_option}=".to_sym, config[config_option]) if config[config_option]
|
42
75
|
end
|
76
|
+
end
|
43
77
|
|
44
|
-
module Helpers
|
45
|
-
def s3_sync_options
|
46
|
-
::Middleman::S3Sync.s3_sync_options
|
47
|
-
end
|
48
78
|
|
49
|
-
|
50
|
-
|
51
|
-
|
79
|
+
module ClassMethods
|
80
|
+
def s3_sync_options
|
81
|
+
::Middleman::S3SyncExtension.s3_sync_options
|
82
|
+
end
|
52
83
|
|
53
|
-
|
54
|
-
|
55
|
-
|
84
|
+
def default_caching_policy(policy = {})
|
85
|
+
::Middleman::S3Sync.add_caching_policy(:default, policy)
|
86
|
+
end
|
87
|
+
|
88
|
+
def caching_policy(content_type, policy = {})
|
89
|
+
::Middleman::S3Sync.add_caching_policy(content_type, policy)
|
56
90
|
end
|
57
91
|
end
|
58
92
|
end
|
93
|
+
|
94
|
+
::Middleman::Extensions.register(:s3_sync, S3SyncExtension)
|
59
95
|
end
|
96
|
+
|
data/lib/middleman/s3_sync.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
require 'fog/aws'
|
2
|
-
require '
|
2
|
+
require 'fog/aws/storage'
|
3
3
|
require 'digest/md5'
|
4
4
|
require 'middleman/s3_sync/version'
|
5
5
|
require 'middleman/s3_sync/options'
|
6
|
-
require 'middleman
|
6
|
+
require 'middleman/s3_sync/caching_policy'
|
7
7
|
require 'middleman/s3_sync/status'
|
8
8
|
require 'middleman/s3_sync/resource'
|
9
9
|
require 'middleman-s3_sync/extension'
|
10
|
+
require 'parallel'
|
10
11
|
require 'ruby-progressbar'
|
11
12
|
require 'thread'
|
12
13
|
|
@@ -14,20 +15,23 @@ module Middleman
|
|
14
15
|
module S3Sync
|
15
16
|
class << self
|
16
17
|
include Status
|
18
|
+
include CachingPolicy
|
19
|
+
|
17
20
|
@@bucket_lock = Mutex.new
|
18
21
|
@@bucket_files_lock = Mutex.new
|
19
22
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
self.s3_sync_options = options
|
23
|
+
attr_accessor :s3_sync_options
|
24
|
+
attr_accessor :mm_resources
|
25
|
+
attr_reader :app
|
24
26
|
|
27
|
+
def sync()
|
28
|
+
say_status "Let's see if there's work to be done..."
|
25
29
|
unless work_to_be_done?
|
26
|
-
say_status "
|
30
|
+
say_status "All S3 files are up to date."
|
27
31
|
return
|
28
32
|
end
|
29
33
|
|
30
|
-
say_status "
|
34
|
+
say_status "Ready to apply updates to #{s3_sync_options.bucket}."
|
31
35
|
|
32
36
|
update_bucket_versioning
|
33
37
|
|
@@ -36,7 +40,7 @@ module Middleman
|
|
36
40
|
update_resources
|
37
41
|
delete_resources
|
38
42
|
|
39
|
-
|
43
|
+
app.run_hook :after_s3_sync, ignored: files_to_ignore.map(&:path),
|
40
44
|
created: files_to_create.map(&:path),
|
41
45
|
updated: files_to_update.map(&:path),
|
42
46
|
deleted: files_to_delete.map(&:path)
|
@@ -52,13 +56,29 @@ module Middleman
|
|
52
56
|
end
|
53
57
|
end
|
54
58
|
|
59
|
+
def add_local_resource(mm_resource)
|
60
|
+
s3_sync_resources[mm_resource.destination_path] = S3Sync::Resource.new(mm_resource, remote_resource_for_path(mm_resource.destination_path)).tap(&:status)
|
61
|
+
end
|
62
|
+
|
63
|
+
def remote_only_paths
|
64
|
+
paths - s3_sync_resources.keys
|
65
|
+
end
|
66
|
+
|
67
|
+
def app=(app)
|
68
|
+
@app = app
|
69
|
+
@app.extend ::Middleman::S3SyncExtension::ClassMethods
|
70
|
+
end
|
71
|
+
|
72
|
+
def content_types
|
73
|
+
@content_types || {}
|
74
|
+
end
|
75
|
+
|
55
76
|
protected
|
56
77
|
def update_bucket_versioning
|
57
78
|
connection.put_bucket_versioning(s3_sync_options.bucket, "Enabled") if s3_sync_options.version_bucket
|
58
79
|
end
|
59
80
|
|
60
81
|
def connection
|
61
|
-
|
62
82
|
connection_options = {
|
63
83
|
:region => s3_sync_options.region,
|
64
84
|
:path_style => s3_sync_options.path_style
|
@@ -76,37 +96,20 @@ module Middleman
|
|
76
96
|
@connection ||= Fog::Storage::AWS.new(connection_options)
|
77
97
|
end
|
78
98
|
|
79
|
-
def
|
80
|
-
|
81
|
-
progress_bar.increment
|
82
|
-
S3Sync::Resource.new(p, bucket_files.find { |f| f.key == "#{s3_sync_options.prefix}#{p}" }).tap(&:status)
|
83
|
-
end
|
99
|
+
def remote_resource_for_path(path)
|
100
|
+
bucket_files.find { |f| f.key == "#{s3_sync_options.prefix}#{path}" }
|
84
101
|
end
|
85
102
|
|
86
|
-
def
|
87
|
-
@
|
103
|
+
def s3_sync_resources
|
104
|
+
@s3_sync_resources ||= {}
|
88
105
|
end
|
89
106
|
|
90
107
|
def paths
|
91
108
|
@paths ||= begin
|
92
|
-
|
93
|
-
(remote_paths.map { |rp| rp.gsub(/^#{s3_sync_options.prefix}/, '')} + local_paths).uniq.sort
|
109
|
+
(remote_paths.map { |rp| rp.gsub(/^#{s3_sync_options.prefix}/, '')} + s3_sync_resources.keys).uniq.sort
|
94
110
|
end
|
95
111
|
end
|
96
112
|
|
97
|
-
def local_paths
|
98
|
-
@local_paths ||= begin
|
99
|
-
local_paths = (Dir[build_dir + "/**/*"] + Dir[build_dir + "/**/.*"])
|
100
|
-
.reject { |p| File.directory?(p) }
|
101
|
-
|
102
|
-
if s3_sync_options.prefer_gzip
|
103
|
-
local_paths.reject! { |p| p =~ /\.gz$/ && File.exist?(p.gsub(/\.gz$/, '')) }
|
104
|
-
end
|
105
|
-
|
106
|
-
local_paths.pmap(32) { |p| p.gsub(/#{build_dir}\//, '') }
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
113
|
def remote_paths
|
111
114
|
@remote_paths ||= if s3_sync_options.delete
|
112
115
|
bucket_files.map(&:key)
|
@@ -150,27 +153,33 @@ module Middleman
|
|
150
153
|
end
|
151
154
|
|
152
155
|
def work_to_be_done?
|
156
|
+
Parallel.each(mm_resources, in_threads: 8, progress: "Processing sitemap") { |mm_resource| add_local_resource(mm_resource) }
|
157
|
+
|
158
|
+
Parallel.each(remote_only_paths, in_threads: 8, progress: "Processing remote files") do |remote_path|
|
159
|
+
s3_sync_resources[remote_path] ||= S3Sync::Resource.new(nil, remote_resource_for_path(remote_path)).tap(&:status)
|
160
|
+
end
|
161
|
+
|
153
162
|
!(files_to_create.empty? && files_to_update.empty? && files_to_delete.empty?)
|
154
163
|
end
|
155
164
|
|
156
165
|
def files_to_delete
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
166
|
+
if s3_sync_options.delete
|
167
|
+
s3_sync_resources.values.select { |r| r.to_delete? }
|
168
|
+
else
|
169
|
+
[]
|
170
|
+
end
|
162
171
|
end
|
163
172
|
|
164
173
|
def files_to_create
|
165
|
-
|
174
|
+
s3_sync_resources.values.select { |r| r.to_create? }
|
166
175
|
end
|
167
176
|
|
168
177
|
def files_to_update
|
169
|
-
|
178
|
+
s3_sync_resources.values.select { |r| r.to_update? }
|
170
179
|
end
|
171
180
|
|
172
181
|
def files_to_ignore
|
173
|
-
|
182
|
+
s3_sync_resources.values.select { |r| r.to_ignore? }
|
174
183
|
end
|
175
184
|
|
176
185
|
def build_dir
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'map'
|
2
|
+
|
3
|
+
module Middleman
|
4
|
+
module S3Sync
|
5
|
+
module CachingPolicy
|
6
|
+
def add_caching_policy(content_type, options)
|
7
|
+
caching_policies[content_type.to_s] = BrowserCachePolicy.new(options)
|
8
|
+
end
|
9
|
+
|
10
|
+
def caching_policy_for(content_type)
|
11
|
+
caching_policies.fetch(content_type.to_s, caching_policies[:default])
|
12
|
+
end
|
13
|
+
|
14
|
+
def default_caching_policy
|
15
|
+
caching_policies[:default]
|
16
|
+
end
|
17
|
+
|
18
|
+
def caching_policies
|
19
|
+
@caching_policies ||= Map.new
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class BrowserCachePolicy
|
24
|
+
attr_accessor :policies
|
25
|
+
|
26
|
+
def initialize(options = {})
|
27
|
+
@policies = Map.from_hash(options)
|
28
|
+
end
|
29
|
+
|
30
|
+
def cache_control
|
31
|
+
policy = []
|
32
|
+
policy << "max-age=#{policies.max_age}" if policies.has_key?(:max_age)
|
33
|
+
policy << "s-maxage=#{policies.s_maxage}" if policies.has_key?(:s_maxage)
|
34
|
+
policy << "public" if policies.fetch(:public, false)
|
35
|
+
policy << "private" if policies.fetch(:private, false)
|
36
|
+
policy << "no-cache" if policies.fetch(:no_cache, false)
|
37
|
+
policy << "no-store" if policies.fetch(:no_store, false)
|
38
|
+
policy << "must-revalidate" if policies.fetch(:must_revalidate, false)
|
39
|
+
policy << "proxy-revalidate" if policies.fetch(:proxy_revalidate, false)
|
40
|
+
if policy.empty?
|
41
|
+
nil
|
42
|
+
else
|
43
|
+
policy.join(", ")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s
|
48
|
+
cache_control
|
49
|
+
end
|
50
|
+
|
51
|
+
def expires
|
52
|
+
if expiration = policies.fetch(:expires, nil)
|
53
|
+
CGI.rfc1123_date(expiration)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -19,36 +19,16 @@ module Middleman
|
|
19
19
|
:reduced_redundancy_storage,
|
20
20
|
:path_style,
|
21
21
|
:version_bucket,
|
22
|
+
:dry_run,
|
22
23
|
:verbose,
|
23
24
|
:content_types
|
24
25
|
]
|
25
26
|
attr_accessor *OPTIONS
|
26
27
|
|
27
|
-
def initialize
|
28
|
-
# read config from .s3_sync on initialization
|
29
|
-
self.read_config
|
30
|
-
end
|
31
|
-
|
32
28
|
def acl
|
33
29
|
@acl || 'public-read'
|
34
30
|
end
|
35
31
|
|
36
|
-
def add_caching_policy(content_type, options)
|
37
|
-
caching_policies[content_type.to_s] = BrowserCachePolicy.new(options)
|
38
|
-
end
|
39
|
-
|
40
|
-
def caching_policy_for(content_type)
|
41
|
-
caching_policies.fetch(content_type.to_s, caching_policies[:default])
|
42
|
-
end
|
43
|
-
|
44
|
-
def default_caching_policy
|
45
|
-
caching_policies[:default]
|
46
|
-
end
|
47
|
-
|
48
|
-
def caching_policies
|
49
|
-
@caching_policies ||= Map.new
|
50
|
-
end
|
51
|
-
|
52
32
|
def aws_access_key_id=(aws_access_key_id)
|
53
33
|
@aws_access_key_id = aws_access_key_id if aws_access_key_id
|
54
34
|
end
|
@@ -95,75 +75,13 @@ module Middleman
|
|
95
75
|
end
|
96
76
|
|
97
77
|
def prefix
|
98
|
-
@prefix.
|
78
|
+
@prefix.nil? || @prefix.empty? ? "" : "#{@prefix}/"
|
99
79
|
end
|
100
80
|
|
101
81
|
def version_bucket
|
102
82
|
@version_bucket.nil? ? false : @version_bucket
|
103
83
|
end
|
104
84
|
|
105
|
-
def content_types
|
106
|
-
@content_types || {}
|
107
|
-
end
|
108
|
-
|
109
|
-
# Read config options from an IO stream and set them on `self`. Defaults
|
110
|
-
# to reading from the `.s3_sync` file in the MM project root if it exists.
|
111
|
-
#
|
112
|
-
# @param io [IO] an IO stream to read from
|
113
|
-
# @return [void]
|
114
|
-
def read_config(io = nil)
|
115
|
-
unless io
|
116
|
-
root_path = ::Middleman::Application.root
|
117
|
-
config_file_path = File.join(root_path, ".s3_sync")
|
118
|
-
|
119
|
-
# skip if config file does not exist
|
120
|
-
return unless File.exists?(config_file_path)
|
121
|
-
|
122
|
-
io = File.open(config_file_path, "r")
|
123
|
-
end
|
124
|
-
|
125
|
-
config = YAML.load(io).symbolize_keys
|
126
|
-
|
127
|
-
OPTIONS.each do |config_option|
|
128
|
-
self.send("#{config_option}=".to_sym, config[config_option]) if config[config_option]
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
protected
|
133
|
-
class BrowserCachePolicy
|
134
|
-
attr_accessor :policies
|
135
|
-
|
136
|
-
def initialize(options)
|
137
|
-
@policies = Map.from_hash(options)
|
138
|
-
end
|
139
|
-
|
140
|
-
def cache_control
|
141
|
-
policy = []
|
142
|
-
policy << "max-age=#{policies.max_age}" if policies.has_key?(:max_age)
|
143
|
-
policy << "s-maxage=#{policies.s_maxage}" if policies.has_key?(:s_maxage)
|
144
|
-
policy << "public" if policies.fetch(:public, false)
|
145
|
-
policy << "private" if policies.fetch(:private, false)
|
146
|
-
policy << "no-cache" if policies.fetch(:no_cache, false)
|
147
|
-
policy << "no-store" if policies.fetch(:no_store, false)
|
148
|
-
policy << "must-revalidate" if policies.fetch(:must_revalidate, false)
|
149
|
-
policy << "proxy-revalidate" if policies.fetch(:proxy_revalidate, false)
|
150
|
-
if policy.empty?
|
151
|
-
nil
|
152
|
-
else
|
153
|
-
policy.join(", ")
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
def to_s
|
158
|
-
cache_control
|
159
|
-
end
|
160
|
-
|
161
|
-
def expires
|
162
|
-
if expiration = policies.fetch(:expires, nil)
|
163
|
-
CGI.rfc1123_date(expiration)
|
164
|
-
end
|
165
|
-
end
|
166
|
-
end
|
167
85
|
end
|
168
86
|
end
|
169
87
|
end
|
@@ -1,14 +1,15 @@
|
|
1
1
|
module Middleman
|
2
2
|
module S3Sync
|
3
3
|
class Resource
|
4
|
-
attr_accessor :path, :partial_s3_resource, :full_s3_resource, :content_type, :gzipped, :options
|
4
|
+
attr_accessor :path, :resource, :partial_s3_resource, :full_s3_resource, :content_type, :gzipped, :options
|
5
5
|
|
6
6
|
CONTENT_MD5_KEY = 'x-amz-meta-content-md5'
|
7
7
|
|
8
8
|
include Status
|
9
9
|
|
10
|
-
def initialize(
|
11
|
-
@
|
10
|
+
def initialize(resource, partial_s3_resource)
|
11
|
+
@resource = resource
|
12
|
+
@path = resource ? resource.destination_path : partial_s3_resource.key
|
12
13
|
@partial_s3_resource = partial_s3_resource
|
13
14
|
end
|
14
15
|
|
@@ -61,7 +62,7 @@ module Middleman
|
|
61
62
|
s3_resource.merge_attributes(to_h)
|
62
63
|
s3_resource.body = body
|
63
64
|
|
64
|
-
s3_resource.save
|
65
|
+
s3_resource.save unless options.dry_run
|
65
66
|
}
|
66
67
|
end
|
67
68
|
|
@@ -76,23 +77,23 @@ module Middleman
|
|
76
77
|
|
77
78
|
def destroy!
|
78
79
|
say_status ANSI.red { "Deleting" } + " " + remote_path
|
79
|
-
bucket.files.destroy remote_path
|
80
|
+
bucket.files.destroy remote_path unless options.dry_run
|
80
81
|
end
|
81
82
|
|
82
83
|
def create!
|
83
84
|
say_status ANSI.green { "Creating" } + " #{remote_path}#{ gzipped ? ANSI.white {' (gzipped)'} : ''}"
|
84
85
|
local_content { |body|
|
85
|
-
bucket.files.create(to_h.merge(body: body))
|
86
|
+
bucket.files.create(to_h.merge(body: body)) unless options.dry_run
|
86
87
|
}
|
87
88
|
end
|
88
89
|
|
89
90
|
def ignore!
|
90
|
-
reason = if redirect?
|
91
|
-
:redirect
|
92
|
-
elsif directory?
|
93
|
-
:directory
|
94
|
-
end
|
95
91
|
if options.verbose
|
92
|
+
reason = if redirect?
|
93
|
+
:redirect
|
94
|
+
elsif directory?
|
95
|
+
:directory
|
96
|
+
end
|
96
97
|
say_status ANSI.yellow {"Ignoring"} + " #{remote_path} #{ reason ? ANSI.white {"(#{reason})" } : "" }"
|
97
98
|
end
|
98
99
|
end
|
@@ -135,6 +136,8 @@ module Middleman
|
|
135
136
|
elsif local? && remote?
|
136
137
|
if options.force
|
137
138
|
:updated
|
139
|
+
elsif not caching_policy_match?
|
140
|
+
:updated
|
138
141
|
elsif local_object_md5 == remote_object_md5
|
139
142
|
:identical
|
140
143
|
else
|
@@ -155,8 +158,10 @@ module Middleman
|
|
155
158
|
:new
|
156
159
|
elsif remote? && redirect?
|
157
160
|
:ignored
|
158
|
-
|
161
|
+
elsif remote?
|
159
162
|
:deleted
|
163
|
+
else
|
164
|
+
:ignored
|
160
165
|
end
|
161
166
|
end
|
162
167
|
|
@@ -205,12 +210,20 @@ module Middleman
|
|
205
210
|
end
|
206
211
|
|
207
212
|
def content_type
|
208
|
-
@content_type ||=
|
209
|
-
@content_type ||=
|
213
|
+
@content_type ||= Middleman::S3Sync.content_types[path]
|
214
|
+
@content_type ||= resource.content_type
|
210
215
|
end
|
211
216
|
|
212
217
|
def caching_policy
|
213
|
-
@caching_policy ||=
|
218
|
+
@caching_policy ||= Middleman::S3Sync.caching_policy_for(content_type)
|
219
|
+
end
|
220
|
+
|
221
|
+
def caching_policy_match?
|
222
|
+
if (caching_policy)
|
223
|
+
caching_policy.cache_control == full_s3_resource.cache_control
|
224
|
+
else
|
225
|
+
true
|
226
|
+
end
|
214
227
|
end
|
215
228
|
|
216
229
|
protected
|
data/middleman-s3_sync.gemspec
CHANGED
@@ -18,19 +18,20 @@ Gem::Specification.new do |gem|
|
|
18
18
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
19
19
|
gem.require_paths = ["lib"]
|
20
20
|
|
21
|
-
gem.add_runtime_dependency 'middleman-core', '
|
21
|
+
gem.add_runtime_dependency 'middleman-core', '~> 3.3'
|
22
22
|
gem.add_runtime_dependency 'unf'
|
23
23
|
gem.add_runtime_dependency 'fog-aws', '>= 0.1.1'
|
24
24
|
gem.add_runtime_dependency 'map'
|
25
|
-
gem.add_runtime_dependency '
|
25
|
+
gem.add_runtime_dependency 'parallel'
|
26
26
|
gem.add_runtime_dependency 'ruby-progressbar'
|
27
27
|
gem.add_runtime_dependency 'ansi', '~> 1.5.0'
|
28
28
|
|
29
29
|
gem.add_development_dependency 'rake'
|
30
30
|
gem.add_development_dependency 'pry'
|
31
|
-
gem.add_development_dependency 'pry-
|
31
|
+
gem.add_development_dependency 'pry-byebug'
|
32
32
|
gem.add_development_dependency 'rspec', '>= 3.0.0'
|
33
33
|
gem.add_development_dependency 'rspec-its'
|
34
|
+
gem.add_development_dependency 'rspec-mocks'
|
34
35
|
gem.add_development_dependency 'timerizer'
|
35
36
|
gem.add_development_dependency 'travis'
|
36
37
|
gem.add_development_dependency 'travis-lint'
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'middleman/s3_sync/caching_policy'
|
3
|
+
|
4
|
+
describe Middleman::S3Sync::BrowserCachePolicy do
|
5
|
+
let(:options) { Hash.new }
|
6
|
+
subject(:policy) { Middleman::S3Sync::BrowserCachePolicy.new(options) }
|
7
|
+
|
8
|
+
it "should be blank" do
|
9
|
+
policy.should_not be_nil
|
10
|
+
|
11
|
+
policy.to_s.should_not =~ /max-age=/
|
12
|
+
policy.to_s.should_not =~ /s-maxage=/
|
13
|
+
policy.to_s.should_not =~ /public/
|
14
|
+
policy.to_s.should_not =~ /private/
|
15
|
+
policy.to_s.should_not =~ /no-cache/
|
16
|
+
policy.to_s.should_not =~ /no-store/
|
17
|
+
policy.to_s.should_not =~ /must-revalidate/
|
18
|
+
policy.to_s.should_not =~ /proxy-revalidate/
|
19
|
+
policy.expires.should be_nil
|
20
|
+
end
|
21
|
+
|
22
|
+
context "setting max-age" do
|
23
|
+
let(:options) { { max_age: 300 } }
|
24
|
+
|
25
|
+
its(:to_s) { should =~ /max-age=300/ }
|
26
|
+
end
|
27
|
+
|
28
|
+
context "setting s-maxage" do
|
29
|
+
let(:options) { { s_maxage: 300 } }
|
30
|
+
|
31
|
+
its(:to_s) { should =~ /s-maxage=300/ }
|
32
|
+
end
|
33
|
+
|
34
|
+
context "set public flag" do
|
35
|
+
let(:options) { { public: true } }
|
36
|
+
its(:to_s) { should =~ /public/ }
|
37
|
+
end
|
38
|
+
|
39
|
+
context "it should set the private flag if it is set to true" do
|
40
|
+
let(:options) { { private: true } }
|
41
|
+
its(:to_s) { should =~ /private/ }
|
42
|
+
end
|
43
|
+
|
44
|
+
context "it should set the no-cache flag when set property" do
|
45
|
+
let(:options) { { no_cache: true }}
|
46
|
+
its(:to_s) { should =~ /no-cache/ }
|
47
|
+
end
|
48
|
+
|
49
|
+
context "setting the no-store flag" do
|
50
|
+
let(:options) { { no_store: true } }
|
51
|
+
its(:to_s) { should =~ /no-store/ }
|
52
|
+
end
|
53
|
+
|
54
|
+
context "setting the must-revalidate policy" do
|
55
|
+
let(:options) { { must_revalidate: true } }
|
56
|
+
its(:to_s) { should =~ /must-revalidate/ }
|
57
|
+
end
|
58
|
+
|
59
|
+
context "setting the proxy-revalidate policy" do
|
60
|
+
let(:options) { { proxy_revalidate: true } }
|
61
|
+
its(:to_s) { should =~ /proxy-revalidate/ }
|
62
|
+
end
|
63
|
+
|
64
|
+
context "divide caching policiies with a comma and a space" do
|
65
|
+
let(:options) { { :max_age => 300, :public => true } }
|
66
|
+
|
67
|
+
it "splits policies eith commans and spaces" do
|
68
|
+
policies = policy.to_s.split(/, /)
|
69
|
+
policies.length.should == 2
|
70
|
+
policies.first.should == 'max-age=300'
|
71
|
+
policies.last.should == 'public'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context "set the expiration date" do
|
76
|
+
let(:options) { { expires: 1.years.from_now } }
|
77
|
+
|
78
|
+
its(:expires) { should == CGI.rfc1123_date(1.year.from_now )}
|
79
|
+
end
|
80
|
+
end
|
data/spec/resource_spec.rb
CHANGED
@@ -6,14 +6,19 @@ describe Middleman::S3Sync::Resource do
|
|
6
6
|
Middleman::S3Sync::Options.new
|
7
7
|
}
|
8
8
|
|
9
|
+
let(:mm_resource) {
|
10
|
+
double(
|
11
|
+
destination_path: 'path/to/resource.html'
|
12
|
+
)
|
13
|
+
}
|
9
14
|
before do
|
10
|
-
Middleman::S3Sync.
|
15
|
+
Middleman::S3Sync.s3_sync_options = options
|
11
16
|
options.build_dir = "build"
|
12
17
|
options.prefer_gzip = false
|
13
18
|
end
|
14
19
|
|
15
20
|
context "a new resource" do
|
16
|
-
subject(:resource) { Middleman::S3Sync::Resource.new(
|
21
|
+
subject(:resource) { Middleman::S3Sync::Resource.new(mm_resource, nil) }
|
17
22
|
|
18
23
|
context "without a prefix" do
|
19
24
|
before do
|
@@ -75,14 +80,17 @@ describe Middleman::S3Sync::Resource do
|
|
75
80
|
end
|
76
81
|
|
77
82
|
context "the file does not exist locally" do
|
78
|
-
subject(:resource) { Middleman::S3Sync::Resource.new(
|
83
|
+
subject(:resource) { Middleman::S3Sync::Resource.new(nil, remote) }
|
79
84
|
|
80
|
-
let(:remote) {
|
85
|
+
let(:remote) {
|
86
|
+
double(
|
87
|
+
key: 'path/to/resource.html',
|
88
|
+
metadata: {}
|
89
|
+
)
|
90
|
+
}
|
81
91
|
|
82
92
|
before do
|
83
|
-
|
84
|
-
allow(remote).to receive(:metadata).and_return({})
|
85
|
-
resource.full_s3_resource = remote
|
93
|
+
resource.full_s3_resource = remote
|
86
94
|
end
|
87
95
|
|
88
96
|
context "without a prefix" do
|
@@ -116,7 +124,7 @@ describe Middleman::S3Sync::Resource do
|
|
116
124
|
expect(resource).to be_remote
|
117
125
|
end
|
118
126
|
|
119
|
-
it "
|
127
|
+
it "does not exist locally" do
|
120
128
|
expect(resource).not_to be_local
|
121
129
|
end
|
122
130
|
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: middleman-s3_sync
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.0
|
4
|
+
version: 3.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Frederic Jean
|
@@ -9,22 +9,22 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-
|
12
|
+
date: 2015-09-25 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: middleman-core
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
17
17
|
requirements:
|
18
|
-
- - "
|
18
|
+
- - "~>"
|
19
19
|
- !ruby/object:Gem::Version
|
20
|
-
version: 3.
|
20
|
+
version: '3.3'
|
21
21
|
type: :runtime
|
22
22
|
prerelease: false
|
23
23
|
version_requirements: !ruby/object:Gem::Requirement
|
24
24
|
requirements:
|
25
|
-
- - "
|
25
|
+
- - "~>"
|
26
26
|
- !ruby/object:Gem::Version
|
27
|
-
version: 3.
|
27
|
+
version: '3.3'
|
28
28
|
- !ruby/object:Gem::Dependency
|
29
29
|
name: unf
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|
@@ -68,7 +68,7 @@ dependencies:
|
|
68
68
|
- !ruby/object:Gem::Version
|
69
69
|
version: '0'
|
70
70
|
- !ruby/object:Gem::Dependency
|
71
|
-
name:
|
71
|
+
name: parallel
|
72
72
|
requirement: !ruby/object:Gem::Requirement
|
73
73
|
requirements:
|
74
74
|
- - ">="
|
@@ -138,7 +138,7 @@ dependencies:
|
|
138
138
|
- !ruby/object:Gem::Version
|
139
139
|
version: '0'
|
140
140
|
- !ruby/object:Gem::Dependency
|
141
|
-
name: pry-
|
141
|
+
name: pry-byebug
|
142
142
|
requirement: !ruby/object:Gem::Requirement
|
143
143
|
requirements:
|
144
144
|
- - ">="
|
@@ -179,6 +179,20 @@ dependencies:
|
|
179
179
|
- - ">="
|
180
180
|
- !ruby/object:Gem::Version
|
181
181
|
version: '0'
|
182
|
+
- !ruby/object:Gem::Dependency
|
183
|
+
name: rspec-mocks
|
184
|
+
requirement: !ruby/object:Gem::Requirement
|
185
|
+
requirements:
|
186
|
+
- - ">="
|
187
|
+
- !ruby/object:Gem::Version
|
188
|
+
version: '0'
|
189
|
+
type: :development
|
190
|
+
prerelease: false
|
191
|
+
version_requirements: !ruby/object:Gem::Requirement
|
192
|
+
requirements:
|
193
|
+
- - ">="
|
194
|
+
- !ruby/object:Gem::Version
|
195
|
+
version: '0'
|
182
196
|
- !ruby/object:Gem::Dependency
|
183
197
|
name: timerizer
|
184
198
|
requirement: !ruby/object:Gem::Requirement
|
@@ -241,13 +255,14 @@ files:
|
|
241
255
|
- lib/middleman-s3_sync/commands.rb
|
242
256
|
- lib/middleman-s3_sync/extension.rb
|
243
257
|
- lib/middleman/s3_sync.rb
|
258
|
+
- lib/middleman/s3_sync/caching_policy.rb
|
244
259
|
- lib/middleman/s3_sync/options.rb
|
245
260
|
- lib/middleman/s3_sync/resource.rb
|
246
261
|
- lib/middleman/s3_sync/status.rb
|
247
262
|
- lib/middleman/s3_sync/version.rb
|
248
263
|
- lib/middleman_extension.rb
|
249
264
|
- middleman-s3_sync.gemspec
|
250
|
-
- spec/
|
265
|
+
- spec/caching_policy_spec.rb
|
251
266
|
- spec/resource_spec.rb
|
252
267
|
- spec/spec_helper.rb
|
253
268
|
homepage: http://github.com/fredjean/middleman-s3_sync
|
@@ -275,6 +290,6 @@ signing_key:
|
|
275
290
|
specification_version: 4
|
276
291
|
summary: Tries really, really hard not to push files to S3.
|
277
292
|
test_files:
|
278
|
-
- spec/
|
293
|
+
- spec/caching_policy_spec.rb
|
279
294
|
- spec/resource_spec.rb
|
280
295
|
- spec/spec_helper.rb
|
data/spec/options_spec.rb
DELETED
@@ -1,130 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
require 'middleman/s3_sync/options'
|
3
|
-
|
4
|
-
describe Middleman::S3Sync::Options do
|
5
|
-
subject(:options) { Middleman::S3Sync::Options.new }
|
6
|
-
|
7
|
-
its(:delete) { is_expected.to eq(true) }
|
8
|
-
its(:after_build) { is_expected.to eq(false)}
|
9
|
-
its(:prefer_gzip) { is_expected.to eq(true) }
|
10
|
-
its(:aws_secret_access_key) { is_expected.to eq(ENV['AWS_SECRET_ACCESS_KEY']) }
|
11
|
-
its(:aws_access_key_id) { is_expected.to eq(ENV['AWS_ACCESS_KEY_ID']) }
|
12
|
-
its(:caching_policies) { is_expected.to be_empty }
|
13
|
-
its(:default_caching_policy) { is_expected.to be_nil }
|
14
|
-
|
15
|
-
context "browser caching policy" do
|
16
|
-
let(:policy) { options.default_caching_policy }
|
17
|
-
|
18
|
-
it "should have a blank default caching policy" do
|
19
|
-
options.add_caching_policy :default, {}
|
20
|
-
|
21
|
-
policy.should_not be_nil
|
22
|
-
|
23
|
-
policy.to_s.should_not =~ /max-age=/
|
24
|
-
policy.to_s.should_not =~ /s-maxage=/
|
25
|
-
policy.to_s.should_not =~ /public/
|
26
|
-
policy.to_s.should_not =~ /private/
|
27
|
-
policy.to_s.should_not =~ /no-cache/
|
28
|
-
policy.to_s.should_not =~ /no-store/
|
29
|
-
policy.to_s.should_not =~ /must-revalidate/
|
30
|
-
policy.to_s.should_not =~ /proxy-revalidate/
|
31
|
-
policy.expires.should be_nil
|
32
|
-
end
|
33
|
-
|
34
|
-
it "should set the max-age policy" do
|
35
|
-
options.add_caching_policy :default, :max_age => 300
|
36
|
-
|
37
|
-
policy.to_s.should =~ /max-age=300/
|
38
|
-
end
|
39
|
-
|
40
|
-
it "should set the s-maxage policy" do
|
41
|
-
options.add_caching_policy :default, :s_maxage => 300
|
42
|
-
|
43
|
-
policy.to_s.should =~ /s-maxage=300/
|
44
|
-
end
|
45
|
-
|
46
|
-
it "should set the public flag on the policy if set to true" do
|
47
|
-
options.add_caching_policy :default, :public => true
|
48
|
-
|
49
|
-
policy.to_s.should =~ /public/
|
50
|
-
end
|
51
|
-
|
52
|
-
it "should not set the public flag on the policy if it is set to false" do
|
53
|
-
options.add_caching_policy :default, :public => false
|
54
|
-
|
55
|
-
policy.to_s.should_not =~ /public/
|
56
|
-
end
|
57
|
-
|
58
|
-
it "should set the private flag on the policy if it is set to true" do
|
59
|
-
options.add_caching_policy :default, :private => true
|
60
|
-
|
61
|
-
policy.to_s.should =~ /private/
|
62
|
-
end
|
63
|
-
|
64
|
-
it "should set the no-cache flag on the policy if it is set to true" do
|
65
|
-
options.add_caching_policy :default, :no_cache => true
|
66
|
-
|
67
|
-
policy.to_s.should =~ /no-cache/
|
68
|
-
end
|
69
|
-
|
70
|
-
it "should set the no-store flag if it is set to true" do
|
71
|
-
options.add_caching_policy :default, :no_store => true
|
72
|
-
|
73
|
-
policy.to_s.should =~ /no-store/
|
74
|
-
end
|
75
|
-
|
76
|
-
it "should set the must-revalidate policy if it is set to true" do
|
77
|
-
options.add_caching_policy :default, :must_revalidate => true
|
78
|
-
|
79
|
-
policy.to_s.should =~ /must-revalidate/
|
80
|
-
end
|
81
|
-
|
82
|
-
it "should set the proxy-revalidate policy if it is set to true" do
|
83
|
-
options.add_caching_policy :default, :proxy_revalidate => true
|
84
|
-
|
85
|
-
policy.to_s.should =~ /proxy-revalidate/
|
86
|
-
end
|
87
|
-
|
88
|
-
it "should divide caching policies with commas and a space" do
|
89
|
-
options.add_caching_policy :default, :max_age => 300, :public => true
|
90
|
-
|
91
|
-
policies = policy.to_s.split(/, /)
|
92
|
-
policies.length.should == 2
|
93
|
-
policies.first.should == 'max-age=300'
|
94
|
-
policies.last.should == 'public'
|
95
|
-
end
|
96
|
-
|
97
|
-
it "should set the expiration date" do
|
98
|
-
expiration = 1.years.from_now
|
99
|
-
|
100
|
-
options.add_caching_policy :default, :expires => expiration
|
101
|
-
policy.expires.should == CGI.rfc1123_date(expiration)
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
context "#read_config" do
|
106
|
-
let(:aws_access_key_id) { "foo" }
|
107
|
-
let(:aws_secret_access_key) { "bar" }
|
108
|
-
let(:bucket) { "baz" }
|
109
|
-
let(:config) { { "aws_access_key_id" => aws_access_key_id, "aws_secret_access_key" => aws_secret_access_key, "bucket" => bucket } }
|
110
|
-
let(:file) { StringIO.new(YAML.dump(config)) }
|
111
|
-
|
112
|
-
before do
|
113
|
-
options.read_config(file)
|
114
|
-
end
|
115
|
-
|
116
|
-
its(:aws_access_key_id) { should eq(aws_access_key_id) }
|
117
|
-
its(:aws_secret_access_key) { should eq(aws_secret_access_key) }
|
118
|
-
its(:bucket) { should eq(bucket) }
|
119
|
-
end
|
120
|
-
|
121
|
-
context "prefix with http_prefix" do
|
122
|
-
before do
|
123
|
-
options.http_prefix = "/blog"
|
124
|
-
options.prefix = "blog"
|
125
|
-
end
|
126
|
-
|
127
|
-
its(:prefix) { should eq("")}
|
128
|
-
end
|
129
|
-
|
130
|
-
end
|