jabber_data_migrator.rb [plain text]
#!/usr/bin/ruby -rubygems
# This script reads in a line-delimited list of jabberd 1.x XML spool files (1 filename per line) and migrates
# the data in each spool file to a SQLite database file for use with Jabberd 2.x
# The SQLite database file must already exist with the standard jaberd 2.x schema (as created by db-setup.sqlite)
#
# Copyright 2006 Apple Computer Inc. All rights reserved.
#
require "rexml/document"
require "getoptlong"
require "sqlite3"
def usage()
puts "\nusage: #{File.basename($0)} [opts] <input file> <output file>\n"
puts "Arguments:"
puts "-h, --help\tDisplay usage info"
puts "-d, --debug\tEnable debug mode"
puts "-r, --realm\tSpecify a realm to use for JIDs. Otherwise it will be taken from the input spool file path."
puts ""
puts "<input file>: Path of file containing a list of jabberd 1.x XML spool files for migration."
puts "\tThe file should contain filenames delimited by line endings. Ex:"
puts "\t\t user1.xml"
puts "\t\t user2.xml"
puts "\t\t user3.xml"
puts ""
puts "<output file>: Path of destination SQLite database file to be used as the data destination."
puts "\tThe file needs to have the same schema as used by the jabberd 2.x db-setup.sqlite script."
puts ""
end
def log_message(str)
logfile = File.new(@options['logpath'], 'a')
logstr = "#{Time.now}: #{File.basename($0)}: #{str}"
if @options['debug']
puts logstr
end
logfile.puts logstr
logfile.close
end
def bail(str)
log_message(str)
exit 1
end
def quote(str)
SQLite3::Database.quote(str)
end
def migrate_file(file)
db = SQLite3::Database.new( @options['dbfile'] )
db.busy_timeout(1000)
if @options['realm'] == "" then
if file =~ /^.*\/(.+)\/(.+).xml$/ then
realm = $1
username = $2
else
log_message("ERROR: Cannot get realm and/or username from paths provided, for line:\n\t#{file}")
db.close
return
end
else
if file =~ /^.*\/(.+).xml$/ then
realm = @options['realm']
username = $1
else
log_message("ERROR: Cannot get username from paths provided, for line: #{file}")
db.close
return
end
end
jid = username + "@" + realm
log_message("Processing file for user: #{jid}...")
if ! File.exist?(file) then
log_message("ERROR: File does not exist: #{file}")
db.close
return
end
spoolfile = File.new(file, 'r')
doc = REXML::Document.new(spoolfile)
query = "INSERT INTO active (`collection-owner`) VALUES ('#{quote(jid)}');"
log_message("Preparing query: #{query}")
prep = db.prepare(query)
prep.execute
prep.close
#puts db.prepare("INSERT INTO authreg (username, realm) VALUES ('#{username}', '#{realm}');")
doc.root.elements.each do |element|
qname = element.name
if element.namespace != nil then
qname = element.namespace + " " + qname
end
case qname
when "jabber:iq:auth:0k zerok"
#puts "UPDATE authreg SET hash = '#{element.elements["hash"].text}', token = '#{element.elements["token"].text}', " +
"sequence = #{element.elements["sequence"].text} WHERE username = '#{username}' AND realm = '#{realm}';"
when "jabber:iq:auth password"
#puts "UPDATE authreg SET `password` = '#{element.text.nil? ? '' : element.text}' " +
"WHERE username = '#{username}' AND realm = '#{realm}';"
when "jabber:iq:last query"
query = "INSERT INTO logout (`collection-owner`, time) VALUES ('#{quote(jid)}', #{quote(element.attributes["last"])});"
log_message("Preparing query: #{query}")
prep = db.prepare(query)
prep.execute
prep.close
when "jabber:iq:roster query"
element.elements.each("item") do |item|
item_subscription = item.attributes["subscription"]
if (item_subscription == "to" || item_subscription == "both") then
item_to = 1
else
item_to = 0
end
if (item_subscription == "from" || item_subscription == "both") then
item_from = 1
else
item_from = 0
end
if (item.attributes["ask"] == "subscribe") then
item_ask = 1
else
# Note: item_ask = 2 isn't possible since jabberd 1.4 doesn't store pending unsubscribe state.
item_ask = 0
end
item_jid = item.attributes["jid"]
item_name = item.attributes["name"]
if item_name then
query = "INSERT INTO `roster-items` (`collection-owner`, jid, name, `to`, `from`, ask) " +
"VALUES ('#{quote(jid)}', '#{quote(item_jid)}', '#{quote(item_name)}', " +
"#{quote(item_to.to_s)}, #{quote(item_from.to_s)}, #{quote(item_ask.to_s)});"
else
query = "INSERT INTO `roster-items` (`collection-owner`, jid, `to`, `from`, ask) " +
"VALUES ('#{quote(jid)}', '#{quote(item_jid)}', " +
"#{quote(item_to.to_s)}, #{quote(item_from.to_s)}, #{quote(item_ask.to_s)});"
end
log_message("Preparing query: #{query}")
prep = db.prepare(query)
prep.execute
prep.close
item.elements.each("group") do |group|
if (group.text) then
query = "INSERT INTO `roster-groups` (`collection-owner`, jid, `group`) " +
"VALUES ('#{quote(jid)}', '#{quote(item_jid)}', '#{quote(group.text)}');"
log_message("Preparing query: #{query}")
prep = db.prepare(query)
prep.execute
prep.close
end
end
end
when "jabber:x:offline foo"
element.elements.each("message") do |message|
query = "INSERT INTO queue (`collection-owner`, `xml`) VALUES ('#{quote(jid)}', '#{quote(message.to_s)}');"
log_message("Preparing query: #{query}")
prep = db.prepare(query)
prep.execute
prep.close
end
when "vcard-temp vCard", "vcard-temp vcard" # typo
query = "INSERT INTO vcard (`collection-owner`) VALUES ('#{quote(jid)}');"
log_message("Preparing query: #{query}")
prep = db.prepare(query)
prep.execute
prep.close
def vcard_iter
yield "fn", "FN"
yield "nickname", "NICKNAME"
yield "url", "URL"
yield "tel", "TEL/NUMBER"
yield "email", "EMAIL[USERID]/USERID"
yield "title", "TITLE"
yield "role", "ROLE"
yield "bday", "BDAY"
yield "desc", "DESC"
yield "n-given", "N/GIVEN"
yield "n-family", "N/FAMILY"
yield "adr-street", "ADR/STREET"
yield "adr-extadd", "ADR/EXTADD"
yield "adr-locality", "ADR/LOCALITY"
yield "adr-region", "ADR/REGION"
yield "adr-pcode", "ADR/PCODE"
yield "adr-country", "ADR/COUNTRY"
yield "org-orgname", "ORG/ORGNAME"
yield "org-orgunit", "ORG/ORGUNIT"
end
vcard_iter { |vcard_table_field, vcard_xpath|
vcard_field = element.elements[vcard_xpath]
if (vcard_field and vcard_field.text) then
query = "UPDATE vcard SET `#{vcard_table_field}` = '#{quote(vcard_field.text)}' " +
"WHERE `collection-owner` = '#{quote(jid)}';"
log_message("Preparing query: #{query}")
prep = db.prepare(query)
prep.execute
prep.close
end
}
else
if element.attributes["j_private_flag"] == "1" then
query = "INSERT INTO private (`collection-owner`, ns, xml) VALUES ('#{quote(jid)}', '#{quote(element.namespace)}', '#{quote(element.to_s)}');"
log_message("Preparing query: #{query}")
prep = db.prepare(query)
prep.execute
prep.close
else
# We ignore these because non-private arbitrary storage is out of the question.
end # if
end # case qname
end # doc.root.elements.each
db.close
end # def
###### MAIN
# Get options
opts = GetoptLong.new(
[ "--help", "-h", GetoptLong::NO_ARGUMENT ],
[ "--debug", "-d", GetoptLong::NO_ARGUMENT ],
[ "--realm", "-r", GetoptLong::REQUIRED_ARGUMENT ]
)
@options = {}
@options['debug'] = false
@options['realm'] = ""
@options['logpath'] = "/Library/Logs/Migration/jabbermigrator.log"
begin
opts.each do |opt, arg|
case opt
when '--help' || '-h'
usage()
exit 0
when '--debug' || '-d'
@options['debug'] = true
when '--realm' || '-r'
@options['realm'] = arg
end # case
end # opts.each
rescue GetoptLong::InvalidOption => msg
log_message("ERROR: Invalid Option")
usage()
exit 1
end # block
if ARGV[0].nil? || ARGV[1].nil? then
log_message("ERROR: You must supply both an input and output file.")
usage()
exit 1
end
@options['infile'] = ARGV[0]
@options['dbfile'] = ARGV[1]
# Read in the list of input files
@infiles = Array.new
infile = File.open(@options['infile'], "r")
while line = infile.gets
if line =~ /^.*\.xml$/
@infiles.push(line.chomp)
else
log_message("ERROR: line in input file does not have .xml file extension: #{line.chomp}")
end
end
log_message("Read filenames from input file:")
@infiles.each {|spoolfile| log_message(spoolfile)}
# check to make sure the sqlite destination file is writable
if ! File.writable?(@options['dbfile']) then
bail("ERROR: cannot write to file #{@options['dbfile']}")
end
@infiles.each { |spoolfile|
migrate_file(spoolfile)
}
log_message("Migration completed.")