backup_restore 0.0.0
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.
- checksums.yaml +7 -0
- data/lib/backup_restore.rb +232 -0
- metadata +45 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4cc90b331805ae3173b46a005168fab2362fc65d
|
4
|
+
data.tar.gz: 241e2d48b20b02af82146729602ecc7d24f3d7a5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cbe1a94d29b229713833c2dd915d3e1a38189e7474dc0e74a32e0f969c94b4e8a3970bc98657bb13481f71855efe3e3bbfac5863ee1c38235b3b47a7e1b05f75
|
7
|
+
data.tar.gz: e47f474d1cba749be8406f982448172ee31d8058eb8cb532bea9aa7b5ef9760374f642578dad3d5d8f4aa4f0b7b0197edd444b4dfc07348f753be26955bccfdf
|
@@ -0,0 +1,232 @@
|
|
1
|
+
# Encoding: utf-8
|
2
|
+
|
3
|
+
# Copyright (C) 2016 - GPLv3 - Mateusz Konieczny
|
4
|
+
|
5
|
+
require 'io/console'
|
6
|
+
require 'fileutils'
|
7
|
+
|
8
|
+
# some assumptions:
|
9
|
+
# this tool runs on Ubuntu
|
10
|
+
# archive is valid archive generated with backup gem
|
11
|
+
# files are encrypted and archived with tar
|
12
|
+
# archives may be split or not
|
13
|
+
|
14
|
+
class BackupRestore
|
15
|
+
def self.debug(message, priority = :medium)
|
16
|
+
return if priority == :low
|
17
|
+
return if priority == :medium
|
18
|
+
puts message
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.change_directory(target)
|
22
|
+
Dir.chdir(target)
|
23
|
+
return if File.identical?(target, Dir.getwd)
|
24
|
+
raise "failed to change working directory to #{target} (it is #{Dir.getwd})"
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.get_storage_folder(archive_storage_root, archive_name)
|
28
|
+
debug("joining <#{archive_storage_root}> and <#{archive_name}>", :low)
|
29
|
+
target = archive_storage_root + '/' + archive_name
|
30
|
+
debug("looking for date folder in <#{target}>", :low)
|
31
|
+
change_directory(target)
|
32
|
+
directory = Dir.glob('*').select { |f| File.directory? f }
|
33
|
+
if directory.length != 1
|
34
|
+
puts "unexpected multiple backups at once in #{target}, or backup not found"
|
35
|
+
puts "not supposed to happen in my workflow"
|
36
|
+
puts "listing #{directory.length} directories, expected exactly 1:"
|
37
|
+
directory.each do |file|
|
38
|
+
puts file
|
39
|
+
end
|
40
|
+
raise "unhandled workflow"
|
41
|
+
end
|
42
|
+
target += '/' + directory[0] + '/'
|
43
|
+
return target
|
44
|
+
end
|
45
|
+
|
46
|
+
# alternatives - see http://stackoverflow.com/questions/3159945/running-command-line-commands-within-ruby-script
|
47
|
+
def self.execute_command(command, unstandard_error_free_exit_codes = [])
|
48
|
+
output = `#{command}`
|
49
|
+
if $?.success? || unstandard_error_free_exit_codes.include?($?.exitstatus)
|
50
|
+
debug('all done', :low)
|
51
|
+
else
|
52
|
+
raise "<#{command}> command had problem (<#{$?}> with output <#{output}>). Working directory path was <#{Dir.getwd}>"
|
53
|
+
end
|
54
|
+
return output
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.extract_tar_file(file, target_folder = nil)
|
58
|
+
command = "tar --extract --file=#{file}"
|
59
|
+
unless target_folder.nil?
|
60
|
+
command += " --preserve-permissions -C #{target_folder}"
|
61
|
+
# -C
|
62
|
+
# tar will change its current directory to dir before performing any operations
|
63
|
+
# https://www.gnu.org/software/tar/manual/html_node/Option-Summary.html
|
64
|
+
end
|
65
|
+
# base test:
|
66
|
+
# echo "tar: Removing leading \`/' from member names" | grep -v "tar: Removing leading \`/' from member names"
|
67
|
+
# shell should get:
|
68
|
+
# grep -v "tar: Removing leading \`/' from member names"
|
69
|
+
# command += ' | grep -v "tar: Removing leading \`/\' from member names"'
|
70
|
+
# the code above is not proper way to solve this, it will mess up errorcode (hide errors, grep will return error on lack of match)
|
71
|
+
execute_command(command)
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.get_the_only_expected_file(filter = '*')
|
75
|
+
files = Dir.glob(filter)
|
76
|
+
puts files
|
77
|
+
if files.length != 1
|
78
|
+
if files.empty?
|
79
|
+
puts 'no files found'
|
80
|
+
else
|
81
|
+
puts "files:"
|
82
|
+
end
|
83
|
+
for file in files
|
84
|
+
puts file
|
85
|
+
end
|
86
|
+
raise "expected exactly one file, not handled!"
|
87
|
+
end
|
88
|
+
return files[0]
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.uncrypt_archive(archive_storage_root, archive_name, password)
|
92
|
+
storage = get_storage_folder(archive_storage_root, archive_name)
|
93
|
+
output_archive = archive_name + '.tar'
|
94
|
+
change_directory(storage)
|
95
|
+
command = "openssl aes-256-cbc -d -in #{archive_name}.tar.enc -k #{password} -out #{output_archive}"
|
96
|
+
execute_command(command)
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.extract_archive(archive_storage_root, archive_name, unpack_root)
|
100
|
+
debug("unpacking <#{archive_name}>", :high)
|
101
|
+
|
102
|
+
storage = get_storage_folder(archive_storage_root, archive_name)
|
103
|
+
change_directory(storage)
|
104
|
+
debug("archive is stored at <#{storage}>")
|
105
|
+
|
106
|
+
file = get_the_only_expected_file('*.tar')
|
107
|
+
debug("extracting #{file}")
|
108
|
+
extract_tar_file(file)
|
109
|
+
folder_with_unpacked_archive = storage + archive_name
|
110
|
+
debug("unpacked archive with second layer of archive is stored at <#{folder_with_unpacked_archive}>")
|
111
|
+
|
112
|
+
change_directory(folder_with_unpacked_archive + '/archives/')
|
113
|
+
file = get_the_only_expected_file('*.tar.gz')
|
114
|
+
debug("extracting #{file}")
|
115
|
+
extract_tar_file(file, unpack_root)
|
116
|
+
|
117
|
+
change_directory(storage)
|
118
|
+
FileUtils.rm_rf(folder_with_unpacked_archive)
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.is_unsplitting_necessary(archive_storage_root, archive_name)
|
122
|
+
storage = get_storage_folder(archive_storage_root, archive_name)
|
123
|
+
return File.exist?(storage + "#{archive_name}.tar.enc-aaa")
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.unsplit_archive(archive_storage_root, archive_name)
|
127
|
+
storage = get_storage_folder(archive_storage_root, archive_name)
|
128
|
+
change_directory(storage)
|
129
|
+
execute_command("cat #{archive_name}.tar.enc-* > #{archive_name}.tar.enc")
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.process_given_archive(archive_storage_root, archive_name, unpack_root, password)
|
133
|
+
debug("processsing #{archive_name} in #{archive_storage_root} - extracting to #{unpack_root}", :high)
|
134
|
+
if is_unsplitting_necessary(archive_storage_root, archive_name)
|
135
|
+
unsplit_archive(archive_storage_root, archive_name)
|
136
|
+
end
|
137
|
+
uncrypt_archive(archive_storage_root, archive_name, password)
|
138
|
+
extract_archive(archive_storage_root, archive_name, unpack_root)
|
139
|
+
storage = get_storage_folder(archive_storage_root, archive_name)
|
140
|
+
if is_unsplitting_necessary(archive_storage_root, archive_name)
|
141
|
+
FileUtils.rm_rf(storage + archive_name + ".tar.enc")
|
142
|
+
end
|
143
|
+
FileUtils.rm_rf(storage + archive_name + ".tar")
|
144
|
+
end
|
145
|
+
|
146
|
+
def self.compare(compared_path, unpack_root)
|
147
|
+
text = compare_paths(compared_path, unpack_root)
|
148
|
+
return text
|
149
|
+
end
|
150
|
+
|
151
|
+
class UnexpectedData < StandardError
|
152
|
+
def self.initialize(message)
|
153
|
+
super(message)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def self.discard_unimportant(text, unimportant_paths_array, possible_prefix = [])
|
158
|
+
possible_prefix << ""
|
159
|
+
output = ""
|
160
|
+
text.split("\n").each do |line|
|
161
|
+
line.strip!
|
162
|
+
unimportant = false
|
163
|
+
unimportant_paths_array.each do |filter|
|
164
|
+
possible_prefix.each do |prefix|
|
165
|
+
r_filter = (Regexp.escape filter).gsub('/', '\/')
|
166
|
+
r_prefix = (Regexp.escape prefix).gsub('/', '\/')
|
167
|
+
if line =~ /\AOnly in (.+): (.+)\z/
|
168
|
+
filepath_without_file, file = /\AOnly in (.+): (.+)\z/.match(line).captures
|
169
|
+
filepath_without_file += '/' if filepath_without_file[-1] != '/'
|
170
|
+
filepath = filepath_without_file + file
|
171
|
+
unimportant = true if filepath =~ /\A#{r_prefix}#{r_filter}.*\z/
|
172
|
+
elsif line =~ /\AFiles (.+) and (.+) differ\z/
|
173
|
+
filepath_a, filepath_b = /\AFiles (.+) and (.+) differ\z/.match(line).captures
|
174
|
+
unimportant = true if filepath_a =~ /\A#{r_prefix}#{r_filter}.*\z/
|
175
|
+
unimportant = true if filepath_b =~ /\A#{r_prefix}#{r_filter}.*\z/
|
176
|
+
elsif line =~ /\AFile (.+) is a fifo while file (.+) is a fifo\z/
|
177
|
+
unimportant = true
|
178
|
+
elsif line =~ /\AFile (.+) is a character special file while file (.+) is a character special file\z/
|
179
|
+
unimportant = true
|
180
|
+
elsif line == everything_is_fine_message.strip
|
181
|
+
next
|
182
|
+
elsif line == ""
|
183
|
+
next
|
184
|
+
else
|
185
|
+
raise UnexpectedData, "unexpected line <#{line}>"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
next if unimportant
|
190
|
+
output += line + "\n"
|
191
|
+
end
|
192
|
+
puts
|
193
|
+
return nil if output == ""
|
194
|
+
return output.to_s
|
195
|
+
end
|
196
|
+
|
197
|
+
def self.everything_is_fine_or_unimportant_message
|
198
|
+
"no important differences"
|
199
|
+
end
|
200
|
+
|
201
|
+
def self.compare_paths(path_to_backuped, backup_location)
|
202
|
+
original = path_to_backuped
|
203
|
+
restored = backup_location + path_to_backuped
|
204
|
+
raise "missing folder for comparison: #{original}" unless Dir.exist?(original)
|
205
|
+
raise "missing folder for comparison: #{restored}" unless Dir.exist?(restored)
|
206
|
+
command = "diff --brief -r --no-dereference '#{original}' '#{restored}'"
|
207
|
+
puts
|
208
|
+
puts command
|
209
|
+
puts
|
210
|
+
returned = execute_command(command, [1])
|
211
|
+
if returned == ""
|
212
|
+
return everything_is_fine_message
|
213
|
+
else
|
214
|
+
return returned
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def self.everything_is_fine_message
|
219
|
+
return "everything is fine!" + "\n"
|
220
|
+
end
|
221
|
+
|
222
|
+
def self.directory_size(path)
|
223
|
+
size = 0
|
224
|
+
Dir.glob(File.join(path, '**', '*')) { |file| size += File.size(file) }
|
225
|
+
return size
|
226
|
+
end
|
227
|
+
|
228
|
+
def self.is_it_at_least_this_size_in_mb(path, mb)
|
229
|
+
size = directory_size(path)
|
230
|
+
return size > mb * 1024 * 1024
|
231
|
+
end
|
232
|
+
end
|
metadata
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: backup_restore
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mateusz Konieczny
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-05-21 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: 'Script for unpacking backups produced by the backup gem. See https://github.com/backup/backup-features/issues/28
|
14
|
+
for discussion about this feature in backup gem itself. '
|
15
|
+
email: matkoniecz@gmail.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- lib/backup_restore.rb
|
21
|
+
homepage: https://github.com/matkoniecz/backup-gem-extractor-crutch
|
22
|
+
licenses:
|
23
|
+
- GPL-3.0
|
24
|
+
metadata: {}
|
25
|
+
post_install_message:
|
26
|
+
rdoc_options: []
|
27
|
+
require_paths:
|
28
|
+
- lib
|
29
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
requirements: []
|
40
|
+
rubyforge_project:
|
41
|
+
rubygems_version: 2.6.4
|
42
|
+
signing_key:
|
43
|
+
specification_version: 4
|
44
|
+
summary: Script for unpacking backups produced by the backup gem.
|
45
|
+
test_files: []
|