aaf-mdqt 0.8.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 (66) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/codeql-analysis.yml +70 -0
  3. data/.github/workflows/ruby.yml +41 -0
  4. data/.gitignore +25 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +1 -0
  7. data/.rubocop_todo.yml +296 -0
  8. data/.ruby-version +1 -0
  9. data/.tool-versions +1 -0
  10. data/.travis.yml +7 -0
  11. data/CHANGELOG.md +168 -0
  12. data/CODE_OF_CONDUCT.md +74 -0
  13. data/Gemfile +9 -0
  14. data/LICENSE.txt +21 -0
  15. data/Makefile +4 -0
  16. data/README.md +268 -0
  17. data/Rakefile +5 -0
  18. data/aaf-mdqt.gemspec +46 -0
  19. data/bin/console +14 -0
  20. data/bin/setup +8 -0
  21. data/cucumber.yml +2 -0
  22. data/exe/mdqt +174 -0
  23. data/lib/mdqt/cli/base.rb +190 -0
  24. data/lib/mdqt/cli/cache_control.rb +25 -0
  25. data/lib/mdqt/cli/check.rb +78 -0
  26. data/lib/mdqt/cli/compliance.rb +0 -0
  27. data/lib/mdqt/cli/defaults.rb +70 -0
  28. data/lib/mdqt/cli/entities.rb +47 -0
  29. data/lib/mdqt/cli/exists.rb +0 -0
  30. data/lib/mdqt/cli/get.rb +130 -0
  31. data/lib/mdqt/cli/list.rb +65 -0
  32. data/lib/mdqt/cli/ln.rb +81 -0
  33. data/lib/mdqt/cli/ls.rb +54 -0
  34. data/lib/mdqt/cli/rename.rb +75 -0
  35. data/lib/mdqt/cli/reset.rb +27 -0
  36. data/lib/mdqt/cli/services.rb +25 -0
  37. data/lib/mdqt/cli/transform.rb +33 -0
  38. data/lib/mdqt/cli/url.rb +37 -0
  39. data/lib/mdqt/cli/version.rb +17 -0
  40. data/lib/mdqt/cli.rb +24 -0
  41. data/lib/mdqt/client/identifier_utils.rb +51 -0
  42. data/lib/mdqt/client/metadata_file.rb +144 -0
  43. data/lib/mdqt/client/metadata_response.rb +182 -0
  44. data/lib/mdqt/client/metadata_service.rb +194 -0
  45. data/lib/mdqt/client/metadata_validator.rb +81 -0
  46. data/lib/mdqt/client.rb +83 -0
  47. data/lib/mdqt/schema/MetadataExchange.xsd +112 -0
  48. data/lib/mdqt/schema/mdqt_check_schema.xsd +5 -0
  49. data/lib/mdqt/schema/oasis-200401-wss-wssecurity-secext-1.0.xsd +195 -0
  50. data/lib/mdqt/schema/oasis-200401-wss-wssecurity-utility-1.0.xsd +108 -0
  51. data/lib/mdqt/schema/saml-schema-assertion-2.0.xsd +283 -0
  52. data/lib/mdqt/schema/saml-schema-metadata-2.0.xsd +337 -0
  53. data/lib/mdqt/schema/ws-addr.xsd +137 -0
  54. data/lib/mdqt/schema/ws-authorization.xsd +145 -0
  55. data/lib/mdqt/schema/ws-federation.xsd +471 -0
  56. data/lib/mdqt/schema/ws-securitypolicy-1.2.xsd +1205 -0
  57. data/lib/mdqt/schema/xenc-schema.xsd +136 -0
  58. data/lib/mdqt/schema/xml.xsd +287 -0
  59. data/lib/mdqt/schema/xmldsig-core-schema.xsd +309 -0
  60. data/lib/mdqt/version.rb +3 -0
  61. data/lib/mdqt.rb +5 -0
  62. data/lib/tasks/cucumber.rake +8 -0
  63. data/lib/tasks/spec.rake +5 -0
  64. data/lib/tasks/tests.rake +6 -0
  65. data/lib/tasks/yard.rake +6 -0
  66. metadata +332 -0
@@ -0,0 +1,190 @@
1
+ module MDQT
2
+
3
+ class CLI
4
+
5
+ class Base
6
+
7
+ require 'mdqt/cli'
8
+ require 'pastel'
9
+ require 'pathname'
10
+
11
+ def self.run(args, options)
12
+
13
+ check_requirements(options)
14
+ introduce(options)
15
+
16
+ self.new(args, options).run
17
+ end
18
+
19
+ def self.check_requirements(options)
20
+
21
+ unless options.service == :not_required
22
+ abort "No MDQ service URL has been specified. Please use --service, MDQT_SERVICE or MDQ_BASE_URL" unless service_url(options).to_s.start_with?("http")
23
+ end
24
+
25
+ if options.save_to
26
+ dir = options.save_to
27
+ begin
28
+ FileUtils.mkdir_p(dir) unless File.exist?(dir)
29
+ rescue
30
+ abort "Error: Directory #{dir} did not exist, and we can't create it"
31
+ end
32
+ abort "Error: '#{dir}' is not a writable directory!" if (File.directory?(dir) && !File.writable?(dir))
33
+ abort "Error: '#{dir}' is not a directory!" unless File.directory?(dir)
34
+ end
35
+
36
+ end
37
+
38
+ def self.introduce(options)
39
+ if options.verbose
40
+ STDERR.puts "MDQT version #{MDQT::VERSION}"
41
+ STDERR.puts "Using #{service_url(options)}" unless options.service == :not_required
42
+ STDERR.puts "Caching is #{MDQT::CLI::CacheControl.caching_on?(options) ? 'on' : 'off'}"
43
+ STDERR.print "XML validation is #{MDQT::Client.verification_available? ? 'available' : 'not available'}"
44
+ STDERR.puts " #{options.validate ? "and active" : "but inactive"} for this request" if MDQT::Client.verification_available?
45
+ STDERR.print "Signature verification is #{MDQT::Client.verification_available? ? 'available' : 'not available'}"
46
+ STDERR.puts " #{options.verify_with ? "and active" : "but inactive"} for this request" if MDQT::Client.verification_available?
47
+ STDERR.puts "Output directory for saved files is: #{File.absolute_path(options.save_to)}" if options.save_to
48
+ STDERR.puts("Warning! TLS certificate verification has been disabled!") if options.tls_risky
49
+ STDERR.puts
50
+ end
51
+ end
52
+
53
+ def initialize(cli_args, options)
54
+ piped_input = get_stdin
55
+ @args = cli_args.concat(piped_input)
56
+ @options = options
57
+ end
58
+
59
+ def get_stdin
60
+ return $stdin.readlines.map(&:split).flatten.map(&:strip) if pipeable?
61
+ []
62
+ end
63
+
64
+ def pipeable?
65
+ return false if ENV["MDQT_STDIN"].to_s.strip.downcase == "off" # Workaround Aruba testing weirdness?
66
+ !STDIN.tty? && !$stdin.closed? && $stdin.stat.pipe?
67
+ end
68
+
69
+ def args
70
+ @args
71
+ end
72
+
73
+ def options=(new_options)
74
+ @options = new_options
75
+ end
76
+
77
+ def options
78
+ @options
79
+ end
80
+
81
+ def self.service_url(options)
82
+
83
+ return nil if options.service == :not_required
84
+
85
+ choice = options.service.to_s.strip
86
+
87
+ if choice.downcase.start_with? "http"
88
+ normalize_base_url(choice)
89
+ else
90
+ Defaults.lookup_service_alias(choice)
91
+ end
92
+
93
+ end
94
+
95
+ def service_url(options)
96
+ self.class.service_url(options)
97
+ end
98
+
99
+ def output(response)
100
+ if response.ok?
101
+ yay response.message
102
+ hey explain(response) if options.explain
103
+ trailer = response.data[-1] == "\n" ? "" : "\n"
104
+ response.data + trailer
105
+ else
106
+ hey response.message
107
+ end
108
+
109
+ end
110
+
111
+ def explain(response)
112
+ unless response.explanation.empty?
113
+ require 'terminal-table'
114
+ misc_rows = [['URL', response.explanation[:url]], ["Method", response.explanation[:method]], ['Status', response.explanation[:status]]]
115
+ req_header_rows = response.explanation[:request_headers].map { |k, v| ['C', k, v] }
116
+ resp_header_rows = response.explanation[:response_headers].map { |k, v| ['S', k, v] }
117
+
118
+ btw Terminal::Table.new :title => "HTTP Misc", :rows => misc_rows
119
+ btw Terminal::Table.new :title => "Client Request Headers", :headings => ['C/S', 'Header', 'Value'], :rows => req_header_rows
120
+ btw Terminal::Table.new :title => "Server Response Headers", :headings => ['C/S', 'Header', 'Value'], :rows => resp_header_rows
121
+
122
+ end
123
+ end
124
+
125
+ def advise_on_xml_signing_support
126
+ hey "XML signature verification and XML validation are not available. Install the 'xmldsig' gem if you can." unless MDQT::Client.verification_available?
127
+ end
128
+
129
+ def extract_certificate_paths(cert_paths = options.verify_with)
130
+ cert_paths.collect do |cert_path|
131
+ begin
132
+ halt! "Cannot read certificate at '#{cert_path}'!" unless File.readable?(cert_path)
133
+ halt! "File at '#{cert_path}' does not seem to be a PEM format certificate" unless IO.binread(cert_path).include?("-----BEGIN CERTIFICATE-----")
134
+ cert_path
135
+ rescue
136
+ halt! "Unable to validate the certificate at '#{cert_path}'"
137
+ end
138
+ end
139
+ end
140
+
141
+ def colour_shell?
142
+ TTY::Color.color?
143
+ end
144
+
145
+ def pastel
146
+ @pastel ||= Pastel.new
147
+ end
148
+
149
+ def say(text)
150
+ STDOUT.puts(text)
151
+ end
152
+
153
+ def hey(comment)
154
+ STDERR.puts(comment)
155
+ end
156
+
157
+ def btw(comment)
158
+ STDERR.puts(comment) if options.verbose
159
+ end
160
+
161
+ def yay(comment)
162
+ btw pastel.green(comment)
163
+ end
164
+
165
+ def halt!(comment)
166
+ abort pastel.red("Error: #{comment}")
167
+ end
168
+
169
+ def run
170
+ halt! "No action has been defined for this command!"
171
+ end
172
+
173
+ private
174
+
175
+ ## Base URLs should end with a "/", it might be easier to just add one rather than raise an error
176
+ def self.normalize_base_url(url)
177
+ if url.end_with?('/')
178
+ url
179
+ else
180
+ "#{url}/"
181
+ end
182
+ end
183
+
184
+ end
185
+
186
+ end
187
+
188
+ #
189
+ end
190
+
@@ -0,0 +1,25 @@
1
+ module MDQT
2
+ class CLI
3
+
4
+ class CacheControl
5
+
6
+ class << self
7
+
8
+ def caching_on?(options)
9
+ return false if cache_type(options) == :none
10
+ true
11
+ end
12
+
13
+ def cache_type(options)
14
+ return :none if options.refresh
15
+ return :memcache if options.cache && options.memcache
16
+ return :file if options.cache
17
+ :none
18
+ end
19
+
20
+ end
21
+
22
+ end
23
+ end
24
+
25
+ end
@@ -0,0 +1,78 @@
1
+ module MDQT
2
+
3
+ class CLI
4
+
5
+ require 'mdqt/cli/base'
6
+
7
+ class Check < Base
8
+
9
+ def run
10
+
11
+ options.validate = true
12
+
13
+ advise_on_xml_signing_support
14
+ halt!("Cannot check a metadata file without XML support: please install additional gems") unless MDQT::Client.verification_available?
15
+
16
+ client = MDQT::Client.new(
17
+ service_url(options),
18
+ verbose: options.verbose,
19
+ explain: options.explain ? true : false,
20
+ )
21
+
22
+ cert_paths = options.verify_with ? extract_certificate_paths(options.verify_with) : []
23
+
24
+ args.each do |filename|
25
+
26
+ filename = File.absolute_path(filename)
27
+ file = client.open_metadata(filename)
28
+
29
+ halt!("Cannot access file #{filename}") unless file.readable?
30
+
31
+ halt!("XML validation failed for #{filename}:\n#{file.validation_error}") unless file.valid?
32
+ btw "File #{filename} is valid SAML Metadata XML"
33
+
34
+ if options.verify_with
35
+ halt! "XML in #{filename} is not signed, cannot verify!" unless file.signed?
36
+ halt! "The signed XML for #{filename} cannot be verified using #{cert_paths.to_sentence}" unless file.verified_signature?(cert_paths)
37
+ btw "Signed XML for #{filename} has been verified using '#{cert_paths.to_sentence}'"
38
+ end
39
+
40
+ yay "#{filename} OK"
41
+ end
42
+
43
+ end
44
+
45
+ def verify_results(results)
46
+
47
+ # if options.validate
48
+ # results.each do |result|
49
+ # next unless result.ok?
50
+ # halt! "The data for #{result.identifier} is not valid when checked against schema:\n#{result.validation_error}" unless result.valid?
51
+ # btw "Data for #{result.identifier.empty? ? 'aggregate' : result.identifier } has been validated against schema" ## FIXME - needs constistent #label maybe?
52
+ # end
53
+ # end
54
+ #
55
+ # return results unless options.verify_with
56
+ #
57
+ # cert_paths = extract_certificate_paths(options.verify_with)
58
+ #
59
+ # results.each do |result|
60
+ # next unless result.ok?
61
+ # halt! "Data from #{options.service} is not signed, cannot verify!" unless result.signed?
62
+ # halt! "The data for #{result.identifier} cannot be verified using #{cert_paths.to_sentence}" unless result.verified_signature?(cert_paths)
63
+ # btw "Data for #{result.identifier.empty? ? 'aggregate' : result.identifier } has been verified using '#{cert_paths.to_sentence}'" ## FIXME - needs constistent #label maybe?
64
+ # end
65
+ #
66
+ # results
67
+
68
+ end
69
+
70
+ end
71
+
72
+ private
73
+
74
+ end
75
+
76
+ end
77
+
78
+
File without changes
@@ -0,0 +1,70 @@
1
+ module MDQT
2
+ class CLI
3
+
4
+ class Defaults
5
+
6
+ class << self
7
+
8
+ def base_url
9
+
10
+ ENV['MDQT_SERVICE'] || ENV['MDQ_BASE_URL'] || guess_service
11
+
12
+ end
13
+
14
+ def force_hash?
15
+ false
16
+ end
17
+
18
+ def cli_defaults
19
+ {
20
+ hash: force_hash?,
21
+ cache: true,
22
+ refresh: false
23
+ }
24
+ end
25
+
26
+ def guess_service
27
+
28
+ locale = ENV['LANG']
29
+
30
+ service = services.find { |s| s[:locale] == locale }
31
+ #service ||= services.first
32
+
33
+ if service
34
+ url = service[:url]
35
+ STDERR.puts "MDQT is assuming that you want to use #{url}\nPlease configure this using --service, MDQT_SERVICE or MDQ_BASE_URL\n\n"
36
+ url
37
+ else
38
+ nil
39
+ end
40
+
41
+ end
42
+
43
+ def lookup_service_alias(srv_alias)
44
+ service = services.find { |s| s[:alias].to_s.downcase.to_sym == srv_alias.to_s.downcase.to_sym }
45
+ service ? service[:url] : nil
46
+ end
47
+
48
+ def services
49
+ [
50
+ { alias: "ukamf",
51
+ locale: "en_GB.UTF-8",
52
+ url: "http://mdq.ukfederation.org.uk/"
53
+ },
54
+ { alias: "incommon",
55
+ locale: "en_US.UTF-8",
56
+ url: "https://mdq.incommon.org/"
57
+ },
58
+ { alias: "dfn",
59
+ locale: "de_utf8",
60
+ url: "https://mdq.aai.dfn.de/"
61
+ },
62
+ ]
63
+ end
64
+
65
+ end
66
+
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,47 @@
1
+ module MDQT
2
+
3
+ class CLI
4
+
5
+ require 'mdqt/cli/base'
6
+
7
+ class Entities < Base
8
+
9
+ def run
10
+
11
+ options.validate = true
12
+
13
+ advise_on_xml_signing_support
14
+ halt!("Cannot check a metadata file without XML support: please install additional gems") unless MDQT::Client.verification_available?
15
+
16
+ client = MDQT::Client.new(
17
+ service_url(options),
18
+ verbose: options.verbose,
19
+ explain: options.explain ? true : false,
20
+ )
21
+
22
+ args.each do |filename|
23
+
24
+ file = client.open_metadata(filename)
25
+
26
+ halt!("Cannot access file #{filename}") unless file.readable?
27
+
28
+ halt!("XML validation failed for #{filename}:\n#{file.validation_error}") unless file.valid?
29
+
30
+ file.entity_ids.each do |id|
31
+ id = options.sha1 ? [id, MDQT::Client::IdentifierUtils.transform_uri(id)].join(" ") : id
32
+ say(id)
33
+ end
34
+
35
+ end
36
+
37
+ end
38
+
39
+ end
40
+
41
+ private
42
+
43
+ end
44
+
45
+ end
46
+
47
+
File without changes
@@ -0,0 +1,130 @@
1
+ module MDQT
2
+
3
+ class CLI
4
+
5
+ require 'mdqt/cli/base'
6
+
7
+ class Get < Base
8
+
9
+ def run
10
+
11
+ aggregate_confirmation_check!
12
+
13
+ advise_on_xml_signing_support
14
+
15
+ results = verify_results(get_responses)
16
+
17
+ #results = MetadataAggregator.aggregate_responses(results) if options.aggregate
18
+
19
+ output_metadata(results, options)
20
+
21
+ end
22
+
23
+ def get_responses
24
+
25
+ client = MDQT::Client.new(
26
+ service_url(options),
27
+ verbose: options.verbose,
28
+ explain: options.explain ? true : false,
29
+ tls_risky: options.tls_risky ? true : false,
30
+ cache_type: MDQT::CLI::CacheControl.cache_type(options),
31
+ )
32
+
33
+ args.empty? ? [client.get_metadata("")] : args.collect { |entity_id| client.get_metadata(entity_id) }
34
+
35
+ end
36
+
37
+ def verify_results(results)
38
+
39
+ if options.validate
40
+ results.each do |result|
41
+ next unless result.ok?
42
+ halt! "The data for #{result.identifier} is not valid when checked against schema:\n#{result.validation_error}" unless result.valid?
43
+ btw "Data for #{result.identifier.empty? ? 'aggregate' : result.identifier } has been validated against schema" ## FIXME - needs constistent #label maybe?
44
+ end
45
+ end
46
+
47
+ return results unless options.verify_with
48
+
49
+ cert_paths = extract_certificate_paths(options.verify_with)
50
+
51
+ results.each do |result|
52
+ next unless result.ok?
53
+ halt! "Data from #{options.service} is not signed, cannot verify!" unless result.signed?
54
+ halt! "The data for #{result.identifier} cannot be verified using #{cert_paths.to_sentence}" unless result.verified_signature?(cert_paths)
55
+ btw "Data for #{result.identifier.empty? ? 'aggregate' : result.identifier } has been verified using '#{cert_paths.to_sentence}'" ## FIXME - needs constistent #label maybe?
56
+ end
57
+
58
+ results
59
+
60
+ end
61
+
62
+ def output_metadata(results, options)
63
+ case action(results, options)
64
+ when :save_files
65
+ output_files(results, options)
66
+ when :print_to_stdout
67
+ output_to_stdout(results, options)
68
+ else
69
+ halt! "Can't determine output type"
70
+ end
71
+ end
72
+
73
+ def action(results, options)
74
+ case
75
+ when options.save_to
76
+ :save_files
77
+ else
78
+ :print_to_stdout
79
+ end
80
+ end
81
+
82
+ def output_to_stdout(results, options)
83
+ results.each { |r| puts output(r) }
84
+ end
85
+
86
+ def output_files(results, options)
87
+ pwd = Pathname.getwd
88
+ prepare_output_directory(options.save_to)
89
+ results.each do |result|
90
+ main_file = output_file_path(result.filename)
91
+ open(main_file, 'w') { |f|
92
+ f.puts result.data
93
+ }
94
+
95
+ if options.list
96
+ puts Pathname.new(main_file).relative_path_from(pwd)
97
+ end
98
+
99
+ yay "Created #{main_file}"
100
+
101
+ # if options.link_id
102
+ # ["{sha1}#{result.filename.gsub(".xml", "")}"].each do |altname|
103
+ # full_alias = output_file_path(altname)
104
+ # FileUtils.ln_sf(main_file, full_alias)
105
+ # yay "Linked alias #{altname} -> #{main_file}"
106
+ # end
107
+ # end
108
+
109
+ end
110
+ end
111
+
112
+ private
113
+
114
+ def output_file_path(filename)
115
+ File.absolute_path(File.join([options.save_to, filename]))
116
+ end
117
+
118
+ def prepare_output_directory(directory)
119
+ FileUtils.mkdir_p(directory)
120
+ end
121
+
122
+ def aggregate_confirmation_check!
123
+ halt!("Please specify --all if you wish to request all entities from #{options.service}") if args.empty? && !options.all
124
+ end
125
+
126
+ end
127
+
128
+ end
129
+
130
+ end
@@ -0,0 +1,65 @@
1
+ module MDQT
2
+
3
+ class CLI
4
+
5
+ require 'mdqt/cli/base'
6
+
7
+ class List < Base
8
+
9
+ def run
10
+
11
+ options.validate = true
12
+
13
+ advise_on_xml_signing_support
14
+
15
+ halt!("Cannot check a metadata file without XML support: please install additional gems") unless MDQT::Client.verification_available?
16
+
17
+ response = get_response
18
+ result = verify_result(response)
19
+
20
+ puts result.entity_ids
21
+
22
+ end
23
+
24
+ def get_response
25
+
26
+ client = MDQT::Client.new(
27
+ service_url(options),
28
+ verbose: options.verbose,
29
+ explain: options.explain ? true : false,
30
+ tls_risky: options.tls_risky ? true : false,
31
+ cache_type: MDQT::CLI::CacheControl.cache_type(options),
32
+ )
33
+
34
+ client.get_metadata("")
35
+
36
+ end
37
+
38
+ def verify_result(result)
39
+
40
+ if options.validate
41
+ halt! "The data for #{result.identifier} is not valid when checked against schema:\n#{result.validation_error}" unless result.valid?
42
+ btw "Data for #{result.identifier.empty? ? 'aggregate' : result.identifier } has been validated against schema" ## FIXME - needs constistent #label maybe?
43
+ end
44
+
45
+ return result unless options.verify_with
46
+
47
+ cert_paths = extract_certificate_paths(options.verify_with)
48
+
49
+ halt! "Data from #{options.service} is not signed, cannot verify!" unless result.signed?
50
+ halt! "The data for #{result.identifier} cannot be verified using #{cert_paths.to_sentence}" unless result.verified_signature?(cert_paths)
51
+ btw "Data for #{result.identifier.empty? ? 'aggregate' : result.identifier } has been verified using '#{cert_paths.to_sentence}'" ## FIXME - needs constistent #label maybe?
52
+
53
+ result
54
+
55
+ end
56
+
57
+ end
58
+
59
+ private
60
+
61
+ end
62
+
63
+ end
64
+
65
+
@@ -0,0 +1,81 @@
1
+ module MDQT
2
+
3
+ class CLI
4
+
5
+ require 'mdqt/cli/base'
6
+
7
+ class Ln < Base
8
+
9
+ def run
10
+
11
+ options.validate = true
12
+
13
+ advise_on_xml_signing_support
14
+ halt!("Cannot check a metadata file without XML support: please install additional gems") unless MDQT::Client.verification_available?
15
+
16
+ client = MDQT::Client.new(
17
+ options.service,
18
+ verbose: options.verbose,
19
+ explain: options.explain ? true : false,
20
+ )
21
+
22
+ halt!("Please specify a file to link to!") if args.empty?
23
+
24
+ args.each do |filename|
25
+
26
+ next if File.symlink?(filename)
27
+
28
+ file = client.open_metadata(filename)
29
+
30
+ halt!("Cannot access file #{filename}") unless file.readable?
31
+ halt!("File #{filename} is a metadata aggregate, cannot create entityID hashed link!") if file.aggregate?
32
+ halt!("XML validation failed for #{filename}:\n#{file.validation_error}") unless file.valid?
33
+
34
+ halt!("Cannot find entityID for #{filename}") unless file.entity_id
35
+
36
+ linkname = file.linkname
37
+
38
+ if filename == linkname
39
+ if options.force
40
+ hey("Warning: Cannot link file to itself, skipping! #{filename}")
41
+ next
42
+ else
43
+ halt!("Cannot link file to itself! #{filename}")
44
+ next
45
+ end
46
+ btw("Cannot link file to itself! #{filename}")
47
+ end
48
+
49
+ if file.turd?
50
+ hey "Warning: will not process backup/turd files"
51
+ next
52
+ end
53
+
54
+ message = ""
55
+
56
+ if File.exist?(linkname)
57
+ if options.force
58
+ File.delete(linkname)
59
+ else
60
+ old_target = File.readlink(linkname)
61
+ message = old_target == filename ? "File exists" : "Conflicts with #{filename}"
62
+ halt!("#{linkname} -> #{old_target} [#{file.entity_id}] #{message}. Use --force to override")
63
+ next
64
+ end
65
+ end
66
+
67
+ File.symlink(filename, linkname)
68
+ hey("#{linkname} -> #{filename} [#{file.entity_id}] #{message}") if options.verbose
69
+ end
70
+
71
+ end
72
+
73
+ end
74
+
75
+ private
76
+
77
+ end
78
+
79
+ end
80
+
81
+