cloud_encrypted_sync 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +31 -0
- data/LICENSE +17 -0
- data/README.md +60 -0
- data/Rakefile +10 -0
- data/bin/ces +5 -0
- data/cloud_encrypted_sync.gemspec +22 -0
- data/lib/cloud_encrypted_sync.rb +6 -0
- data/lib/cloud_encrypted_sync/adapter_template.rb +34 -0
- data/lib/cloud_encrypted_sync/configuration.rb +82 -0
- data/lib/cloud_encrypted_sync/cryptographer.rb +61 -0
- data/lib/cloud_encrypted_sync/dummy_adapter.rb +47 -0
- data/lib/cloud_encrypted_sync/master.rb +248 -0
- data/lib/cloud_encrypted_sync/progress_meter.rb +43 -0
- data/lib/cloud_encrypted_sync/version.rb +3 -0
- data/test/test_helper.rb +63 -0
- data/test/unit/configuration_test.rb +33 -0
- data/test/unit/cryptographer_test.rb +34 -0
- data/test/unit/master_test.rb +155 -0
- data/test/unit/progress_meter_test.rb +40 -0
- metadata +137 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
cloud_encrypted_sync (0.0.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: http://rubygems.org/
|
8
|
+
specs:
|
9
|
+
activesupport (3.2.3)
|
10
|
+
i18n (~> 0.6)
|
11
|
+
multi_json (~> 1.0)
|
12
|
+
fakefs (0.4.0)
|
13
|
+
i18n (0.6.0)
|
14
|
+
metaclass (0.0.1)
|
15
|
+
mocha (0.11.4)
|
16
|
+
metaclass (~> 0.0.1)
|
17
|
+
multi_json (1.3.4)
|
18
|
+
simplecov (0.6.4)
|
19
|
+
multi_json (~> 1.0)
|
20
|
+
simplecov-html (~> 0.5.3)
|
21
|
+
simplecov-html (0.5.3)
|
22
|
+
|
23
|
+
PLATFORMS
|
24
|
+
ruby
|
25
|
+
|
26
|
+
DEPENDENCIES
|
27
|
+
activesupport
|
28
|
+
cloud_encrypted_sync!
|
29
|
+
fakefs
|
30
|
+
mocha
|
31
|
+
simplecov
|
data/LICENSE
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
Copyright (c) 2012 Jonathan S. Garvin
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
4
|
+
software and associated documentation files (the "Software"), to deal in the Software
|
5
|
+
without restriction, including without limitation the rights to use, copy, modify, merge,
|
6
|
+
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
7
|
+
to whom the Software is furnished to do so, subject to the following conditions:
|
8
|
+
|
9
|
+
The above copyright notice and this permission notice shall be included in all copies or
|
10
|
+
substantial portions of the Software.
|
11
|
+
|
12
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
13
|
+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
14
|
+
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
15
|
+
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
16
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
17
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# Cloud Encrypted Sync
|
2
|
+
|
3
|
+
Cloud Encrypted Sync (CES) is a command line tool distributed as a Ruby gem for syncing a local
|
4
|
+
folder to cloud storage via external adapters, with localy managed encryption (you control the
|
5
|
+
keys).
|
6
|
+
|
7
|
+
Even though you could simply use CES to backup a local folder to the cloud, it's original
|
8
|
+
intended purpose is to sync a folder across multiple computers, so it should work for that,
|
9
|
+
too.
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
gem install cloud_encrypted_sync
|
14
|
+
|
15
|
+
In addition to this gem you'll also need to install an adapter gem for the particular cloud
|
16
|
+
you want to backup to. At the time of this writing, the only available adapter is for Amazon
|
17
|
+
S3, although anyone can create an adapter to work with other clouds. Search rubygems.org for
|
18
|
+
"cloud encrypted sync" to find out if someone has already created an adapter for your
|
19
|
+
preferred cloud.
|
20
|
+
|
21
|
+
## Getting started
|
22
|
+
|
23
|
+
CES runs as a command line tool and takes options as CLI arguments and/or from a config file.
|
24
|
+
Arguments passed at the command line take precedence over those in the config file.
|
25
|
+
|
26
|
+
### Creating a valid encryption key and initialization vector.
|
27
|
+
|
28
|
+
TODO
|
29
|
+
|
30
|
+
### Example
|
31
|
+
|
32
|
+
ces --adapter=s3 --bucket=my-backup-bucket \
|
33
|
+
--s3-credentials=ACCESS_KEY_ID,SECRET_ACCESS_KEY \
|
34
|
+
--encryption-key=MYENCRYPTIONKEY /path/to/source/folder
|
35
|
+
|
36
|
+
## Configuration
|
37
|
+
|
38
|
+
Configuration options may be passed to CES via the command line, or via a config yaml file.
|
39
|
+
|
40
|
+
The default location for the config file is `~/.cloud_encrypted_sync/config.rc.yml`
|
41
|
+
|
42
|
+
### Available Settings
|
43
|
+
|
44
|
+
CES requires the following configuration settings. Any of thse may alternatively be placed in
|
45
|
+
the `config.rc.yml` execpt for `--data-dir` (which tells CES which folder contains the config
|
46
|
+
file to use).
|
47
|
+
|
48
|
+
* `--adapter=ADAPTERNAME` The name of the adapter to use. See instructions for your preferred
|
49
|
+
adapter for instructions of what to place here.
|
50
|
+
* `--encryption-key=XXX` The encryption key (shocking, I know).
|
51
|
+
|
52
|
+
In addition to these settings, your chosen adapter will probably also have additional adapter
|
53
|
+
specific settings as well, such as credentials to log into your cloud storage account. Adapter
|
54
|
+
specific settings may work the same the the above standard settings in that they may be included
|
55
|
+
on the command or in the `config.rc.yml` file (unless the adapter author breaks convention and
|
56
|
+
tries to do something weird).
|
57
|
+
|
58
|
+
## Creating your own adapter
|
59
|
+
|
60
|
+
TODO
|
data/Rakefile
ADDED
data/bin/ces
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.expand_path('../lib/cloud_encrypted_sync/version', __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "cloud_encrypted_sync"
|
5
|
+
s.version = CloudEncryptedSync::VERSION
|
6
|
+
s.platform = Gem::Platform::RUBY
|
7
|
+
s.authors = ["Jonathan S. Garvin"]
|
8
|
+
s.email = ["jon@5valleys.com"]
|
9
|
+
s.homepage = "https://github.com/jsgarvin/cloud_encrypted_sync"
|
10
|
+
s.summary = %q{Encrypted sync of folder contents to/from cloud storage.}
|
11
|
+
s.description = %q{Encrypted sync of folder contents to/from cloud storage with user controlled encryption keys.}
|
12
|
+
|
13
|
+
s.add_development_dependency('mocha')
|
14
|
+
s.add_development_dependency('simplecov')
|
15
|
+
s.add_development_dependency('fakefs')
|
16
|
+
s.add_development_dependency('activesupport')
|
17
|
+
|
18
|
+
s.files = `git ls-files`.split("\n")
|
19
|
+
s.test_files = `git ls-files -- test/*`.split("\n")
|
20
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
21
|
+
s.require_paths = ["lib"]
|
22
|
+
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
require File.expand_path('../cloud_encrypted_sync/master', __FILE__)
|
2
|
+
require File.expand_path('../cloud_encrypted_sync/configuration', __FILE__)
|
3
|
+
require File.expand_path('../cloud_encrypted_sync/cryptographer', __FILE__)
|
4
|
+
require File.expand_path('../cloud_encrypted_sync/adapter_template', __FILE__)
|
5
|
+
require File.expand_path('../cloud_encrypted_sync/dummy_adapter', __FILE__)
|
6
|
+
require File.expand_path('../cloud_encrypted_sync/progress_meter', __FILE__)
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module CloudEncryptedSync
|
2
|
+
module Adapters
|
3
|
+
class Template
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def inherited(subclass)
|
8
|
+
Master.register(subclass)
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse_command_line_options(opts,command_line_options)
|
12
|
+
raise 'called template method: parse_command_line_options'
|
13
|
+
end
|
14
|
+
|
15
|
+
def write(data, key)
|
16
|
+
raise 'called template method: write'
|
17
|
+
end
|
18
|
+
|
19
|
+
def read(key)
|
20
|
+
raise 'called template method: read'
|
21
|
+
end
|
22
|
+
|
23
|
+
def delete(key)
|
24
|
+
raise 'called template method: delete'
|
25
|
+
end
|
26
|
+
|
27
|
+
def key_exists?(key)
|
28
|
+
raise 'called template method: key_exists?'
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module CloudEncryptedSync
|
4
|
+
class Configuration
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
attr_reader :option_parser
|
9
|
+
|
10
|
+
def settings
|
11
|
+
@settings ||= load_settings
|
12
|
+
end
|
13
|
+
|
14
|
+
def data_folder_path
|
15
|
+
command_line_options[:data_dir]
|
16
|
+
end
|
17
|
+
|
18
|
+
#######
|
19
|
+
private
|
20
|
+
#######
|
21
|
+
|
22
|
+
def load_settings
|
23
|
+
touch_data_folder
|
24
|
+
loaded_settings = {}
|
25
|
+
loaded_settings = YAML.load_file(config_file_path) if File.exist?(config_file_path)
|
26
|
+
loaded_settings.merge!(command_line_options)
|
27
|
+
loaded_settings = loaded_settings.inject({}) do |options, (key, value)|
|
28
|
+
options[(key.to_sym rescue key) || key] = value
|
29
|
+
options
|
30
|
+
end
|
31
|
+
loaded_settings[:sync_path] = ARGV.shift unless ARGV.empty?
|
32
|
+
|
33
|
+
if loaded_settings[:sync_path].nil?
|
34
|
+
message = "You must supply a path to a folder to sync.\n\n#{option_parser.help}"
|
35
|
+
raise IncompleteConfigurationError.new(message)
|
36
|
+
elsif loaded_settings[:encryption_key].nil? or loaded_settings[:encryption_key].empty?
|
37
|
+
message = "You must supply an encryption key.\n\n#{option_parser.help}"
|
38
|
+
raise IncompleteConfigurationError.new(message)
|
39
|
+
end
|
40
|
+
|
41
|
+
return loaded_settings
|
42
|
+
end
|
43
|
+
|
44
|
+
def touch_data_folder
|
45
|
+
FileUtils.mkdir_p(data_folder_path) unless Dir.exists?(data_folder_path)
|
46
|
+
end
|
47
|
+
|
48
|
+
def config_file_path
|
49
|
+
data_folder_path+'/config.rc.yml'
|
50
|
+
end
|
51
|
+
|
52
|
+
def command_line_options
|
53
|
+
@command_line_options ||= parse_command_line_options
|
54
|
+
end
|
55
|
+
|
56
|
+
def parse_command_line_options
|
57
|
+
executable_name = File.basename($PROGRAM_NAME)
|
58
|
+
clo = {:data_dir => "#{Etc.getpwuid.dir}/.cloud_encrypted_sync"}
|
59
|
+
|
60
|
+
@option_parser = OptionParser.new do |opts|
|
61
|
+
opts.banner = "Usage: #{executable_name} [options] /path/to/folder/to/sync"
|
62
|
+
opts.on('--data-dir PATH',"Data directory where snapshots and config file are found.") do |path|
|
63
|
+
clo[:data_dir] = path
|
64
|
+
end
|
65
|
+
opts.on('--adapter ADAPTERNAME', 'Name of cloud adapter to use.') do |adapter_name|
|
66
|
+
clo[:adapter_name] = adapter_name
|
67
|
+
clo = Master.adapters[adapter_name.to_sym].parse_command_line_options(opts,clo)
|
68
|
+
end
|
69
|
+
opts.on('--encryption-key KEY') do |key|
|
70
|
+
clo[:encryption_key] = key
|
71
|
+
end
|
72
|
+
end
|
73
|
+
@option_parser.parse!
|
74
|
+
|
75
|
+
return clo
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class IncompleteConfigurationError < RuntimeError; end
|
82
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'digest'
|
3
|
+
|
4
|
+
module CloudEncryptedSync
|
5
|
+
class Cryptographer
|
6
|
+
ALGORITHM = 'AES-256-CBC'
|
7
|
+
|
8
|
+
class << self
|
9
|
+
|
10
|
+
|
11
|
+
def encrypt_data(data)
|
12
|
+
iv = generate_random_iv
|
13
|
+
encrypted_data = crypt_data(:encrypt, iv, data)
|
14
|
+
return iv + encrypted_data
|
15
|
+
end
|
16
|
+
|
17
|
+
def decrypt_data(ivdata)
|
18
|
+
iv= ivdata.byteslice(0..15)
|
19
|
+
data = ivdata.byteslice(16..-1)
|
20
|
+
crypt_data(:decrypt, iv, data)
|
21
|
+
end
|
22
|
+
|
23
|
+
def hash_data(data)
|
24
|
+
Digest::SHA2.hexdigest(data,512)
|
25
|
+
end
|
26
|
+
|
27
|
+
def generate_random_key
|
28
|
+
initialized_cipher.random_key.unpack('H*')[0]
|
29
|
+
end
|
30
|
+
|
31
|
+
#######
|
32
|
+
private
|
33
|
+
#######
|
34
|
+
|
35
|
+
def generate_random_iv
|
36
|
+
initialized_cipher.random_iv
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialized_cipher(crypt = nil)
|
40
|
+
cipher = OpenSSL::Cipher::Cipher.new(ALGORITHM)
|
41
|
+
cipher.send(crypt) if crypt
|
42
|
+
return cipher
|
43
|
+
end
|
44
|
+
|
45
|
+
def setup_cipher(crypt,iv)
|
46
|
+
cipher = initialized_cipher(crypt)
|
47
|
+
cipher.key = hash_data(Configuration.settings[:encryption_key])
|
48
|
+
cipher.iv = iv
|
49
|
+
return cipher
|
50
|
+
end
|
51
|
+
|
52
|
+
def crypt_data(direction,iv,precrypted_data)
|
53
|
+
cipher = setup_cipher(direction,iv)
|
54
|
+
crypted_data = cipher.update(precrypted_data)
|
55
|
+
crypted_data << cipher.final
|
56
|
+
return crypted_data
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module CloudEncryptedSync
|
2
|
+
module Adapters
|
3
|
+
class Dummy < Template
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def write(data,key)
|
8
|
+
stored_data[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_name] = bucket_name
|
14
|
+
end
|
15
|
+
return command_line_options
|
16
|
+
end
|
17
|
+
|
18
|
+
def read(key)
|
19
|
+
raise "key doesn't exist" unless key_exists?(key)
|
20
|
+
stored_data[bucket_name][key]
|
21
|
+
end
|
22
|
+
|
23
|
+
def delete(key)
|
24
|
+
stored_data[bucket_name].delete(key)
|
25
|
+
end
|
26
|
+
|
27
|
+
def key_exists?(key)
|
28
|
+
stored_data[bucket_name][key] ? true : false
|
29
|
+
end
|
30
|
+
|
31
|
+
#######
|
32
|
+
private
|
33
|
+
#######
|
34
|
+
|
35
|
+
def stored_data
|
36
|
+
@stored_data ||= { bucket_name => {} }
|
37
|
+
end
|
38
|
+
|
39
|
+
def bucket_name
|
40
|
+
raise RuntimeError, Configuration.settings.inspect
|
41
|
+
Configuration.settings[:bucket_name].to_sym
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,248 @@
|
|
1
|
+
require 'find'
|
2
|
+
require 'active_support/core_ext/string'
|
3
|
+
|
4
|
+
module CloudEncryptedSync
|
5
|
+
class Master
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_accessor :finalize_required
|
9
|
+
attr_reader :command_line_options, :adapters
|
10
|
+
attr_writer :sync_path
|
11
|
+
|
12
|
+
def register(adapter)
|
13
|
+
@adapters ||= {}
|
14
|
+
name = adapter.name.match(/([^:]+)$/)[0].underscore.to_sym
|
15
|
+
raise RegistrationError, "#{name} already registered" if @adapters[name]
|
16
|
+
@adapters[name] = adapter
|
17
|
+
end
|
18
|
+
|
19
|
+
def activate!
|
20
|
+
find_and_require_adapters
|
21
|
+
sync
|
22
|
+
end
|
23
|
+
|
24
|
+
def sync
|
25
|
+
begin
|
26
|
+
CloudEncryptedSync::Master.delete_local_files!
|
27
|
+
CloudEncryptedSync::Master.delete_remote_files!
|
28
|
+
CloudEncryptedSync::Master.pull_files!
|
29
|
+
CloudEncryptedSync::Master.push_files!
|
30
|
+
CloudEncryptedSync::Master.finalize!
|
31
|
+
rescue IncompleteConfigurationError => exception
|
32
|
+
puts exception.message
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def push_files!
|
37
|
+
progress_meter = ProgressMeter.new(files_to_pull.keys.size,:label => 'Pushing Files: ')
|
38
|
+
pushed_files_counter = 0
|
39
|
+
files_to_push.each_pair do |key,relative_path|
|
40
|
+
if adapter.key_exists?(key)
|
41
|
+
#already exists. probably left over from an earlier aborted push
|
42
|
+
puts "Not Pushing (already exists): #{relative_path}"
|
43
|
+
else
|
44
|
+
puts "Pushing: #{relative_path}"
|
45
|
+
encrypt_to_adapter(File.read(full_file_path(relative_path)),key)
|
46
|
+
self.finalize_required = true
|
47
|
+
end
|
48
|
+
pushed_files_counter += 1
|
49
|
+
print progress_meter.update(pushed_files_counter)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def pull_files!
|
54
|
+
progress_meter = ProgressMeter.new(files_to_pull.keys.size,:label => 'Pulling Files: ')
|
55
|
+
pulled_files_counter = 0
|
56
|
+
files_to_pull.each_pair do |key,relative_path|
|
57
|
+
full_path = full_file_path(relative_path)
|
58
|
+
if File.exist?(full_path) and (file_key(full_path) == key)
|
59
|
+
#already exists. probably left over from an earlier aborted pull
|
60
|
+
puts "Not Pulling (already exists): #{path}"
|
61
|
+
else
|
62
|
+
Dir.mkdir(File.dirname(full_path)) unless File.exist?(File.dirname(full_path))
|
63
|
+
puts "Pulling: #{relative_path}"
|
64
|
+
begin
|
65
|
+
File.open(full_path,'w') { |file| file.write(decrypt_from_adapter(key)) }
|
66
|
+
self.finalize_required = true
|
67
|
+
rescue #AWS::S3::Errors::NoSuchKey Should provide error for adapters to raise
|
68
|
+
puts "Failed to pull #{relative_path}"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
pulled_files_counter += 1
|
72
|
+
print progress_meter.update(pulled_files_counter)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def delete_remote_files!
|
77
|
+
remote_files_to_delete.each_pair do |key,path|
|
78
|
+
puts "Deleting Remote: #{path}"
|
79
|
+
adapter.delete(key)
|
80
|
+
self.finalize_required = true
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def delete_local_files!
|
85
|
+
local_files_to_delete.each_pair do |key,relative_path|
|
86
|
+
full_path = full_file_path(relative_path)
|
87
|
+
if !File.exist?(full_path) or (file_key(full_path) == key)
|
88
|
+
puts "Not Deleting Local: #{relative_path}"
|
89
|
+
else
|
90
|
+
puts "Deleting Local: #{relative_path}"
|
91
|
+
File.delete(full_path)
|
92
|
+
self.finalize_required = true
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def finalize!
|
98
|
+
if finalize_required
|
99
|
+
store_directory_hash_file
|
100
|
+
File.open(snapshot_file_path, 'w') { |file| YAML.dump(directory_hash, file) }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
#######
|
105
|
+
private
|
106
|
+
#######
|
107
|
+
|
108
|
+
def find_and_require_adapters
|
109
|
+
latest_versions_of_installed_adapters.each_pair do |adapter_name,adapter_version|
|
110
|
+
require File.expand_path("../../../../cloud_encrypted_sync_#{adapter_name}_adapter-#{adapter_version}", __FILE__)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def latest_versions_of_installed_adapters
|
115
|
+
glob_path = '../../../../cloud_encrypted_sync_*_adapter-*/lib/*.rb'
|
116
|
+
Dir.glob(File.expand_path(glob_path,__FILE__)).inject({}) do |hash,adapter_path|
|
117
|
+
if adapter_path.match(/cloud_encrypted_sync_(.+)_adapter-(.+)/)
|
118
|
+
adapter_name = $1
|
119
|
+
adapter_version = $2
|
120
|
+
if hash[adapter_name].to_s < adapter_version
|
121
|
+
hash[adapter_name] = adapter_version
|
122
|
+
end
|
123
|
+
end
|
124
|
+
hash
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def adapter
|
129
|
+
@adapters[Configuration.settings[:adapter_name].to_sym]
|
130
|
+
end
|
131
|
+
|
132
|
+
def encrypt_to_adapter(data,key)
|
133
|
+
adapter.write(Cryptographer.encrypt_data(data),key)
|
134
|
+
end
|
135
|
+
|
136
|
+
def decrypt_from_adapter(key)
|
137
|
+
Cryptographer.decrypt_data(adapter.read(key))
|
138
|
+
end
|
139
|
+
|
140
|
+
def directory_hash
|
141
|
+
return @directory_hash if @directory_hash
|
142
|
+
@directory_hash = {}
|
143
|
+
progress_meter = ProgressMeter.new(Dir["#{normalized_sync_path}/**/*"].length,:label => 'Compiling Directory Analysis: ')
|
144
|
+
completed_files = 0
|
145
|
+
Find.find(normalized_sync_path) do |path|
|
146
|
+
print progress_meter.update(completed_files)
|
147
|
+
if FileTest.directory?(path)
|
148
|
+
completed_files += 1
|
149
|
+
next
|
150
|
+
else
|
151
|
+
@directory_hash[file_key(path)] = relative_file_path(path)
|
152
|
+
completed_files += 1
|
153
|
+
end
|
154
|
+
end
|
155
|
+
puts
|
156
|
+
return @directory_hash
|
157
|
+
end
|
158
|
+
|
159
|
+
def directory_key
|
160
|
+
@directory_key ||= Cryptographer.hash_data(Configuration.settings[:encryption_key])
|
161
|
+
end
|
162
|
+
|
163
|
+
def normalized_sync_path
|
164
|
+
@normalized_sync_path ||= normalize_sync_path
|
165
|
+
end
|
166
|
+
|
167
|
+
def normalize_sync_path
|
168
|
+
path = Configuration.settings[:sync_path]
|
169
|
+
if path.match(/\/$/)
|
170
|
+
return path
|
171
|
+
else
|
172
|
+
return path + '/'
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def last_sync_date
|
177
|
+
@last_sync_date ||= File.exist?(snapshot_file_path) ? File.stat(snapshot_file_path).ctime : nil
|
178
|
+
end
|
179
|
+
|
180
|
+
def last_sync_hash
|
181
|
+
@last_sync_hash ||= File.exist?(snapshot_file_path) ? YAML.load(File.read(snapshot_file_path)) : {}
|
182
|
+
end
|
183
|
+
|
184
|
+
def files_to_push
|
185
|
+
syncable_files_check(directory_hash,remote_directory_hash)
|
186
|
+
end
|
187
|
+
|
188
|
+
def files_to_pull
|
189
|
+
syncable_files_check(remote_directory_hash,directory_hash)
|
190
|
+
end
|
191
|
+
|
192
|
+
def remote_files_to_delete
|
193
|
+
deletable_files_check(remote_directory_hash,directory_hash)
|
194
|
+
end
|
195
|
+
|
196
|
+
def local_files_to_delete
|
197
|
+
deletable_files_check(directory_hash,remote_directory_hash)
|
198
|
+
end
|
199
|
+
|
200
|
+
def remote_directory_hash
|
201
|
+
@remote_directory_hash ||= begin
|
202
|
+
YAML.parse(decrypt_from_adapter(directory_key)).to_ruby
|
203
|
+
rescue #AWS::S3::Errors::NoSuchKey should provide error for adapters to raise
|
204
|
+
{}
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def store_directory_hash_file
|
209
|
+
@directory_hash = nil #force re-compile before pushing to remote
|
210
|
+
encrypt_to_adapter(directory_hash.to_yaml,directory_key)
|
211
|
+
end
|
212
|
+
|
213
|
+
def deletable_files_check(source_hash,comparison_hash)
|
214
|
+
combined_file_check(source_hash,comparison_hash,true)
|
215
|
+
end
|
216
|
+
|
217
|
+
def syncable_files_check(source_hash,comparison_hash)
|
218
|
+
combined_file_check(source_hash,comparison_hash,false)
|
219
|
+
end
|
220
|
+
|
221
|
+
def combined_file_check(source_hash,comparison_hash,last_sync_has_key)
|
222
|
+
source_hash.select{|k,v| !comparison_hash.has_key?(k) and (last_sync_has_key ? last_sync_hash.has_key?(k) : !last_sync_hash.has_key?(k)) }
|
223
|
+
end
|
224
|
+
|
225
|
+
def snapshot_file_path
|
226
|
+
"#{Configuration.data_folder_path}/#{snapshot_filename}"
|
227
|
+
end
|
228
|
+
|
229
|
+
def snapshot_filename
|
230
|
+
"#{normalized_sync_path.gsub(/[^A-Za-z0-9]/,'_')}.snapshot.yml"
|
231
|
+
end
|
232
|
+
|
233
|
+
def file_key(full_path)
|
234
|
+
Cryptographer.hash_data(relative_file_path(full_path) + File.open(full_path).read).to_s
|
235
|
+
end
|
236
|
+
|
237
|
+
def relative_file_path(full_path)
|
238
|
+
full_path.gsub(normalized_sync_path,'')
|
239
|
+
end
|
240
|
+
|
241
|
+
def full_file_path(relative_path)
|
242
|
+
normalized_sync_path+'/'+relative_path
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
class RegistrationError < RuntimeError; end
|
248
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module CloudEncryptedSync
|
2
|
+
class ProgressMeter
|
3
|
+
attr_accessor :completed_index
|
4
|
+
attr_reader :max_index, :start_time, :label
|
5
|
+
|
6
|
+
def initialize(max_index,options = {})
|
7
|
+
@max_index = max_index.to_f
|
8
|
+
@label = options[:label] || ''
|
9
|
+
@completed_index = 0.0
|
10
|
+
@start_time = Time.now
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
sprintf("\r#{label}%0.1f%% Complete. Time Remaining %s", percent_completed, estimated_time_remaining.strftime('%M:%S'))
|
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
|
23
|
+
end
|
24
|
+
|
25
|
+
def estimated_finish_time
|
26
|
+
if percent_completed > 0
|
27
|
+
start_time + ((100/percent_completed)*time_elapsed)
|
28
|
+
else
|
29
|
+
start_time + 3600
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def estimated_time_remaining
|
34
|
+
Time.at(estimated_finish_time - Time.now)
|
35
|
+
end
|
36
|
+
|
37
|
+
def update(completed_index)
|
38
|
+
self.completed_index = completed_index
|
39
|
+
return self
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'simplecov'
|
4
|
+
require 'fakefs/safe'
|
5
|
+
require 'active_support/test_case'
|
6
|
+
require 'test/unit'
|
7
|
+
require 'etc'
|
8
|
+
|
9
|
+
SimpleCov.start
|
10
|
+
|
11
|
+
require 'cloud_encrypted_sync'
|
12
|
+
|
13
|
+
module CloudEncryptedSync
|
14
|
+
class ActiveSupport::TestCase
|
15
|
+
|
16
|
+
setup :activate_fake_fs
|
17
|
+
setup :preset_environment
|
18
|
+
setup :capture_stdout
|
19
|
+
teardown :deactivate_fake_fs
|
20
|
+
teardown :release_stdout
|
21
|
+
|
22
|
+
def preset_environment
|
23
|
+
Configuration.instance_variable_set(:@settings,nil)
|
24
|
+
Configuration.instance_variable_set(:@command_line_options,nil)
|
25
|
+
Master.instance_variable_set(:@directory_hash, nil)
|
26
|
+
FileUtils.mkdir_p test_source_folder
|
27
|
+
FileUtils.mkdir_p test_source_folder + '/test_sub_folder'
|
28
|
+
File.open(test_source_folder + '/test_sub_folder/test_file_one.txt', 'w') do |test_file|
|
29
|
+
test_file.write('Test File One')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_source_folder
|
34
|
+
@test_source_folder ||= File.expand_path('../test_folder', __FILE__)
|
35
|
+
end
|
36
|
+
|
37
|
+
def activate_fake_fs
|
38
|
+
FakeFS.activate!
|
39
|
+
FakeFS::FileSystem.clear
|
40
|
+
end
|
41
|
+
|
42
|
+
def deactivate_fake_fs
|
43
|
+
FakeFS.deactivate!
|
44
|
+
end
|
45
|
+
|
46
|
+
#Capture STDOUT from program for testing and not cluttering test output
|
47
|
+
def capture_stdout
|
48
|
+
@stdout = $stdout
|
49
|
+
$stdout = StringIO.new
|
50
|
+
end
|
51
|
+
|
52
|
+
def release_stdout
|
53
|
+
$stdout = @stdout
|
54
|
+
end
|
55
|
+
|
56
|
+
#Redirect intentional puts from within tests to the real STDOUT for troublshooting purposes.
|
57
|
+
def puts(*args)
|
58
|
+
@stdout.puts(*args)
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
require 'mocha'
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module CloudEncryptedSync
|
4
|
+
class ConfigurationTest < ActiveSupport::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
Configuration.instance_variable_set(:@command_line_options,nil)
|
8
|
+
Configuration.instance_variable_set(:@settings,nil)
|
9
|
+
Configuration.instance_variable_set(:@option_parser,nil)
|
10
|
+
Object.send(:remove_const,:ARGV) #if defined?(::ARGV)
|
11
|
+
end
|
12
|
+
|
13
|
+
test 'should parse command line options' do
|
14
|
+
::ARGV = '--adapter dummy --bucket foobar --data-dir ~/test/folder --encryption-key somestringofcharacters /some/path'.split(/\s/)
|
15
|
+
settings = Configuration.settings
|
16
|
+
assert_equal('dummy',settings[:adapter_name])
|
17
|
+
assert_equal('~/test/folder',settings[:data_dir])
|
18
|
+
assert_equal('somestringofcharacters',settings[:encryption_key])
|
19
|
+
assert_equal('foobar',settings[:bucket_name])
|
20
|
+
end
|
21
|
+
|
22
|
+
test 'should gracefully fail without path in ARGV' do
|
23
|
+
::ARGV = '--adapter dummy --bucket foobar'.split(/\s/)
|
24
|
+
assert_raise(IncompleteConfigurationError) { Configuration.settings }
|
25
|
+
end
|
26
|
+
|
27
|
+
test 'should gracefully fail when not provided encryption_key and vector provided path in ARGV' do
|
28
|
+
::ARGV = '--adapter dummy --bucket foobar /some/path/to/sync'.split(/\s/)
|
29
|
+
assert_raise(IncompleteConfigurationError) { Configuration.settings }
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module CloudEncryptedSync
|
4
|
+
class CryptographerTest < ActiveSupport::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
Configuration.stubs(:settings).returns({
|
8
|
+
:encryption_key => 'asdf',
|
9
|
+
:initialization_vector => 'qwerty',
|
10
|
+
:adapter_name => 'dummy',
|
11
|
+
:bucket => "test-bucket",
|
12
|
+
:data_dir => "#{Etc.getpwuid.dir}/.cloud_encrypted_sync",
|
13
|
+
:sync_path => test_source_folder
|
14
|
+
})
|
15
|
+
end
|
16
|
+
|
17
|
+
test 'should hash data' do
|
18
|
+
hash = Cryptographer.hash_data('abc123')
|
19
|
+
assert_equal('c70b5dd9ebfb6f51d09d4132b7170c9d20750a7852f00680f65658f0310e810056e6763c34c9a00b0e940076f54495c169fc2302cceb312039271c43469507dc',hash)
|
20
|
+
end
|
21
|
+
|
22
|
+
test 'should encrypt and decrypt data' do
|
23
|
+
unencrypted_data = "123xyz"
|
24
|
+
encrypted_data = Cryptographer.encrypt_data(unencrypted_data)
|
25
|
+
decrypted_data = Cryptographer.decrypt_data(encrypted_data)
|
26
|
+
assert_equal(unencrypted_data,decrypted_data)
|
27
|
+
end
|
28
|
+
|
29
|
+
test 'test should generate random key' do
|
30
|
+
assert_equal(String,Cryptographer.generate_random_key.class)
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module CloudEncryptedSync
|
5
|
+
class MasterTest < ActiveSupport::TestCase
|
6
|
+
|
7
|
+
def setup
|
8
|
+
Configuration.stubs(:settings).returns({
|
9
|
+
:encryption_key => 'asdf',
|
10
|
+
:initialization_vector => 'qwerty',
|
11
|
+
:adapter_name => 'dummy',
|
12
|
+
:bucket => "test-bucket",
|
13
|
+
:sync_path => test_source_folder
|
14
|
+
})
|
15
|
+
Configuration.stubs(:data_folder_path).returns("#{Etc.getpwuid.dir}/.cloud_encrypted_sync")
|
16
|
+
end
|
17
|
+
|
18
|
+
test 'should generate directory hash' do
|
19
|
+
assert_equal('',$stdout.string)
|
20
|
+
hash = Master.send(:directory_hash)
|
21
|
+
assert_match(/\% Complete/,$stdout.string)
|
22
|
+
assert_equal(1,hash.keys.size)
|
23
|
+
assert_equal('test_sub_folder/test_file_one.txt',hash[hash.keys.first])
|
24
|
+
end
|
25
|
+
|
26
|
+
test 'should_return_nil_if_never_synced_before' do
|
27
|
+
Master.stubs(:snapshot_file_path).returns('/non/existant/file')
|
28
|
+
assert_equal(nil,Master.send(:last_sync_date))
|
29
|
+
end
|
30
|
+
|
31
|
+
test 'should want to push everything on first run with local files and empty remote' do
|
32
|
+
Master.stubs(:remote_directory_hash).returns({})
|
33
|
+
Master.stubs(:directory_hash).returns({"old_file_key"=>"test_sub_folder/old_file.txt"})
|
34
|
+
Master.stubs(:last_sync_hash).returns({})
|
35
|
+
assert_equal(Master.directory_hash,Master.send(:files_to_push))
|
36
|
+
end
|
37
|
+
|
38
|
+
test 'should encrypt when writing' do
|
39
|
+
precrypted_data = File.read(test_source_folder + '/test_sub_folder/test_file_one.txt')
|
40
|
+
key = Cryptographer.hash_data('test_file_key')
|
41
|
+
Adapters::Dummy.expects(:write).with(anything,key).returns(true)
|
42
|
+
Master.send(:encrypt_to_adapter,precrypted_data,key)
|
43
|
+
end
|
44
|
+
|
45
|
+
test 'should decrypt_when_reading' do
|
46
|
+
precrypted_data = File.read(test_source_folder + '/test_sub_folder/test_file_one.txt')
|
47
|
+
encrypted_data = Cryptographer.encrypt_data(precrypted_data)
|
48
|
+
key = Cryptographer.hash_data('test_file_key')
|
49
|
+
Adapters::Dummy.expects(:read).with(key).returns(encrypted_data)
|
50
|
+
assert_equal(precrypted_data,Master.send(:decrypt_from_adapter,key))
|
51
|
+
end
|
52
|
+
|
53
|
+
test 'should push files' do
|
54
|
+
Master.stubs(:remote_directory_hash).returns({})
|
55
|
+
Master.stubs(:last_sync_hash).returns({})
|
56
|
+
Adapters::Dummy.stubs(:key_exists?).returns(false)
|
57
|
+
Adapters::Dummy.expects(:write).with(any_parameters).returns(true)
|
58
|
+
assert_equal('',$stdout.string)
|
59
|
+
Master.push_files!
|
60
|
+
assert_match(/\% Complete/,$stdout.string)
|
61
|
+
end
|
62
|
+
|
63
|
+
test 'should want to pull everything on first run with remote files and empty local' do
|
64
|
+
Master.stubs(:remote_directory_hash).returns({'new_file_key' => 'test_sub_folder/new_file.txt'})
|
65
|
+
Master.stubs(:directory_hash).returns({})
|
66
|
+
Master.stubs(:last_sync_hash).returns({})
|
67
|
+
assert_equal({'new_file_key' => 'test_sub_folder/new_file.txt'},Master.send(:files_to_pull))
|
68
|
+
end
|
69
|
+
|
70
|
+
test 'should pull files' do
|
71
|
+
Master.stubs(:remote_directory_hash).returns({'new_file_key' => 'test_sub_folder/new_file.txt'})
|
72
|
+
Master.stubs(:directory_hash).returns({})
|
73
|
+
Master.stubs(:last_sync_hash).returns({})
|
74
|
+
Adapters::Dummy.expects(:read).with('new_file_key').returns(Cryptographer.encrypt_data('foobar'))
|
75
|
+
assert_equal('',$stdout.string)
|
76
|
+
assert_difference('Dir["#{test_source_folder}/**/*"].length') do
|
77
|
+
Master.pull_files!
|
78
|
+
end
|
79
|
+
assert_match(/\% Complete/,$stdout.string)
|
80
|
+
end
|
81
|
+
|
82
|
+
test 'should only want to push new files on later run' do
|
83
|
+
Master.stubs(:remote_directory_hash).returns({'old_file_key' => 'test_sub_folder/old_file.txt'})
|
84
|
+
Master.stubs(:directory_hash).returns({'new_file_key' => 'test_sub_folder/new_file.txt', 'old_file_key' => 'test_sub_folder/old_file.txt'})
|
85
|
+
Master.stubs(:last_sync_hash).returns({'old_file_key' => 'test_sub_folder/old_file.txt'})
|
86
|
+
assert_equal({'new_file_key' => 'test_sub_folder/new_file.txt'},Master.send(:files_to_push))
|
87
|
+
end
|
88
|
+
|
89
|
+
test 'should want to pull new files from cloud' do
|
90
|
+
Master.stubs(:remote_directory_hash).returns({'new_file_key' => 'test_sub_folder/new_file.txt', 'old_file_key' => 'test_sub_folder/old_file.txt'})
|
91
|
+
Master.stubs(:directory_hash).returns({'old_file_key' => 'test_sub_folder/old_file.txt'})
|
92
|
+
Master.stubs(:last_sync_hash).returns({'old_file_key' => 'test_sub_folder/old_file.txt'})
|
93
|
+
assert_equal({'new_file_key' => 'test_sub_folder/new_file.txt'},Master.send(:files_to_pull))
|
94
|
+
end
|
95
|
+
|
96
|
+
test 'should want to delete locally missing files from cloud' do
|
97
|
+
Master.stubs(:remote_directory_hash).returns({'saved_file_key' => 'test_sub_folder/saved_file.txt', 'deleted_file_key' => 'test_sub_folder/deleted_file.txt'})
|
98
|
+
Master.stubs(:directory_hash).returns({'saved_file_key' => 'test_sub_folder/saved_file.txt'})
|
99
|
+
Master.stubs(:last_sync_hash).returns({'saved_file_key' => 'test_sub_folder/saved_file.txt', 'deleted_file_key' => 'test_sub_folder/deleted_file.txt'})
|
100
|
+
assert_equal({'deleted_file_key' => 'test_sub_folder/deleted_file.txt'},Master.send(:remote_files_to_delete))
|
101
|
+
end
|
102
|
+
|
103
|
+
test 'should delete files from cloud' do
|
104
|
+
Master.stubs(:remote_directory_hash).returns({'saved_file_key' => 'test_sub_folder/saved_file.txt', 'deleted_file_key' => 'test_sub_folder/deleted_file.txt'})
|
105
|
+
Master.stubs(:directory_hash).returns({'saved_file_key' => 'test_sub_folder/saved_file.txt'})
|
106
|
+
Master.stubs(:last_sync_hash).returns({'saved_file_key' => 'test_sub_folder/saved_file.txt', 'deleted_file_key' => 'test_sub_folder/deleted_file.txt'})
|
107
|
+
Adapters::Dummy.expects(:delete).with('deleted_file_key').returns(true)
|
108
|
+
Master.delete_remote_files!
|
109
|
+
end
|
110
|
+
|
111
|
+
test 'should want to delete appropriate files locally' do
|
112
|
+
Master.stubs(:remote_directory_hash).returns({'saved_file_key' => 'test_sub_folder/saved_file.txt'})
|
113
|
+
Master.stubs(:directory_hash).returns({'saved_file_key' => 'test_sub_folder/saved_file.txt', 'deleted_file_key' => 'test_sub_folder/deleted_file.txt'})
|
114
|
+
Master.stubs(:last_sync_hash).returns({'saved_file_key' => 'test_sub_folder/saved_file.txt', 'deleted_file_key' => 'test_sub_folder/deleted_file.txt'})
|
115
|
+
assert_equal({'deleted_file_key' => 'test_sub_folder/deleted_file.txt'},Master.send(:local_files_to_delete))
|
116
|
+
end
|
117
|
+
|
118
|
+
test 'should delete local files' do
|
119
|
+
Master.stubs(:remote_directory_hash).returns({'saved_file_key' => 'test_sub_folder/saved_file.txt'})
|
120
|
+
Master.stubs(:last_sync_hash).returns({'saved_file_key' => 'test_sub_folder/saved_file.txt', 'deleted_file_key' => 'test_sub_folder/deleted_file.txt'}.merge(Master.send(:directory_hash)))
|
121
|
+
assert_difference('Dir["#{test_source_folder}/**/*"].length',-1) do
|
122
|
+
Master.delete_local_files!
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
test 'should finalize' do
|
127
|
+
FileUtils.mkdir_p(Configuration.data_folder_path)
|
128
|
+
sample_directory_hash = {'sample_file_key' => 'test_sub_folder/sample_file.txt'}
|
129
|
+
Master.instance_variable_set(:@finalize_required,true)
|
130
|
+
Master.stubs(:directory_hash).returns(sample_directory_hash)
|
131
|
+
Adapters::Dummy.expects(:write).with(anything,Master.send(:directory_key)).returns(true)
|
132
|
+
Master.finalize!
|
133
|
+
end
|
134
|
+
|
135
|
+
test 'should decrypt remote directory file' do
|
136
|
+
#setup mock data
|
137
|
+
sample_directory_hash = {'sample_file_key' => 'test_sub_folder/sample_file.txt'}
|
138
|
+
encrypted_directory_hash = Cryptographer.encrypt_data(sample_directory_hash.to_yaml)
|
139
|
+
Adapters::Dummy.stubs(:read).with(Master.send(:directory_key)).returns(encrypted_directory_hash)
|
140
|
+
|
141
|
+
#do actual test
|
142
|
+
decrypted_remote_hash = Master.send(:remote_directory_hash)
|
143
|
+
assert_equal(sample_directory_hash,decrypted_remote_hash)
|
144
|
+
end
|
145
|
+
|
146
|
+
test 'should puts error message to stdout' do
|
147
|
+
Configuration.stubs(:settings).raises(IncompleteConfigurationError,'test message')
|
148
|
+
assert_equal('',$stdout.string)
|
149
|
+
Master.expects(:pull_files).never
|
150
|
+
Master.sync
|
151
|
+
assert_match(/test message/,$stdout.string)
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module CloudEncryptedSync
|
4
|
+
class ProgressMeterTest < ActiveSupport::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@progress_meter = ProgressMeter.new(4)
|
8
|
+
@progress_meter.instance_variable_set(:@start_time,Time.now-42)
|
9
|
+
end
|
10
|
+
|
11
|
+
test 'should calculate percent completed' do
|
12
|
+
@progress_meter.update(1)
|
13
|
+
assert_equal(25,@progress_meter.percent_completed)
|
14
|
+
end
|
15
|
+
|
16
|
+
test 'should calculate time elapsed' do
|
17
|
+
assert_in_delta(42,@progress_meter.time_elapsed,0.01)
|
18
|
+
end
|
19
|
+
|
20
|
+
test 'should estimate finish time' do
|
21
|
+
@progress_meter.update(1)
|
22
|
+
assert_in_delta(Time.now+(42*3),@progress_meter.estimated_finish_time,0.01)
|
23
|
+
end
|
24
|
+
|
25
|
+
test 'should estimate time remaining' do
|
26
|
+
@progress_meter.update(1)
|
27
|
+
assert_in_delta((42*3),@progress_meter.estimated_time_remaining.to_f,0.01)
|
28
|
+
end
|
29
|
+
|
30
|
+
test 'should update progress and return self' do
|
31
|
+
assert_difference('@progress_meter.completed_index',2) do
|
32
|
+
assert_equal(@progress_meter,@progress_meter.update(2))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
test 'should render string' do
|
37
|
+
assert_match(/0\% Complete/,@progress_meter.to_s)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
metadata
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cloud_encrypted_sync
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jonathan S. Garvin
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-11-14 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: mocha
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: simplecov
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: fakefs
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: activesupport
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
description: Encrypted sync of folder contents to/from cloud storage with user controlled
|
79
|
+
encryption keys.
|
80
|
+
email:
|
81
|
+
- jon@5valleys.com
|
82
|
+
executables:
|
83
|
+
- ces
|
84
|
+
extensions: []
|
85
|
+
extra_rdoc_files: []
|
86
|
+
files:
|
87
|
+
- .gitignore
|
88
|
+
- Gemfile
|
89
|
+
- Gemfile.lock
|
90
|
+
- LICENSE
|
91
|
+
- README.md
|
92
|
+
- Rakefile
|
93
|
+
- bin/ces
|
94
|
+
- cloud_encrypted_sync.gemspec
|
95
|
+
- lib/cloud_encrypted_sync.rb
|
96
|
+
- lib/cloud_encrypted_sync/adapter_template.rb
|
97
|
+
- lib/cloud_encrypted_sync/configuration.rb
|
98
|
+
- lib/cloud_encrypted_sync/cryptographer.rb
|
99
|
+
- lib/cloud_encrypted_sync/dummy_adapter.rb
|
100
|
+
- lib/cloud_encrypted_sync/master.rb
|
101
|
+
- lib/cloud_encrypted_sync/progress_meter.rb
|
102
|
+
- lib/cloud_encrypted_sync/version.rb
|
103
|
+
- test/test_helper.rb
|
104
|
+
- test/unit/configuration_test.rb
|
105
|
+
- test/unit/cryptographer_test.rb
|
106
|
+
- test/unit/master_test.rb
|
107
|
+
- test/unit/progress_meter_test.rb
|
108
|
+
homepage: https://github.com/jsgarvin/cloud_encrypted_sync
|
109
|
+
licenses: []
|
110
|
+
post_install_message:
|
111
|
+
rdoc_options: []
|
112
|
+
require_paths:
|
113
|
+
- lib
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
115
|
+
none: false
|
116
|
+
requirements:
|
117
|
+
- - ! '>='
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
requirements: []
|
127
|
+
rubyforge_project:
|
128
|
+
rubygems_version: 1.8.24
|
129
|
+
signing_key:
|
130
|
+
specification_version: 3
|
131
|
+
summary: Encrypted sync of folder contents to/from cloud storage.
|
132
|
+
test_files:
|
133
|
+
- test/test_helper.rb
|
134
|
+
- test/unit/configuration_test.rb
|
135
|
+
- test/unit/cryptographer_test.rb
|
136
|
+
- test/unit/master_test.rb
|
137
|
+
- test/unit/progress_meter_test.rb
|