backupgem 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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