palobr 0.1.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.
- data/Gemfile +11 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +19 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/bin/palobr +209 -0
- data/lib/backup.rb +120 -0
- data/lib/backup/file_item.rb +18 -0
- data/lib/backup/file_item/base.rb +37 -0
- data/lib/backup/file_item/cloud.rb +91 -0
- data/lib/backup/file_item/local.rb +26 -0
- data/lib/backup/jar.rb +163 -0
- data/lib/backup/timestamp.rb +48 -0
- data/lib/crypto.rb +43 -0
- data/lib/helpers.rb +80 -0
- data/palobr.gemspec +77 -0
- data/test/helper.rb +15 -0
- data/test/test_backup.rb +7 -0
- data/test/test_backup_file_item.rb +31 -0
- data/test/test_backup_timestamp.rb +65 -0
- data/test/test_crypto.rb +59 -0
- metadata +145 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
module Backup
|
2
|
+
module FileItem
|
3
|
+
class Base
|
4
|
+
def semantic_path(path)
|
5
|
+
if Dir.exists? path
|
6
|
+
path += '/'
|
7
|
+
else
|
8
|
+
path
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def stat(file, timestamp = nil)
|
13
|
+
files = {}
|
14
|
+
|
15
|
+
stat = File.new(file).stat
|
16
|
+
files[file] = {
|
17
|
+
:uid => stat.uid,
|
18
|
+
:gid => stat.gid,
|
19
|
+
:mode => stat.mode
|
20
|
+
}
|
21
|
+
files[file][:timestamp] = timestamp if timestamp
|
22
|
+
|
23
|
+
unless Dir.exists?(file)
|
24
|
+
files[file][:checksum] = Digest::MD5.hexdigest(File.open(file).read)
|
25
|
+
end
|
26
|
+
|
27
|
+
files
|
28
|
+
rescue Exception => e
|
29
|
+
STDERR.puts e
|
30
|
+
end
|
31
|
+
|
32
|
+
def file_hash(file)
|
33
|
+
Digest::MD5.hexdigest file
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'backup/file_item/base'
|
2
|
+
|
3
|
+
module Backup
|
4
|
+
module FileItem
|
5
|
+
class Cloud < Backup::FileItem::Base
|
6
|
+
attr_reader :key, :secret, :backet, :provider
|
7
|
+
|
8
|
+
def initialize(args = {})
|
9
|
+
puts_fail "Empty hash in Cloud initialize method" if args.empty?
|
10
|
+
|
11
|
+
[:key, :secret, :bucket].each do |arg|
|
12
|
+
puts_fail "'#{arg.to_s.green}' should not be empty" if args[arg].nil?
|
13
|
+
instance_eval %{@#{arg} = args[:#{arg}]}
|
14
|
+
end
|
15
|
+
|
16
|
+
try_to_connect_with_cloud
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_directory_once(*directories)
|
20
|
+
# Nothing happen
|
21
|
+
end
|
22
|
+
|
23
|
+
def create_file_once(file, data)
|
24
|
+
try_to_work_with_cloud do
|
25
|
+
@directory.files.create(
|
26
|
+
:key => delete_slashes(file),
|
27
|
+
:body => data
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def read_file(file)
|
33
|
+
try_to_work_with_cloud do
|
34
|
+
file = delete_slashes(file)
|
35
|
+
remote_file = @directory.files.get(file)
|
36
|
+
remote_file.body if remote_file
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def dir(path, mask = "*")
|
41
|
+
path = delete_slashes(path)
|
42
|
+
mask = mask.gsub('.', '\.').gsub('*', '[^\/]')
|
43
|
+
|
44
|
+
try_to_work_with_cloud do
|
45
|
+
files = @directory.files.map &:key
|
46
|
+
end
|
47
|
+
|
48
|
+
files.map do |item|
|
49
|
+
match = item.match(/^#{path}\/([^\/]+#{mask}).*$/)
|
50
|
+
match[1] if match
|
51
|
+
end.compact.uniq
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def delete_slashes(str)
|
57
|
+
str.chop! if str =~ /\/$/
|
58
|
+
str = str[1, str.length] if str =~ /^\//
|
59
|
+
str
|
60
|
+
end
|
61
|
+
|
62
|
+
def try_to_work_with_cloud(&block)
|
63
|
+
begin
|
64
|
+
yield
|
65
|
+
rescue Exception => e
|
66
|
+
try_to_connect_with_cloud
|
67
|
+
|
68
|
+
yield
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
def try_to_connect_with_cloud
|
74
|
+
begin
|
75
|
+
@connection = ::Fog::Storage.new(
|
76
|
+
:provider => 'AWS',
|
77
|
+
:aws_secret_access_key => @secret,
|
78
|
+
:aws_access_key_id => @key
|
79
|
+
)
|
80
|
+
|
81
|
+
@directory = @connection.directories.get(@bucket)
|
82
|
+
rescue Exception => e
|
83
|
+
puts_verbose e.message
|
84
|
+
puts_fail "403 Forbidden"
|
85
|
+
end
|
86
|
+
|
87
|
+
puts_fail "Bucket '#{@bucket}' is not exists." if @directory.nil?
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'backup/file_item/base'
|
2
|
+
|
3
|
+
module Backup
|
4
|
+
module FileItem
|
5
|
+
class Local < Backup::FileItem::Base
|
6
|
+
def create_directory_once(*directories)
|
7
|
+
directories.each do |path|
|
8
|
+
FileUtils.mkdir_p(path) unless Dir.exists?(path)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def create_file_once(file, data)
|
13
|
+
date = date.read if date.is_a? File
|
14
|
+
File.open(file, "w").puts(data) unless File.exists?(file)
|
15
|
+
end
|
16
|
+
|
17
|
+
def read_file(file)
|
18
|
+
open(file).read if File.exists? file
|
19
|
+
end
|
20
|
+
|
21
|
+
def dir(path, mask = "*")
|
22
|
+
Dir["#{path}/#{mask}"]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/backup/jar.rb
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
module Backup
|
2
|
+
class Jar
|
3
|
+
def initialize(file_item, root_path, local_path)
|
4
|
+
@root_path = root_path
|
5
|
+
@local_path = local_path
|
6
|
+
@timestamp = Backup::Timestamp.create
|
7
|
+
@file_item = file_item
|
8
|
+
end
|
9
|
+
|
10
|
+
def jar_hash
|
11
|
+
Digest::MD5.hexdigest(@local_path)
|
12
|
+
end
|
13
|
+
|
14
|
+
def save(increment = false)
|
15
|
+
unless increment
|
16
|
+
@local_files = hash_local_files
|
17
|
+
else
|
18
|
+
@local_files = {}
|
19
|
+
current_files = hash_local_files
|
20
|
+
|
21
|
+
last_timestamp = Jar.jar_versions(@root_path, jar_hash, true).last
|
22
|
+
|
23
|
+
if last_timestamp.nil?
|
24
|
+
puts_fail "First you must create a full backup for #{@local_path.dark_green}"
|
25
|
+
end
|
26
|
+
|
27
|
+
last_index = Jar.fetch_index_for(@root_path, jar_hash, last_timestamp)
|
28
|
+
|
29
|
+
current_files.keys.each do |file|
|
30
|
+
@local_files[file] = current_files[file]
|
31
|
+
|
32
|
+
#TODO: Cut to a new method {
|
33
|
+
current = current_files[file].dup
|
34
|
+
current.delete(:timestamp)
|
35
|
+
|
36
|
+
unless last_index[file].nil?
|
37
|
+
backup = last_index[file].dup
|
38
|
+
backup.delete(:timestamp)
|
39
|
+
|
40
|
+
if (current == backup) or
|
41
|
+
(!current[:checksum].nil? and current[:checksum] == backup[:checksum])
|
42
|
+
|
43
|
+
@local_files[file][:timestamp] = last_index[file][:timestamp]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
# }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
@file_item.create_directory_once meta_jars_path, meta_jar_path, jar_data_path
|
51
|
+
@file_item.create_file_once(
|
52
|
+
"#{meta_jars_path}/#{jar_hash}",
|
53
|
+
@file_item.semantic_path(@local_path)
|
54
|
+
)
|
55
|
+
@file_item.create_file_once(
|
56
|
+
"#{meta_jar_path}/#{@timestamp}.yml",
|
57
|
+
@local_files.to_yaml
|
58
|
+
)
|
59
|
+
|
60
|
+
if @file_item.is_a? Backup::FileItem::Cloud
|
61
|
+
pbar = ProgressBar.new(
|
62
|
+
"Uploading",
|
63
|
+
@local_files.keys.count
|
64
|
+
)
|
65
|
+
else
|
66
|
+
pbar = ProgressBar.new(
|
67
|
+
"Copying",
|
68
|
+
@local_files.keys.count
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
pbar.bar_mark = '*'
|
73
|
+
|
74
|
+
@local_files.keys.each do |file|
|
75
|
+
unless Dir.exists?(file)
|
76
|
+
@file_item.create_file_once "#{jar_data_path}/#{@file_item.file_hash file}",
|
77
|
+
File.open(file)
|
78
|
+
pbar.inc
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
pbar.finish
|
83
|
+
end
|
84
|
+
|
85
|
+
def hash_local_files
|
86
|
+
files = {}
|
87
|
+
|
88
|
+
puts_verbose "Create index for #{@local_path.dark_green}"
|
89
|
+
|
90
|
+
if Dir.exists? @local_path
|
91
|
+
matches = Dir.glob(File.join(@local_path, "/**/*"), File::FNM_DOTMATCH)
|
92
|
+
|
93
|
+
matches = matches.select do |match|
|
94
|
+
match[/\/..$/].nil? and match[/\/.$/].nil?
|
95
|
+
end
|
96
|
+
|
97
|
+
matches << @local_path
|
98
|
+
|
99
|
+
matches.each do |match|
|
100
|
+
files.merge!(@file_item.stat(match, @timestamp))
|
101
|
+
end
|
102
|
+
else
|
103
|
+
files = @file_item.stat(@local_path, @timestamp)
|
104
|
+
end
|
105
|
+
|
106
|
+
files
|
107
|
+
end
|
108
|
+
|
109
|
+
class << self
|
110
|
+
def hash_to_path(file_item, root_path, hash)
|
111
|
+
file_item.read_file("#{root_path}/meta/jars/#{hash}").chomp
|
112
|
+
rescue Errno::ENOENT
|
113
|
+
""
|
114
|
+
end
|
115
|
+
|
116
|
+
def all(file_item, root_path)
|
117
|
+
hashes = file_item.dir("#{root_path}/meta/jars").map do |backup|
|
118
|
+
backup[/[0-9a-z]{32}$/]
|
119
|
+
end.compact.sort
|
120
|
+
|
121
|
+
result = {}
|
122
|
+
|
123
|
+
hashes.each do |hash|
|
124
|
+
jar_local_path = Jar.hash_to_path(file_item, root_path, hash)
|
125
|
+
result[jar_local_path] = hash unless jar_local_path.empty?
|
126
|
+
end
|
127
|
+
|
128
|
+
result
|
129
|
+
end
|
130
|
+
|
131
|
+
def jar_versions(file_item, root_path, jar, hash = false)
|
132
|
+
jar = jar.chop if jar =~ /\/$/
|
133
|
+
jar = Digest::MD5.hexdigest(jar) unless hash
|
134
|
+
|
135
|
+
meta_jar_path = "#{root_path}/meta/#{jar}"
|
136
|
+
|
137
|
+
file_item.dir(meta_jar_path, "*.yml").map do |file|
|
138
|
+
match = file.match(/^\/?([0-9]{12}).yml$/)
|
139
|
+
match[1] if match
|
140
|
+
end.compact.sort
|
141
|
+
end
|
142
|
+
|
143
|
+
def fetch_index_for(file_item, root_path, hash, timestamp)
|
144
|
+
index = file_item.read_file "#{root_path}/meta/#{hash}/#{timestamp}.yml"
|
145
|
+
YAML::load(index) unless index.nil?
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
def meta_jars_path
|
152
|
+
"#{@root_path}/meta/jars"
|
153
|
+
end
|
154
|
+
|
155
|
+
def meta_jar_path
|
156
|
+
"#{@root_path}/meta/#{jar_hash}"
|
157
|
+
end
|
158
|
+
|
159
|
+
def jar_data_path
|
160
|
+
"#{@root_path}/#{jar_hash}/#{@timestamp}"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Backup
|
2
|
+
class Timestamp
|
3
|
+
def self.parse_timestamp(version, last = false)
|
4
|
+
version = version.gsub(".", "").gsub(" ", "").gsub(":", "")
|
5
|
+
|
6
|
+
puts_fail "Invalid date format: #{version}" unless version.match /[0-9]{6,}/
|
7
|
+
|
8
|
+
year, month, day, hour, min, sec =
|
9
|
+
version.split(/([0-9]{2})/).map do |date|
|
10
|
+
date.to_i unless date.empty?
|
11
|
+
end.compact
|
12
|
+
|
13
|
+
if last
|
14
|
+
hour = 23 if hour.nil?
|
15
|
+
min = 59 if min.nil?
|
16
|
+
sec = 59 if sec.nil?
|
17
|
+
end
|
18
|
+
|
19
|
+
time = Time.new(year + 2000, month, day, hour, min, sec, 0)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.last_from(list, end_date, start_date = nil)
|
23
|
+
list.sort.reverse.find do |version|
|
24
|
+
version = Backup::Timestamp.parse_timestamp version
|
25
|
+
|
26
|
+
unless start_date.nil?
|
27
|
+
version >= start_date and version <= end_date
|
28
|
+
else
|
29
|
+
version <= end_date
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.create(time = nil)
|
35
|
+
time = time.nil? ? Time.now : time
|
36
|
+
|
37
|
+
time.utc.strftime "%y%m%d%H%M%S"
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.to_str(version)
|
41
|
+
to_s parse_timestamp(version)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.to_s(time)
|
45
|
+
time.strftime "%y.%m.%d %H:%M:%S" if time.is_a? Time
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/crypto.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
module Crypto
|
5
|
+
|
6
|
+
def self.create_keys(priv = "rsa_key", pub = "#{priv}.pub", bits = 4096)
|
7
|
+
private_key = OpenSSL::PKey::RSA.new(bits)
|
8
|
+
File.open(priv, "w+") { |fp| fp << private_key.to_s }
|
9
|
+
File.open(pub, "w+") { |fp| fp << private_key.public_key.to_s }
|
10
|
+
private_key
|
11
|
+
end
|
12
|
+
|
13
|
+
class Key
|
14
|
+
def initialize(data)
|
15
|
+
@public = (data =~ /^-----BEGIN (RSA|DSA) PRIVATE KEY-----$/).nil?
|
16
|
+
@key = OpenSSL::PKey::RSA.new(data)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.from_file(filename)
|
20
|
+
self.new File.read( filename )
|
21
|
+
end
|
22
|
+
|
23
|
+
def encrypt(text)
|
24
|
+
@key.send("#{key_type}_encrypt", text)
|
25
|
+
end
|
26
|
+
|
27
|
+
def decrypt(text)
|
28
|
+
@key.send("#{key_type}_decrypt", text)
|
29
|
+
end
|
30
|
+
|
31
|
+
def private?
|
32
|
+
!@public
|
33
|
+
end
|
34
|
+
|
35
|
+
def public?
|
36
|
+
@public
|
37
|
+
end
|
38
|
+
|
39
|
+
def key_type
|
40
|
+
@public ? :public : :private
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/helpers.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
def puts_fail(msg)
|
2
|
+
STDERR.puts "#{"Error: ".red}#{msg}"
|
3
|
+
|
4
|
+
exit msg.length
|
5
|
+
end
|
6
|
+
|
7
|
+
def puts_verbose(msg)
|
8
|
+
puts msg if $PRINT_VERBOSE
|
9
|
+
end
|
10
|
+
|
11
|
+
def print_verbose(msg)
|
12
|
+
print msg if $PRINT_VERBOSE
|
13
|
+
end
|
14
|
+
|
15
|
+
def safe_require(&block)
|
16
|
+
yield
|
17
|
+
rescue Exception => e
|
18
|
+
puts_fail %Q{This script use these gems: fog, slop.
|
19
|
+
Make sure that you have them all.
|
20
|
+
If you don't have, you may install them: $ gem install fog slop ruby-progressbar
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def try_create_dir(dir)
|
25
|
+
begin
|
26
|
+
FileUtils.mkdir_p dir unless Dir.exists? dir
|
27
|
+
rescue Errno::EACCES
|
28
|
+
puts_fail "Permission denied for #{dir.dark_green}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def check_mode(file, first, second)
|
33
|
+
unless first == second
|
34
|
+
puts_fail "Permission wasn't changed for #{file.dark_green}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def check_rights(file, first_uid, first_gid, second_uid, second_gid)
|
39
|
+
unless first_uid == second_uid and first_gid == second_gid
|
40
|
+
puts_fail "Group and user wasn't change for #{file.dark_green}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class String
|
45
|
+
def red
|
46
|
+
colorize(self, "\e[1m\e[31m")
|
47
|
+
end
|
48
|
+
|
49
|
+
def green
|
50
|
+
colorize(self, "\e[1m\e[32m")
|
51
|
+
end
|
52
|
+
|
53
|
+
def dark_green
|
54
|
+
colorize(self, "\e[32m")
|
55
|
+
end
|
56
|
+
|
57
|
+
def yellow
|
58
|
+
colorize(self, "\e[1m\e[33m")
|
59
|
+
end
|
60
|
+
|
61
|
+
def blue
|
62
|
+
colorize(self, "\e[1m\e[34m")
|
63
|
+
end
|
64
|
+
|
65
|
+
def dark_blue
|
66
|
+
colorize(self, "\e[34m")
|
67
|
+
end
|
68
|
+
|
69
|
+
def pur
|
70
|
+
colorize(self, "\e[1m\e[35m")
|
71
|
+
end
|
72
|
+
|
73
|
+
def colorize(text, color_code)
|
74
|
+
if $COLORIZE
|
75
|
+
"#{color_code}#{text}\e[0m"
|
76
|
+
else
|
77
|
+
text
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|