5 # :title: RBot Registry Backup, Restore and Migration Script.
7 # You can use this script to,
8 # - backup the rbot registry in a format that is platform/engine independent
9 # - restore these backups in supported formats (dbm, daybreak)
10 # - migrate old rbot registries bdb (ruby 1.8) and tokyocabinet.
12 # For more information, just execute the script without any arguments!
14 # Author:: apoc (Matthias Hecker) <apoc@geekosphere.org>
15 # Copyright:: (C) 2014 Matthias Hecker
18 begin; require 'rubygems'; rescue Exception; end
20 # load registry formats:
21 begin; require 'bdb'; rescue Exception; end
22 begin; require 'tokyocabinet'; rescue Exception; end
23 begin; require 'dbm'; rescue Exception; end
24 begin; require 'daybreak'; rescue Exception; end
25 begin; require 'sqlite3'; rescue Exception; end
27 puts 'RBot Registry Backup/Restore/Migrate'
28 puts '[%s]' % ['Ruby: ' + RUBY_VERSION,
29 'DBM: ' + (DBM::VERSION rescue '-'),
30 'BDB: ' + (BDB::VERSION rescue '-'),
31 'TokyoCabinet: ' + (TokyoCabinet::VERSION rescue '-'),
32 'Daybreak: ' + (Daybreak::VERSION rescue '-'),
33 'SQLite: ' + (SQLite3::VERSION rescue '-'),
39 TYPES = [:bdb, :tc, :dbm, :daybreak, :sqlite]
41 :profile => '~/.rbot',
43 :dbfile => './%s.rbot' % DateTime.now.strftime('backup_%Y-%m-%d_%H%M%S'),
46 opt_parser = OptionParser.new do |opt|
47 opt.banner = 'Usage: rbotdb COMMAND [OPTIONS]'
49 opt.separator 'Commands:'
50 opt.separator ' backup: store rbot registry platform-independently in a file.'
51 opt.separator ' restore: restore rbot registry from such a file.'
53 opt.separator 'Options:'
55 opt.on('-t', '--type TYPE', TYPES, 'format to backup/restore. Values: %s.' % [TYPES.join(', ')]) do |type|
59 opt.on('-p', '--profile [PROFILE]', 'rbot profile directory. Defaults to: %s.' % options[:profile]) do |profile|
60 options[:profile] = profile
63 opt.on('-r', '--registry [REGISTRY]', 'registry-path to read/write, Optional, defaults to: <PROFILE>/registry_<TYPE>.') do |profile|
64 options[:registry] = profile
67 opt.on('-f', '--file [DBFILE]', 'cross-platform file to backup to/restore from. Defaults to: %s.' % options[:dbfile]) do |dbfile|
68 options[:dbfile] = dbfile
75 def initialize(profile, type, registry)
76 @profile = File.expand_path profile
79 puts 'Using type=%s profile=%s registry=%s' % [@type, @profile, @registry.inspect]
82 # returns a hash with the complete registry data
85 puts 'Found registry types: bdb=%d tc=%d dbm=%d daybreak=%d sqlite=%d' % [
86 listings[:bdb].length, listings[:tc].length,
87 listings[:dbm].length, listings[:daybreak].length, listings[:sqlite].length
89 if listings[@type].empty?
90 puts 'No suitable registry found!'
93 puts 'Using registry type: %s' % @type
98 print "~Reading... (this might take a moment)\r"
101 listing.each do |file|
103 data[file.key] = case @type
115 count += data[file.key].length
117 puts 'ERROR: <%s> %s' % [$!.class, $!]
119 puts 'Keep in mind that, even minor version differences of'
120 puts 'Barkeley DB or Tokyocabinet make files unreadable. Use this'
121 puts 'script on the exact same platform rbot was running!'
125 puts 'Read %d registry files, with %d entries.' % [data.length, count]
132 db = BDB::Hash.open(file.abs, nil, 'r')
134 db = BDB::Btree.open(file.abs, nil, 'r')
136 db.each do |key, value|
145 db = TokyoCabinet::BDB.new
146 db.open(file.abs, TokyoCabinet::BDB::OREADER)
147 db.each do |key, value|
155 db = DBM.open(file.abs.gsub(/\.[^\.]+$/,''), 0666, DBM::READER)
161 def read_daybreak(file)
163 db = Daybreak::DB.new(file.abs)
164 db.each do |key, value|
171 def read_sqlite(file)
173 db = SQLite3::Database.new(file.abs)
174 res = db.execute('SELECT key, value FROM data')
183 # searches in profile directory for existing registry formats
186 :bdb => list(get_registry, '*.db'),
187 :tc => list(get_registry('_tc'), '*.tdb'),
188 :dbm => list(get_registry('_dbm'), '*.*'),
189 :daybreak => list(get_registry('_daybreak'), '*.db'),
190 :sqlite => list(get_registry('_sqlite'), '*.db'),
194 def get_registry(suffix='')
196 File.expand_path(@registry)
198 File.join(@profile, 'registry'+suffix)
203 def initialize(folder, name)
206 @key = name.gsub(/\.[^\.]+$/,'')
208 attr_reader :folder, :name, :key
210 File.expand_path(File.join(@folder, @name))
217 def list(folder, ext='*.db')
218 return [] if not File.directory? folder
220 Dir.glob(File.join('**', ext)).map do |name|
221 RegistryFile.new(folder, name) if File.exists?(name)
227 class RestoreRegistry
228 def initialize(profile, type, registry)
229 @profile = File.expand_path profile
230 @registry = registry ? File.expand_path(registry) : nil
232 puts 'Using type=%s profile=%s' % [@type, @profile]
236 puts 'Using registry type: %s' % @type
237 folder = create_folder
238 print "~Restoring... (this might take a moment)\r"
239 data.each do |file, hash|
240 file = File.join(folder, file)
244 write_dbm(file, hash)
248 write_daybreak(file, hash)
250 write_sqlite(file, hash)
253 puts 'Restore successful! '
256 def write_dbm(file, data)
257 db = DBM.open(file, 0666, DBM::WRCREAT)
258 data.each_pair do |key, value|
264 def write_tc(file, data)
265 db = TokyoCabinet::BDB.new
266 db.open(file + '.tdb',
267 TokyoCabinet::BDB::OREADER |
268 TokyoCabinet::BDB::OCREAT |
269 TokyoCabinet::BDB::OWRITER)
270 data.each_pair do |key, value|
277 def write_daybreak(file, data)
278 db = Daybreak::DB.new(file + '.db')
279 data.each_pair do |key, value|
285 def write_sqlite(file, data)
286 db = SQLite3::Database.new(file + '.db')
287 db.execute('CREATE TABLE data (key PRIMARY_KEY, value)')
288 data.each_pair do |key, value|
289 db.execute('INSERT INTO data VALUES (?, ?)',
296 Dir.mkdir(@profile) unless File.directory?(@profile)
300 folder = File.join(@profile, 'registry_%s' % [@type.to_s])
302 Dir.mkdir(folder) unless File.directory?(folder)
303 if File.directory?(folder) and Dir.glob(File.join(folder, '**')).select{|f|File.file? f}.length>0
304 puts 'ERROR: Unable to restore!'
305 puts 'Restore folder exists and is not empty: ' + folder
311 # used to create subregistry folders
312 def create_subdir(path)
313 dirs = File.dirname(path).split('/')
314 dirs.length.times { |i|
315 dir = dirs[0,i+1].join("/")+"/"
316 unless File.exist?(dir)
324 if ARGV.length > 0 and options[:type].nil?
326 puts 'Missing Argument: -t [type]'
332 if File.exists? options[:dbfile]
333 puts 'Backup file already exists.'
337 reg = BackupRegistry.new(options[:profile], options[:type], options[:registry])
342 File.open(options[:dbfile], 'w') do |f|
343 f.write(Marshal.dump(data))
345 puts 'Written registry to ' + options[:dbfile]
349 unless File.exists? options[:dbfile]
350 puts 'Backup file does not exist.'
354 reg = RestoreRegistry.new(options[:profile], options[:type], options[:registry])
355 data = Marshal.load File.read(options[:dbfile])
357 puts 'Read %d registry files from backup file.' % data.length