jfoundry 0.1.0.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. data/LICENSE +746 -0
  2. data/Rakefile +10 -0
  3. data/lib/cc_api_stub/applications.rb +53 -0
  4. data/lib/cc_api_stub/domains.rb +32 -0
  5. data/lib/cc_api_stub/frameworks.rb +22 -0
  6. data/lib/cc_api_stub/helper.rb +139 -0
  7. data/lib/cc_api_stub/login.rb +21 -0
  8. data/lib/cc_api_stub/organization_users.rb +21 -0
  9. data/lib/cc_api_stub/organizations.rb +70 -0
  10. data/lib/cc_api_stub/routes.rb +26 -0
  11. data/lib/cc_api_stub/runtimes.rb +22 -0
  12. data/lib/cc_api_stub/service_bindings.rb +22 -0
  13. data/lib/cc_api_stub/service_instances.rb +22 -0
  14. data/lib/cc_api_stub/services.rb +21 -0
  15. data/lib/cc_api_stub/spaces.rb +49 -0
  16. data/lib/cc_api_stub/users.rb +85 -0
  17. data/lib/cc_api_stub.rb +16 -0
  18. data/lib/jfoundry/auth_token.rb +63 -0
  19. data/lib/jfoundry/baseclient.rb +177 -0
  20. data/lib/jfoundry/chatty_hash.rb +46 -0
  21. data/lib/jfoundry/client.rb +39 -0
  22. data/lib/jfoundry/concerns/proxy_options.rb +17 -0
  23. data/lib/jfoundry/errors.rb +163 -0
  24. data/lib/jfoundry/rest_client.rb +331 -0
  25. data/lib/jfoundry/signature/version.rb +27 -0
  26. data/lib/jfoundry/signer.rb +13 -0
  27. data/lib/jfoundry/test_support.rb +3 -0
  28. data/lib/jfoundry/timer.rb +13 -0
  29. data/lib/jfoundry/trace_helpers.rb +64 -0
  30. data/lib/jfoundry/upload_helpers.rb +222 -0
  31. data/lib/jfoundry/v2/app.rb +357 -0
  32. data/lib/jfoundry/v2/app_event.rb +13 -0
  33. data/lib/jfoundry/v2/base.rb +92 -0
  34. data/lib/jfoundry/v2/client.rb +78 -0
  35. data/lib/jfoundry/v2/domain.rb +20 -0
  36. data/lib/jfoundry/v2/managed_service_instance.rb +13 -0
  37. data/lib/jfoundry/v2/model.rb +209 -0
  38. data/lib/jfoundry/v2/model_magic/attribute.rb +49 -0
  39. data/lib/jfoundry/v2/model_magic/client_extensions.rb +170 -0
  40. data/lib/jfoundry/v2/model_magic/has_summary.rb +49 -0
  41. data/lib/jfoundry/v2/model_magic/queryable_by.rb +39 -0
  42. data/lib/jfoundry/v2/model_magic/to_many.rb +138 -0
  43. data/lib/jfoundry/v2/model_magic/to_one.rb +81 -0
  44. data/lib/jfoundry/v2/model_magic.rb +93 -0
  45. data/lib/jfoundry/v2/organization.rb +22 -0
  46. data/lib/jfoundry/v2/quota_definition.rb +12 -0
  47. data/lib/jfoundry/v2/route.rb +25 -0
  48. data/lib/jfoundry/v2/service.rb +20 -0
  49. data/lib/jfoundry/v2/service_auth_token.rb +10 -0
  50. data/lib/jfoundry/v2/service_binding.rb +10 -0
  51. data/lib/jfoundry/v2/service_broker.rb +11 -0
  52. data/lib/jfoundry/v2/service_instance.rb +13 -0
  53. data/lib/jfoundry/v2/service_plan.rb +13 -0
  54. data/lib/jfoundry/v2/space.rb +18 -0
  55. data/lib/jfoundry/v2/stack.rb +10 -0
  56. data/lib/jfoundry/v2/user.rb +104 -0
  57. data/lib/jfoundry/v2/user_provided_service_instance.rb +7 -0
  58. data/lib/jfoundry/validator.rb +41 -0
  59. data/lib/jfoundry/version.rb +4 -0
  60. data/lib/jfoundry/zip.rb +56 -0
  61. data/lib/jfoundry.rb +5 -0
  62. data/lib/tasks/gem_release.rake +42 -0
  63. data/vendor/errors/README.md +3 -0
  64. data/vendor/errors/v1.yml +189 -0
  65. data/vendor/errors/v2.yml +470 -0
  66. metadata +269 -0
@@ -0,0 +1,331 @@
1
+ require "jfoundry/trace_helpers"
2
+ require "net/https"
3
+ require "net/http/post/multipart"
4
+ require "multi_json"
5
+ require "fileutils"
6
+ require "jfoundry/timer"
7
+ require "jfoundry/signature/version"
8
+
9
+ module JFoundry
10
+ class RestClient
11
+ class HTTPFactory
12
+ def self.create(uri, proxy_options = [])
13
+ http = Net::HTTP.new(uri.host, uri.port, *proxy_options)
14
+
15
+ if uri.is_a?(URI::HTTPS)
16
+ http.use_ssl = true
17
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
18
+ end
19
+
20
+ return http
21
+ end
22
+ end
23
+
24
+ include JFoundry::TraceHelpers
25
+ include JFoundry::ProxyOptions
26
+ include JFoundry::Timer
27
+ include JFoundry::Signature::Version
28
+
29
+ LOG_LENGTH = 10
30
+
31
+ HTTP_METHODS = {
32
+ "GET" => Net::HTTP::Get,
33
+ "PUT" => Net::HTTP::Put,
34
+ "POST" => Net::HTTP::Post,
35
+ "DELETE" => Net::HTTP::Delete,
36
+ "HEAD" => Net::HTTP::Head,
37
+ }
38
+
39
+ DEFAULT_OPTIONS = {
40
+ :follow_redirects => true
41
+ }
42
+
43
+ attr_reader :target
44
+
45
+ attr_accessor :trace, :backtrace, :log,
46
+ :request_id, :access_key, :secret_key, :version, :http_proxy, :https_proxy
47
+
48
+ def initialize(target, access_key, secret_key, version)
49
+ @target = target
50
+ #@token = token
51
+ @access_key = access_key
52
+ @secret_key = secret_key
53
+ @version = version
54
+ @trace = false
55
+ @backtrace = false
56
+ @log = false
57
+ end
58
+
59
+ def target=(target)
60
+ return if target == @target
61
+
62
+ @target = target
63
+ #@token = nil
64
+ end
65
+
66
+ def request(method, path, options = {})
67
+ request_uri(method, construct_url(path), DEFAULT_OPTIONS.merge(options))
68
+ end
69
+
70
+ def generate_headers(uri, payload, options)
71
+ headers = {}
72
+
73
+ if payload.is_a?(String)
74
+ headers["Content-Length"] = payload.size
75
+ elsif !payload
76
+ headers["Content-Length"] = 0
77
+ end
78
+
79
+ headers["X-Request-Id"] = @request_id if @request_id
80
+ #headers["Authorization"] = @token.auth_header if @token
81
+ headers['Version'] = @version
82
+ headers['Date'] = get_time
83
+ headers['ACCESS-KEY'] = @access_key
84
+ headers['Path'] = URI.parse(uri.to_s).path
85
+
86
+ if accept_type = mimetype(options[:accept])
87
+ headers["Accept"] = accept_type
88
+ end
89
+
90
+ if content_type = mimetype(options[:content])
91
+ headers["Content-Type"] = content_type
92
+ end
93
+
94
+ headers.merge!(options[:headers]) if options[:headers]
95
+ headers
96
+ end
97
+
98
+ private
99
+
100
+ def request_uri(method, uri, options = {})
101
+ uri = URI.parse(uri)
102
+
103
+ unless uri.is_a?(URI::HTTP)
104
+ raise InvalidTarget.new(@target)
105
+ end
106
+
107
+ # keep original options in case there's a redirect to follow
108
+ original_options = options.dup
109
+ payload = options[:payload]
110
+
111
+ if options[:params]
112
+ encoded_params = encode_params(options[:params])
113
+ if encoded_params.respond_to?(:empty?) ? !encoded_params.empty? : encoded_params
114
+ if uri.query
115
+ uri.query += "&" + encoded_params
116
+ else
117
+ uri.query = encoded_params
118
+ end
119
+ end
120
+ end
121
+
122
+ unless payload.is_a?(String)
123
+ case options[:content]
124
+ when :json
125
+ payload = MultiJson.dump(payload)
126
+ when :form
127
+ payload = encode_params(payload)
128
+ end
129
+ end
130
+
131
+ method_class = get_method_class(method)
132
+ if payload.is_a?(Hash)
133
+ multipart = method_class.const_get(:Multipart)
134
+ request = multipart.new(uri.request_uri, payload)
135
+ else
136
+ request = method_class.new(uri.request_uri)
137
+ request.body = payload if payload
138
+ end
139
+
140
+ headers = generate_headers(uri, payload, options)
141
+
142
+ request_hash = {
143
+ :url => uri.to_s,
144
+ :method => method,
145
+ :headers => headers,
146
+ :body => payload
147
+ }
148
+
149
+ print_request(request_hash) if @trace
150
+
151
+ add_headers(request, headers)
152
+
153
+ signature = generate_signature(@secret_key, method, request.to_hash())
154
+ request['signature'] = signature
155
+
156
+ http = HTTPFactory.create(uri, proxy_options_for(uri))
157
+
158
+ # TODO remove this when staging returns streaming responses
159
+ http.read_timeout = 300
160
+
161
+ before = Time.now
162
+ http.start do
163
+ response = http.request(request)
164
+ time = Time.now - before
165
+
166
+ response_hash = {
167
+ :headers => sane_headers(response),
168
+ :status => response.code,
169
+ :body => response.body
170
+ }
171
+
172
+ print_response(response_hash) if @trace
173
+ print_backtrace(caller) if @trace
174
+
175
+ log_request(time, request, response)
176
+
177
+ if response.is_a?(Net::HTTPRedirection) && options[:follow_redirects]
178
+ request_uri("GET", response["location"], original_options)
179
+ else
180
+ return request_hash, response_hash
181
+ end
182
+ end
183
+ rescue ::Timeout::Error => e
184
+ raise Timeout.new(method, uri, e)
185
+ rescue SocketError, Errno::ECONNREFUSED => e
186
+ raise TargetRefused, e.message
187
+ rescue URI::InvalidURIError
188
+ raise InvalidTarget.new(@target)
189
+ end
190
+
191
+ def construct_url(path)
192
+ uri = URI.parse(path)
193
+ return path if uri.scheme
194
+
195
+ path = "/#{path}" unless path[0] == ?\/
196
+ target + path
197
+ end
198
+
199
+ def get_method_class(method_string)
200
+ HTTP_METHODS[method_string.upcase]
201
+ end
202
+
203
+ def add_headers(request, headers)
204
+ headers.each { |key, value| request[key] = value }
205
+ end
206
+
207
+ def mimetype(content)
208
+ case content
209
+ when String
210
+ content
211
+ when :json
212
+ "application/json"
213
+ when :form
214
+ "application/x-www-form-urlencoded"
215
+ when nil
216
+ nil
217
+ # return request headers (not really Accept)
218
+ else
219
+ raise JFoundry::Error, "Unknown mimetype '#{content.inspect}'"
220
+ end
221
+ end
222
+
223
+ def encode_params(hash, escape = true)
224
+ hash.keys.map do |k|
225
+ v = hash[k]
226
+ v = MultiJson.dump(v) if v.is_a?(Hash)
227
+ v = URI.escape(v.to_s, /[^#{URI::PATTERN::UNRESERVED}]/) if escape
228
+ "#{k}=#{v}"
229
+ end.join("&")
230
+ end
231
+
232
+ def log_data(time, request, response)
233
+ { :time => time,
234
+ :request => {
235
+ :method => request.method,
236
+ :url => request.path,
237
+ :headers => sane_headers(request)
238
+ },
239
+ :response => {
240
+ :code => response.code,
241
+ :headers => sane_headers(response)
242
+ }
243
+ }
244
+ end
245
+
246
+ def log_line(io, data)
247
+ io.printf(
248
+ "[%s] %0.3fs %6s -> %d %s\n",
249
+ Time.now.strftime("%F %T"),
250
+ data[:time],
251
+ data[:request][:method].to_s.upcase,
252
+ data[:response][:code],
253
+ data[:request][:url])
254
+ end
255
+
256
+ def log_request(time, request, response)
257
+ return unless @log
258
+
259
+ data = log_data(time, request, response)
260
+
261
+ case @log
262
+ when IO
263
+ log_line(@log, data)
264
+ return
265
+ when String
266
+ if File.exists?(@log)
267
+ log = File.readlines(@log).last(LOG_LENGTH - 1)
268
+ elsif !File.exists?(File.dirname(@log))
269
+ FileUtils.mkdir_p(File.dirname(@log))
270
+ end
271
+
272
+ File.open(@log, "w") do |io|
273
+ log.each { |l| io.print l } if log
274
+ log_line(io, data)
275
+ end
276
+
277
+ return
278
+ end
279
+
280
+ if @log.respond_to?(:call)
281
+ @log.call(data)
282
+ return
283
+ end
284
+
285
+ if @log.respond_to?(:<<)
286
+ @log << data
287
+ return
288
+ end
289
+ end
290
+
291
+ def print_request(request)
292
+ $stderr.puts ">>>"
293
+ $stderr.puts request_trace(request)
294
+ end
295
+
296
+ def print_response(response)
297
+ $stderr.puts response_trace(response)
298
+ $stderr.puts "<<<"
299
+ end
300
+
301
+ def print_backtrace(locs)
302
+ return unless @backtrace
303
+
304
+ interesting_locs = locs.drop_while { |loc|
305
+ loc =~ /\/(jfoundry\/|restclient\/|net\/http)/
306
+ }
307
+
308
+ $stderr.puts "--- backtrace:"
309
+
310
+ $stderr.puts "... (boring)" unless locs == interesting_locs
311
+
312
+ trimmed_locs = interesting_locs[0..5]
313
+
314
+ trimmed_locs.each do |loc|
315
+ $stderr.puts "=== #{loc}"
316
+ end
317
+
318
+ $stderr.puts "... (trimmed)" unless trimmed_locs == interesting_locs
319
+ end
320
+
321
+ def sane_headers(obj)
322
+ hds = {}
323
+
324
+ obj.each_header do |k, v|
325
+ hds[k] = v
326
+ end
327
+
328
+ hds
329
+ end
330
+ end
331
+ end
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/ruby
2
+ #encoding=utf-8
3
+
4
+ require 'jfoundry/signer'
5
+
6
+ module JFoundry
7
+ module Signature
8
+ module Version
9
+
10
+ include JFoundry::Signer
11
+
12
+ def generate_signature secret_key, method, req
13
+ return sign(secret_key, string_to_sign(method, req))
14
+ end
15
+
16
+ def string_to_sign method, req
17
+ str = [
18
+ method.downcase,
19
+ req['content-md5'],
20
+ req['content-type'],
21
+ req['date'],
22
+ req['path']
23
+ ].join("\n")
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/ruby
2
+ #encoding=utf-8
3
+
4
+ require 'base64'
5
+ require 'openssl'
6
+
7
+ module JFoundry
8
+ module Signer
9
+ def sign secret_key, string_to_sign, digest = 'sha1'
10
+ return Base64.encode64(OpenSSL::HMAC.digest(digest, secret_key, string_to_sign)).strip
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ Dir[File.expand_path('../../../spec/{support}/**/*.rb', __FILE__)].each do |file|
2
+ require file unless file =~ /factory_girl/
3
+ end
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/ruby
2
+ #encoding=utf-8
3
+
4
+ require "time"
5
+
6
+ module JFoundry
7
+ module Timer
8
+ def get_time
9
+ return Time.now.utc.strftime("%Y%m%dT%H%M%SZ")
10
+ end
11
+ end
12
+ end
13
+
@@ -0,0 +1,64 @@
1
+ require "net/https"
2
+ require "multi_json"
3
+
4
+ module JFoundry
5
+ module TraceHelpers
6
+ PROTECTED_ATTRIBUTES = ['Authorization', 'credentials']
7
+
8
+ def request_trace(request)
9
+ return nil unless request
10
+ info = ["REQUEST: #{request[:method]} #{request[:url]}"]
11
+ info << "REQUEST_HEADERS:"
12
+ info << header_trace(request[:headers])
13
+ info << "REQUEST_BODY: #{request[:body]}" if request[:body]
14
+ info.join("\n")
15
+ end
16
+
17
+
18
+ def response_trace(response)
19
+ return nil unless response
20
+ info = ["RESPONSE: [#{response[:status]}]"]
21
+ info << "RESPONSE_HEADERS:"
22
+ info << header_trace(response[:headers])
23
+ info << "RESPONSE_BODY:"
24
+ begin
25
+ parsed_body = MultiJson.load(response[:body])
26
+ filter_protected_attributes(parsed_body)
27
+ info << MultiJson.dump(parsed_body, :pretty => true)
28
+ rescue
29
+ info << "#{response[:body]}"
30
+ end
31
+ info.join("\n")
32
+ end
33
+
34
+ private
35
+
36
+ def header_trace(headers)
37
+ headers.sort.map do |key, value|
38
+ unless PROTECTED_ATTRIBUTES.include?(key)
39
+ " #{key} : #{value}"
40
+ else
41
+ " #{key} : [PRIVATE DATA HIDDEN]"
42
+ end
43
+ end
44
+ end
45
+
46
+ def filter_protected_attributes(hash_or_array)
47
+ if hash_or_array.is_a? Array
48
+ hash_or_array.each do |value|
49
+ filter_protected_attributes(value)
50
+ end
51
+ else
52
+ hash_or_array.each do |key, value|
53
+ if PROTECTED_ATTRIBUTES.include? key
54
+ hash_or_array[key] = "[PRIVATE DATA HIDDEN]"
55
+ else
56
+ if value.is_a?(Hash) || value.is_a?(Array)
57
+ filter_protected_attributes(value)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,222 @@
1
+ require "tmpdir"
2
+ require "fileutils"
3
+ require "pathname"
4
+ require "digest/sha1"
5
+
6
+ require "jfoundry/zip"
7
+
8
+ module JFoundry
9
+ module UploadHelpers
10
+ # Default paths to exclude from upload payload.
11
+ UPLOAD_EXCLUDE = %w{.git _darcs .svn}
12
+
13
+ # Minimum size for an application payload to bother checking resources.
14
+ RESOURCE_CHECK_LIMIT = 64 * 1024
15
+
16
+ # Upload application's code to target. Do this after #create! and before
17
+ # #start!
18
+ #
19
+ # [path]
20
+ # A path pointing to either a directory, or a .jar, .war, or .zip
21
+ # file.
22
+ #
23
+ # If a .jfignore file is detected under the given path, it will be used
24
+ # to exclude paths from the payload, similar to a .gitignore.
25
+ #
26
+ # [check_resources]
27
+ # If set to `false`, the entire payload will be uploaded
28
+ # without checking the resource cache.
29
+ #
30
+ # Only do this if you know what you're doing.
31
+ def upload(path, check_resources = true)
32
+ unless File.exist? path
33
+ raise JFoundry::Error, "Invalid application path '#{path}'"
34
+ end
35
+
36
+ zipfile = "#{Dir.tmpdir}/#{@guid}.zip"
37
+ tmpdir = "#{Dir.tmpdir}/.jf_#{@guid}_files"
38
+
39
+ FileUtils.rm_f(zipfile)
40
+ FileUtils.rm_rf(tmpdir)
41
+
42
+ prepare_package(path, tmpdir)
43
+
44
+ resources = determine_resources(tmpdir) if check_resources
45
+
46
+ packed = JFoundry::Zip.pack(tmpdir, zipfile)
47
+
48
+ @client.base.upload_app(@guid, packed && zipfile, resources || [])
49
+ ensure
50
+ FileUtils.rm_f(zipfile) if zipfile
51
+ FileUtils.rm_rf(tmpdir) if tmpdir
52
+ end
53
+
54
+ private
55
+
56
+ def prepare_package(path, to)
57
+ if path =~ /\.(jar|war|zip)$/
58
+ JFoundry::Zip.unpack(path, to)
59
+ elsif (war_file = Dir.glob("#{path}/*.war").first)
60
+ JFoundry::Zip.unpack(war_file, to)
61
+ else
62
+ FileUtils.mkdir(to)
63
+
64
+ exclude = UPLOAD_EXCLUDE
65
+ if File.exists?("#{path}/.jfignore")
66
+ exclude += File.read("#{path}/.jfignore").split(/\n+/)
67
+ end
68
+
69
+ files = files_to_consider(path, exclude)
70
+
71
+ check_unreachable_links(files, path)
72
+
73
+ copy_tree(files, path, to)
74
+
75
+ find_sockets(to).each do |s|
76
+ File.delete s
77
+ end
78
+ end
79
+ end
80
+
81
+ def check_unreachable_links(files, path)
82
+ # only used for friendlier error message
83
+ pwd = Pathname.pwd
84
+
85
+ abspath = File.expand_path(path)
86
+ unreachable = []
87
+ files.each do |f|
88
+ file = Pathname.new(f)
89
+ if file.symlink? && !file.realpath.to_s.start_with?(abspath)
90
+ unreachable << file.relative_path_from(pwd)
91
+ end
92
+ end
93
+
94
+ unless unreachable.empty?
95
+ root = Pathname.new(path).relative_path_from(pwd)
96
+ raise JFoundry::Error,
97
+ "Path contains links '#{unreachable}' that point outside '#{root}'"
98
+ end
99
+ end
100
+
101
+ def files_to_consider(path, exclusions)
102
+ entries = all_files(path)
103
+
104
+ exclusions.each do |exclusion|
105
+ ignore_pattern = exclusion.start_with?("/") ? File.join(path, exclusion) : File.join(path, "**", exclusion)
106
+ dir_glob = Dir.glob(ignore_pattern, File::FNM_DOTMATCH)
107
+ entries -= dir_glob
108
+
109
+ ignore_pattern = File.join(path, "**", exclusion, "**", "*")
110
+ dir_glob = Dir.glob(ignore_pattern, File::FNM_DOTMATCH)
111
+ entries -= dir_glob
112
+ end
113
+
114
+ entries
115
+ end
116
+
117
+ def glob_matches?(file, path, pattern)
118
+ name = file.sub("#{path}/", "/")
119
+ flags = File::FNM_DOTMATCH
120
+
121
+ # when pattern ends with /, match only directories
122
+ if pattern.end_with?("/") && File.directory?(file)
123
+ name = "#{name}/"
124
+ end
125
+
126
+ case pattern
127
+ # when pattern contains /, do a pathname match
128
+ when /\/./
129
+ flags |= File::FNM_PATHNAME
130
+
131
+ # otherwise, match any file path
132
+ else
133
+ pattern = "**/#{pattern}"
134
+ end
135
+
136
+ File.fnmatch(pattern, name, flags)
137
+ end
138
+
139
+ def find_sockets(path)
140
+ all_files(path).select { |f| File.socket? f }
141
+ end
142
+
143
+ def determine_resources(path)
144
+ fingerprints, total_size = make_fingerprints(path)
145
+
146
+ return if total_size <= RESOURCE_CHECK_LIMIT
147
+
148
+ resources = @client.base.resource_match(fingerprints)
149
+
150
+ resources.each do |resource|
151
+ FileUtils.rm_f resource[:fn]
152
+ resource[:fn].sub!("#{path}/", "")
153
+ end
154
+
155
+ prune_empty_directories(path)
156
+
157
+ resources
158
+ end
159
+
160
+ # OK, HERES THE PLAN...
161
+ #
162
+ # 1. Get all the directories in the entire file tree.
163
+ # 2. Sort them by the length of their absolute path.
164
+ # 3. Go through the list, longest paths first, and remove
165
+ # the directories that are empty.
166
+ #
167
+ # This ensures that directories containing empty directories
168
+ # are also pruned.
169
+ def prune_empty_directories(path)
170
+ all_files = all_files(path)
171
+
172
+ directories = all_files.select { |x| File.directory?(x) }
173
+ directories.sort! { |a, b| b.size <=> a.size }
174
+
175
+ directories.each do |directory|
176
+ entries = all_files(directory)
177
+ FileUtils.rmdir(directory) if entries.empty?
178
+ end
179
+ end
180
+
181
+ def make_fingerprints(path)
182
+ fingerprints = []
183
+ total_size = 0
184
+
185
+ all_files(path).each do |filename|
186
+ next if File.directory?(filename)
187
+
188
+ size = File.size(filename)
189
+
190
+ total_size += size
191
+
192
+ fingerprints << {
193
+ :size => size,
194
+ :sha1 => Digest::SHA1.file(filename).hexdigest,
195
+ :fn => filename
196
+ }
197
+ end
198
+
199
+ [fingerprints, total_size]
200
+ end
201
+
202
+ def all_files(path)
203
+ Dir.glob("#{path}/**/*", File::FNM_DOTMATCH).reject do |fn|
204
+ fn =~ /\.$/
205
+ end
206
+ end
207
+
208
+ def copy_tree(files, path, to)
209
+ files.each do |file|
210
+ dest = file.sub("#{path}/", "#{to}/")
211
+
212
+ if File.directory?(file)
213
+ FileUtils.mkdir_p(dest)
214
+ else
215
+ destdir = File.dirname(dest)
216
+ FileUtils.mkdir_p(destdir)
217
+ FileUtils.cp(file, dest)
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end