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.
data/classes/Target.rb ADDED
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'singleton'
4
+ require 'uri'
5
+ require 'zlib'
6
+
7
+ #noinspection RubyResolve
8
+ require 'classes/ProxyFile'
9
+ #noinspection RubyResolve
10
+ require 'classes/ProxyHTTP'
11
+ #noinspection RubyResolve
12
+ require 'classes/GPG'
13
+ #noinspection RubyResolve
14
+ require 'classes/cui'
15
+
16
+ $myVERBOSE = false unless $myVERBOSE
17
+ $myDEBUG = false unless $myDEBUG
18
+
19
+ $options = nil unless $options
20
+
21
+ class Target
22
+ include Singleton
23
+
24
+ def initialize()
25
+ @cui = Cui.instance
26
+ connect
27
+ if $options[:mode] == :backup then
28
+ # Export the key only if a backup is in progress.
29
+ export_key unless exists?(:root, 'key')
30
+ export_key unless exists?(:root, 'trust')
31
+ end
32
+ end
33
+
34
+ def connect()
35
+ @proxy = ProxyFile.new() if !($options[:target].include?(":"))
36
+ case URI.parse($options[:target]).scheme
37
+ when 'file'
38
+ @proxy = ProxyFile.new()
39
+ when 'http'
40
+ @proxy = ProxyHTTP.new()
41
+ when 'https'
42
+ @proxy = ProxyHTTP.new()
43
+ else
44
+ raise "No idea how to connect to #{$options[:target]}"
45
+ end
46
+ @cui.message("Initialized proxy as #{@proxy.class}.") if $myVERBOSE
47
+ begin
48
+ if @proxy.caching? then
49
+ if not $options[:empty_cache] then
50
+ @cui.message("Trying to restore cache...") if $myVERBOSE
51
+ compressed_cache_data = read(:cache, "flist")
52
+ begin
53
+ # Check if the data is compressed and uncompress the data
54
+ cache_data = Zlib::Inflate.inflate(compressed_cache_data)
55
+ @cui.message("Uncompressed cache data from #{compressed_cache_data.length} bytes to #{cache_data.length} bytes.")
56
+ rescue Exception
57
+ # Uncompress failed, so the data was probably uncompressed from the beginning.
58
+ cache_data = compressed_cache_data
59
+ end
60
+ #noinspection RubyUnusedLocalVariable
61
+ compressed_cache_data = nil # Free up the memory
62
+ @proxy.load_cache(cache_data)
63
+ end
64
+ end
65
+ rescue Exception
66
+ # Nothing to do here.
67
+ end
68
+ GC.start
69
+ end
70
+
71
+ def connected?()
72
+ if @proxy then
73
+ true
74
+ else
75
+ false
76
+ end
77
+ end
78
+
79
+ def exists?(type, file)
80
+ @cui.exists(@proxy.exists?($options[:target], type, file + '.gpg'))
81
+ end
82
+
83
+ def read(type, file)
84
+ gpg = GPG.new()
85
+ gpg.decrypt(@proxy.read($options[:target], type, file + '.gpg'))
86
+ end
87
+
88
+ def write(type, file, data)
89
+ gpg = GPG.new()
90
+ encrypted_data = gpg.encrypt(data)
91
+ @proxy.write($options[:target], type, file + '.gpg', encrypted_data)
92
+ end
93
+
94
+ def upload_cache()
95
+ # Upload the cache to save time for the next runs...
96
+ if @proxy.caching? then
97
+ cache_data = @proxy.dump_cache
98
+ return if cache_data.nil? # Skip dumping if the data has not changed
99
+ # Compress the data to save some space...
100
+ begin
101
+ compressed_cache_data = Zlib::Deflate.deflate(cache_data, 9)
102
+ rescue Exception
103
+ # Seems like compression does not work. We'll just use the uncompressed data then.
104
+ compressed_cache_data = cache_data
105
+ @cui.message('Unable to compress the cache data for upload. Using uncompressed data.')
106
+ end
107
+ @cui.error("DEBUG[#{Thread.current.inspect}]: Cache has a length of #{compressed_cache_data.length}. Uploading...") if $myDEBUG
108
+ write(:cache, 'flist', compressed_cache_data)
109
+ end
110
+ end
111
+
112
+ def export_key()
113
+ @cui.error("DEBUG[#{Thread.current.inspect}]: Exporting keys...") if $myDEBUG
114
+ IO.popen('"' + $options[:gpg_bin] + '" ' + '--export-ownertrust', 'w+') do | gpg |
115
+ gpg.close_write()
116
+ trust = gpg.read()
117
+ # Need to use the proxy directly as the data would get encrypted otherwise
118
+ @proxy.write($options[:target], :root, 'trust.gpg', trust)
119
+ end
120
+ IO.popen('"' + $options[:gpg_bin] + '" ' + '--export-secret-keys --armor --default-key ' + $options[:gpg_key], 'w+') do | gpg |
121
+ gpg.close_write()
122
+ key = gpg.read()
123
+ @proxy.write($options[:target], :root, 'key.gpg', key)
124
+ end
125
+ end
126
+
127
+ def list(type)
128
+ @proxy.list($options[:target], type)
129
+ end
130
+ end
data/classes/cui.rb ADDED
@@ -0,0 +1,148 @@
1
+ require 'singleton'
2
+ #noinspection RubyResolve
3
+ require 'thread'
4
+
5
+
6
+ # Usage of stdout and stderr:
7
+ # stdout will be used for normal status messages like the status bar.
8
+ # In short, messages that could be redirected to /dev/null without impact.
9
+ # stderr will be used for all messages that a user is supposed to read,
10
+ # or that should show up in logfiles (e.g. error messages). By definition
11
+ # this includes DEBUG messages. Cui.error will automatically append
12
+ # information like the current file, if it's known.
13
+
14
+
15
+ class Cui
16
+ include Singleton
17
+ def initialize()
18
+ @update_thread = nil
19
+ @mutex = Mutex.new
20
+ @total_size = 0 # These are bytes!
21
+ @finished_size = 0 # These are bytes!
22
+ @total_files = 0
23
+ @finished_files = 0
24
+ @current_file_size = 0
25
+ @current_file_finished_size = 0
26
+ @current_file_name = ""
27
+ @last_file_name = ""
28
+ @last_length = 0
29
+ @changed = false
30
+ @active = false
31
+ @exists = true
32
+ end
33
+ def exists(e)
34
+ @exists = e
35
+ end
36
+ def total_size_add(size)
37
+ @mutex.synchronize {
38
+ @total_size += size
39
+ @changed = true
40
+ }
41
+ end
42
+ def finished_size_add(size)
43
+ @mutex.synchronize {
44
+ @finished_size += size
45
+ @current_file_finished_size += size
46
+ @changed = true
47
+ }
48
+ end
49
+ def total_files_inc()
50
+ @mutex.synchronize {
51
+ @total_files += 1
52
+ @changed = true
53
+ }
54
+ end
55
+ def current_file_size(size)
56
+ @mutex.synchronize {
57
+ @current_file_size = size
58
+ @changed = true
59
+ }
60
+ end
61
+ def current_file_name(file)
62
+ @mutex.synchronize {
63
+ @finished_files += 1 unless @last_file_name == ""
64
+ @current_file_finished_size = 0
65
+ @current_file_size = 0
66
+ @last_file_name = @current_file_name
67
+ @current_file_name = file.to_str
68
+ update
69
+ }
70
+ end
71
+ def start()
72
+ stop if @update_thread # Stop the thread if it's already running
73
+ @changed = true
74
+ @active = true
75
+ @update_thread = Thread.new {
76
+ while @active
77
+ sleep(0.3)
78
+ @mutex.synchronize {
79
+ update if @changed
80
+ }
81
+ end
82
+ }
83
+ end
84
+ def update()
85
+ finished_size = @finished_size / 1024 / 1024
86
+ total_size = @total_size / 1024 / 1024
87
+ current_file_size = ("%.1f" % (@current_file_size.to_f / 1024 / 1024)).to_f
88
+ current_file_finished_size = ("%.1f" % (@current_file_finished_size.to_f / 1024 / 1024)).to_f
89
+ size_length = total_size.to_s.length
90
+ files_length = @total_files.to_s.length
91
+ current_file_size_length = current_file_size.to_s.length
92
+ uploading = "Uploading..."
93
+ uploading = "Checking..." if @exists
94
+ template = "Total %#{size_length}d/%#{size_length}d MB | Files %#{files_length}d/%#{files_length}d | Current %#{current_file_size_length}.1f/%#{current_file_size_length}.1f MB " + uploading
95
+ if @current_file_name != @last_file_name then
96
+ text = ""
97
+ text += "\b" * @last_length if @last_length > 0
98
+ text += @current_file_name
99
+ text += " " * (@last_length - @current_file_name.length) if (@last_length - @current_file_name.length) > 0
100
+ text += "\n"
101
+ $stdout.print(text)
102
+ @last_file_name = @current_file_name
103
+ @last_length = 0
104
+ end
105
+ backspace = ("\b" * @last_length)
106
+ line = template % [ finished_size, total_size, @finished_files, @total_files, current_file_finished_size, current_file_size ]
107
+ @last_length = line.length
108
+ $stdout.print(backspace + line)
109
+ @changed = false
110
+ end
111
+ def message(text)
112
+ @mutex.synchronize {
113
+ case @active
114
+ when true
115
+ backspace = ("\b" * @last_length)
116
+ text += " " * (@last_length - text.length) if (@last_length - text.length) > 0
117
+ @last_length = 0
118
+ $stdout.print(backspace + text + "\n")
119
+ update
120
+ when false
121
+ # If there's no regular update, we can just write out a new line
122
+ $stdout.print("\n" + text + "\n")
123
+ end
124
+ }
125
+ end
126
+ def error(text)
127
+ @mutex.synchronize {
128
+ case @active
129
+ when true
130
+ backspace = ("\b" * @last_length) # Calculate the amount of backspaces required
131
+ spaces = ""
132
+ spaces = " " * (@last_length - text.length) if (@last_length - text.length) > 0
133
+ $stderr.print(backspace + text + spaces + "\n")
134
+ @last_length = 0
135
+ update
136
+ when false
137
+ # If there's no regular update, we can just write out a new line
138
+ $stdout.print("\n" + text + "\n")
139
+ end
140
+ }
141
+ end
142
+ def stop()
143
+ @active = false
144
+ @update_thread.run
145
+ @update_thread.join
146
+ @update_thread = nil
147
+ end
148
+ end
@@ -0,0 +1,16 @@
1
+ $options = nil unless $options
2
+
3
+ class ProxyHTTPCacheData
4
+ attr_accessor :time_validated
5
+
6
+ def initialize()
7
+ # Create an entry that's valid at least 1 second...
8
+ @time_validated = Time.now + ( Kernel.rand($options[:cache_max_age] - 1) + 1 )
9
+ end
10
+
11
+ def valid?()
12
+ return false if @time_validated.nil?
13
+ return false if (Time.now - @time_validated) > $options[:cache_max_age]
14
+ return true
15
+ end
16
+ end
@@ -0,0 +1,72 @@
1
+ #noinspection RubyResolve
2
+ require 'classes/proxy_http_cache_data.rb'
3
+
4
+ $options = nil unless $options
5
+
6
+ class ProxyHTTPCache_Hash
7
+ def initialize()
8
+ @chunks = Hash.new
9
+ @files = Hash.new
10
+ @changed = false
11
+ end
12
+
13
+ def add(type, key)
14
+ case type
15
+ when :chunk
16
+ add_key(@chunks, key)
17
+ when :file
18
+ add_key(@files, key)
19
+ else
20
+ raise "no such cache - #{type.to_s}"
21
+ end
22
+ @changed = true
23
+ end
24
+
25
+ def mark_valid(type, key)
26
+ case type
27
+ when :chunk
28
+ set_validated(@chunks, key)
29
+ when :file
30
+ set_validated(@files, key)
31
+ else
32
+ raise "no such cache - #{type.to_s}"
33
+ end
34
+ @changed = true
35
+ end
36
+
37
+ def search(type, key)
38
+ case type
39
+ when :chunk
40
+ result = search_tree(@chunks, key)
41
+ return result.valid? unless result.nil?
42
+ return false
43
+ when :file
44
+ result = search_tree(@files, key)
45
+ return result.valid? unless result.nil?
46
+ return false
47
+ else
48
+ raise "no such cache - #{type.to_s}"
49
+ end
50
+ end
51
+
52
+ def dump()
53
+ return nil if not @changed
54
+ return Marshal.dump(self)
55
+ end
56
+
57
+ private
58
+ def add_key(hash, key)
59
+ data = ProxyHTTPCacheData.new
60
+ hash[key] = data
61
+ end
62
+
63
+ def set_validated(hash, key)
64
+ data = hash[key]
65
+ data.time_validated = Time.now
66
+ end
67
+
68
+ def search_tree(hash, key)
69
+ data = hash[key]
70
+ return data
71
+ end
72
+ end
@@ -0,0 +1,69 @@
1
+ This document describes the options hash.
2
+
3
+ $options = {
4
+ :program_name => 'rubyyabt',
5
+ :version => [0, 0, 1],
6
+ :verbose => false,
7
+ :debug => false,
8
+ :excl_incl => [],
9
+ :username => "",
10
+ :password => "",
11
+ :chunk_size => 2 ** 20, # 1 Megabyte chunk size by default
12
+ :gpg_bin => '/usr/bin/gpg',
13
+ :source => '/home/user',
14
+ :target => 'https://dav.example.org/path/to/backupfiles',
15
+ :gpg_key => 'ABCDEF12',
16
+ :gpg_pass => 'mysecretpass',
17
+ :user_agent => options[:program_name] + " " + options[:version].join(".")
18
+ :ssl_cert_dir => '/etc/ssl/certs',
19
+ :backup_name => '20100410T113956',
20
+ :mode => :backup | :restore,
21
+ :empty_cache => false
22
+ }
23
+
24
+ The hash contains the above listed symbols. An explanation follows:
25
+ :program_name
26
+ String containing the name of the program.
27
+ :version
28
+ Array containing 3 numbers that define the version number of the program
29
+ :verbose
30
+ Boolean that shows if the user wants verbose or just minimum output
31
+ :debug
32
+ Boolean defining if full scale debugging output is requested
33
+ :excl_incl
34
+ Array that contains a list of includes and excludes (Strings). The first character of the string has to be "-" or
35
+ "+" to define what happens if there is a match. A "-" will exclude the file or in case of a directory, the directory
36
+ and all of its children. The last match decides the fate of the file/dir.
37
+ :username
38
+ The username (string) required to connect to :target.
39
+ :password
40
+ The password (string) required to connect to :target.
41
+ :chunk_size
42
+ Defines the maximum size of a chunk. Every file that is backed up will be split in chunks of this size. The last
43
+ chunk can of course be smaller. Changing this value when a backup already exists in :target is unsupported and
44
+ should not be done.
45
+ Restores are not affected by this option.
46
+ :gpg_bin
47
+ The path of the GPG binary that will be used to encrypt the data.
48
+ :gpg_key
49
+ The GPG key that will be used to encrypt the data. Accepts whatever gpg accepts with the --recipient and
50
+ --local-user option.
51
+ :gpg_pass
52
+ The password required for the gpg key. Required both for backup and restore as during the backup the data is
53
+ signed, which requires the password.
54
+ :source
55
+ The local directory that contains the data to be backed up or acts as the destination directory for the restore.
56
+ This should have been called :local_dir instead...
57
+ :target
58
+ The remote directory or URL that the backup will be written to. In case of a restore the data will be read from this
59
+ location. Should have been called :remote_location instead...
60
+ :user_agent
61
+ The HTTP User Agent sent to the server.
62
+ :ssl_cert_dir
63
+ Directory that contains valid root certificates for verifying certificates of https servers.
64
+ :backup_name
65
+ Needs to be set for a restore. Defines which backup will be restored.
66
+ :mode
67
+ Is either :backup or :restore, depending on what the user is requesting.
68
+ :empty_cache
69
+ Prevents the load of the cache on the server
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubyyabt
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 5
9
+ version: 0.0.5
10
+ platform: ruby
11
+ authors:
12
+ - Daniel Frank
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-11-06 00:00:00 +01:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: rubyyabt backups file to a WebDAV location. The data is splitted into chunks and encrypted with GPG.
22
+ email: tokudan@rubyforge.org
23
+ executables:
24
+ - rubyyabt-backup.rb
25
+ - rubyyabt-restore.rb
26
+ extensions: []
27
+
28
+ extra_rdoc_files: []
29
+
30
+ files:
31
+ - bin/rubyyabt-backup.rb
32
+ - bin/rubyyabt-restore.rb
33
+ - classes/Backup.rb
34
+ - classes/Chunk.rb
35
+ - classes/cui.rb
36
+ - classes/GPG.rb
37
+ - classes/ProxyFile.rb
38
+ - classes/ProxyHTTP.rb
39
+ - classes/proxy_http_cache_data.rb
40
+ - classes/proxy_http_cache_hash.rb
41
+ - classes/SMGFile.rb
42
+ - classes/Source.rb
43
+ - classes/Target.rb
44
+ - doc/dev/options.txt
45
+ has_rdoc: true
46
+ homepage: http://rubyforge.org/projects/rubyyabt/
47
+ licenses: []
48
+
49
+ post_install_message:
50
+ rdoc_options: []
51
+
52
+ require_paths:
53
+ - .
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ segments:
59
+ - 1
60
+ - 8
61
+ - 7
62
+ version: 1.8.7
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ segments:
68
+ - 0
69
+ version: "0"
70
+ requirements: []
71
+
72
+ rubyforge_project: rubyyabt
73
+ rubygems_version: 1.3.6
74
+ signing_key:
75
+ specification_version: 3
76
+ summary: backs up data to WebDAV
77
+ test_files: []
78
+