odisk 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/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2012, Peter Ohler
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ - Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ - Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ - Neither the name of Peter Ohler nor the names of its contributors may be
15
+ used to endorse or promote products derived from this software without
16
+ specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,150 @@
1
+ oDisk
2
+ =====
3
+
4
+ Remote Encrypted File Synchronization, oDisk
5
+
6
+ oDisk is a file synchronization application. The need for oDisk came from the
7
+ discontinuation of Apple's iDisk. oDisk provides a means to backup to a remote
8
+ host. oDisk encrypts and compresses backups to save space and provide a layer
9
+ of security not found in iDisk or other backup schemes. Backups are stored as
10
+ individual files to provide finer granularity in recover if everything else
11
+ falls apart.
12
+
13
+ oDisk is also a demonstration of the [Opee](http://www.ohler.com/opee) gem
14
+ which utilizes an alternative approach to dealing with multiple threads.
15
+
16
+ ## <a name="source">Source</a>
17
+
18
+ *GitHub* *repo*: https://github.com/ohler55/odisk
19
+
20
+ *RubyGems* *repo*: https://rubygems.org/gems/odisk
21
+
22
+ ## <a name="links">Links of Interest</a>
23
+
24
+ [Object-based Parallel Evaluation Environment](http://www.ohler.com/opee) the gem oDisk is built on.
25
+
26
+ ## <a name="release">Release Notes</a>
27
+
28
+ ### Release 0.2.0
29
+
30
+ - Renamed Orefs to oDisk.
31
+
32
+ # Plans and Notes
33
+
34
+ oDisk has only recently been opened up for public viewing. It is barely ready
35
+ for use. I am using it to backup some financial records but I am also keeping
36
+ copies on more than one computer in case something fails. So far the basic
37
+ backup functionality works with backups encrypted and compressed on a remote
38
+ server. Adding new files and making modifications works just fine. Removing
39
+ files has not been implemented yet and changing ownership or mode only take
40
+ effect if the file is touched as well. Feel free to give it a try and let me
41
+ know when you run into bugs.
42
+
43
+ - Support exclusion of file and directories
44
+ - specify on command line and create .odisk/exclude file
45
+ - use ::File.fnmatch(pattern, path, ::File::FNM_DOTMATCH)
46
+ - pass array of excludes to digest creation
47
+ - loosen up current restriction on any file that begins with .
48
+
49
+ - Support file removal
50
+ - Detect file removal based on previous digest or by calling a remove script
51
+ - Keep record of removals in digest
52
+ - Note conflicts if modifications are more recent that removal
53
+ - Use a script to pick remove or keep
54
+
55
+ - Handle changes in mode, owner, and group
56
+ - Compare to previous digest to detect changes
57
+ - File modification times are not changes by mode, owner, or group changes
58
+ - Note conflicts if modifications are more recent than remote
59
+ - Use a script to pick change or keep local version
60
+
61
+ - Add progress tracker
62
+ - Have component pass info to progress actor which will update progress
63
+ - Planner sends info on all changes along with sizes and comp/crypt flags to progress actor
64
+ - come up with algoritm for estimating time based on size and flags
65
+ - comp, crypt, and transfer send status to progress actor
66
+ - Optional terminal display
67
+
68
+ - Support backgroup application with web front end (much later)
69
+
70
+ ## Installation
71
+
72
+ Installation requires Ruby 1.9.3. After that, install the odisk gem.
73
+
74
+ gem install odisk
75
+
76
+ The net-ssh and net-sftp gems are also needed as are the oj and opee gems.
77
+
78
+ gem install net-ssh
79
+ gem install net-sftp
80
+ gem install oj
81
+ gem install opee
82
+
83
+ GnuPG must be installed. It can be down loaded from
84
+ [GnuPG.org](http://www.gnupg.org). Follow the instruction on
85
+ [GnuPG.org](http://www.gnupg.org) site for installation.
86
+
87
+ oDisk is now ready to use. ssh and sftp must be running on the remote site and
88
+ credentials must be installed so that the user is not prompted for a password
89
+ when using ssh or sftp.
90
+
91
+ ## Usage
92
+
93
+ After a directory has been selected for backing up *odisk* can be run to
94
+ copy the directory to a remote server. For purposes of this description the
95
+ directory to be backed up is *~/backup*.
96
+
97
+ A passphrase file will also be needed for encryption. The recommended location
98
+ is in a *~/.odisk* directory. The contents of the file will be the passphrase
99
+ for *gpg*.
100
+
101
+ Make sure you have a remote server that has an sftp and ssh daemon
102
+ running. Your credentials must be set in the authorized_keys file. If you can
103
+ login without a password it is set up correctly.
104
+
105
+ The first time *odisk* is used to backup a directory information about the
106
+ remote server must be provided. After the first time that information is not
107
+ needed again. Alternatively a *~/.odisk/remotes* file can be set up before
108
+ running *odisk*.
109
+
110
+ To backup to *my_server.remote.com* for user *me* to the *backup* directory on
111
+ the remote server with a passphrase file of *~/.odisk/backup.pass* the
112
+ following command should be executed.
113
+
114
+ odisk -r me@my_server.remote.com:~/.odisk/backup.pass ~/backup
115
+
116
+ A file named *~/backup/.odisk/remote* will be created with the connection
117
+ information for future invocations so that the next time a backup is made on
118
+ the *~/backup* directory the command only needs to be:
119
+
120
+ odisk ~/backup
121
+
122
+ ## License:
123
+
124
+ Copyright (c) 2012, Peter Ohler
125
+ All rights reserved.
126
+
127
+ Redistribution and use in source and binary forms, with or without
128
+ modification, are permitted provided that the following conditions are met:
129
+
130
+ - Redistributions of source code must retain the above copyright notice, this
131
+ list of conditions and the following disclaimer.
132
+
133
+ - Redistributions in binary form must reproduce the above copyright notice,
134
+ this list of conditions and the following disclaimer in the documentation
135
+ and/or other materials provided with the distribution.
136
+
137
+ - Neither the name of Peter Ohler nor the names of its contributors may be
138
+ used to endorse or promote products derived from this software without
139
+ specific prior written permission.
140
+
141
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
142
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
143
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
144
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
145
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
146
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
147
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
148
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
149
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
150
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env ruby -wW1
2
+ # encoding: UTF-8
3
+
4
+ while (i = ARGV.index('-I'))
5
+ x,path = ARGV.slice!(i, 2)
6
+ $: << path
7
+ end
8
+
9
+ # TBD tmp for testing
10
+ $: << ::File.join(::File.dirname(__FILE__), "../../oj/lib")
11
+ $: << ::File.join(::File.dirname(__FILE__), "../../oj/ext")
12
+ $: << ::File.join(::File.dirname(__FILE__), "../../opee/lib")
13
+ $: << ::File.join(::File.dirname(__FILE__), "../lib")
14
+
15
+ require 'optparse'
16
+ begin
17
+ v = $VERBOSE
18
+ $VERBOSE = false
19
+ require 'net/ssh'
20
+ require 'net/sftp'
21
+ $VERBOSE = v
22
+ end
23
+ require 'opee'
24
+ require 'oj'
25
+ require 'odisk'
26
+
27
+ $verbose = Logger::WARN
28
+ $dir = '.'
29
+ $digests_only = false
30
+ $dry_run = false
31
+ $master = nil
32
+ $plain = false
33
+ $remote = ::ODisk::Remote.new()
34
+ $crypter_count = 2
35
+ $copier_count = 4
36
+
37
+ opts = OptionParser.new('Usage: odisk [options] <local_directory>')
38
+ opts.on('-s', 'decrease verbosity') { $verbose += 1 unless 5 == $verbose }
39
+ opts.on('-v', 'increase verbosity') { $verbose -= 1 unless 0 == $verbose }
40
+ opts.on('-d', 'generate digests only') { $digests_only = true }
41
+ opts.on('-u', 'unencrypted or plain remote files') { $plain = true }
42
+ opts.on('-n', 'dry run / no modifications') { $dry_run = true }
43
+ opts.on('-r', '--remote [user@host:dir:pass_file]',
44
+ String, 'remote user, host, directory, passphrase file for gpg') { |r|
45
+ $remote.update(r)
46
+ }
47
+ opts.on('-m', '--master [local | remote]',
48
+ String, 'force master to local or remote') { |m|
49
+ case m
50
+ when 'local'
51
+ $master = ::ODisk::Planner::Step::LOCAL
52
+ when 'remote'
53
+ $master = ::ODisk::Planner::Step::REMOTE
54
+ else
55
+ puts opts.help
56
+ Process.exit!(0)
57
+ end
58
+ }
59
+ opts.on('-e', '--encrypt-count Integer', Integer, 'number of encryption actors') { |i| $crypter_count = i }
60
+ opts.on('-c', '--copier-count Integer', Integer, 'number of copier actors') { |i| $copier_count = i }
61
+ opts.on('-h', '--help', 'Show this display') { puts opts.help; Process.exit!(0) }
62
+ dirs = opts.parse(ARGV)
63
+
64
+ if 1 != dirs.size
65
+ puts opts.help
66
+ Process.exit!(0)
67
+ end
68
+ $local_top = ::File.expand_path(dirs[0])
69
+
70
+ # Walk up the directory tree looking for .odisk directories and a remote.json
71
+ # in that directory. If the directory does not exist then stop the walk. If
72
+ # not found check the ~/.odisk/remotes.json file for a matching top.
73
+ top = $local_top
74
+ if $ruser.nil? || $rhost.nil? || $rtop.nil?
75
+ while ::File.directory?(::File.join(top, '.odisk'))
76
+ rfile = ::File.join(top, '.odisk', 'remote')
77
+ if ::File.file?(rfile)
78
+ rstr = ::File.read(rfile).strip()
79
+ #$remote.pass_file = ::File.expand_path($remote.pass_file) unless $remote.pass_file.nil? || $remote.pass_file.empty?
80
+ orig_pass_file = $remote.pass_file
81
+ $remote.update(rstr)
82
+ $remote.pass_file = ::File.expand_path($remote.pass_file) unless $remote.pass_file.nil? || $remote.pass_file.empty?
83
+ if !$remote.dir.nil? && !$remote.dir.empty? && top != $local_top
84
+ $remote.dir = $remote.dir + $local_top[top.size..-1]
85
+ end
86
+ if $remote.pass_file != orig_pass_file && !::File.file?($remote.pass_file)
87
+ $remote.pass_file = ::File.join(top, '.odisk', $remote.pass_file)
88
+ end
89
+ break
90
+ end
91
+ top = ::File.dirname(top)
92
+ end
93
+ end
94
+
95
+ ODisk.info_from_remotes($local_top, $remote) unless $remote.complete?
96
+ $remote.user = ENV['USER'] if $remote.user.nil?
97
+ $remote.pass_file = nil if $plain
98
+ unless $remote.okay?
99
+ puts "*** user@host:top_dir not specified on command line, in local .odisk/remote file, or in ~/.odisk/remotes"
100
+ puts opts.help
101
+ Process.exit!(0)
102
+ end
103
+
104
+ Thread.current[:name] = 'main'
105
+ ::Opee::Env.logger.formatter = proc { |s,t,p,m|
106
+ s = '' if s.nil?
107
+ "#{s[0]} [#{t.strftime('%Y-%m-%dT%H:%M:%S.%6N')} ##{p}]: #{m}\n"
108
+ }
109
+ ::Opee::Env.logger.severity = $verbose
110
+
111
+ if Logger::INFO >= $verbose
112
+ if $digests_only
113
+ ::Opee::Env.info(%{
114
+ Generate Local Digests
115
+ local directory: #{::File.expand_path($local_top)}
116
+ })
117
+ else
118
+ ::Opee::Env.info(%{
119
+ Synchronize
120
+ remote host: #{$remote.host}
121
+ remote user: #{$remote.user}
122
+ remote directory: #{$remote.dir}
123
+ local: #{::File.expand_path($local_top)}
124
+ dry run: #{$dry_run}
125
+ master: #{$master.nil? ? 'NONE' : (::ODisk::Planner::Step::LOCAL == $master ? 'LOCAL' : 'REMOTE')}
126
+ })
127
+ end
128
+ end
129
+
130
+ # If $local_top/.odisk/remote does not exist or is different that what is in $remote, replace it.
131
+ remote_str = $remote.to_s
132
+ top_remote_path = ::File.join($local_top, '.odisk', 'remote')
133
+ if !::File.file?(top_remote_path) || ::File.read(top_remote_path).strip() != remote_str
134
+ ::Opee::Env.info("Writing #{top_remote_path}")
135
+ unless $dry_run
136
+ `mkdir -p #{::File.join($local_top, '.odisk')}`
137
+ ::File.open(top_remote_path, 'w') { |f| f.write(remote_str + "\n") }
138
+ end
139
+ end
140
+
141
+ fetcher = nil
142
+ planner_inputs = [:digester, :starter]
143
+
144
+ fixer = ::ODisk::StatFixer.new(:name => 'Fixer')
145
+ dir_wq = ::Opee::WorkQueue.new(:method => :start, :name => 'DirWorkQueue')
146
+ if $digests_only
147
+ copy_wq = nil
148
+ crypt_wq = nil
149
+ else
150
+ copy_wq = ::Opee::AskQueue.new(:name => 'CopyQueue')
151
+ crypt_wq = ::Opee::AskQueue.new(:name => 'CryptQueue')
152
+ $copier_count.times { |i|
153
+ ::ODisk::Copier.new(:name => "Copier-#{i}",
154
+ :crypt_queue => crypt_wq,
155
+ :copy_queue => copy_wq,
156
+ :fixer => fixer)
157
+ }
158
+ $crypter_count.times { |i|
159
+ ::ODisk::Crypter.new(:name => "Crypter-#{i}",
160
+ :crypt_queue => crypt_wq,
161
+ :copy_queue => copy_wq,
162
+ :fixer => fixer)
163
+ }
164
+ planner_inputs << :fetcher
165
+ end
166
+ planner = ::ODisk::Planner.new(:name => 'Planner',
167
+ :dir_queue => dir_wq,
168
+ :copy_queue => copy_wq,
169
+ :crypt_queue => crypt_wq,
170
+ :inputs => planner_inputs,
171
+ :fixer => fixer)
172
+ fetcher = ::ODisk::Fetcher.new(:name => 'Fetcher',
173
+ :collector => planner) unless $digests_only
174
+ digester = ::ODisk::Digester.new(:name => 'Digester',
175
+ :collector => planner)
176
+ starter = ::ODisk::SyncStarter.new(:name => 'Starter',
177
+ :dir_queue => dir_wq,
178
+ :digester => digester,
179
+ :fetcher => fetcher,
180
+ :collector => planner)
181
+
182
+ dir_wq.ask(:add, '')
183
+
184
+ ::Opee::Env.wait_close()
@@ -0,0 +1,46 @@
1
+
2
+ module ODisk
3
+
4
+ def self.info_from_remotes(local_dir, remote)
5
+ orig_pass_file = remote.pass_file
6
+ local_dir = ::File.expand_path(local_dir)
7
+ remotes_file = ::File.join(::File.expand_path('~'), '.odisk', 'remotes')
8
+ if ::File.file?(remotes_file)
9
+ ::File.readlines(remotes_file).each do |line|
10
+ l,r = line.split(':', 2)
11
+ l = ::File.expand_path(l)
12
+ if l == local_dir || local_dir.start_with?(l + '/')
13
+ remote.update(r)
14
+ remote.dir = remote.dir + remote.dir[l.size..-1] if l != local_dir && !remote.dir.nil?
15
+ if remote.pass_file != orig_pass_file && !::File.file?(remote.pass_file)
16
+ remote.pass_file = ::File.join(::File.expand_path('~'), '.odisk', remote.pass_file)
17
+ end
18
+ break
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ # data
27
+ require 'odisk/remote'
28
+ require 'odisk/info'
29
+ require 'odisk/dir'
30
+ require 'odisk/link'
31
+ require 'odisk/file'
32
+ require 'odisk/digest'
33
+ require 'odisk/diff'
34
+ # jobs
35
+ require 'odisk/dirsyncjob'
36
+ require 'odisk/statjob'
37
+ require 'odisk/syncjob'
38
+ # collectors
39
+ require 'odisk/statfixer'
40
+ require 'odisk/planner'
41
+ # actors
42
+ require 'odisk/copier'
43
+ require 'odisk/crypter'
44
+ require 'odisk/digester'
45
+ require 'odisk/fetcher'
46
+ require 'odisk/syncstarter'
@@ -0,0 +1,74 @@
1
+
2
+ module ODisk
3
+ class Copier < ::Opee::Actor
4
+
5
+ def initialize(options={})
6
+ @ftp = nil
7
+ @ssh = nil
8
+ super(options)
9
+ @copy_queue.ask(:ready, self)
10
+ end
11
+
12
+ def set_options(options)
13
+ super(options)
14
+ @copy_queue = options[:copy_queue]
15
+ @crypt_queue = options[:crypt_queue]
16
+ @fixer = options[:fixer]
17
+ end
18
+
19
+ def close()
20
+ @ftp.close_channel() unless @ftp.nil?
21
+ @ftp = nil
22
+ @ssh.close() unless @ssh.nil?
23
+ super()
24
+ end
25
+
26
+ private
27
+
28
+ def upload(local, remote, delete_after=false)
29
+ ::Opee::Env.info("upload \"#{local}\" to \"#{remote}\"#{delete_after ? ' then delete' : ''}")
30
+ unless $dry_run
31
+ @ftp = Net::SFTP.start($remote.host, $remote.user) if @ftp.nil?
32
+ begin
33
+ @ftp.upload!(local, remote)
34
+ `rm "#{local}"` if delete_after
35
+ ::Opee::Env.warn("Uploaded \"#{local}\"")
36
+ rescue Net::SFTP::StatusException => e
37
+ if Net::SFTP::Constants::StatusCodes::FX_NO_SUCH_FILE == e.code
38
+ assure_dirs_exist(::File.dirname(remote))
39
+ retry
40
+ else
41
+ ::Opee::Env.error("Upload of \"#{local}\" failed: #{e.class}: (#{e.code}) #{e.description}\n #{e.text}\n #{e.response}")
42
+ end
43
+ end
44
+ end
45
+ @copy_queue.ask(:ready, self)
46
+ end
47
+
48
+ def download(remote, local, decrypt_path=nil)
49
+ ::Opee::Env.info("download #{remote} to #{local}")
50
+ @ftp = Net::SFTP.start($remote.host, $remote.user) if @ftp.nil?
51
+ begin
52
+ @ftp.download!(remote, local)
53
+ if decrypt_path.nil?
54
+ @fixer.ask(:collect, local, :copier) unless @fixer.nil?
55
+ ::Opee::Env.warn("Downloaded \"#{local}\"")
56
+ else
57
+ @crypt_queue.add_method(:decrypt, local, decrypt_path)
58
+ end
59
+ rescue Exception => e
60
+ ::Opee::Env.error("Download of \"#{local}\" failed: #{e.class}: #{e.message}")
61
+ #::Opee::Env.rescue(e)
62
+ end
63
+ @copy_queue.ask(:ready, self)
64
+ end
65
+
66
+ def assure_dirs_exist(dir)
67
+ ::Opee::Env.info("creating remote dir \"#{dir}\"")
68
+ @ssh = Net::SSH.start($remote.host, $remote.user) if @ssh.nil?
69
+ out = @ssh.exec!(%{mkdir -p "#{dir}"})
70
+ raise out unless out.nil? || out.strip().empty?
71
+ end
72
+
73
+ end # Copier
74
+ end # ODisk