revenc 0.1.3 → 0.2.1
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.
- data/.gemfiles +53 -0
- data/.gitignore +4 -8
- data/Gemfile.lock +42 -37
- data/HISTORY.markdown +9 -2
- data/LICENSE +1 -1
- data/README.markdown +33 -38
- data/Rakefile +24 -34
- data/TODO.markdown +3 -0
- data/VERSION +1 -1
- data/bin/revenc +30 -13
- data/config/cucumber.yml +4 -3
- data/examples/rsync/encrypted_data/key/encfs6.xml +27 -27
- data/examples/rsync/revenc.conf +2 -2
- data/examples/simple/encfs6.xml +27 -27
- data/features/app.feature +17 -17
- data/features/bin.feature +6 -6
- data/features/configuration.feature +9 -9
- data/features/copy.feature +15 -12
- data/features/generator.feature +1 -1
- data/features/mount.feature +14 -14
- data/features/settings.feature +119 -0
- data/features/step_definitions/revenc_steps.rb +1 -2
- data/features/support/aruba.rb +9 -9
- data/features/support/env.rb +8 -2
- data/features/unmount.feature +8 -8
- data/lib/revenc.rb +12 -3
- data/lib/revenc/app.rb +27 -53
- data/lib/revenc/core/array.rb +11 -0
- data/lib/revenc/core/hash.rb +45 -0
- data/lib/revenc/encfs_wrapper.rb +20 -24
- data/lib/revenc/errors.rb +3 -3
- data/lib/revenc/io.rb +13 -13
- data/lib/revenc/settings.rb +98 -0
- data/revenc.gemspec +28 -17
- data/spec/aruba_helper.rb +25 -0
- data/spec/basic_app/array_spec.rb +48 -0
- data/spec/basic_gem/aruba_helper_spec.rb +33 -0
- data/spec/basic_gem/basic_gem_spec.rb +71 -1
- data/spec/basic_gem/gemspec_spec.rb +68 -0
- data/spec/revenc/error_spec.rb +2 -2
- data/spec/revenc/io_spec.rb +12 -12
- data/spec/spec_helper.rb +4 -9
- data/spec/watchr.rb +48 -26
- metadata +120 -177
- data/.yardopts +0 -6
- data/spec/spec.opts +0 -2
data/lib/revenc/encfs_wrapper.rb
CHANGED
@@ -9,21 +9,18 @@ def initialize(base_dir, options)
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def mount(source=nil, mount_point_folder=nil)
|
12
|
+
mount_point_options = @options || {}
|
13
|
+
mount_point_options = mount_point_options.merge(@options[:mount].dup) if @options[:mount]
|
12
14
|
|
13
15
|
# add params from config file if not specified
|
14
|
-
source =
|
15
|
-
mount_point_folder =
|
16
|
+
source = (mount_point_options[:source] ? mount_point_options[:source][:name] : nil) unless source
|
17
|
+
mount_point_folder = (mount_point_options[:mountpoint] ? mount_point_options[:mountpoint][:name] : nil) unless mount_point_folder
|
16
18
|
|
17
19
|
# sanity check params
|
18
20
|
raise "source folder not specified" unless source
|
19
21
|
raise "mountpoint not specified" unless mount_point_folder
|
20
22
|
|
21
|
-
|
22
|
-
mount_point_options = mount_point_options.merge(:keyfile => configatron.mount.keyfile.name)
|
23
|
-
mount_point_options = mount_point_options.merge(:cmd => configatron.mount.cmd) if configatron.mount.cmd
|
24
|
-
mount_point_options = mount_point_options.merge(:executable => configatron.mount.executable) if configatron.mount.executable
|
25
|
-
|
26
|
-
mount_point = MountPoint.new(mount_point_folder, source, mount_point_options)
|
23
|
+
mount_point = MountPoint.new(mount_point_folder, source, mount_point_options.merge(@options))
|
27
24
|
|
28
25
|
if @options[:verbose]
|
29
26
|
puts "mount: source=#{mount_point.source.name}".cyan
|
@@ -38,18 +35,18 @@ def mount(source=nil, mount_point_folder=nil)
|
|
38
35
|
end
|
39
36
|
|
40
37
|
def unmount(foldername = nil)
|
41
|
-
|
38
|
+
unmount_point_options = @options || {}
|
39
|
+
unmount_point_options = unmount_point_options.merge(@options[:unmount].dup) if @options[:unmount]
|
40
|
+
mount_point_options = @options[:mount] ? @options[:mount].dup : {}
|
41
|
+
|
42
42
|
# add param from config file if not specified, try specific unmount
|
43
|
-
foldername =
|
43
|
+
foldername = (unmount_point_options[:mountpoint] ? unmount_point_options[:mountpoint][:name] : nil) unless foldername
|
44
44
|
# fallback to mount.mountpoint if specified
|
45
|
-
foldername =
|
46
|
-
|
45
|
+
foldername = (mount_point_options[:mountpoint] ? mount_point_options[:mountpoint][:name] : nil) unless foldername
|
46
|
+
|
47
47
|
# sanity check params
|
48
48
|
raise "mountpoint not specified" unless foldername
|
49
49
|
|
50
|
-
unmount_point_options = @options || {}
|
51
|
-
unmount_point_options = unmount_point_options.merge(:cmd => configatron.unmount.cmd) if configatron.umount.cmd
|
52
|
-
unmount_point_options = unmount_point_options.merge(:executable => configatron.unmount.executable) if configatron.umount.executable
|
53
50
|
unmount_point = UnmountPoint.new(foldername, unmount_point_options)
|
54
51
|
|
55
52
|
if @options[:verbose]
|
@@ -62,23 +59,22 @@ def unmount(foldername = nil)
|
|
62
59
|
end
|
63
60
|
|
64
61
|
def copy(source=nil, destination=nil)
|
62
|
+
copy_options = @options || {}
|
63
|
+
copy_options = copy_options.merge(@options[:copy].dup) if @options[:copy]
|
64
|
+
mount_point_options = @options[:mount] ? @options[:mount].dup : {}
|
65
65
|
|
66
66
|
# add params from config file if not specified
|
67
|
-
source =
|
67
|
+
source = (copy_options[:source] ? copy_options[:source][:name] : nil) unless source
|
68
68
|
# fallback
|
69
|
-
source =
|
70
|
-
destination =
|
69
|
+
source = (mount_point_options[:mountpoint] ? mount_point_options[:mountpoint][:name] : nil) unless source
|
70
|
+
destination = (copy_options[:destination] ? copy_options[:destination][:name] : nil) unless destination
|
71
71
|
|
72
72
|
# sanity check params
|
73
73
|
raise "source folder not specified" unless source
|
74
74
|
raise "destination not specified" unless destination
|
75
75
|
|
76
|
-
copy_options =
|
77
|
-
|
78
|
-
copy_options = copy_options.merge(:executable => configatron.copy.executable) if configatron.copy.executable
|
79
|
-
copy_options = copy_options.merge(:mountpoint => configatron.mount.mountpoint.name) if configatron.mount.mountpoint.name
|
80
|
-
|
81
|
-
copy_folder = CopySourceFolder.new( source, destination, copy_options)
|
76
|
+
copy_options = copy_options.merge(:mountpoint => mount_point_options[:mountpoint][:name]) if mount_point_options[:mountpoint]
|
77
|
+
copy_folder = CopySourceFolder.new(source, destination, copy_options)
|
82
78
|
|
83
79
|
if @options[:verbose]
|
84
80
|
puts "copy: source=#{copy_folder.name}".cyan
|
data/lib/revenc/errors.rb
CHANGED
@@ -20,7 +20,7 @@ def add(error_on, message = "Unknown error")
|
|
20
20
|
error_on_str = error_on_str.gsub(/_/, ' ')
|
21
21
|
error_on_str = error_on_str.gsub(/^revenc/, '').strip
|
22
22
|
#error_on_str = error_on_str.capitalize
|
23
|
-
|
23
|
+
|
24
24
|
@errors[error_on_str] ||= []
|
25
25
|
@errors[error_on_str] << message.to_s
|
26
26
|
end
|
@@ -28,7 +28,7 @@ def add(error_on, message = "Unknown error")
|
|
28
28
|
def empty?
|
29
29
|
@errors.empty?
|
30
30
|
end
|
31
|
-
|
31
|
+
|
32
32
|
def clear
|
33
33
|
@errors = {}
|
34
34
|
end
|
@@ -40,7 +40,7 @@ def each
|
|
40
40
|
def size
|
41
41
|
@errors.values.inject(0) { |error_count, attribute| error_count + attribute.size }
|
42
42
|
end
|
43
|
-
|
43
|
+
|
44
44
|
alias_method :count, :size
|
45
45
|
alias_method :length, :size
|
46
46
|
|
data/lib/revenc/io.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'mutagem'
|
2
|
+
require 'erb'
|
2
3
|
|
3
4
|
module Revenc
|
4
5
|
|
@@ -61,7 +62,6 @@ def system_cmd(cmd=nil)
|
|
61
62
|
system cmd
|
62
63
|
end
|
63
64
|
|
64
|
-
# Runs the YAML file through ERB
|
65
65
|
def render(value, b = binding)
|
66
66
|
ERB.new(value).result(b)
|
67
67
|
end
|
@@ -91,7 +91,7 @@ def empty?
|
|
91
91
|
contents = nil
|
92
92
|
File.open(@name, "r") do |f|
|
93
93
|
contents = f.read
|
94
|
-
end
|
94
|
+
end
|
95
95
|
contents.empty?
|
96
96
|
end
|
97
97
|
|
@@ -108,7 +108,7 @@ def initialize(name='passphrase', options={})
|
|
108
108
|
end
|
109
109
|
|
110
110
|
def validate
|
111
|
-
super
|
111
|
+
super
|
112
112
|
errors.add(self, "is empty") if empty?
|
113
113
|
end
|
114
114
|
end
|
@@ -120,7 +120,7 @@ def initialize(name='encfs6.xml', options={})
|
|
120
120
|
end
|
121
121
|
|
122
122
|
def validate
|
123
|
-
super
|
123
|
+
super
|
124
124
|
errors.add(self, "is empty") if exists? && empty?
|
125
125
|
end
|
126
126
|
end
|
@@ -155,8 +155,8 @@ class ActionFolder < FileFolder
|
|
155
155
|
|
156
156
|
def initialize(name=nil, options={})
|
157
157
|
super
|
158
|
-
@passphrasefile = PassphraseFile.new(options[:passphrasefile])
|
159
|
-
@keyfile = KeyFile.new(options[:keyfile])
|
158
|
+
@passphrasefile = PassphraseFile.new(options[:passphrasefile] ? options[:passphrasefile][:name] : nil)
|
159
|
+
@keyfile = KeyFile.new(options[:keyfile] ? options[:keyfile][:name] : nil)
|
160
160
|
@cmd = options[:cmd]
|
161
161
|
@executable = options[:executable]
|
162
162
|
end
|
@@ -177,17 +177,17 @@ def executable
|
|
177
177
|
# run the action if valid and return true if successful
|
178
178
|
def execute
|
179
179
|
raise errors.to_sentences unless valid?
|
180
|
-
|
180
|
+
|
181
181
|
# default failing command
|
182
182
|
result = false
|
183
|
-
|
183
|
+
|
184
184
|
# protect command from recursion
|
185
185
|
mutex = Mutagem::Mutex.new('revenc.lck')
|
186
|
-
|
186
|
+
lock_successful = mutex.execute do
|
187
187
|
result = system_cmd(cmd)
|
188
188
|
end
|
189
|
-
|
190
|
-
raise "action failed, lock file present" unless
|
189
|
+
|
190
|
+
raise "action failed, lock file present" unless lock_successful
|
191
191
|
result
|
192
192
|
end
|
193
193
|
end
|
@@ -225,7 +225,7 @@ def initialize(name=nil, options={})
|
|
225
225
|
@cmd = options[:cmd] || "<%= executable %> -u <%= mountpoint.name %>"
|
226
226
|
@executable = options[:executable] || 'fusermount'
|
227
227
|
end
|
228
|
-
|
228
|
+
|
229
229
|
# allow clarity in config files, instead of <%= name %> you can use <%= mountpoint.name %>
|
230
230
|
def mountpoint
|
231
231
|
self
|
@@ -247,7 +247,7 @@ def initialize(name=nil, destination_name=nil, options={})
|
|
247
247
|
@cmd = options[:cmd] || "<%= executable %> -r <%= source.name %> <%= destination.name %>"
|
248
248
|
@executable = options[:executable] || 'cp'
|
249
249
|
end
|
250
|
-
|
250
|
+
|
251
251
|
# allow clarity in config files, instead of <%= name %> you can use <%= source.name %>
|
252
252
|
def source
|
253
253
|
self
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Revenc
|
4
|
+
|
5
|
+
class Settings
|
6
|
+
|
7
|
+
def initialize(working_dir, options={})
|
8
|
+
@working_dir = working_dir
|
9
|
+
@options = options
|
10
|
+
configure
|
11
|
+
end
|
12
|
+
|
13
|
+
def options
|
14
|
+
@options
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
# read options from YAML config
|
20
|
+
def configure
|
21
|
+
|
22
|
+
# config file default options
|
23
|
+
configuration = {
|
24
|
+
:options => {
|
25
|
+
:verbose => false,
|
26
|
+
:coloring => 'AUTO'
|
27
|
+
},
|
28
|
+
:mount => {
|
29
|
+
:source => {
|
30
|
+
:name => nil
|
31
|
+
},
|
32
|
+
:mountpoint => {
|
33
|
+
:name => nil
|
34
|
+
},
|
35
|
+
:passphrasefile => {
|
36
|
+
:name => 'passphrase'
|
37
|
+
},
|
38
|
+
:keyfile => {
|
39
|
+
:name => 'encfs6.xml'
|
40
|
+
},
|
41
|
+
:cmd => nil,
|
42
|
+
:executable => nil
|
43
|
+
},
|
44
|
+
:unmount => {
|
45
|
+
:mountpoint => {
|
46
|
+
:name => nil
|
47
|
+
},
|
48
|
+
:cmd => nil,
|
49
|
+
:executable => nil
|
50
|
+
},
|
51
|
+
:copy => {
|
52
|
+
:source => {
|
53
|
+
:name => nil
|
54
|
+
},
|
55
|
+
:destination => {
|
56
|
+
:name => nil
|
57
|
+
},
|
58
|
+
:cmd => nil,
|
59
|
+
:executable => nil
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
# set default config if not given on command line
|
64
|
+
config = @options[:config]
|
65
|
+
unless config
|
66
|
+
config = [
|
67
|
+
File.join(@working_dir, "revenc.conf"),
|
68
|
+
File.join(@working_dir, ".revenc.conf"),
|
69
|
+
File.join(@working_dir, "config", "revenc.conf"),
|
70
|
+
File.expand_path(File.join("~", ".revenc.conf"))
|
71
|
+
].detect { |filename| File.exists?(filename) }
|
72
|
+
end
|
73
|
+
|
74
|
+
if config && File.exists?(config)
|
75
|
+
# rewrite options full path for config for later use
|
76
|
+
@options[:config] = config
|
77
|
+
|
78
|
+
# load options from the config file, overwriting hard-coded defaults
|
79
|
+
config_contents = YAML::load(File.open(config))
|
80
|
+
configuration.merge!(config_contents.symbolize_keys!) if config_contents && config_contents.is_a?(Hash)
|
81
|
+
else
|
82
|
+
# user specified a config file?, no error if user did not specify config file
|
83
|
+
raise "config file not found" if @options[:config]
|
84
|
+
end
|
85
|
+
|
86
|
+
# the command line options override options read from the config file
|
87
|
+
@options = configuration[:options].merge!(@options)
|
88
|
+
@options.symbolize_keys!
|
89
|
+
|
90
|
+
# mount, unmount and copy configuration hashes
|
91
|
+
@options[:mount] = configuration[:mount].recursively_symbolize_keys! if configuration[:mount]
|
92
|
+
@options[:unmount] = configuration[:unmount].recursively_symbolize_keys! if configuration[:unmount]
|
93
|
+
@options[:copy] = configuration[:copy].recursively_symbolize_keys! if configuration[:copy]
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
data/revenc.gemspec
CHANGED
@@ -1,10 +1,25 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
2
|
#
|
3
3
|
#
|
4
|
-
|
5
4
|
Gem::Specification.new do |s|
|
5
|
+
|
6
|
+
# avoid shelling out to run git every time the gemspec is evaluated
|
7
|
+
#
|
8
|
+
# @see spec/gemspec_spec.rb
|
9
|
+
#
|
10
|
+
gemfiles_cache = File.join(File.dirname(__FILE__), '.gemfiles')
|
11
|
+
if File.exists?(gemfiles_cache)
|
12
|
+
gemfiles = File.open(gemfiles_cache, "r") {|f| f.read}
|
13
|
+
# normalize EOL
|
14
|
+
gemfiles.gsub!(/\r\n/, "\n")
|
15
|
+
else
|
16
|
+
# .gemfiles missing, run 'rake gemfiles' to create it
|
17
|
+
# falling back to 'git ls-files'"
|
18
|
+
gemfiles = `git ls-files`
|
19
|
+
end
|
20
|
+
|
6
21
|
s.name = "revenc"
|
7
|
-
s.version = File.open(File.join(File.dirname(__FILE__),
|
22
|
+
s.version = File.open(File.join(File.dirname(__FILE__), 'VERSION'), "r") { |f| f.read }
|
8
23
|
s.platform = Gem::Platform::RUBY
|
9
24
|
s.authors = ["Robert Wahler"]
|
10
25
|
s.email = ["robert@gearheadforhire.com"]
|
@@ -17,26 +32,22 @@ Gem::Specification.new do |s|
|
|
17
32
|
|
18
33
|
s.add_dependency 'mutagem', '>= 0.1.3'
|
19
34
|
s.add_dependency 'term-ansicolor', '>= 1.0.4'
|
20
|
-
s.add_dependency 'configatron', '>= 2.5.1'
|
21
35
|
|
22
|
-
s.add_development_dependency "bundler", ">= 1.0.
|
23
|
-
s.add_development_dependency "rspec", ">=
|
24
|
-
s.add_development_dependency "cucumber", "
|
25
|
-
s.add_development_dependency "aruba", "
|
36
|
+
s.add_development_dependency "bundler", ">= 1.0.14"
|
37
|
+
s.add_development_dependency "rspec", ">= 2.6.0"
|
38
|
+
s.add_development_dependency "cucumber", "~> 1.0"
|
39
|
+
s.add_development_dependency "aruba", "~> 0.4.2"
|
26
40
|
s.add_development_dependency "rake", ">= 0.8.7"
|
27
|
-
s.add_development_dependency "yard", ">= 0.6.1"
|
28
|
-
s.add_development_dependency "rdiscount", ">= 1.6.5"
|
29
41
|
|
30
|
-
s.files =
|
31
|
-
s.executables =
|
32
|
-
s.
|
33
|
-
s.require_path = 'lib'
|
42
|
+
s.files = gemfiles.split("\n")
|
43
|
+
s.executables = gemfiles.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
|
44
|
+
s.require_paths = ["lib"]
|
34
45
|
|
35
46
|
s.has_rdoc = 'yard'
|
36
|
-
s.rdoc_options = [
|
37
|
-
'--title', 'Revenc Documentation',
|
38
|
-
'--main', 'README.markdown',
|
47
|
+
s.rdoc_options = [
|
48
|
+
'--title', 'Revenc Documentation',
|
49
|
+
'--main', 'README.markdown',
|
39
50
|
'--line-numbers',
|
40
|
-
'--inline-source'
|
51
|
+
'--inline-source'
|
41
52
|
]
|
42
53
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Aruba
|
2
|
+
module Api
|
3
|
+
|
4
|
+
# @return full path to files in the aruba tmp folder
|
5
|
+
def fullpath(filename)
|
6
|
+
path = File.expand_path(File.join(current_dir, filename))
|
7
|
+
if path.match(/^\/cygdrive/)
|
8
|
+
# match /cygdrive/c/path/to and return c:\\path\\to
|
9
|
+
path = `cygpath -w #{path}`.chomp
|
10
|
+
elsif path.match(/.\:/)
|
11
|
+
# match c:/path/to and return c:\\path\\to
|
12
|
+
path = path.gsub(/\//, '\\')
|
13
|
+
end
|
14
|
+
path
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return the contents of "filename" in the aruba tmp folder
|
18
|
+
def get_file_contents(filename)
|
19
|
+
in_current_dir do
|
20
|
+
IO.read(filename)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Array do
|
4
|
+
|
5
|
+
describe 'recursively_symbolize_keys!' do
|
6
|
+
|
7
|
+
it "should recursively convert a hash with string keys to a hash with symbol keys" do
|
8
|
+
hash_symbols = {
|
9
|
+
:options => {
|
10
|
+
:verbose => false,
|
11
|
+
},
|
12
|
+
:repos => {
|
13
|
+
:repo1 => {:path => "something"}
|
14
|
+
}
|
15
|
+
}
|
16
|
+
|
17
|
+
hash_strings = {
|
18
|
+
'options' => {
|
19
|
+
'verbose' => false,
|
20
|
+
},
|
21
|
+
'repos' => {
|
22
|
+
'repo1' => {'path' => "something"}
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
hash_symbols.should == hash_strings.recursively_symbolize_keys!
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should should handle hashes that are already symbolized" do
|
30
|
+
hash_symbols = {
|
31
|
+
:options => {
|
32
|
+
:verbose => false,
|
33
|
+
},
|
34
|
+
:repos => {
|
35
|
+
:repo1 => {:path => "something"}
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
hash_copy = hash_symbols.dup
|
40
|
+
|
41
|
+
hash_copy.should == hash_symbols.recursively_symbolize_keys!
|
42
|
+
hash_symbols[:repos][:repo1].should == {:path => "something"}
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Revenc do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@filename = 'input.txt'
|
7
|
+
write_file(@filename, "the quick brown fox")
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'Aruba::API.current_dir' do
|
11
|
+
|
12
|
+
it "should return the current dir as 'tmp/aruba'" do
|
13
|
+
current_dir.should match(/^tmp\/aruba$/)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "aruba_helper fullpath('input.txt')" do
|
18
|
+
|
19
|
+
it "should return a valid expanded path to 'input.txt'" do
|
20
|
+
path = fullpath('input.txt')
|
21
|
+
path.should match(/tmp..*aruba/)
|
22
|
+
File.exists?(path).should == true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "aruba_helper get_file_contents('input.txt')" do
|
27
|
+
|
28
|
+
it "should return the contents of 'input.txt' as a String" do
|
29
|
+
get_file_contents('input.txt').should == 'the quick brown fox'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|