diff options
author | Ivan Kohler <ivan@freeside.biz> | 2015-07-09 22:18:55 -0700 |
---|---|---|
committer | Ivan Kohler <ivan@freeside.biz> | 2015-07-09 22:18:55 -0700 |
commit | 1c538bfabc2cd31f27067505f0c3d1a46cba6ef0 (patch) | |
tree | 96922ad4459eda1e649327fd391d60c58d454c53 /rt/lib/RT/Migrate | |
parent | 4f5619288413a185e9933088d9dd8c5afbc55dfa (diff) |
RT 4.2.11, ticket#13852
Diffstat (limited to 'rt/lib/RT/Migrate')
-rw-r--r-- | rt/lib/RT/Migrate/Importer.pm | 468 | ||||
-rw-r--r-- | rt/lib/RT/Migrate/Importer/File.pm | 208 | ||||
-rw-r--r-- | rt/lib/RT/Migrate/Incremental.pm | 657 | ||||
-rw-r--r-- | rt/lib/RT/Migrate/Serializer.pm | 492 | ||||
-rw-r--r-- | rt/lib/RT/Migrate/Serializer/File.pm | 171 | ||||
-rw-r--r-- | rt/lib/RT/Migrate/Serializer/IncrementalRecord.pm | 80 | ||||
-rw-r--r-- | rt/lib/RT/Migrate/Serializer/IncrementalRecords.pm | 69 |
7 files changed, 2145 insertions, 0 deletions
diff --git a/rt/lib/RT/Migrate/Importer.pm b/rt/lib/RT/Migrate/Importer.pm new file mode 100644 index 0000000..58ee632 --- /dev/null +++ b/rt/lib/RT/Migrate/Importer.pm @@ -0,0 +1,468 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +package RT::Migrate::Importer; + +use strict; +use warnings; + +use Storable qw//; +use File::Spec; +use Carp qw/carp/; + +sub new { + my $class = shift; + my $self = bless {}, $class; + $self->Init(@_); + return $self; +} + +sub Init { + my $self = shift; + my %args = ( + OriginalId => undef, + Progress => undef, + Statefile => undef, + DumpObjects => undef, + HandleError => undef, + @_, + ); + + # Should we attempt to preserve record IDs as they are created? + $self->{OriginalId} = $args{OriginalId}; + + $self->{Progress} = $args{Progress}; + + $self->{HandleError} = sub { 0 }; + $self->{HandleError} = $args{HandleError} + if $args{HandleError} and ref $args{HandleError} eq 'CODE'; + + if ($args{DumpObjects}) { + require Data::Dumper; + $self->{DumpObjects} = { map { $_ => 1 } @{$args{DumpObjects}} }; + } + + # Objects we've created + $self->{UIDs} = {}; + + # Columns we need to update when an object is later created + $self->{Pending} = {}; + + # Objects missing from the source database before serialization + $self->{Invalid} = []; + + # What we created + $self->{ObjectCount} = {}; + + # To know what global CFs need to be unglobal'd and applied to what + $self->{NewQueues} = []; + $self->{NewCFs} = []; +} + +sub Metadata { + my $self = shift; + return $self->{Metadata}; +} + +sub LoadMetadata { + my $self = shift; + my ($data) = @_; + + return if $self->{Metadata}; + $self->{Metadata} = $data; + + die "Incompatible format version: ".$data->{Format} + if $data->{Format} ne "0.8"; + + $self->{Organization} = $data->{Organization}; + $self->{Clone} = $data->{Clone}; + $self->{Incremental} = $data->{Incremental}; + $self->{Files} = $data->{Files} if $data->{Final}; +} + +sub InitStream { + my $self = shift; + + die "Stream initialized after objects have been recieved!" + if keys %{ $self->{UIDs} }; + + die "Cloning does not support importing the Original Id separately\n" + if $self->{OriginalId} and $self->{Clone}; + + die "RT already contains data; overwriting will not work\n" + if ($self->{Clone} and not $self->{Incremental}) + and RT->SystemUser->Id; + + # Basic facts of life, as a safety net + $self->Resolve( RT->System->UID => ref RT->System, RT->System->Id ); + $self->SkipTransactions( RT->System->UID ); + + if ($self->{OriginalId}) { + # Where to shove the original ticket ID + my $cf = RT::CustomField->new( RT->SystemUser ); + $cf->LoadByName( Name => $self->{OriginalId}, LookupType => RT::Ticket->CustomFieldLookupType, ObjectId => 0 ); + unless ($cf->Id) { + warn "Failed to find global CF named $self->{OriginalId} -- creating one"; + $cf->Create( + Queue => 0, + Name => $self->{OriginalId}, + Type => 'FreeformSingle', + ); + } + } +} + +sub Resolve { + my $self = shift; + my ($uid, $class, $id) = @_; + $self->{UIDs}{$uid} = [ $class, $id ]; + return unless $self->{Pending}{$uid}; + + for my $ref (@{$self->{Pending}{$uid}}) { + my ($pclass, $pid) = @{ $self->Lookup( $ref->{uid} ) }; + my $obj = $pclass->new( RT->SystemUser ); + $obj->LoadByCols( Id => $pid ); + $obj->__Set( + Field => $ref->{column}, + Value => $id, + ) if defined $ref->{column}; + $obj->__Set( + Field => $ref->{classcolumn}, + Value => $class, + ) if defined $ref->{classcolumn}; + $obj->__Set( + Field => $ref->{uri}, + Value => $self->LookupObj($uid)->URI, + ) if defined $ref->{uri}; + } + delete $self->{Pending}{$uid}; +} + +sub Lookup { + my $self = shift; + my ($uid) = @_; + unless (defined $uid) { + carp "Tried to lookup an undefined UID"; + return; + } + return $self->{UIDs}{$uid}; +} + +sub LookupObj { + my $self = shift; + my ($uid) = @_; + my $ref = $self->Lookup( $uid ); + return unless $ref; + my ($class, $id) = @{ $ref }; + + my $obj = $class->new( RT->SystemUser ); + $obj->Load( $id ); + return $obj; +} + +sub Postpone { + my $self = shift; + my %args = ( + for => undef, + uid => undef, + column => undef, + classcolumn => undef, + uri => undef, + @_, + ); + my $uid = delete $args{for}; + + if (defined $uid) { + push @{$self->{Pending}{$uid}}, \%args; + } else { + push @{$self->{Invalid}}, \%args; + } +} + +sub SkipTransactions { + my $self = shift; + my ($uid) = @_; + return if $self->{Clone}; + $self->{SkipTransactions}{$uid} = 1; +} + +sub ShouldSkipTransaction { + my $self = shift; + my ($uid) = @_; + return exists $self->{SkipTransactions}{$uid}; +} + +sub MergeValues { + my $self = shift; + my ($obj, $data) = @_; + for my $col (keys %{$data}) { + next if defined $obj->__Value($col) and length $obj->__Value($col); + next unless defined $data->{$col} and length $data->{$col}; + + if (ref $data->{$col}) { + my $uid = ${ $data->{$col} }; + my $ref = $self->Lookup( $uid ); + if ($ref) { + $data->{$col} = $ref->[1]; + } else { + $self->Postpone( + for => $obj->UID, + uid => $uid, + column => $col, + ); + next; + } + } + $obj->__Set( Field => $col, Value => $data->{$col} ); + } +} + +sub SkipBy { + my $self = shift; + my ($column, $class, $uid, $data) = @_; + + my $obj = $class->new( RT->SystemUser ); + $obj->Load( $data->{$column} ); + return unless $obj->Id; + + $self->SkipTransactions( $uid ); + + $self->Resolve( $uid => $class => $obj->Id ); + return $obj; +} + +sub MergeBy { + my $self = shift; + my ($column, $class, $uid, $data) = @_; + + my $obj = $self->SkipBy(@_); + return unless $obj; + $self->MergeValues( $obj, $data ); + return 1; +} + +sub Qualify { + my $self = shift; + my ($string) = @_; + return $string if $self->{Clone}; + return $string if not defined $self->{Organization}; + return $string if $self->{Organization} eq $RT::Organization; + return $self->{Organization}.": $string"; +} + +sub Create { + my $self = shift; + my ($class, $uid, $data) = @_; + + # Use a simpler pre-inflation if we're cloning + if ($self->{Clone}) { + $class->RT::Record::PreInflate( $self, $uid, $data ); + } else { + # Non-cloning always wants to make its own id + delete $data->{id}; + return unless $class->PreInflate( $self, $uid, $data ); + } + + my $obj = $class->new( RT->SystemUser ); + my ($id, $msg) = eval { + # catch and rethrow on the outside so we can provide more info + local $SIG{__DIE__}; + $obj->DBIx::SearchBuilder::Record::Create( + %{$data} + ); + }; + if (not $id or $@) { + $msg ||= ''; # avoid undef + my $err = "Failed to create $uid: $msg $@\n" . Data::Dumper::Dumper($data) . "\n"; + if (not $self->{HandleError}->($self, $err)) { + die $err; + } else { + return; + } + } + + $self->{ObjectCount}{$class}++; + $self->Resolve( $uid => $class, $id ); + + # Load it back to get real values into the columns + $obj = $class->new( RT->SystemUser ); + $obj->Load( $id ); + $obj->PostInflate( $self ); + + return $obj; +} + +sub ReadStream { + my $self = shift; + my ($fh) = @_; + + no warnings 'redefine'; + local *RT::Ticket::Load = sub { + my $self = shift; + my $id = shift; + $self->LoadById( $id ); + return $self->Id; + }; + + my $loaded = Storable::fd_retrieve($fh); + + # Metadata is stored at the start of the stream as a hashref + if (ref $loaded eq "HASH") { + $self->LoadMetadata( $loaded ); + $self->InitStream; + return; + } + + my ($class, $uid, $data) = @{$loaded}; + + if ($self->{Incremental}) { + my $obj = $class->new( RT->SystemUser ); + $obj->Load( $data->{id} ); + if (not $uid) { + # undef $uid means "delete it" + $obj->Delete; + $self->{ObjectCount}{$class}++; + } elsif ( $obj->Id ) { + # If it exists, update it + $class->RT::Record::PreInflate( $self, $uid, $data ); + $obj->__Set( Field => $_, Value => $data->{$_} ) + for keys %{ $data }; + $self->{ObjectCount}{$class}++; + } else { + # Otherwise, make it + $obj = $self->Create( $class, $uid, $data ); + } + $self->{Progress}->($obj) if $obj and $self->{Progress}; + return; + } elsif ($self->{Clone}) { + my $obj = $self->Create( $class, $uid, $data ); + $self->{Progress}->($obj) if $obj and $self->{Progress}; + return; + } + + # If it's a queue, store its ID away, as we'll need to know + # it to split global CFs into non-global across those + # fields. We do this before inflating, so that queues which + # got merged still get the CFs applied + push @{$self->{NewQueues}}, $uid + if $class eq "RT::Queue"; + + my $origid = $data->{id}; + my $obj = $self->Create( $class, $uid, $data ); + return unless $obj; + + # If it's a ticket, we might need to create a + # TicketCustomField for the previous ID + if ($class eq "RT::Ticket" and $self->{OriginalId}) { + my ($id, $msg) = $obj->AddCustomFieldValue( + Field => $self->{OriginalId}, + Value => $self->Organization . ":$origid", + RecordTransaction => 0, + ); + warn "Failed to add custom field to $uid: $msg" + unless $id; + } + + # If it's a CF, we don't know yet if it's global (the OCF + # hasn't been created yet) to store away the CF for later + # inspection + push @{$self->{NewCFs}}, $uid + if $class eq "RT::CustomField" + and $obj->LookupType =~ /^RT::Queue/; + + $self->{Progress}->($obj) if $self->{Progress}; +} + +sub CloseStream { + my $self = shift; + + $self->{Progress}->(undef, 'force') if $self->{Progress}; + + return if $self->{Clone}; + + # Take global CFs which we made and make them un-global + my @queues = grep {$_} map {$self->LookupObj( $_ )} @{$self->{NewQueues}}; + for my $obj (map {$self->LookupObj( $_ )} @{$self->{NewCFs}}) { + my $ocf = $obj->IsGlobal or next; + $ocf->Delete; + $obj->AddToObject( $_ ) for @queues; + } + $self->{NewQueues} = []; + $self->{NewCFs} = []; +} + + +sub ObjectCount { + my $self = shift; + return %{ $self->{ObjectCount} }; +} + +sub Missing { + my $self = shift; + return wantarray ? sort keys %{ $self->{Pending} } + : keys %{ $self->{Pending} }; +} + +sub Invalid { + my $self = shift; + return wantarray ? sort { $a->{uid} cmp $b->{uid} } @{ $self->{Invalid} } + : $self->{Invalid}; +} + +sub Organization { + my $self = shift; + return $self->{Organization}; +} + +sub Progress { + my $self = shift; + return defined $self->{Progress} unless @_; + return $self->{Progress} = $_[0]; +} + +1; diff --git a/rt/lib/RT/Migrate/Importer/File.pm b/rt/lib/RT/Migrate/Importer/File.pm new file mode 100644 index 0000000..176bc26 --- /dev/null +++ b/rt/lib/RT/Migrate/Importer/File.pm @@ -0,0 +1,208 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +package RT::Migrate::Importer::File; + +use strict; +use warnings; +use base qw(RT::Migrate::Importer); + +sub Init { + my $self = shift; + my %args = ( + Directory => undef, + Resume => undef, + @_ + ); + + # Directory is required + die "Directory is required" unless $args{Directory}; + die "Invalid path $args{Directory}" unless -d $args{Directory}; + $self->{Directory} = $args{Directory}; + + # Load metadata, if present + if (-e "$args{Directory}/rt-serialized") { + my $dat = eval { Storable::retrieve("$args{Directory}/rt-serialized"); } + or die "Failed to load metadata" . ($@ ? ": $@" : ""); + $self->LoadMetadata($dat); + } + + # Support resuming + $self->{Statefile} = $args{Statefile} || "$args{Directory}/partial-import"; + unlink $self->{Statefile} + if -f $self->{Statefile} and not $args{Resume}; + + return $self->SUPER::Init(@_); +} + +sub Import { + my $self = shift; + my $dir = $self->{Directory}; + + if ($self->{Metadata} and $self->{Metadata}{Files}) { + $self->{Files} = [ map {s|^.*/|$dir/|;$_} @{$self->{Metadata}{Files}} ]; + } else { + $self->{Files} = [ <$dir/*.dat> ]; + } + $self->{Files} = [ map {File::Spec->rel2abs($_)} @{ $self->{Files} } ]; + + $self->RestoreState( $self->{Statefile} ); + + local $SIG{ INT } = sub { $self->{INT} = 1 }; + local $SIG{__DIE__} = sub { warn "\n", @_; $self->SaveState; exit 1 }; + + $self->{Progress}->(undef) if $self->{Progress}; + while (@{$self->{Files}}) { + $self->{Filename} = shift @{$self->{Files}}; + open(my $fh, "<", $self->{Filename}) + or die "Can't read $self->{Filename}: $!"; + if ($self->{Seek}) { + seek($fh, $self->{Seek}, 0) + or die "Can't seek to $self->{Seek} in $self->{Filename}"; + $self->{Seek} = undef; + } + while (not eof($fh)) { + $self->{Position} = tell($fh); + + # Stop when we're at a good stopping point + die "Caught interrupt, quitting.\n" if $self->{INT}; + + $self->ReadStream( $fh ); + } + } + + $self->CloseStream; + + # Return creation counts + return $self->ObjectCount; +} + +sub List { + my $self = shift; + my $dir = $self->{Directory}; + + my %found = ( "RT::System" => 1 ); + my @files = ($self->{Metadata} and $self->{Metadata}{Files}) ? + @{ $self->{Metadata}{Files} } : <$dir/*.dat>; + @files = map {File::Spec->rel2abs($_)} @files; + + for my $filename (@files) { + open(my $fh, "<", $filename) + or die "Can't read $filename: $!"; + while (not eof($fh)) { + my $loaded = Storable::fd_retrieve($fh); + if (ref $loaded eq "HASH") { + $self->LoadMetadata( $loaded ); + next; + } + + if ($self->{DumpObjects}) { + print STDERR Data::Dumper::Dumper($loaded), "\n" + if $self->{DumpObjects}{ $loaded->[0] }; + } + + my ($class, $uid, $data) = @{$loaded}; + $self->{ObjectCount}{$class}++; + $found{$uid} = 1; + delete $self->{Pending}{$uid}; + for (grep {ref $data->{$_}} keys %{$data}) { + my $uid_ref = ${ $data->{$_} }; + unless (defined $uid_ref) { + push @{ $self->{Invalid} }, { uid => $uid, column => $_ }; + next; + } + next if $found{$uid_ref}; + next if $uid_ref =~ /^RT::Principal-/; + push @{$self->{Pending}{$uid_ref} ||= []}, {uid => $uid}; + } + } + } + + return $self->ObjectCount; +} + +sub RestoreState { + my $self = shift; + my ($statefile) = @_; + return unless $statefile && -f $statefile; + + my $state = Storable::retrieve( $self->{Statefile} ); + $self->{$_} = $state->{$_} for keys %{$state}; + unlink $self->{Statefile}; + + print STDERR "Resuming partial import...\n"; + sleep 2; + return 1; +} + +sub SaveState { + my $self = shift; + + my %data; + unshift @{$self->{Files}}, $self->{Filename}; + $self->{Seek} = $self->{Position}; + $data{$_} = $self->{$_} for + qw/Filename Seek Position Files + Organization ObjectCount + NewQueues NewCFs + SkipTransactions Pending Invalid + UIDs + OriginalId Clone + /; + Storable::nstore(\%data, $self->{Statefile}); + + print STDERR <<EOT; + +Importer state has been written to the file: + $self->{Statefile} + +It may be possible to resume the import by re-running rt-importer. +EOT +} + +1; diff --git a/rt/lib/RT/Migrate/Incremental.pm b/rt/lib/RT/Migrate/Incremental.pm new file mode 100644 index 0000000..61aea6c --- /dev/null +++ b/rt/lib/RT/Migrate/Incremental.pm @@ -0,0 +1,657 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +package RT::Migrate::Incremental; + +use strict; +use warnings; +require Storable; +require MIME::Base64; + +our %UPGRADES = ( + '3.3.0' => { + 'RT::Transaction' => sub { + my ($ref) = @_; + $ref->{ObjectType} = 'RT::Ticket'; + $ref->{ObjectId} = delete $ref->{Ticket}; + delete $ref->{EffectiveTicket}; + }, + 'RT::TicketCustomFieldValue' => sub { + my ($ref, $classref) = @_; + $$classref = "RT::ObjectCustomFieldValue"; + $ref->{ObjectType} = 'RT::Ticket'; + $ref->{ObjectId} = delete $ref->{Ticket}; + }, + '-RT::TicketCustomFieldValue' => sub { + my ($ref, $classref) = @_; + $$classref = "RT::ObjectCustomFieldValue"; + }, + 'RT::CustomField' => sub { + my ($ref) = @_; + $ref->{MaxValues} = 0 if $ref->{Type} =~ /Multiple$/; + $ref->{MaxValues} = 1 if $ref->{Type} =~ /Single$/; + $ref->{Type} = 'Select' if $ref->{Type} =~ /^Select/; + $ref->{Type} = 'Freeform' if $ref->{Type} =~ /^Freeform/; + $ref->{LookupType} = 'RT::Queue-RT::Ticket'; + delete $ref->{Queue}; + }, + '+RT::CustomField' => sub { + my ($ref) = @_; + return [ + "RT::ObjectCustomField" => rand(1), + { + id => undef, + CustomField => $ref->{id}, + ObjectId => $ref->{Queue}, + SortOrder => $ref->{SortOrder}, + Creator => $ref->{Creator}, + LastUpdatedBy => $ref->{LastUpdatedBy}, + } + ]; + } + }, + + '3.3.11' => { + 'RT::ObjectCustomFieldValue' => sub { + my ($ref) = @_; + $ref->{Disabled} = not delete $ref->{Current}; + }, + }, + + '3.7.19' => { + 'RT::Scrip' => sub { + my ($ref) = @_; + return if defined $ref->{Description} and length $ref->{Description}; + + my $scrip = RT::Scrip->new( $RT::SystemUser ); + $scrip->Load( $ref->{id} ); + my $condition = $scrip->ConditionObj->Name + || $scrip->ConditionObj->Description + || ('On Condition #'. $scrip->Condition); + my $action = $scrip->ActionObj->Name + || $scrip->ActionObj->Description + || ('Run Action #'. $scrip->Action); + $ref->{Description} = join ' ', $condition, $action; + }, + }, + + # XXX BrandedQueues + # XXX iCal + + '3.8.2' => { + 'RT::Template' => sub { + my ($ref) = @_; + return unless $ref->{Queue}; + + my $queue = RT::Queue->new( $RT::SystemUser ); + $queue->Load( $ref->{Queue} ); + return unless $queue->Id and $queue->Name eq "___Approvals"; + + $ref->{Name} = "[OLD] ".$ref->{Name}; + }, + 'RT::Attribute' => sub { + my ($ref) = @_; + return unless $ref->{Name} eq "Dashboard"; + + my $v = eval { + Storable::thaw(MIME::Base64::decode_base64($ref->{Content})) + }; + return unless $v and exists $v->{Searches}; + $v->{Panes} = { + body => [ + map { + my ($privacy, $id, $desc) = @$_; + +{ + portlet_type => 'search', + privacy => $privacy, + id => $id, + description => $desc, + pane => 'body', + } + } @{ delete $v->{Searches} } + ], + }; + $ref->{Content} = MIME::Base64::encode_base64( + Storable::nfreeze($v) ); + }, + 'RT::Scrip' => sub { + my ($ref, $classref) = @_; + return unless $ref->{Queue}; + + my $queue = RT::Queue->new( $RT::SystemUser ); + $queue->Load( $ref->{Queue} ); + return unless $queue->Id and $queue->Name eq "___Approvals"; + + $$classref = undef; + }, + }, + + '3.8.3' => { + 'RT::ScripAction' => sub { + my ($ref) = @_; + return unless ($ref->{Argument}||"") eq "All"; + if ($ref->{ExecModule} eq "Notify") { + $ref->{Name} = 'Notify Owner, Requestors, Ccs and AdminCcs'; + $ref->{Description} = 'Send mail to owner and all watchers'; + } elsif ($ref->{ExecModule} eq "NotifyAsComment") { + $ref->{Name} = 'Notify Owner, Requestors, Ccs and AdminCcs as Comment'; + $ref->{Description} = 'Send mail to owner and all watchers as a "comment"'; + } + }, + }, + + '3.8.4' => { + 'RT::ScripAction' => sub { + my ($ref) = @_; + return unless $ref->{ExecModule} eq "NotifyGroup" + or $ref->{ExecModule} eq "NotifyGroupAsComment"; + + my $argument = $ref->{Argument}; + if ( my $struct = eval { Storable::thaw( $argument ) } ) { + my @res; + foreach my $r ( @{ $struct } ) { + my $obj; + next unless $r->{'Type'}; + if( lc $r->{'Type'} eq 'user' ) { + $obj = RT::User->new( $RT::SystemUser ); + } elsif ( lc $r->{'Type'} eq 'group' ) { + $obj = RT::Group->new( $RT::SystemUser ); + } else { + next; + } + $obj->Load( $r->{'Instance'} ); + next unless $obj->id ; + + push @res, $obj->id; + } + $ref->{Argument} = join ",", @res; + } else { + $ref->{Argument} = join ",", grep length, split /[^0-9]+/, $argument; + } + }, + }, + + '3.8.8' => { + 'RT::ObjectCustomField' => sub { + # XXX Removing OCFs applied both global and non-global + # XXX Fixing SortOrder on OCFs + }, + }, + + '3.8.9' => { + 'RT::Link' => sub { + my ($ref) = @_; + my $prefix = RT::URI::fsck_com_rt->LocalURIPrefix . '/ticket/'; + for my $dir (qw(Target Base)) { + next unless $ref->{$dir} =~ /^$prefix(.*)/; + next unless int($1) eq $1; + next if $ref->{'Local'.$dir}; + $ref->{'Local'.$dir} = $1; + } + }, + 'RT::Template' => sub { + my ($ref) = @_; + + return unless $ref->{Name} =~ + /^(All Approvals Passed|Approval Passed|Approval Rejected)$/; + + my $queue = RT::Queue->new( $RT::SystemUser ); + $queue->Load( $ref->{Queue} ); + return unless $queue->Id and $queue->Name eq "___Approvals"; + + $ref->{Content} =~ +s!(?<=Your ticket has been (?:approved|rejected) by \{ eval \{ )\$Approval->OwnerObj->Name!\$Approver->Name!; + }, + }, + + '3.9.1' => { + 'RT::Template' => sub { + my ($ref) = @_; + $ref->{Type} = 'Perl'; + }, + # XXX: Add ExecuteCode to principals that currently have ModifyTemplate or ModifyScrips + }, + + '3.9.2' => { + 'RT::ACE' => sub { + my ($ref, $classref) = @_; + $$classref = undef if $ref->{DelegatedBy} > 0 + or $ref->{DelegatedFrom} > 0; + }, + + 'RT::GroupMember' => sub { + my ($ref, $classref) = @_; + my $group = RT::Group->new( $RT::SystemUser ); + $group->Load( $ref->{GroupId} ); + $$classref = undef if $group->Domain eq "Personal"; + }, + 'RT::Group' => sub { + my ($ref, $classref) = @_; + $$classref = undef if $ref->{Domain} eq "Personal"; + }, + 'RT::Principal' => sub { + my ($ref, $classref) = @_; + return unless $ref->{PrincipalType} eq "Group"; + my $group = RT::Group->new( $RT::SystemUser ); + $group->Load( $ref->{ObjectId} ); + $$classref = undef if $group->Domain eq "Personal"; + }, + }, + + '3.9.3' => { + 'RT::ACE' => sub { + my ($ref) = @_; + delete $ref->{DelegatedBy}; + delete $ref->{DelegatedFrom}; + }, + }, + + '3.9.5' => { + 'RT::CustomFieldValue' => sub { + my ($ref) = @_; + my $attr = RT::Attribute->new( $RT::SystemUser ); + $attr->LoadByCols( + ObjectType => "RT::CustomFieldValue", + ObjectId => $ref->{Id}, + Name => "Category", + ); + $ref->{Category} = $attr->Content if $attr->id; + }, + 'RT::Attribute' => sub { + my ($ref, $classref) = @_; + $$classref = undef if $ref->{Name} eq "Category" + and $ref->{ObjectType} eq "RT::CustomFieldValue"; + }, + }, + + '3.9.7' => { + 'RT::User' => sub { + my ($ref) = @_; + my $attr = RT::Attribute->new( $RT::SystemUser ); + $attr->LoadByCols( + ObjectType => "RT::User", + ObjectId => $ref->{id}, + Name => "AuthToken", + ); + $ref->{AuthToken} = $attr->Content if $attr->id; + }, + 'RT::CustomField' => sub { + my ($ref) = @_; + for my $name (qw/RenderType BasedOn ValuesClass/) { + my $attr = RT::Attribute->new( $RT::SystemUser ); + $attr->LoadByCols( + ObjectType => "RT::CustomField", + ObjectId => $ref->{id}, + Name => $name, + ); + $ref->{$name} = $attr->Content if $attr->id; + } + }, + 'RT::Queue' => sub { + my ($ref) = @_; + my $attr = RT::Attribute->new( + ObjectType => "RT::System", + ObjectId => 1, + Name => "BrandedSubjectTag", + );; + return unless $attr->id; + my $map = $attr->Content || {}; + return unless $map->{$ref->{id}}; + $ref->{SubjectTag} = $map->{$ref->{id}}; + }, + 'RT::Attribute' => sub { + my ($ref, $classref) = @_; + if ($ref->{ObjectType} eq "RT::User" and $ref->{Name} eq "AuthToken") { + $$classref = undef; + } elsif ($ref->{ObjectType} eq "RT::CustomField" and $ref->{Name} eq "RenderType") { + $$classref = undef; + } elsif ($ref->{ObjectType} eq "RT::CustomField" and $ref->{Name} eq "BasedOn") { + $$classref = undef; + } elsif ($ref->{ObjectType} eq "RT::CustomField" and $ref->{Name} eq "ValuesClass") { + $$classref = undef; + } elsif ($ref->{ObjectType} eq "RT::System" and $ref->{Name} eq "BrandedSubjectTag") { + $$classref = undef; + } + }, + }, + + '3.9.8' => { + # XXX RTFM => Articles + }, + + '4.0.0rc7' => { + 'RT::Queue' => sub { + my ($ref) = @_; + return unless $ref->{Name} eq '___Approvals'; + $ref->{Lifecycle} = "approvals"; + }, + }, + + '4.0.1' => { + 'RT::ACE' => sub { + my ($ref, $classref) = @_; + my $group = RT::Group->new( $RT::SystemUser ); + $group->LoadByCols( + id => $ref->{PrincipalId}, + Domain => "Personal", + ); + $$classref = undef if $group->id; + $$classref = undef if $ref->{RightName} =~ + /^(AdminOwnPersonalGroups|AdminAllPersonalGroups|DelegateRights)$/; + $$classref = undef if $ref->{RightName} =~ + /^(RejectTicket|ModifyTicketStatus)$/; + }, + }, + + '4.0.4' => { + 'RT::Template' => sub { + my ($ref) = @_; + $ref->{Type} ||= 'Perl'; + }, + }, + + '4.0.6' => { + 'RT::Transaction' => sub { + my ($ref) = @_; + return unless $ref->{ObjectType} eq "RT::User" and $ref->{Field} eq "Password"; + $ref->{OldValue} = $ref->{NewValue} = '********'; + }, + }, + + '4.0.9' => { + 'RT::Queue' => sub { + my ($ref) = @_; + $ref->{Lifecycle} ||= 'default'; + }, + }, + + '4.0.19' => { + 'RT::CustomField' => sub { + my ($ref) = @_; + $ref->{LookupType} = 'RT::Class-RT::Article' + if $ref->{LookupType} eq 'RT::FM::Class-RT::FM::Article'; + }, + 'RT::ObjectCustomFieldValue' => sub { + my ($ref) = @_; + $ref->{ObjectType} = 'RT::Article' + if $ref->{ObjectType} eq 'RT::FM::Article'; + }, + }, + + + '4.1.0' => { + 'RT::Attribute' => sub { + my ($ref) = @_; + return unless $ref->{Name} eq "HomepageSettings"; + + my $v = eval { + Storable::thaw(MIME::Base64::decode_base64($ref->{Content})) + }; + return if not $v or $v->{sidebar}; + $v->{sidebar} = delete $v->{summary}; + $ref->{Content} = MIME::Base64::encode_base64( + Storable::nfreeze($v) ); + }, + }, + + '4.1.1' => { + '+RT::Scrip' => sub { + my ($ref) = @_; + my $new = [ + "RT::ObjectScrip" => rand(1), + { + id => undef, + Scrip => $ref->{id}, + Stage => delete $ref->{Stage}, + ObjectId => delete $ref->{Queue}, + Creator => $ref->{Creator}, + Created => $ref->{Created}, + LastUpdatedBy => $ref->{LastUpdatedBy}, + LastUpdated => $ref->{LastUpdated}, + } + ]; + if ( $new->[2]{Stage} eq "Disabled" ) { + $ref->{Disabled} = 1; + $new->[2]{Stage} = "TransactionCreate"; + } else { + $ref->{Disabled} = 0; + } + # XXX SortOrder + return $new; + }, + }, + + '4.1.4' => { + 'RT::Group' => sub { + my ($ref) = @_; + $ref->{Instance} = 1 + if $ref->{Domain} eq "RT::System-Role" + and $ref->{Instance} = 0; + }, + # XXX Invalid rights + }, + + '4.1.5' => { + 'RT::Scrip' => sub { + my ($ref) = @_; + my $template = RT::Template->new( $RT::SystemUser ); + $template->Load( $ref->{Template} ); + $ref->{Template} = $template->id ? $template->Name : 'Blank'; + }, + }, + + '4.1.6' => { + 'RT::Attribute' => sub { + my ($ref) = @_; + return unless $ref->{Name} eq RT::User::_PrefName( RT->System ) + and $ref->{ObjectType} eq "RT::User"; + my $v = eval { + Storable::thaw(MIME::Base64::decode_base64($ref->{Content})) + }; + return if not $v or $v->{ShowHistory}; + $v->{ShowHistory} = delete $v->{DeferTransactionLoading} + ? "click" : "delay"; + $ref->{Content} = MIME::Base64::encode_base64( + Storable::nfreeze($v) ); + }, + }, + + '4.1.7' => { + 'RT::Transaction' => sub { + my ($ref) = @_; + return unless $ref->{ObjectType} eq 'RT::Ticket' + and $ref->{Type} eq 'Set' + and $ref->{Field} eq 'TimeWorked'; + $ref->{TimeTaken} = $ref->{NewValue} - $ref->{OldValue}; + }, + }, + + '4.1.8' => { + 'RT::Ticket' => sub { + my ($ref) = @_; + $ref->{IsMerged} = 1 if $ref->{id} != $ref->{EffectiveId}; + }, + }, + + '4.1.10' => { + 'RT::ObjectcustomFieldValue' => sub { + my ($ref) = @_; + $ref->{Content} = undef if defined $ref->{LargeContent} + and defined $ref->{Content} and $ref->{Content} eq ''; + }, + }, + + '4.1.11' => { + 'RT::CustomField' => sub { + my ($ref) = @_; + delete $ref->{Repeated}; + }, + }, + + '4.1.13' => { + 'RT::Group' => sub { + my ($ref) = @_; + $ref->{Name} = $ref->{Type} + if $ref->{Domain} =~ /^(ACLEquivalence|SystemInternal|.*-Role)$/; + }, + }, + + '4.1.14' => { + 'RT::Scrip' => sub { + my ($ref) = @_; + delete $ref->{ConditionRules}; + delete $ref->{ActionRules}; + }, + }, + + '4.1.17' => { + 'RT::Attribute' => sub { + my ($ref) = @_; + return unless $ref->{Name} eq 'SavedSearch'; + my $v = eval { + Storable::thaw(MIME::Base64::decode_base64($ref->{Content})) + }; + return unless $v and ref $v and ($v->{SearchType}||'') eq 'Chart'; + + # Switch from PrimaryGroupBy to GroupBy name + # Switch from "CreatedMonthly" to "Created.Monthly" + $v->{GroupBy} ||= [delete $v->{PrimaryGroupBy}]; + for (@{$v->{GroupBy}}) { + next if /\./; + s/(?<=[a-z])(?=[A-Z])/./; + } + $ref->{Content} = MIME::Base64::encode_base64( + Storable::nfreeze($v) ); + }, + }, + + '4.1.19' => { + 'RT::Template' => sub { + my ($ref) = @_; + delete $ref->{Language}; + delete $ref->{TranslationOf}; + }, + }, + + '4.1.20' => { + 'RT::Template' => sub { + my ($ref) = @_; + if ($ref->{Name} eq 'Forward') { + $ref->{Description} = 'Forwarded message'; + if ( $ref->{Content} =~ + m/^\n*This is (a )?forward of transaction #\{\s*\$Transaction->id\s*\} of (a )?ticket #\{\s*\$Ticket->id\s*\}\n*$/ + ) { + $ref->{Content} = q{ +{ $ForwardTransaction->Content =~ /\S/ ? $ForwardTransaction->Content : "This is a forward of transaction #".$Transaction->id." of ticket #". $Ticket->id } +}; + } else { + RT->Logger->error('Current "Forward" template is not the default version, please check docs/UPGRADING-4.2'); + } + } elsif ($ref->{Name} eq 'Forward Ticket') { + $ref->{Description} = 'Forwarded ticket message'; + if ( $ref->{Content} eq q{ + +This is a forward of ticket #{ $Ticket->id } +} ) { + $ref->{Content} = q{ +{ $ForwardTransaction->Content =~ /\S/ ? $ForwardTransaction->Content : "This is a forward of ticket #". $Ticket->id } +}; + } else { + RT->Logger->error('Current "Forward Ticket" template is not the default version, please check docs/UPGRADING-4.2'); + } + } + }, + }, + + '4.1.21' => { + # XXX User dashboards + }, + + '4.1.22' => { + 'RT::Template' => sub { + my ($ref) = @_; + return unless $ref->{Name} eq 'Error: bad GnuPG data'; + $ref->{Name} = 'Error: bad encrypted data'; + $ref->{Description} = + 'Inform user that a message he sent has invalid encryption data'; + $ref->{Content} =~ s/GnuPG signature/signature/g; + }, + # XXX SMIME keys + 'RT::Attribute' => sub { + my ($ref, $classref) = @_; + if ($ref->{ObjectType} eq "RT::User" and $ref->{Name} eq "SMIMEKeyNotAfter") { + $$classref = undef; + } + }, + }, + + '4.2.1' => { + 'RT::Attribute' => sub { + my ($ref, $classref) = @_; + if ($ref->{ObjectType} eq "RT::System" and $ref->{Name} eq "BrandedSubjectTag") { + $$classref = undef; + } + }, + }, + + '4.2.2' => { + 'RT::CustomField' => sub { + my ($ref) = @_; + $ref->{LookupType} = 'RT::Class-RT::Article' + if $ref->{LookupType} eq 'RT::FM::Class-RT::FM::Article'; + }, + 'RT::ObjectCustomFieldValue' => sub { + my ($ref) = @_; + $ref->{ObjectType} = 'RT::Article' + if $ref->{ObjectType} eq 'RT::FM::Article'; + }, + }, + +); + +1; diff --git a/rt/lib/RT/Migrate/Serializer.pm b/rt/lib/RT/Migrate/Serializer.pm new file mode 100644 index 0000000..92be629 --- /dev/null +++ b/rt/lib/RT/Migrate/Serializer.pm @@ -0,0 +1,492 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +package RT::Migrate::Serializer; + +use strict; +use warnings; + +use base 'RT::DependencyWalker'; + +use Storable qw//; +sub cmp_version($$) { RT::Handle::cmp_version($_[0],$_[1]) }; +use RT::Migrate::Incremental; +use RT::Migrate::Serializer::IncrementalRecord; +use RT::Migrate::Serializer::IncrementalRecords; + +sub Init { + my $self = shift; + + my %args = ( + AllUsers => 1, + AllGroups => 1, + FollowDeleted => 1, + + FollowScrips => 0, + FollowTickets => 1, + FollowACL => 0, + + Clone => 0, + Incremental => 0, + + Verbose => 1, + @_, + ); + + $self->{Verbose} = delete $args{Verbose}; + + $self->{$_} = delete $args{$_} + for qw/ + AllUsers + AllGroups + FollowDeleted + FollowScrips + FollowTickets + FollowACL + Clone + Incremental + /; + + $self->{Clone} = 1 if $self->{Incremental}; + + $self->SUPER::Init(@_, First => "top"); + + # Keep track of the number of each type of object written out + $self->{ObjectCount} = {}; + + if ($self->{Clone}) { + $self->PushAll; + } else { + $self->PushBasics; + } +} + +sub Metadata { + my $self = shift; + + # Determine the highest upgrade step that we run + my @versions = ($RT::VERSION, keys %RT::Migrate::Incremental::UPGRADES); + my ($max) = reverse sort cmp_version @versions; + # we don't want to run upgrades to 4.2.x if we're running + # the serializier on an 4.0 instance. + $max = $RT::VERSION unless $self->{Incremental}; + + return { + Format => "0.8", + VersionFrom => $RT::VERSION, + Version => $max, + Organization => $RT::Organization, + Clone => $self->{Clone}, + Incremental => $self->{Incremental}, + ObjectCount => { $self->ObjectCount }, + @_, + }, +} + +sub PushAll { + my $self = shift; + + # To keep unique constraints happy, we need to remove old records + # before we insert new ones. This fixes the case where a + # GroupMember was deleted and re-added (with a new id, but the same + # membership). + if ($self->{Incremental}) { + my $removed = RT::Migrate::Serializer::IncrementalRecords->new( RT->SystemUser ); + $removed->Limit( FIELD => "UpdateType", VALUE => 3 ); + $removed->OrderBy( FIELD => 'id' ); + $self->PushObj( $removed ); + } + # XXX: This is sadly not sufficient to deal with the general case of + # non-id unique constraints, such as queue names. If queues A and B + # existed, and B->C and A->B renames were done, these will be + # serialized with A->B first, which will fail because there already + # exists a B. + + # Principals first; while we don't serialize these separately during + # normal dependency walking (we fold them into users and groups), + # having them separate during cloning makes logic simpler. + $self->PushCollections(qw(Principals)); + + # Users and groups + $self->PushCollections(qw(Users Groups GroupMembers)); + + # Tickets + $self->PushCollections(qw(Queues Tickets Transactions Attachments Links)); + + # Articles + $self->PushCollections(qw(Articles), map { ($_, "Object$_") } qw(Classes Topics)); + + # Custom Fields + if (RT::ObjectCustomFields->require) { + $self->PushCollections(map { ($_, "Object$_") } qw(CustomFields CustomFieldValues)); + } elsif (RT::TicketCustomFieldValues->require) { + $self->PushCollections(qw(CustomFields CustomFieldValues TicketCustomFieldValues)); + } + + # ACLs + $self->PushCollections(qw(ACL)); + + # Scrips + $self->PushCollections(qw(Scrips ObjectScrips ScripActions ScripConditions Templates)); + + # Attributes + $self->PushCollections(qw(Attributes)); +} + +sub PushCollections { + my $self = shift; + + for my $type (@_) { + my $class = "RT::\u$type"; + + $class->require or next; + my $collection = $class->new( RT->SystemUser ); + $collection->FindAllRows; # be explicit + $collection->CleanSlate; # some collections (like groups and users) join in _Init + $collection->UnLimit; + $collection->OrderBy( FIELD => 'id' ); + + if ($self->{Clone}) { + if ($collection->isa('RT::Tickets')) { + $collection->{allow_deleted_search} = 1; + $collection->IgnoreType; # looking_at_type + } + elsif ($collection->isa('RT::ObjectCustomFieldValues')) { + # FindAllRows (find_disabled_rows) isn't used by OCFVs + $collection->{find_expired_rows} = 1; + } + + if ($self->{Incremental}) { + my $alias = $collection->Join( + ALIAS1 => "main", + FIELD1 => "id", + TABLE2 => "IncrementalRecords", + FIELD2 => "ObjectId", + ); + $collection->DBIx::SearchBuilder::Limit( + ALIAS => $alias, + FIELD => "ObjectType", + VALUE => ref($collection->NewItem), + ); + } + } + + $self->PushObj( $collection ); + } +} + +sub PushBasics { + my $self = shift; + + # System users + for my $name (qw/RT_System root nobody/) { + my $user = RT::User->new( RT->SystemUser ); + my ($id, $msg) = $user->Load( $name ); + warn "No '$name' user found: $msg" unless $id; + $self->PushObj( $user ) if $id; + } + + # System groups + foreach my $name (qw(Everyone Privileged Unprivileged)) { + my $group = RT::Group->new( RT->SystemUser ); + my ($id, $msg) = $group->LoadSystemInternalGroup( $name ); + warn "No '$name' group found: $msg" unless $id; + $self->PushObj( $group ) if $id; + } + + # System role groups + my $systemroles = RT::Groups->new( RT->SystemUser ); + $systemroles->LimitToRolesForObject( RT->System ); + $self->PushObj( $systemroles ); + + # CFs on Users, Groups, Queues + my $cfs = RT::CustomFields->new( RT->SystemUser ); + $cfs->Limit( + FIELD => 'LookupType', + OPERATOR => 'IN', + VALUE => [ qw/RT::User RT::Group RT::Queue/ ], + ); + $self->PushObj( $cfs ); + + # Global attributes + my $attributes = RT::Attributes->new( RT->SystemUser ); + $attributes->LimitToObject( $RT::System ); + $self->PushObj( $attributes ); + + # Global ACLs + if ($self->{FollowACL}) { + my $acls = RT::ACL->new( RT->SystemUser ); + $acls->LimitToObject( $RT::System ); + $self->PushObj( $acls ); + } + + # Global scrips + if ($self->{FollowScrips}) { + my $scrips = RT::Scrips->new( RT->SystemUser ); + $scrips->LimitToGlobal; + + my $templates = RT::Templates->new( RT->SystemUser ); + $templates->LimitToGlobal; + + $self->PushObj( $scrips, $templates ); + $self->PushCollections(qw(ScripActions ScripConditions)); + } + + if ($self->{AllUsers}) { + my $users = RT::Users->new( RT->SystemUser ); + $users->LimitToPrivileged; + $self->PushObj( $users ); + } + + if ($self->{AllGroups}) { + my $groups = RT::Groups->new( RT->SystemUser ); + $groups->LimitToUserDefinedGroups; + $self->PushObj( $groups ); + } + + if (RT::Articles->require) { + $self->PushCollections(qw(Topics Classes)); + } + + $self->PushCollections(qw(Queues)); +} + +sub InitStream { + my $self = shift; + + # Write the initial metadata + my $meta = $self->Metadata; + $! = 0; + Storable::nstore_fd( $meta, $self->{Filehandle} ); + die "Failed to write metadata: $!" if $!; + + return unless cmp_version($meta->{VersionFrom}, $meta->{Version}) < 0; + + my %transforms; + for my $v (sort cmp_version keys %RT::Migrate::Incremental::UPGRADES) { + for my $ref (keys %{$RT::Migrate::Incremental::UPGRADES{$v}}) { + push @{$transforms{$ref}}, $RT::Migrate::Incremental::UPGRADES{$v}{$ref}; + } + } + for my $ref (keys %transforms) { + # XXX Does not correctly deal with updates of $classref, which + # should technically apply all later transforms of the _new_ + # class. This is not relevant in the current upgrades, as + # RT::ObjectCustomFieldValues do not have interesting later + # upgrades if you start from 3.2 (which does + # RT::TicketCustomFieldValues -> RT::ObjectCustomFieldValues) + $self->{Transform}{$ref} = sub { + my ($dat, $classref) = @_; + my @extra; + for my $c (@{$transforms{$ref}}) { + push @extra, $c->($dat, $classref); + return @extra if not $$classref; + } + return @extra; + }; + } +} + +sub NextPage { + my $self = shift; + my ($collection, $last) = @_; + + $last ||= 0; + + if ($self->{Clone}) { + # Clone provides guaranteed ordering by id and with no other id limits + # worry about trampling + + # Use DBIx::SearchBuilder::Limit explicitly to avoid shenanigans in RT::Tickets + $collection->DBIx::SearchBuilder::Limit( + FIELD => 'id', + OPERATOR => '>', + VALUE => $last, + ENTRYAGGREGATOR => 'none', # replaces last limit on this field + ); + } else { + # XXX TODO: this could dig around inside the collection to see how it's + # limited and do the faster paging above under other conditions. + $self->SUPER::NextPage(@_); + } +} + +sub Process { + my $self = shift; + my %args = ( + object => undef, + @_ + ); + + my $obj = $args{object}; + my $uid = $obj->UID; + + # Skip all dependency walking if we're cloning; go straight to + # visiting them. + if ($self->{Clone} and $uid) { + return if $obj->isa("RT::System"); + $self->{progress}->($obj) if $self->{progress}; + return $self->Visit(%args); + } + + return $self->SUPER::Process( @_ ); +} + +sub StackSize { + my $self = shift; + return scalar @{$self->{stack}}; +} + +sub ObjectCount { + my $self = shift; + return %{ $self->{ObjectCount} }; +} + +sub Observe { + my $self = shift; + my %args = ( + object => undef, + direction => undef, + from => undef, + @_ + ); + + my $obj = $args{object}; + my $from = $args{from}; + if ($obj->isa("RT::Ticket")) { + return 0 if $obj->Status eq "deleted" and not $self->{FollowDeleted}; + return $self->{FollowTickets}; + } elsif ($obj->isa("RT::ACE")) { + return $self->{FollowACL}; + } elsif ($obj->isa("RT::Scrip") or $obj->isa("RT::Template") or $obj->isa("RT::ObjectScrip")) { + return $self->{FollowScrips}; + } elsif ($obj->isa("RT::GroupMember")) { + my $grp = $obj->GroupObj->Object; + if ($grp->Domain =~ /^RT::(Queue|Ticket)-Role$/) { + return 0 unless $grp->UID eq $from; + } elsif ($grp->Domain eq "SystemInternal") { + return 0 if $grp->UID eq $from; + } + } + + return 1; +} + +sub Visit { + my $self = shift; + my %args = ( + object => undef, + @_ + ); + + # Serialize it + my $obj = $args{object}; + warn "Writing ".$obj->UID."\n" if $self->{Verbose}; + my @store; + if ($obj->isa("RT::Migrate::Serializer::IncrementalRecord")) { + # These are stand-ins for record removals + my $class = $obj->ObjectType; + my %data = ( id => $obj->ObjectId ); + # -class is used for transforms when dropping a record + if ($self->{Transform}{"-$class"}) { + $self->{Transform}{"-$class"}->(\%data,\$class) + } + @store = ( + $class, + undef, + \%data, + ); + } elsif ($self->{Clone}) { + # Short-circuit and get Just The Basics, Sir if we're cloning + my $class = ref($obj); + my $uid = $obj->UID; + my %data = $obj->RT::Record::Serialize( UIDs => 0 ); + + # +class is used when seeing a record of one class might insert + # a separate record into the stream + if ($self->{Transform}{"+$class"}) { + my @extra = $self->{Transform}{"+$class"}->(\%data,\$class); + for my $e (@extra) { + $! = 0; + Storable::nstore_fd($e, $self->{Filehandle}); + die "Failed to write: $!" if $!; + $self->{ObjectCount}{$e->[0]}++; + } + } + + # Upgrade the record if necessary + if ($self->{Transform}{$class}) { + $self->{Transform}{$class}->(\%data,\$class); + } + + # Transforms set $class to undef to drop the record + return unless $class; + + @store = ( + $class, + $uid, + \%data, + ); + } else { + @store = ( + ref($obj), + $obj->UID, + { $obj->Serialize }, + ); + } + + # Write it out; nstore_fd doesn't trap failures to write, so we have + # to; by clearing $! and checking it afterwards. + $! = 0; + Storable::nstore_fd(\@store, $self->{Filehandle}); + die "Failed to write: $!" if $!; + + $self->{ObjectCount}{$store[0]}++; +} + +1; diff --git a/rt/lib/RT/Migrate/Serializer/File.pm b/rt/lib/RT/Migrate/Serializer/File.pm new file mode 100644 index 0000000..2832365 --- /dev/null +++ b/rt/lib/RT/Migrate/Serializer/File.pm @@ -0,0 +1,171 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +package RT::Migrate::Serializer::File; + +use strict; +use warnings; + +use base 'RT::Migrate::Serializer'; + +sub Init { + my $self = shift; + + my %args = ( + Directory => undef, + Force => undef, + MaxFileSize => 32, + + @_, + ); + + # Set up the output directory we'll be writing to + my ($y,$m,$d) = (localtime)[5,4,3]; + $args{Directory} = $RT::Organization . + sprintf(":%d-%02d-%02d",$y+1900,$m+1,$d) + unless defined $args{Directory}; + system("rm", "-rf", $args{Directory}) if $args{Force}; + die "Output directory $args{Directory} already exists" + if -d $args{Directory}; + mkdir $args{Directory} + or die "Can't create output directory $args{Directory}: $!\n"; + $self->{Directory} = delete $args{Directory}; + + # How many megabytes each chunk should be, approximitely + $self->{MaxFileSize} = delete $args{MaxFileSize}; + + # Which file we're writing to + $self->{FileCount} = 1; + + $self->SUPER::Init(@_); +} + +sub Metadata { + my $self = shift; + return $self->SUPER::Metadata( + Files => [ $self->Files ], + @_, + ) +} + +sub Export { + my $self = shift; + + # Set up our output file + $self->OpenFile; + + # Write the initial metadata + $self->InitStream; + + # Walk the objects + $self->Walk( @_ ); + + # Close everything back up + $self->CloseFile; + + # Write the summary file + Storable::nstore( + $self->Metadata( Final => 1 ), + $self->Directory . "/rt-serialized" + ); + + return $self->ObjectCount; +} + +sub Visit { + my $self = shift; + + # Rotate if we get too big + my $maxsize = 1024 * 1024 * $self->{MaxFileSize}; + $self->RotateFile if tell($self->{Filehandle}) > $maxsize; + + # Serialize it + $self->SUPER::Visit( @_ ); +} + + +sub Files { + my $self = shift; + return @{ $self->{Files} }; +} + +sub Filename { + my $self = shift; + return sprintf( + "%s/%03d.dat", + $self->{Directory}, + $self->{FileCount} + ); +} + +sub Directory { + my $self = shift; + return $self->{Directory}; +} + +sub OpenFile { + my $self = shift; + open($self->{Filehandle}, ">", $self->Filename) + or die "Can't write to file @{[$self->Filename]}: $!"; + push @{$self->{Files}}, $self->Filename; +} + +sub CloseFile { + my $self = shift; + close($self->{Filehandle}) + or die "Can't close @{[$self->Filename]}: $!"; + $self->{FileCount}++; +} + +sub RotateFile { + my $self = shift; + $self->CloseFile; + $self->OpenFile; +} + +1; diff --git a/rt/lib/RT/Migrate/Serializer/IncrementalRecord.pm b/rt/lib/RT/Migrate/Serializer/IncrementalRecord.pm new file mode 100644 index 0000000..d5df8c8 --- /dev/null +++ b/rt/lib/RT/Migrate/Serializer/IncrementalRecord.pm @@ -0,0 +1,80 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +package RT::Migrate::Serializer::IncrementalRecord; +use base qw/RT::Record/; + +use strict; +use warnings; + +sub Table {'IncrementalRecords'} + +sub _CoreAccessible { + return { + id => { read => 1 }, + ObjectType => { read => 1 }, + ObjectId => { read => 1 }, + UpdateType => { read => 1 }, + AlteredAt => { read => 1 }, + }; +}; + +1; + +__END__ + +CREATE TABLE IncrementalRecords ( + id INTEGER NOT NULL AUTO_INCREMENT, + ObjectType VARCHAR(50) NOT NULL, + ObjectId INTEGER NOT NULL, + UpdateType TINYINT NOT NULL, + AlteredAt TIMESTAMP NOT NULL, + PRIMARY KEY(ObjectType, ObjectId), + UNIQUE KEY(id), + KEY(UpdateType) +); diff --git a/rt/lib/RT/Migrate/Serializer/IncrementalRecords.pm b/rt/lib/RT/Migrate/Serializer/IncrementalRecords.pm new file mode 100644 index 0000000..a729caa --- /dev/null +++ b/rt/lib/RT/Migrate/Serializer/IncrementalRecords.pm @@ -0,0 +1,69 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +package RT::Migrate::Serializer::IncrementalRecords; +use base qw/RT::SearchBuilder/; + +use strict; +use warnings; + +sub _Init { + my $self = shift; + $self->{'table'} = 'IncrementalRecords'; + $self->{'primary_key'} = 'id'; + return ( $self->SUPER::_Init(@_) ); +} + +sub Table {'IncrementalRecords'} + +sub NewItem { + my $self = shift; + return(RT::Migrate::Serializer::IncrementalRecord->new($self->CurrentUser)); +} + +1; |