dated_backup 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,186 @@
1
+
2
+ = Introduction
3
+
4
+ Dated Backup is a program which does exactly what it's name says:
5
+ It creates backups of any directory, timestamping the backups. It then
6
+ performs incremental backups on every subsequent run. The really nice thing here
7
+ is that those backups are fully viewable as snapshots, even though they are
8
+ also incremental.
9
+
10
+ This method of backup uses the hard-link technique in combination
11
+ with rsync. For more information on this technique, see:
12
+
13
+ http://www.mikerubel.org/computers/rsync_snapshots/
14
+
15
+ At the moment, this program can be thought of as a limited, Ruby version of
16
+ the popular unix utility rsnapshot. Dated Backup's feature set already does
17
+ things a little different from rsnapshot, and in the future will diverge widely.
18
+
19
+ Dated Backup no longer depends on GNU cp, but instead uses rsync's --link-dest
20
+ option to simulate the hard-link method.
21
+
22
+
23
+ == Backup Assumptions
24
+
25
+ * Your backup *server* is POSIX compliant (a modern day UNIX - Linux, *BSD, Mac OS X)
26
+ * You would like to perform incremental snapshots with timestamps
27
+
28
+ = Installation:
29
+
30
+ * sudo gem install 'datedbackup' --include-dependencies
31
+
32
+ == Dependencies
33
+
34
+ Dated Backup has the following dependencies:
35
+
36
+ * Ruby
37
+ * Rubygems
38
+ * Rails' 'ActiveSupport' gem
39
+ * A copy of rsync, which supports the --link-dest option.
40
+
41
+ Rsync is not required on the machine to be backed up - only on the machine which stores
42
+ the backup.
43
+
44
+
45
+ = HOWTO Backup with Dated Backup
46
+
47
+ Each backup source will correspond to a configuration file, which defines the source
48
+ directory (or remote location), and the local destination directory. These two
49
+ parameters are the only requirements for a backup configuration file to be valid.
50
+ The script can be run with the executable dbackup:
51
+
52
+ % dbackup my_script
53
+
54
+ Other scripts can be run sequentially by listing them in order:
55
+
56
+ % dbackup my_first_script my_second_script
57
+
58
+ All of the configuration occurs in the configuration file.
59
+
60
+
61
+ == The DSL, or How To Write A Configuration File
62
+
63
+ Here are the valid key words which can be set in the main section of the configuration file:
64
+
65
+ * source
66
+ * sources
67
+ * destination
68
+ * options
69
+ * user_domain
70
+
71
+ The values for these should be strings. They are specified like so:
72
+
73
+ source '/etc'
74
+
75
+ Multiple values can also be given:
76
+
77
+ sources '/etc', '/home'
78
+
79
+ The destination keyword only takes one value, but in the next release (0.3), this should
80
+ be fixed to allow backups to be copied to multiple locations.
81
+
82
+ The 'source' (or 'sources') keyword and the 'destination' keyword
83
+ are the only ones needed for a valid backup config file. The 'user_domain' keyword is used
84
+ when the source is not a local directory (or local file). The user_domain should be in user@server
85
+ style. As for the 'options' keyword, this should be specified as a string for extra options to be
86
+ feed into rsync. It too, is optional.
87
+
88
+
89
+ === Before and After Filters
90
+
91
+ It is very convenient, and often necessary to perform something before or after a backup script
92
+ runs. Any actions must be inside a 'before' or 'after' block:
93
+
94
+ before do
95
+ ...some before action here
96
+ end
97
+
98
+ after {
99
+ ..some other action here
100
+ }
101
+
102
+ You have the pick of do...end, or { ... }, thanks to Matz. No doubt, this will be familiar to any
103
+ Ruby programmer.
104
+
105
+ At the time of this release (0.2), there is only one valid action - 'remove_old'. Other actions, such
106
+ as running a script (or any number of scripts, in sequence), as well as running a command specified
107
+ in the configuration file itself, should be coming in subsequent releases.
108
+
109
+ For some example configuration files, see the examples bundled with RDoc, or in the example_configs
110
+ directory.
111
+
112
+
113
+ === remove_old
114
+
115
+ The remove_old block takes several different natural language time forms. All of the statements inside
116
+ a remove_old block must begin with 'keep'. An example would work best to illustrate how to use this:
117
+
118
+ after do
119
+ remove_old do
120
+ keep this months backups
121
+ keep last months backups
122
+ keep monthly backups
123
+ end
124
+ end
125
+
126
+ This says the following: After the backup runs, remove all backups that do not conform to the criteria
127
+ given. The first line will save all backups which have occured this month, regardless of what time
128
+ they occurred (as long as they occurred in the current month). The next line says the same, except for
129
+ last month's backups.
130
+
131
+ The last line, "keep monthly backups", will keep one backup from each month not already kept. If you perform
132
+ daily backups every day, this would end of keeping the last day's backup from every month (the 29, 30, or 31,
133
+ according to the month)
134
+
135
+ A few things should be notice here: These config files read very easily, so don't let them fool you.
136
+ They will delete your backups, forever lost. I've already burned myself with the following:
137
+
138
+ after {
139
+ remove_old {
140
+ keep this weeks backups
141
+ keep last weeks backups
142
+ keep weekly backups from this month # or: keep this months weekly backups
143
+ keep monthly backups # or: keep all monthly backups
144
+ }
145
+ }
146
+
147
+ After a month of backups, the month rolled over, and the next backup on the first of the month deleted
148
+ all backups, except the one just performed, and the one from the last day of the month before.
149
+ So beware, and think before you specify any remove_old block at all.
150
+
151
+ Another thing to notice (for any of you non-ruby programmers out there): The config file must be in valid
152
+ ruby, so specifying a "week's backups" should be written as a 'weeks backups',
153
+ i.e., don't put any apostrophes in there.
154
+
155
+ The config file shown above should give you some hints on the possibilities. Here are the valid keywords:
156
+
157
+ Time specifiers:
158
+ * this
159
+ * last
160
+ Time ranges (these can also be pluralized):
161
+ * day
162
+ * month
163
+ * week
164
+ * year
165
+ Incremental time slices (one per * methods):
166
+ * daily
167
+ * weekly
168
+ * monthly
169
+ * yearly
170
+ And some placeholders, which have no affect, but allow the file to read nicely:
171
+ * backup
172
+ * backups
173
+ * from
174
+ * all
175
+
176
+ Some other keywords, such as 'yesterday', and 'today' will be added in subsequent releases.
177
+
178
+ And finally, a final warning: If a remove_old block is given, but no keep rules are given, every backup
179
+ will be deleted! This may change in a subsequent release, but for now, beware!
180
+
181
+
182
+ = Contributions
183
+
184
+ If you are interested in contributing code or documentation, please contact me,
185
+ Scott Taylor, at scott AT railsnewbie DOT com.
186
+
data/RELEASES ADDED
@@ -0,0 +1,21 @@
1
+
2
+ = 0.2.0
3
+ =======
4
+
5
+ - A Script Runner/binary has been added (dbackup)
6
+ - A DSL has been added for configuring backups
7
+ - Removal of old backups can now be specified in the DSL, using
8
+ natural language forms
9
+ - Before + After actions for the script runner
10
+ - RSpec + RCov = Bug free code
11
+ - Documentation added in the README file
12
+ - Developer Documentation in Rake and RSpec Report
13
+ - A series of example config files have been attached for Documentation
14
+ - Root backup directories are now created automatically
15
+ - Removed dependency on GNU's cp, so now this utility can be used
16
+ on any *NIX, including Mac OS X
17
+
18
+ = 0.1.0
19
+ =======
20
+
21
+ - Initial Release
data/bin/dbackup ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # DatedBackup, A snapshot backup utility
4
+ # Copyright (C) Scott Taylor (scott@railsnewbie.com), 2007
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ #
19
+
20
+ require File.dirname(__FILE__) + "/../lib/dated_backup"
21
+
22
+ DatedBackup::ExecutionContext.new :main, *ARGV
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env ruby
2
-
3
1
  # A script which will copy the directories /etc and /home
4
2
  # into /var/backups/network/backups/example.com/#{date}. Note that
5
3
  # we wanted to get everything - even private keys owned by root
@@ -28,11 +26,7 @@
28
26
  #
29
27
 
30
28
 
31
- require "dated_backup"
32
-
33
- DatedBackup.new(
34
- :user_domain => "nbackup@example.com",
35
- :options => '-v -e "ssh -i /root/.ssh/rsync-key" --rsync-path="sudo rsync"',
36
- :sources => %w(/etc /home),
37
- :destination => "/var/backups/network/backups/example.com"
38
- ).run
29
+ user_domain "nbackup@example.com"
30
+ options "-v -e 'ssh -i /root/.ssh/rsync-key' --rsync-path='sudo rsync'"
31
+ sources "/etc", "/home"
32
+ destination "/var/backups/network/backups/example.com"
@@ -0,0 +1,10 @@
1
+ # A script to back up etc, locally, in /root/etc_backup
2
+
3
+ source '/etc'
4
+ destination '/root/etc_backup'
5
+
6
+ after {
7
+ remove_old {
8
+ keep monthly backups
9
+ }
10
+ }
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env ruby
2
-
3
1
  # A general purpose script to copy some WinXP/2000 shares
4
2
  # on a local network. The shares are the 'C' drives
5
3
  # of the various nodes on the network. They are mounted
@@ -16,23 +14,21 @@
16
14
  # there is no reason that it couldn't also be used for mounted
17
15
  # NFS drives, or even for a rudimentary Version Control
18
16
  # for any set of files (locally, or remotely)
19
-
20
- require "dated_backup"
21
17
 
22
- puts "* mounting the samba clients"
23
- %x(mount //teresa2/c)
24
- %x(mount //jay/c)
25
- %x(mount //claudio/c)
26
18
 
27
- DatedBackup.new(
28
- :source => "/mnt/shares",
29
- :destination => "/var/backups/network/backups/shares",
30
- :options => "-v"
31
- ).run
19
+ source '/mnt/shares'
20
+ destination '/var/backups/network/backups/shares'
21
+ options '-v'
32
22
 
33
- # umount the clients
34
- puts "* umounting samba clients"
35
- %x(umount //teresa2/c)
36
- %x(umount //jay/c)
37
- %x(umount //claudio/c)
23
+ after do
38
24
 
25
+ # CAREFUL! This will remove all old backups
26
+ # except the ones specified inside the block by the 'keep' rules
27
+ remove_old do
28
+ keep this weeks backups
29
+ keep last weeks backups
30
+ keep weekly backups from this month # or: keep this months weekly backups
31
+ keep monthly backups # or: keep all monthly backups
32
+ end
33
+
34
+ end
@@ -0,0 +1,43 @@
1
+
2
+
3
+ module DatedBackup
4
+ class Core
5
+ class BackupRemover
6
+ class << self
7
+
8
+ include DatedBackup::Core::CommandLine
9
+
10
+ def remove!(dir, keep_rules=[])
11
+ find_removable_sets(dir, keep_rules)
12
+
13
+ unless no_sets_to_remove?
14
+ execute("rm -rf #{to_remove.map{ |element| "#{element} " }.to_s.strip}")
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def find_removable_sets(dir, rules)
21
+ complete_set = BackupSet.find_files_in_directory dir
22
+ @to_remove = set_to_remove(complete_set, rules)
23
+ end
24
+
25
+ attr_reader :to_remove
26
+
27
+ def no_sets_to_remove?
28
+ to_remove.empty?
29
+ end
30
+
31
+ def set_to_remove(set, keep_rules)
32
+ if keep_rules.empty?
33
+ set
34
+ else
35
+ to_remove = set - set.filter_by_rule(keep_rules.car)
36
+ return set_to_remove(to_remove, keep_rules.cdr)
37
+ end
38
+ end
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,118 @@
1
+
2
+ module DatedBackup
3
+ class Core
4
+
5
+ class BackupSet < ReverseSortedUniqueArray
6
+
7
+ class << self
8
+ # Given the base of the backup directories as a string, this method should find all of the Backup Directories,
9
+ # and return these Directories as a BackupSet
10
+ def find_files_in_directory(dir)
11
+ raise InvalidDirectoryError, "A valid directory must be given." unless File.directory?(dir)
12
+ new(Dir.glob "#{dir}/*")
13
+ end
14
+
15
+ # Creates the boolean include_*? methods (include_month?, include_year? and so on).
16
+ # See the notes on the create_per_time_methods, and TimeSymbol.valid_symbols
17
+ def create_include_time_boolean_methods(*methods)
18
+ methods.each do |method|
19
+ define_method "include_#{method}?" do |time_value|
20
+ truth_value = false
21
+ self.each_as_time do |t|
22
+ truth_value = true if t.send(method) == time_value
23
+ end
24
+ truth_value
25
+ end
26
+ end
27
+ end
28
+
29
+ # Creates the one_per_* (one_per_month, one_per_year, one_per_week, and one_per_day)
30
+ # methods. Each of those methods will call the appropriate include_* methods,
31
+ # which is also dynamically defined
32
+ def create_one_per_time_methods(*methods)
33
+ methods.each do |method|
34
+ define_method "one_per_#{method}" do
35
+ set = BackupSet.new
36
+ reject_with_string_and_timestamp do |string, timestamp|
37
+ set.push string unless set.send("include_#{method}?", timestamp.send("#{method}"))
38
+ end
39
+ set
40
+ end
41
+ end
42
+ end
43
+
44
+ # Creates the many similar time methods:
45
+ #
46
+ # * include_year?
47
+ # * include_month?
48
+ # * include_day?
49
+ # * include_week
50
+ #
51
+ # * one_per_year
52
+ # * one_per_month
53
+ # * one_per_day
54
+ # * one_per_week
55
+ #
56
+ def create_dynamic_time_methods(time_array=[])
57
+ create_include_time_boolean_methods *time_array
58
+ create_one_per_time_methods *time_array
59
+ end
60
+ end
61
+
62
+ create_dynamic_time_methods TimeSymbol.valid_symbols
63
+
64
+ def filter_by_rule(rule)
65
+ obj = self.dup
66
+ obj = obj.filter_by_range(rule[:constraint]) if rule[:constraint]
67
+ obj = obj.filter_by_scope(rule[:scope]) if rule[:scope]
68
+ return obj
69
+ end
70
+
71
+ def filter_by_scope(scope)
72
+ case scope
73
+ when :yearly
74
+ one_per_year
75
+ when :monthly
76
+ one_per_month
77
+ when :weekly
78
+ one_per_week
79
+ when :daily
80
+ one_per_day
81
+ end
82
+ end
83
+
84
+ def filter_by_range(time_range)
85
+ reject_with_timestamp do |timestamp|
86
+ !(time_range.include? timestamp)
87
+ end
88
+ end
89
+
90
+ def reject_with_timestamp &blk
91
+ reject do |element|
92
+ yield element.to_time
93
+ end
94
+ end
95
+
96
+ def reject_with_string_and_timestamp &blk
97
+ reject do |element|
98
+ yield element, element.to_time
99
+ end
100
+ end
101
+
102
+ def each_as_time &blk
103
+ self.each do |obj|
104
+ yield obj.to_time
105
+ end
106
+ end
107
+
108
+ # BackupSet#- should return a new BackupSet,
109
+ # not an array
110
+ define_method "-" do |obj|
111
+ (self.to_a - obj.to_a).to_backup_set
112
+ end
113
+
114
+ end
115
+
116
+ end
117
+ end
118
+
@@ -0,0 +1,11 @@
1
+ module DatedBackup
2
+ class Core
3
+ module CommandLine
4
+ def execute(cmd, kernel_class=Kernel)
5
+ kernel_class.puts "* running: #{cmd}"
6
+ output = kernel_class.send :`, cmd
7
+ kernel_class.puts output
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,71 @@
1
+
2
+ module DatedBackup
3
+ class Core
4
+
5
+ BACKUP_REGEXP = /[12][0-9]{3}\-[01][0-9]\-[0-3][0-9]\-[0-2][0-9]h\-[0-6][0-9]m\-[0-6][0-9]s/
6
+
7
+ include DatedBackup::Core::CommandLine
8
+ include DatedBackup::Core::Tasks
9
+
10
+ attr_accessor :sources, :destination, :options, :backup_root, :user_domain
11
+ attr_reader :pre_run_commands, :kernel
12
+ attr_reader :before_run, :after_run
13
+
14
+ def initialize(procs={}, kernel=Kernel)
15
+ @kernel = kernel
16
+ @before_run = procs[:before] || Proc.new {}
17
+ @after_run = procs[:after] || Proc.new {}
18
+ end
19
+
20
+ def set_attributes(h={})
21
+ parse_command_options(h)
22
+ @destination = generate_backup_filename
23
+ if @user_domain
24
+ @sources.map! { |src| "#{@user_domain}:#{src}" }
25
+ end
26
+ end
27
+
28
+ def check_for_directory_errors
29
+ if sources.nil? || sources.empty?
30
+ raise DirectoryError, "No source directory given"
31
+ elsif backup_root.nil? || backup_root.empty?
32
+ raise DirectoryError, "No destination directory given"
33
+ end
34
+ end
35
+
36
+ # create the first backup, if non-existent
37
+ # otherwise cp -al (or # replace cp -al a b with cd a && find . -print | cpio -dpl ../b )
38
+ # and then create the backup of the dirs using rsync -a --delete
39
+ # the files, in the end, should be read only and undeletable
40
+ def run
41
+ DatedBackup::ExecutionContext.new :before, &@before_run
42
+ run_tasks
43
+ DatedBackup::ExecutionContext.new :after, &@after_run
44
+ end
45
+
46
+ private
47
+
48
+ def generate_backup_filename
49
+ timestamp = Time.now.strftime "%Y-%m-%d-%Hh-%Mm-%Ss"
50
+ "#{@backup_root}/#{timestamp}"
51
+ end
52
+
53
+ protected
54
+
55
+ def parse_command_options(h={})
56
+ @pre_run_commands = h[:pre_run_commands]
57
+ @pre_run_commands = h[:pre_run_command].to_a if h[:pre_run_command]
58
+
59
+ @backup_root = *h[:destination]
60
+ @options = h[:options] ? h[:options].map { |e| "#{e} "}.to_s.strip : ""
61
+
62
+ @user_domain = h[:user_domain]
63
+ @sources = h[:sources] || h[:source]
64
+ check_for_directory_errors
65
+ end
66
+
67
+ end
68
+
69
+ end
70
+
71
+
@@ -0,0 +1,44 @@
1
+
2
+ module DatedBackup
3
+ class Core
4
+ module Tasks
5
+
6
+ def run_tasks
7
+ create_main_backup_directory
8
+ create_backup
9
+ end
10
+
11
+ def create_backup
12
+ additional_options = has_backup? ? "--link-dest #{latest_backup} " : ""
13
+
14
+ sources.each do |source|
15
+ cmd = "rsync -a --delete #{additional_options}#{options} #{source} #{destination}"
16
+ execute cmd, kernel
17
+ kernel.puts "\n\n"
18
+ end
19
+ end
20
+
21
+ def create_main_backup_directory
22
+ unless File.exists? backup_root
23
+ kernel.puts "* Creating main backup directory #{backup_root}"
24
+ Dir.mkdir backup_root
25
+ end
26
+ end
27
+
28
+ def find_latest_backup
29
+ backup_directories.sort.reverse.first
30
+ end
31
+
32
+ alias :latest_backup :find_latest_backup
33
+
34
+ def backup_directories
35
+ Dir.glob("#{backup_root}/*").grep(BACKUP_REGEXP)
36
+ end
37
+
38
+ def has_backup?
39
+ backup_directories.empty? ? false : true
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,15 @@
1
+
2
+ module DatedBackup
3
+ module Warnings
4
+
5
+ def execute_silently(&blk)
6
+ old_warning_level = $VERBOSE
7
+ $VERBOSE = nil
8
+ yield
9
+ $VERBOSE = old_warning_level
10
+ end
11
+
12
+ module_function :execute_silently
13
+
14
+ end
15
+ end
@@ -0,0 +1,6 @@
1
+ require File.dirname(__FILE__) + "/core/warnings"
2
+ require File.dirname(__FILE__) + "/core/backup_set"
3
+ require File.dirname(__FILE__) + "/core/tasks"
4
+ require File.dirname(__FILE__) + "/core/command_line"
5
+ require File.dirname(__FILE__) + "/core/backup_remover"
6
+ require File.dirname(__FILE__) + "/core/dated_backup"
@@ -0,0 +1,56 @@
1
+
2
+ module DatedBackup
3
+ class ExecutionContext
4
+
5
+ def initialize(name, *params, &blk)
6
+ DatedBackup::Warnings.execute_silently do
7
+ if name == :main
8
+ params.each do |filename|
9
+ Main.load filename
10
+ end
11
+ elsif name == :before || name == :after
12
+ Around.new &blk
13
+ end
14
+ end
15
+ end
16
+
17
+ class Main
18
+ class << self
19
+ def load(filename)
20
+ klass = Class.new
21
+ klass.send(:include, DSL::Main)
22
+ instance = klass.new
23
+
24
+ File.open filename, "r" do |file|
25
+ instance.instance_eval file.read
26
+ end
27
+
28
+ @main_instance = DatedBackup::Core.new(instance.procs)
29
+ @main_instance.set_attributes(instance.hash)
30
+ @main_instance.run
31
+ end
32
+
33
+ attr_reader :main_instance
34
+ alias :core_instance :main_instance
35
+ alias :instance :main_instance
36
+ end
37
+ end
38
+
39
+ class Around
40
+ def initialize(around=self, &blk)
41
+ around.instance_eval &blk
42
+ end
43
+
44
+ def remove_old(&blk)
45
+ klass = Class.new
46
+ klass.send(:include, DSL::TimeExtensions)
47
+ instance = klass.new
48
+
49
+ instance.instance_eval &blk
50
+
51
+ Core::BackupRemover.remove!(Main.instance.backup_root, instance.kept)
52
+ end
53
+ end
54
+
55
+ end
56
+ end