backupgem 0.0.2

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/doc/styles.css ADDED
@@ -0,0 +1,157 @@
1
+ /* Layout */
2
+ html,body{margin:0;padding:0}
3
+ body{font: 100% arial,sans-serif}
4
+ p{margin:0 10px 10px}
5
+ a{color: #981793;}
6
+ div#header h1{height:80px;line-height:80px;margin:0;
7
+ padding-left:10px;background: #EEE;color: #79B30B}
8
+ div#content p{line-height:1.4}
9
+ div#navigation{background:#B9CAFF}
10
+ div#extra{background:#FF8539}
11
+ div#footer{background: #333;color: #FFF}
12
+ div#footer p{margin:0;padding:5px 10px}
13
+ div#wrapper{float:right;width:100%;margin-left:-205px}
14
+ div#content{margin-left:205px}
15
+
16
+ div#navigation {
17
+ float:left;
18
+ width:200px;
19
+ margin-right: 5px;
20
+ }
21
+
22
+ div#extra{float:left;clear:left;width:200px}
23
+ div#footer{clear:both;width:100%}
24
+
25
+
26
+ /* Styles */
27
+ body {
28
+ background-color: white;
29
+ color: #222;
30
+ font-family: Georgia, Times, serif;
31
+ line-height: 110%;
32
+ }
33
+
34
+ /* Code styling */
35
+ .example {
36
+ border: solid 1px #C9B;
37
+ background-color: #FFF5F9;
38
+ padding: 3px;
39
+ margin: 5px 40px 5px 22px;
40
+ }
41
+
42
+ pre {
43
+ background-color: #FFFEF9;
44
+ color: #936;
45
+ padding: 5px;
46
+ margin: 0px;
47
+ line-height: 170%;
48
+ }
49
+
50
+ blockquote code, p code, li code {
51
+ color: #936;
52
+ font-style: normal;
53
+ background-color: #FFFEF9;
54
+ padding: 2px;
55
+ line-height: 170%;
56
+ font-size: 12pt;
57
+ }
58
+
59
+ p code, li code {
60
+ line-height: 100%;
61
+ padding: 0px;
62
+ font-size: 130%;
63
+ }
64
+
65
+ blockquote code {
66
+ padding: 5px;
67
+ font-size: .9em;
68
+ }
69
+
70
+ div#content li {
71
+ margin-bottom: 5px;
72
+ }
73
+
74
+ div.longlist li {
75
+ padding-bottom: 8px;
76
+ }
77
+
78
+ .last_updated {
79
+ margin-left: 60%;
80
+ text-align: right;
81
+ font-size:80%;
82
+ }
83
+
84
+
85
+ /* syntax highlighting */
86
+ .keyword {
87
+ font-weight: bold;
88
+ }
89
+ .comment {
90
+ color: #555;
91
+ }
92
+ .string, .number {
93
+ color: #396;
94
+ }
95
+ .string {
96
+ background-color: #E9F5F5;
97
+ }
98
+ .regex {
99
+ background-color: #E9F1F5;
100
+ color: #435;
101
+ }
102
+ .ident {
103
+ color: #369;
104
+ }
105
+ .symbol {
106
+ color: #000;
107
+ }
108
+ .constant, .class {
109
+ color: #630;
110
+ font-weight: bold;
111
+ }
112
+
113
+ /* tables */
114
+ table {
115
+ margin: 1em 1em 1em 0;
116
+ background: #f9f9f9;
117
+ border: 1px #aaaaaa solid;
118
+ border-collapse: collapse;
119
+ }
120
+
121
+ th, td {
122
+ border: 1px #aaaaaa solid;
123
+ padding: 0.2em;
124
+ }
125
+
126
+ th {
127
+ background: #f2f2f2;
128
+ text-align: center;
129
+ }
130
+
131
+ /* navigation */
132
+ div#navigation a, div#extra a {
133
+ color: #08052B;
134
+ text-decoration: none;
135
+ }
136
+
137
+ div#navigation a:hover, div#extra a:hover {
138
+ text-decoration: underline;
139
+ }
140
+
141
+ div#navigation ol, div#extra ul {
142
+ margin-left: 6px;
143
+ padding-left: 20px;
144
+ }
145
+
146
+ div#navigation ol ol {
147
+ padding-left: 20px;
148
+ }
149
+
150
+ div#navigation ol ol li a, ol ol li {
151
+ color: #424E75;
152
+ font-size: 90%;
153
+ }
154
+
155
+ .rightad {
156
+ float: right;
157
+ }
@@ -0,0 +1,28 @@
1
+ #------------------------------------------------------------------------------
2
+ # Global Settings for Backup
3
+ # This file sets the global settings for all recipes in the same directory
4
+ # @author: Nate Murray <nate@natemurray.com>
5
+ #------------------------------------------------------------------------------
6
+
7
+ # This file specifies many, but not all, of the setting you can change for Backup.
8
+ # Any setting that is set to the default is commented out. Uncomment and
9
+ # change any to your liking.
10
+
11
+ # A single or array of servers to execute the backup tasks on
12
+ # set :servers, %w{ localhost someotherhost } # default: localhost
13
+
14
+ # The directory to place the backup on the backup server
15
+ set :backup_path, "/var/local/backups/mediawiki"
16
+
17
+ # Name of the SSH user
18
+ # set :ssh_user, ENV['USER']
19
+
20
+ # Path to your SSH key
21
+ # set :identity_key, ENV['HOME'] + "/.ssh/id_rsa"
22
+
23
+ # Set global actions
24
+ # action :compress, :method => :tar_bz2 # tar_bz2 is the default
25
+ # action :deliver, :method => :mv # mv is the default
26
+ # action :encrypt, :method => :gpg # gpg is the default
27
+
28
+ set :tmp_dir, File.dirname(__FILE__) + "/../tmp"
@@ -0,0 +1,24 @@
1
+ #------------------------------------------------------------------------------
2
+ # Mediawiki Backup script
3
+ # @author: Nate Murray <nate@natemurray.com>
4
+ #------------------------------------------------------------------------------
5
+ action(:content) do
6
+ dump = c[:tmp_dir] + "/test.sql"
7
+ sh "mysqldump -uroot test > #{dump}"
8
+ # should something happen here to cleanup the last task? maybe something
9
+ # should happen to the stack. like if the next task goes through then you
10
+ # remove he last file from the stack. Ah. if everything goes well you clean
11
+ # up everything from the stack.
12
+ dump
13
+ end
14
+
15
+ # action :compress, :method => :tar_bz2 # could be set in global
16
+ # action :delivery, :method => :scp # could be set in global
17
+
18
+ # settings for backup servers are global unless specified otherwise
19
+ # rotate settings are global unless specified herer
20
+
21
+
22
+
23
+
24
+
@@ -0,0 +1,196 @@
1
+ require 'rake'
2
+
3
+ module Backup
4
+ include FileUtils
5
+ # An Actor is the entity that actually does the work of determining which
6
+ # servers should be the target of a particular task, and of executing the
7
+ # task on each of them in parallel. An Actor is never instantiated
8
+ # directly--rather, you create a new Configuration instance, and access the
9
+ # new actor via Configuration#actor.
10
+ class Actor
11
+ # The configuration instance associated with this actor.
12
+ attr_reader :configuration
13
+
14
+ # Alias for #configuration
15
+ alias_method :c, :configuration
16
+
17
+ # A hash of the tasks known to this actor, keyed by name. The values are
18
+ # instances of Actor::Action.
19
+ attr_reader :action
20
+
21
+ # A stack of the results of the actions called
22
+ attr_reader :result_history
23
+
24
+ # the rotator instance
25
+ attr_reader :rotator
26
+
27
+ # Returns an Array of Strings specifying dirty files. These files will be
28
+ # +rm -rf+ when the +cleanup+ task is called.
29
+ attr_reader :dirty_files
30
+
31
+ class Action #:nodoc:
32
+ attr_reader :name, :actor, :options
33
+
34
+ def initialize(name, actor, options)
35
+ @name, @actor, @options = name, actor, options
36
+ end
37
+ end
38
+
39
+ def initialize(config) #:nodoc:
40
+ @configuration = config
41
+ @action = {}
42
+ @result_history = []
43
+ @dirty_files = []
44
+ @rotator = Backup::Rotator.new(self)
45
+ end
46
+
47
+ # each action in the action_order is part of the chain. so you start by
48
+ # setting the output as 'nil' then you try to call before_ action, then
49
+ # store the output, then cal action with the args if action takes the args
50
+ # you are sending. if it doesnt give an intelligent error message. do this
51
+ # for all actions. then call after_action with the output if it exists.
52
+ # each time out are calilng the method with the arguemtns f the method
53
+ # exists and the method takes the arguments.
54
+ def start_process!
55
+ configuration[:action_order].each do |a|
56
+ self.send_and_store("before_" + a)
57
+ self.send_and_store(a)
58
+ self.send_and_store("after_" + a)
59
+ end
60
+ last_result
61
+ end
62
+
63
+ def send_and_store(name)
64
+ store_result self.send(name) if self.respond_to? name
65
+ end
66
+
67
+ # Define a new task for this actor. The block will be invoked when this
68
+ # task is called.
69
+ # todo, this might be more complex if the before and after tasks are going
70
+ # to be part of the input and output chain
71
+ def define_action(name, options={}, &block)
72
+ @action[name] = (options[:action_class] || Action).new(name, self, options)
73
+
74
+ if self.respond_to?(name) && !block_given?
75
+ # if it was already defined and we aren't trying to re-define it then
76
+ # what we are trying to do is define it the same way it is defined now
77
+ # only with options being sent to it.
78
+ metaclass.send(:alias_method, "old_#{name}".intern, name)
79
+ #self.class.send(:alias_method, "old_#{name}".intern, name)
80
+ #define_method("#{name.to_s}_new".intern) do
81
+ define_method(name) do
82
+ begin
83
+ result = self.send("old_#{name}", options)
84
+ end
85
+ result
86
+ end
87
+ return
88
+ end
89
+
90
+ define_method(name) do
91
+ #logger.debug "executing task #{name}"
92
+ begin
93
+ if block_given?
94
+ result = instance_eval( &block )
95
+ elsif options[:method]
96
+ #result = self.send(options[:method], options[:args])
97
+ result = self.send(options[:method])
98
+ # here we need to have a thing where we can send the arguments
99
+ # define the method 'content' so that would take the other options
100
+ # if there are options (any hash) just send along that hash. this needs more work
101
+ end
102
+ end
103
+ result
104
+ end
105
+
106
+ end
107
+
108
+ def metaclass
109
+ class << self; self; end
110
+ end
111
+
112
+ # rotate Actions
113
+ def via_mv; rotator.rotate_via_mv(last_result); end
114
+ def via_ssh; rotator.rotate_via_ssh(last_result); end
115
+ def via_ftp; rotator.rotate_via_ftp(last_result); end
116
+
117
+ # By default, +:content+ can perform one of three actions
118
+ # * +:is_file+
119
+ # * +:is_folder+
120
+ # + +:is_contents_of+
121
+ #
122
+ # Examples:
123
+ # action :content, :is_file => "/path/to/file" # content is a single file
124
+ # action :content, :is_folder => "/path/to/folder" # content is the folder itself
125
+ # action :content, :is_contents_of => "/path/to/other/folder" # files in folder/
126
+ #
127
+ # +:is_file+ and +:is_folder+ are basically the same thing in that they
128
+ # backup the whole file/folder whereas +:is_contents_of+ backs up the
129
+ # <em>contents</em> of the given folder.
130
+ #
131
+ # Note that +:is_contents_of+ performs a very spcific action:
132
+ # * a temporary directory is created
133
+ # * all of the files are moved (including subdirectories) into the temporary directory
134
+ # * the archive is created from the temporary directory
135
+ #
136
+ # If you wish to copy the files out of the original directory instead of
137
+ # moving them. Then you may specify the +copy+ option passing a +true+
138
+ # value like so:
139
+ #
140
+ # action :content, :is_contents_of => "/path/to/other/folder",
141
+ # :copy => true
142
+ #
143
+ # This will copy recursively.
144
+ #
145
+ # If this is not your desired behavior then you can easily write your own.
146
+ # Also, these options only work for local files. If you are getting the
147
+ # files from a foreign server you will have to write a custom +:content+
148
+ # method.
149
+ #
150
+ def content(opts={})
151
+ return opts[:is_file] if opts[:is_file]
152
+ return opts[:is_folder] if opts[:is_folder]
153
+ if opts[:is_contents_of]
154
+ orig = opts[:is_contents_of]
155
+ tmpdir = c[:tmp_dir] + "/tmp_" + Time.now.strftime("%Y%m%d%H%M%S") +"_#{rand}"
156
+ new_orig = tmpdir + "/" + File.basename(orig)
157
+ sh "mkdir #{tmpdir}"
158
+ sh "mkdir #{new_orig}"
159
+ cmd = opts[:copy] ? "cp -r" : "mv"
160
+ sh "#{cmd} #{orig}/* #{new_orig}/"
161
+ dirty_file new_orig
162
+ return new_orig
163
+ end
164
+ raise "Unknown option in :content. Try :is_file, :is_folder " +
165
+ "or :is_contents_of"
166
+ end
167
+
168
+ # Given name of a file in +string+ adds that file to @dirty_files. These
169
+ # files will be removed when the +cleanup+ task is called.
170
+ def dirty_file(string)
171
+ @dirty_files << string
172
+ end
173
+
174
+ # +cleanup+ takes every element from @dirty_files and performs an +rm -rf+ on the value
175
+ def cleanup(opts={})
176
+ dirty_files.each do |f|
177
+ sh "rm -rf #{f}"
178
+ end
179
+ end
180
+
181
+ private
182
+ def define_method(name, &block)
183
+ metaclass.send(:define_method, name, &block)
184
+ end
185
+
186
+ def store_result(result)
187
+ @result_history.push result
188
+ end
189
+
190
+ def last_result
191
+ @result_history.last
192
+ end
193
+
194
+ end
195
+
196
+ end
data/lib/backup/cli.rb ADDED
@@ -0,0 +1,105 @@
1
+ require 'optparse'
2
+ require 'backup'
3
+
4
+ module Backup
5
+ # The CLI class encapsulates the behavior of backup when it is invoked
6
+ # as a command-line utility.
7
+ class CLI
8
+ # Invoke capistrano using the ARGV array as the option parameters.
9
+ def self.execute!
10
+ new.execute!
11
+ end
12
+
13
+ # The array of (unparsed) command-line options
14
+ attr_reader :args
15
+
16
+ # The hash of (parsed) command-line options
17
+ attr_reader :options
18
+
19
+ # Docs for creating a new instance go here
20
+ def initialize(args = ARGV)
21
+ @args = args
22
+ @options = { :recipes => [], :actions => [],
23
+ :vars => {}, # :pre_vars => {},
24
+ :global => nil }
25
+
26
+ OptionParser.new do |opts|
27
+ opts.banner = "Usage: #{$0} [options] [args]"
28
+
29
+ opts.separator ""
30
+ opts.separator "Recipe Options -----------------------"
31
+ opts.separator ""
32
+
33
+ opts.on("-r", "--recipe RECIPE",
34
+ "A recipe file to load. Multiple recipes may",
35
+ "be specified, and are loaded in the given order."
36
+ ) { |value| @options[:recipes] << value }
37
+
38
+ opts.on("-s", "--set NAME=VALUE",
39
+ "Specify a variable and it's value to set. This",
40
+ "will be set after loading all recipe files."
41
+ ) do |pair|
42
+ name, value = pair.split(/=/, 2)
43
+ @options[:vars][name.to_sym] = value
44
+ end
45
+
46
+ opts.on("-g", "--global RECIPE",
47
+ "Specify a specific file to load as the global file",
48
+ "for the recipes. By default the recipes load the",
49
+ "file +global.rb+ in the same directory."
50
+ ) { |value| @options[:recipes] << value }
51
+
52
+ if args.empty?
53
+ puts opts
54
+ exit
55
+ else
56
+ opts.parse!(args)
57
+ end
58
+
59
+ check_options!
60
+
61
+ end
62
+
63
+ end
64
+
65
+ # Begin running Backup based on the configured options.
66
+ def execute!
67
+ #if !@options[:recipes].empty? # put backk
68
+ execute_recipes!
69
+ # elsif @options[:apply_to]
70
+ # execute_apply_to!
71
+ #end
72
+ end
73
+
74
+
75
+ private
76
+ def check_options!
77
+ # performa sanity check
78
+ end
79
+
80
+ # Load the recipes specified by the options, and execute the actions
81
+ # specified.
82
+ def execute_recipes!
83
+ config = Backup::Configuration.new
84
+ #config.logger.level = options[:verbose]
85
+ #options[:pre_vars].each { |name, value| config.set(name, value) }
86
+ options[:vars].each { |name, value| config.set(name, value) }
87
+
88
+ # load the standard recipe definition
89
+ config.load "standard"
90
+ options[:recipes].each do |recipe|
91
+ global = options[:global] || File.dirname(recipe) + "/global.rb"
92
+ config.load global if File.exists? global # cache this?
93
+ end
94
+ options[:recipes].each { |recipe| config.load(recipe) }
95
+ #options[:vars].each { |name, value| config.set(name, value) }
96
+
97
+ actor = config.actor
98
+ actor.start_process! # eventually make more options, like the ability
99
+ # to run each action individually
100
+ #options[:actions].each { |action| actor.send action }
101
+ end
102
+
103
+ end
104
+ end
105
+
@@ -0,0 +1,138 @@
1
+ require 'backup/actor'
2
+ require 'backup/rotator'
3
+
4
+ module Backup
5
+ # Represents a specific Backup configuration. A Configuration instance
6
+ # may be used to load multiple recipe files, define and describe tasks,
7
+ # define roles, create an actor, and set configuration variables.
8
+ class Configuration
9
+ # The actor created for this configuration instance.
10
+ attr_reader :actor
11
+
12
+ # The logger instance defined for this configuration.
13
+ attr_reader :logger
14
+
15
+ # The load paths used for locating recipe files.
16
+ attr_reader :load_paths
17
+
18
+ # The hash of variables currently known by the configuration
19
+ attr_reader :variables
20
+
21
+ def initialize(actor_class=Actor) #:nodoc:
22
+ @actor = actor_class.new(self)
23
+ #@logger = Logger.new
24
+ @load_paths = [".", File.join(File.dirname(__FILE__), "recipes")]
25
+ @variables = {}
26
+ end
27
+
28
+ # Set a variable to the given value.
29
+ def set(variable, value=nil, &block)
30
+ # if the variable is uppercase, then we add it as a constant to the
31
+ # actor. This is to allow uppercase "variables" to be set and referenced
32
+ # in recipes.
33
+ if variable.to_s[0].between?(?A, ?Z)
34
+ klass = @actor.metaclass
35
+ klass.send(:remove_const, variable) if klass.const_defined?(variable)
36
+ klass.const_set(variable, value)
37
+ end
38
+
39
+ value = block if value.nil? && block_given?
40
+ @variables[variable] = value
41
+ end
42
+
43
+ alias :[]= :set
44
+
45
+ def [](variable)
46
+ # TODO have it raise if it doesn exist
47
+ @variables[variable]
48
+ end
49
+
50
+ # Require another file. This is identical to the standard require method,
51
+ # with the exception that it sets the reciever as the "current" configuration
52
+ # so that third-party task bundles can include themselves relative to
53
+ # that configuration.
54
+ def require(*args) #:nodoc:
55
+ original, Backup.configuration = Backup.configuration, self
56
+ super
57
+ ensure
58
+ # restore the original, so that require's can be nested
59
+ Backup.configuration = original
60
+ end
61
+
62
+ # Disclaimer: This method written by Jamis Buck. Taken directly from his
63
+ # excellent code Capistrano.
64
+ #
65
+ # Load a configuration file or string into this configuration.
66
+ #
67
+ # Usage:
68
+ #
69
+ # load("recipe"):
70
+ # Look for and load the contents of 'recipe.rb' into this
71
+ # configuration.
72
+ #
73
+ # load(:file => "recipe"):
74
+ # same as above
75
+ #
76
+ # load(:string => "set :scm, :subversion"):
77
+ # Load the given string as a configuration specification.
78
+ #
79
+ # load { ... }
80
+ # Load the block in the context of the configuration.
81
+ def load(*args, &block)
82
+ options = args.last.is_a?(Hash) ? args.pop : {}
83
+ args.each { |arg| load options.merge(:file => arg) }
84
+ return unless args.empty?
85
+
86
+ if block
87
+ raise "loading a block requires 0 parameters" unless args.empty?
88
+ load(options.merge(:proc => block))
89
+
90
+ elsif options[:file]
91
+ file = options[:file]
92
+ unless file[0] == ?/
93
+ load_paths.each do |path|
94
+ if File.file?(File.join(path, file))
95
+ file = File.join(path, file)
96
+ break
97
+ elsif File.file?(File.join(path, file) + ".rb")
98
+ file = File.join(path, file + ".rb")
99
+ break
100
+ end
101
+ end
102
+ end
103
+ load :string => File.read(file), :name => options[:name] || file
104
+
105
+ elsif options[:string]
106
+ #logger.trace "loading configuration #{options[:name] || "<eval>"}"
107
+ instance_eval(options[:string], options[:name] || "<eval>")
108
+
109
+ elsif options[:proc]
110
+ #logger.trace "loading configuration #{options[:proc].inspect}"
111
+ instance_eval(&options[:proc])
112
+
113
+ else
114
+ raise ArgumentError, "don't know how to load #{options.inspect}"
115
+ end
116
+ end
117
+
118
+ # Describe the next task to be defined. The given text will be attached to
119
+ # the next task that is defined and used as its description.
120
+ def desc(text)
121
+ @next_description = text
122
+ end
123
+
124
+ # Define a new task. If a description is active (see #desc), it is added to
125
+ # the options under the <tt>:desc</tt> key. This method ultimately
126
+ # delegates to Actor#define_task.
127
+ def action(name, options={}, &block)
128
+ # raise ArgumentError, "expected a block or method" unless block or options[:method] # ??
129
+ if @next_description
130
+ options = options.merge(:desc => @next_description)
131
+ @next_description = nil
132
+ end
133
+
134
+ actor.define_action(name, options, &block)
135
+ end
136
+
137
+ end
138
+ end
@@ -0,0 +1,37 @@
1
+ module Backup
2
+ class DateParser
3
+
4
+ def self.date_from(what)
5
+ DateParser.new.date_from(what)
6
+ end
7
+
8
+ # the test is going to be whatever is returned here .include? the day of
9
+ # today. so if we want to do something every day than this needs to return
10
+ # something that will lincde the righ daY:W
11
+ def date_from(what)
12
+ if what.kind_of?(Symbol)
13
+ return Runt::DIWeek.new( Time.num_from_day(what) ) if day_of_week?(what)
14
+ return Runt::REDay.new(0,0,24,01) if what == :daily
15
+ if what.to_s =~ /^last_/
16
+ what.to_s =~ /^last_(\w+)_of_the_month$/
17
+ day = $1
18
+ return Runt::DIMonth.new(Runt::Last, Time.num_from_day(day.intern))
19
+ end
20
+ end
21
+ raise "#{what} is not a valid time" unless what.respond_to?(:include?)
22
+ what
23
+ end
24
+
25
+ private
26
+ def day_of_week?(word)
27
+ days_of_week.include?(word.to_s.downcase[0..2])
28
+ end
29
+
30
+ def days_of_week
31
+ %w{mon tue wed thu fri sat sun}
32
+ end
33
+
34
+ end
35
+ end
36
+
37
+
@@ -0,0 +1,17 @@
1
+ class Time
2
+ def days_since_epoch
3
+ self.to_i / 60 / 60 / 24
4
+ end
5
+
6
+ def self.num_from_day(day)
7
+ days = { :sun => 0,
8
+ :mon => 1,
9
+ :tue => 2,
10
+ :wed => 3,
11
+ :thu => 4,
12
+ :fri => 5,
13
+ :sat => 6}
14
+ days[day]
15
+ end
16
+
17
+ end