#
# migrate.rb
# ==========
#
# Migrate an I2 Wiki to Instiki. There must be only one 'book' in I2. A default
# 'Web' is created in Instiki for this, with a system password of "changeme",
# configured for Textile with CamelCase Wiki words disabled - only the
# '[[foo]]' syntax is allowed.
#
# Run this script using "script/runner" inside the target Instiki installation.
#
# Create a base class which establishes the alternative database connection.
# Update the connection details as required to read the source Wiki data.
class I2Database < ActiveRecord::Base
establish_connection(
"adapter"=>"postgresql",
"host"=>"/home/rool/postgres/",
"database"=>"i2",
"username"=>"rool",
"password"=>"set-password-here"
)
end
# We're going to use namespaced models to avoid name clashes below. This
# provokes problems with ActiveRecord; see:
#
# https://rails.lighthouseapp.com/projects/8994/tickets/2283-unnecessary-exception-raised-in-asdependenciesload_missing_constant
#
# The ugly but effective monkey patch given in the above ticket is applied
# below. More structural changes were eventually considered, but this is
# sufficient for our needs here.
module ActiveSupport
module Dependencies
extend self
#def load_missing_constant(from_mod, const_name)
def forgiving_load_missing_constant( from_mod, const_name )
begin
old_load_missing_constant(from_mod, const_name)
rescue ArgumentError => arg_err
if arg_err.message == "#{from_mod} is not missing constant #{const_name}!"
return from_mod.const_get(const_name)
else
raise
end
end
end
alias :old_load_missing_constant :load_missing_constant
alias :load_missing_constant :forgiving_load_missing_constant
end
end
# Create a module to avoid namespace clashes (with 'Page' in particular) -
# use "I2::Page" for an I2 page model, c.f. just "Page" for an Instiki model.
module I2
class Page < I2Database
set_table_name 'pages'
has_many :versions
end
class Version < I2Database
set_table_name 'versions'
belongs_to :author
belongs_to :page
end
class Author < I2Database
set_table_name 'authors'
has_many :versions
has_many :pages, :through => :versions
end
end
# Create the basic System and Web objects.
system = System.new
system.password = "changeme"
system.save!
web = Web.new
web.name = "Documentation"
web.address = "documentation"
web.additional_style = ""
web.allow_uploads = 1
web.published = 0
web.count_pages = 0
web.markup = "textile"
web.color = "008B26"
web.max_upload_size = 100
web.safe_mode = 0
web.brackets_only = 1
web.save!
# Given an I2::Version instance, return body text suitable for Instiki.
#
def process_body( i2version )
# Complications:
#
# I2 allowed a nil revision body for some of its Versions. Instiki fails
# if you have that, so make sure at least an empty string is present.
content = ( i2version.body ) || ''
# New RedCloth versions don't like it if you try to use an ID/anchor with
# capital letters, so use a rather permissive gsub to fix these - it
# risks catching other "#foo" cases than just anchor/IDs in links or
# definitions, but (at least in ROOL's case) that seems to be very rare
# (visual examination of about 1700 matches didn't spot any). We also
# need to change "+"s to "%20" - see the next complication for why.
content.gsub!(/\#[A-Za-z\-\+_]+/) { | str | str.downcase.gsub('+', '%20') }
# # Textilised references of the form "a+b+c" were interpreted as "a b c"
# # under I2 (i.e. "+" mapped to " ") but this is not the case under
# # Instiki. Replacing the "+"s with "%20" solves the problem albeit in an
# # ugly fashion - but it's less trouble than patching Instiki and having
# # difficulty with upgrades later.
#
# 2011-03-13 (ADH): Commented out as changes to the Page model within
# Instiki handle this case via a different method.
#
# content.gsub!(/((\"\:)|(\]))[A-Za-z0-9_\-\%]+\+([A-Za-z0-9_\-\%]+(\+)?)+/) { | str | str.gsub('+', '%20') }
# A common vertical spacing hack in the ROOL wiki to separate tables was
# to use
just above the table definition, but in new Textile any
# block element must have a blank line above it else the block text is
# rendered 'raw'. CSS improvements for the new Wiki make sure that such
# blocks don't vertically abut so the line breaks are useless - remove,
# but keep the line feed to make sure we don't let Textile block markup
# touch without an intervening blank line, provoking the problem we're
# trying to solve.
return content.gsub(/\n\
/, "\n")
end
# Set the ID of the Web all pages are to be placed into here; set the ID of the
# I2 Book to copy over here as well.
target_instiki_web = Web.first
target_instiki_web_id = target_instiki_web.id
source_i2_book_id = 1 # Get this e.g. from observation of SQL.
#Web.transaction do
begin
# Pages
# =====
#
# Instiki Page maps to I2 Page
#
# I2 field Instiki field
# ======================================
# title name
# created_at created_at
# updated_at updated_at
# book_id 1 (well, Web.first.id)
# locked_by and locked_at => nil or empty or whatever
#
# Revisons
# ========
#
# Instiki Version maps to I2 Revision
#
# I2 field Instiki field
# ======================================
# page_id mapped equivalent "page_id"
# author_id get author and write name to "author"
# created_at created_at
# body content
# book_id N/A (redundant; Revision has Web through Page)
# updated_at => copy created_at
# revised_at => copy created_at
# ip => Make up (e.g. 127.0.0.1)
page_count = 0
page_total = I2::Page.count
version_count = 0
version_total = I2::Version.count
wiki = Wiki.new
app = ActionController::Integration::Session.new
app.host = "www.riscosopen.org"
app.https!
PageRenderer.setup_url_generator( UrlGenerator.new( app ) )
I2::Page.all( :order => 'id ASC' ).each do | i2page |
next unless i2page.book_id == source_i2_book_id
# Create and save the equivalent Instiki page and revisions. I2 has a very
# shaky use of
i2versions = i2page.versions.find( :all, :order => 'created_at ASC' )
i2version = i2versions.try( :shift )
if ( i2version.nil? )
puts "WARNING: No versions found for page '#{ i2page.title }' (#{ i2page.id }) - skipping"
next
end
# Instiki assumes a name of "HomePage" for the default starting page in
# any given 'Web'.
name = i2page.title
name = "HomePage" if ( name == "Home Page" )
# I2 on the ROOL site has had one or two nasty hiccups wherein a page with
# a duplicate title was created. We can't handle that. Instiki just adds
# a new revision to whatever existing page has a matching title, even if
# you use its "write_page" call (see below). The best solution based on
# observation of the data at hand is to simply skip the duplicate page; the
# original page of a given title always takes precedence and any later
# same-name copies are ignored.
next unless ( Page.find_by_name( name ).nil? )
# Create the page and associated first revision.
begin
page_count += 1
version_count += 1
puts "Page #{ page_count } of #{ page_total } ('#{ name }')..."
wiki.write_page(
target_instiki_web.address,
name,
process_body( i2version ),
i2page.created_at,
Author.new( i2version.author.name, '127.0.0.1' ),
PageRenderer.new
)
rescue => e
puts "WARNING: Could not save I2 page ID #{ i2page.id }:"
puts e.message
next # Must not try to process other revisions since we have no page
end
# Now handle any other revisions.
i2versions.each do | i2version |
begin
version_count += 1
puts "version #{ version_count } of #{ version_total } done..."
wiki.revise_page(
target_instiki_web.address,
name,
name,
process_body( i2version ),
i2version.created_at,
Author.new( i2version.author.name, '127.0.0.1' ),
PageRenderer.new
)
rescue => e
puts "WARNING: Could not save I2 version ID #{ i2version.id }:"
puts e.message
end
end
end
puts "Finished"
end
# Done!