cloud_encrypted_sync 0.1.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/.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
|