smarbs 0.9.3

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/lib/helpers.rb ADDED
@@ -0,0 +1,438 @@
1
+ require 'fileutils'
2
+ require 'types'
3
+ require 'smarbs'
4
+
5
+ class ConfigString
6
+ # Takes as args the arguments to the specific type (like positive), as desc
7
+ # the description of the value, and as defaults an Array or a single default
8
+ # value that is shown when no other value is added.
9
+ def initialize(name, args, desc, defaults)
10
+ @list = false
11
+ @positive = false
12
+ args.split(",").each{|arg| eval("@#{arg} = true") unless arg.empty?}
13
+ @desc = desc
14
+ @defaults = defaults
15
+ @defaults = "" if @defaults.empty?
16
+ @name = name
17
+ clear
18
+ value
19
+ end
20
+
21
+ # Returns the value, as an Array if list is false, as a single value
22
+ # otherwise. If the value is empty, return the default value(s).
23
+ def value
24
+ val = nil
25
+ if @value.empty? then
26
+ set(@defaults)
27
+ val = @value
28
+ clear
29
+ else
30
+ val = @value
31
+ end
32
+ if @list then
33
+ val
34
+ else
35
+ val[0]
36
+ end
37
+ end
38
+
39
+ # Returns the name of this option.
40
+ def name
41
+ @name
42
+ end
43
+
44
+ # Prints out the configfile-text with description.
45
+ def to_s
46
+ if @list then
47
+ ([@desc] + value.collect{|value| format(value)}).join("\n" + name + " ")
48
+ else
49
+ @desc + "\n" + name + " " + format(value)
50
+ end
51
+ end
52
+
53
+ # Adds the value(s) to @value (can be an Array). Doesn't strip!
54
+ def add(value)
55
+ if value.class == Array then
56
+ value.each{ |v| addvalue(v) }
57
+ else
58
+ addvalue(value)
59
+ end
60
+ end
61
+
62
+ # Sets the value as value of the object (can be an Array).
63
+ def set(value)
64
+ clear
65
+ add(value)
66
+ end
67
+
68
+ def clear
69
+ @value = []
70
+ end
71
+
72
+ private
73
+
74
+
75
+ # Adds the single string-value to @value
76
+ def addvalue(string)
77
+ raise "Only one '#{name}' allowed!" unless (@value.size == 0 or @list)
78
+ @value << check(string) unless @value.include?(check(string))
79
+ end
80
+
81
+ def check(string)
82
+ string.to_s
83
+ end
84
+
85
+ def format(value)
86
+ value.to_s
87
+ end
88
+ end
89
+
90
+ class ConfigDirectory < ConfigString
91
+ def check(string)
92
+ string.strip!
93
+ string.chop! if string[-1, 1] == "/" and string.size > 1
94
+ string
95
+ end
96
+ end
97
+
98
+ class ConfigSize < ConfigString
99
+ def check(value)
100
+ if value.class == Size then
101
+ value
102
+ else
103
+ Size.new(value)
104
+ end
105
+ end
106
+
107
+ def format(value)
108
+ value.short
109
+ end
110
+ end
111
+
112
+ class ConfigBoolean < ConfigString
113
+ def check(value)
114
+ if value == true or value == false then
115
+ value
116
+ else
117
+ value.downcase == "yes"
118
+ end
119
+ end
120
+
121
+ def format(value)
122
+ if value then "yes" else "no" end
123
+ end
124
+ end
125
+
126
+ class ConfigInteger < ConfigString
127
+ def check(value)
128
+ raise "Format #{value} not valid for an integer in '#{name}'!" unless value.to_i.to_s == value if value.class == String
129
+ value = value.to_i
130
+ raise "'#{name}' must be positive, you set it to #{value}!" unless value >= 0 if @positive
131
+ value
132
+ end
133
+ end
134
+
135
+ class ConfigExclude < ConfigString
136
+ def initialize(name, args, desc, defaults)
137
+ super(name, args + ",list", desc, defaults)
138
+ end
139
+ end
140
+
141
+ class ConfigTemplate
142
+
143
+ # Initializes a new ConfigTemplate and creates the internal structures needed.
144
+ # To read the actual data use read_template.
145
+ def initialize
146
+ @hash = Hash.new
147
+ @order = Array.new
148
+ end
149
+
150
+ # Returns everything within this ConfigTemplate as a parseable configfile
151
+ # text.
152
+ def to_s
153
+ out = ""
154
+ @order.each do |section|
155
+ out += preference_text(section[0]) + "\n\n"
156
+ section[1..-1].each do |option|
157
+ out += @hash[option].to_s + "\n\n"
158
+ end
159
+ end
160
+ out
161
+ end
162
+
163
+ def clear
164
+ @order.each do |section|
165
+ section[1..-1].each do |option|
166
+ @hash[option].clear
167
+ end
168
+ end
169
+ end
170
+
171
+ # Reads the template file at the given path and
172
+ def read_template(path)
173
+ sections = nil
174
+ File.open(path, "r") do |file|
175
+ sections = file.readlines
176
+ sections.each { |line| line.rstrip! }
177
+ sections = sections.join("\n").split("\n\n")
178
+ end
179
+
180
+ sections.each do |section|
181
+ parse_section(section)
182
+ end
183
+ end
184
+
185
+ # Parses the string (or Array of strings) as if it was part of a configfile,
186
+ # and *adds* the value accordingly, if it is not already there.
187
+ def parse(string)
188
+ if string.class == Array then
189
+ string
190
+ else
191
+ string.split("\n")
192
+ end.each do |line|
193
+ if line =~ /^(\w)+ +.+/
194
+ line = line.strip.split(" ", 2)
195
+ raise "No option '#{line[0]}' existing!" if @hash[line[0]].nil?
196
+ @hash[line[0]].add(line[1])
197
+ end
198
+ end
199
+ end
200
+
201
+ def preference_text(string)
202
+ i = (80 - string.length)/2
203
+ "-"*i + string + "-"*(80-i-string.length)
204
+ end
205
+
206
+ def parse_section(section)
207
+ if section[0,1] == "=" then
208
+ @order << [section[1..-2]]
209
+ return
210
+ end
211
+ lines = section.split("\n")
212
+ first = lines.shift.split(": ")
213
+ name = first.shift
214
+ raise InternalError, "Order still empty!" if @order.empty?
215
+ @order.last << name
216
+
217
+ classname = /^[^(]*/.match(first[0]).to_s
218
+ arguments = /\(.*\)/.match(first[0]).to_s
219
+ arguments = arguments[1..-2] unless arguments.nil?
220
+
221
+ desc = []
222
+ defaults = []
223
+ lines.each do |line|
224
+ if line =~ /^#{name} */ then
225
+ defaults << line[(name.length+1)..-1] unless line[(name.length+1)..-1].nil?
226
+ else
227
+ desc << line
228
+ end
229
+ end
230
+ @hash[name] = eval("Config#{classname}").new(name, arguments.to_s, desc.join("\n"), defaults)
231
+ end
232
+
233
+ private
234
+
235
+ def method_missing(name_orig, *args)
236
+ name = name_orig.to_s
237
+ if @hash.has_key?(name)
238
+ @hash[name].value
239
+ elsif @hash.has_key?(name[0..-2]) and name[-1] == "="[0]
240
+ @hash[name[0..-2]].set(args.first)
241
+ else
242
+ super
243
+ end
244
+ end
245
+ end
246
+
247
+ class ConfigFile
248
+
249
+ attr_reader :new
250
+
251
+ # Path should be a String. Creates or opens a new configfile and immediately
252
+ # writes it back again, to add new features etc (just in case).
253
+ def initialize(path)
254
+ @path = path
255
+ @template = ConfigTemplate.new
256
+ @template.read_template(SMARBS::DATADIR + "/configuration.txt")
257
+
258
+ if File.file?(@path)
259
+ raise @path + " is not writable!" if not File.writable?(@path)
260
+ read
261
+ write
262
+ else
263
+ if Directory.writable?(File.dirname(@path)) # Create example file
264
+ FileUtils.makedirs(File.dirname(@path)) # Create directory
265
+ write
266
+ puts "First run:\nConfig file created, edit " + @path + "!"
267
+ @new = true
268
+ else
269
+ raise(File.dirname(@path) + " is not writable!")
270
+ end
271
+ end
272
+ end
273
+
274
+ # Returns the filename (name only!) of the configfile.
275
+ def filename
276
+ return File.basename(@path)
277
+ end
278
+
279
+ # Puts the Configfile-text into the given file, with all the options as-is.
280
+ def write
281
+ file=File.open(@path, "w")
282
+ file.puts intro
283
+ file.puts @template
284
+ file.puts outro
285
+ file.close
286
+ end
287
+
288
+ def intro
289
+ txt = nil
290
+ File.open(SMARBS::DATADIR + "/intro.txt", "r") do |file|
291
+ txt = file.readlines
292
+ end
293
+ txt[0] = txt[0].strip + " #{SMARBS::VERSION} (#{SMARBS::DATE})\n"
294
+ txt.join
295
+ end
296
+
297
+ def outro
298
+ txt = nil
299
+ File.open(SMARBS::DATADIR + "/outro.txt", "r") do |file|
300
+ txt = file.readlines
301
+ end
302
+ txt.join
303
+ end
304
+
305
+ def read
306
+ File.open(@path, "r") do |file|
307
+ lines = file.readlines
308
+ index1 = lines.index{|string| string.include? "-preferences-"}
309
+ index2 = lines.index{|string| string.include? "-end of preferences-"}
310
+
311
+ raise "Configfile #{@path} is invalid. Please delete it!" if index1 == nil or index2 == nil
312
+ @template.clear
313
+ @template.parse(lines[index1..index2])
314
+ end
315
+ end
316
+
317
+ private
318
+
319
+ def method_missing(name, *args)
320
+ eval "@template.#{name}(args[0])"
321
+ end
322
+ end
323
+
324
+ # Specifies a directory that contains smarbs backups.
325
+ class SmarbsBackupDir < Directory
326
+ # Creates or reads out a SmarbsBackupDir, testing if it has valid contents.
327
+ def initialize(directory, ignore_leftovers=false)
328
+ super(directory, :writable)
329
+
330
+ return if files.size == 0 # Return if the directory is empty.
331
+ list = dirlist
332
+ numbers = list.collect {|x| x[0]}
333
+ raise "Two directories with same 'number' detected in " + @dir + "!" if numbers.uniq != numbers
334
+ raise "Directory with negative 'number' detected in #{@dir}!" if numbers[0] < 0
335
+ if (not ignore_leftovers) && directory_left
336
+ raise(NoConfigException, "\nAborted backup directory '#{directory_left}' left in #{@dir}\nMove or delete it to continue!")
337
+ end
338
+ end
339
+
340
+ public
341
+
342
+ # Returns the name of the directory (like 12-34-56-backup.0) when a
343
+ # '...backup.0' directory is left. Otherwise returns false.
344
+ def directory_left
345
+ list = dirlist
346
+ if list[0][0] == 0
347
+ return (list[0][1]).to_s+"-backup.0"
348
+ else
349
+ return false
350
+ end
351
+ end
352
+
353
+ # Returns a sorted array of arrays of the splitted directories within the smarbsbackupdir
354
+ # of the form [7, 12-34-56] (from "12-34-56-backup.7").
355
+ #
356
+ # Raises an error, if some directories are "malformed", respectively not in the form <date>-backup.<positive_number>.
357
+ def dirlist
358
+ list=files
359
+ for i in 0...list.size
360
+ raise "File '#{list[i]}' left in the destination directory #{@dir}!" if not File.directory?(@dir+"/"+list[i]) # No files are allowed.
361
+
362
+ tmp = list[i].split("-backup.")
363
+ raise @dir + list[i] + " has wrong format!" if tmp.size != 2
364
+ raise @dir + list[i] + " has wrong format in number!" if tmp[1].to_i.to_s != tmp[1]
365
+
366
+ list[i]=[tmp[1].to_i, tmp[0]] # Converts "12-34-56-backup.7" to an Array entry [7, 12-34-56].
367
+ end
368
+ list.sort!
369
+ list.compact!
370
+ return list
371
+ end
372
+
373
+ # Deletes the directory differing most from the "1, 2, 4, 8, 16, ..." scheme.
374
+ # If no such is found, deletes the last of all the directories. Also logs what
375
+ # is happening.
376
+ #
377
+ # Raises an error if the next deletion would go below the 'minbackuplevels'.
378
+ def delete(logger, minbackuplevels, oldestfirst = false)
379
+ list=dirlist
380
+ deleted=false
381
+
382
+ unless list.size > minbackuplevels or minbackuplevels <= 1
383
+ raise "Not enough space available, next deletion would go below your minbackuplevels of #{minbackuplevels}!"
384
+ end
385
+
386
+ unless list.size > 1
387
+ raise "Not enough space available to do an incremental backup!"
388
+ end
389
+
390
+ if oldestfirst then
391
+ a = dirlist[-1][1]
392
+ b = dirlist[-1][0]
393
+ else
394
+ for i in 0...(dirlist.size-1)
395
+ if (dirlist[i+1][0]-(2**i)).abs < (dirlist[i][0]-(2**i)).abs && dirlist[i][0] != 0 #Smarbs CORE, the secret formula :)
396
+ deleted=true
397
+ a = dirlist[i][1]
398
+ b = dirlist[i][0]
399
+ break
400
+ end
401
+ end
402
+
403
+ if not deleted
404
+ a = dirlist[-1][1]
405
+ b = dirlist[-1][0]
406
+ end
407
+ end
408
+ logger.p("...removing backup." + b.to_s + "...", 1)
409
+ FileUtils.remove_dir(self.to_s + a + "-backup." + b.to_s)
410
+ logger.p(@dir.to_s + a + "-backup." + b.to_s + " removed!", 2, true)
411
+ end
412
+
413
+ # Removes all backups directories except the last one
414
+ # (the one with the smallest 'number').
415
+ #
416
+ # Usually, this is "*-backup.0".
417
+ def remove_except_one
418
+ list = dirlist
419
+ for i in 1...list.size do
420
+ FileUtils.remove_dir "#{@dir}/#{list[i][1]}-backup.#{list[i][0]}"
421
+ end
422
+ end
423
+
424
+ # Increments all the directory numbers by 1. So for example "12-34-56-backup.7" becomes "12-34-56-backup.8".
425
+ def increment
426
+ dirlist.reverse.each do |element|
427
+ original=@dir.to_s+element[1].to_s+"-backup."+element[0].to_s
428
+ incremented=@dir.to_s+element[1].to_s+"-backup."+(element[0]+1).to_s
429
+ FileUtils.move(original, incremented)
430
+ end
431
+ end
432
+ end
433
+
434
+ class NoConfigException < RuntimeError
435
+ end
436
+
437
+ class InternalError < RuntimeError
438
+ end
data/lib/log.rb ADDED
@@ -0,0 +1,214 @@
1
+ require 'net/smtp'
2
+ require 'fileutils'
3
+ require 'syslog'
4
+
5
+ # Class to do automated logging to the console, to a logfile and to an email-account.
6
+ #
7
+ # It is possible to set the verbosity of logging and various other options.
8
+ #
9
+ # $Id$
10
+
11
+ class Log
12
+ # Initializes the logger object. Takes an (optional) path as argument, and
13
+ # a string ("yes" or "no") or boolean value for the verbosity level within this logfile.
14
+ #
15
+ # If the logfile is not yet existing, a new one will be created, if possible. Otherwise an error is thrown.
16
+ def initialize(logfile="", advancedlog=true, syslog=false)
17
+ @logfile=logfile.strip
18
+ @syslog = syslog
19
+ @mail_initialized = false
20
+ @smpt=false
21
+
22
+ @logging_to_file = false
23
+ unless @logfile == ""
24
+ @logfile.insert(0, '/') unless @logfile[0, 1].to_s == "/"
25
+ @logging_to_file = true
26
+ end
27
+
28
+ @logging_to_file = false if @syslog
29
+
30
+ @slog = SyslogWrapper.new() if @syslog
31
+
32
+ @advancedlog=advancedlog
33
+
34
+ if @logging_to_file and not Directory.writable?(@logfile)
35
+ @initialized = false
36
+ raise @logfile.to_s + " is not writable!"
37
+ elsif @logging_to_file and not File.file?(@logfile)
38
+ array = @logfile.split("/")
39
+ array.delete_at(-1)
40
+ FileUtils.makedirs("/" + array.join("/"))
41
+ end
42
+ @initialized = true
43
+ end
44
+
45
+ public
46
+ # Initialize the email function, and check if all the given parameters are correct.
47
+ # If tls is true, then see if ruby-openssl is installed.
48
+ #
49
+ # If any errors occur, it raises an error.
50
+ #
51
+ # Also sends a test email, if testm is set.
52
+ def initialize_email(email, smtp, port, uname, passwd, tls, testm, backupname)
53
+ @email=email
54
+ @smtp=smtp
55
+ @port=port
56
+ @uname=uname
57
+ @passwd=passwd
58
+ @tls=tls
59
+ @testm=testm
60
+ @backupname = backupname
61
+
62
+ if (@uname == "" and @passwd != "") or (@passwd == "" and @uname != "")
63
+ @mail_initialized=false
64
+ raise "Both or none of the uname and the passwd must be specified!"
65
+ end
66
+
67
+ if @port == ""
68
+ @mail_initialized = false
69
+ raise "Port mustn't be empty to send e-mails!"
70
+ end
71
+ if @smpt == ""
72
+ @mail_initialized = false
73
+ raise "Smtp mustn't be empty to send e-mails!"
74
+ end
75
+
76
+ unless (@email =~ /./ and @email =~ /@/)
77
+ @mail_initialized = false
78
+ raise "E-mail provided in Configfile is wrong or empty!"
79
+ end
80
+
81
+ @mail_initialized=true
82
+
83
+ if @testm
84
+ text='From: smarbs <' + @email + '>
85
+ To: smarbs Admin <'+@email+'>
86
+ Subject: Test email, ' + @backupname + '
87
+ This test email was sent to check the smtp-connection of the configfile "' + @backupname + '".
88
+ The "testm" option in the script should have been automatically disabled by now,
89
+ so you wont receive test emails anymore.
90
+
91
+ ---
92
+
93
+ For any questions concerning smarbs, you can contact me here: rggjan@gmail.com'
94
+ sendmail(text)
95
+ end
96
+
97
+ end
98
+
99
+
100
+ # if "date" is true, print the date into (only) the logfile. For example:
101
+ #
102
+ # 01 Nov 2008 19h23: Backup NOT successful!
103
+ #
104
+ # instead of
105
+ #
106
+ # Backup NOT successful!
107
+
108
+ # levels are:
109
+ # * 0: debug
110
+ # * 1: info
111
+ # * 2: notice
112
+ # * 3: warning
113
+ # * 4: error
114
+ # * 98: print only to command line (special case)
115
+ # * 99: successmail (special case)
116
+
117
+ def p(message, level=1, date=false)
118
+ case level
119
+ when 0..1
120
+ puts message
121
+ @slog.log(message, level) if @syslog
122
+ message=Time.now.strftime("%d %b %Y %Hh%M: ")+message if date
123
+ File.open(@logfile, "a") {|f| f.puts message} if @logging_to_file and @advancedlog
124
+
125
+ when 2..3
126
+ puts message
127
+ @slog.log(message, level) if @syslog
128
+ message=Time.now.strftime("%d %b %Y %Hh%M: ")+message if date
129
+ File.open(@logfile, "a") {|f| f.puts message} if @logging_to_file
130
+
131
+ when 4
132
+ @slog.log(message, level) if @syslog
133
+ message = Time.now.strftime("\n%d %b %Y %Hh%M: Backup NOT successful!\n") + "The following error occured: " + message
134
+ puts message
135
+ File.open(@logfile, "a") {|f| f.puts message } if @logging_to_file
136
+
137
+ if @mail_initialized
138
+ text='From: smarbs <' + @email + '>
139
+ To: smarbs Admin <'+@email+'>
140
+ Subject: An error occured in ' + @backupname + '
141
+ An error occured in ' + @backupname + ":\n" + message
142
+
143
+ if @logging_to_file
144
+ text += '
145
+
146
+ Here is the full logfile:
147
+
148
+ ' + IO.read(@logfile)
149
+ end
150
+ text +='
151
+
152
+ ---
153
+
154
+ For any questions concerning smarbs, you can contact me here: rggjan@gmail.com'
155
+ sendmail(text)
156
+ end
157
+
158
+ when 98
159
+ puts message
160
+ when 99
161
+ if @mail_initialized then
162
+ text='From: smarbs <' + @email + '>
163
+ To: smarbs Admin <'+@email+'>
164
+ Subject: Smarbs, backup "' + @backupname + '" successful
165
+ Smarbs backup ended with the following message:
166
+ ' + message
167
+
168
+ if @logging_to_file then text += '
169
+
170
+ Here is the full logfile:
171
+
172
+ ' + IO.read(@logfile)
173
+ end
174
+ text += '
175
+
176
+ ---
177
+
178
+ For any questions concerning smarbs, you can contact me here: rggjan@gmail.com'
179
+ sendmail(text)
180
+ end
181
+ end
182
+ end
183
+
184
+ # Sends an email with the appropriate message.
185
+ def sendmail(message)
186
+ begin
187
+ smtp = Net::SMTP.new(@smtp, @port)
188
+ if @uname != ""
189
+ if @tls then
190
+ smtp.enable_starttls
191
+ smtp.start("localhost.localdomain",@uname,@passwd, :plain) do |i|
192
+ i.send_message message, @email, @email
193
+ end
194
+ else
195
+ smtp.start("localhost.localdomain",@uname,@passwd, :login) do |i|
196
+ i.send_message message, @email, @email
197
+ end
198
+ end
199
+ else
200
+ smtp.start do |i|
201
+ i.send_message message, @email, @email
202
+ end
203
+ end
204
+ rescue Timeout::Error
205
+ @mail_initialized=false
206
+ raise "Error, SMTP timed out, " + $!.to_s
207
+ rescue
208
+ @mail_initialized=false
209
+ raise "Error with SMTP authentification, " + $!.to_s
210
+ end
211
+ end
212
+ end
213
+
214
+ # $Id$