jfoundry 0.1.0.pre

Sign up to get free protection for your applications and to get access to all the features.
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