kpm 0.7.2 → 0.10.0

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 (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