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/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