smarbs 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
data/lib/script.rb ADDED
@@ -0,0 +1,217 @@
1
+ require 'types'
2
+ require 'backup'
3
+ require 'smarbs'
4
+
5
+ class Script
6
+
7
+ def initialize (configdir, argv = [])
8
+ @halt = false
9
+ @argv=argv
10
+ @configfile = nil
11
+ begin
12
+ @config_dir = Directory.new(configdir)
13
+ getconfigfiles
14
+ rescue RuntimeError
15
+ puts "Error: " + $!.to_s
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def getconfigfiles #return an array of config files
22
+ @configfile_names = @config_dir.files(false)
23
+
24
+ if @configfile_names.size == 0 #create sample configfile
25
+ @configfile=ConfigFile.new(@config_dir.to_s+"backup1")
26
+ end
27
+
28
+ parse(@argv)
29
+ end
30
+
31
+ def backup
32
+ if @status
33
+ begin
34
+ require 'gtk2'
35
+ rescue LoadError
36
+ raise "Error: No ruby - gtk2 installed, which is needed when using the '--status' option."
37
+ end
38
+
39
+ datadir=SMARBS::DATADIR
40
+
41
+ Gtk.init
42
+ menu = Gtk::Menu.new
43
+
44
+ item3 = Gtk::ImageMenuItem.new(Gtk::Stock::EXECUTE)
45
+ item3.show
46
+ item1 = Gtk::CheckMenuItem.new("Shutdown after Backup")
47
+ if @config_dir.to_s == "/etc/smarbs/" then
48
+ item1.show
49
+ else
50
+ puts @config_dir.to_s
51
+ end
52
+ item2 = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT)
53
+ item2.show
54
+
55
+ menu.append(item3)
56
+ menu.append(item1)
57
+ menu.append(item2)
58
+
59
+ icon = Gtk::StatusIcon.new
60
+ icon.pixbuf=Gdk::Pixbuf.new(datadir + "/icongreen.png")
61
+ icon.tooltip="Smarbs is ready to back up!";
62
+ icon.blinking=true
63
+
64
+ handler = icon.signal_connect("activate") {item3.signal_emit("activate") }
65
+
66
+ handler2 = icon.signal_connect('popup-menu') { |w, button, time|
67
+ menu.popup(nil, nil, button, time)
68
+ }
69
+
70
+ item3.signal_connect("activate") do
71
+ icon.blinking=false
72
+ item2.hide
73
+ item3.hide
74
+ icon.tooltip="Smarbs is working...";
75
+ if item1.active?
76
+ icon.pixbuf=Gdk::Pixbuf.new(datadir + "/iconred.png")
77
+ else
78
+ icon.pixbuf=Gdk::Pixbuf.new(datadir + "/iconblue.png")
79
+ end
80
+ if not item1.visible? then
81
+ icon.signal_handler_disconnect handler2
82
+ end
83
+ icon.signal_handler_disconnect handler
84
+ Thread.new{start_backup}
85
+ end
86
+
87
+ if item1.visible? then
88
+ item1.signal_connect("toggled") do
89
+ if item1.active?
90
+ if not item3.visible? then icon.pixbuf=Gdk::Pixbuf.new(datadir + "/iconred.png") end
91
+ @halt = true
92
+ else
93
+ if not item3.visible? then icon.pixbuf=Gdk::Pixbuf.new(datadir + "/iconblue.png") end
94
+ @halt = false
95
+ end
96
+ end
97
+ end
98
+
99
+ item2.signal_connect("activate") { Gtk.main_quit }
100
+
101
+ Gtk.main
102
+ else
103
+ start_backup
104
+ end
105
+
106
+ end
107
+
108
+ def start_backup
109
+ if @configfile_arguments.size == 0
110
+ todo = @configfile_names
111
+ else
112
+ todo = @configfile_arguments
113
+ end
114
+ todo.each do |name|
115
+ Backup.new(@config_dir.to_s + name, @rsync_arguments.join(" "), @verbose)
116
+ end unless not @configfile.nil? and @configfile.new
117
+ if @status then
118
+ Gtk.main_quit
119
+ if @halt then `halt` end
120
+ end
121
+ end
122
+
123
+ def print_help
124
+ print_version
125
+
126
+ puts "\nsmarbs is a backup script written in ruby capable of doing intelligent and\nautomated backups using rsync."
127
+
128
+ puts "\nUsage: smarbs [OPTION]... [CONFIGFILE]... "
129
+ puts "Makes backup according to the specified configfile(s),"
130
+ puts "when no configfile is specified and no information options are invoked,\nall existing configfiles are executed."
131
+
132
+ puts "\nBackup options:"
133
+ puts " -v, --verbose increase verbosity, ignoring the configfile option"
134
+ puts " --pass-rsync=OPTIONS pass OPTIONS to rsync before executing it"
135
+ puts " =>use with caution! usually this is not needed"
136
+ puts " -s, --status only usable when \"gtk2-ruby\" is installed"
137
+ puts " =>shows a gtk tray/status icon with the options"
138
+ puts " \"backup\", \"quit\" and as root a checkbox"
139
+ puts " \"shutdown after backup\""
140
+
141
+ puts "\nInformation options:"
142
+ puts "(won't work together with backup options)"
143
+ puts " -l, --list list available configfiles"
144
+ puts " -h, --help show this help"
145
+ puts " -V, --Version print version number"
146
+ puts "\nConfigfiles are stored in the directory /etc/smarbs \nor ~/.smarbs, if not executed as root."
147
+ end
148
+
149
+ def print_version
150
+ puts "smarbs " + SMARBS::VERSION.to_s + " (" + SMARBS::DATE.to_s + ")"
151
+ puts "Copyright (C) 2006-2009 by Jan Rueegg (rggjan)"
152
+ puts "<http://smarbs.sourceforge.net/>\n\n"
153
+
154
+ puts 'smarbs comes with ABSOLUTELY NO WARRANTY. This is free software, and you
155
+ are welcome to redistribute it under certain conditions. See the GNU
156
+ General Public Licence for details.'
157
+ end
158
+
159
+ public
160
+
161
+ def parse (arguments)
162
+ if not arguments.nil? then
163
+ @configfile_arguments = Array.new
164
+ @rsync_arguments = Array.new
165
+ infarg = Array.new #Information Arguments (--help or --version)
166
+ @verbose = false
167
+ @status = false
168
+ begin
169
+ arguments.each do
170
+ |argv|
171
+ case argv
172
+ when "--version", "-V"
173
+ infarg.push argv
174
+ when "--help", "-h", "-?"
175
+ infarg.push argv
176
+ when "--list", "-l"
177
+ infarg.push argv
178
+ when "--verbose", "-v"
179
+ @verbose=true
180
+ when /^--pass-rsync=/
181
+ @rsync_arguments.push(argv.gsub(/^--pass-rsync=/, ""))
182
+ when "--status", "-s"
183
+ @status=true
184
+ else
185
+ if not @configfile_names.include? argv then
186
+ raise "Unknown argument '#{argv}'!\nUse '--help' to see possible options."
187
+ end
188
+ @configfile_arguments.push(argv)
189
+ end
190
+ end
191
+
192
+ if infarg.size == 0 then
193
+ backup
194
+ else
195
+ if @configfile_arguments.size != 0 or @verbose == true or @rsync_arguments.size != 0 then
196
+ raise "You cannot backup and use '#{infarg[0]}' at the same time."
197
+ end
198
+ infarg.uniq.each do
199
+ |arg|
200
+ case arg
201
+ when "--version", "-V"
202
+ print_version
203
+ when "--help", "-h", "-?"
204
+ print_help
205
+ when "--list", "-l"
206
+ puts "Available configfiles:"
207
+ puts @configfile_names
208
+ end
209
+ end
210
+ end
211
+
212
+ rescue RuntimeError
213
+ puts $!
214
+ end
215
+ end
216
+ end
217
+ end
data/lib/smarbs.rb ADDED
@@ -0,0 +1,5 @@
1
+ module SMARBS
2
+ VERSION = "0.9.3"
3
+ DATE = "16. December 2009"
4
+ DATADIR = File.expand_path("../data/", __FILE__)
5
+ end
data/lib/syslog.rb ADDED
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Syslog wrapper class
4
+ # might be needed for reasons discussed on
5
+ # http://dev.animoto.com/articles/ruby-syslog-considered-harmful-or-at-least-undocumented
6
+ #
7
+ # Paavo Pokkinen (C) 2009
8
+ # License: GPL
9
+ #
10
+ # $Id$
11
+
12
+ require 'syslog'
13
+
14
+ class SyslogWrapper
15
+
16
+ def initialize()
17
+ # facility is "user", since there is no "backup" facility
18
+ # also, if we are running root, perhaps we should log with
19
+ # "daemon" facility?
20
+ #
21
+ # facilies and priorities are listed here:
22
+ # http://docstore.mik.ua/orelly/networking/puis/ch10_05.htm
23
+ @@log = Syslog.open('smarbs', Syslog::LOG_PID, Syslog::LOG_USER)
24
+
25
+ # normally log up to INFO level
26
+ # for debug, try DEBUG here (if we have any debug messages)
27
+ @@log.mask = Syslog::LOG_UPTO(Syslog::LOG_INFO)
28
+ end
29
+
30
+ def finalize()
31
+ # not sure if closing is really necessary,
32
+ # nothing bad really happens if we pass this...
33
+ @@log.close() if @@log
34
+ end
35
+
36
+ public
37
+
38
+ # logger function, does some input satitation, and logs to syslog
39
+ def log(msg="", level=1)
40
+
41
+ # Oh yes, we need to sanitaze here.
42
+
43
+ msg.gsub!(/%/, ' ') # '%' needs to be removed, else there will be runtimeError
44
+ msg.gsub!(/\n/, ' ')
45
+ msg.gsub!(/\0/, ' ')
46
+
47
+ # Ideally we would split the msg, and post in multiple parts
48
+ # but I guess this is enough, we should never have that long lines anyway
49
+ if msg.length >= 1024
50
+ msg = msg[0..1023]
51
+ end
52
+
53
+ # levels are:
54
+ # * 0: debug
55
+ # * 1: info
56
+ # * 2: notice
57
+ # * 3: warning
58
+ # * 4: error
59
+
60
+ case level
61
+ when 1
62
+ @@log.info(msg) unless msg.empty?
63
+ when 2
64
+ @@log.notice(msg) unless msg.empty?
65
+ when 3
66
+ @@log.warning(msg) unless msg.empty?
67
+ when 4
68
+ @@log.err(msg) unless msg.empty?
69
+ when 0
70
+ @@log.debug(msg) unless msg.empty?
71
+ else
72
+ # something tries to log with nonexisting level, or with too high level
73
+ # its probably better to halt the execution than log with wrong level, or not log at all
74
+ raise RuntimeError.new("Wrong loglevel defined somewhere! This is a bug.")
75
+ end
76
+ end
77
+
78
+ end
79
+
80
+ if __FILE__ == $0
81
+ log = SyslogWrapper.new()
82
+
83
+ log.log("testing")
84
+ log.log("Just a notice.", 2)
85
+ log.log("Should not print", 0)
86
+ #log.log("We can't print with too high priority", 5)
87
+
88
+ # Torture test strings, from animoto's blog
89
+ test1 = (0..255).map{|x|"<#{x.chr}>\n" }.join
90
+ test2 = (1..3000).map{|x|"<#{(97+(x%26)).chr}>\n" }.join
91
+ log.log( test1 + test2 + "is this thing still on?")
92
+
93
+ # empty msg, should not print
94
+ log.log("")
95
+
96
+ end
97
+
98
+ # $Id$
data/lib/types.rb ADDED
@@ -0,0 +1,172 @@
1
+ # types.rb contains all the classes that might also be used in other projects,
2
+ # for example to handle files / directories or to represent sizes.
3
+
4
+ require 'fileutils'
5
+
6
+ # Can be used to create directories, to get files within a directory or to see
7
+ # if a directory is writable.
8
+ class Directory
9
+
10
+ # Takes a (path)string as input and creates a directory there, if possible.
11
+ def initialize(directory, *args)
12
+ @dir = Directory.prepare(directory)
13
+ if not @dir[-1, 1].to_s == "/"
14
+ @dir.insert(-1, '/')
15
+ end
16
+ if args.include?(:writable)
17
+ if not Directory.writable?(File.dirname(@dir))
18
+ raise "#{@dir} is not writable!"
19
+ end
20
+ end
21
+ if not File.directory?(@dir)
22
+ if not Directory.writable?(File.dirname(@dir))
23
+ raise "Directory '#{@dir}' is not existing!"
24
+ else
25
+ FileUtils.makedirs(@dir)
26
+ puts "#{@dir} created."
27
+ end
28
+ end
29
+ end
30
+
31
+ public
32
+
33
+ attr_reader(:dir)
34
+
35
+ # Checks if input is a valid argument and returns normalized string.
36
+ def Directory.prepare (input)
37
+ raise "Internal Error: Argument not a String" if input.class != String
38
+ input.strip!
39
+ raise "No directory specified!" if input.empty?
40
+ input.insert(0, '/') unless input[0, 1].to_s == "/"
41
+ return input
42
+ end
43
+
44
+ # Checks if the directory (relative to the root directory) is writable, or, if
45
+ # not yet existing, creatable.
46
+ def Directory.writable?(path)
47
+ Directory.prepare(path)
48
+ if File.exists?(path) and File.directory?(path)
49
+ return File.writable?(path)
50
+ end
51
+ return writable?(File.dirname(path))
52
+ end
53
+
54
+ # Compares if the two directories are equal.
55
+ def == (other_directory)
56
+ return @dir == other_directory.dir
57
+ end
58
+
59
+ # Returns the normalized directory path as a string.
60
+ def to_s
61
+ @dir
62
+ end
63
+
64
+ # Returns how much free space there is (on the whole partition, using "df")
65
+ # as a Float of bytes
66
+ def free
67
+ path = to_s
68
+ `df --block-size=1 #{path}`.split[-3].to_f
69
+ end
70
+
71
+ # Gives back (in bytes) how much space is used for the whole Smarbsbackupdir.
72
+ #
73
+ # If ownpartition is true, it will use 'df' instead of 'du' which is much faster.
74
+ def space(ownpartition = false)
75
+ path = to_s
76
+ if ownpartition
77
+ `df --block-size=1 #{path}`.split[-4].to_f
78
+ else
79
+ `du -bcs #{path}`.split[-2].to_f
80
+ end
81
+ end
82
+
83
+ # Returns an array of files and folders that the Directory contains, excluding "." and "..".
84
+ #
85
+ # When 'all' is 'false', then it excludes files with a "~" at the end and all folders.
86
+ def files(all=true)
87
+ if all then
88
+ return Dir.entries(@dir) - ["."] - [".."]
89
+ else
90
+ configfile_names = Array.new
91
+ Dir.foreach(@dir) do |filename|
92
+ path = @dir+"/"+filename
93
+ if File.file?(path) and not path =~ /.*~/ # put every file without ~ at the end into the array
94
+ configfile_names.push(filename)
95
+ end
96
+ end
97
+ return configfile_names
98
+ end
99
+ end
100
+ end
101
+
102
+ # A class that represents sizes of directories. The main feature is to
103
+ # give back a size in a "human readable" format, like "5 MiB".
104
+ class Size
105
+ # Takes a size as a string. With no ending it is assumed as a size in bytes.
106
+ # Otherwise, (for example gvien a "32m") it is interpreted depending on the ending:
107
+ # g:: GiB
108
+ # m:: MiB
109
+ # k:: KiB
110
+ # Raises an error if an unexpected string is given (like "m32" or "32 MiB").
111
+ def initialize(string)
112
+ size = string.to_s.strip.downcase
113
+ case size
114
+ when "" then @size=0
115
+ when /g$/ then @size=$`.to_f * 1024**3
116
+ raise "Wrong Size format, '" + string + "' not valid!" unless $`.to_f.to_s == $` or $`.to_i.to_s == $`
117
+ when /m$/ then @size=$`.to_f * 1024**2
118
+ raise "Wrong Size format, '" + string + "' not valid!" unless $`.to_f.to_s == $` or $`.to_i.to_s == $`
119
+ when /k$/ then @size=$`.to_f * 1024
120
+ raise "Wrong Size format, '" + string + "' not valid!" unless $`.to_f.to_s == $` or $`.to_i.to_s == $`
121
+ else
122
+ @size=size.to_f
123
+ raise "Wrong Size format, '" + string + "' not valid!" unless size.to_f.to_s == size or size.to_i.to_s == size
124
+ end
125
+ end
126
+
127
+ # Returns a string which is "optimal" formatted, like "34.333 GiB", rounded
128
+ # to "dec" digits after the comma.
129
+ #
130
+ # Default dec: 2. A dec of "-1" means no rounding.
131
+ #
132
+ # Uses "B", "KiB", "MiB" or "GiB" as ending.
133
+ def opt(dec = 2)
134
+ case @size.abs
135
+ when 0..1023 then s, d=@size, " B"
136
+ when 1024..1024**2-1 then s, d=@size/1024, " KiB"
137
+ when 1024**2..1024**3-1 then s, d=@size/1024**2, " MiB"
138
+ else s, d=@size/1024**3, " GiB"
139
+ end
140
+ return dec==-1 ? s.to_s + d : ((s * 10**dec).round.to_f / 10**dec).to_s + d
141
+ end
142
+
143
+ # Returns a string which is "optimal" formatted, but with a short
144
+ # ending, like "34.33g", rounded to "dec" digits after the comma.
145
+ #
146
+ # Default dec: 2. A dec of "-1" means no rounding.
147
+ #
148
+ # Uses "", "k", "m" or "g" as ending.
149
+ def short(dec = 2)
150
+ case @size.abs
151
+ when 0..1023 then s, d=@size, ""
152
+ when 1024..1024**2-1 then s, d=@size.to_f/1024, "k"
153
+ when 1024**2..1024**3-1 then s, d=@size.to_f/1024**2, "m"
154
+ else s, d=@size.to_f/1024**3, "g"
155
+ end
156
+ return dec==-1 ? s.to_s + d : ((s * 10**dec).round.to_f / 10**dec).to_s + d
157
+ end
158
+
159
+ # Returns the size in bytes as an integer.
160
+ def b
161
+ @size.round
162
+ end
163
+
164
+ # Returns the size in bytes as a string.
165
+ def to_s
166
+ return @size.to_s
167
+ end
168
+
169
+ def ==(other)
170
+ return b == other.b
171
+ end
172
+ end
@@ -0,0 +1,114 @@
1
+ require 'stringio'
2
+ require 'smarbs'
3
+ require 'test/unit'
4
+
5
+ module SMARBS
6
+ begin
7
+ remove_const "VERSION"
8
+ remove_const "DATE"
9
+ rescue NameError
10
+ end
11
+ VERSION = "0.9.1"
12
+ DATE = "4. September 2009"
13
+ end
14
+
15
+ module SmarbsTest
16
+ def clean
17
+ clean_all
18
+ `mkdir /tmp/smarbs`
19
+ `cd /tmp/smarbs && mkdir smarbs && mkdir backmeup && mkdir backedup`
20
+ end
21
+
22
+ def clean_all
23
+ `rm -r /tmp/smarbs` if File.exists?("/tmp/smarbs")
24
+ end
25
+
26
+ # give :nonequal to test for non-equality, :exact for exact results as argument
27
+ def assert_output(strings, *params)
28
+ output = with_stdout_captured { yield }
29
+
30
+ strings = [strings] if strings.class == String or strings.class == Regexp
31
+ strings.each do |string|
32
+ unless params.include? :nonequal
33
+ if params.include? :exact
34
+ assert_equal string, output
35
+ else
36
+ string = Regexp.new(string) if string.class == String
37
+ assert_match string, output
38
+ end
39
+ else
40
+ if params.include? :exact
41
+ assert_not_equal string, output
42
+ else
43
+ string = Regexp.new(string) if string.class == String
44
+ assert_no_match string, output
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ def assert_success(string="") #TODO write tests
51
+ arg = ["Successfully backed up."]
52
+ if string.class == Array then
53
+ arg += string
54
+ else
55
+ arg << string
56
+ end
57
+ assert_output(arg) do
58
+ yield
59
+ end
60
+ end
61
+
62
+ def assert_fail(string="")
63
+ arg = ["Backup NOT successful!"]
64
+ if string.class == Array then
65
+ arg += string
66
+ else
67
+ arg << string
68
+ end
69
+ assert_output(arg) do
70
+ yield
71
+ end
72
+ end
73
+
74
+ def assert_backup_dirs(array)
75
+ actual = Dir.entries("/tmp/smarbs/backedup/smarbsbackup/")
76
+ actual.delete(".")
77
+ actual.delete("..")
78
+ actual.collect! {|name| name.slice(/-backup.*$/)}
79
+ actual.sort!
80
+ array.collect! {|name| "-backup." + name.to_s}
81
+ array.sort!
82
+ assert_equal(array, actual)
83
+ end
84
+
85
+ def default_configfile
86
+ cf = nil
87
+ assert_output "" do
88
+ cf = ConfigFile.new("/tmp/smarbs/smarbs/backup1")
89
+ end
90
+ cf.src = ["/tmp/smarbs/backmeup"]
91
+ cf.dest = "/tmp/smarbs/backedup"
92
+ cf.write
93
+ cf
94
+ end
95
+
96
+ def assert_raise_message(string, error = RuntimeError)
97
+ m = assert_raise( error ) {yield}
98
+ assert_equal(string, m.message)
99
+ end
100
+
101
+ private
102
+
103
+ def with_stdout_captured
104
+ old_stdout = $stdout
105
+ out = StringIO.new
106
+ $stdout = out
107
+ begin
108
+ yield
109
+ ensure
110
+ $stdout = old_stdout
111
+ end
112
+ out.string
113
+ end
114
+ end