rubyyabt 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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