rubyyabt 0.0.5

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.
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $stdout.sync = true
4
+ $stderr.sync = true
5
+
6
+ require 'optparse'
7
+
8
+ # Defaults for the command line options
9
+ options = {
10
+ :program_name => 'rubyyabt',
11
+ :version => [0, 0, 1],
12
+ :verbose => false,
13
+ :debug => false,
14
+ :excl_incl => [],
15
+ :username => "",
16
+ :password => "",
17
+ :chunk_size => 2 ** 20, # 1 Megabyte chunk size by default
18
+ :gpg_bin => '/usr/bin/gpg',
19
+ :ssl_cert_dir => '/etc/ssl/certs',
20
+ :mode => :backup,
21
+ :empty_cache => false,
22
+ :cache_max_age => 90 * (24*60*60), # By default allow 30 days cache time
23
+ :http_timeout => 5 * 60 # By default allow a http timeout of 5 minutes
24
+ }
25
+
26
+ # Parse the command line...
27
+ OptionParser.new do |opts|
28
+ opts.banner = "Usage: rubyyabt-backup.rb [options] --source <source> --target <target>"
29
+ opts.on("-s", "--source SOURCE", "Specify the source path") { |v| options[:source] = v }
30
+ opts.on("-e", "--exclincl FILE", "Load excludes/includes from FILE") { |v| options[:excl_incl] = File.readlines(v) }
31
+ opts.on("-t", "--target TARGET", "Specify the target path") { |v| options[:target] = v }
32
+ opts.on("-u", "--user USER", "Username for target") { |v| options[:username] = v }
33
+ opts.on("-p", "--pass PASS", "Password for target") { |v| options[:password] = v }
34
+ opts.on("-P", "--passfile FILE", "Read password for target from FILE") { |v| options[:password] = File.read(v) }
35
+ opts.on("-G", "--gpg-bin FILE", "use FILE as gpg binary") { |v| options[:gpg_bin] = v }
36
+ opts.on("-g", "--gpg-key KEY", "Use KEY as GPG key for encryption and signing") { |v| options[:gpg_key] = v }
37
+ opts.on("-h", "--gpg-pass PASS", "Password for GPG key") { |v| options[:gpg_pass] = v }
38
+ opts.on("-H", "--gpg-passfile FILE", "Read password for GPG key from FILE") { |v| options[:gpg_pass] = File.read(v) }
39
+ opts.on("-c", "--cache-max-age DAYS", "Allow maximum of DAYS time for a positive cache hit. False hits are never cached.") { |v| options[:cache_max_age] = v.to_i *(24*60*60) }
40
+ opts.on("", "--http-timeout SECS", "Require all http requests to complete within SECS seconds.") { |v| options[:http_timeout] = v.to_i }
41
+ opts.on("-C", "--empty-cache", "Start with an empty cache instead of restoring the cache from saved data") { options[:empty_cache] = true }
42
+ opts.on("-v", "--[no-]verbose", "Run verbosely") { |v| options[:verbose] = v }
43
+ opts.on("-d", "--[no-]debug", "Show debug messages") { |v| options[:debug] = v }
44
+ opts.on_tail("-?", "--help", "Show this message") {
45
+ puts opts
46
+ exit
47
+ }
48
+ end.parse!
49
+
50
+ abort('--source must be specified') if not options.include?(:source)
51
+ abort('--target must be specified') if not options.include?(:target)
52
+ abort('--gpg-key must be specified') if not options.include?(:gpg_key)
53
+ abort('--gpg-pass must be specified') if not options.include?(:gpg_pass)
54
+
55
+ # Now load the main program
56
+ #noinspection RubyResolve
57
+ require 'classes/Backup'
58
+
59
+ Thread.abort_on_exception = true
60
+ $myDEBUG = options[:debug]
61
+ $myVERBOSE = options[:verbose]
62
+
63
+ backup = Backup.new(options)
64
+
65
+ exit 1 if backup.backup! > 0
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env ruby
2
+ $stdout.sync = true
3
+ $stderr.sync = true
4
+
5
+ require 'optparse'
6
+
7
+ # Defaults for the command line options
8
+ options = {
9
+ :program_name => 'rubyyabt',
10
+ :version => [0, 0, 1],
11
+ :verbose => false,
12
+ :debug => false,
13
+ :excl_incl => [],
14
+ :username => "",
15
+ :password => "",
16
+ :chunk_size => 2 ** 20, # 1 Megabyte chunk size by default
17
+ :gpg_bin => '/usr/bin/gpg',
18
+ :ssl_cert_dir => '/etc/ssl/certs',
19
+ :mode => :restore
20
+ }
21
+ # Parse the command line...
22
+ OptionParser.new do |opts|
23
+ opts.banner = "Usage: rubyyabt-restore.rb [options] --source <LOCAL> --target <REMOTE> --backup <BACKUPNAME>"
24
+ opts.on("-s", "--source LOCAL", "Specify the local path (restore will write to this directory)") { |v| options[:source] = v }
25
+ opts.on("-e", "--excl_incl FILE", "Load excludes/includes from FILE") { |v| options[:excl_incl] = File.readlines(v) }
26
+ opts.on("-t", "--target REMOTE", "Specifies the remote path that contains the backed up data") { |v| options[:target] = v }
27
+ opts.on("-b", "--backup BACKUPNAME", "Specifies which backup should be restored (no .gpg extension)") { |v| options[:backup_name] = v }
28
+ opts.on("-u", "--user USER", "Username for target") { |v| options[:username] = v }
29
+ opts.on("-p", "--pass PASS", "Password for target") { |v| options[:password] = v }
30
+ opts.on("-P", "--passfile FILE", "Read password for target from FILE") { |v| options[:password] = File.read(v) }
31
+ opts.on("-g", "--gpg-key KEY", "Use KEY as GPG key for encryption and signing") { |v| options[:gpg_key] = v }
32
+ opts.on("-h", "--gpg-pass PASS", "Password for GPG key") { |v| options[:gpg_pass] = v }
33
+ opts.on("-H", "--gpg-passfile FILE", "Read password for GPG key from FILE") { |v| options[:gpg_pass] = File.read(v) }
34
+ opts.on("-v", "--[no-]verbose", "Run verbosely") { |v| options[:verbose] = v }
35
+ opts.on("-d", "--[no-]debug", "Show debug messages") { |v| options[:debug] = v }
36
+ opts.on_tail("-?", "--help", "Show this message") {
37
+ puts opts
38
+ exit
39
+ }
40
+ end.parse!
41
+
42
+ abort('--source must be specified') if not options.include?(:source)
43
+ abort('--target must be specified') if not options.include?(:target)
44
+ abort('--gpg-key must be specified') if not options.include?(:gpg_key)
45
+ abort('--gpg-pass must be specified') if not options.include?(:gpg_pass)
46
+
47
+ # Now load the main program
48
+ #noinspection RubyResolve
49
+ require 'classes/Backup'
50
+
51
+ Thread.abort_on_exception = true
52
+ $myDEBUG = options[:debug]
53
+ $myVERBOSE = options[:verbose]
54
+
55
+ backup = Backup.new(options)
56
+ backup.restore!
data/classes/Backup.rb ADDED
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #noinspection RubyResolve
4
+ require 'classes/SMGFile'
5
+ #noinspection RubyResolve
6
+ require 'classes/Source'
7
+ #noinspection RubyResolve
8
+ require 'classes/Target'
9
+ #noinspection RubyResolve
10
+ require 'classes/cui'
11
+
12
+ $stderr.sync = true
13
+ $stdout.sync = true
14
+
15
+ $myDEBUG = false unless $myDEBUG
16
+
17
+ # TODO: Implement a trap for SIGINT to catch CTRL+c
18
+
19
+ class Backup
20
+ def initialize(options)
21
+ $options = options
22
+ @cui = Cui.instance()
23
+ @target = Target.instance()
24
+ @source = Source.instance()
25
+ @metadata = Array.new
26
+ # Find a good name for the backup. If one has been given through options[:backup_name] then use it,
27
+ # else create one by using the current date and time.
28
+ @name = $options[:backup_name] if options.include?(:backup_name)
29
+ if (options[:mode] == :restore) and not (options.include?(:backup_name)) then
30
+ raise("No backup to restore given.")
31
+ end
32
+ @time = Time.now
33
+ @time.utc
34
+ @name = @time.strftime("%Y%m%dT%H%M%S") unless @name
35
+ @cui.message("Initialized backup #{@name}")
36
+ end
37
+
38
+ def backup!()
39
+ $errors = 0 if not $errors
40
+ @cui.message("Scanning for files...")
41
+ @cui.start
42
+ @cui.message("Found #{@source.files.count.to_s} files and directories")
43
+ backup_meta = Array.new
44
+ backup_meta << '[Backup]'
45
+ cache_timer = Time.now
46
+ @source.files.each { | f |
47
+ if (Time.now - cache_timer) > 900 then
48
+ @cui.message("Uploading cache data...")
49
+ @target.upload_cache
50
+ cache_timer = Time.now
51
+ end
52
+ begin
53
+ file = SMGFile.new(f)
54
+ @cui.error("DEBUG[#{Thread.current.inspect}]: created new SMGFile object from #{f}") if $myDEBUG
55
+ file.backup!
56
+ file_meta = Array.new
57
+ file_meta << 'file=' + file.meta_sha256 + '.' + file.meta_md5
58
+ file_meta << 'size=' + file.size.to_s
59
+ file_meta << 'mtime=' + file.mtime
60
+ file_meta << 'name=' + f
61
+ file_meta = file_meta.join(";")
62
+ backup_meta << file_meta
63
+ rescue Exception
64
+ @cui.error("Error: #{$!.to_s}. Skipping this object.")
65
+ $errors += 1
66
+ end
67
+ }
68
+ @cui.message("Waiting for last chunk to finish uploading...")
69
+ @cui.error("DEBUG[#{Thread.current.inspect}]: Joining chunk threads") if $myDEBUG
70
+ chunk = Chunk.new
71
+ chunk.join
72
+ @cui.stop
73
+ @cui.update
74
+ @cui.message("Finished backing up data. Now uploading metadata for backup #{@name}.")
75
+ backup_meta = backup_meta.join("\n")
76
+ @cui.error("DEBUG[#{Thread.current.inspect}]: Writing backup info") if $myDEBUG
77
+ @target.write(:backup, @name, backup_meta)
78
+ @cui.message("Uploading cache data...")
79
+ @target.upload_cache
80
+ @cui.error("Errors: A total of #{$errors} errors have been encountered.") if $errors > 0
81
+ return $errors
82
+ end
83
+
84
+ def restore!()
85
+ backup_meta = @target.read(:backup, @name)
86
+ backup_meta.lines { | line |
87
+ line.chomp!
88
+ if !(line.eql?('[Backup]')) then
89
+ sha256 = line[5..68]
90
+ md5 = line[70..101]
91
+ size = /^[^;]*;size=([0-9]*);/.match(line).captures[0].to_i
92
+ mtime = Time.rfc2822(/^[^;]*;[^;]*;mtime=([^;]*);/.match(line).captures[0])
93
+ name = /^[^;]*;[^;]*;[^;]*;name=(.*)$/.match(line).captures[0]
94
+ @cui.message("Restoring file: size=#{size} mtime=#{mtime} name=#{name}")
95
+ f = SMGFile.new(nil, md5, sha256)
96
+ f.restore!
97
+ end
98
+ }
99
+ end
100
+ end
data/classes/Chunk.rb ADDED
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'digest/md5'
4
+ #noinspection RubyResolve
5
+ require 'digest/sha2'
6
+ #noinspection RubyResolve
7
+ require 'thread'
8
+ #noinspection RubyResolve
9
+ require 'classes/Target'
10
+ #noinspection RubyResolve
11
+ require 'classes/cui'
12
+
13
+ $myDEBUG = false unless $myDEBUG
14
+
15
+ class Chunk
16
+ attr_reader :data, :length
17
+ @@upload_mutex = Mutex.new
18
+ @@upload_thread = nil
19
+
20
+ def initialize()
21
+ @md5 = nil
22
+ @md5thread = nil
23
+ @sha256 = nil
24
+ @sha256thread = nil
25
+ @data = nil
26
+ @cui = Cui.instance
27
+ end
28
+
29
+ def join()
30
+ @cui.error("DEBUG[#{Thread.current.inspect}]: Joining chunk thread") if $myDEBUG
31
+ @@upload_thread.join if @@upload_thread
32
+ end
33
+
34
+ def set_data(data)
35
+ @cui.error("DEBUG[#{Thread.current.inspect}: Entered chunk set_data") if $myDEBUG
36
+ @data = data
37
+ @cui.error("DEBUG[#{Thread.current.inspect}: Set data") if $myDEBUG
38
+ @length = @data.length
39
+ @cui.error("DEBUG[#{Thread.current.inspect}: Calculated length") if $myDEBUG
40
+ @md5 = nil
41
+ @md5 = Digest::MD5.hexdigest(@data)
42
+ @sha256 = nil
43
+ @sha256 = Digest::SHA2.new(256).hexdigest(@data)
44
+ end
45
+
46
+ def md5()
47
+ return @md5 if @md5
48
+ @md5 = Digest::MD5.hexdigest(@data)
49
+ end
50
+
51
+ def md5=(md5)
52
+ @data = nil
53
+ @md5 = md5
54
+ end
55
+
56
+ def sha256()
57
+ return @sha256 if @sha256
58
+ @sha256 = Digest::SHA2.new(256).hexdigest(@data)
59
+ end
60
+
61
+ def sha256=(sha256)
62
+ @data = nil
63
+ @sha256 = sha256
64
+ end
65
+
66
+ def backup!()
67
+ @cui.error("DEBUG[#{Thread.current.inspect}]: Locking chunk upload mutex in thread #{Thread.current.inspect}") if $myDEBUG
68
+ @@upload_mutex.synchronize {
69
+ @cui.error("DEBUG[#{Thread.current.inspect}]: Got lock of chunk upload mutex in thread #{Thread.current.inspect}") if $myDEBUG
70
+ @cui.error("DEBUG[#{Thread.current.inspect}]: Trying to join earlier upload threads if any (@@uploadThread: #{@@upload_thread.inspect})") if $myDEBUG
71
+ @@upload_thread.join if @@upload_thread
72
+ @@upload_thread = Thread.new {
73
+ target = Target.instance # Get a target object, then ...
74
+ @cui.error("DEBUG[#{Thread.current.inspect}]: Checking if target chunk exists (#{sha256}, #{md5})") if $myDEBUG
75
+ if not target.exists?(:chunk, "#{sha256}.#{md5}") then
76
+ @cui.error("Uploading chunk #{sha256}.#{md5}...") if $myDEBUG
77
+ target.write(:chunk, "#{sha256}.#{md5}", @data)
78
+ end
79
+ @cui.error("DEBUG[#{Thread.current.inspect}]: Upload chunk thread finished (thread id: #{Thread.current.inspect})") if $myDEBUG
80
+ }
81
+ @cui.error("DEBUG[#{Thread.current.inspect}]: New upload thread: @@upload_thread = #{@@upload_thread.inspect}") if $myDEBUG
82
+ }
83
+ end
84
+
85
+ def restore!()
86
+ @cui.message("Downloading chunk #{sha256}.#{md5}...")
87
+ target = Target.instance
88
+ @data = target.read(:chunk, "#{sha256}.#{md5}")
89
+ @length = @data.length
90
+ expected_md5 = @md5
91
+ expected_sha256 = @sha256
92
+ @md5 = nil
93
+ @sha256 = nil
94
+ if (expected_md5 == md5) and (expected_sha256 == sha256) then
95
+ @cui.message("chunk verified")
96
+ else
97
+ @cui.message("checksum error in chunk #{sha256}.#{md5}")
98
+ raise 'checksum error'
99
+ end
100
+ end
101
+ end
data/classes/GPG.rb ADDED
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'tempfile'
4
+ #noinspection RubyResolve
5
+ require 'classes/cui'
6
+
7
+ $options = nil unless $options
8
+
9
+ class GPG
10
+ def initialize()
11
+ @cui = Cui.instance
12
+ end
13
+
14
+ def encrypt(data)
15
+ # Setup the pipes
16
+ pass = IO::pipe # Pipe for providing the password
17
+ pi = IO::pipe # Data will be written to this pipe (stdin for gpg)
18
+ po = IO::pipe # Encrypted data can be read from this pipe (stdout for gpg)
19
+
20
+ gpg_proc = Kernel.fork do
21
+ # Cleanup unused end of the pipes
22
+ pass[1].close
23
+ pi[1].close
24
+ po[0].close
25
+
26
+ # Remap the STDIN and STDOUT pipes
27
+ STDIN.reopen(pi[0])
28
+ pi[0].close
29
+ STDOUT.reopen(po[1])
30
+ po[1].close
31
+
32
+ # Execute gpg
33
+ Kernel.exec($options[:gpg_bin], '--batch', '--encrypt', '--sign', '--passphrase-fd', pass[0].fileno.to_s, '--recipient', $options[:gpg_key], '--local-user', $options[:gpg_key])
34
+ raise 'Error running GPG'
35
+ end
36
+
37
+ # Cleanup unused end of the pipes for ruby
38
+ pass[0].close
39
+ pi[0].close
40
+ po[1].close
41
+
42
+ # Start new threads to send the data to GPG. This hopefully avoids any blocking.
43
+ thread_pass = Thread.new do
44
+ pass[1].write($options[:gpg_pass])
45
+ pass[1].close
46
+ end
47
+ thread_pi = Thread.new do
48
+ pi[1].write(data)
49
+ pi[1].close
50
+ end
51
+
52
+ # Run a new thread to read data from GPG.
53
+ encrypted_data = String.new # Initialize the variable for reading from GPG
54
+ thread_po = Thread.new do
55
+ # Read from GPG until the pipe is closed
56
+ #while (new_encrypted_data = po[0].read)
57
+ #encrypted_data += new_encrypted_data
58
+ while (!po[0].eof?)
59
+ encrypted_data += po[0].read
60
+ end
61
+ po[0].close
62
+ end
63
+ # Now cleanup the process from GPG and the threads
64
+ Process.wait(gpg_proc)
65
+ thread_pi.join
66
+ #noinspection RubyUnusedLocalVariable
67
+ data = nil # Free up the variable and hopefully some memory while we're waiting for the other threads
68
+ thread_pass.join
69
+ thread_po.join
70
+ # Return the data, if there's no data, GPG has probably thrown an error.
71
+ return encrypted_data if encrypted_data.length > 0
72
+ @cui.error("Failed to run gpg to encrypt data. Command run: " + [$options[:gpg_bin], '--batch', '--encrypt', '--sign', '--passphrase-fd', pass[0].fileno.to_s, '--recipient', $options[:gpg_key], '--local-user', $options[:gpg_key]].inspect)
73
+ raise 'GPG error'
74
+ end
75
+
76
+ def decrypt(encrypted_data)
77
+ # Setup the pipes
78
+ pass = IO::pipe # Pipe for providing the password
79
+ pi = IO::pipe # Data will be written to this pipe (stdin for gpg)
80
+ po = IO::pipe # Encrypted data can be read from this pipe (stdout for gpg)
81
+
82
+ gpg_proc = Kernel.fork do
83
+ # Cleanup unused end of the pipes
84
+ pass[1].close
85
+ pi[1].close
86
+ po[0].close
87
+
88
+ # Remap the STDIN and STDOUT pipes
89
+ STDIN.reopen(pi[0])
90
+ pi[0].close
91
+ STDOUT.reopen(po[1])
92
+ po[1].close
93
+
94
+ # Execute gpg
95
+ Kernel.exec($options[:gpg_bin], '--batch', '--decrypt', '--passphrase-fd', pass[0].fileno.to_s)
96
+ raise 'Error running GPG'
97
+ end
98
+
99
+ # Cleanup unused end of the pipes for ruby
100
+ pass[0].close
101
+ pi[0].close
102
+ po[1].close
103
+
104
+ # Start new threads to send the data to GPG. This hopefully avoids any blocking.
105
+ thread_pass = Thread.new do
106
+ pass[1].write($options[:gpg_pass])
107
+ pass[1].close
108
+ end
109
+ thread_pi = Thread.new do
110
+ pi[1].write(encrypted_data)
111
+ pi[1].close
112
+ end
113
+
114
+ # Run a new thread to read data from GPG.
115
+ data = String.new # Initialize the variable for reading from GPG
116
+ thread_po = Thread.new do
117
+ # Read from GPG until the pipe is closed
118
+ #while (new_data = po[0].read)
119
+ #data += new_data
120
+ while (!po[0].eof?)
121
+ data += po[0].read
122
+ end
123
+ po[0].close
124
+ end
125
+ # Now cleanup the process from GPG and the threads
126
+ Process.wait(gpg_proc)
127
+ thread_pi.join
128
+ #noinspection RubyUnusedLocalVariable
129
+ encrypted_data = nil # Free up the variable and hopefully some memory while we're waiting for the other threads
130
+ thread_pass.join
131
+ thread_po.join
132
+ # Return the data, if there's no data, GPG has probably thrown an error.
133
+ return data if data.length > 0
134
+ @cui.error("Failed to run gpg to encrypt data. Command run: " + [$options[:gpg_bin], '--batch', '--decrypt', '--passphrase-fd', pass[0].fileno.to_s].inspect)
135
+ raise 'GPG error'
136
+ end
137
+ end
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ class ProxyFile
4
+ def initialize()
5
+ @types = {
6
+ :chunk => 'chunks',
7
+ :file => 'files',
8
+ :backup => 'backups',
9
+ :root => ''
10
+ }
11
+ end
12
+
13
+ def caching?()
14
+ return false
15
+ end
16
+
17
+ def read(target_dir, type, file)
18
+ target_file = File.expand_path(target_dir.to_s + '/' + @types[type] + '/' + file)
19
+ open(target_file, "rb") {|io| io.read }
20
+ end
21
+
22
+ def write(target_dir, type, file, data)
23
+ target file = File.expand_path(target_dir.to_s + '/' + @types[type] + '/' + file)
24
+ open(target file, "wb") {|io| io.write(data) }
25
+ end
26
+
27
+ def exists?(target_dir, type, file)
28
+ target_file = File.expand_path(target_dir.to_s + '/' + @types[type] + '/' + file)
29
+ if File.exists?(target_file) then
30
+ if File.directory?(target_file) then
31
+ false
32
+ else
33
+ true
34
+ end
35
+ else
36
+ false
37
+ end
38
+ end
39
+
40
+ def list(target_dir, type)
41
+ Dir.entries(target_dir + '/' + @types[type])
42
+ end
43
+ end