syc-backup 0.0.4
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/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
|