mogbak 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +10 -0
- data/Gemfile.lock +31 -7
- data/bin/mogbak +33 -2
- data/lib/backup.rb +70 -55
- data/lib/forkinator.rb +0 -7
- data/lib/list.rb +2 -1
- data/lib/log.rb +17 -0
- data/lib/mogbak_version.rb +1 -1
- data/lib/restore.rb +4 -2
- data/lib/signal_handler.rb +19 -0
- data/lib/validations.rb +1 -1
- data/mogbak.gemspec +0 -3
- metadata +5 -18
data/CHANGES
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
2012-04-03: Release version 0.2.0
|
2
|
+
|
3
|
+
* Graceful handling of SIGINT and SIGTERM have been added. Upon receiving, mogbak will finish
|
4
|
+
the files being transfered and will exit. (Jesse Angell <jesse.angell@firespring.com>)
|
5
|
+
|
6
|
+
* Backup now has an optional --non-stop switch. This will cause mogbak to perform backup after backup
|
7
|
+
without exiting until SIGTERM or SIGINT is received. (Jesse Angell <jesse.angell@firespring.com>)
|
8
|
+
|
9
|
+
* --log-file option added to Restore, List, and Backup. Optional parameter that will redirect
|
10
|
+
output to a log file. Works well in conjunction with --non-stop. (Jesse Angell <jesse.angell@firespring.com>)
|
data/Gemfile.lock
CHANGED
@@ -1,20 +1,44 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
mogbak (0.
|
4
|
+
mogbak (0.1.3)
|
5
|
+
activerecord-import (>= 0.2.9)
|
6
|
+
gli (>= 1.5.1)
|
7
|
+
mogilefs-client (>= 3.1.1)
|
8
|
+
mysql2 (>= 0.3.11)
|
9
|
+
sqlite3 (>= 1.3.5)
|
5
10
|
|
6
11
|
GEM
|
7
12
|
remote: http://rubygems.org/
|
8
13
|
specs:
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
14
|
+
activemodel (3.2.2)
|
15
|
+
activesupport (= 3.2.2)
|
16
|
+
builder (~> 3.0.0)
|
17
|
+
activerecord (3.2.2)
|
18
|
+
activemodel (= 3.2.2)
|
19
|
+
activesupport (= 3.2.2)
|
20
|
+
arel (~> 3.0.2)
|
21
|
+
tzinfo (~> 0.3.29)
|
22
|
+
activerecord-import (0.2.9)
|
23
|
+
activerecord (~> 3.0)
|
24
|
+
activerecord (~> 3.0)
|
25
|
+
activesupport (3.2.2)
|
26
|
+
i18n (~> 0.6)
|
27
|
+
multi_json (~> 1.0)
|
28
|
+
arel (3.0.2)
|
29
|
+
awesome_print (1.0.2)
|
30
|
+
builder (3.0.0)
|
31
|
+
gli (1.5.1)
|
32
|
+
i18n (0.6.0)
|
33
|
+
mogilefs-client (3.1.1)
|
34
|
+
multi_json (1.2.0)
|
35
|
+
mysql2 (0.3.11)
|
36
|
+
sqlite3 (1.3.5)
|
37
|
+
tzinfo (0.3.32)
|
13
38
|
|
14
39
|
PLATFORMS
|
15
40
|
ruby
|
16
41
|
|
17
42
|
DEPENDENCIES
|
43
|
+
awesome_print
|
18
44
|
mogbak!
|
19
|
-
rake
|
20
|
-
rdoc
|
data/bin/mogbak
CHANGED
@@ -26,6 +26,11 @@ require 'sqlite3'
|
|
26
26
|
require 'yaml'
|
27
27
|
require 'forkinator'
|
28
28
|
require 'path_helper'
|
29
|
+
require 'signal_handler'
|
30
|
+
require 'log'
|
31
|
+
|
32
|
+
#activate signal handler
|
33
|
+
SignalHandler.instance
|
29
34
|
|
30
35
|
|
31
36
|
#Set master process name
|
@@ -102,15 +107,25 @@ command :backup do |c|
|
|
102
107
|
c.desc 'do not remove deleted files from the backup (faster)'
|
103
108
|
c.switch ['no-delete']
|
104
109
|
|
110
|
+
c.desc 'after backup is complete, start another backup. This causes mogbak to run non-stop. Send SIGINT or SIGTERM to exit cleanly'
|
111
|
+
c.switch ['non-stop']
|
112
|
+
|
105
113
|
c.desc 'Number of worker processes'
|
106
114
|
c.default_value 1
|
107
115
|
c.flag :workers
|
108
116
|
|
117
|
+
c.desc 'send backup output to log file'
|
118
|
+
c.flag ['log-file']
|
119
|
+
|
109
120
|
c.action do |global_options, options, args|
|
110
121
|
raise '[backup_path] is required - see: mogbak help backup' unless args[0]
|
111
122
|
$backup_path = args[0]
|
123
|
+
|
124
|
+
#setup a logger for output
|
125
|
+
Log.instance(options[:'log-file'])
|
126
|
+
|
112
127
|
mog = Backup.new(:backup_path => args[0], :workers => options[:workers].to_i)
|
113
|
-
mog.backup(:no_delete => options[:"no-delete"])
|
128
|
+
mog.backup(:no_delete => options[:"no-delete"], :non_stop => options[:"non-stop"])
|
114
129
|
end
|
115
130
|
|
116
131
|
end
|
@@ -142,11 +157,17 @@ command :restore do |c|
|
|
142
157
|
c.desc 'restore a single file by dkey'
|
143
158
|
c.flag :"single-file"
|
144
159
|
|
160
|
+
c.desc 'send restore output to log file'
|
161
|
+
c.flag ['log-file']
|
162
|
+
|
145
163
|
c.action do |global_options, options, args|
|
146
164
|
raise 'domain parameter is required - see: mogbak help restore' unless options[:domain]
|
147
165
|
raise '[backup_path] is required - see: mogbak help restore' unless args[0]
|
148
166
|
$backup_path = args[0]
|
149
167
|
|
168
|
+
#setup a logger for output
|
169
|
+
Log.instance(options[:'log-file'])
|
170
|
+
|
150
171
|
restore = Restore.new(:tracker_ip => options[:trackerip],
|
151
172
|
:tracker_port => options[:trackerport],
|
152
173
|
:domain => options[:domain],
|
@@ -162,18 +183,28 @@ Output is: fid,dkey,length,classname
|
|
162
183
|
EOS
|
163
184
|
arg_name '[backup_path]'
|
164
185
|
command :list do |c|
|
186
|
+
|
187
|
+
c.desc 'send list output to log file'
|
188
|
+
c.flag ['log-file']
|
189
|
+
|
165
190
|
c.action do |global_options, options, args|
|
166
191
|
raise '[backup_path] is required - see: mogbak help list' unless args[0]
|
167
192
|
$backup_path = args[0]
|
168
193
|
|
194
|
+
#setup a logger for output
|
195
|
+
Log.instance(options[:'log-file'])
|
196
|
+
|
169
197
|
list = List.new(:backup_path => args[0])
|
170
198
|
list.list
|
171
199
|
end
|
172
200
|
end
|
173
201
|
|
174
|
-
#
|
202
|
+
#Handle --debug
|
175
203
|
pre do |global_options, command, options, args|
|
204
|
+
|
205
|
+
#This is used to enable some more verbose output
|
176
206
|
$debug = global_options[:debug]
|
207
|
+
|
177
208
|
true
|
178
209
|
end
|
179
210
|
|
data/lib/backup.rb
CHANGED
@@ -22,7 +22,7 @@ class Backup
|
|
22
22
|
|
23
23
|
|
24
24
|
#run validations and setup
|
25
|
-
raise
|
25
|
+
raise unless check_backup_path
|
26
26
|
create_sqlite_db
|
27
27
|
connect_sqlite
|
28
28
|
migrate_sqlite
|
@@ -43,9 +43,9 @@ class Backup
|
|
43
43
|
def bak_file(file)
|
44
44
|
saved = file.bak_it
|
45
45
|
if saved
|
46
|
-
|
46
|
+
Log.instance.info("Backed up: FID #{file.fid}")
|
47
47
|
else
|
48
|
-
|
48
|
+
Log.instance.info("Error - will try again on next run: FID #{file.fid}")
|
49
49
|
end
|
50
50
|
|
51
51
|
return saved
|
@@ -72,11 +72,13 @@ class Backup
|
|
72
72
|
SqliteActiveRecord.clear_active_connections!
|
73
73
|
}
|
74
74
|
|
75
|
-
#This proc receives an array of BakFiles, proccesses them, and returns a result array to the parent proc
|
75
|
+
#This proc receives an array of BakFiles, proccesses them, and returns a result array to the parent proc. We will break
|
76
|
+
#from the files if the signal handler says so.
|
76
77
|
child = Proc.new { |files|
|
77
78
|
result = []
|
78
79
|
files.each do |file|
|
79
80
|
break if file.nil?
|
81
|
+
break if SignalHandler.instance.should_quit
|
80
82
|
saved = bak_file(file)
|
81
83
|
result << {:saved => saved, :file => file}
|
82
84
|
end
|
@@ -91,16 +93,18 @@ class Backup
|
|
91
93
|
#param [Array] files must be an array of BakFiles that need to be deleted
|
92
94
|
def launch_delete_workers(fids)
|
93
95
|
|
94
|
-
#This proc receives an array of BakFiles, handles them, and spits them back to the parent
|
96
|
+
#This proc receives an array of BakFiles, handles them, and spits them back to the parent, break from the fids if
|
97
|
+
#the signal handler says so.
|
95
98
|
child = Proc.new { |fids|
|
96
99
|
result = []
|
97
100
|
fids.each do |fid|
|
98
101
|
break if fid.nil?
|
102
|
+
break if SignalHandler.instance.should_quit
|
99
103
|
deleted = BakFile.delete_from_fs(fid)
|
100
104
|
if deleted
|
101
|
-
|
105
|
+
Log.instance.info("Deleting from backup: FID #{fid}")
|
102
106
|
else
|
103
|
-
|
107
|
+
Log.instance.info("Failed to delete from backup: FID #{fid}")
|
104
108
|
end
|
105
109
|
|
106
110
|
result << fid
|
@@ -133,64 +137,75 @@ class Backup
|
|
133
137
|
#@param [Hash] o if :no_delete then don't remove deleted files from the backup (intensive process)
|
134
138
|
def backup(o = {})
|
135
139
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
files
|
140
|
-
|
140
|
+
#Loop over the main backup logic. We'll break out at the end unless o[:non_stop] is set
|
141
|
+
loop do
|
142
|
+
files = []
|
143
|
+
#first we retry files that we haven't been able to backup successfully, if any.
|
144
|
+
BakFile.find_each(:conditions => ['saved = ?', false]) do |bak_file|
|
145
|
+
files << bak_file
|
146
|
+
end
|
141
147
|
|
142
|
-
|
148
|
+
launch_backup_workers(files)
|
143
149
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
150
|
+
#now back up any new files. if they fail to be backed up we'll retry them the next time the backup
|
151
|
+
#command is ran.
|
152
|
+
dmid = Domain.find_by_namespace(self.domain)
|
153
|
+
results = Fid.find_in_batches(:conditions => ['dmid = ? AND fid > ?', dmid, BakFile.max_fid], :batch_size => 500 * self.workers.to_i, :include => [:domain, :fileclass]) do |batch|
|
154
|
+
|
155
|
+
#Insert all the files into our bak db with :saved false so that we don't think we backed up something that crashed
|
156
|
+
files = []
|
157
|
+
batch.each do |file|
|
158
|
+
files << BakFile.new(:fid => file.fid,
|
159
|
+
:domain => file.domain.namespace,
|
160
|
+
:dkey => file.dkey,
|
161
|
+
:length => file.length,
|
162
|
+
:classname => file.classname,
|
163
|
+
:saved => false)
|
164
|
+
end
|
148
165
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
:dkey => file.dkey,
|
155
|
-
:length => file.length,
|
156
|
-
:classname => file.classname,
|
157
|
-
:saved => false)
|
158
|
-
end
|
166
|
+
#There is no way to do a bulk insert in sqlite so this generates a lot of inserts. wrapping all of the inserts
|
167
|
+
#inside a single transaction makes it much much faster.
|
168
|
+
BakFile.transaction do
|
169
|
+
BakFile.import files, :validate => false
|
170
|
+
end
|
159
171
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
172
|
+
#Fire up the workers now that we have work for them to do
|
173
|
+
launch_backup_workers(files)
|
174
|
+
|
175
|
+
#Terminate program if the signal handler says so and this is a clean place to do it
|
176
|
+
return true if SignalHandler.instance.should_quit
|
164
177
|
end
|
165
178
|
|
166
|
-
#
|
167
|
-
|
179
|
+
#Delete files from the backup that no longer exist in the mogilefs domain. Unfortunently there is no easy way to detect
|
180
|
+
#which files have been deleted from the MogileFS domain. Our only option is to brute force our way through. This is a bulk
|
181
|
+
#query that checks a thousand files in each query against the MogileFS database server. The query is kind of tricky because
|
182
|
+
#I wanted to do this with nothing but SELECT privileges which meant I couldn't create a temporary table (which would require,
|
183
|
+
#create temporary table and insert privleges). You might want to only run this operation every once and awhile if you have a
|
184
|
+
#very large domain. In my testing, it is able to get through domains with millions of files in a matter of a second. So
|
185
|
+
#all in all it's not so bad
|
186
|
+
if !o[:no_delete]
|
168
187
|
|
169
|
-
|
188
|
+
BakFile.find_in_batches { |bak_files|
|
189
|
+
union = "SELECT #{bak_files.first.fid} as fid"
|
190
|
+
bak_files.shift
|
191
|
+
bak_files.each do |bakfile|
|
192
|
+
union = "#{union} UNION SELECT #{bakfile.fid}"
|
193
|
+
end
|
194
|
+
connection = ActiveRecord::Base.connection
|
195
|
+
files = connection.select_values("SELECT t1.fid FROM (#{union}) as t1 LEFT JOIN file on t1.fid = file.fid WHERE file.fid IS NULL")
|
196
|
+
launch_delete_workers(files)
|
170
197
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
#I wanted to do this with nothing but SELECT privileges which meant I couldn't create a temporary table (which would require,
|
175
|
-
#create temporary table and insert privleges). You might want to only run this operation every once and awhile if you have a
|
176
|
-
#very large domain. In my testing, it is able to get through domains with millions of files in a matter of a second. So
|
177
|
-
#all in all it's not so bad
|
178
|
-
if !o[:no_delete]
|
179
|
-
files_to_delete = Array.new
|
180
|
-
BakFile.find_in_batches { |bak_files|
|
181
|
-
|
182
|
-
union = "SELECT #{bak_files.first.fid} as fid"
|
183
|
-
bak_files.shift
|
184
|
-
bak_files.each do |bakfile|
|
185
|
-
union = "#{union} UNION SELECT #{bakfile.fid}"
|
186
|
-
end
|
187
|
-
connection = ActiveRecord::Base.connection
|
188
|
-
files = connection.select_values("SELECT t1.fid FROM (#{union}) as t1 LEFT JOIN file on t1.fid = file.fid WHERE file.fid IS NULL")
|
189
|
-
files_to_delete += files
|
190
|
-
}
|
198
|
+
#Terminate program if the signal handler says so and this is a clean place to do it
|
199
|
+
return true if SignalHandler.instance.should_quit
|
200
|
+
}
|
191
201
|
|
192
|
-
launch_delete_workers(files_to_delete)
|
193
202
|
|
203
|
+
|
204
|
+
end
|
205
|
+
|
206
|
+
#Break out of infinite loop unless o[:non_stop] is set
|
207
|
+
break unless o[:non_stop]
|
208
|
+
sleep 1
|
194
209
|
end
|
195
210
|
|
196
211
|
end
|
data/lib/forkinator.rb
CHANGED
@@ -88,13 +88,6 @@ class Forkinator
|
|
88
88
|
children = []
|
89
89
|
qty.times { children << make_child(child_proc)}
|
90
90
|
|
91
|
-
#register signal handler so that children kill if program receives a SIGINT
|
92
|
-
#which will happen if the user ctrl c's the parent process
|
93
|
-
Signal.trap :SIGINT do
|
94
|
-
children.each { |child| Process.kill(:KILL, child[:pid]) if child[:pid]}
|
95
|
-
exit 1
|
96
|
-
end
|
97
|
-
|
98
91
|
#For each worker
|
99
92
|
qty.times do |i|
|
100
93
|
|
data/lib/list.rb
CHANGED
@@ -22,7 +22,8 @@ class List
|
|
22
22
|
#fid,key,length,class
|
23
23
|
def list
|
24
24
|
files = BakFile.find_each(:conditions => ['saved = ?', true]) do |file|
|
25
|
-
|
25
|
+
Log.instance.info("#{file.fid},#{file.dkey},#{file.length},#{file.classname}")
|
26
|
+
break if SignalHandler.instance.should_quit
|
26
27
|
end
|
27
28
|
end
|
28
29
|
end
|
data/lib/log.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'logger'
|
2
|
+
#Create a Logger that is a singleton provided by the instance method
|
3
|
+
class Log
|
4
|
+
def self.instance(log_file = nil)
|
5
|
+
@@instance ||= create_logger(log_file)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.create_logger(log_file)
|
9
|
+
log_file = STDOUT if log_file == nil
|
10
|
+
logger = Logger.new(log_file)
|
11
|
+
logger.datetime_format = "%Y-%m-%d %H:%M:%S"
|
12
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
13
|
+
"#{datetime}: #{msg}\n"
|
14
|
+
end
|
15
|
+
logger
|
16
|
+
end
|
17
|
+
end
|
data/lib/mogbak_version.rb
CHANGED
data/lib/restore.rb
CHANGED
@@ -28,9 +28,9 @@ class Restore
|
|
28
28
|
|
29
29
|
def output_save(save, fid)
|
30
30
|
if save
|
31
|
-
|
31
|
+
Log.instance.info("Restored: FID #{fid}")
|
32
32
|
else
|
33
|
-
|
33
|
+
Log.instance.info("Error: FID #{fid}")
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
@@ -39,6 +39,7 @@ class Restore
|
|
39
39
|
results = []
|
40
40
|
files.each do |file|
|
41
41
|
break if file.nil?
|
42
|
+
break if SignalHandler.instance.should_quit
|
42
43
|
save = file.restore
|
43
44
|
output_save(save, file.fid)
|
44
45
|
results << {:restored => save, :fid => file.fid}
|
@@ -64,6 +65,7 @@ class Restore
|
|
64
65
|
|
65
66
|
BakFile.find_in_batches(:conditions => ['saved = ?', true], :batch_size => 2000) do |batch|
|
66
67
|
launch_restore_workers(batch)
|
68
|
+
break if SignalHandler.instance.should_quit
|
67
69
|
end
|
68
70
|
|
69
71
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
#Singleton class used to intercept signals. If a SIGINT or SIGTERM is received a message is outputted and @should_quit
|
4
|
+
#is set to true
|
5
|
+
class SignalHandler
|
6
|
+
include Singleton
|
7
|
+
attr_reader :should_quit
|
8
|
+
|
9
|
+
def handle_signal
|
10
|
+
puts "PID #{Process.pid} is gracefully shutting down..."
|
11
|
+
@should_quit = true
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@should_quit = false
|
16
|
+
Signal.trap("SIGINT") { handle_signal }
|
17
|
+
Signal.trap("SIGTERM") { handle_signal }
|
18
|
+
end
|
19
|
+
end
|
data/lib/validations.rb
CHANGED
@@ -45,7 +45,7 @@ module Validations
|
|
45
45
|
#@return [Bool]
|
46
46
|
def connect_sqlite(raise_msg = nil)
|
47
47
|
begin
|
48
|
-
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => "#{$backup_path}/db.sqlite", :timeout =>
|
48
|
+
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => "#{$backup_path}/db.sqlite", :timeout => 10000)
|
49
49
|
rescue Exception => e
|
50
50
|
raise raise_msg if raise_msg
|
51
51
|
raise e if $debug
|
data/mogbak.gemspec
CHANGED
@@ -19,9 +19,6 @@ spec = Gem::Specification.new do |s|
|
|
19
19
|
s.add_runtime_dependency('gli', '>= 1.5.1')
|
20
20
|
s.add_runtime_dependency('mysql2', '>= 0.3.11')
|
21
21
|
s.add_runtime_dependency('mogilefs-client','>= 3.1.1')
|
22
|
-
s.add_runtime_dependency('json','>= 1.6.5')
|
23
22
|
s.add_runtime_dependency('sqlite3','>=1.3.5')
|
24
23
|
s.add_runtime_dependency('activerecord-import','>=0.2.9')
|
25
|
-
|
26
|
-
|
27
24
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mogbak
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-03
|
12
|
+
date: 2012-04-03 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: awesome_print
|
@@ -75,22 +75,6 @@ dependencies:
|
|
75
75
|
- - ! '>='
|
76
76
|
- !ruby/object:Gem::Version
|
77
77
|
version: 3.1.1
|
78
|
-
- !ruby/object:Gem::Dependency
|
79
|
-
name: json
|
80
|
-
requirement: !ruby/object:Gem::Requirement
|
81
|
-
none: false
|
82
|
-
requirements:
|
83
|
-
- - ! '>='
|
84
|
-
- !ruby/object:Gem::Version
|
85
|
-
version: 1.6.5
|
86
|
-
type: :runtime
|
87
|
-
prerelease: false
|
88
|
-
version_requirements: !ruby/object:Gem::Requirement
|
89
|
-
none: false
|
90
|
-
requirements:
|
91
|
-
- - ! '>='
|
92
|
-
- !ruby/object:Gem::Version
|
93
|
-
version: 1.6.5
|
94
78
|
- !ruby/object:Gem::Dependency
|
95
79
|
name: sqlite3
|
96
80
|
requirement: !ruby/object:Gem::Requirement
|
@@ -131,6 +115,7 @@ extensions: []
|
|
131
115
|
extra_rdoc_files: []
|
132
116
|
files:
|
133
117
|
- .gitignore
|
118
|
+
- CHANGES
|
134
119
|
- Gemfile
|
135
120
|
- Gemfile.lock
|
136
121
|
- LICENSE
|
@@ -145,10 +130,12 @@ files:
|
|
145
130
|
- lib/fileclass.rb
|
146
131
|
- lib/forkinator.rb
|
147
132
|
- lib/list.rb
|
133
|
+
- lib/log.rb
|
148
134
|
- lib/mogbak_version.rb
|
149
135
|
- lib/monkey_patch.rb
|
150
136
|
- lib/path_helper.rb
|
151
137
|
- lib/restore.rb
|
138
|
+
- lib/signal_handler.rb
|
152
139
|
- lib/validations.rb
|
153
140
|
- mogbak.gemspec
|
154
141
|
homepage: http://www.github.com/firespring/mogbak
|