revenc 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitattributes +1 -0
- data/.gitignore +30 -0
- data/CLONING.rdoc +108 -0
- data/LICENSE +20 -0
- data/README.rdoc +198 -0
- data/Rakefile +64 -0
- data/VERSION +1 -0
- data/bin/revenc +81 -0
- data/config/cucumber.yml +7 -0
- data/examples/rsync/encrypted_data/key/encfs6.xml +35 -0
- data/examples/rsync/revenc.conf +18 -0
- data/examples/rsync/scripts/passphrase +1 -0
- data/examples/rsync/unencrypted_data/test_file1.txt +1 -0
- data/examples/rsync/unencrypted_data/test_file2.txt +1 -0
- data/examples/simple/encfs6.xml +35 -0
- data/examples/simple/passphrase +1 -0
- data/examples/simple/unencrypted_data/test_file1.txt +1 -0
- data/examples/simple/unencrypted_data/test_file2.txt +1 -0
- data/features/app.feature +59 -0
- data/features/bin.feature +35 -0
- data/features/configuration.feature +98 -0
- data/features/copy.feature +169 -0
- data/features/generator.feature +15 -0
- data/features/mount.feature +133 -0
- data/features/step_definitions/.gitignore +0 -0
- data/features/step_definitions/revenc_steps.rb +64 -0
- data/features/support/aruba.rb +21 -0
- data/features/support/env.rb +4 -0
- data/features/support/hooks.rb +6 -0
- data/features/unmount.feature +58 -0
- data/lib/revenc/app.rb +128 -0
- data/lib/revenc/encfs_wrapper.rb +96 -0
- data/lib/revenc/errors.rb +78 -0
- data/lib/revenc/io.rb +265 -0
- data/lib/revenc/lockfile.rb +66 -0
- data/lib/revenc.rb +22 -0
- data/spec/revenc/error_spec.rb +50 -0
- data/spec/revenc/io_spec.rb +185 -0
- data/spec/revenc/lockfile_spec.rb +44 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/watchr.rb +142 -0
- metadata +179 -0
data/lib/revenc/app.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'configatron'
|
2
|
+
require 'term/ansicolor'
|
3
|
+
|
4
|
+
class String
|
5
|
+
include Term::ANSIColor
|
6
|
+
end
|
7
|
+
|
8
|
+
module Revenc
|
9
|
+
|
10
|
+
AVAILABLE_ACTIONS = %w[mount unmount copy]
|
11
|
+
|
12
|
+
class App
|
13
|
+
|
14
|
+
def initialize(base_dir, options={})
|
15
|
+
@base_dir = base_dir
|
16
|
+
@options = options
|
17
|
+
if @options[:verbose]
|
18
|
+
puts "base_dir: #{@base_dir}".cyan
|
19
|
+
puts "options: #{@options.inspect}".cyan
|
20
|
+
end
|
21
|
+
configure(options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def run
|
25
|
+
begin
|
26
|
+
|
27
|
+
if action_argument_required?
|
28
|
+
action = ARGV.shift
|
29
|
+
unless AVAILABLE_ACTIONS.include?(action)
|
30
|
+
if action.nil?
|
31
|
+
puts "revenc action required"
|
32
|
+
else
|
33
|
+
puts "revenc invalid action: #{action}"
|
34
|
+
end
|
35
|
+
puts "revenc --help for more information"
|
36
|
+
exit 1
|
37
|
+
end
|
38
|
+
puts "revenc run action: #{action}".cyan if @options[:verbose]
|
39
|
+
raise "action #{action} not implemented" unless respond_to?(action)
|
40
|
+
result = send(action)
|
41
|
+
else
|
42
|
+
#
|
43
|
+
# default action if action_argument_required? is false
|
44
|
+
#
|
45
|
+
result = 0
|
46
|
+
end
|
47
|
+
|
48
|
+
exit(result ? 0 : 1)
|
49
|
+
|
50
|
+
rescue SystemExit => e
|
51
|
+
# This is the normal exit point, exit code from the send result
|
52
|
+
# or exit from another point in the system
|
53
|
+
puts "revenc run system exit: #{e}, status code: #{e.status}".green if @options[:verbose]
|
54
|
+
exit(e.status)
|
55
|
+
rescue Exception => e
|
56
|
+
STDERR.puts("revenc command failed, error(s) follow:")
|
57
|
+
STDERR.puts("#{e.message}".red)
|
58
|
+
STDERR.puts(e.backtrace.join("\n")) if @options[:verbose]
|
59
|
+
exit(1)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
#
|
64
|
+
# app commands start
|
65
|
+
#
|
66
|
+
# TODO: Add status command, use encfsctl
|
67
|
+
|
68
|
+
def mount
|
69
|
+
EncfsWrapper.new(@base_dir, @options).mount(ARGV.shift, ARGV.shift)
|
70
|
+
end
|
71
|
+
|
72
|
+
def unmount
|
73
|
+
EncfsWrapper.new(@base_dir, @options).unmount(ARGV.shift)
|
74
|
+
end
|
75
|
+
|
76
|
+
def copy
|
77
|
+
EncfsWrapper.new(@base_dir, @options).copy(ARGV.shift, ARGV.shift)
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# app commands end
|
82
|
+
#
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
# true if application requires an action to be specified on the command line
|
87
|
+
def action_argument_required?
|
88
|
+
!AVAILABLE_ACTIONS.empty?
|
89
|
+
end
|
90
|
+
|
91
|
+
# read options for YAML config with ERB processing and initialize configatron
|
92
|
+
def configure(options)
|
93
|
+
# TODO: read ~/.revenc.conf before looking in the current folder for revenc.conf, read BOTH files
|
94
|
+
config = @options[:config]
|
95
|
+
config = File.join(@base_dir, 'revenc.conf') unless config
|
96
|
+
if File.exists?(config)
|
97
|
+
# load configatron options from the config file
|
98
|
+
puts "loading config file: #{config}".cyan if @options[:verbose]
|
99
|
+
configatron.configure_from_yaml(config)
|
100
|
+
else
|
101
|
+
# user specified a config file?
|
102
|
+
raise "config file not found" if @options[:config]
|
103
|
+
# no error if user did not specify config file
|
104
|
+
puts "#{config} not found".yellow if @options[:verbose]
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# set defaults, these will NOT override setting read from YAML
|
109
|
+
#
|
110
|
+
configatron.mount.source.set_default(:name, nil)
|
111
|
+
configatron.mount.mountpoint.set_default(:name, nil)
|
112
|
+
configatron.mount.passphrasefile.set_default(:name, 'passphrase')
|
113
|
+
configatron.mount.keyfile.set_default(:name, 'encfs6.xml')
|
114
|
+
configatron.mount.set_default(:cmd, nil)
|
115
|
+
configatron.mount.set_default(:executable, nil)
|
116
|
+
|
117
|
+
configatron.unmount.mountpoint.set_default(:name, nil)
|
118
|
+
configatron.unmount.set_default(:cmd, nil)
|
119
|
+
configatron.unmount.set_default(:executable, nil)
|
120
|
+
|
121
|
+
configatron.copy.source.set_default(:name, nil)
|
122
|
+
configatron.copy.destination.set_default(:name, nil)
|
123
|
+
configatron.copy.set_default(:cmd, nil)
|
124
|
+
configatron.copy.set_default(:executable, nil)
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Revenc
|
2
|
+
|
3
|
+
class EncfsWrapper
|
4
|
+
|
5
|
+
def initialize(base_dir, options)
|
6
|
+
raise ArgumentError, "Options should be a hash" unless options.is_a?(Hash)
|
7
|
+
@base_dir = base_dir
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def mount(source=nil, mount_point_folder=nil)
|
12
|
+
|
13
|
+
# add params from config file if not specified
|
14
|
+
source = configatron.mount.source.name unless source
|
15
|
+
mount_point_folder = configatron.mount.mountpoint.name unless mount_point_folder
|
16
|
+
|
17
|
+
# sanity check params
|
18
|
+
raise "source folder not specified" unless source
|
19
|
+
raise "mountpoint not specified" unless mount_point_folder
|
20
|
+
|
21
|
+
mount_point_options = @options.merge(:passphrasefile => configatron.mount.passphrasefile.name)
|
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)
|
27
|
+
|
28
|
+
if @options[:verbose]
|
29
|
+
puts "mount: source=#{mount_point.source.name}".cyan
|
30
|
+
puts "mount: mountpoint=#{mount_point.name}".cyan
|
31
|
+
puts "mount: passphrasefile=#{mount_point.passphrasefile.name}".cyan
|
32
|
+
puts "mount: keyfile=#{mount_point.keyfile.name}".cyan
|
33
|
+
puts "mount: cmd=#{mount_point.cmd}".cyan
|
34
|
+
puts "mount: executable=#{mount_point.executable}".cyan
|
35
|
+
end
|
36
|
+
|
37
|
+
mount_point.execute
|
38
|
+
end
|
39
|
+
|
40
|
+
def unmount(foldername = nil)
|
41
|
+
|
42
|
+
# add param from config file if not specified, try specific unmount
|
43
|
+
foldername = configatron.unmount.mountpoint.name unless foldername
|
44
|
+
# fallback to mount.mountpoint if specified
|
45
|
+
foldername = configatron.mount.mountpoint.name unless foldername
|
46
|
+
|
47
|
+
# sanity check params
|
48
|
+
raise "mountpoint not specified" unless foldername
|
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
|
+
unmount_point = UnmountPoint.new(foldername, unmount_point_options)
|
54
|
+
|
55
|
+
if @options[:verbose]
|
56
|
+
puts "unmount: mountpoint=#{unmount_point.mountpoint.name}".cyan
|
57
|
+
puts "unmount: cmd=#{unmount_point.cmd}".cyan
|
58
|
+
puts "unmount: executable=#{unmount_point.executable}".cyan
|
59
|
+
end
|
60
|
+
|
61
|
+
unmount_point.execute
|
62
|
+
end
|
63
|
+
|
64
|
+
def copy(source=nil, destination=nil)
|
65
|
+
|
66
|
+
# add params from config file if not specified
|
67
|
+
source = configatron.copy.source.name unless source
|
68
|
+
# fallback
|
69
|
+
source = configatron.mount.mountpoint.name unless source
|
70
|
+
destination = configatron.copy.destination.name unless destination
|
71
|
+
|
72
|
+
# sanity check params
|
73
|
+
raise "source folder not specified" unless source
|
74
|
+
raise "destination not specified" unless destination
|
75
|
+
|
76
|
+
copy_options = @options || {}
|
77
|
+
copy_options = copy_options.merge(:cmd => configatron.copy.cmd) if configatron.copy.cmd
|
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)
|
82
|
+
|
83
|
+
if @options[:verbose]
|
84
|
+
puts "copy: source=#{copy_folder.name}".cyan
|
85
|
+
puts "copy: destination=#{copy_folder.destination.name}".cyan
|
86
|
+
puts "copy: cmd=#{copy_folder.cmd}".cyan
|
87
|
+
puts "copy: executable=#{copy_folder.executable}".cyan
|
88
|
+
end
|
89
|
+
|
90
|
+
copy_folder.execute
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Revenc
|
2
|
+
|
3
|
+
class Errors
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@errors ={}
|
8
|
+
end
|
9
|
+
|
10
|
+
# add errors, error_on can be a symbol or object instance
|
11
|
+
def add(error_on, message = "Unknown error")
|
12
|
+
|
13
|
+
# humanize error_on
|
14
|
+
if error_on.is_a?(Symbol)
|
15
|
+
error_on_str = error_on.to_s
|
16
|
+
else
|
17
|
+
error_on_str = underscore(error_on.class.name)
|
18
|
+
end
|
19
|
+
error_on_str = error_on_str.gsub(/\//, '_')
|
20
|
+
error_on_str = error_on_str.gsub(/_/, ' ')
|
21
|
+
error_on_str = error_on_str.gsub(/^revenc/, '').strip
|
22
|
+
#error_on_str = error_on_str.capitalize
|
23
|
+
|
24
|
+
@errors[error_on_str] ||= []
|
25
|
+
@errors[error_on_str] << message.to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
def empty?
|
29
|
+
@errors.empty?
|
30
|
+
end
|
31
|
+
|
32
|
+
def clear
|
33
|
+
@errors = {}
|
34
|
+
end
|
35
|
+
|
36
|
+
def each
|
37
|
+
@errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
|
38
|
+
end
|
39
|
+
|
40
|
+
def size
|
41
|
+
@errors.values.inject(0) { |error_count, attribute| error_count + attribute.size }
|
42
|
+
end
|
43
|
+
|
44
|
+
alias_method :count, :size
|
45
|
+
alias_method :length, :size
|
46
|
+
|
47
|
+
def messages
|
48
|
+
messages = []
|
49
|
+
|
50
|
+
@errors.each_key do |attr|
|
51
|
+
@errors[attr].each do |message|
|
52
|
+
next unless message
|
53
|
+
attr_name = attr.to_s
|
54
|
+
messages << attr_name + ' ' + message
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
messages
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_sentences
|
62
|
+
messages.join("\n")
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# from ActiveSupport
|
68
|
+
def underscore(camel_cased_word)
|
69
|
+
camel_cased_word.to_s.gsub(/::/, '/').
|
70
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
71
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
72
|
+
tr("-", "_").
|
73
|
+
downcase
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
data/lib/revenc/io.rb
ADDED
@@ -0,0 +1,265 @@
|
|
1
|
+
module Revenc
|
2
|
+
|
3
|
+
class BasicAction
|
4
|
+
|
5
|
+
def initialize(name=nil, options={})
|
6
|
+
raise ArgumentError, "Options should be a hash" unless options.is_a?(Hash)
|
7
|
+
@name = name
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def name
|
12
|
+
@name
|
13
|
+
end
|
14
|
+
|
15
|
+
def name=(value)
|
16
|
+
@name = value
|
17
|
+
end
|
18
|
+
|
19
|
+
def options
|
20
|
+
@options ||= {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def options=(value)
|
24
|
+
@options = value
|
25
|
+
end
|
26
|
+
|
27
|
+
def errors
|
28
|
+
@errors ||= Errors.new
|
29
|
+
end
|
30
|
+
|
31
|
+
def exists?
|
32
|
+
false
|
33
|
+
end
|
34
|
+
|
35
|
+
def validate
|
36
|
+
end
|
37
|
+
|
38
|
+
def valid?
|
39
|
+
errors.clear
|
40
|
+
validate
|
41
|
+
errors.empty?
|
42
|
+
end
|
43
|
+
|
44
|
+
def cmd
|
45
|
+
return nil unless @cmd
|
46
|
+
# process ERB
|
47
|
+
render(@cmd)
|
48
|
+
end
|
49
|
+
|
50
|
+
def cmd=(value)
|
51
|
+
@cmd = value
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def system_cmd(cmd=nil)
|
57
|
+
raise "ERROR: cmd not given" unless cmd
|
58
|
+
return true if options[:dry_run]
|
59
|
+
system cmd
|
60
|
+
end
|
61
|
+
|
62
|
+
# Runs the YAML file through ERB
|
63
|
+
def render(value, b = binding)
|
64
|
+
ERB.new(value).result(b)
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
class FileSystemEntity < BasicAction
|
70
|
+
|
71
|
+
# return fully qualified name
|
72
|
+
def name
|
73
|
+
return @name unless (@name && File.exists?(@name))
|
74
|
+
File.expand_path(@name)
|
75
|
+
end
|
76
|
+
|
77
|
+
def exists?
|
78
|
+
File.exists?(name) if name
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
class TextFile < FileSystemEntity
|
84
|
+
|
85
|
+
|
86
|
+
def empty?
|
87
|
+
return true unless exists?
|
88
|
+
|
89
|
+
contents = nil
|
90
|
+
File.open(@name, "r") do |f|
|
91
|
+
contents = f.read
|
92
|
+
end
|
93
|
+
contents.empty?
|
94
|
+
end
|
95
|
+
|
96
|
+
def validate
|
97
|
+
errors.add(self, "filename not specified") if @name.nil?
|
98
|
+
errors.add(self, "not found") unless exists?
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class PassphraseFile < TextFile
|
103
|
+
|
104
|
+
def initialize(name='passphrase', options={})
|
105
|
+
super name, options
|
106
|
+
end
|
107
|
+
|
108
|
+
def validate
|
109
|
+
super
|
110
|
+
errors.add(self, "is empty") if empty?
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
class KeyFile < TextFile
|
115
|
+
|
116
|
+
def initialize(name='encfs6.xml', options={})
|
117
|
+
super name, options
|
118
|
+
end
|
119
|
+
|
120
|
+
def validate
|
121
|
+
super
|
122
|
+
errors.add(self, "is empty") if exists? && empty?
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
class FileFolder < FileSystemEntity
|
127
|
+
|
128
|
+
def exists?
|
129
|
+
!@name.nil? && File.directory?(@name)
|
130
|
+
end
|
131
|
+
|
132
|
+
def empty?
|
133
|
+
return true unless exists?
|
134
|
+
Dir.entries(@name).sort == [".", ".."].sort
|
135
|
+
end
|
136
|
+
|
137
|
+
def validate
|
138
|
+
errors.add(self, "not found") unless exists?
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
class SourceFolder < FileFolder
|
143
|
+
|
144
|
+
def validate
|
145
|
+
super
|
146
|
+
errors.add(self, "is empty") if exists? && empty?
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
class ActionFolder < FileFolder
|
151
|
+
attr_accessor :passphrasefile
|
152
|
+
attr_accessor :keyfile
|
153
|
+
|
154
|
+
def initialize(name=nil, options={})
|
155
|
+
super
|
156
|
+
@passphrasefile = PassphraseFile.new(options[:passphrasefile])
|
157
|
+
@keyfile = KeyFile.new(options[:keyfile])
|
158
|
+
@cmd = options[:cmd]
|
159
|
+
@executable = options[:executable]
|
160
|
+
end
|
161
|
+
|
162
|
+
def validate
|
163
|
+
super
|
164
|
+
errors.add(self, "executable filename not specified") unless @executable
|
165
|
+
errors.add(self, "#{@executable} executable not found") if executable.empty?
|
166
|
+
errors.add(self, "cmd not specified") unless cmd
|
167
|
+
end
|
168
|
+
|
169
|
+
def executable
|
170
|
+
return nil unless @executable
|
171
|
+
result = `which #{@executable}`
|
172
|
+
result.strip
|
173
|
+
end
|
174
|
+
|
175
|
+
# run the action if valid and return true if successful
|
176
|
+
def execute
|
177
|
+
raise errors.to_sentences unless valid?
|
178
|
+
|
179
|
+
# default failing command
|
180
|
+
result = false
|
181
|
+
|
182
|
+
# protect command from recursion
|
183
|
+
mutex = Revenc::Mutex.new
|
184
|
+
lock_sucessful = mutex.execute do
|
185
|
+
result = system_cmd(cmd)
|
186
|
+
end
|
187
|
+
|
188
|
+
raise "action failed, lock file present" unless lock_sucessful
|
189
|
+
result
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
class MountPoint < ActionFolder
|
194
|
+
attr_accessor :source
|
195
|
+
|
196
|
+
def initialize(name=nil, source_folder_name=nil, options={})
|
197
|
+
super name, options
|
198
|
+
@source = SourceFolder.new(source_folder_name)
|
199
|
+
@cmd = options[:cmd] || "cat <%= passphrasefile.name %> | ENCFS6_CONFIG=<%= keyfile.name %> \
|
200
|
+
<%= executable %> --stdinpass --reverse <%= source.name %> <%= mountpoint.name %> -- -o ro"
|
201
|
+
@executable = options[:executable] || 'encfs'
|
202
|
+
end
|
203
|
+
|
204
|
+
# allow clarity in config files, instead of <%= name %> you can use <%= mountpoint.name %>
|
205
|
+
def mountpoint
|
206
|
+
self
|
207
|
+
end
|
208
|
+
|
209
|
+
def validate
|
210
|
+
super
|
211
|
+
errors.add(self, "is not empty") unless empty?
|
212
|
+
errors.add(self, source.errors.to_sentences) unless source.valid?
|
213
|
+
errors.add(self, keyfile.errors.to_sentences) unless keyfile.valid?
|
214
|
+
errors.add(self, passphrasefile.errors.to_sentences) unless passphrasefile.valid?
|
215
|
+
end
|
216
|
+
|
217
|
+
end
|
218
|
+
|
219
|
+
class UnmountPoint < ActionFolder
|
220
|
+
|
221
|
+
def initialize(name=nil, options={})
|
222
|
+
super
|
223
|
+
@cmd = options[:cmd] || "<%= executable %> -u <%= mountpoint.name %>"
|
224
|
+
@executable = options[:executable] || 'fusermount'
|
225
|
+
end
|
226
|
+
|
227
|
+
# allow clarity in config files, instead of <%= name %> you can use <%= mountpoint.name %>
|
228
|
+
def mountpoint
|
229
|
+
self
|
230
|
+
end
|
231
|
+
|
232
|
+
end
|
233
|
+
|
234
|
+
class DestinationPoint < BasicAction
|
235
|
+
end
|
236
|
+
|
237
|
+
class CopySourceFolder < ActionFolder
|
238
|
+
attr_accessor :destination
|
239
|
+
attr_accessor :mountpoint
|
240
|
+
|
241
|
+
def initialize(name=nil, destination_name=nil, options={})
|
242
|
+
super name, options
|
243
|
+
@destination = DestinationPoint.new(destination_name)
|
244
|
+
@mountpoint = MountPoint.new(options[:mountpoint])
|
245
|
+
@cmd = options[:cmd] || "<%= executable %> -r <%= source.name %> <%= destination.name %>"
|
246
|
+
@executable = options[:executable] || 'cp'
|
247
|
+
end
|
248
|
+
|
249
|
+
# allow clarity in config files, instead of <%= name %> you can use <%= source.name %>
|
250
|
+
def source
|
251
|
+
self
|
252
|
+
end
|
253
|
+
|
254
|
+
def validate
|
255
|
+
super
|
256
|
+
errors.add(self, "is empty") if exists? && empty?
|
257
|
+
errors.add(self, "mountpoint not found") if (mountpoint.name && !mountpoint.exists?)
|
258
|
+
errors.add(self, "mountpoint is empty") if (mountpoint.name && mountpoint.exists? && mountpoint.empty?)
|
259
|
+
errors.add(self, destination.errors.to_sentences) unless destination.valid?
|
260
|
+
end
|
261
|
+
|
262
|
+
end
|
263
|
+
|
264
|
+
end
|
265
|
+
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Revenc
|
2
|
+
|
3
|
+
class Lockfile
|
4
|
+
attr_accessor :lockfile
|
5
|
+
|
6
|
+
def initialize(lockfile=nil)
|
7
|
+
raise ArgumentError, "lockfile not specified" unless lockfile
|
8
|
+
@lockfile = lockfile
|
9
|
+
end
|
10
|
+
|
11
|
+
def locked?
|
12
|
+
return false unless File.exists?(lockfile)
|
13
|
+
result = false
|
14
|
+
open(lockfile, 'w') do |f|
|
15
|
+
# exclusive non-blocking lock
|
16
|
+
result = !lock(f, File::LOCK_EX | File::LOCK_NB)
|
17
|
+
end
|
18
|
+
result
|
19
|
+
end
|
20
|
+
|
21
|
+
# flock, get a file lock
|
22
|
+
#
|
23
|
+
# Typical usage:
|
24
|
+
#
|
25
|
+
# open('output', 'w') do |f|
|
26
|
+
# flock(f, File::LOCK_EX) do |f|
|
27
|
+
# f << "write to file"
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
def lock(file, mode)
|
31
|
+
result = file.flock(mode)
|
32
|
+
if result
|
33
|
+
begin
|
34
|
+
yield file if block_given?
|
35
|
+
ensure
|
36
|
+
file.flock(File::LOCK_UN)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
return result
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class Mutex < Lockfile
|
44
|
+
|
45
|
+
def initialize(lockfile='revenc.lck')
|
46
|
+
super lockfile
|
47
|
+
end
|
48
|
+
|
49
|
+
def execute
|
50
|
+
result = false
|
51
|
+
begin
|
52
|
+
open(@lockfile, 'w') do |f|
|
53
|
+
# exclusive non-blocking lock
|
54
|
+
result = lock(f, File::LOCK_EX | File::LOCK_NB) do |f|
|
55
|
+
yield if block_given?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
ensure
|
59
|
+
# clean up but only if we have a positive result meaning we wrote the lockfile
|
60
|
+
FileUtils.rm(@lockfile) if (result && File.exists?(@lockfile))
|
61
|
+
end
|
62
|
+
result
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
data/lib/revenc.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
|
5
|
+
module Revenc
|
6
|
+
|
7
|
+
# return the contents of the VERSION file
|
8
|
+
# VERSION format: 0.0.0
|
9
|
+
def self.version
|
10
|
+
version_info_file = File.join(File.dirname(__FILE__), *%w[.. VERSION])
|
11
|
+
File.open(version_info_file, "r") do |f|
|
12
|
+
f.read
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
require 'revenc/app'
|
19
|
+
require 'revenc/io'
|
20
|
+
require 'revenc/errors'
|
21
|
+
require 'revenc/lockfile'
|
22
|
+
require 'revenc/encfs_wrapper'
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Revenc::Errors do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@error_obj = Revenc::Errors.new
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should be empty when created" do
|
10
|
+
@error_obj.should be_empty
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should return a count of the errors" do
|
14
|
+
@error_obj.should be_empty
|
15
|
+
@error_obj.add(:test_error1)
|
16
|
+
@error_obj.add(:test_error2)
|
17
|
+
@error_obj.should_not be_empty
|
18
|
+
@error_obj.size.should == 2
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should clear the errors" do
|
22
|
+
@error_obj.should be_empty
|
23
|
+
@error_obj.add(:test_error1)
|
24
|
+
@error_obj.add(:test_error2)
|
25
|
+
@error_obj.should_not be_empty
|
26
|
+
@error_obj.size.should be(2)
|
27
|
+
@error_obj.clear
|
28
|
+
@error_obj.should be_empty
|
29
|
+
@error_obj.size.should be(0)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should return the errors in full sentences for errors on symbol" do
|
33
|
+
@error_obj.should be_empty
|
34
|
+
@error_obj.add(:test_error1, "Error no 1")
|
35
|
+
@error_obj.add(:test_error2, "Error no 2")
|
36
|
+
@error_obj.size.should be(2)
|
37
|
+
@error_obj.to_sentences.should == "test error1 Error no 1\ntest error2 Error no 2"
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should return the errors in full sentences for errors on class names" do
|
41
|
+
@error_obj.should be_empty
|
42
|
+
@error_obj.add(@error_obj, "error no 1")
|
43
|
+
@error_obj.add(@error_obj, "error no 2")
|
44
|
+
@error_obj.size.should be(2)
|
45
|
+
@error_obj.to_sentences.should == "errors error no 1\nerrors error no 2"
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
|