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
@@ -0,0 +1,224 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'net/https'
|
4
|
+
require 'uri'
|
5
|
+
require 'timeout'
|
6
|
+
#noinspection RubyResolve
|
7
|
+
require 'thread'
|
8
|
+
#noinspection RubyResolve
|
9
|
+
require 'classes/proxy_http_cache_hash'
|
10
|
+
|
11
|
+
$myDEBUG = false if not $myDEBUG
|
12
|
+
$myVERBOSE = false if not $myVERBOSE
|
13
|
+
$options = nil unless $options
|
14
|
+
|
15
|
+
class ProxyHTTP
|
16
|
+
def initialize()
|
17
|
+
# Defines the subdirs for each type of data
|
18
|
+
@types = {
|
19
|
+
:chunk => 'chunks',
|
20
|
+
:file => 'files',
|
21
|
+
:backup => 'backups',
|
22
|
+
:root => 'meta',
|
23
|
+
:cache => 'cache'
|
24
|
+
}
|
25
|
+
@@valid_caches = [:chunk, :file]
|
26
|
+
$options[:target] += "/" if $options[:target][-1] != "/"
|
27
|
+
# Calculate the user agent for HTTP requests
|
28
|
+
$options[:user_agent] = $options[:program_name] + " " + $options[:version].join(".")
|
29
|
+
@mutex = Mutex.new
|
30
|
+
@url = URI.parse($options[:target])
|
31
|
+
@http = Net::HTTP.new(@url.host, @url.port)
|
32
|
+
@http.use_ssl = true if @url.scheme = 'https'
|
33
|
+
@http.open_timeout = 15
|
34
|
+
@http.read_timeout = 280
|
35
|
+
if $options[:ssl_cert_dir] then
|
36
|
+
#noinspection RubyResolve
|
37
|
+
@http.ca_path = $options[:ssl_cert_dir]
|
38
|
+
#noinspection RubyResolve
|
39
|
+
@http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
40
|
+
#noinspection RubyResolve
|
41
|
+
@http.verify_depth = 5
|
42
|
+
end
|
43
|
+
@cui = Cui.instance
|
44
|
+
@cache = ProxyHTTPCache_Hash.new
|
45
|
+
end
|
46
|
+
|
47
|
+
def caching?()
|
48
|
+
return true
|
49
|
+
end
|
50
|
+
|
51
|
+
def dump_cache()
|
52
|
+
return @cache.dump
|
53
|
+
end
|
54
|
+
|
55
|
+
def load_cache(cache_data)
|
56
|
+
#noinspection RubyResolve
|
57
|
+
begin
|
58
|
+
@cache = Marshal.load(cache_data)
|
59
|
+
if not @cache.kind_of?(ProxyHTTPCache_Hash) then
|
60
|
+
@cui.error("Cache is not compatible. Starting with an empty cache.")
|
61
|
+
@cache = ProxyHTTPCache_Hash.new
|
62
|
+
end
|
63
|
+
rescue Exception # Just start out with an empty cache
|
64
|
+
@cache = ProxyHTTPCache_Hash.new
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
#noinspection RubyScope
|
70
|
+
def request(http_request, timeout = 300, tries = 20)
|
71
|
+
@cui.error("DEBUG[#{Thread.current.inspect}]: Trying to lock ProxyHTTP request mutex in thread #{Thread.current.inspect}") if $myDEBUG
|
72
|
+
@mutex.synchronize {
|
73
|
+
@cui.error("DEBUG[#{Thread.current.inspect}]: Got ProxyHTTP request mutex in thread #{Thread.current.inspect}") if $myDEBUG
|
74
|
+
response = ''
|
75
|
+
while tries > 0 do
|
76
|
+
@cui.error("DEBUG[#{Thread.current.inspect}]: Got #{tries} tries left for request #{http_request.inspect} #{http_request.path}, timeout #{timeout}") if $myDEBUG
|
77
|
+
begin
|
78
|
+
Timeout::timeout(timeout) {
|
79
|
+
@cui.error("DEBUG[#{Thread.current.inspect}]: Sending request #{http_request.inspect} for #{http_request.path}....") if $myDEBUG
|
80
|
+
response = @http.request(http_request)
|
81
|
+
}
|
82
|
+
rescue Timeout::Error
|
83
|
+
tries -= 1
|
84
|
+
@cui.error("Timeout during request. #{tries} tries left")
|
85
|
+
retry if tries > 0
|
86
|
+
raise
|
87
|
+
rescue Exception => ex
|
88
|
+
@cui.error("Caught exception #{ex} with message #{ex.message} during request. #{tries} tries left") if $myVERBOSE
|
89
|
+
tries -= 1
|
90
|
+
retry if tries > 0
|
91
|
+
raise
|
92
|
+
end
|
93
|
+
code = response.code.to_i
|
94
|
+
@cui.error("DEBUG[#{Thread.current.inspect}]: Response code: #{code}") if $myDEBUG
|
95
|
+
return response if (code >= 200) and (code < 300)
|
96
|
+
if (code >= 400) and (code < 500) # No retries for these errors...
|
97
|
+
# Yeehaw! we need an exception for code 401 since humyo sometimes replies with it with correct authorization
|
98
|
+
tries = 0 if (code != 401)
|
99
|
+
end
|
100
|
+
tries -= 1
|
101
|
+
@cui.error("DEBUG[#{Thread.current.inspect}]: #{tries} tries left for this request. Retrying after sleep if > 0") if $myDEBUG
|
102
|
+
sleep(rand) if tries > 0 # Sleep up to one second before retrying
|
103
|
+
end
|
104
|
+
@cui.error("DEBUG[#{Thread.current.inspect}]: Raising exception for failed request: #{response.code} #{response.message}") if $myDEBUG
|
105
|
+
raise 'HTTP request failed: ' + response.code + ' ' + response.message
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
#noinspection RubyUnusedLocalVariable
|
110
|
+
def read(target_dir, type, file)
|
111
|
+
subdir_type = type
|
112
|
+
subdir_type = (type.to_s + file[0..2]).to_sym if type == :chunk
|
113
|
+
subdir_type = (type.to_s + file[0..2]).to_sym if type == :file
|
114
|
+
subdir = ''
|
115
|
+
subdir = file[0..2] + '/' if type == :chunk
|
116
|
+
subdir = file[0..2] + '/' if type == :file
|
117
|
+
uri = @url.merge(@types[type] + '/').merge(subdir).merge(file)
|
118
|
+
get = Net::HTTP::Get.new(uri.request_uri)
|
119
|
+
get.initialize_http_header({"User-Agent" => $options[:user_agent]})
|
120
|
+
get.basic_auth($options[:username], $options[:password])
|
121
|
+
@cui.error("DEBUG[#{Thread.current.inspect}]: Reading from #{uri.to_s}") if $myDEBUG
|
122
|
+
begin
|
123
|
+
response = request(get, $options[:http_timeout])
|
124
|
+
rescue Exception => ex
|
125
|
+
@cui.error(ex.message + ' at URL: ' + uri.to_s)
|
126
|
+
raise
|
127
|
+
else
|
128
|
+
return response.body
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
#noinspection RubyUnusedLocalVariable
|
133
|
+
def write(target_dir, type, file, data)
|
134
|
+
subdir_type = type
|
135
|
+
subdir_type = (type.to_s + file[0..2]).to_sym if type == :chunk
|
136
|
+
subdir_type = (type.to_s + file[0..2]).to_sym if type == :file
|
137
|
+
subdir = ''
|
138
|
+
subdir = file[0..2] + '/' if type == :chunk
|
139
|
+
subdir = file[0..2] + '/' if type == :file
|
140
|
+
uri = @url.merge(@types[type] + '/').merge(subdir).merge(file)
|
141
|
+
put = Net::HTTP::Put.new(uri.request_uri)
|
142
|
+
put.initialize_http_header({"User-Agent" => $options[:user_agent], "Content-Type" => "application/octet-stream"})
|
143
|
+
put.basic_auth($options[:username], $options[:password])
|
144
|
+
put.body = data
|
145
|
+
@cui.error("DEBUG[#{Thread.current.inspect}]: Writing to #{uri.to_s}") if $myDEBUG
|
146
|
+
begin
|
147
|
+
response = request(put, 180)
|
148
|
+
rescue RuntimeError => ex
|
149
|
+
if ex.message == "HTTP request failed: 409 Conflict" then
|
150
|
+
@cui.error("DEBUG[#{Thread.current.inspect}]: Caught a 409 Conflict error") if $myDEBUG
|
151
|
+
if (subdir_type != type) then
|
152
|
+
begin
|
153
|
+
@cui.error("DEBUG[#{Thread.current.inspect}]: Trying to create the directory #{@url.merge(@types[type] + '/').to_s}") if $myDEBUG
|
154
|
+
mkdir(@url.merge(@types[type] + '/'))
|
155
|
+
rescue RuntimeError => rt
|
156
|
+
@cui.error("DEBUG[#{Thread.current.inspect}]: Caught exception during mkdir: #{rt.message}") if $myDEBUG
|
157
|
+
raise if rt.message[0..23] != "HTTP request failed: 405"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
@cui.error("DEBUG[#{Thread.current.inspect}]: Trying to create the directory #{@url.merge(@types[type] + '/').merge(subdir).to_s}") if $myDEBUG
|
161
|
+
mkdir(@url.merge(@types[type] + '/').merge(subdir))
|
162
|
+
end
|
163
|
+
retry
|
164
|
+
rescue Exception => ex
|
165
|
+
@cui.error(ex.message + ' at URL: ' + uri.to_s)
|
166
|
+
raise
|
167
|
+
else
|
168
|
+
@cache.add(type, file) if @@valid_caches.include?(type)
|
169
|
+
@cui.error("DEBUG[#{Thread.current.inspect}]: Added #{file} to cache list for #{subdir_type}") if $myDEBUG
|
170
|
+
return true
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def mkdir(uri)
|
175
|
+
mkcol = Net::HTTP::Mkcol.new(uri.request_uri)
|
176
|
+
mkcol.initialize_http_header({"User-Agent" => $options[:user_agent]})
|
177
|
+
mkcol.basic_auth($options[:username], $options[:password])
|
178
|
+
@cui.error("DEBUG[#{Thread.current.inspect}]: Trying to create directory #{uri.to_s}") if $myDEBUG
|
179
|
+
begin
|
180
|
+
request(mkcol)
|
181
|
+
rescue Exception => ex
|
182
|
+
# Check if it's just a 405 that happens when the directory already exists...
|
183
|
+
return if ex.message[0..23] == "HTTP request failed: 405"
|
184
|
+
@cui.error(ex.message + ' at URL: ' + uri.to_s)
|
185
|
+
raise
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def exists?(target_dir, type, file)
|
190
|
+
return @cache.search(type, file) if @@valid_caches.include?(type)
|
191
|
+
return false # TODO: Need to do a real check on the server instead...
|
192
|
+
end
|
193
|
+
|
194
|
+
def list(uri)
|
195
|
+
propfind = Net::HTTP::Propfind.new(uri.request_uri)
|
196
|
+
propfind.initialize_http_header({"User-Agent" => $options[:user_agent], "Depth" => "1", "Content-Type" => "application/octet-stream"})
|
197
|
+
propfind.basic_auth($options[:username], $options[:password])
|
198
|
+
propfind.body = '<?xml version="1.0" encoding="utf-8" ?><propfind xmlns="DAV:"><propname/></propfind>'
|
199
|
+
@cui.error("Retrieving file list for #{uri.to_s}...") if $myDEBUG
|
200
|
+
begin
|
201
|
+
response = request(propfind, 30)
|
202
|
+
rescue RuntimeError => ex
|
203
|
+
@cui.error("DEBUG[#{Thread.current.inspect}]: Caught exception #{ex} with message #{ex.message} during list") if $myDEBUG
|
204
|
+
return Array.new if ex.message[0..23] == "HTTP request failed: 404"
|
205
|
+
@cui.error("Caught exception #{ex.message} for URL #{uri.to_s} with request #{propfind.inspect}") if $myVERBOSE
|
206
|
+
rescue Exception => ex
|
207
|
+
@cui.error(ex.message + ' at URL: ' + uri.to_s)
|
208
|
+
raise
|
209
|
+
else
|
210
|
+
@cui.error("DEBUG[#{Thread.current.inspect}]: Parsing result for list...") if $myDEBUG
|
211
|
+
regexp = Regexp.new('<D:href>([^<]*)</D:href>', Regexp::IGNORECASE)
|
212
|
+
list = response.body.scan(regexp)
|
213
|
+
list.each_index { | i |
|
214
|
+
list[i] = list[i][0]
|
215
|
+
list[i] = list[i][(uri.request_uri.length)..(list[i].length)]
|
216
|
+
list[i] = nil if list[i] == ""
|
217
|
+
}
|
218
|
+
list.compact!
|
219
|
+
list.sort!
|
220
|
+
@cui.error("Found #{list.count} files") if $myDEBUG
|
221
|
+
list
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
data/classes/SMGFile.rb
ADDED
@@ -0,0 +1,292 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
#noinspection RubyResolve
|
4
|
+
require 'classes/Chunk'
|
5
|
+
#noinspection RubyResolve
|
6
|
+
require 'classes/Source'
|
7
|
+
#noinspection RubyResolve
|
8
|
+
require 'classes/Target'
|
9
|
+
require 'digest/md5'
|
10
|
+
#noinspection RubyResolve
|
11
|
+
require 'digest/sha2'
|
12
|
+
require 'time'
|
13
|
+
|
14
|
+
$myDEBUG = false if not $myDEBUG
|
15
|
+
$myVERBOSE = false if not $myVERBOSE
|
16
|
+
$options = nil unless $options
|
17
|
+
|
18
|
+
class SMGFile
|
19
|
+
attr_reader :directory, :name, :type
|
20
|
+
|
21
|
+
def initialize(file = nil, md5 = nil, sha256 = nil)
|
22
|
+
@cui = Cui.instance
|
23
|
+
@source = Source.instance
|
24
|
+
@target = Target.instance
|
25
|
+
@is_complete = false
|
26
|
+
@filename = file
|
27
|
+
@md5 = nil
|
28
|
+
@sha256 = nil
|
29
|
+
@size = 0
|
30
|
+
@mtime = 0
|
31
|
+
@meta_md5 = md5
|
32
|
+
@meta_sha256 = sha256
|
33
|
+
@chunks = Array.new
|
34
|
+
end
|
35
|
+
|
36
|
+
def backup!()
|
37
|
+
# Check the file type
|
38
|
+
case @source.ftype(@filename)
|
39
|
+
when 'file'
|
40
|
+
backup_file()
|
41
|
+
when 'directory'
|
42
|
+
backup_dir()
|
43
|
+
when 'characterSpecial'
|
44
|
+
# backup_dev(@filename) TODO
|
45
|
+
when 'blockSpecial'
|
46
|
+
# backup_dev(@filename) TODO
|
47
|
+
when 'fifo'
|
48
|
+
# backup_fifo(@filename) TODO
|
49
|
+
when 'link'
|
50
|
+
backup_link()
|
51
|
+
when 'socket'
|
52
|
+
# backup_socket(@filename) TODO
|
53
|
+
when 'unknown'
|
54
|
+
@cui.message("[E] Unsupported file type for: #{@filename}")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def backup_file()
|
59
|
+
@cui.current_file_name(@filename)
|
60
|
+
fd = @source.open_read(@filename)
|
61
|
+
stat = fd.lstat
|
62
|
+
ftype = stat.ftype
|
63
|
+
mtime = stat.mtime.utc.rfc2822
|
64
|
+
mode = "%o" % stat.mode
|
65
|
+
uid = stat.uid.to_s
|
66
|
+
gid = stat.gid.to_s
|
67
|
+
expected_size = stat.size
|
68
|
+
@cui.current_file_size(expected_size)
|
69
|
+
size = 0
|
70
|
+
md5hashing = Digest::MD5.new
|
71
|
+
sha256hashing = Digest::SHA2.new(256)
|
72
|
+
@cui.message("DEBUG[#{Thread.current.inspect}: Created hashing objects") if $myDEBUG
|
73
|
+
chunks = ''
|
74
|
+
while !fd.eof?
|
75
|
+
@cui.message("DEBUG[#{Thread.current.inspect}: Checked for EOF") if $myDEBUG
|
76
|
+
chunk = Chunk.new
|
77
|
+
@cui.message("DEBUG[#{Thread.current.inspect}: Created new chunk") if $myDEBUG
|
78
|
+
data = fd.read($options[:chunk_size])
|
79
|
+
@cui.message("DEBUG[#{Thread.current.inspect}: Read data") if $myDEBUG
|
80
|
+
chunk.set_data(data)
|
81
|
+
@cui.message("DEBUG[#{Thread.current.inspect}: Saved data in chunk") if $myDEBUG
|
82
|
+
md5hashing << data
|
83
|
+
@cui.message("DEBUG[#{Thread.current.inspect}: hashed for MD5") if $myDEBUG
|
84
|
+
sha256hashing << data
|
85
|
+
@cui.message("DEBUG[#{Thread.current.inspect}: hashed for sha256") if $myDEBUG
|
86
|
+
size += data.length
|
87
|
+
@cui.message("DEBUG[#{Thread.current.inspect}: Updated read size") if $myDEBUG
|
88
|
+
chunk.backup! # Upload this chunk
|
89
|
+
@cui.message("DEBUG[#{Thread.current.inspect}: backed up chunk") if $myDEBUG
|
90
|
+
chunks += chunk.sha256 + '.' + chunk.md5 + "\n"
|
91
|
+
@cui.finished_size_add(data.length)
|
92
|
+
end
|
93
|
+
fd.close()
|
94
|
+
@cui.message("DEBUG[#{Thread.current.inspect}: Closed FD") if $myDEBUG
|
95
|
+
md5 = md5hashing.hexdigest
|
96
|
+
@cui.message("DEBUG[#{Thread.current.inspect}: Got MD5") if $myDEBUG
|
97
|
+
sha256 = sha256hashing.hexdigest
|
98
|
+
@cui.message("DEBUG[#{Thread.current.inspect}: Got sha256") if $myDEBUG
|
99
|
+
# Build the metadata file
|
100
|
+
@metadata = "[stat]\n"
|
101
|
+
@metadata += "filename = " + @filename + "\n"
|
102
|
+
@metadata += "ftype = " + ftype + "\n"
|
103
|
+
@metadata += "mtime = " + mtime + "\n"
|
104
|
+
@metadata += "mode = " + mode + "\n"
|
105
|
+
@metadata += "uid = " + uid + "\n"
|
106
|
+
@metadata += "gid = " + gid + "\n"
|
107
|
+
@metadata += "size = " + size.to_s + "\n"
|
108
|
+
@metadata += "\n"
|
109
|
+
@metadata += "[checksums]\n"
|
110
|
+
@metadata += "md5 = " + md5 + "\n"
|
111
|
+
@metadata += "sha256 = " + sha256 + "\n"
|
112
|
+
@metadata += "\n"
|
113
|
+
@metadata += "[chunks]\n"
|
114
|
+
@metadata += chunks
|
115
|
+
@cui.message("Size mismatch: expected #{expected_size} but found #{size}!") if expected_size != size
|
116
|
+
@size = size
|
117
|
+
@mtime = mtime
|
118
|
+
@cui.message("DEBUG[#{Thread.current.inspect}: Checking if file already exists on server") if $myDEBUG
|
119
|
+
return true if @target.exists?(:file, "#{meta_sha256}.#{meta_md5}") # if this chunk already exists, no need to upload it
|
120
|
+
@cui.message("Uploading file metadata #{@filename}: #{meta_sha256}.#{meta_md5}...") if $myDEBUG
|
121
|
+
@target.write(:file, "#{meta_sha256}.#{meta_md5}", @metadata)
|
122
|
+
@cui.message("DEBUG[#{Thread.current.inspect}: Uploaded file metadata") if $myDEBUG
|
123
|
+
end
|
124
|
+
|
125
|
+
def backup_dir()
|
126
|
+
@cui.current_file_name(@filename)
|
127
|
+
stat = @source.lstat(@filename)
|
128
|
+
ftype = stat.ftype
|
129
|
+
mtime = stat.mtime.utc.rfc2822
|
130
|
+
mode = "%o" % stat.mode
|
131
|
+
uid = stat.uid.to_s
|
132
|
+
gid = stat.gid.to_s
|
133
|
+
# Build the metadata file
|
134
|
+
@metadata = "[stat]\n"
|
135
|
+
@metadata += "filename = " + @filename + "\n"
|
136
|
+
@metadata += "ftype = " + ftype + "\n"
|
137
|
+
@metadata += "mtime = " + mtime + "\n"
|
138
|
+
@metadata += "mode = " + mode + "\n"
|
139
|
+
@metadata += "uid = " + uid + "\n"
|
140
|
+
@metadata += "gid = " + gid + "\n"
|
141
|
+
@size = 0
|
142
|
+
@mtime = mtime
|
143
|
+
return true if @target.exists?(:file, "#{meta_sha256}.#{meta_md5}") # if this chunk already exists, no need to upload it
|
144
|
+
@cui.message("Uploading directory metadata #{@filename}: #{meta_sha256}.#{meta_md5}...") if $myVERBOSE
|
145
|
+
@target.write(:file, "#{meta_sha256}.#{meta_md5}", @metadata)
|
146
|
+
end
|
147
|
+
|
148
|
+
def backup_link()
|
149
|
+
@cui.message("Backing up link #{@filename}") if $myVERBOSE
|
150
|
+
stat = @source.lstat(@filename)
|
151
|
+
ftype = stat.ftype
|
152
|
+
mtime = stat.mtime.utc.rfc2822
|
153
|
+
mode = "%o" % stat.mode
|
154
|
+
uid = stat.uid.to_s
|
155
|
+
gid = stat.gid.to_s
|
156
|
+
link_target = @source.readlink(@filename)
|
157
|
+
@metadata = "[stat]\n"
|
158
|
+
@metadata += "filename = " + @filename + "\n"
|
159
|
+
@metadata += "ftype = " + ftype + "\n"
|
160
|
+
@metadata += "target = " + link_target + "\n"
|
161
|
+
@metadata += "mode = " + mode + "\n"
|
162
|
+
@metadata += "uid = " + uid + "\n"
|
163
|
+
@metadata += "gid = " + gid + "\n"
|
164
|
+
@size = 0
|
165
|
+
@mtime = mtime
|
166
|
+
return true if @target.exists?(:file, "#{meta_sha256}.#{meta_md5}") # if this chunk already exists, no need to upload it
|
167
|
+
@cui.message("Uploading file metadata #{@filename}: #{meta_sha256}.#{meta_md5}...") if $myVERBOSE
|
168
|
+
@target.write(:file, "#{meta_sha256}.#{meta_md5}", @metadata)
|
169
|
+
end
|
170
|
+
|
171
|
+
def restore!()
|
172
|
+
return false if ! @target.exists?(:file, "#{meta_sha256}.#{meta_md5}") # Cannot restore if metadata not available
|
173
|
+
@metadata = @target.read(:file, "#{meta_sha256}.#{meta_md5}")
|
174
|
+
parse_metadata
|
175
|
+
case @ftype
|
176
|
+
when 'file'
|
177
|
+
restore_file()
|
178
|
+
when 'directory'
|
179
|
+
restore_dir()
|
180
|
+
when 'characterSpecial'
|
181
|
+
# restore_dev(@filename) TODO
|
182
|
+
when 'blockSpecial'
|
183
|
+
# restore_dev(@filename) TODO
|
184
|
+
when 'fifo'
|
185
|
+
# restore_fifo(@filename) TODO
|
186
|
+
when 'link'
|
187
|
+
restore_link()
|
188
|
+
when 'socket'
|
189
|
+
# restore_socket(@filename) TODO
|
190
|
+
when 'unknown'
|
191
|
+
@cui.error("Unsupported file type for: #{@filename}")
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def restore_file()
|
196
|
+
fd = @source.open_write(@filename)
|
197
|
+
size = 0
|
198
|
+
md5hashing = Digest::MD5.new
|
199
|
+
sha256hashing = Digest::SHA2.new(256)
|
200
|
+
@chunks.each { | chunk_sum |
|
201
|
+
# Get sha256 and md5 for the chunk, they are verified in the chunk
|
202
|
+
sha256 = chunk_sum[0..63]
|
203
|
+
md5 = chunk_sum[65..96]
|
204
|
+
chunk = Chunk.new
|
205
|
+
chunk.md5 = md5
|
206
|
+
chunk.sha256 = sha256
|
207
|
+
chunk.restore!
|
208
|
+
fd.write(chunk.data)
|
209
|
+
size += chunk.length
|
210
|
+
md5hashing << chunk.data
|
211
|
+
sha256hashing << chunk.data
|
212
|
+
}
|
213
|
+
fd.close()
|
214
|
+
# Recover the file information
|
215
|
+
@source.set_stat(@filename, @mtime, @mode, @uid, @gid)
|
216
|
+
@cui.error("File size mismatch in file #{@filename}: #{sha256}.#{md5}") if size != @size
|
217
|
+
# Now get the hashes for the whole file and compare them
|
218
|
+
md5 = md5hashing.hexdigest
|
219
|
+
sha256 = sha256hashing.hexdigest
|
220
|
+
if (@md5 = md5) and (@sha256 = sha256) then
|
221
|
+
@cui.message("File verified")
|
222
|
+
else
|
223
|
+
@cui.error("Checksum error in file #{@filename}: #{sha256}.#{md5}")
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def restore_link()
|
228
|
+
@source.symlink(@filename, @target)
|
229
|
+
# @source.set_stat(@filename, Time.now, @mode, @uid, @gid)
|
230
|
+
end
|
231
|
+
|
232
|
+
def restore_dir()
|
233
|
+
if !(@source.exists?(@filename)) then
|
234
|
+
@source.mkdir(@filename)
|
235
|
+
@source.set_stat(@filename, @mtime, @mode, @uid, @gid)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def parse_metadata
|
240
|
+
chunk_data = /\[chunks\]\n([0-9a-zA-Z]{64}\.[0-9a-zA-Z]{32}\n)+/.match(@metadata)
|
241
|
+
chunk_data = chunk_data.to_s
|
242
|
+
chunk_data = chunk_data.split("\n")
|
243
|
+
chunk_data.delete_at(0)
|
244
|
+
@chunks = chunk_data
|
245
|
+
@metadata.lines { | line |
|
246
|
+
case
|
247
|
+
when line =~ /^[a-z0-9A-Z]+ = /
|
248
|
+
field = /^([a-z0-9A-Z]+) = /.match(line).captures[0]
|
249
|
+
case field
|
250
|
+
when "filename"
|
251
|
+
@filename = /^[a-z0-9A-Z]+ = (.*)$/.match(line).captures[0]
|
252
|
+
when "ftype"
|
253
|
+
@ftype = /^[a-z0-9A-Z]+ = (.*)$/.match(line).captures[0]
|
254
|
+
when "mtime"
|
255
|
+
@mtime = Time.rfc2822(/^[a-z0-9A-Z]+ = (.*)$/.match(line).captures[0])
|
256
|
+
when "mode"
|
257
|
+
@mode = /^[a-z0-9A-Z]+ = (.*)$/.match(line).captures[0].oct
|
258
|
+
when "uid"
|
259
|
+
@uid = /^[a-z0-9A-Z]+ = (.*)$/.match(line).captures[0].to_i
|
260
|
+
when "gid"
|
261
|
+
@gid = /^[a-z0-9A-Z]+ = (.*)$/.match(line).captures[0].to_i
|
262
|
+
when "size"
|
263
|
+
@size = /^[a-z0-9A-Z]+ = (.*)$/.match(line).captures[0].to_i
|
264
|
+
when "md5"
|
265
|
+
@md5 = /^[a-z0-9A-Z]+ = (.*)$/.match(line).captures[0]
|
266
|
+
when "sha256"
|
267
|
+
@sha256 = /^[a-z0-9A-Z]+ = (.*)$/.match(line).captures[0]
|
268
|
+
when "target"
|
269
|
+
@target = /^[a-z0-9A-Z]+ = (.*)$/.match(line).captures[0]
|
270
|
+
end
|
271
|
+
end
|
272
|
+
}
|
273
|
+
end
|
274
|
+
|
275
|
+
def meta_md5()
|
276
|
+
return @meta_md5 if @meta_md5
|
277
|
+
@meta_md5 = Digest::MD5.hexdigest(@metadata)
|
278
|
+
end
|
279
|
+
|
280
|
+
def meta_sha256()
|
281
|
+
return @meta_sha256 if @meta_sha256
|
282
|
+
@meta_sha256 = Digest::SHA2.new(256).hexdigest(@metadata)
|
283
|
+
end
|
284
|
+
|
285
|
+
def size()
|
286
|
+
@size
|
287
|
+
end
|
288
|
+
|
289
|
+
def mtime()
|
290
|
+
@mtime
|
291
|
+
end
|
292
|
+
end
|
data/classes/Source.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
require 'find'
|
5
|
+
|
6
|
+
$options = nil unless $options
|
7
|
+
|
8
|
+
class Source
|
9
|
+
include Singleton
|
10
|
+
|
11
|
+
def initialize()
|
12
|
+
@cui = Cui.instance
|
13
|
+
@files = nil
|
14
|
+
raise "Source (#{$options[:source]}) is not a directory" if !(File.directory?($options[:source]))
|
15
|
+
# Remove line endings if necessary
|
16
|
+
$options[:excl_incl].each { | s | s.chomp! }
|
17
|
+
end
|
18
|
+
|
19
|
+
def exists?(path = '')
|
20
|
+
File.exists?($options[:source] + path)
|
21
|
+
end
|
22
|
+
|
23
|
+
def open_read(path)
|
24
|
+
File.new($options[:source] + path, 'rb+')
|
25
|
+
end
|
26
|
+
|
27
|
+
def open_write(path)
|
28
|
+
File.new($options[:source] + path, 'wb+')
|
29
|
+
end
|
30
|
+
|
31
|
+
def mkdir(path)
|
32
|
+
Dir.mkdir($options[:source] + path)
|
33
|
+
end
|
34
|
+
|
35
|
+
def ftype(path)
|
36
|
+
File.ftype($options[:source] + path)
|
37
|
+
end
|
38
|
+
|
39
|
+
def lstat(path)
|
40
|
+
File.lstat($options[:source] + path)
|
41
|
+
end
|
42
|
+
|
43
|
+
def readlink(path)
|
44
|
+
File.readlink($options[:source] + path)
|
45
|
+
end
|
46
|
+
|
47
|
+
def symlink(path, target)
|
48
|
+
File.symlink(target, $options[:source] + path)
|
49
|
+
end
|
50
|
+
|
51
|
+
def set_stat(path, mtime, mode, uid, gid)
|
52
|
+
if File.lstat($options[:source] + path).ftype != 'link' then
|
53
|
+
File.utime(0, mtime, $options[:source] + path)
|
54
|
+
File.chmod(mode, $options[:source] + path)
|
55
|
+
File.chown(uid, gid, $options[:source] + path)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def files(rescan = false)
|
60
|
+
if not rescan then
|
61
|
+
return @files if @files
|
62
|
+
end
|
63
|
+
@files = Array.new
|
64
|
+
options_source_length = $options[:source].length
|
65
|
+
Find.find($options[:source]) { | f |
|
66
|
+
# Remove the leading full path
|
67
|
+
f = f[(options_source_length)..99999]
|
68
|
+
f = '/' if f.empty?
|
69
|
+
include = true # by default include everything
|
70
|
+
# Now loop over the include/exclude globs
|
71
|
+
$options[:excl_incl].each { | ei |
|
72
|
+
what = ei[0..0] # First character, this syntax is required for ruby 1.8
|
73
|
+
glob = ei[1..99999] # More than a hundred thousand characters? bad luck...
|
74
|
+
case what # Check the glob for a match and update include variable
|
75
|
+
when '-'
|
76
|
+
include = false # glob matched but was an exclude
|
77
|
+
when '+'
|
78
|
+
include = true # glob matched and is an include
|
79
|
+
end if File.fnmatch(glob, f)
|
80
|
+
}
|
81
|
+
if include then
|
82
|
+
size = 0
|
83
|
+
size = File.size($options[:source] + f) if File.file?($options[:source] + f)
|
84
|
+
@cui.total_size_add(size)
|
85
|
+
@cui.total_files_inc
|
86
|
+
@files << f
|
87
|
+
else
|
88
|
+
Find.prune # Skip directories or files if they are not to be included
|
89
|
+
end
|
90
|
+
}
|
91
|
+
@files
|
92
|
+
end
|
93
|
+
end
|