cloud_encrypted_sync 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +1 -1
- data/README.md +2 -0
- data/lib/cloud_encrypted_sync.rb +1 -0
- data/lib/cloud_encrypted_sync/adapter_template.rb +31 -8
- data/lib/cloud_encrypted_sync/configuration.rb +22 -11
- data/lib/cloud_encrypted_sync/dummy_adapter.rb +27 -30
- data/lib/cloud_encrypted_sync/index.rb +6 -8
- data/lib/cloud_encrypted_sync/progress_meter.rb +19 -15
- data/lib/cloud_encrypted_sync/synchronizer.rb +46 -36
- data/lib/cloud_encrypted_sync/version.rb +1 -1
- data/test/unit/adapter_template_test.rb +1 -1
- data/test/unit/progress_meter_test.rb +13 -9
- data/test/unit/synchronizer_test.rb +9 -9
- metadata +2 -2
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
data/lib/cloud_encrypted_sync.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
module CloudEncryptedSync
|
2
2
|
module Adapters
|
3
3
|
class Template
|
4
|
+
include Singleton
|
4
5
|
|
5
6
|
class << self
|
6
7
|
|
7
8
|
def inherited(subclass)
|
8
|
-
|
9
|
+
register_with_parent(subclass)
|
10
|
+
super
|
9
11
|
end
|
10
12
|
|
11
13
|
def children
|
@@ -13,39 +15,60 @@ module CloudEncryptedSync
|
|
13
15
|
end
|
14
16
|
|
15
17
|
def parse_command_line_options(opts,command_line_options)
|
16
|
-
|
18
|
+
instance.parse_command_line_options(opts,command_line_options)
|
17
19
|
end
|
18
20
|
|
19
21
|
def write(data, key)
|
20
|
-
|
22
|
+
instance.write(data, key)
|
21
23
|
end
|
22
24
|
|
23
25
|
def read(key)
|
24
|
-
|
26
|
+
instance.read(key)
|
25
27
|
end
|
26
28
|
|
27
29
|
def delete(key)
|
28
|
-
|
30
|
+
instance.delete(key)
|
29
31
|
end
|
30
32
|
|
31
33
|
def key_exists?(key)
|
32
|
-
|
34
|
+
instance.key_exists?(key)
|
33
35
|
end
|
34
36
|
|
35
37
|
#######
|
36
38
|
private
|
37
39
|
#######
|
38
40
|
|
39
|
-
def
|
41
|
+
def register_with_parent(subclass)
|
40
42
|
name = formated_name_of(subclass)
|
41
43
|
children[name] ||= subclass
|
42
44
|
end
|
43
45
|
|
44
46
|
def formated_name_of(subclass)
|
45
|
-
puts "Subclass: #{subclass}"
|
46
47
|
subclass.name.match(/([^:]+)$/)[0].underscore.to_sym
|
47
48
|
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
def parse_command_line_options(opts,command_line_options)
|
53
|
+
raise Errors::TemplateMethodCalled.new('parse_command_line_options')
|
54
|
+
end
|
55
|
+
|
56
|
+
def write(data, key)
|
57
|
+
raise Errors::TemplateMethodCalled.new('write')
|
58
|
+
end
|
59
|
+
|
60
|
+
def read(key)
|
61
|
+
raise Errors::TemplateMethodCalled.new('read')
|
62
|
+
end
|
63
|
+
|
64
|
+
def delete(key)
|
65
|
+
raise Errors::TemplateMethodCalled.new('delete')
|
48
66
|
end
|
67
|
+
|
68
|
+
def key_exists?(key)
|
69
|
+
raise Errors::TemplateMethodCalled.new('key_exists?')
|
70
|
+
end
|
71
|
+
|
49
72
|
end
|
50
73
|
end
|
51
74
|
end
|
@@ -6,7 +6,7 @@ module CloudEncryptedSync
|
|
6
6
|
attr_reader :option_parser
|
7
7
|
|
8
8
|
def settings
|
9
|
-
@settings ||=
|
9
|
+
@settings ||= load
|
10
10
|
end
|
11
11
|
|
12
12
|
def data_folder_path
|
@@ -17,17 +17,30 @@ module CloudEncryptedSync
|
|
17
17
|
private
|
18
18
|
#######
|
19
19
|
|
20
|
-
def
|
20
|
+
def load
|
21
21
|
touch_data_folder
|
22
|
-
loaded_settings =
|
23
|
-
|
24
|
-
loaded_settings.merge!(command_line_options)
|
25
|
-
loaded_settings = loaded_settings.inject({}) do |options, (key, value)|
|
26
|
-
options[(key.to_sym rescue key) || key] = value
|
27
|
-
options
|
28
|
-
end
|
22
|
+
loaded_settings = config_file_settings.merge(command_line_options).with_indifferent_access
|
23
|
+
|
29
24
|
loaded_settings[:sync_path] = ARGV.shift unless ARGV.empty?
|
30
25
|
|
26
|
+
validate_settings(loaded_settings)
|
27
|
+
|
28
|
+
return loaded_settings
|
29
|
+
end
|
30
|
+
|
31
|
+
def config_file_settings
|
32
|
+
@config_file_settings ||= load_config_file_settings
|
33
|
+
end
|
34
|
+
|
35
|
+
def load_config_file_settings
|
36
|
+
if File.exist?(config_file_path)
|
37
|
+
YAML.load_file(config_file_path)
|
38
|
+
else
|
39
|
+
{}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def validate_settings(loaded_settings)
|
31
44
|
if loaded_settings[:sync_path].nil?
|
32
45
|
message = "You must supply a path to a folder to sync.\n\n#{option_parser.help}"
|
33
46
|
raise Errors::IncompleteConfigurationError.new(message)
|
@@ -35,8 +48,6 @@ module CloudEncryptedSync
|
|
35
48
|
message = "You must supply an encryption key.\n\n#{option_parser.help}"
|
36
49
|
raise Errors::IncompleteConfigurationError.new(message)
|
37
50
|
end
|
38
|
-
|
39
|
-
return loaded_settings
|
40
51
|
end
|
41
52
|
|
42
53
|
def touch_data_folder
|
@@ -2,45 +2,42 @@ module CloudEncryptedSync
|
|
2
2
|
module Adapters
|
3
3
|
class Dummy < Template
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
stored_data[bucket_name][key] = data
|
9
|
-
end
|
10
|
-
|
11
|
-
def parse_command_line_options(opts,command_line_options)
|
12
|
-
opts.on('--bucket BUCKETNAME', 'Name of cloud adapter to use.') do |bucket_name|
|
13
|
-
command_line_options[:bucket] = bucket_name
|
14
|
-
end
|
15
|
-
return command_line_options
|
16
|
-
end
|
5
|
+
def write(data,key)
|
6
|
+
stored_data[bucket_name][key] = data
|
7
|
+
end
|
17
8
|
|
18
|
-
|
19
|
-
|
20
|
-
|
9
|
+
def parse_command_line_options(opts,command_line_options)
|
10
|
+
opts.on('--bucket BUCKETNAME', 'Name of cloud adapter to use.') do |bucket_name|
|
11
|
+
command_line_options[:bucket] = bucket_name
|
21
12
|
end
|
13
|
+
return command_line_options
|
14
|
+
end
|
22
15
|
|
23
|
-
|
24
|
-
|
25
|
-
|
16
|
+
def read(key)
|
17
|
+
raise Errors::NoSuchKey.new("key doesn't exist: #{key}") unless key_exists?(key)
|
18
|
+
stored_data[bucket_name][key]
|
19
|
+
end
|
26
20
|
|
27
|
-
|
28
|
-
|
29
|
-
|
21
|
+
def delete(key)
|
22
|
+
stored_data[bucket_name].delete(key)
|
23
|
+
end
|
30
24
|
|
31
|
-
|
32
|
-
|
33
|
-
|
25
|
+
def key_exists?(key)
|
26
|
+
stored_data[bucket_name][key] ? true : false
|
27
|
+
end
|
34
28
|
|
35
|
-
|
36
|
-
|
37
|
-
|
29
|
+
#######
|
30
|
+
private
|
31
|
+
#######
|
38
32
|
|
39
|
-
|
40
|
-
|
41
|
-
|
33
|
+
def stored_data
|
34
|
+
@stored_data ||= { bucket_name => {} }
|
35
|
+
end
|
42
36
|
|
37
|
+
def bucket_name
|
38
|
+
Configuration.settings[:bucket].to_sym
|
43
39
|
end
|
40
|
+
|
44
41
|
end
|
45
42
|
end
|
46
43
|
end
|
@@ -43,16 +43,14 @@ module CloudEncryptedSync
|
|
43
43
|
|
44
44
|
def compile_local_hash
|
45
45
|
hash = {}
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
46
|
+
ProgressMeter.new(Dir["#{normalized_sync_path}/**/*"].length,:label => 'Compiling Local Index: ') do |progress_meter|
|
47
|
+
Find.find(normalized_sync_path) do |path|
|
48
|
+
unless FileTest.directory?(path)
|
49
|
+
hash[file_key(path)] = relative_file_path(path)
|
50
|
+
end
|
51
|
+
progress_meter.increment_completed_index
|
52
52
|
end
|
53
|
-
completed_files += 1
|
54
53
|
end
|
55
|
-
puts #newline for progress meter
|
56
54
|
return hash
|
57
55
|
end
|
58
56
|
|
@@ -8,18 +8,11 @@ module CloudEncryptedSync
|
|
8
8
|
@label = options[:label] || ''
|
9
9
|
@completed_index = 0.0
|
10
10
|
@start_time = Time.now
|
11
|
+
yield self if block_given?
|
11
12
|
end
|
12
13
|
|
13
|
-
def
|
14
|
-
|
15
|
-
end
|
16
|
-
|
17
|
-
def percent_completed
|
18
|
-
(completed_index/max_index)*100
|
19
|
-
end
|
20
|
-
|
21
|
-
def time_elapsed
|
22
|
-
Time.now - start_time
|
14
|
+
def estimated_time_remaining
|
15
|
+
Time.at(estimated_finish_time - Time.now)
|
23
16
|
end
|
24
17
|
|
25
18
|
def estimated_finish_time
|
@@ -30,14 +23,25 @@ module CloudEncryptedSync
|
|
30
23
|
end
|
31
24
|
end
|
32
25
|
|
33
|
-
def
|
34
|
-
|
26
|
+
def percent_completed
|
27
|
+
(completed_index/max_index)*100
|
28
|
+
end
|
29
|
+
|
30
|
+
def time_elapsed
|
31
|
+
Time.now - start_time
|
35
32
|
end
|
36
33
|
|
37
|
-
def
|
38
|
-
self.completed_index
|
39
|
-
|
34
|
+
def increment_completed_index(amount = 1)
|
35
|
+
self.completed_index += amount
|
36
|
+
notify
|
40
37
|
end
|
41
38
|
|
39
|
+
#######
|
40
|
+
private
|
41
|
+
#######
|
42
|
+
|
43
|
+
def notify
|
44
|
+
print sprintf("\r#{label}%0.1f%% Complete. Time Remaining %s", percent_completed, estimated_time_remaining.strftime('%M:%S'))
|
45
|
+
end
|
42
46
|
end
|
43
47
|
end
|
@@ -16,45 +16,59 @@ module CloudEncryptedSync
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
+
#######
|
20
|
+
private
|
21
|
+
#######
|
22
|
+
|
19
23
|
def push_files
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
#already exists. probably left over from an earlier aborted push
|
26
|
-
puts "Not Pushing (already exists): #{relative_path}"
|
27
|
-
else
|
28
|
-
puts "Pushing: #{relative_path}"
|
29
|
-
liaison.push(File.read(Index.full_file_path(relative_path)),key)
|
30
|
-
self.finalize_required = true
|
24
|
+
ProgressMeter.new(files_to_push.keys.size,:label => 'Pushing Files: ') do |progress_meter|
|
25
|
+
pushed_files_counter = 0
|
26
|
+
files_to_push.each_pair do |key,relative_path|
|
27
|
+
push_file_if_necessary(key,relative_path)
|
28
|
+
progress_meter.increment_completed_index
|
31
29
|
end
|
32
|
-
|
33
|
-
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def push_file_if_necessary(key,relative_path)
|
34
|
+
if liaison.key_exists?(key)
|
35
|
+
#already exists. probably left over from an earlier aborted push
|
36
|
+
puts "\nNot Pushing (already exists): #{relative_path}"
|
37
|
+
else
|
38
|
+
puts "\nPushing: #{relative_path}"
|
39
|
+
liaison.push(File.read(Index.full_file_path(relative_path)),key)
|
40
|
+
self.finalize_required = true
|
34
41
|
end
|
35
42
|
end
|
36
43
|
|
37
44
|
def pull_files
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
puts #newline for progress meter
|
43
|
-
if File.exist?(full_path) and (Index.file_key(full_path) == key)
|
44
|
-
#already exists. probably left over from an earlier aborted pull
|
45
|
-
puts "Not Pulling (already exists): #{full_path}"
|
46
|
-
else
|
47
|
-
Dir.mkdir(File.dirname(full_path)) unless File.exist?(File.dirname(full_path))
|
48
|
-
puts "Pulling: #{relative_path}"
|
49
|
-
begin
|
50
|
-
File.open(full_path,'w') { |file| file.write(liaison.pull(key)) }
|
51
|
-
self.finalize_required = true
|
52
|
-
rescue Errors::NoSuchKey
|
53
|
-
puts "Failed to pull #{relative_path}"
|
54
|
-
end
|
45
|
+
ProgressMeter.new(files_to_pull.keys.size,:label => 'Pulling Files: ') do |progress_meter|
|
46
|
+
files_to_pull.each_pair do |key,relative_path|
|
47
|
+
pull_file_if_necessary(key,relative_path)
|
48
|
+
progress_meter.increment_completed_index
|
55
49
|
end
|
56
|
-
|
57
|
-
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def pull_file_if_necessary(key,relative_path)
|
54
|
+
full_path = Index.full_file_path(relative_path)
|
55
|
+
if File.exist?(full_path) and (Index.file_key(full_path) == key)
|
56
|
+
#already exists. probably left over from an earlier aborted pull
|
57
|
+
puts "\nNot Pulling (already exists): #{full_path}"
|
58
|
+
else
|
59
|
+
Dir.mkdir(File.dirname(full_path)) unless File.exist?(File.dirname(full_path))
|
60
|
+
puts "\nPulling: #{relative_path}"
|
61
|
+
pull_file_or_rescue(key,relative_path)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def pull_file_or_rescue(key,relative_path)
|
66
|
+
full_path = Index.full_file_path(relative_path)
|
67
|
+
begin
|
68
|
+
File.open(full_path,'w') { |file| file.write(liaison.pull(key)) }
|
69
|
+
self.finalize_required = true
|
70
|
+
rescue Errors::NoSuchKey
|
71
|
+
puts "\nFailed to pull #{relative_path}"
|
58
72
|
end
|
59
73
|
end
|
60
74
|
|
@@ -83,10 +97,6 @@ module CloudEncryptedSync
|
|
83
97
|
Index.write if finalize_required
|
84
98
|
end
|
85
99
|
|
86
|
-
#######
|
87
|
-
private
|
88
|
-
#######
|
89
|
-
|
90
100
|
def liaison
|
91
101
|
AdapterLiaison.instance
|
92
102
|
end
|
@@ -4,7 +4,7 @@ module CloudEncryptedSync
|
|
4
4
|
class AdapterTemplateTest < ActiveSupport::TestCase
|
5
5
|
|
6
6
|
test 'should register with parent class on inheritance' do
|
7
|
-
Adapters::Template.expects(:
|
7
|
+
Adapters::Template.expects(:register_with_parent).returns(true)
|
8
8
|
Class.new(Adapters::Template)
|
9
9
|
end
|
10
10
|
|
@@ -9,32 +9,36 @@ module CloudEncryptedSync
|
|
9
9
|
end
|
10
10
|
|
11
11
|
test 'should calculate percent completed' do
|
12
|
-
@progress_meter.
|
12
|
+
@progress_meter.increment_completed_index
|
13
13
|
assert_equal(25,@progress_meter.percent_completed)
|
14
14
|
end
|
15
15
|
|
16
16
|
test 'should calculate time elapsed' do
|
17
|
-
assert_in_delta(42,@progress_meter.time_elapsed,0.
|
17
|
+
assert_in_delta(42,@progress_meter.time_elapsed,0.02)
|
18
18
|
end
|
19
19
|
|
20
20
|
test 'should estimate finish time' do
|
21
|
-
@progress_meter.
|
22
|
-
assert_in_delta(Time.now+(42*3),@progress_meter.estimated_finish_time,0.
|
21
|
+
@progress_meter.increment_completed_index
|
22
|
+
assert_in_delta(Time.now+(42*3),@progress_meter.estimated_finish_time,0.02)
|
23
23
|
end
|
24
24
|
|
25
25
|
test 'should estimate time remaining' do
|
26
|
-
@progress_meter.
|
27
|
-
assert_in_delta((42*3),@progress_meter.estimated_time_remaining.to_f,0.
|
26
|
+
@progress_meter.increment_completed_index
|
27
|
+
assert_in_delta((42*3),@progress_meter.estimated_time_remaining.to_f,0.02)
|
28
28
|
end
|
29
29
|
|
30
|
-
test 'should
|
30
|
+
test 'should increment counter and write to stdout' do
|
31
|
+
assert_equal('',$stdout.string)
|
31
32
|
assert_difference('@progress_meter.completed_index',2) do
|
32
|
-
|
33
|
+
@progress_meter.increment_completed_index(2)
|
33
34
|
end
|
35
|
+
assert_match(/\% Complete/,$stdout.string)
|
34
36
|
end
|
35
37
|
|
36
38
|
test 'should render string' do
|
37
|
-
|
39
|
+
assert_equal('',$stdout.string)
|
40
|
+
@progress_meter.send(:notify)
|
41
|
+
assert_match(/\% Complete/,$stdout.string)
|
38
42
|
end
|
39
43
|
end
|
40
44
|
end
|
@@ -25,13 +25,13 @@ module CloudEncryptedSync
|
|
25
25
|
Adapters::Dummy.expects(:write)
|
26
26
|
|
27
27
|
assert_equal('',$stdout.string)
|
28
|
-
Synchronizer.push_files
|
28
|
+
Synchronizer.send(:push_files)
|
29
29
|
assert_match(/\% Complete/,$stdout.string)
|
30
30
|
end
|
31
31
|
|
32
32
|
test 'should not push files that already exist' do
|
33
33
|
AdapterLiaison.instance.stubs(:key_exists?).returns(true)
|
34
|
-
Synchronizer.push_files
|
34
|
+
Synchronizer.send(:push_files)
|
35
35
|
assert_match(/\(already exists\)/,$stdout.string)
|
36
36
|
end
|
37
37
|
|
@@ -40,7 +40,7 @@ module CloudEncryptedSync
|
|
40
40
|
Adapters::Dummy.expects(:read).with('new_file_key').returns(Cryptographer.encrypt_data('foobar'))
|
41
41
|
assert_equal('',$stdout.string)
|
42
42
|
assert_difference('Dir["#{test_source_folder}/**/*"].length') do
|
43
|
-
Synchronizer.pull_files
|
43
|
+
Synchronizer.send(:pull_files)
|
44
44
|
end
|
45
45
|
assert_match(/\% Complete/,$stdout.string)
|
46
46
|
end
|
@@ -49,14 +49,14 @@ module CloudEncryptedSync
|
|
49
49
|
Synchronizer.stubs(:files_to_pull).returns({:foo => 'bar'})
|
50
50
|
File.stubs(:exist?).returns(true)
|
51
51
|
Index.stubs(:file_key).returns(:foo)
|
52
|
-
Synchronizer.pull_files
|
52
|
+
Synchronizer.send(:pull_files)
|
53
53
|
assert_match(/\(already exists\)/,$stdout.string)
|
54
54
|
end
|
55
55
|
|
56
56
|
test 'should gracefully recover if pull fails' do
|
57
57
|
Synchronizer.stubs(:files_to_pull).returns({:foo => 'bar'})
|
58
58
|
AdapterLiaison.instance.stubs(:pull).raises(Errors::NoSuchKey)
|
59
|
-
Synchronizer.pull_files
|
59
|
+
Synchronizer.send(:pull_files)
|
60
60
|
assert_match(/Failed to pull/,$stdout.string)
|
61
61
|
end
|
62
62
|
|
@@ -65,7 +65,7 @@ module CloudEncryptedSync
|
|
65
65
|
Index.stubs(:local).returns({'saved_file_key' => 'test_sub_folder/saved_file.txt'})
|
66
66
|
Synchronizer.stubs(:last_sync_hash).returns({'saved_file_key' => 'test_sub_folder/saved_file.txt', 'deleted_file_key' => 'test_sub_folder/deleted_file.txt'})
|
67
67
|
Adapters::Dummy.expects(:delete).with('deleted_file_key').returns(true)
|
68
|
-
Synchronizer.delete_remote_files
|
68
|
+
Synchronizer.send(:delete_remote_files)
|
69
69
|
assert_match(/Deleting Remote/,$stdout.string)
|
70
70
|
end
|
71
71
|
|
@@ -73,7 +73,7 @@ module CloudEncryptedSync
|
|
73
73
|
Index.stubs(:remote).returns({'saved_file_key' => 'test_sub_folder/saved_file.txt'})
|
74
74
|
Synchronizer.stubs(:last_sync_hash).returns({'saved_file_key' => 'test_sub_folder/saved_file.txt'}.merge(Index.local))
|
75
75
|
assert_difference('Dir["#{test_source_folder}/**/*"].length',-1) do
|
76
|
-
Synchronizer.delete_local_files
|
76
|
+
Synchronizer.send(:delete_local_files)
|
77
77
|
end
|
78
78
|
assert_match(/Deleting Local/,$stdout.string)
|
79
79
|
end
|
@@ -81,7 +81,7 @@ module CloudEncryptedSync
|
|
81
81
|
test 'should gracefully recover if local file disappears before delete' do
|
82
82
|
Synchronizer.stubs(:local_files_to_delete).returns({:foo => 'bar'})
|
83
83
|
File.stubs(:exist?).returns(false)
|
84
|
-
Synchronizer.delete_local_files
|
84
|
+
Synchronizer.send(:delete_local_files)
|
85
85
|
assert_match(/Not Deleting Local/,$stdout.string)
|
86
86
|
end
|
87
87
|
|
@@ -89,7 +89,7 @@ module CloudEncryptedSync
|
|
89
89
|
Synchronizer.instance_variable_set(:@finalize_required,true)
|
90
90
|
Index.expects(:write)
|
91
91
|
|
92
|
-
Synchronizer.finalize
|
92
|
+
Synchronizer.send(:finalize)
|
93
93
|
end
|
94
94
|
|
95
95
|
test 'should want to push everything on first run with local files and empty remote' do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cloud_encrypted_sync
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-11-
|
12
|
+
date: 2012-11-21 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: mocha
|