#!/usr/bin/perl -w # This code is a part of Slash, and is released under the GPL. # Copyright 1997-2001 by Open Source Development Network. See README # and COPYING for more information, or see http://slashcode.com/. # $Id: slash2toslash2.2,v 1.1.2.18 2001/10/30 22:30:31 pudge Exp $ use strict; use File::Basename; use FindBin '$Bin'; use Getopt::Std; use DBIx::Password; use Digest::MD5 'md5_hex'; (my $VERSION) = ' $Revision: 1.1.2.18 $ ' =~ /\$Revision:\s+([^\s]+)/; my $PROGNAME = basename($0); (my $PREFIX = $Bin) =~ s|/[^/]+/?$||; my(%opts, %forced_plugins, %sacred_tables); my($del_users, $poll_uid, $process_menus); my(%topics, %discussions, %polls, %comments, %skip_polls, %poll_data); my(%vars, %story_authors); # Remember to doublecheck these match usage()! usage('Options used incorrectly') unless getopts('IMhvu:U:P:X:', \%opts); usage() if $opts{'h'}; version() if $opts{'v'}; usage('Need v2 virtual user') unless $opts{'U'}; usage('Need new virtual user') unless $opts{'u'}; $process_menus++ if $opts{'M'}; # Determine plugins to force conversion for. for (split /,/, ($opts{P} || '')) { $forced_plugins{$_} = 1; } # Determine tables to be left alone. for (split /,/, ($opts{X} || '')) { $sacred_tables{$_} = 1; } #################################### # disclaimer { # We don't ask this if we are in incremental-mode. last if $opts{I}; my $answer = ask(<<'EOT'); Slash 2.0.0 (Bender) to Slash 2.2.0 (Fry) Conversion Utility By running this I realize that there is no warranty, expressed or implied. Any data loss as a result of running this program is my responsibility. I have read the documentation for this program, I understand it, and I have taken the necessary precautions and done the required preparation. [yes/No]: EOT exit unless $answer eq 'yes'; } #################################### # setup my $dbh_old = DBIx::Password->connect($opts{'U'}); die "Can't open connection to old (v2) database!" unless $dbh_old; my $dbh_new = DBIx::Password->connect($opts{'u'}); die "Can't open connection to new database!" unless $dbh_new; END { $dbh_old->disconnect if $dbh_old; $dbh_new->disconnect if $dbh_new; } my @user_tables = qw( users users_comments users_info users_index users_prefs users_param ); # Run ONLY for plugins that were installed on both sites. my %plugins = ( Journal => { tables => [qw( journals journals_text journal_friends journal_themes )], }, Search => undef, ); # Run ONLY for plugins that were installed on both sites. my(%plugin_cleanup) = ( Journal => sub { $dbh_new->do(< sub { my $m_list = $dbh_old->selectcol_arrayref(<do(< undef, 'menus' => undef, 'tzcodes' => undef, 'users_comments' => undef, 'users_info' => undef, 'users_param' => undef, 'users_prefs' => undef, 'vars' => undef, # This is solely for fixup, we have NON NULL columns that were once # NULL. 'sections' => sub { my($data) = @_; delete $data->{artcount} if !defined($data->{artcount}); delete $data->{isolate} if !defined($data->{isolate}); delete $data->{issue} if !defined($data->{issue}); return $data; }, # Plugin tables. 'journals' => sub { my($data) = @_; $data->{description} ||= ''; $data->{discussion} = $data->{comment_header}; delete $data->{comment_header}; return $data; }, 'journals_text' => sub { my($data) = @_; $data->{article} ||= ''; return $data; }, 'journal_friends' => undef, 'journal_themes' => undef, # Processed tables. 'blocks' => sub { my($data) = @_; $data->{seclev} ||= 0; $data->{portal} ||= 0; $data->{retrieve} ||= 0; $data->{title} ||= ''; # If it's not an allowed enum, ditch it. delete $data->{type} if $data->{type} !~ /(static|color|portald)/; return $data; }, 'content_filters' => sub { my($data) = @_; delete $data->{maximum_length}; # Since this used to be "comment_filters" and then evolved into # "comtent_filters", we should be specific about what we # are filtering. The default is for 'comments' as The Beast # didn't have a concept of much more than that. $data->{form} = 'comments'; return $data; }, 'topics' => sub { my($data) = @_; $data->{name} = $data->{tid}; delete $data->{tid}; return $data; }, 'discussions' => sub { # Convert discussion primary key into a sequence. my($data, $story_hash) = @_; if (! $data->{url}) { printf <{sid}) ? "($data->{sid})":''; Discussion "$data->{title}" %s had null URL; skipping... EOT return; } if (! exists $story_hash->{$data->{sid}} && ! exists $polls{$data->{sid}}) { print <{sid}' is not a story or poll; skipping... EOT return; } # Program should accept a default topic for discussions. $data->{'topic'} = (defined($story_hash->{$data->{sid}}{tid})) ? $topics{$story_hash->{$data->{sid}}{tid}} : $vars{defaulttopic}; # Admins must disable stories via their own criterion. $data->{type} = 'open'; $data->{'ALLOWED_FIELDS'} = [qw( id sid title url topic ts type uid commentcount )]; return $data; }, 'users' => sub { my($data, $maillist) = @_; # Handle author flag conversion. $data->{author} = 1 if exists $story_authors{lc($data->{nickname})}; return $data; }, 'users_index' => sub { my($data) = @_; if ($data->{exaid}) { my @auth_uids; for (split /,/, $data->{exaid}) { push @auth_uids, $story_authors{$_} if $story_authors{$_}; } $data->{exaid} = join ',', @auth_uids; } return $data; }, 'stories' => sub { my($data, $hitlist, $storylist) = @_; # We need to COPY the data to the story list if writestatus # allows. my $datacopy; while (my($key, $value) = each %{$data}) { $datacopy->{$key} = $value; } delete $data->{aid}; $data->{tid} = $topics{$data->{tid}} || $vars{defaulttopic}; # Dammit, this relationship becomes CIRCULAR if looked at from # an incremental perspective! So for now we must load this info # somewhere else. # #$data->{discussion} = $discussions{$data->{sid}}; $data->{hits} = $hitlist->{$data->{sid}}{hits} || 0; # We only care about accurate representation of writestats when # we do a full import. if (!$opts{I}) { my $ws = $data->{writestatus}; $data->{writestatus} = 0; $data->{writestatus} = 'delete' if $ws==5 || $ws==10; $data->{writestatus} = 'dirty' if $ws==1; $data->{writestatus} ||= 'ok'; } else { $data->{writestatus} = 'dirty'; } push @{$storylist}, $datacopy if $storylist; # Use to properly assign topics to story-based discussions. #$story_topics{$data->{sid}} = $data->{tid}; my %newfields; map { $newfields{$_} = $dbh_new->quote($data->{$_}) } qw[sid introtext bodytext relatedtext]; $dbh_new->do(<{introtext}; delete $data->{bodytext}; delete $data->{relatedtext}; # extratext just goes awaaaayyy. delete $data->{extratext}; return $data; }, 'pollquestions' => sub { my($data, $poll_data) = @_; $data->{'discussion'} = $discussions{$data->{qid}}; $data->{'topic'} = $vars{defaulttopic}; $data->{'uid'} = $poll_uid; # QID is now SID and the new QID is now a serial key. $data->{'sid'} = $data->{'qid'}; $poll_data->{$data->{qid}} = $data if $poll_data; delete $data->{qid}; return $data; }, 'pollanswers' => sub { my($data) = @_; return if exists $skip_polls{$data->{qid}}; # Note AID here does means "ANSWER ID", not "author id". my $oldpoll = $data->{qid}; $data->{qid} = $polls{$oldpoll}; if (! $data->{qid}) { print < sub { my($data) = @_; $data->{qid} = $polls{$data->{qid}}; return $data; }, 'moderatorlog' => sub { my($data) = @_; my $sid = $data->{sid}; $data->{sid} = $discussions{$data->{sid}}; # We do not import records for comments without a discussion. return if !$data->{sid}; my $oldcid = $data->{cid}; $data->{cid} = $comments{$sid}->{$oldcid}; return $data if $data->{cid}; print < sub { my($data) = @_; $data->{tid} = $topics{$data->{tid}} if $data->{tid}; $data->{tid} ||= $vars{defaulttopic}; $data->{$_} ||= '' for qw(comment name email note story); return $data; }, ); # List the conditions necessary for incremental updates for all # updatable tables. # # Users have the same condition, so we encapsulate it here. my $usercond = { cond => 'uid > %ld', field => 'uid', type => 'int', }; # Each condition is an sprintf format string. my (%conditions) = ( 'abusers' => { cond => 'abuser_id > %ld', field => 'abuser_id', type => 'int', }, 'accesslog' => { cond => 'id > %ld', field => 'id', type => 'int', }, 'comments' => { cond => 'date > %s', field => 'date', type => 'date', }, 'discussions' => { cond => 'ts > %s', field => 'ts', type => 'date', }, #Ignoring hitters 'metamodlog' => { cond => 'id > %ld', field => 'id', type => 'int', }, 'moderatorlog' => { cond => 'id > %ld', field => 'id', type => 'int', }, 'pollquestions' => { cond => 'date > %s', field => 'date', type => 'date', }, #Stories will need special treatment. 'submissions' => { cond => 'time > %s', field => 'time', type => 'date', }, 'users' => $usercond, 'users_comments' => $usercond, 'users_index' => $usercond, 'users_info' => $usercond, 'users_prefs' => $usercond, ); #################################### # main body { my %tmpvars; load_vars($dbh_old, \%tmpvars); # Populate aid->uid author resolver. get_authors(); if ($opts{I}) { incremental(); # We're done. exit 0; } #################################### # Conversion Questions $del_users = ask(<<'EOT') eq 'yes'; Should we delete all rows from the existing users tables? If not, you should take steps to insure that no user collisions will result as the importer will otherwise attempt to merge the user databases, and a collision will cause a fatal error. [yes/No]: EOT if ($del_users) { for (@user_tables) { $dbh_new->do("DELETE FROM $_"); } } $poll_uid = ''; while ($poll_uid !~ /^\d+$/) { $poll_uid = ask(<{value}]: EOT $poll_uid = $tmpvars{anonymous_coward_uid}->{value} if !length($poll_uid); print "Invalid UID!\n\n" if $poll_uid !~ /^\d+$/; } convert(); } sub incremental { # Load vars from existing 2.2 site. load_vars($dbh_new); # Reload topic data for the rest of the show. reload_keys('topics', \%topics, 'tid', 'alttext'); # Stories. # # Determine list of authors, before story processing. update_stories(); # Polls. update('pollquestions', \%poll_data); reload_keys('pollquestions', \%polls, 'qid', 'date'); update_pollresults(\%poll_data); %skip_polls=(); # If you need to be SURE of consistent comment records, a full import is # your best bet, although incrementals should be "good enough". copy_comments(); %polls = (); %poll_data = (); # And the rest of the rabble. update('moderatorlog', 'id'); %discussions=(); %comments=(); for my $table (@user_tables, qw(submissions metamodlog)) { update($table); } %topics=(); users_keys(); } sub merge_vars { my (%oldvars, %newvars); local $" = '|'; print "Processing vars\n"; load_vars($dbh_new, \%newvars); load_vars($dbh_old, \%oldvars); # These vars are not replaced in the new setup. my @install_vars = qw( logdir basedir datadir siteid ); my $search = "^(@install_vars)\$"; for (keys %oldvars) { next if /$search/; $newvars{$_} = $oldvars{$_}; } $dbh_new->do('DELETE FROM vars'); for my $key (sort keys %newvars) { # Remember to set the global vars on merge! $vars{$key} = $newvars{$key}->{value}; map { $newvars{$key}->{$_} = $dbh_new->quote($newvars{$key}->{$_}) } qw(name value description); $dbh_new->do(<{name}, $newvars{$key}->{value}, $newvars{$key}->{description} ) EOT } } # This doesn't yet support incremental mode. sub convert_plugins { my($new_plugins, $old_plugins); $new_plugins = $dbh_new->selectcol_arrayref( 'SELECT value FROM site_info WHERE name="plugin"' ); $old_plugins = $dbh_old->selectcol_arrayref( 'SELECT value FROM site_info WHERE name="plugin"' ); my $printed = 0; my $alit; { local $" = '|'; $alit = "(@{$old_plugins})"; } for my $pi (@{$new_plugins}) { # Handle plugin fixup. if ($plugin_fixup{$pi}) { print "\nExecuting '$pi' plugin fixup.\n\n"; &{$plugin_fixup{$pi}}(); } # Handle plugin conversion and cleanup. if ($forced_plugins{$pi} || $pi =~ /^$alit$/) { if (! $printed) { print "\nConverting PLUGINS...\n"; $printed++; } if ($plugins{$pi} && ! $plugins{$pi}{tables}) { print <prepare('SELECT * FROM vars'); $sth->execute; while (my $rec = $sth->fetchrow_hashref) { # If VARHASH is passed, we create a hash of hashrefs based on # var name. if ($varhash) { $varhash->{$rec->{name}} = $rec; next; } # Otherwise we populate the global vars hash. $vars{$rec->{name}} = $rec->{value}; } } # Technically, this will work on any table with a non date/int key that # we want to update. Something to think about for the future. sub update_stories { my (@stories, %hitlist); map { s/^(database=)?([^;]+);?(.+)?$/$2/; } my($old_name, $new_name)=($dbh_old->{Name}, $dbh_new->{Name}); # There should be a DESTQUERY "DELETE FROM stories..." to remove stories # within the last two weeks. Then these stories (remember to grab SIDs!) # should be imported/updated and/or marked dirty for refreshing. my $datecond = <do("DELETE FROM stories WHERE time>=$datecond") if $opts{I}; # Update stories. my $sql = 'SELECT * FROM stories'; $sql .= " WHERE time >= $datecond" if $opts{I}; my $sth_s = $dbh_old->prepare($sql); $sth_s->execute; die "SQL: $sql\n" if $dbh_new->errstr; printf "Processing stories\n"; do_handle('stories', $sth_s, !$opts{I}, \%hitlist, \@stories); # Now go back and handle discussion updates. print "Reprocessing discussions and hitparade...\n"; # Now what we NEED to do here is flatten @stories into a temporary # hash keyed on SID so when discussions gets processed it doesn't take # forever to get the associated record. my %story_hash; for (@stories) { $story_hash{$_->{sid}} = $_; } my $cond = ''; if ($opts{I}) { $cond = "WHERE ts >= $datecond"; # Preserve min discussion ID of deleted records so we can # properly delete discussion_hitparade. my $sth_d = $dbh_new->prepare(<execute; my($min_did) = $sth_d->fetchrow_array; $deleted_discussions = $dbh_new->do("DELETE FROM discussions $cond"); $dbh_new->do(<= $min_did EOT } $sql = <prepare($sql); $sth_s->execute; die "SQL: $sql\n" if $dbh_old->errstr; # Need a list of the Discussion SIDs that are updated, here. printf "Processing discussions\n"; do_handle('discussions', $sth_s, 0, \%story_hash); reload_keys('discussions', \%discussions, 'sid', 'sid', 'id'); # Update stories with the proper discussion ID. for my $story (@stories) { # If an ARCHIVED STORY has a non-existant discussion ID, then # we must create one. if (! $discussions{$story->{sid}}) { $sql = <quote($story->{sid})]}, @{[$dbh_new->quote($story->{title})]}, '$vars{rootdir}/article.pl?sid=$story->{sid}', @{[$topics{$story->{tid}} || $vars{defaulttopic}]}, '$story->{time}', 'dirty' ) EOT $dbh_new->do($sql); die "SQL: $sql\n" if $dbh_new->errstr; $discussions{$story->{sid}} = getLastInsertID(); print <{title}" ($story->{sid}) as #$discussions{$story->{sid}} EOT } # Make SURE we update stories....damned circular relationships! $dbh_new->do(<{sid}} WHERE sid=@{[$dbh_new->quote($story->{sid})]} EOT } print <prepare(<quote($_)]} EOT $sth_s->execute; do_handle('pollanswers', $sth_s, $del); $sth_s = $dbh_old->prepare(<quote($_)]} EOT $sth_s->execute; do_handle('pollvoters', $sth_s, $del); # Create discussion for existing poll if it doesn't already # exist. if (! $discussions{$_}) { my $poll_url = "$vars{rootdir}/pollBooth.pl?qid=$_"; my $insert_q = <{question}, @{[$dbh_new->quote($poll_url)]}, $vars{defaulttopic}, $poll_data{$_}->{date}, 'dirty' ) EOT $dbh_new->do($insert_q); die "SQL: $insert_q\n" if $dbh_new->errstr; $discussions{$_} = getLastInsertID(); # Now have to update the poll! $dbh_new->do(<{question}" ($_/$discussions{$_}) EOT } # Only want del active the first time thru this, if it's active # at all. $del = 0 if $del; } # Make sure we get the latest poll. This should only return an array # with one value. Note that this is a TRANSFER of data, so use of # %vars here, is incorrect. my $newpoll_ar = $dbh_old->selectall_arrayref(<do(<[0][0]} WHERE name='currentqid' EOT } #################################### # Full conversion subroutine. sub convert { # Remember that the order of processing is CRITICAL. # First we merge vars between the two sites. See details in the # subroutine def. merge_vars(); # These are direct copy tables that don't depend on anything else. duplicate('content_filters', 1); duplicate('tzcodes', 'tz'); duplicate('menus', 1) if $process_menus; duplicate('topics', 1); reload_keys('topics', \%topics, 'tid', 'alttext'); duplicate('sections', 1); for (@user_tables) { duplicate($_, 0); } # Stories: Merge with storiestuff data, uid, aid, sid, tid # removal of all text fields into their own table (story_text). update_stories(); duplicate('pollquestions', 1, \%poll_data); reload_keys('pollquestions', \%polls, 'qid', 'date'); update_pollresults(\%poll_data, 1); # Done with polls by this point. %skip_polls = (); copy_comments(); %polls = (); %poll_data = (); # Moderatorlog: Fix sid, cid. duplicate('moderatorlog', 1); # Desperately needed, if you have a site the size of Slashdot, you're # probably running out of memory right about now. %discussions = (); %comments = (); # Submissions: Fix uid, tid duplicate('submissions', 1); # do all the blocks stuff duplicate('blocks', 'bid'); $dbh_new->do('INSERT INTO backup_blocks SELECT bid, block FROM blocks'); # Ignoring: formkeys, accesslog duplicate('metamodlog', 1); convert_plugins(); # CLEAN UP PHASE -- THE MISCELLANY OF QUERIES # # Deal with section_topics table (new for Fry). print "\nPerforming clean up...\n\n"; print "Constructing section_topics\n"; $dbh_new->do(<do(< 0 EOT # Construct author_cache table (new for Fry). print "Constructing authors_cache\n"; $dbh_new->do(<do(<do(<); print "\n"; return $answer; } sub load_storystuff { my ($hitlist) = @_; print "Loading hitcounts into story metadata...\n"; my $sth_s = $dbh_old->prepare('SELECT sid, hits FROM storiestuff'); $sth_s->execute; while (my $data = $sth_s->fetchrow_hashref) { $hitlist->{$data->{sid}}{hits} = $data->{hits}; } $sth_s->finish; } sub copy_comments { my %dead_discussions; my($lastdate, $cond) = ('', ''); # Is this sufficient or should this be SID based? my $datecond = <prepare("SELECT $datecond"); $sth_d->execute; my($display) = $sth_d->fetchrow_array; if ($opts{I}) { $cond = "WHERE date > $datecond"; print "Deleting stale comments...\n"; my(@delete_sids) = @{$dbh_new->selectall_arrayref(<[0] } @delete_sids; my $deleted_comm = $#delete_sids + 1; # We delete in batches to prevent the SQL buffer from # overflowing. my $batchsize = 100; LOOP: { do { my @batch = grep { defined($_) } @delete_sids[0 .. $batchsize-1]; local $" = ','; $dbh_new->do(<do("DELETE FROM comments $cond"); print "Copying comment data and texts from $display...\n"; } else { print "Copying comment data and texts...\n"; } my $sth_s = $dbh_old->prepare("SELECT * FROM comments $cond"); $sth_s->execute; LOOP: while (my $data = $sth_s->fetchrow_hashref) { next if exists $dead_discussions{$data->{sid}}; my $sid = $discussions{$data->{sid}}; if (! $sid) { $dead_discussions{$data->{sid}}++; print <{sid}' EOT next LOOP; } my $insert_d = scalar join(', ', map { $dbh_new->quote($data->{$_}) } qw [pid date subject uid points lastmod reason] ); my $insert_q = <do($insert_q); die "SQL: $insert_q\n" if $dbh_new->errstr; my $new_cid = getLastInsertID(); # This only needs to be done for comments that are "live". $comments{$data->{sid}}->{$data->{cid}} = $new_cid if $data->{date} ge $display; $data->{comment} = $dbh_new->quote($data->{comment}); my $sql = <{comment}) EOT $dbh_new->do($sql); } $sth_s->finish; # Now update PIDs. print "Updating comment heirarchy.\n"; $sth_s = $dbh_old->prepare(<execute; while (my $data = $sth_s->fetchrow_hashref) { next if !$data->{pid} || exists $dead_discussions{$data->{sid}}; my $pid = $comments{$data->{sid}}->{$data->{pid}} || 0; my $SQL = <{sid}} AND pid=$data->{pid} EOT $dbh_new->do($SQL); print STDERR <errstr; ($data->{sid}|$data->{pid}) SQL: $SQL EOT } # We need to grab the lowest discussion ID that we are INSERTING. # circular dependencies again (and the fact that there may be # dirty hitparade columns from the EXISTING data. $sth_s = $dbh_new->prepare(<= $datecond EOT $sth_s->execute; my($min_did) = $sth_s->fetchrow_array; # Now update discussion_hitparade... print "Updating story comment counts..."; $sth_d = $dbh_new->prepare(<execute(); printf "%s\n", ($sth_d->rows) ? (sprintf "%d rows", $sth_d->rows) : ''; while (my($id, $s_id) = $sth_d->fetchrow_array) { # We COULD take the hitparade from the story, but instead # we take it from COMMENTS because the data from the # story is probably dirty. my $sth_d1 = $dbh_new->prepare(<execute; while (my($val, $count) = $sth_d1->fetchrow_array) { $total += $count; $hitparade[$val - $vars{comment_minscore}] = $total; } $sth_d1->finish; for (qw(discussions stories)) { my $field = (/^stories$/) ? 'sid' : 'id'; my $insert_id = ($_ eq 'stories') ? $dbh_new->quote($s_id) : $id; my $hp = ''; $hp = ", hitparade='" . join(',', @hitparade) . "'" if $_ eq 'stories'; my $sql = <do($sql); die "SQL $sql\n" if $dbh_new->errstr; } } # Make sure we update discussion type. $dbh_new->do(<do( "UPDATE discussions SET flags='ok' WHERE flags='dirty'" ); } sub reload_keys { my($table, $key_tbl, $f1, $f2, $f3) = @_; $f3 ||= $f1; my(%old, %new); my $sth_s = $dbh_old->prepare("SELECT $f1, $f2 FROM $table"); $sth_s->execute; while (my $data = $sth_s->fetchrow_hashref) { $old{$data->{$f1}} = $data->{$f2}; }; $sth_s->finish; my $sth_d = $dbh_new->prepare("SELECT $f3, $f2 FROM $table"); $sth_d->execute; while (my $data = $sth_d->fetchrow_hashref) { $new{$data->{$f2}} = $data->{$f3}; } $sth_d->finish; for (keys %old) { $key_tbl->{$_} = $new{$old{$_}}; } } sub get_authors { my $sql = <prepare($sql); $sth_s->execute; my $err = $dbh_old->errstr; die "SQL: $sql\n" if $err; my @uidlist; while (my($uid, $auth) = $sth_s->fetchrow_array) { $story_authors{lc($auth)} = $uid; } } sub update { my($table, @opt) = @_; my($where, $cond); return if !$table; if (!exists $conditions{$table}) { print "X == SKIPPING update on '$table': no conditions\n"; return; } my $sql = <{field}) FROM $table EOT my $sth_d = $dbh_new->prepare($sql); $sth_d->execute(); my $err = $dbh_new->errstr; die "SQL ($table): $sql\n" if $err; my($max_id)= $sth_d->fetchrow_array(); $cond = sprintf $conditions{$table}->{cond}, $max_id if $conditions{$table}->{type} eq 'int'; $cond = sprintf $conditions{$table}->{cond}, $dbh_old->quote($max_id) if $conditions{$table}->{type} eq 'date'; my $sth_s = $dbh_old->prepare(<execute(); print "Updating $table...\n" if $sth_s->rows; do_handle($table, $sth_s, 0, @opt); } #################################### # the main function to copy data from old DB to new DB # if $opt is 1, delete table contents before inserting # if $opt is 0, do nothing with table before inserting # if $opt is a string, delete that specific column before inserting sub duplicate { my($table, $opt, @extra) = @_; my $filter = $conversions{$table}; if ($sacred_tables{$table}) { print "Not processing $table, as requested!\n"; return; } print "Processing $table\n"; my $sth_s = $dbh_old->prepare("SELECT * FROM $table"); $sth_s->execute; do_handle($table, $sth_s, $opt, @extra); } sub do_handle { my ($table, $sth, $opt, @extra) = @_; die "No conversion filter for table '$table'!" unless exists $conversions{$table}; if ($opt eq 1) { $dbh_new->do("DELETE FROM $table"); } my $filter = $conversions{$table}; while (my $data = $sth->fetchrow_hashref) { $data = $filter->($data, @extra) if ref $filter eq 'CODE'; next unless $data; # Remove any potential extraneous fields if specified. if (exists $data->{ALLOWED_FIELDS}) { my @fieldlist; push @fieldlist, @{$data->{ALLOWED_FIELDS}}; delete $data->{ALLOWED_FIELDS}; my $fl = join ('|', @fieldlist); map { delete $data->{$_} } grep { ! /^($fl)$/ } keys %{$data}; } map { $data->{$_} = (/^-/) ? $_:$dbh_new->quote($data->{$_}); } keys %$data; my $insert = sprintf("INSERT INTO $table (%s) VALUES (%s)", join(', ', map { s/^\-//; $_ } keys %$data), join(', ', values %$data) ); if ($opt =~ /[a-zA-Z]/) { $dbh_new->do(<{$opt} EOT } $dbh_new->do($insert); my $err = $dbh_new->errstr; die "$table: $err" if $err; # This hack might be a better way to do what reload_keys() # does and is required in a few places. # [unused, but let's leave it for now. It shouldn't hurt # anything as long as nothing refers to it] if (exists $data->{KEY_HASH}) { ($data->{KEY_HASH}{$data->{$data->{KEY_OLD}}}) = getLastInsertID(); } } } sub getLastInsertID { my($ret) = $dbh_new->selectall_arrayref("SELECT LAST_INSERT_ID()"); return $ret->[0][0]; } sub usage { print "*** $_[0]\n" if $_[0]; # Remember to doublecheck these match getopts()! print < Force presence (and conversion) of comma separated list of plugins: -X Do not convert any of the tables listed in comma separated list: EOT exit; } sub version { print <. You might want to read this section before running the program. Note that this is designed for converting a Slash 2.0.0 database; any schema changes you've made, or incompatible changes from earlier versions of Slash, may break this program. Please follow these instructions precisely to convert your Slash 2.0 site to Slash 2.2. =head2 Requirements =over 4 =item * You will need a new database to copy the new data to. This could be on the same database server as the Slash 2.0 data, but because the installed modules (specifically, Slash.pm) are not compatible between the sites, you will most likely want to have the installed code for 2.0 and 2.2 on separate boxes. =item * You will need Slash 2.2 installed on some box. =item * You will need access to both the 2.0 and 2.2 databases from the same box. The program will be run from the box that has Slash 2.2 installed, so if that box does not have access to the Slash 2.0 database, you will need to either temporarily edit the mysql.user table of the 2.0 server to grant access, or copy the database from the 2.0 server to the 2.2 server. =back =head2 BACK UP YOUR DATA If you lose your data, it is your problem, not ours. We've deleted data from sites, before, while preparing these upgrade programs. Fortunately we had backups ready to go. Back up your data on your Slash 2.0 database. You have been warned. Also, consider what happens if you have two Slash sites on one machine; what if you give this program the wrong virtual user? Perhaps you just deleted a working site! Back up any existing data on the target database server, too. See, even after writing this warning, we've been guilty and typed "slash" as the virtual user instead of the intended name, overwriting some of the existing database on some of our test sites! This program does not write to your Slash 2.0 database, so you should be fine, but there are no warranties, expressed or implied. If you are running a Slash site, you should be backing up your database nightly anyway, right? So backup all your data on both boxes, so you don't feel like a moron! =head2 Database Preparation You probably won't need to change any data. Here are some things to check before starting: =over 4 =item * Decide whether or not you want to keep the new admin account created in your Slash 2.2 database. Chances are, you won't want to or need to. However, if you do want to keep it, then you need to delete/modify all references to that user's uid in your Slash 2.0 database. It is easier if you just allow this program to delete that uid from your Slash 2.2 database, though, so there is no conflict. =back =head2 Install Slash 2.2 Slash Site Run the C program as described in that program's documentation. Remember which I you used to install the site. Do not make any changes to the database. =head2 Run It Run the program, using the proper value for virtual_user and the proper path to the slashdotrc.pl file: slash2toslash2 -U old_virtual_user -u virtual_user You will be asked if you want to delete the new admin user created by C (probably yes). =head2 Add Final Touches Copy over any images or static files you have, and adjust the site's templates, blocks, and variables as needed. =head1 DETAILS [Section should be updated!] This is just a detailed run-down of what the program does, in the order it does it. =over 4 =item * [ Section to be completed, later. ] =item * If you chose to delete the new admin user account created by C, it is deleted from each of the users* tables. If you choose not to, then UID 2 will not be copied from the 2.0 database; however, all stories, comments, etc. assigned to UID 2 I still be copied. We do not bother deleting comments and stories, because those will be overwritten later. =item * For the following tables, the contents are copied directly from the 1.0 database to the 2.2 database: [ Complete this list ] =item * The existing values from the 2.0 database vars table are copied to the 2.2 database. =item * Templates are NOT copied, and there is a good reason for this. Templates are going to be the most volatile element of your site's design, it's impossible for us to plan for what changes there may be so no specific upgrade path for templates exists except to update them by hand, as necessary. Use the template-check tool to compare templates before upgrading your site, and make whatever changes are necessary once you have your converted site back in place. I know it's a pain, but you'll be _much_ happier with the results. =back =head1 INCREMENTAL UPDATES If you invoke this program using the -I switch, an incremental conversion will be performed. The name implies exactly what it means, each table has a condition that will allow for incremental updates to be performed between a Beast site and a Fry site, which is useful for sites attempting to upgrade to Fry by using another system in tandem. An incremental update assumes that a previous conversion (or a working Slash site already exists at the database referred to by the specified virtual user. The following tables will remain UNTOUCHED in an incremental update: blocks code_param commentmodes content_filters dateformats menus sections templates topics tzcodes The above tables are generally part of a theme, and this should allow for a site\ admin to CHANGE the given theme without needing to reimport the entire dataset, if they so desire. =head1 VERSION $Id: slash2toslash2.2,v 1.1.2.18 2001/10/30 22:30:31 pudge Exp $