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/bin/rubyyabt-backup.rb +65 -0
- data/bin/rubyyabt-restore.rb +56 -0
- data/classes/Backup.rb +100 -0
- data/classes/Chunk.rb +101 -0
- data/classes/GPG.rb +137 -0
- data/classes/ProxyFile.rb +43 -0
- data/classes/ProxyHTTP.rb +224 -0
- data/classes/SMGFile.rb +292 -0
- data/classes/Source.rb +93 -0
- data/classes/Target.rb +130 -0
- data/classes/cui.rb +148 -0
- data/classes/proxy_http_cache_data.rb +16 -0
- data/classes/proxy_http_cache_hash.rb +72 -0
- data/doc/dev/options.txt +69 -0
- metadata +78 -0
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
|
data/doc/dev/options.txt
ADDED
@@ -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
|
+
|