kpm 0.7.2 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +2 -0
  3. data/.rubocop.yml +138 -0
  4. data/Gemfile +2 -0
  5. data/README.adoc +144 -107
  6. data/Rakefile +2 -1
  7. data/bin/kpm +4 -2
  8. data/kpm.gemspec +11 -8
  9. data/lib/kpm.rb +3 -0
  10. data/lib/kpm/account.rb +268 -338
  11. data/lib/kpm/base_artifact.rb +33 -39
  12. data/lib/kpm/base_installer.rb +69 -83
  13. data/lib/kpm/blob.rb +29 -0
  14. data/lib/kpm/cli.rb +3 -1
  15. data/lib/kpm/coordinates.rb +10 -12
  16. data/lib/kpm/database.rb +94 -113
  17. data/lib/kpm/diagnostic_file.rb +126 -147
  18. data/lib/kpm/formatter.rb +76 -48
  19. data/lib/kpm/inspector.rb +24 -34
  20. data/lib/kpm/installer.rb +53 -46
  21. data/lib/kpm/kaui_artifact.rb +4 -3
  22. data/lib/kpm/killbill_plugin_artifact.rb +10 -7
  23. data/lib/kpm/killbill_server_artifact.rb +13 -12
  24. data/lib/kpm/migrations.rb +26 -11
  25. data/lib/kpm/nexus_helper/actions.rb +52 -9
  26. data/lib/kpm/nexus_helper/cloudsmith_api_calls.rb +83 -0
  27. data/lib/kpm/nexus_helper/github_api_calls.rb +70 -0
  28. data/lib/kpm/nexus_helper/nexus_api_calls_v2.rb +130 -108
  29. data/lib/kpm/nexus_helper/nexus_facade.rb +5 -3
  30. data/lib/kpm/plugins_directory.rb +9 -8
  31. data/lib/kpm/plugins_directory.yml +14 -173
  32. data/lib/kpm/plugins_manager.rb +29 -24
  33. data/lib/kpm/sha1_checker.rb +31 -18
  34. data/lib/kpm/system.rb +104 -135
  35. data/lib/kpm/system_helpers/cpu_information.rb +56 -55
  36. data/lib/kpm/system_helpers/disk_space_information.rb +60 -63
  37. data/lib/kpm/system_helpers/entropy_available.rb +37 -39
  38. data/lib/kpm/system_helpers/memory_information.rb +52 -51
  39. data/lib/kpm/system_helpers/os_information.rb +45 -47
  40. data/lib/kpm/system_helpers/system_proxy.rb +10 -10
  41. data/lib/kpm/tasks.rb +381 -438
  42. data/lib/kpm/tenant_config.rb +68 -83
  43. data/lib/kpm/tomcat_manager.rb +10 -8
  44. data/lib/kpm/trace_logger.rb +18 -16
  45. data/lib/kpm/uninstaller.rb +81 -14
  46. data/lib/kpm/utils.rb +13 -14
  47. data/lib/kpm/version.rb +3 -1
  48. data/packaging/Gemfile +2 -0
  49. data/pom.xml +211 -40
  50. data/spec/kpm/remote/base_artifact_spec.rb +20 -20
  51. data/spec/kpm/remote/base_installer_spec.rb +35 -34
  52. data/spec/kpm/remote/cloudsmith_api_calls_spec.rb +40 -0
  53. data/spec/kpm/remote/github_api_calls_spec.rb +40 -0
  54. data/spec/kpm/remote/installer_spec.rb +80 -79
  55. data/spec/kpm/remote/kaui_artifact_spec.rb +7 -6
  56. data/spec/kpm/remote/killbill_plugin_artifact_spec.rb +25 -30
  57. data/spec/kpm/remote/killbill_server_artifact_spec.rb +17 -16
  58. data/spec/kpm/remote/migrations_spec.rb +12 -11
  59. data/spec/kpm/remote/nexus_facade_spec.rb +32 -28
  60. data/spec/kpm/remote/tenant_config_spec.rb +30 -29
  61. data/spec/kpm/remote/tomcat_manager_spec.rb +4 -3
  62. data/spec/kpm/unit/actions_spec.rb +52 -0
  63. data/spec/kpm/unit/base_artifact_spec.rb +19 -18
  64. data/spec/kpm/unit/cpu_information_spec.rb +67 -0
  65. data/spec/kpm/unit/disk_space_information_spec.rb +47 -0
  66. data/spec/kpm/unit/entropy_information_spec.rb +36 -0
  67. data/spec/kpm/unit/formatter_spec.rb +163 -0
  68. data/spec/kpm/unit/inspector_spec.rb +34 -42
  69. data/spec/kpm/unit/installer_spec.rb +7 -6
  70. data/spec/kpm/unit/memory_information_spec.rb +102 -0
  71. data/spec/kpm/unit/os_information_spec.rb +38 -0
  72. data/spec/kpm/unit/plugins_directory_spec.rb +38 -22
  73. data/spec/kpm/unit/plugins_manager_spec.rb +62 -66
  74. data/spec/kpm/unit/sha1_checker_spec.rb +107 -60
  75. data/spec/kpm/unit/uninstaller_spec.rb +118 -72
  76. data/spec/kpm/unit_mysql/account_spec.rb +127 -142
  77. data/spec/spec_helper.rb +20 -18
  78. data/tasks/package.rake +18 -18
  79. metadata +42 -22
@@ -1,14 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'tmpdir'
2
4
  require 'json'
3
5
  require 'killbill_client'
4
6
 
5
7
  module KPM
6
-
7
8
  class TenantConfig
8
9
  # Killbill server
9
10
  KILLBILL_HOST = ENV['KILLBILL_HOST'] || '127.0.0.1'
10
- KILLBILL_URL = 'http://'.concat(KILLBILL_HOST).concat(':8080')
11
- KILLBILL_API_VERSION = '1.0'
11
+ KILLBILL_URL = "http://#{KILLBILL_HOST}:8080"
12
12
 
13
13
  # USER/PWD
14
14
  KILLBILL_USER = ENV['KILLBILL_USER'] || 'admin'
@@ -20,112 +20,97 @@ module KPM
20
20
 
21
21
  # Temporary directory
22
22
  TMP_DIR_PEFIX = 'killbill'
23
- TMP_DIR = Dir.mktmpdir(TMP_DIR_PEFIX);
24
-
25
- #Tenant key prefixes
26
- KEY_PREFIXES = ['PLUGIN_CONFIG','PUSH_NOTIFICATION_CB','PER_TENANT_CONFIG',
27
- 'PLUGIN_PAYMENT_STATE_MACHINE','CATALOG','OVERDUE_CONFIG',
28
- 'INVOICE_TRANSLATION','CATALOG_TRANSLATION','INVOICE_TEMPLATE','INVOICE_MP_TEMPLATE']
29
-
30
-
23
+ TMP_DIR = Dir.mktmpdir(TMP_DIR_PEFIX)
24
+
25
+ # Tenant key prefixes
26
+ KEY_PREFIXES = %w[PLUGIN_CONFIG PUSH_NOTIFICATION_CB PER_TENANT_CONFIG
27
+ PLUGIN_PAYMENT_STATE_MACHINE CATALOG OVERDUE_CONFIG
28
+ INVOICE_TRANSLATION CATALOG_TRANSLATION INVOICE_TEMPLATE INVOICE_MP_TEMPLATE].freeze
29
+
31
30
  def initialize(killbill_api_credentials = nil, killbill_credentials = nil, killbill_url = nil, logger = nil)
32
31
  @killbill_api_key = KILLBILL_API_KEY
33
- @killbill_api_secrets = KILLBILL_API_SECRET
32
+ @killbill_api_secret = KILLBILL_API_SECRET
34
33
  @killbill_url = KILLBILL_URL
35
34
  @killbill_user = KILLBILL_USER
36
35
  @killbill_password = KILLBILL_PASSWORD
37
36
  @logger = logger
38
37
 
39
- set_killbill_options(killbill_api_credentials,killbill_credentials,killbill_url)
40
-
38
+ set_killbill_options(killbill_api_credentials, killbill_credentials, killbill_url)
41
39
  end
42
-
43
- def export(key_prefix = nil)
44
40
 
41
+ def export(key_prefix = nil)
45
42
  export_data = fetch_export_data(key_prefix)
46
-
47
- if export_data.size == 0
48
- raise Interrupt, 'key_prefix not found'
49
- end
50
-
43
+
44
+ raise ArgumentError, "Data for key_prefix=#{key_prefix} not found" if export_data.empty?
45
+
51
46
  export_file = store_into_file(export_data)
52
47
 
53
- if not File.exist?(export_file)
54
- raise Interrupt, 'key_prefix not found'
55
- else
56
- @logger.info "\e[32mData exported under #{export_file}\e[0m"
57
- end
48
+ @logger.info "\e[32mData exported under #{export_file}\e[0m"
58
49
 
59
50
  export_file
60
51
  end
61
-
52
+
62
53
  private
63
-
64
- def fetch_export_data(key_prefix)
65
- tenant_config = []
66
- pefixes = key_prefix.nil? ? KEY_PREFIXES : [key_prefix]
67
-
68
- pefixes.each do |prefix|
69
-
70
- config_data = call_client(prefix)
71
-
72
- if config_data.size > 0
73
- config_data.each {|data| tenant_config << data }
74
- @logger.info "Data for key prefix \e[1m#{prefix.to_s}\e[0m was \e[1mfound and is ready to be exported\e[0m."
75
- else
76
- @logger.info "Data for key prefix \e[1m#{prefix.to_s}\e[0m was \e[31mnot found\e[0m."
77
- end
78
- end
79
-
80
- tenant_config
81
- end
82
-
83
- def call_client(key_prefix)
84
-
85
- KillBillClient.url = @killbill_url
86
- options = {
87
- :username => @killbill_user,
88
- :password => @killbill_password,
89
- :api_key => @killbill_api_key,
90
- :api_secret => @killbill_api_secrets
91
- }
92
-
93
- tenant_config_data = KillBillClient::Model::Tenant.search_tenant_config(key_prefix, options)
94
-
95
- tenant_config_data
96
- end
97
-
98
- def store_into_file(export_data)
99
- export_file = TMP_DIR + File::SEPARATOR + 'kbdump'
100
54
 
101
- File.open(export_file, 'w') { |io| io.puts export_data.to_json }
55
+ def fetch_export_data(key_prefix)
56
+ tenant_config = []
57
+ pefixes = key_prefix.nil? ? KEY_PREFIXES : [key_prefix]
102
58
 
103
- export_file
59
+ pefixes.each do |prefix|
60
+ config_data = call_client(prefix)
61
+
62
+ if !config_data.empty?
63
+ config_data.each { |data| tenant_config << data }
64
+ @logger.debug "Data for key prefix \e[1m#{prefix}\e[0m was \e[1mfound and is ready to be exported\e[0m."
65
+ else
66
+ @logger.debug "Data for key prefix \e[1m#{prefix}\e[0m was \e[31mnot found\e[0m."
67
+ end
104
68
  end
105
-
106
- def set_killbill_options(killbill_api_credentials, killbill_credentials, killbill_url)
107
69
 
108
- if not killbill_api_credentials.nil?
70
+ tenant_config
71
+ end
109
72
 
110
- @killbill_api_key = killbill_api_credentials[0]
111
- @killbill_api_secrets = killbill_api_credentials[1]
73
+ def call_client(key_prefix)
74
+ KillBillClient.url = @killbill_url
75
+ KillBillClient.logger = @logger
76
+ options = {
77
+ username: @killbill_user,
78
+ password: @killbill_password,
79
+ api_key: @killbill_api_key,
80
+ api_secret: @killbill_api_secret
81
+ }
82
+
83
+ begin
84
+ KillBillClient::Model::Tenant.search_tenant_config(key_prefix, options)
85
+ rescue KillBillClient::API::Unauthorized
86
+ raise ArgumentError, "Unable to export tenant details, wrong credentials? username=#{@killbill_user}, password=#{mask(@killbill_password)}, api_key=#{@killbill_api_key}, api_secret=#{mask(@killbill_api_secret)}"
87
+ end
88
+ end
112
89
 
113
- end
90
+ def store_into_file(export_data)
91
+ export_file = TMP_DIR + File::SEPARATOR + 'kbdump'
114
92
 
115
- if not killbill_credentials.nil?
93
+ File.open(export_file, 'w') { |io| io.puts export_data.to_json }
116
94
 
117
- @killbill_user = killbill_credentials[0]
118
- @killbill_password = killbill_credentials[1]
95
+ export_file
96
+ end
119
97
 
120
- end
98
+ def set_killbill_options(killbill_api_credentials, killbill_credentials, killbill_url)
99
+ unless killbill_api_credentials.nil?
100
+ @killbill_api_key = killbill_api_credentials[0]
101
+ @killbill_api_secret = killbill_api_credentials[1]
102
+ end
121
103
 
122
- if not killbill_url.nil?
104
+ unless killbill_credentials.nil?
105
+ @killbill_user = killbill_credentials[0]
106
+ @killbill_password = killbill_credentials[1]
107
+ end
123
108
 
124
- @killbill_url = killbill_url
109
+ @killbill_url = killbill_url unless killbill_url.nil?
110
+ end
125
111
 
126
- end
127
- end
128
-
112
+ def mask(string, all_but = 3, char = '*')
113
+ string.gsub(/.(?=.{#{all_but}})/, char)
114
+ end
129
115
  end
130
-
131
- end
116
+ end
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
1
4
  require 'net/http'
2
5
  require 'uri'
3
6
 
4
7
  module KPM
5
8
  class TomcatManager
6
-
7
9
  DOWNLOAD_URL = 'https://s3.amazonaws.com/kb-binaries/apache-tomcat-7.0.42.tar.gz'
8
10
 
9
11
  def initialize(tomcat_dir, logger)
@@ -19,10 +21,10 @@ module KPM
19
21
  file = Pathname.new(dir).join('tomcat.tar.gz')
20
22
 
21
23
  @logger.info "Starting download of #{DOWNLOAD_URL} to #{file}"
22
- Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |http|
23
- File.open(file, 'wb+') do |file|
24
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
25
+ File.open(file, 'wb+') do |f|
24
26
  http.get(uri.path) do |body|
25
- file.write(body)
27
+ f.write(body)
26
28
  end
27
29
  end
28
30
  end
@@ -36,7 +38,7 @@ module KPM
36
38
 
37
39
  def setup
38
40
  # Remove default webapps
39
- %w(ROOT docs examples host-manager manager).each do |webapp|
41
+ %w[ROOT docs examples host-manager manager].each do |webapp|
40
42
  FileUtils.rm_rf(@tomcat_dir.join('webapps').join(webapp))
41
43
  end
42
44
 
@@ -55,9 +57,9 @@ module KPM
55
57
 
56
58
  def help
57
59
  "Tomcat installed at #{@tomcat_dir}
58
- Start script: #{@tomcat_dir.join('bin').join('startup.sh').to_s}
59
- Stop script: #{@tomcat_dir.join('bin').join('shutdown.sh').to_s}
60
- Logs: #{@tomcat_dir.join('logs').to_s}"
60
+ Start script: #{@tomcat_dir.join('bin').join('startup.sh')}
61
+ Stop script: #{@tomcat_dir.join('bin').join('shutdown.sh')}
62
+ Logs: #{@tomcat_dir.join('logs')}"
61
63
  end
62
64
 
63
65
  private
@@ -1,13 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
 
3
5
  module KPM
4
6
  class TraceLogger
5
7
  def initialize
6
- @trace = Hash.new
8
+ @trace = {}
7
9
  end
8
10
 
9
11
  # Return JSON representation of the logs
10
- def to_json
12
+ def to_json(*_args)
11
13
  @trace.to_json
12
14
  end
13
15
 
@@ -21,32 +23,32 @@ module KPM
21
23
  @trace
22
24
  end
23
25
 
24
- def add(group=nil, key, message)
25
- add_to_hash(group,key,message);
26
+ def add(group, key, message)
27
+ add_to_hash(group, key, message)
26
28
  end
27
29
 
28
30
  private
29
- # This procedures will store the logs into a hash to be later returned
30
- def add_to_hash(group=nil, key, message)
31
31
 
32
+ # This procedures will store the logs into a hash to be later returned
33
+ def add_to_hash(group, key, message)
32
34
  if group.nil? || key.nil?
33
35
  add_with_key(group || key, message)
34
36
  else
35
37
  container_key = group.to_sym
36
38
 
37
- @trace[container_key] ||= Hash.new
39
+ @trace[container_key] ||= {}
38
40
  child_key = key.to_sym
39
41
 
40
- unless @trace[container_key][child_key].nil?
41
- child_is_an_array = @trace[container_key][child_key].kind_of?(Array)
42
+ if @trace[container_key][child_key].nil?
43
+ @trace[container_key][child_key] = message
44
+ else
45
+ child_is_an_array = @trace[container_key][child_key].is_a?(Array)
42
46
 
43
47
  old_message = nil
44
48
  old_message = @trace[container_key][child_key] unless child_is_an_array
45
49
  @trace[container_key][child_key] = [] unless child_is_an_array
46
50
  @trace[container_key][child_key].push(old_message) unless old_message.nil?
47
51
  @trace[container_key][child_key].push(message)
48
- else
49
- @trace[container_key][child_key] = message
50
52
  end
51
53
  end
52
54
  end
@@ -54,17 +56,17 @@ module KPM
54
56
  def add_with_key(key, message)
55
57
  child_key = key.to_sym
56
58
 
57
- unless @trace[child_key].nil?
58
- child_is_an_array = @trace[child_key].kind_of?(Array)
59
+ if @trace[child_key].nil?
60
+ @trace[child_key] = message
61
+ else
62
+ child_is_an_array = @trace[child_key].is_a?(Array)
59
63
 
60
64
  old_message = nil
61
65
  old_message = @trace[child_key] unless child_is_an_array
62
66
  @trace[child_key] = [] unless child_is_an_array
63
67
  @trace[child_key].push(old_message) unless old_message.nil?
64
68
  @trace[child_key].push(message)
65
- else
66
- @trace[child_key] = message
67
69
  end
68
70
  end
69
71
  end
70
- end
72
+ end
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
1
5
  module KPM
2
6
  class Uninstaller
3
7
  def initialize(destination, logger = nil)
@@ -7,21 +11,45 @@ module KPM
7
11
  @logger.level = Logger::INFO
8
12
  end
9
13
 
10
- destination ||= KPM::BaseInstaller::DEFAULT_BUNDLES_DIR
11
- @installed_plugins = Inspector.new.inspect(destination)
14
+ @destination = (destination || KPM::BaseInstaller::DEFAULT_BUNDLES_DIR)
15
+ refresh_installed_plugins
12
16
 
13
- plugins_installation_path = File.join(destination, 'plugins')
17
+ plugins_installation_path = File.join(@destination, 'plugins')
14
18
  @plugins_manager = PluginsManager.new(plugins_installation_path, @logger)
15
19
 
16
- sha1_file_path = File.join(destination, KPM::BaseInstaller::SHA1_FILENAME)
20
+ sha1_file_path = File.join(@destination, KPM::BaseInstaller::SHA1_FILENAME)
17
21
  @sha1checker = KPM::Sha1Checker.from_file(sha1_file_path, @logger)
18
22
  end
19
23
 
20
- def uninstall_plugin(plugin, force = false)
24
+ def uninstall_plugin(plugin, force = false, version = nil)
21
25
  plugin_info = find_plugin(plugin)
22
26
  raise "No plugin with key/name '#{plugin}' found installed. Try running 'kpm inspect' for more info" unless plugin_info
23
27
 
24
- remove_all_plugin_versions(plugin_info, force)
28
+ versions = version.nil? ? plugin_info[:versions].map { |artifact| artifact[:version] } : [version]
29
+ remove_plugin_versions(plugin_info, force, versions)
30
+ end
31
+
32
+ def uninstall_non_default_plugins(dry_run = false)
33
+ plugins = categorize_plugins
34
+
35
+ if plugins[:to_be_deleted].empty?
36
+ KPM.ui.say 'Nothing to do'
37
+ return false
38
+ end
39
+
40
+ if dry_run
41
+ msg = "The following plugin versions would be removed:\n"
42
+ msg += plugins[:to_be_deleted].map { |p| " #{p[0][:plugin_name]}: #{p[1]}" }.join("\n")
43
+ msg += "\nThe following plugin versions would be kept:\n"
44
+ msg += plugins[:to_keep].map { |p| " #{p[0][:plugin_name]}: #{p[1]}" }.join("\n")
45
+ KPM.ui.say msg
46
+ false
47
+ else
48
+ plugins[:to_be_deleted].each do |p|
49
+ remove_plugin_version(p[0], p[1])
50
+ end
51
+ true
52
+ end
25
53
  end
26
54
 
27
55
  private
@@ -40,23 +68,48 @@ module KPM
40
68
  plugin_info
41
69
  end
42
70
 
43
- def remove_all_plugin_versions(plugin_info, force = false)
44
- versions = plugin_info[:versions].map { |artifact| artifact[:version] }
71
+ def categorize_plugins
72
+ plugins = { to_be_deleted: [], to_keep: [] }
73
+ @installed_plugins.each do |_, info|
74
+ info[:versions].each do |artifact|
75
+ (artifact[:is_default] ? plugins[:to_keep] : plugins[:to_be_deleted]) << [info, artifact[:version]]
76
+ end
77
+ end
78
+ plugins
79
+ end
80
+
81
+ def remove_plugin_versions(plugin_info, force = false, versions = [])
45
82
  KPM.ui.say "Removing the following versions of the #{plugin_info[:plugin_name]} plugin: #{versions.join(', ')}"
46
83
  if !force && versions.length > 1
47
- return false unless 'y' == KPM.ui.ask('Are you sure you want to continue?', limited_to: %w(y n))
84
+ return false unless KPM.ui.ask('Are you sure you want to continue?', limited_to: %w[y n]) == 'y'
48
85
  end
49
86
 
50
- FileUtils.rmtree(plugin_info[:plugin_path])
51
-
52
- @plugins_manager.remove_plugin_identifier_key(plugin_info[:plugin_key])
53
87
  versions.each do |version|
54
- remove_sha1_entry(plugin_info, version)
88
+ remove_plugin_version(plugin_info, version)
55
89
  end
56
-
57
90
  true
58
91
  end
59
92
 
93
+ def remove_plugin_version(plugin_info, version)
94
+ # Be safe
95
+ raise ArgumentError, 'plugin_path is empty' if plugin_info[:plugin_path].empty?
96
+ raise ArgumentError, "version is empty (plugin_path=#{plugin_info[:plugin_path]})" if version.empty?
97
+
98
+ plugin_version_path = File.expand_path(File.join(plugin_info[:plugin_path], version))
99
+ safe_rmrf(plugin_version_path)
100
+
101
+ remove_sha1_entry(plugin_info, version)
102
+
103
+ # Remove the identifier if this was the last version installed
104
+ refresh_installed_plugins
105
+ if @installed_plugins[plugin_info[:plugin_name]][:versions].empty?
106
+ safe_rmrf(plugin_info[:plugin_path])
107
+ @plugins_manager.remove_plugin_identifier_key(plugin_info[:plugin_key])
108
+ end
109
+
110
+ refresh_installed_plugins
111
+ end
112
+
60
113
  def remove_sha1_entry(plugin_info, version)
61
114
  coordinates = KPM::Coordinates.build_coordinates(group_id: plugin_info[:group_id],
62
115
  artifact_id: plugin_info[:artifact_id],
@@ -65,5 +118,19 @@ module KPM
65
118
  version: version)
66
119
  @sha1checker.remove_entry!(coordinates)
67
120
  end
121
+
122
+ def refresh_installed_plugins
123
+ @installed_plugins = Inspector.new.inspect(@destination)
124
+ end
125
+
126
+ def safe_rmrf(dir)
127
+ validate_dir_for_rmrf(dir)
128
+ FileUtils.rmtree(dir)
129
+ end
130
+
131
+ def validate_dir_for_rmrf(dir)
132
+ raise ArgumentError, "Path #{dir} is not a valid directory" unless File.directory?(dir)
133
+ raise ArgumentError, "Path #{dir} is not a subdirectory of #{@destination}" unless Pathname.new(dir).fnmatch?(File.join(@destination, '**'))
134
+ end
68
135
  end
69
136
  end