cloud_encrypted_sync 0.1.2 → 0.2.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.
- 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
|