knife-essentials 0.5.4 → 0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/chef/knife/raw.rb +3 -4
- data/lib/chef/sandbox_uploader.rb +208 -0
- data/lib/chef_fs/command_line.rb +90 -50
- data/lib/chef_fs/file_system.rb +123 -17
- data/lib/chef_fs/file_system/base_fs_object.rb +37 -4
- data/lib/chef_fs/file_system/chef_repository_file_system_entry.rb +25 -0
- data/lib/chef_fs/file_system/cookbook_dir.rb +45 -11
- data/lib/chef_fs/file_system/cookbook_file.rb +26 -0
- data/lib/chef_fs/file_system/file_system_entry.rb +2 -2
- data/lib/chef_fs/file_system/rest_list_entry.rb +17 -5
- data/lib/chef_fs/version.rb +1 -1
- data/spec/chef_fs/diff_spec.rb +3 -77
- data/spec/chef_fs/file_pattern_spec.rb +3 -2
- data/spec/chef_fs/file_system/chef_server_root_dir_spec.rb +1 -0
- data/spec/chef_fs/file_system/cookbooks_dir_spec.rb +2 -1
- data/spec/chef_fs/file_system/data_bags_dir_spec.rb +2 -1
- data/spec/chef_fs/file_system_spec.rb +1 -1
- data/spec/support/file_system_support.rb +0 -22
- data/spec/support/spec_helper.rb +8 -0
- metadata +4 -3
- data/lib/chef_fs/diff.rb +0 -167
data/lib/chef/knife/raw.rb
CHANGED
@@ -64,8 +64,7 @@ class Chef
|
|
64
64
|
chef_rest.retriable_rest_request(method, url, json_body, headers) do |rest_request|
|
65
65
|
response = rest_request.call {|r| r.read_body}
|
66
66
|
|
67
|
-
|
68
|
-
response_body = response.body
|
67
|
+
response_body = chef_rest.decompress_body(response)
|
69
68
|
|
70
69
|
if response.kind_of?(Net::HTTPSuccess)
|
71
70
|
if config[:pretty] && response['content-type'] =~ /json/
|
@@ -78,7 +77,7 @@ class Chef
|
|
78
77
|
follow_redirect {api_request(:GET, create_url(redirect_location))}
|
79
78
|
else
|
80
79
|
# have to decompress the body before making an exception for it. But the body could be nil.
|
81
|
-
|
80
|
+
response.body.replace(chef_rest.decompress_body(response)) if response.body.respond_to?(:replace)
|
82
81
|
|
83
82
|
if response['content-type'] =~ /json/
|
84
83
|
exception = response_body
|
@@ -98,7 +97,7 @@ class Chef
|
|
98
97
|
headers['Accept'] = "application/json" unless raw
|
99
98
|
headers["Content-Type"] = 'application/json' if json_body
|
100
99
|
headers['Content-Length'] = json_body.bytesize.to_s if json_body
|
101
|
-
|
100
|
+
headers[Chef::REST::RESTRequest::ACCEPT_ENCODING] = Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE
|
102
101
|
headers.merge!(chef_rest.authentication_headers(method, url, json_body)) if chef_rest.sign_requests?
|
103
102
|
headers.merge!(Chef::Config[:custom_http_headers]) if Chef::Config[:custom_http_headers]
|
104
103
|
headers
|
@@ -0,0 +1,208 @@
|
|
1
|
+
#
|
2
|
+
# Author:: John Keiser (<jkeiser@opscode.com>)
|
3
|
+
# Copyright:: Copyright (c) 2009 Opscode, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
|
20
|
+
class Chef
|
21
|
+
#
|
22
|
+
# Represents a sandbox uploading session. You pass in a set of files with
|
23
|
+
#
|
24
|
+
# Much of this is shamelessly stolen from Chef::CookbookUploader, placed at
|
25
|
+
# the sandbox level to allow any set of files to be uploaded and to allow
|
26
|
+
# uploading to plug in to the host's threading model (if any).
|
27
|
+
#
|
28
|
+
# A single-threaded caller will want to:
|
29
|
+
#
|
30
|
+
# uploader = SandboxUploader.new(checksum_files, rest)
|
31
|
+
# while uploader.upload_next_file do
|
32
|
+
# end
|
33
|
+
# uploader.commit
|
34
|
+
#
|
35
|
+
# A multithreaded caller will want to keep doing this:
|
36
|
+
#
|
37
|
+
# if !uploader.upload_next_file
|
38
|
+
# uploader.commit
|
39
|
+
class SandboxUploader
|
40
|
+
# Create a new SandboxUploader.
|
41
|
+
#
|
42
|
+
# ==== Attributes
|
43
|
+
#
|
44
|
+
# * +checksums+ - a list of checksums of files you plan to upload
|
45
|
+
# * +rest+ - the rest client to use
|
46
|
+
#
|
47
|
+
# ==== Throws
|
48
|
+
#
|
49
|
+
# This method will throw an HTTP exception if the sandbox creation fails.
|
50
|
+
def initialize(checksum, rest)
|
51
|
+
# Grab the list of URLs we need to upload to
|
52
|
+
checksums_hash = checksums.inject({}) { |hash,checksum| hash[checksum] = nil; hash }
|
53
|
+
response = rest.post_rest("sandboxes", { :checksums => checksums_hash })
|
54
|
+
@sandbox_url = response['uri']
|
55
|
+
@to_upload = []
|
56
|
+
response['checksums'].each do |checksum, info|
|
57
|
+
if info['needs_upload'] == true
|
58
|
+
@to_upload << {
|
59
|
+
:checksum => checksum,
|
60
|
+
:url => info['url']
|
61
|
+
}
|
62
|
+
else
|
63
|
+
Chef::Log.debug("#{checksum_files[checksum]} has not changed")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
@uploading = 0
|
67
|
+
end
|
68
|
+
|
69
|
+
# Picks a file off the list and uploads it.
|
70
|
+
#
|
71
|
+
# ==== Parameters
|
72
|
+
#
|
73
|
+
# * +&block+ - if there is a file to upload, +block.call(checksum)+ will be
|
74
|
+
# invoked, and this should return the file contents for the given
|
75
|
+
# checksum.
|
76
|
+
#
|
77
|
+
# ==== Throws
|
78
|
+
#
|
79
|
+
# An HTTP error is thrown if the upload fails.
|
80
|
+
#
|
81
|
+
# ==== Returns
|
82
|
+
#
|
83
|
+
# +{ :checksum => checksum, :url => url }+ if a file was uploaded. +nil+ if
|
84
|
+
# there was no file left to upload.
|
85
|
+
def upload_next_file(&block)
|
86
|
+
@uploading += 1
|
87
|
+
begin
|
88
|
+
file = @to_upload.pop
|
89
|
+
return nil if !file
|
90
|
+
|
91
|
+
begin
|
92
|
+
return upload_file(block.call(file[:checksum]), file[:checksum], file[:url])
|
93
|
+
rescue
|
94
|
+
# Stick the work on top so we work on other things first
|
95
|
+
@to_upload.unshift(file)
|
96
|
+
raise
|
97
|
+
end
|
98
|
+
ensure
|
99
|
+
@uploading -= 1
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Commits the sandbox to Chef.
|
104
|
+
#
|
105
|
+
# ==== Throws
|
106
|
+
#
|
107
|
+
# * An HTTP error if the commit fails.
|
108
|
+
# * "Attempt to commit "
|
109
|
+
#
|
110
|
+
# ==== Returns
|
111
|
+
#
|
112
|
+
# +true+ if a commit occurred. +false+ if commit
|
113
|
+
# has already been called.
|
114
|
+
def commit
|
115
|
+
return false if !@sandbox_url
|
116
|
+
raise "Attempt to commit before uploads are complete!" if !uploads_complete?
|
117
|
+
# nil out sandbox url to avoid race conditions where multiple
|
118
|
+
# callers try to commit
|
119
|
+
sandbox_url = @sandbox_url
|
120
|
+
@sandbox_url = nil
|
121
|
+
@to_upload = nil
|
122
|
+
|
123
|
+
Chef::Log.debug("Committing sandbox")
|
124
|
+
# Retry if S3 is claims a checksum doesn't exist (the eventual
|
125
|
+
# in eventual consistency)
|
126
|
+
retries = 0
|
127
|
+
begin
|
128
|
+
rest.put_rest(sandbox_url, {:is_completed => true})
|
129
|
+
rescue Net::HTTPServerException => e
|
130
|
+
if e.message =~ /^400/ && (retries += 1) <= 5
|
131
|
+
sleep 2
|
132
|
+
retry
|
133
|
+
else
|
134
|
+
raise
|
135
|
+
end
|
136
|
+
end
|
137
|
+
true
|
138
|
+
end
|
139
|
+
|
140
|
+
def cancel
|
141
|
+
# Yeah, so uh, delete doesn't work. But we'd like to imagine it will.
|
142
|
+
sandbox_url = @sandbox_url
|
143
|
+
@sandbox_url = nil
|
144
|
+
@to_upload = nil
|
145
|
+
rest.delete_rest(sandbox_url)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Tells whether uploads are all complete.
|
149
|
+
def uploads_complete?
|
150
|
+
@to_upload.length == 0 && @uploading == 0
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
def upload_file(file_contents, checksum, url)
|
156
|
+
# Checksum is the hexadecimal representation of the md5,
|
157
|
+
# but we need the base64 encoding for the content-md5
|
158
|
+
# header
|
159
|
+
checksum64 = Base64.encode64([checksum].pack("H*")).strip
|
160
|
+
timestamp = Time.now.utc.iso8601
|
161
|
+
# TODO - 5/28/2010, cw: make signing and sending the request streaming
|
162
|
+
sign_obj = Mixlib::Authentication::SignedHeaderAuth.signing_object(
|
163
|
+
:http_method => :put,
|
164
|
+
:path => URI.parse(url).path,
|
165
|
+
:body => file_contents,
|
166
|
+
:timestamp => timestamp,
|
167
|
+
:user_id => rest.client_name
|
168
|
+
)
|
169
|
+
headers = { 'content-type' => 'application/x-binary', 'content-md5' => checksum64, :accept => 'application/json' }
|
170
|
+
headers.merge!(sign_obj.sign(OpenSSL::PKey::RSA.new(rest.signing_key)))
|
171
|
+
|
172
|
+
begin
|
173
|
+
RestClient::Resource.new(url, :headers=>headers, :timeout=>1800, :open_timeout=>1800).put(file_contents)
|
174
|
+
rescue RestClient::Exception => e
|
175
|
+
Chef::Knife.ui.error("Failed to upload #@cookbook : #{e.message}\n#{e.response.body}")
|
176
|
+
raise
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# For CookbookDir
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
def upload(entries, force, freeze)
|
185
|
+
# knife cookbook upload: Validate environment
|
186
|
+
# knife cookbook upload: Warn about cookbook shadowing
|
187
|
+
# knife cookbook upload: Check for broken links
|
188
|
+
# knife cookbook upload: Check for dependencies
|
189
|
+
# CookbookUploader: Validate cookbook syntax, ruby files and templates
|
190
|
+
# SandboxUploader!
|
191
|
+
entries_by_checksum = entries.inject({}) do |hash,entry|
|
192
|
+
if entry.respond_to?(:checksum)
|
193
|
+
checksum = entry.checksum
|
194
|
+
else
|
195
|
+
checksum = ChefFS::Diff.calc_checksum(entry.read)
|
196
|
+
end
|
197
|
+
hash[checksum] = entry
|
198
|
+
end
|
199
|
+
|
200
|
+
uploader = SandboxUploader.new(entries_by_checksum.keys, rest)
|
201
|
+
while uploader.upload_next_file { |checksum| entries_by_checksum[checksum].read }
|
202
|
+
end
|
203
|
+
uploader.commit
|
204
|
+
# CookbookUploader: PUT the manifest to either the force_save_url or save_url for the cookbook
|
205
|
+
# knife cookbook upload: update version constraints for environment
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
data/lib/chef_fs/command_line.rb
CHANGED
@@ -1,115 +1,155 @@
|
|
1
|
-
require 'chef_fs/
|
1
|
+
require 'chef_fs/file_system'
|
2
2
|
|
3
3
|
module ChefFS
|
4
4
|
module CommandLine
|
5
5
|
def self.diff(pattern, a_root, b_root, recurse_depth, output_mode)
|
6
6
|
found_result = false
|
7
|
-
ChefFS::
|
7
|
+
ChefFS::FileSystem.list_pairs(pattern, a_root, b_root) do |a, b|
|
8
8
|
found_result = true
|
9
|
-
|
10
|
-
|
9
|
+
diff_entries(a, b, recurse_depth, output_mode) do |diff|
|
10
|
+
yield diff
|
11
|
+
end
|
11
12
|
end
|
12
13
|
if !found_result && pattern.exact_path
|
13
14
|
yield "#{pattern}: No such file or directory on remote or local"
|
14
15
|
end
|
15
16
|
end
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
# Diff two known leaves (could be files or dirs)
|
20
|
-
def self.diff_leaves(old_file, new_file, output_mode)
|
21
|
-
result = ''
|
18
|
+
# Diff two known entries (could be files or dirs)
|
19
|
+
def self.diff_entries(old_entry, new_entry, recurse_depth, output_mode)
|
22
20
|
# If both are directories
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
21
|
+
if old_entry.dir?
|
22
|
+
if new_entry.dir?
|
23
|
+
if recurse_depth == 0
|
24
|
+
if output_mode != :name_only && output_mode != :name_status
|
25
|
+
yield "Common subdirectories: #{old_entry.path}\n"
|
26
|
+
end
|
27
|
+
else
|
28
|
+
ChefFS::FileSystem.child_pairs(old_entry, new_entry).each do |old_child,new_child|
|
29
|
+
diff_entries(old_child, new_child,
|
30
|
+
recurse_depth ? recurse_depth - 1 : nil, output_mode) do |diff|
|
31
|
+
yield diff
|
32
|
+
end
|
33
|
+
end
|
29
34
|
end
|
30
|
-
|
35
|
+
|
36
|
+
# If old is a directory and new is a file
|
37
|
+
elsif new_entry.exists?
|
31
38
|
if output_mode == :name_only
|
32
|
-
|
39
|
+
yield "#{new_entry.path_for_printing}\n"
|
33
40
|
elsif output_mode == :name_status
|
34
|
-
|
41
|
+
yield "T\t#{new_entry.path_for_printing}\n"
|
35
42
|
else
|
36
|
-
|
43
|
+
yield "File #{new_entry.path_for_printing} is a directory while file #{new_entry.path_for_printing} is a regular file\n"
|
37
44
|
end
|
38
|
-
|
45
|
+
|
46
|
+
# If old is a directory and new does not exist
|
47
|
+
elsif new_entry.parent.can_have_child?(old_entry.name, old_entry.dir?)
|
39
48
|
if output_mode == :name_only
|
40
|
-
|
49
|
+
yield "#{new_entry.path_for_printing}\n"
|
41
50
|
elsif output_mode == :name_status
|
42
|
-
|
51
|
+
yield "D\t#{new_entry.path_for_printing}\n"
|
43
52
|
else
|
44
|
-
|
53
|
+
yield "Only in #{old_entry.parent.path_for_printing}: #{old_entry.name}\n"
|
45
54
|
end
|
46
55
|
end
|
47
56
|
|
48
|
-
# If new is a directory and old does not exist
|
49
57
|
# If new is a directory and old is a file
|
50
|
-
elsif
|
51
|
-
if
|
58
|
+
elsif new_entry.dir?
|
59
|
+
if old_entry.exists?
|
52
60
|
if output_mode == :name_only
|
53
|
-
|
61
|
+
yield "#{new_entry.path_for_printing}\n"
|
54
62
|
elsif output_mode == :name_status
|
55
|
-
|
63
|
+
yield "T\t#{new_entry.path_for_printing}\n"
|
56
64
|
else
|
57
|
-
|
65
|
+
yield "File #{old_entry.path_for_printing} is a regular file while file #{old_entry.path_for_printing} is a directory\n"
|
58
66
|
end
|
59
|
-
|
67
|
+
|
68
|
+
# If new is a directory and old does not exist
|
69
|
+
elsif old_entry.parent.can_have_child?(new_entry.name, new_entry.dir?)
|
60
70
|
if output_mode == :name_only
|
61
|
-
|
71
|
+
yield "#{new_entry.path_for_printing}\n"
|
62
72
|
elsif output_mode == :name_status
|
63
|
-
|
73
|
+
yield "A\t#{new_entry.path_for_printing}\n"
|
64
74
|
else
|
65
|
-
|
75
|
+
yield "Only in #{new_entry.parent.path_for_printing}: #{new_entry.name}\n"
|
66
76
|
end
|
67
77
|
end
|
68
78
|
|
69
79
|
else
|
70
80
|
# Neither is a directory, so they are diffable with file diff
|
71
|
-
|
72
|
-
if
|
81
|
+
are_same, old_value, new_value = ChefFS::FileSystem.compare(old_entry, new_entry)
|
82
|
+
if !are_same
|
83
|
+
if old_value == :none
|
84
|
+
old_exists = false
|
85
|
+
elsif old_value.nil?
|
86
|
+
old_exists = old_entry.exists?
|
87
|
+
else
|
88
|
+
old_exists = true
|
89
|
+
end
|
90
|
+
if new_value == :none
|
91
|
+
new_exists = false
|
92
|
+
elsif new_value.nil?
|
93
|
+
new_exists = new_entry.exists?
|
94
|
+
else
|
95
|
+
new_exists = true
|
96
|
+
end
|
97
|
+
|
73
98
|
# If one of the files doesn't exist, we only want to print the diff if the
|
74
99
|
# other file *could be uploaded/downloaded*.
|
75
|
-
if !
|
76
|
-
return
|
100
|
+
if !old_exists && !old_entry.parent.can_have_child?(new_entry.name, new_entry.dir?)
|
101
|
+
return
|
77
102
|
end
|
78
|
-
if !
|
79
|
-
return
|
103
|
+
if !new_exists && !new_entry.parent.can_have_child?(old_entry.name, old_entry.dir?)
|
104
|
+
return
|
80
105
|
end
|
81
106
|
|
82
107
|
if output_mode == :name_only
|
83
|
-
|
108
|
+
yield "#{new_entry.path_for_printing}\n"
|
84
109
|
elsif output_mode == :name_status
|
85
|
-
if !
|
86
|
-
|
87
|
-
elsif
|
88
|
-
|
110
|
+
if old_value == :none || (old_value == nil && !old_entry.exists?)
|
111
|
+
yield "A\t#{new_entry.path_for_printing}\n"
|
112
|
+
elsif new_value == :none
|
113
|
+
yield "D\t#{new_entry.path_for_printing}\n"
|
89
114
|
else
|
90
|
-
|
115
|
+
yield "M\t#{new_entry.path_for_printing}\n"
|
91
116
|
end
|
92
117
|
else
|
93
|
-
|
94
|
-
|
118
|
+
# If we haven't read the values yet, get them now.
|
119
|
+
begin
|
120
|
+
old_value = old_entry.read if old_value.nil?
|
121
|
+
rescue ChefFS::FileSystem::NotFoundError
|
122
|
+
old_value = :none
|
123
|
+
end
|
124
|
+
begin
|
125
|
+
new_value = new_entry.read if new_value.nil?
|
126
|
+
rescue ChefFS::FileSystem::NotFoundError
|
127
|
+
new_value = :none
|
128
|
+
end
|
129
|
+
|
130
|
+
old_path = old_entry.path_for_printing
|
131
|
+
new_path = new_entry.path_for_printing
|
132
|
+
result = ''
|
95
133
|
result << "diff --knife #{old_path} #{new_path}\n"
|
96
|
-
if
|
134
|
+
if old_value == :none
|
97
135
|
result << "new file\n"
|
98
136
|
old_path = "/dev/null"
|
99
137
|
old_value = ''
|
100
138
|
end
|
101
|
-
if
|
139
|
+
if new_value == :none
|
102
140
|
result << "deleted file\n"
|
103
141
|
new_path = "/dev/null"
|
104
142
|
new_value = ''
|
105
143
|
end
|
106
144
|
result << diff_text(old_path, new_path, old_value, new_value)
|
145
|
+
yield result
|
107
146
|
end
|
108
147
|
end
|
109
148
|
end
|
110
|
-
return result
|
111
149
|
end
|
112
150
|
|
151
|
+
private
|
152
|
+
|
113
153
|
def self.sort_keys(json_object)
|
114
154
|
if json_object.is_a?(Array)
|
115
155
|
json_object.map { |o| sort_keys(o) }
|
data/lib/chef_fs/file_system.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'chef_fs/path_utils'
|
2
|
-
require 'chef_fs/diff'
|
3
2
|
|
4
3
|
module ChefFS
|
5
4
|
module FileSystem
|
@@ -90,24 +89,114 @@ module ChefFS
|
|
90
89
|
#
|
91
90
|
# ==== Examples
|
92
91
|
#
|
93
|
-
# ChefFS::FileSystem.copy_to(FilePattern.new('/cookbooks',
|
92
|
+
# ChefFS::FileSystem.copy_to(FilePattern.new('/cookbooks'),
|
93
|
+
# chef_fs, local_fs, nil, true) do |message|
|
94
|
+
# puts message
|
95
|
+
# end
|
94
96
|
#
|
95
97
|
def self.copy_to(pattern, src_root, dest_root, recurse_depth, options)
|
96
98
|
found_result = false
|
97
|
-
|
98
|
-
ChefFS::Diff::diffable_leaves_from_pattern(pattern, src_root, dest_root, recurse_depth) do |src_leaf, dest_leaf, child_recurse_depth|
|
99
|
+
list_pairs(pattern, src_root, dest_root) do |a, b|
|
99
100
|
found_result = true
|
100
|
-
|
101
|
+
copy_entries(a, b, recurse_depth, options)
|
101
102
|
end
|
102
103
|
if !found_result && pattern.exact_path
|
103
|
-
|
104
|
+
puts "#{pattern}: No such file or directory on remote or local"
|
104
105
|
end
|
105
106
|
end
|
106
107
|
|
108
|
+
# Yield entries for children that are in either +a_root+ or +b_root+, with
|
109
|
+
# matching pairs matched up.
|
110
|
+
#
|
111
|
+
# ==== Yields
|
112
|
+
#
|
113
|
+
# Yields matching entries in pairs:
|
114
|
+
#
|
115
|
+
# [ a_entry, b_entry ]
|
116
|
+
#
|
117
|
+
# ==== Example
|
118
|
+
#
|
119
|
+
# ChefFS::FileSystem.list_pairs(FilePattern.new('**x.txt', a_root, b_root)) do |a, b|
|
120
|
+
# ...
|
121
|
+
# end
|
122
|
+
#
|
123
|
+
def self.list_pairs(pattern, a_root, b_root)
|
124
|
+
# Make sure everything on the server is also on the filesystem, and diff
|
125
|
+
found_paths = Set.new
|
126
|
+
ChefFS::FileSystem.list(a_root, pattern) do |a|
|
127
|
+
found_paths << a.path
|
128
|
+
b = ChefFS::FileSystem.resolve_path(b_root, a.path)
|
129
|
+
yield [ a, b ]
|
130
|
+
end
|
131
|
+
|
132
|
+
# Check the outer regex pattern to see if it matches anything on the
|
133
|
+
# filesystem that isn't on the server
|
134
|
+
ChefFS::FileSystem.list(b_root, pattern) do |b|
|
135
|
+
if !found_paths.include?(b.path)
|
136
|
+
a = ChefFS::FileSystem.resolve_path(a_root, b.path)
|
137
|
+
yield [ a, b ]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Get entries for children of either a or b, with matching pairs matched up.
|
143
|
+
#
|
144
|
+
# ==== Returns
|
145
|
+
#
|
146
|
+
# An array of child pairs.
|
147
|
+
#
|
148
|
+
# [ [ a_child, b_child ], ... ]
|
149
|
+
#
|
150
|
+
# If a child is only in a or only in b, the other child entry will be
|
151
|
+
# retrieved by name (and will most likely be a "nonexistent child").
|
152
|
+
#
|
153
|
+
# ==== Example
|
154
|
+
#
|
155
|
+
# ChefFS::FileSystem.child_pairs(a, b).length
|
156
|
+
#
|
157
|
+
def self.child_pairs(a, b)
|
158
|
+
# If both are directories, recurse into them and diff the children instead of returning ourselves.
|
159
|
+
result = []
|
160
|
+
a_children_names = Set.new
|
161
|
+
a.children.each do |a_child|
|
162
|
+
a_children_names << a_child.name
|
163
|
+
result << [ a_child, b.child(a_child.name) ]
|
164
|
+
end
|
165
|
+
|
166
|
+
# Check b for children that aren't in a
|
167
|
+
b.children.each do |b_child|
|
168
|
+
if !a_children_names.include?(b_child.name)
|
169
|
+
result << [ a.child(b_child.name), b_child ]
|
170
|
+
end
|
171
|
+
end
|
172
|
+
result
|
173
|
+
end
|
174
|
+
|
175
|
+
def self.compare(a, b)
|
176
|
+
are_same, a_value, b_value = a.compare_to(b)
|
177
|
+
if are_same.nil?
|
178
|
+
are_same, b_value, a_value = b.compare_to(a)
|
179
|
+
end
|
180
|
+
if are_same.nil?
|
181
|
+
begin
|
182
|
+
a_value = a.read if a_value.nil?
|
183
|
+
rescue ChefFS::FileSystem::NotFoundError
|
184
|
+
a_value = :none
|
185
|
+
end
|
186
|
+
begin
|
187
|
+
b_value = b.read if b_value.nil?
|
188
|
+
rescue ChefFS::FileSystem::NotFoundError
|
189
|
+
b_value = :none
|
190
|
+
end
|
191
|
+
are_same = (a_value == b_value)
|
192
|
+
end
|
193
|
+
[ are_same, a_value, b_value ]
|
194
|
+
end
|
195
|
+
|
107
196
|
private
|
108
197
|
|
109
|
-
# Copy two
|
110
|
-
def self.
|
198
|
+
# Copy two entries (could be files or dirs)
|
199
|
+
def self.copy_entries(src_entry, dest_entry, recurse_depth, options)
|
111
200
|
# A NOTE about this algorithm:
|
112
201
|
# There are cases where this algorithm does too many network requests.
|
113
202
|
# knife upload with a specific filename will first check if the file
|
@@ -148,7 +237,7 @@ module ChefFS
|
|
148
237
|
if recurse_depth != 0
|
149
238
|
src_entry.children.each do |src_child|
|
150
239
|
new_dest_child = new_dest_dir.child(src_child.name)
|
151
|
-
|
240
|
+
copy_entries(src_child, new_dest_child, recurse_depth ? recurse_depth - 1 : recurse_depth, options)
|
152
241
|
end
|
153
242
|
end
|
154
243
|
else
|
@@ -163,10 +252,30 @@ module ChefFS
|
|
163
252
|
|
164
253
|
else
|
165
254
|
# Both exist.
|
255
|
+
|
256
|
+
# If the entry can do a copy directly, do that.
|
257
|
+
if dest_entry.respond_to?(:copy_from)
|
258
|
+
if options[:force] || compare(src_entry, dest_entry)[0] == false
|
259
|
+
if options[:dry_run]
|
260
|
+
puts "Would update #{dest_entry.path_for_printing}"
|
261
|
+
else
|
262
|
+
puts "Updating #{dest_entry.path_for_printing} ..."
|
263
|
+
dest_entry.copy_from(src_entry)
|
264
|
+
puts "Updated #{dest_entry.path_for_printing}"
|
265
|
+
end
|
266
|
+
end
|
267
|
+
return
|
268
|
+
end
|
269
|
+
|
166
270
|
# If they are different types, log an error.
|
167
271
|
if src_entry.dir?
|
168
272
|
if dest_entry.dir?
|
169
|
-
# If
|
273
|
+
# If both are directories, recurse into their children
|
274
|
+
if recurse_depth != 0
|
275
|
+
child_pairs(src_entry, dest_entry).each do |src_child, dest_child|
|
276
|
+
copy_entries(src_child, dest_child, recurse_depth ? recurse_depth - 1 : recurse_depth, options)
|
277
|
+
end
|
278
|
+
end
|
170
279
|
else
|
171
280
|
# If they are different types.
|
172
281
|
Chef::Log.error("File #{dest_entry.path_for_printing} is a directory while file #{dest_entry.path_for_printing} is a regular file\n")
|
@@ -180,19 +289,16 @@ module ChefFS
|
|
180
289
|
# Both are files! Copy them unless we're sure they are the same.
|
181
290
|
if options[:force]
|
182
291
|
should_copy = true
|
183
|
-
src_value =
|
292
|
+
src_value = nil
|
184
293
|
else
|
185
|
-
|
186
|
-
|
187
|
-
if should_copy == nil
|
188
|
-
should_copy = true
|
189
|
-
end
|
294
|
+
are_same, src_value, dest_value = compare(src_entry, dest_entry)
|
295
|
+
should_copy = !are_same
|
190
296
|
end
|
191
297
|
if should_copy
|
192
298
|
if options[:dry_run]
|
193
299
|
puts "Would update #{dest_entry.path_for_printing}"
|
194
300
|
else
|
195
|
-
src_value = src_entry.read if src_value
|
301
|
+
src_value = src_entry.read if src_value.nil?
|
196
302
|
dest_entry.write(src_value)
|
197
303
|
puts "Updated #{dest_entry.path_for_printing}"
|
198
304
|
end
|