fig 1.0.0 → 1.1.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.
@@ -9,6 +9,8 @@ require 'fig/command/action/list_configs'
9
9
  require 'fig/command/action/list_dependencies'
10
10
  require 'fig/command/action/list_dependencies/all_configs'
11
11
  require 'fig/command/action/list_dependencies/default'
12
+ require 'fig/command/action/list_dependencies/graphviz'
13
+ require 'fig/command/action/list_dependencies/graphviz_all_configs'
12
14
  require 'fig/command/action/list_dependencies/tree'
13
15
  require 'fig/command/action/list_dependencies/tree_all_configs'
14
16
  require 'fig/command/action/list_local'
@@ -16,6 +18,8 @@ require 'fig/command/action/list_remote'
16
18
  require 'fig/command/action/list_variables'
17
19
  require 'fig/command/action/list_variables/all_configs'
18
20
  require 'fig/command/action/list_variables/default'
21
+ require 'fig/command/action/list_variables/graphviz'
22
+ require 'fig/command/action/list_variables/graphviz_all_configs'
19
23
  require 'fig/command/action/list_variables/tree'
20
24
  require 'fig/command/action/list_variables/tree_all_configs'
21
25
  require 'fig/command/action/options'
@@ -163,6 +167,10 @@ class Fig::Command::Options
163
167
  return @list_tree
164
168
  end
165
169
 
170
+ def graphviz?()
171
+ return @graphviz
172
+ end
173
+
166
174
  def strip_shell_command(argv)
167
175
  argv.each_with_index do |arg, i|
168
176
  case arg
@@ -308,6 +316,13 @@ class Fig::Command::Options
308
316
  @list_tree = true
309
317
  end
310
318
 
319
+ @parser.on(
320
+ '--graphviz',
321
+ 'for listings, output DOT (http://graphviz.org/content/dot-language)'
322
+ ) do
323
+ @graphviz = true
324
+ end
325
+
311
326
  @parser.on(
312
327
  '--list-all-configs',
313
328
  'for listings, follow all configurations of the base package'
@@ -523,7 +538,7 @@ class Fig::Command::Options
523
538
  end
524
539
 
525
540
  @parser.on(
526
- '-l', '--login', 'login to remote repo as a non-anonymous user'
541
+ '-l', '--login', 'login to FTP repo as a non-anonymous user'
527
542
  ) do
528
543
  @login = true
529
544
  end
@@ -678,6 +693,12 @@ class Fig::Command::Options
678
693
  raise Fig::Command::OptionError.new(
679
694
  'Cannot use --suppress-all-includes/--suppress-cross-package-includes with --list-tree.'
680
695
  )
696
+ elsif graphviz?
697
+ # Not conceptually incompatible, just not implemented (would need to
698
+ # handle in command/action/role/list_*)
699
+ raise Fig::Command::OptionError.new(
700
+ 'Cannot use --suppress-all-includes/--suppress-cross-package-includes with --graphviz.'
701
+ )
681
702
  elsif list_all_configs?
682
703
  # Not conceptually incompatible, just not implemented (would need to
683
704
  # handle in command/action/role/list_*)
@@ -696,17 +717,27 @@ class Fig::Command::Options
696
717
  )
697
718
  end
698
719
  elsif list_tree?
699
- if ! @base_action.list_dependencies? && ! @base_action.list_variables?
700
- raise Fig::Command::OptionError.new(
701
- %q<The --list-tree option isn't useful without --list-dependencies/--list-variables.>
702
- )
703
- end
720
+ validate_list_option '--list-tree'
721
+ elsif graphviz?
722
+ validate_list_option '--graphviz'
704
723
  elsif list_all_configs?
705
- if ! @base_action.list_dependencies? && ! @base_action.list_variables?
706
- raise Fig::Command::OptionError.new(
707
- %q<The --list-all-configs option isn't useful without --list-dependencies/--list-variables.>
708
- )
709
- end
724
+ validate_list_option '--list-all-configs'
725
+ end
726
+
727
+ if list_tree? && graphviz?
728
+ raise Fig::Command::OptionError.new(
729
+ 'Cannot use --list-tree and --graphviz at the same time.'
730
+ )
731
+ end
732
+
733
+ return
734
+ end
735
+
736
+ def validate_list_option(option)
737
+ if ! @base_action.list_dependencies? && ! @base_action.list_variables?
738
+ raise Fig::Command::OptionError.new(
739
+ %Q<The #{option} option isn't useful without --list-dependencies/--list-variables.>
740
+ )
710
741
  end
711
742
 
712
743
  return
@@ -720,6 +751,8 @@ class Fig::Command::Options
720
751
  sub_action_name = :Default
721
752
  if list_tree?
722
753
  sub_action_name = list_all_configs? ? :TreeAllConfigs : :Tree
754
+ elsif graphviz?
755
+ sub_action_name = list_all_configs? ? :GraphvizAllConfigs : :Graphviz
723
756
  elsif list_all_configs?
724
757
  sub_action_name = :AllConfigs
725
758
  end
@@ -63,13 +63,18 @@ Querying:
63
63
 
64
64
  fig {--list-local | --list-remote} [...]
65
65
  fig {-g | --get} VARIABLE [DESCRIPTOR] [...]
66
- fig --list-dependencies [--list-tree] [--list-all-configs] [DESCRIPTOR] [...]
67
- fig --list-variables [--list-tree] [--list-all-configs] [DESCRIPTOR] [...]
66
+ fig --list-dependencies [...list options...] [DESCRIPTOR] [...]
67
+ fig --list-variables [...list options...] [DESCRIPTOR] [...]
68
68
  fig --list-configs [DESCRIPTOR] [...]
69
69
  fig --dump-package-definition-text [DESCRIPTOR] [...]
70
70
  fig --dump-package-definition-parsed [DESCRIPTOR] [...]
71
71
  fig --dump-package-definition-for-command-line [DESCRIPTOR] [...]
72
72
 
73
+ List options (represented as "[...list options...]" above):
74
+
75
+ [--list-tree | --graphviz]
76
+ [--list-all-configs]
77
+
73
78
  Standard options (represented as "[...]" above):
74
79
 
75
80
  [-u | --update | -m | --update-if-missing]
data/lib/fig/figrc.rb CHANGED
@@ -17,7 +17,7 @@ class Fig::FigRC
17
17
  def self.find(
18
18
  override_path,
19
19
  specified_repository_url,
20
- login,
20
+ operating_system,
21
21
  fig_home,
22
22
  disable_figrc = false
23
23
  )
@@ -33,7 +33,7 @@ class Fig::FigRC
33
33
  configuration.remote_repository_url = repository_url
34
34
 
35
35
  handle_repository_configuration(
36
- configuration, repository_url, login, fig_home
36
+ configuration, repository_url, operating_system, fig_home
37
37
  )
38
38
 
39
39
  return configuration
@@ -77,7 +77,7 @@ class Fig::FigRC
77
77
  end
78
78
 
79
79
  def self.handle_repository_configuration(
80
- configuration, repository_url, login, fig_home
80
+ configuration, repository_url, operating_system, fig_home
81
81
  )
82
82
  return if repository_url.nil?
83
83
 
@@ -85,11 +85,9 @@ class Fig::FigRC
85
85
  repo_figrc_path =
86
86
  File.expand_path(File.join(fig_home, REPOSITORY_CONFIGURATION))
87
87
 
88
- os = Fig::OperatingSystem.new(login)
89
-
90
88
  repo_config_exists = nil
91
89
  begin
92
- os.download( figrc_url, repo_figrc_path )
90
+ operating_system.download figrc_url, repo_figrc_path
93
91
  repo_config_exists = true
94
92
  rescue Fig::FileNotFoundError
95
93
  repo_config_exists = false
@@ -1,24 +1,19 @@
1
1
  require 'cgi'
2
2
  require 'fileutils'
3
- require 'find'
4
3
  # Must specify absolute path of ::Archive when using
5
4
  # this module to avoid conflicts with Fig::Statement::Archive
6
5
  require 'libarchive_ruby'
7
- require 'net/http'
8
- require 'net/ssh'
9
- require 'net/sftp'
10
- require 'net/netrc'
11
6
  require 'rbconfig'
12
- require 'tempfile'
13
-
14
- require 'highline/import'
15
7
 
16
8
  require 'fig/at_exit'
17
9
  require 'fig/environment_variables/case_insensitive'
18
10
  require 'fig/environment_variables/case_sensitive'
19
- require 'fig/file_not_found_error'
20
11
  require 'fig/logging'
21
12
  require 'fig/network_error'
13
+ require 'fig/protocol/file'
14
+ require 'fig/protocol/ftp'
15
+ require 'fig/protocol/http'
16
+ require 'fig/protocol/sftp'
22
17
  require 'fig/repository_error'
23
18
  require 'fig/url'
24
19
  require 'fig/user_input_error'
@@ -64,33 +59,11 @@ class Fig::OperatingSystem
64
59
  end
65
60
 
66
61
  def initialize(login)
67
- @login = login
68
- @username = ENV['FIG_USERNAME']
69
- @password = ENV['FIG_PASSWORD']
70
- end
71
-
72
- def get_username()
73
- # #ask() comes from highline
74
- @username ||= ask('Username: ') { |q| q.echo = true }
75
- end
76
-
77
- def get_password()
78
- # #ask() comes from highline
79
- @password ||= ask('Password: ') { |q| q.echo = false }
80
- end
81
-
82
- def ftp_login(ftp, host)
83
- if @login
84
- rc = Net::Netrc.locate(host)
85
- if rc
86
- @username = rc.login
87
- @password = rc.password
88
- end
89
- ftp.login(get_username, get_password)
90
- else
91
- ftp.login()
92
- end
93
- ftp.passive = true
62
+ @protocols = {}
63
+ @protocols['file'] = Fig::Protocol::File.new
64
+ @protocols['ftp'] = Fig::Protocol::FTP.new login
65
+ @protocols['http'] = Fig::Protocol::HTTP.new
66
+ @protocols['sftp'] = Fig::Protocol::SFTP.new
94
67
  end
95
68
 
96
69
  def list(dir)
@@ -105,55 +78,11 @@ class Fig::OperatingSystem
105
78
  File.open(path, 'wb') { |f| f.binmode; f << content }
106
79
  end
107
80
 
108
- def strip_paths_for_list(ls_output, packages, path)
109
- if not ls_output.nil?
110
- ls_output = ls_output.gsub(path + '/', '').gsub(path, '').split("\n")
111
- ls_output.each do |line|
112
- parts = line.gsub(/\\/, '/').sub(/^\.\//, '').sub(/:$/, '').chomp().split('/')
113
- packages << parts.join('/') if parts.size == 2
114
- end
115
- end
116
- end
117
-
118
81
  def download_list(url)
119
82
  begin
120
- uri = Fig::URL.parse(url)
121
- rescue
122
- Fig::Logging.fatal %Q<Unable to parse url: "#{url}">
123
- raise Fig::NetworkError.new
124
- end
83
+ protocol, uri = decode_protocol url
125
84
 
126
- begin
127
- case uri.scheme
128
- when 'ftp'
129
- ftp = Net::FTP.new(uri.host)
130
- ftp_login(ftp, uri.host)
131
- ftp.chdir(uri.path)
132
- dirs = ftp.nlst
133
- ftp.close
134
-
135
- download_ftp_list(uri, dirs)
136
- when 'ssh'
137
- packages = []
138
- Net::SSH.start(uri.host, uri.user) do |ssh|
139
- ls = ssh.exec!("[ -d #{uri.path} ] && find #{uri.path}")
140
- strip_paths_for_list(ls, packages, uri.path)
141
- end
142
- packages
143
- when 'file'
144
- packages = []
145
- unescaped_path = CGI.unescape uri.path
146
- return packages if ! File.exist?(unescaped_path)
147
-
148
- ls = ''
149
- Find.find(unescaped_path) { |file| ls << file.to_s; ls << "\n" }
150
-
151
- strip_paths_for_list(ls, packages, unescaped_path)
152
- return packages
153
- else
154
- Fig::Logging.fatal "Protocol not supported: #{url}"
155
- raise Fig::NetworkError.new "Protocol not supported: #{url}"
156
- end
85
+ return protocol.download_list uri
157
86
  rescue SocketError => error
158
87
  Fig::Logging.debug error.message
159
88
  raise Fig::NetworkError.new "#{url}: #{error.message}"
@@ -163,150 +92,23 @@ class Fig::OperatingSystem
163
92
  end
164
93
  end
165
94
 
166
- def download_ftp_list(uri, dirs)
167
- # Run a bunch of these in parallel since they're slow as hell
168
- num_threads = (ENV['FIG_FTP_THREADS'] || '16').to_i
169
- threads = []
170
- all_packages = []
171
- (0..num_threads-1).each { |num| all_packages[num] = [] }
172
- (0..num_threads-1).each do |num|
173
- threads << Thread.new do
174
- packages = all_packages[num]
175
- ftp = Net::FTP.new(uri.host)
176
- ftp_login(ftp, uri.host)
177
- ftp.chdir(uri.path)
178
- pos = num
179
- while pos < dirs.length
180
- pkg = dirs[pos]
181
- begin
182
- ftp.nlst(dirs[pos]).each do |ver|
183
- packages << pkg + '/' + ver
184
- end
185
- rescue Net::FTPPermError
186
- # Ignore this error because it's indicative of the FTP library
187
- # encountering a file or directory that it does not have
188
- # permission to open. Fig needs to be able to have secure
189
- # repos/packages and there is no way easy way to deal with the
190
- # permissions issues other than consuming these errors.
191
- #
192
- # Actually, with FTP, you can't tell the difference between a
193
- # file not existing and not having permission to access it (which
194
- # is probably a good thing).
195
- end
196
- pos += num_threads
197
- end
198
- ftp.close
199
- end
200
- end
201
- threads.each { |thread| thread.join }
202
- all_packages.flatten.sort
203
- end
204
-
205
95
  # Determine whether we need to update something. Returns nil to indicate
206
96
  # "don't know".
207
97
  def path_up_to_date?(url, path)
208
98
  return false if ! File.exist? path
209
99
 
210
- uri = Fig::URL.parse(url)
211
- case uri.scheme
212
- when 'ftp'
213
- begin
214
- ftp = Net::FTP.new(uri.host)
215
- ftp_login(ftp, uri.host)
216
-
217
- if ftp.mtime(uri.path) <= File.mtime(path)
218
- return true
219
- end
220
-
221
- return false
222
- rescue Net::FTPPermError => error
223
- Fig::Logging.debug error.message
224
- raise Fig::FileNotFoundError.new error.message, url
225
- rescue SocketError => error
226
- Fig::Logging.debug error.message
227
- raise Fig::FileNotFoundError.new error.message, url
228
- end
229
- when 'http'
230
- return nil # Not implemented
231
- when 'ssh'
232
- when 'file'
233
- begin
234
- unescaped_path = CGI.unescape uri.path
235
- if File.mtime(unescaped_path) <= File.mtime(path)
236
- return true
237
- end
238
-
239
- return false
240
- rescue Errno::ENOENT => error
241
- raise Fig::FileNotFoundError.new error.message, url
242
- end
243
- else
244
- raise_unknown_protocol(url)
245
- end
100
+ protocol, uri = decode_protocol url
101
+ return protocol.path_up_to_date? uri, path
246
102
  end
247
103
 
248
104
  # Returns whether the file was not downloaded because the file already
249
105
  # exists and is already up-to-date.
250
106
  def download(url, path)
251
- FileUtils.mkdir_p(File.dirname(path))
252
- uri = Fig::URL.parse(url)
253
- case uri.scheme
254
- when 'ftp'
255
- begin
256
- ftp = Net::FTP.new(uri.host)
257
- ftp_login(ftp, uri.host)
258
-
259
- if File.exist?(path) && ftp.mtime(uri.path) <= File.mtime(path)
260
- Fig::Logging.debug "#{path} is up to date."
261
- return false
262
- else
263
- log_download(url, path)
264
- ftp.getbinaryfile(uri.path, path, 256*1024)
265
- return true
266
- end
267
- rescue Net::FTPPermError => error
268
- Fig::Logging.debug error.message
269
- raise Fig::FileNotFoundError.new error.message, url
270
- rescue SocketError => error
271
- Fig::Logging.debug error.message
272
- raise Fig::FileNotFoundError.new error.message, url
273
- rescue Errno::ETIMEDOUT => error
274
- Fig::Logging.debug error.message
275
- raise Fig::FileNotFoundError.new error.message, url
276
- end
277
- when 'http'
278
- log_download(url, path)
279
- File.open(path, 'wb') do |file|
280
- file.binmode
281
-
282
- begin
283
- download_via_http_get(url, file)
284
- rescue SystemCallError => error
285
- Fig::Logging.debug error.message
286
- raise Fig::FileNotFoundError.new error.message, url
287
- rescue SocketError => error
288
- Fig::Logging.debug error.message
289
- raise Fig::FileNotFoundError.new error.message, url
290
- end
291
- end
292
- when 'ssh'
293
- # TODO need better way to do conditional download
294
- timestamp = File.exist?(path) ? File.mtime(path).to_i : 0
295
- # Requires that remote installation of fig be at the same location as the local machine.
296
- command = `which fig-download`.strip + " #{timestamp} #{uri.path}"
297
- log_download(url, path)
298
- ssh_download(uri.user, uri.host, path, command)
299
- when 'file'
300
- begin
301
- unescaped_path = CGI.unescape uri.path
302
- FileUtils.cp(unescaped_path, path)
303
- return true
304
- rescue Errno::ENOENT => error
305
- raise Fig::FileNotFoundError.new error.message, url
306
- end
307
- else
308
- raise_unknown_protocol(url)
309
- end
107
+ protocol, uri = decode_protocol url
108
+
109
+ FileUtils.mkdir_p(File.dirname path)
110
+
111
+ return protocol.download uri, path
310
112
  end
311
113
 
312
114
  # Returns the basename and full path to the download.
@@ -343,43 +145,12 @@ class Fig::OperatingSystem
343
145
 
344
146
  def upload(local_file, remote_file)
345
147
  Fig::Logging.debug "Uploading #{local_file} to #{remote_file}."
346
- uri = Fig::URL.parse(remote_file)
347
- case uri.scheme
348
- when 'ssh'
349
- ssh_upload(uri.user, uri.host, local_file, remote_file)
350
- when 'ftp'
351
- # fail unless system "curl -T #{local_file} --create-dirs --ftp-create-dirs #{remote_file}"
352
- require 'net/ftp'
353
- ftp_uri = Fig::URL.parse(ENV['FIG_REMOTE_URL'])
354
- ftp_root_path = ftp_uri.path
355
- ftp_root_dirs = ftp_uri.path.split('/')
356
- remote_publish_path = uri.path[0, uri.path.rindex('/')]
357
- remote_publish_dirs = remote_publish_path.split('/')
358
- # Use array subtraction to deduce which project/version folder to upload
359
- # to, i.e. [1,2,3] - [2,3,4] = [1]
360
- remote_project_dirs = remote_publish_dirs - ftp_root_dirs
361
- Net::FTP.open(uri.host) do |ftp|
362
- ftp_login(ftp, uri.host)
363
- # Assume that the FIG_REMOTE_URL path exists.
364
- ftp.chdir(ftp_root_path)
365
- remote_project_dirs.each do |dir|
366
- # Can't automatically create parent directories, so do it manually.
367
- if ftp.nlst().index(dir).nil?
368
- ftp.mkdir(dir)
369
- ftp.chdir(dir)
370
- else
371
- ftp.chdir(dir)
372
- end
373
- end
374
- ftp.putbinaryfile(local_file)
375
- end
376
- when 'file'
377
- unescaped_path = CGI.unescape uri.path
378
- FileUtils.mkdir_p(File.dirname(unescaped_path))
379
- FileUtils.cp(local_file, unescaped_path)
380
- else
381
- raise_unknown_protocol(uri)
382
- end
148
+
149
+
150
+ protocol, uri = decode_protocol remote_file
151
+ protocol.upload local_file, uri
152
+
153
+ return
383
154
  end
384
155
 
385
156
  def delete_and_recreate_directory(dir)
@@ -397,7 +168,7 @@ class Fig::OperatingSystem
397
168
  end
398
169
  else
399
170
  if ! File.exist?(target) || File.mtime(source) != File.mtime(target)
400
- log_info "#{msg} #{target}" if msg
171
+ Fig::Logging.info "#{msg} #{target}" if msg
401
172
  FileUtils.mkdir_p(File.dirname(target))
402
173
  FileUtils.cp(source, target)
403
174
  File.utime(File.atime(source), File.mtime(source), target)
@@ -409,10 +180,6 @@ class Fig::OperatingSystem
409
180
  Dir.chdir(directory) { FileUtils.mv(from, to, :force => true) }
410
181
  end
411
182
 
412
- def log_info(msg)
413
- Fig::Logging.info msg
414
- end
415
-
416
183
  # Expects files_to_archive as an Array of filenames.
417
184
  def create_archive(archive_name, files_to_archive)
418
185
  # TODO: Need to verify files_to_archive exists.
@@ -509,9 +276,13 @@ class Fig::OperatingSystem
509
276
 
510
277
  private
511
278
 
512
- SUCCESS = 0
513
- NOT_MODIFIED = 3
514
- NOT_FOUND = 4
279
+ def decode_protocol(url)
280
+ uri = Fig::URL.parse(url)
281
+ protocol = @protocols[uri.scheme]
282
+ raise_unknown_protocol(url) if protocol.nil?
283
+
284
+ return protocol, uri
285
+ end
515
286
 
516
287
  def check_archive_entry_for_windows(entry, archive_path)
517
288
  bad_type = nil
@@ -536,77 +307,6 @@ class Fig::OperatingSystem
536
307
  return
537
308
  end
538
309
 
539
- # path = The local path the file should be downloaded to.
540
- # command = The command to be run on the remote host.
541
- def ssh_download(user, host, path, command)
542
- return_code = nil
543
- tempfile = Tempfile.new('tmp')
544
- Net::SSH.start(host, user) do |ssh|
545
- ssh.open_channel do |channel|
546
- channel.exec(command)
547
- channel.on_data() { |ch, data| tempfile << data }
548
- channel.on_extended_data() { |ch, type, data| Fig::Logging.error "SSH Download ERROR: #{data}" }
549
- channel.on_request('exit-status') { |ch, request|
550
- return_code = request.read_long
551
- }
552
- end
553
- end
554
-
555
- tempfile.close()
556
-
557
- case return_code
558
- when NOT_MODIFIED
559
- tempfile.delete
560
- return false
561
- when NOT_FOUND
562
- tempfile.delete
563
- raise Fig::FileNotFoundError.new 'Remote path not found', path
564
- when SUCCESS
565
- FileUtils.mv(tempfile.path, path)
566
- return true
567
- else
568
- tempfile.delete
569
- Fig::Logging.fatal "Unable to download file #{path}: #{return_code}"
570
- raise Fig::NetworkError.new("Unable to download file #{path}: #{return_code}")
571
- end
572
- end
573
-
574
- def ssh_upload(user, host, local_file, remote_file)
575
- uri = Fig::URL.parse(remote_file)
576
- dir = uri.path[0, uri.path.rindex('/')]
577
- Net::SSH.start(host, user) do |ssh|
578
- ssh.exec!("mkdir -p #{dir}")
579
- end
580
- Net::SFTP.start(host, user) do |sftp|
581
- sftp.upload!(local_file, uri.path)
582
- end
583
- end
584
-
585
- def download_via_http_get(uri_string, file, redirection_limit = 10)
586
- if redirection_limit < 1
587
- Fig::Logging.debug 'Too many HTTP redirects.'
588
- raise Fig::FileNotFoundError.new 'Too many HTTP redirects.', uri_string
589
- end
590
-
591
- response = Net::HTTP.get_response(URI(uri_string))
592
-
593
- case response
594
- when Net::HTTPSuccess then
595
- file.write(response.body)
596
- when Net::HTTPRedirection then
597
- location = response['location']
598
- Fig::Logging.debug "Redirecting to #{location}."
599
- download_via_http_get(location, file, redirection_limit - 1)
600
- else
601
- Fig::Logging.debug "Download failed: #{response.code} #{response.message}."
602
- raise Fig::FileNotFoundError.new(
603
- "Download failed: #{response.code} #{response.message}.", uri_string
604
- )
605
- end
606
-
607
- return
608
- end
609
-
610
310
  def raise_unknown_protocol(url)
611
311
  Fig::Logging.fatal %Q<Don't know how to handle the protocol in "#{url}".>
612
312
  raise Fig::NetworkError.new(
@@ -615,8 +315,4 @@ class Fig::OperatingSystem
615
315
 
616
316
  return
617
317
  end
618
-
619
- def log_download(url, path)
620
- Fig::Logging.debug "Downloading #{url} to #{path}."
621
- end
622
318
  end