syc-backup 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +48 -0
- data/Rakefile +7 -0
- data/bin/sycbackup +6 -0
- data/doc/Backup.html +186 -0
- data/doc/Backup/CronEdit.html +381 -0
- data/doc/Backup/Environment.html +231 -0
- data/doc/Backup/FileBackup.html +363 -0
- data/doc/Backup/MySQLBackup.html +305 -0
- data/doc/Backup/Options.html +328 -0
- data/doc/Backup/Process.html +261 -0
- data/doc/Backup/Runner.html +244 -0
- data/doc/README_rdoc.html +202 -0
- data/doc/Rakefile.html +118 -0
- data/doc/TestCronEdit.html +211 -0
- data/doc/TestEnvironment.html +156 -0
- data/doc/TestFileBackup.html +237 -0
- data/doc/TestMySQLBackup.html +167 -0
- data/doc/TestOptions.html +156 -0
- data/doc/TestProcess.html +236 -0
- data/doc/created.rid +18 -0
- data/doc/images/add.png +0 -0
- data/doc/images/brick.png +0 -0
- data/doc/images/brick_link.png +0 -0
- data/doc/images/bug.png +0 -0
- data/doc/images/bullet_black.png +0 -0
- data/doc/images/bullet_toggle_minus.png +0 -0
- data/doc/images/bullet_toggle_plus.png +0 -0
- data/doc/images/date.png +0 -0
- data/doc/images/delete.png +0 -0
- data/doc/images/find.png +0 -0
- data/doc/images/loadingAnimation.gif +0 -0
- data/doc/images/macFFBgHack.png +0 -0
- data/doc/images/package.png +0 -0
- data/doc/images/page_green.png +0 -0
- data/doc/images/page_white_text.png +0 -0
- data/doc/images/page_white_width.png +0 -0
- data/doc/images/plugin.png +0 -0
- data/doc/images/ruby.png +0 -0
- data/doc/images/tag_blue.png +0 -0
- data/doc/images/tag_green.png +0 -0
- data/doc/images/transparent.png +0 -0
- data/doc/images/wrench.png +0 -0
- data/doc/images/wrench_orange.png +0 -0
- data/doc/images/zoom.png +0 -0
- data/doc/index.html +106 -0
- data/doc/js/darkfish.js +153 -0
- data/doc/js/jquery.js +18 -0
- data/doc/js/navigation.js +142 -0
- data/doc/js/search.js +94 -0
- data/doc/js/search_index.js +1 -0
- data/doc/js/searcher.js +228 -0
- data/doc/rdoc.css +543 -0
- data/doc/table_of_contents.html +148 -0
- data/lib/backup/cron_edit.rb +127 -0
- data/lib/backup/environment.rb +44 -0
- data/lib/backup/file_backup.rb +94 -0
- data/lib/backup/mysql_backup.rb +58 -0
- data/lib/backup/options.rb +199 -0
- data/lib/backup/process.rb +99 -0
- data/lib/backup/runner.rb +79 -0
- data/lib/backup_version.rb +9 -0
- data/syc-backup-0.0.1.gem +0 -0
- data/syc-backup-0.0.3.gem +0 -0
- data/sycbackup.gemspec +20 -0
- data/test/test_cron_edit.rb +49 -0
- data/test/test_environment.rb +22 -0
- data/test/test_file_backup.rb +70 -0
- data/test/test_mysql_backup.rb +71 -0
- data/test/test_options.rb +189 -0
- data/test/test_process.rb +40 -0
- metadata +123 -0
@@ -0,0 +1,199 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require_relative '../backup_version'
|
3
|
+
|
4
|
+
module Backup
|
5
|
+
|
6
|
+
# Parses the command line options the user has provided on the command line
|
7
|
+
class Options
|
8
|
+
# If the user doesn't provide a backup folder a default folder is used
|
9
|
+
DEFAULT_BACKUP_FOLDER = File.expand_path("~/backup/")
|
10
|
+
|
11
|
+
# Retrieve the database name that has to be backed up
|
12
|
+
attr_reader :database
|
13
|
+
# The user that is allowed to access the database
|
14
|
+
attr_reader :user
|
15
|
+
# The user's password to access the database
|
16
|
+
attr_reader :password
|
17
|
+
# The files to be backed up
|
18
|
+
attr_reader :files
|
19
|
+
# The backup directory where the files to be backed up
|
20
|
+
attr_reader :backup_folder
|
21
|
+
# Determines whether the backup folder to be overridden when already exists
|
22
|
+
attr_reader :override
|
23
|
+
# The cron schedule
|
24
|
+
attr_reader :cron
|
25
|
+
# Determines whether to compress the backup if not to compress it returns
|
26
|
+
# false, otherwise true
|
27
|
+
attr_reader :no_compress
|
28
|
+
|
29
|
+
# Takes the arguments from the command line and parses them
|
30
|
+
def initialize(argv)
|
31
|
+
@exit_code = 0
|
32
|
+
init_exit_messages
|
33
|
+
parse(argv)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Initializes the error messages belonging to the error codes
|
39
|
+
def init_exit_messages
|
40
|
+
@exit_message = {"1" => "Database missing",
|
41
|
+
"2" => "User missing",
|
42
|
+
"4" => "Password missing",
|
43
|
+
"8" => "Invalid cron data",
|
44
|
+
"16" => "Missing database or files"}
|
45
|
+
end
|
46
|
+
|
47
|
+
# Checks the values provided in the array c whether they are valid cron
|
48
|
+
# values representing one of the values minute, hour, day of month,
|
49
|
+
# month or day of week. Allowed values are
|
50
|
+
# minute 0..59, *
|
51
|
+
# hour 0..23, *
|
52
|
+
# day of month 1..31, *
|
53
|
+
# month 1..12, *
|
54
|
+
# day of week 1..7 , *
|
55
|
+
# If invalid values are detected bit 3 of exit_code is set and nil is
|
56
|
+
# returned. Otherwise a cron time string like "30 3 * * *" is returned.
|
57
|
+
def validate_cron_values(c)
|
58
|
+
cron_values = [0..59, 0..23, 1..31, 1..12, 1..7]
|
59
|
+
c.each.with_index do |v,i|
|
60
|
+
v.split(/,/).each do |s|
|
61
|
+
unless cron_values[i].cover?(s.to_i) or s == '*'
|
62
|
+
@exit_code |= 0b1000
|
63
|
+
return nil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
c.fill('*', c.size..4)
|
68
|
+
c.join(' ')
|
69
|
+
end
|
70
|
+
|
71
|
+
# Check if all requested arguments are provided. If arguments are missing a
|
72
|
+
# exit code not equal to 0 is set. Following exit codes are set
|
73
|
+
# Argument Exit code
|
74
|
+
# database 00001
|
75
|
+
# user 00010
|
76
|
+
# password 00100
|
77
|
+
# cron 01000
|
78
|
+
# files and database 10000
|
79
|
+
# If for instance the database and the user is missing the exit code will
|
80
|
+
# return 3. And can be caught with
|
81
|
+
# begin
|
82
|
+
# opts = Options.new(ARGV)
|
83
|
+
# rescue ExitStatus => e
|
84
|
+
# puts "database missing" if e.status == 1
|
85
|
+
# puts "user missing" if e.status == 2
|
86
|
+
# puts "password missing" if e.status == 4
|
87
|
+
# puts "cron invalid" if e.status == 8
|
88
|
+
# puts "ether database or files required" if e.status == 16
|
89
|
+
# end
|
90
|
+
def check_for_missing_arguments
|
91
|
+
if not @database and not @files
|
92
|
+
@exit_code |= 0b10000
|
93
|
+
elsif @database or @user or @password
|
94
|
+
@exit_code |= 0b00001 unless @database
|
95
|
+
@exit_code |= 0b00010 unless @user
|
96
|
+
@exit_code |= 0b00100 unless @password
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Initializes values as the backup folder with a default value if not
|
101
|
+
# provided by the user
|
102
|
+
def initialize_default_arguments_if_missing
|
103
|
+
@backup_folder = DEFAULT_BACKUP_FOLDER unless @backup_folder
|
104
|
+
unless @override
|
105
|
+
timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
|
106
|
+
if @backup_folder and File.exists?(@backup_folder)
|
107
|
+
@backup_folder = File.dirname(@backup_folder) + '/' +
|
108
|
+
File.basename(@backup_folder) + '_' + timestamp
|
109
|
+
end
|
110
|
+
end
|
111
|
+
@backup_folder = File.expand_path(@backup_folder)
|
112
|
+
@backup_folder += '/' unless @backup_folder.match(/.*\/\Z/)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Parses the user input and initializes the application
|
116
|
+
def parse(argv)
|
117
|
+
|
118
|
+
OptionParser.new do |opts|
|
119
|
+
app_name = File.basename($PROGRAM_NAME)
|
120
|
+
opts.banner = "Backup files and a database to a backup folder\n\n"+
|
121
|
+
"Usage: #{app_name} [options] [backup_folder]"
|
122
|
+
|
123
|
+
opts.on("-d", "--database DATABASE", "Database to backup") do |d|
|
124
|
+
@database = d
|
125
|
+
end
|
126
|
+
|
127
|
+
opts.on("-u", "--user USER", "User of the database") do |u|
|
128
|
+
@user = u
|
129
|
+
end
|
130
|
+
|
131
|
+
opts.on("-p", "--password PASSWORD",
|
132
|
+
"User's password to access the database") do |p|
|
133
|
+
@password = p
|
134
|
+
end
|
135
|
+
|
136
|
+
opts.on("-f", "--file f1,f2,f3", Array,
|
137
|
+
"A list of files to backup") do |f|
|
138
|
+
@files = f.map {|f| f.strip}
|
139
|
+
end
|
140
|
+
|
141
|
+
opts.on("--no-compress",
|
142
|
+
"Do not compress the backed up files",
|
143
|
+
"and database") do |n|
|
144
|
+
@no_compress = true
|
145
|
+
end
|
146
|
+
|
147
|
+
opts.on("--override", "Override the backup folder if it exists") do |o|
|
148
|
+
@override = true
|
149
|
+
end
|
150
|
+
|
151
|
+
opts.on("--cron 'm h dom m dow'", String,
|
152
|
+
"Create a cron job that automatically ",
|
153
|
+
"invokes #{app_name}",
|
154
|
+
"m = minute 0..59",
|
155
|
+
"h = hour 0..23",
|
156
|
+
"dom = day of month 1..31",
|
157
|
+
"m = month 1..12",
|
158
|
+
"dow = day of week 1..7",
|
159
|
+
"30 3 * * * will run the cron job at 3:30am") do |c|
|
160
|
+
@cron = validate_cron_values c.split(/ /).slice(0..4)
|
161
|
+
end
|
162
|
+
|
163
|
+
opts.on("-v", "--version", "Show version") do |v|
|
164
|
+
puts Backup::VERSION
|
165
|
+
exit 0
|
166
|
+
end
|
167
|
+
|
168
|
+
opts.on("-h", "--help", "Show this message") do |h|
|
169
|
+
puts opts
|
170
|
+
exit 0
|
171
|
+
end
|
172
|
+
|
173
|
+
begin
|
174
|
+
opts.parse!(argv)
|
175
|
+
rescue OptionParser::ParseError => e
|
176
|
+
STDERR.puts e.message, "\n", opts
|
177
|
+
exit(-1)
|
178
|
+
end
|
179
|
+
|
180
|
+
@backup_folder = argv.shift
|
181
|
+
|
182
|
+
check_for_missing_arguments
|
183
|
+
|
184
|
+
@exit_code.size.times do |i|
|
185
|
+
result = @exit_code & 0b00001 << i
|
186
|
+
STDERR.puts @exit_message[result.to_s] if result > 0
|
187
|
+
end
|
188
|
+
|
189
|
+
if @exit_code > 0
|
190
|
+
puts opts
|
191
|
+
exit(@exit_code)
|
192
|
+
end
|
193
|
+
|
194
|
+
initialize_default_arguments_if_missing
|
195
|
+
end
|
196
|
+
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'open3'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
# The module Backup provides functions to backup MySQL databases and files and
|
5
|
+
# folders. The backup will be compressed to a default or a specified backup
|
6
|
+
# folder. Rather than backing up the files a cron job can be scheduled
|
7
|
+
# invoking the provided command.
|
8
|
+
module Backup
|
9
|
+
|
10
|
+
# Conducts the backups of a MySQL database and files.
|
11
|
+
class Process
|
12
|
+
|
13
|
+
# Takes the backup_folder where the files are backed up to. If override is
|
14
|
+
# provided the files in the backup folder are overridden. no_compress will
|
15
|
+
# prevent compressing the backed up files and will just copy them to the
|
16
|
+
# provided backup folder
|
17
|
+
def initialize(backup_folder, files, override, no_compress)
|
18
|
+
@backup_folder = backup_folder
|
19
|
+
@files = files
|
20
|
+
@override = override
|
21
|
+
@no_compress = no_compress
|
22
|
+
end
|
23
|
+
|
24
|
+
# Creates the backup of the database and the files. If at least one of the
|
25
|
+
# provided files doesn't exist the application will print an error message
|
26
|
+
# with the inexistent files and terminates
|
27
|
+
def backup
|
28
|
+
inexistent_files = check_for_inexistent_files
|
29
|
+
unless inexistent_files.empty?
|
30
|
+
STDERR.puts "Cannot backup inexistent files"
|
31
|
+
STDERR.puts inexistent_files.join(" ")
|
32
|
+
exit 1
|
33
|
+
end
|
34
|
+
|
35
|
+
FileUtils.mkdir_p @backup_folder unless File.exists? @backup_folder
|
36
|
+
|
37
|
+
if @no_compress
|
38
|
+
copy_files
|
39
|
+
else
|
40
|
+
compress_files_and_copy
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# Checks if files to backup have been provided that don't exist. Returns the
|
48
|
+
# inexistent files
|
49
|
+
def check_for_inexistent_files
|
50
|
+
inexistent_files = []
|
51
|
+
@files.each do |file|
|
52
|
+
inexistent_files << file unless File.exists? file
|
53
|
+
end
|
54
|
+
|
55
|
+
inexistent_files
|
56
|
+
end
|
57
|
+
|
58
|
+
# Copies the files to the backup folder. Is only invoked if --no-compress
|
59
|
+
# is provided
|
60
|
+
def copy_files
|
61
|
+
@files.each do |file|
|
62
|
+
basename = File.basename file
|
63
|
+
FileUtils.cp file, @backup_folder + basename if File.file? file
|
64
|
+
FileUtils.cp_r file, @backup_folder + basename if File.directory? file
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Compresses the files and copies the compressed file to the backup folder.
|
69
|
+
# The compression is done with
|
70
|
+
#
|
71
|
+
# tar cfz backup_folder/YYYYmmdd-HHMMSS_syc-backup.tar.gz
|
72
|
+
#
|
73
|
+
# If an error occurs while compressing the error message of tar is
|
74
|
+
# displayed and the application exits. If the method runs without errors
|
75
|
+
# the tar file is returned
|
76
|
+
def compress_files_and_copy
|
77
|
+
timestamp = ""
|
78
|
+
unless @override
|
79
|
+
timestamp = Time.now.strftime("%Y%m%d-%H%M%S") + '_'
|
80
|
+
end
|
81
|
+
tar_file = @backup_folder + timestamp + "syc-backup.tar.gz"
|
82
|
+
tar_command = "tar cfz #{tar_file} #{@files.join(" ")}"
|
83
|
+
|
84
|
+
stdout, stderr, status = Open3.capture3(tar_command)
|
85
|
+
|
86
|
+
unless status.exitstatus == 0
|
87
|
+
STDERR.puts "There was a problem executing command"
|
88
|
+
STDERR.puts tar_command
|
89
|
+
STDERR.puts stderr
|
90
|
+
exit status.exitstatus
|
91
|
+
end
|
92
|
+
|
93
|
+
tar_file
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require_relative 'options'
|
2
|
+
require_relative 'mysql_backup'
|
3
|
+
require_relative 'cron_edit'
|
4
|
+
require_relative 'process'
|
5
|
+
require_relative 'environment'
|
6
|
+
|
7
|
+
module Backup
|
8
|
+
|
9
|
+
# Is invoked from the command line application and invokes the application's
|
10
|
+
# optons as provided by the user on the command line
|
11
|
+
class Runner
|
12
|
+
|
13
|
+
# Takes the command line options and parses them
|
14
|
+
def initialize(argv)
|
15
|
+
@options = Options.new(argv)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Operates on the options and invokes the respective functions
|
19
|
+
def run
|
20
|
+
if @options.cron
|
21
|
+
create_cron
|
22
|
+
else
|
23
|
+
create_backup
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# Creates a cron job based on the provided options and the schedule
|
30
|
+
# provided with the --cron flag
|
31
|
+
def create_cron
|
32
|
+
cron_edit = CronEdit.new
|
33
|
+
command = cron_edit.add_command create_cron_command, Environment.ruby
|
34
|
+
puts "--> added command to cron"
|
35
|
+
puts " #{command}"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Creates the cron command based on the options provided by the user.
|
39
|
+
def create_cron_command
|
40
|
+
command = @options.cron + " sycbackup #{@options.backup_folder}"
|
41
|
+
command += ' -d ' + @options.database +
|
42
|
+
' -u' + @options.user +
|
43
|
+
' -p' + @options.password if @options.database
|
44
|
+
command += ' -f ' + @options.files.join(',') if @options.files
|
45
|
+
command += ' --no-compress' if @options.no_compress
|
46
|
+
command += ' --override' if @options.override
|
47
|
+
|
48
|
+
command
|
49
|
+
end
|
50
|
+
|
51
|
+
# Backs up the files provided by the user on the command line. Provides a
|
52
|
+
# summary of the backed up files
|
53
|
+
def create_backup
|
54
|
+
files = []
|
55
|
+
if @options.database
|
56
|
+
db_backup = MySQLBackup.new(@options.database,
|
57
|
+
@options.user,
|
58
|
+
@options.password)
|
59
|
+
files << db_backup.backup
|
60
|
+
end
|
61
|
+
|
62
|
+
files << @options.files if @options.files
|
63
|
+
|
64
|
+
files.flatten!
|
65
|
+
|
66
|
+
process = Process.new(@options.backup_folder,
|
67
|
+
files,
|
68
|
+
@options.override,
|
69
|
+
@options.no_compress)
|
70
|
+
process.backup
|
71
|
+
puts "--> backed up files"
|
72
|
+
puts " #{files.join("\n ")}"
|
73
|
+
puts "--> to #{@options.backup_folder}"
|
74
|
+
|
75
|
+
File.delete files[0] if @options.database
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# Backup contains functions to backup a MySQL database and files and directories
|
2
|
+
# to a default or specified backup directory. Instead of instant backup the
|
3
|
+
# invoked command can be added to a crontab and invoked based on the provided
|
4
|
+
# schedule that is a parameter of the --cron option. The backed up files are
|
5
|
+
# per default compressed but this can be ommitted
|
6
|
+
module Backup
|
7
|
+
# Version of the application
|
8
|
+
VERSION = '0.0.4'
|
9
|
+
end
|
Binary file
|
Binary file
|
data/sycbackup.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
$LOAD_PATH.push File.expand_path("../lib", __FILE__)
|
2
|
+
require 'backup_version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "syc-backup"
|
6
|
+
s.summary = %q{Back up a database and files}
|
7
|
+
s.description = %q{Back up a database and files or schedule cron job
|
8
|
+
for backup}
|
9
|
+
s.requirements = ['No requirements']
|
10
|
+
s.version = Backup::VERSION
|
11
|
+
s.author = "Pierre Sugar"
|
12
|
+
s.email = "pierre@sugaryourcoffee.de"
|
13
|
+
s.homepage = "http://syc.dyndns.org/drupal"
|
14
|
+
s.platform = Gem::Platform::RUBY
|
15
|
+
s.required_ruby_version = '>=1.9'
|
16
|
+
s.files = Dir['**/**']
|
17
|
+
s.executables = ['sycbackup']
|
18
|
+
s.test_files = Dir['test/test*.rb']
|
19
|
+
s.has_rdoc = true
|
20
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'shoulda'
|
3
|
+
require_relative '../lib/backup/cron_edit.rb'
|
4
|
+
|
5
|
+
# Test for CronEdit will add commands to the users crontab. After each test
|
6
|
+
# the test entries are removed. So the crontab should not be changed after a
|
7
|
+
# test.
|
8
|
+
# In case of changes to CronEdit that will lead to a system exit it might leave
|
9
|
+
# the crontab with test data. In that case you have to remove the test data
|
10
|
+
# manually. But subsequent successfull runs should remove all test entries.
|
11
|
+
class TestCronEdit < Test::Unit::TestCase
|
12
|
+
|
13
|
+
context "Crontab operation" do
|
14
|
+
|
15
|
+
# Determines the count of the provided _command_ in the crontab.
|
16
|
+
def crontab_count(command)
|
17
|
+
content = `crontab -l`
|
18
|
+
count = 0
|
19
|
+
content.split(/\n/).each do |c|
|
20
|
+
count += 1 if c == command
|
21
|
+
end
|
22
|
+
count
|
23
|
+
end
|
24
|
+
|
25
|
+
# Adds a command to crontab and checks that it is entered. Removes command
|
26
|
+
# after test.
|
27
|
+
should "add command to crontab" do
|
28
|
+
ce = Backup::CronEdit.new
|
29
|
+
command = "1 2 * * * ls -l"
|
30
|
+
ce.add_command command
|
31
|
+
assert_equal 1, crontab_count(command)
|
32
|
+
ce.remove_command command
|
33
|
+
assert_equal 0, crontab_count(command)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Adds a command twice to crontab and expects that it is contained only
|
37
|
+
# once. Removes added command at end of test
|
38
|
+
should "not add duplicate command to crontab" do
|
39
|
+
ce = Backup::CronEdit.new
|
40
|
+
command = "2 2 2 * * syc-backup -d test -upierre -ppass"
|
41
|
+
ce.add_command command
|
42
|
+
assert_equal 1, crontab_count(command)
|
43
|
+
ce.add_command command
|
44
|
+
assert_equal 1, crontab_count(command)
|
45
|
+
ce.remove_command command
|
46
|
+
assert_equal 0, crontab_count(command)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|