aaf-mdqt 0.8.0

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