dated_backup 0.1.0 → 0.2.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/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