root/branches/release-35/lib/MT/BackupRestore.pm @ 1984

Revision 1984, 39.7 kB (checked in by fumiakiy, 20 months ago)

Changed the way to show progress message for MT::Author so the empty message won't overwrite the previous valid messages. BugId:69074

  • Property svn:keywords set to Id Author Date Revision
Line 
1# Movable Type (r) Open Source (C) 2001-2008 Six Apart, Ltd.
2# This program is distributed under the terms of the
3# GNU General Public License, version 2.
4#
5# $Id$
6
7package MT::BackupRestore;
8use strict;
9
10use MT::Util qw( encode_url );
11use Symbol;
12use base qw( MT::ErrorHandler );
13
14use constant NS_MOVABLETYPE => 'http://www.sixapart.com/ns/movabletype';
15
16use File::Spec;
17use File::Copy;
18
19MT->add_callback('restore', 3, MT->instance, sub {
20    my ($cb, $objects, $deferred, $errors, $callback) = @_;
21    MT::BackupRestore->cb_restore_objects( $objects, $callback );
22});
23
24MT->add_callback('restore_asset', 3, MT->instance, sub {
25    my ($cb, $asset, $callback) = @_;
26    MT::BackupRestore->cb_restore_asset( $asset, $callback );
27});
28
29sub _populate_obj_to_backup {
30    my $pkg = shift;
31    my ($blog_ids, $skip, $later) = @_;
32
33    my %populated;
34    if ( defined($blog_ids) && scalar(@$blog_ids) ) {
35        # author will be handled at last
36        $populated{MT->model('author')} = 1;
37    }
38
39    my @obj_to_backup;
40    my $types = MT->registry('object_types');
41    foreach my $key (keys %$types) {
42        next if $key =~ /\w+\.\w+/; # skip subclasses
43        my $class = MT->model($key);
44        next unless $class;
45        next if $class eq $key; # FIXME: to remove plugin object_classes
46        next if exists $skip->{$class};
47        next if exists $later->{$class};
48        next if exists $populated{$class};
49        $pkg->_create_obj_to_backup(
50            $class, $blog_ids, \@obj_to_backup, \%populated, $later);
51    }
52    foreach my $class (keys %$later) {
53        next if exists $skip->{$class};
54        $pkg->_create_obj_to_backup(
55            $class, $blog_ids, \@obj_to_backup, \%populated, {});
56    }
57
58    if ( defined($blog_ids) && scalar(@$blog_ids) ) {
59        # Author has two ways to be associated to a blog
60        my $class = MT->model('author');
61        unshift @obj_to_backup, {$class => { 
62            terms => undef, 
63            args => { 'join' => 
64                [ MT->model('association'), 'author_id', { blog_id => $blog_ids }, { unique => 1 } ] 
65            }}}; 
66        unshift @obj_to_backup, {$class => { 
67            terms => undef, 
68            args => { 'join' => 
69                [ MT->model('permission'), 'author_id', { blog_id => $blog_ids }, { unique => 1 } ] 
70            }}}; 
71    }
72    return \@obj_to_backup;
73}
74
75sub _create_obj_to_backup {
76    my $pkg = shift;
77    my ($class, $blog_ids, $obj_to_backup, $populated, $later) = @_;
78
79    my $columns = $class->column_names;
80    foreach my $column (@$columns) {
81        if ( $column =~ /^(\w+)_id$/ ) {
82            my $parent = $1;
83            my $p_class = MT->model($parent);
84            next unless $p_class;
85            next if exists $later->{$p_class};
86            next if exists $populated->{$p_class};
87            $pkg->_create_obj_to_backup(
88                $p_class, $blog_ids, $obj_to_backup, $populated, $later);
89        }
90    }
91   
92    if ( $class->can('backup_terms_args') ) {
93        push @$obj_to_backup, { $class => $class->backup_terms_args($blog_ids) };
94    }
95    else {
96        push @$obj_to_backup, $pkg->_default_terms_args($class, $blog_ids);
97    }
98
99    $populated->{$class} = 1;
100}
101
102sub _default_terms_args {
103    my $pkg = shift;
104    my ($class, $blog_ids) = @_;
105
106    if (defined($blog_ids) && scalar(@$blog_ids)) {
107        return { $class =>
108          {
109            terms => { 'blog_id' => $blog_ids }, 
110            args => undef
111          }
112        };
113    }
114    else {
115        return
116          {
117            $class => { terms => undef, args => undef }
118          };
119    }
120}
121   
122
123sub backup {
124    my $class = shift;
125    my ($blog_ids, $printer, $splitter, $finisher, $progress, $size, $enc, $metadata) = @_;
126    push @$blog_ids, '0'
127        if defined($blog_ids) && scalar(@$blog_ids);
128    my $obj_to_backup = $class->_populate_obj_to_backup(
129        $blog_ids,
130        {
131            MT->model('config') => 1, 
132            MT->model('session') => 1,
133            MT->model('ts_job') => 1,
134            MT->model('ts_error') => 1,
135            MT->model('ts_exitstatus') => 1,
136            MT->model('ts_funcmap') => 1,
137        },
138        {
139            MT->model('placement') => 1, 
140            MT->model('ping') => 1, 
141            MT->model('objecttag') => 1, 
142            MT->model('objectasset') => 1, 
143            MT->model('objectscore') => 1,
144        }
145    );
146
147    my $header .= "<movabletype xmlns='" . NS_MOVABLETYPE . "'\n";
148    $header .= join ' ', map { $_ . "='" . $metadata->{$_} . "'" } keys %$metadata;
149    $header .= ">\n";
150    $header = "<?xml version='1.0' encoding='$enc'?>\n$header" if $enc !~ m/utf-?8/i;
151    $printer->($header);
152
153    my $files = {};
154    _loop_through_objects(
155        $printer, $splitter, $finisher, $progress, $size, $obj_to_backup, $files);
156
157    my $else_xml = MT->run_callbacks('Backup', $blog_ids, $progress);
158    $printer->($else_xml) if $else_xml ne '1';
159
160    $printer->('</movabletype>');
161    $finisher->($files);
162}
163
164sub _loop_through_objects {
165    my ($printer, $splitter, $finisher, $progress, $size, $obj_to_backup, $files) = @_;
166
167    my $counter = 1;
168    my $bytes = 0;
169    my %authors_seen;
170    my $author_pkg = MT->model('author');
171    for my $class_hash (@$obj_to_backup) {
172        my ($class, $term_arg) = each(%$class_hash);
173        eval "require $class;";
174        my $children = $class->properties->{child_classes} || {};
175        for my $child_class (keys %$children) {
176            eval "require $child_class;";
177        }
178        if (my $err = $@) {
179            $progress->("$err\n", 'Error');
180            next;
181        }
182        my @metacolumns;
183        if ( exists( $class->properties->{meta} )
184          && $class->properties->{meta} ) {
185            require MT::Meta;
186            @metacolumns = MT::Meta->metadata_by_class( $class );
187        }
188        my $records = 0;
189        my $state = MT->translate('Backing up [_1] records:', $class);
190        $progress->($state, $class->class_type || $class->datasource);
191        my $limit = 50;
192        my $offset = 0;
193        my $terms = $term_arg->{terms} || {};
194        my $args = $term_arg->{args};
195        unless ( exists $args->{sort} ) {
196            $args->{sort} = 'id';
197            $args->{direction} = 'ascend';
198        }
199        while (1) {
200            $args->{offset} = $offset;
201            $args->{limit} = $limit + 1;
202            my $iter;
203            eval {
204                $iter = $class->load_iter($terms, $args);
205            };
206            if (my $err = $@) {
207                $progress->("$class:$err\n", 'Error');
208            }
209            last unless $iter;
210            my $count = 0;
211            my $next  = 0;
212            while ( my $object = $iter->() ) {
213                if ( $count == $limit ) {
214                    $iter->('finish');
215                    $next = 1;
216                    $offset += $count;
217                    last;
218                }
219                $count++;
220                if ( ( $class eq $author_pkg )
221                  && ( exists $authors_seen{$object->id} ) ) {
222                    next;
223                }
224                $bytes += $printer->($object->to_xml(undef, \@metacolumns) . "\n");
225                $records++;
226                if ($size && ($bytes >= $size)) {
227                    $splitter->(++$counter);
228                    $bytes = 0;
229                }
230                if ( $class eq $author_pkg ) {
231                    # Authors may be duplicated because of how terms and args are created.
232                    $authors_seen{$object->id} = 1;
233                } elsif ( $class->datasource eq 'asset' ) {
234                    $files->{$object->id} = [$object->url, $object->file_path, $object->file_name];
235                }
236            }
237            last unless $next;
238            $progress->($state . " " . MT->translate("[_1] records backed up...", $records), $class->datasource)
239                if $records && ($records % 100 == 0);
240        }
241        if ( $class eq $author_pkg && %authors_seen ) {
242            my $count = scalar(keys %authors_seen);
243            $progress->($state . " " . MT->translate("[_1] records backed up.", $count), $class->class_type || $class->datasource);
244        } elsif ($records) {
245            $progress->($state . " " . MT->translate("[_1] records backed up.", $records), $class->class_type || $class->datasource);
246        } else {
247            $progress->($state . " " . MT->translate("There were no [_1] records to be backed up.", $class), $class->class_type || $class->datasource);
248        }
249    }
250}
251
252sub restore_file {
253    my $class = shift;
254    my ($fh, $errormsg, $schema_version, $overwrite, $callback) = @_;
255
256    my $objects = {};
257    my $deferred = {};
258    my $errors = [];
259
260    my ($blog_ids, $asset_ids) = eval { $class->restore_process_single_file(
261        $fh, $objects, $deferred, $errors, $schema_version, $overwrite, $callback
262    ); };
263
264    MT->run_callbacks('restore', $objects, $deferred, $errors, $callback);
265    $$errormsg = join('; ', @$errors);
266    ($deferred, $blog_ids);
267}
268 
269sub restore_process_single_file {
270    my $class = shift;
271    my ($fh, $objects, $deferred, $errors, $schema_version, $overwrite,  $callback) = @_;
272   
273    my %restored_blogs = map { $objects->{$_}->id => 1; } grep { 'blog' eq $objects->{$_}->datasource } keys %$objects;
274
275    require XML::SAX;
276    require MT::BackupRestore::BackupFileHandler;
277    my $handler = MT::BackupRestore::BackupFileHandler->new(
278        callback => $callback,
279        objects => $objects,
280        deferred => $deferred,
281        errors => $errors,
282        schema_version => $schema_version,
283        overwrite_template => $overwrite,
284    );
285
286    require MT::Util;
287    my $parser = MT::Util::sax_parser();
288    $callback->(ref($parser) . "\n") if MT->config->DebugMode;
289    $parser->{Handler} = $handler;
290    eval { $parser->parse_file($fh); };
291    if (my $e = $@) {
292        push @$errors, $e;
293        $callback->($e);
294        die $e if $handler->{critical}; 
295    }
296
297    my @blog_ids;
298    my @asset_ids;
299
300    while (my ($key, $value) = each %$objects) {
301        if ('blog' eq $value->datasource) {
302            push @blog_ids, $value->id unless exists $restored_blogs{$value->id};
303        } elsif ('asset' eq $value->datasource) {
304            my ($old_id) = $key =~ /^.+#(\d+)$/;
305            push @asset_ids, $value->id, $old_id;
306        }
307    }
308    my $blog_ids = scalar(@blog_ids) ? \@blog_ids : undef;
309    my $asset_ids = scalar(@asset_ids) ? \@asset_ids : undef;
310    ($blog_ids, $asset_ids);
311}
312
313sub restore_directory {
314    my $class = shift;
315    my ($dir, $errors, $error_assets, $schema_version, $overwrite, $callback) = @_;
316
317    my $manifest;
318    my @files;
319    opendir my $dh, $dir or push(@$errors, MT->translate("Can't open directory '[_1]': [_2]", $dir, "$!")), return undef;
320    for my $f (readdir $dh) {
321        next if $f !~ /^.+\.manifest$/i;
322        $manifest = File::Spec->catfile($dir, $f);
323        last;
324    }
325    closedir $dh;
326    unless ($manifest) {
327        push @$errors, MT->translate("No manifest file could be found in your import directory [_1].", $dir);
328        return (undef, undef);
329    }
330
331    my $fh = gensym;
332    open $fh, "<$manifest" or push(@$errors, MT->translate("Can't open [_1].", $manifest)), return 0;
333    my $backups = __PACKAGE__->process_manifest($fh);
334    close $fh;
335    unless($backups) {
336        push @$errors, MT->translate("Manifest file [_1] was not a valid Movable Type backup manifest file.", $manifest);
337        return (undef, undef);
338    }
339
340    $callback->(MT->translate("Manifest file: [_1]", $manifest) . "\n");
341
342    my %objects;
343    my $deferred = {};
344
345    my $files = $backups->{files};
346    my @blog_ids;
347    my @asset_ids;
348    for my $file (@$files) {
349        my $fh = gensym;
350        my $filepath = File::Spec->catfile($dir, $file);
351        open $fh, "<$filepath" or push @$errors, MT->translate("Can't open [_1]."), next;
352
353        my ($tmp_blog_ids, $tmp_asset_ids) = eval { __PACKAGE__->restore_process_single_file(
354            $fh, \%objects, $deferred, $errors, $schema_version, $overwrite, $callback); };
355
356        close $fh;
357        last if $@;
358
359        push @blog_ids, @$tmp_blog_ids if defined $tmp_blog_ids;
360        push @asset_ids, @$tmp_asset_ids if defined $tmp_asset_ids;
361    }
362
363    MT->run_callbacks('restore', \%objects, $deferred, $errors, $callback);
364    my $blog_ids = scalar(@blog_ids) ? \@blog_ids : undef;
365    my $asset_ids = scalar(@asset_ids) ? \@asset_ids : undef;
366    ($deferred, $blog_ids, $asset_ids);
367}
368
369sub process_manifest {
370    my $class = shift;
371    my ($stream) = @_;
372
373    if ((ref($stream) eq 'Fh') || (ref($stream) eq 'GLOB')){
374        seek($stream, 0, 0) or return undef;
375        require XML::SAX;
376        require MT::BackupRestore::ManifestFileHandler;
377        my $handler = MT::BackupRestore::ManifestFileHandler->new();
378
379        require MT::Util;
380        my $parser = MT::Util::sax_parser();
381        $parser->{Handler} = $handler;
382        eval { $parser->parse_file($stream); };
383        if (my $e = $@) {
384            die $e;
385        }
386        return $handler->{backups};
387    }
388    return undef;
389}
390
391sub restore_asset {
392    my $class = shift;
393    my ($file, $asset, $old_id, $fmgr, $errors, $callback) = @_;
394
395    my $id = $asset->id;
396
397    my $path = $asset->file_path;
398    unless (defined($path)) {
399        $callback->(MT->translate('Path was not found for the file ([_1]).', $id));
400        return 0;
401    }
402    my ($vol, $dir, $fn) = File::Spec->splitpath($path);
403    my $voldir =  "$vol$dir";
404    if (!-w $voldir) {
405        unless (defined $fmgr) {
406            my $blog = MT->model('blog')->load($asset->blog_id);
407            $fmgr = $blog->file_mgr if $blog;
408        }
409        unless (defined $fmgr) {
410            # we do need utf8_off here
411            $errors->{$id} = MT->translate('[_1] is not writable.', MT::I18N::utf8_off($voldir)) ;
412        } else {
413            $voldir =~ s|/$|| unless $voldir eq '/';  ## OS X doesn't like / at the end in mkdir().
414            unless ($fmgr->exists($voldir)) {
415                $fmgr->mkpath($voldir) or
416                    $errors->{$id} = MT->translate("Error making path '[_1]': [_2]", $path, $fmgr->errstr);
417            }
418        }
419    }
420    if (-w $voldir) {
421        my $filename = "$old_id-" . $asset->file_name;
422        $callback->(MT->translate("Copying [_1] to [_2]...", $filename, $path));
423        copy($file, $path)
424            or $errors->{$id} = $!;
425    }
426
427    if ( exists $errors->{$id} ) {
428        return $callback->( MT->translate('Failed: ') . $errors->{$id} . "\n" );
429    }
430
431    $callback->( MT->translate("Done.") . "\n" );
432
433    MT->run_callbacks('restore_asset', $asset, $callback);
434   
435    1;
436}
437
438sub _sync_asset_id {
439    my ($text, $related) = @_;
440
441    $text =~ s!<form([^>]*?\s)mt:asset-id=(["'])(\d+)(["'])([^>]*?)>(.+?)</form>!
442        my $old_id = $3;
443        my $asset = $related->{$old_id};
444        my $result = '<form' . $1 . 'mt:asset-id=' . $2 . $asset->id . $4 . $5 . '>';
445        my $html = $6;
446        my $filename = quotemeta(encode_url($asset->file_name));
447        my $url = $asset->url;
448        my @children = MT->model('asset')->load(
449            { parent => $asset->id, blog_id => $asset->blog_id, class => '*' }
450        );
451        my %children = map {
452            $_->id => {
453                'filename' => quotemeta(encode_url($_->file_name)),
454                'url' => $_->url
455            }
456        } @children;
457        $result .= $html . '</form>';
458        $result;
459    !igem;
460    $text;
461}
462
463sub cb_restore_objects {
464    my $pkg = shift;
465    my ($all_objects, $callback) = @_;
466
467    my %entries;
468    my %assets;
469    my %old_ids;
470    for my $key ( keys %$all_objects ) {
471        if ( $key =~ /^MT::Entry#(\d+)$/ ) {
472            my $new_id = $all_objects->{$key}->id;
473            $entries{$new_id} = $all_objects->{$key};
474        } elsif ( $key =~ /^MT::Asset#(\d+)$/ ) {
475            my $old_id = $1;
476            my $new_id = $all_objects->{$key}->id;
477            $assets{$new_id} = {
478                object => $all_objects->{$key},
479                old_id => $old_id,
480            };
481        } elsif ( $key =~ /^MT::Author#(\d+)$/ ) {
482            # restore userpic association now
483            my $new_author = $all_objects->{$key};
484            if ( my $userpic_id = $new_author->userpic_asset_id ) {
485                if ( my $new_asset = $all_objects->{'MT::Asset#' . $userpic_id} ) {
486                    $new_author->userpic_asset_id( $new_asset->id );
487                    $new_author->update;
488                }
489            }
490        }
491    }
492
493    my $i = 0;
494    $callback->(
495        MT->translate("Restoring asset associations ... ( [_1] )", $i++),
496        'cb-restore-entry-asset'
497    );
498    for my $obj_id ( keys %entries ) {
499        my $entry = $entries{$obj_id};
500
501        my @placements = MT->model('objectasset')->load( {
502            object_id => $obj_id, 
503            object_ds => 'entry', 
504            blog_id => $entry->blog_id
505        });
506        next unless @placements;
507
508        my %related;
509        for my $placement ( @placements ) {
510            my $asset_hash = $assets{$placement->asset_id};
511            next unless $asset_hash;
512            $related{ $asset_hash->{old_id} } = $asset_hash->{object};
513        }
514
515        if ($entry->class == 'entry') {
516            $callback->(
517                MT->translate("Restoring asset associations in entry ... ( [_1] )", $i++),
518                'cb-restore-entry-asset'
519            );
520        } else {
521            $callback->(
522                MT->translate("Restoring asset associations in page ... ( [_1] )", $i++),
523                'cb-restore-entry-asset'
524            );
525        }
526       
527        for my $col ( qw( text text_more ) ) {
528            my $text = $entry->$col;
529            next unless $text;
530            $text = _sync_asset_id( $text, \%related ); 
531            $entry->$col($text);
532        }
533        $entry->update();  # directly call update to bypass processing in save()
534    }
535    $callback->( MT->translate("Done.") . "\n" );
536    1;
537}
538
539sub _sync_asset_url {
540    my ($text, $asset) = @_;
541
542    my $filename = quotemeta(encode_url($asset->file_name));
543    my $url = $asset->url;
544    my $id = $asset->id;
545    my @children = MT->model('asset')->load(
546        { parent => $asset->id, blog_id => $asset->blog_id, class => '*' }
547    );
548    my %children = map {
549        $_->id => {
550            'filename' => quotemeta(encode_url($_->file_name)),
551            'url' => $_->url
552        }
553    } @children;
554
555    $text =~ s!<form([^>]*?\s)mt:asset-id=(["'])$id(["'])([^>]*?)>(.+?)</form>!
556        my $result = '<form' . $1 . 'mt:asset-id=' . $2 . $id . $3 . $4 . '>';
557        my $html = $5;
558        $html =~ s#<a([^>]*? )href=(["'])[^>]+?/$filename(["'])([^>]*?)>#<a$1href=$2$url$3$4>#gim;
559        $html =~ s#<img([^>]*? )src=(["'])[^>]+?/$filename(["'])([^>]*?)(/? *)>#<img$1src=$2$url$3$4$5>#gim;
560        if ( %children ) {
561            for my $child (values %children) {
562                my $child_filename = $child->{filename};
563                my $child_url = $child->{url};
564                $html =~ s#<img([^>]*? )src=(["'])[^>]+?/$child_filename(["'])([^>]*?)>#<img$1src=$2$child_url$3$4>#gim;
565                $html =~ s#<a([^>]*? )href=(["'])[^>]+?/$child_filename(["'])([^>]*?)>#<a$1href=$2$child_url$3$4>#gim;
566                $html =~ s#<a([^>]*? )onclick=(["'])[^>]+?/$child_filename(["'])([^>]*?)>#<a$1onclick=$2$child_url$3$4>#gim;
567            }
568        }
569        $result .= $html . '</form>';
570        $result;
571    !igem;
572    $text;
573}
574
575sub cb_restore_asset {
576    my $pkg = shift;
577    my ($asset, $callback) = @_;
578   
579    my @placements = MT->model('objectasset')->load( {
580        asset_id => $asset->id, 
581        blog_id => $asset->blog_id
582    });
583
584    my $i = 0;
585    $callback->(
586        MT->translate('Restoring url of the assets ( [_1] )...', $i++),
587        'cb-restore-asset-url'
588    );
589    for my $placement (@placements) { 
590        next unless 'entry' eq $placement->object_ds; 
591        my $entry = MT->model('entry')->load( $placement->object_id ); 
592        next unless $entry; 
593       
594        if ($entry->class == 'entry') {
595            $callback->(
596                MT->translate('Restoring url of the assets in entry ( [_1] )...', $i++),
597                'cb-restore-asset-url'
598            );
599        } else {
600            $callback->(
601                MT->translate('Restoring url of the assets in page ( [_1] )...', $i++),
602                'cb-restore-asset-url'
603            );
604        }
605        for my $col ( qw( text text_more ) ) {
606            my $text = $entry->$col;
607            next unless $text;
608            $text = _sync_asset_url( $text, $asset );
609            $entry->$col($text);
610        }
611        $entry->update();  # directly call update to bypass processing in save()
612    }
613    $callback->( MT->translate("Done.") . "\n" );
614    1;
615}
616
617sub _restore_asset_multi {
618    my $class = shift;
619    my ($asset_element, $objects, $errors, $callback, $blogs_meta) = @_;
620
621    my $old_id = $asset_element->{asset_id};
622    if (!defined($old_id)) {
623        $callback->(MT->translate('ID for the file was not set.'));
624        return 0;
625    }
626    my $asset_class = MT->model('asset');
627    my $asset = $objects->{"$asset_class#$old_id"};
628    unless (defined($asset)) {
629        $callback->(MT->translate('The file ([_1]) was not restored.', $old_id));
630        return 0;
631    }
632
633    my $fmgr;
634    if (exists $blogs_meta->{$asset->blog_id}) {
635        my $blog = MT->model('blog')->load($asset->blog_id)
636            or return 0;
637
638        my $meta = $blogs_meta->{$asset->blog_id};
639        my $path = $asset->file_path;
640        my $url = $asset->url;
641        if (my $archive_path = $meta->{'archive_path'}) {
642            my $old_archive_path = $meta->{'old_archive_path'};
643            $path =~ s/\Q$old_archive_path\E/$archive_path/i;
644            $asset->file_path($path);
645        }
646        if (my $archive_url = $meta->{'archive_url'}) {
647            my $old_archive_url = $meta->{'old_archive_url'};
648            $url =~ s/\Q$old_archive_url\E/$archive_url/i;
649            $asset->url($url);
650        }
651        if (my $site_path = $meta->{'site_path'}) {
652            my $old_site_path = $meta->{'old_site_path'};
653            $path =~ s/\Q$old_site_path\E/$site_path/i;
654            $asset->file_path($path);
655        }
656        if (my $site_url = $meta->{'site_url'}) {
657            my $old_site_url = $meta->{'old_site_url'};
658            $url =~ s/\Q$old_site_url\E/$site_url/i;
659            $asset->url($url);
660        }
661        $callback->(MT->translate("Changing path for the file '[_1]' (ID:[_2])...", $asset->label, $asset->id));
662        $asset->save or $callback->(MT->translate("failed") . "\n");
663        $callback->(MT->translate("ok") . "\n");
664
665        $fmgr = $blog->file_mgr;
666    }
667    my $file = $asset_element->{fh};
668    $class->restore_asset($file, $asset, $old_id, $fmgr, $errors, $callback);
669}
670
671package MT::Object;
672
673sub _is_element {
674    my $obj = shift;
675    my ($def) = @_;
676    return (('text' eq $def->{type}) || (('string' eq $def->{type}) && (255 < $def->{size}))) ? 1 : 0;
677}
678
679sub to_xml {
680    my $obj = shift;
681    my ($namespace, $metacolumns) = @_;
682
683    my $coldefs = $obj->column_defs;
684    my $colnames = $obj->column_names;
685    my $xml;
686
687    my $elem = $obj->datasource;
688    $xml = '<' . $elem;
689    $xml .= " xmlns='$namespace'" if defined($namespace) && $namespace;
690
691    my (@elements, @blobs, @meta);
692    for my $name (@$colnames) {
693        if ($obj->column($name) || (defined($obj->column($name)) && ('0' eq $obj->column($name)))) {
694            if ( ( $obj->properties->{meta_column} || '' ) eq $name ) {
695                push @meta, $name;
696                next;
697            }
698            elsif ($obj->_is_element($coldefs->{$name})) {
699                push @elements, $name;
700                next;
701            } elsif ('blob' eq $coldefs->{$name}->{type}) {
702                push @blobs, $name;
703                next;
704            }
705            $xml .= " $name='" . MT::Util::encode_xml($obj->column($name), 1) . "'";
706        }
707    }
708    my ( @meta_elements, @meta_blobs );
709    if ( defined( $metacolumns ) && @$metacolumns ) {
710        foreach my $metacolumn ( @$metacolumns ) {
711            my $name = $metacolumn->{name};
712            if ($obj->$name || (defined($obj->$name) && ('0' eq $obj->$name))) {
713                if ( 'vclob' eq $metacolumn->{type} ) {
714                    push @meta_elements, $name;
715                }
716                elsif ( 'vblob' eq $metacolumn->{type} ) {
717                    push @meta_blobs, $name;
718                }
719                else {
720                    $xml .= " $name='" . MT::Util::encode_xml($obj->$name, 1) . "'";
721                }
722            }
723        }
724    }
725    $xml .= '>';
726    $xml .= "<$_>" . MT::Util::encode_xml($obj->column($_), 1) . "</$_>" foreach @elements;
727    require MIME::Base64;
728    $xml .= "<$_>" . MIME::Base64::encode_base64($obj->column($_), '') . "</$_>" foreach @blobs;
729    foreach my $meta_col (@meta) {
730        my $hashref = $obj->$meta_col;
731        $xml .= "<$meta_col>" . 
732                MIME::Base64::encode_base64(MT::Serialize->serialize(\$hashref), '') .
733                "</$meta_col>";
734    }
735    $xml .= "<$_>" . MT::Util::encode_xml($obj->$_, 1) . "</$_>" foreach @meta_elements;
736    $xml .= "<$_>" . MIME::Base64::encode_base64($obj->$_, '') . "</$_>" foreach @meta_blobs;
737    $xml .= '</' . $elem . '>';
738    $xml;
739}
740
741sub parents {
742    my $obj = shift;
743    {};
744}
745
746sub _restore_id {
747    my $obj = shift;
748    my ($key, $val, $data, $objects) = @_;
749
750    return 0 unless 'ARRAY' eq ref($val);
751    return 1 if 0 == $data->{$key}; 
752
753    my $new_obj;
754    my $old_id = $data->{$key};
755    foreach (@$val) {
756        $new_obj = $objects->{"$_#$old_id"};
757        last if $new_obj;
758    }
759    return 0 unless $new_obj;
760    $data->{$key} = $new_obj->id;
761    return 1;
762}
763
764sub restore_parent_ids {
765    my $obj = shift;
766    my ($data, $objects) = @_;
767
768    my $parents = $obj->parents;
769    my $count = scalar(keys %$parents);
770
771    my $done = 0;
772    while (my ($key, $val) = each(%$parents)) {
773        $val = [ $val ] unless (ref $val);
774        if ('ARRAY' eq ref($val)) {
775            $done += $obj->_restore_id($key, $val, $data, $objects);
776        }
777        elsif ('HASH' eq ref($val)) {
778            my $v = $val->{class};
779            $v = [ $v ] unless (ref $v);
780            my $result = 0;
781            if (my $relations = $val->{relations}) {
782                my $col = $relations->{key};
783                my $ds = $data->{$col};
784                my $ev = $relations->{$ds . '_id'};
785                $ev = MT->model($ds) unless $ev;
786                return 0 unless $ev;
787                $ev = [ $ev ] unless (ref $ev);
788                $done += $obj->_restore_id($key, $ev, $data, $objects);
789            }
790            else {
791                $result = $obj->_restore_id($key, $v, $data, $objects);
792                $result = 1 if exists($val->{optional}) && $val->{optional};
793                $data->{$key} = -1 
794                  if !$result && (exists($val->{orphanize}) && $val->{orphanize});
795                $done += $result;
796            }
797        }
798    }
799    ($count == $done) ? 1 : 0;   
800}
801
802package MT::Blog;
803
804sub backup_terms_args {
805    my $class = shift;
806    my ($blog_ids) = @_;
807
808    if ( defined($blog_ids) && scalar(@$blog_ids) ) {
809        return
810          {
811            terms => { id => $blog_ids }, 
812            args => undef,
813          };
814    }
815    else {
816        return { terms => undef, args => undef };
817    }
818}
819
820package MT::Tag;
821
822sub backup_terms_args {
823    my $class = shift;
824    my ($blog_ids) = @_;
825
826    if ( defined($blog_ids) && scalar(@$blog_ids) ) {
827        return
828          {
829            terms => undef, 
830            args =>
831              {
832                'join' =>
833                  [
834                    'MT::ObjectTag', 
835                    'tag_id',
836                    { blog_id => $blog_ids }, 
837                    { unique => 1 }
838                  ]
839              }
840          };
841    }
842    else {
843        return { terms => undef, args => undef };
844    }
845}
846
847package MT::Role;
848
849sub backup_terms_args {
850    my $class = shift;
851    my ($blog_ids) = @_;
852
853    if ( defined($blog_ids) && scalar(@$blog_ids) ) {
854        return
855          {
856            terms => undef, 
857            args =>
858              {
859                'join' =>
860                  [
861                    'MT::Association',
862                    'role_id',
863                    { blog_id => $blog_ids },
864                    { unique => 1 }
865                  ]
866              }
867          };
868    }
869    else {
870        return { terms => undef, args => undef };
871    }
872}
873
874package MT::Asset;
875
876sub backup_terms_args {
877    my $class = shift;
878    my ($blog_ids) = @_;
879
880    if ( defined($blog_ids) && scalar(@$blog_ids) ) {
881        return
882          {
883            terms => { 'blog_id' => $blog_ids, 'class' => '*' }, 
884            args => undef
885          }
886    }
887    else {
888        return { terms => { 'class' => '*' }, args => undef };
889    }
890}
891
892my $assets_seen = {};
893
894sub to_xml {
895    my $obj = shift;
896    my $xml = q();
897
898    return $xml if exists $assets_seen->{$obj->id};
899
900    if ($obj->parent) {
901        my $parent = MT->model('asset')->load($obj->parent)
902            or return $xml;
903        $xml .= $parent->to_xml(@_);
904        $xml .= "\n";
905    }
906
907    $xml .= $obj->SUPER::to_xml(@_);
908    $assets_seen->{$obj->id} = 1;
909    $xml;
910}
911
912sub parents {
913    my $obj = shift;
914    {
915        blog_id => MT->model('blog'),
916        parent  => MT->model('asset')
917    };
918}
919
920package MT::PluginData;
921
922sub backup_terms_args {
923    my $class = shift;
924    my ($blog_ids) = @_;
925
926    return { terms => undef, args => undef };
927}
928
929sub restore_parent_ids {
930    my $obj = shift;
931    my ($data, $objects) = @_;
932
933    if ($data->{key} =~ /^configuration:blog:(\d+)$/i) {
934        my $new_blog = $objects->{'MT::Blog#' . $1};
935        if ($new_blog) {
936            $data->{key} = 'configuration:blog:' . $new_blog->id;
937        }
938    }
939    return 1;
940}
941
942package MT::Association;
943
944sub restore_parent_ids {
945    my $obj = shift;
946    my ($data, $objects) = @_;
947
948    my ($u, $b, $g, $r) = (0, 0, 0, 0);
949
950    my $processor = sub {
951        my ($elem) = @_;
952        my $class = MT->model($elem);
953        my $old_id = $data->{$elem . '_id'};
954        my $new_obj = $objects->{"$class#$old_id"};
955        return 0 unless defined($new_obj) && $new_obj;
956        $data->{$elem . '_id'} = $new_obj->id;
957        return 1;
958    };
959
960    $u = $processor->('author');
961    $g = $processor->('group');
962    $b = $processor->('blog');
963    $r = $processor->('role');
964
965    # Combination allowed are:
966    # USER_BLOG_ROLE  => 1;
967    # GROUP_BLOG_ROLE => 2;
968    # USER_GROUP      => 3;
969    # USER_ROLE       => 4;
970    # GROUP_ROLE      => 5;
971
972    ($u && $g) || ($u && $r) || ($g && $r) ? 1 : 0; # || ($u && $b && $r) || ($g && $b && $r)
973}
974
975package MT::Category;
976
977my $category_seen = {};
978
979sub to_xml {
980    my $obj = shift;
981    my $xml = q();
982
983    return $xml if exists $category_seen->{$obj->id};
984   
985    if ('0' ne $obj->parent) {
986        $xml .= $obj->parent_category->to_xml(@_);
987        $xml .= "\n";
988    }
989
990    $xml .= $obj->SUPER::to_xml(@_);
991    $category_seen->{$obj->id} = 1;
992    $xml;
993}
994
995sub parents {
996    my $obj = shift;
997    {
998        blog_id => MT->model('blog'),
999        parent  => [ MT->model('category'), MT->model('folder') ],
1000    };
1001}
1002
1003package MT::Comment;
1004
1005sub parents {
1006    my $obj = shift;
1007    {
1008        entry_id => [ MT->model('entry'), MT->model('page') ],
1009        blog_id => MT->model('blog'),
1010        commenter_id => { class => MT->model('author'), optional => 1 },
1011    };
1012}
1013
1014package MT::Entry;
1015
1016sub parents {
1017    my $obj = shift;
1018    {
1019        blog_id => MT->model('blog'),
1020        author_id => { class => MT->model('author'), optional => 1, orphanize => 1 },
1021    };
1022}
1023
1024package MT::Notification;
1025
1026sub parents {
1027    my $obj = shift;
1028    {
1029        blog_id => MT->model('blog'),
1030    };
1031}
1032
1033package MT::ObjectTag;
1034
1035sub parents {
1036    my $obj = shift;
1037    {
1038        blog_id => MT->model('blog'),
1039        tag_id => MT->model('tag'),
1040        object_id => { relations => {
1041            key => 'object_datasource',
1042            entry_id => [ MT->model('entry'), MT->model('page') ],
1043        }}
1044    };
1045}
1046
1047package MT::Permission;
1048
1049sub parents {
1050    my $obj = shift;
1051    {
1052        blog_id => { class => MT->model('blog'), optional => 1 },
1053        author_id => { class => MT->model('author'), optional => 1 },
1054    };
1055}
1056
1057package MT::Placement;
1058
1059sub parents {
1060    my $obj = shift;
1061    {
1062        category_id => [ MT->model('category'), MT->model('folder') ],
1063        blog_id => MT->model('blog'),
1064        entry_id => [ MT->model('entry'), MT->model('page') ],
1065    };
1066}
1067
1068package MT::TBPing;
1069
1070sub parents {
1071    my $obj = shift;
1072    {
1073        blog_id => MT->model('blog'),
1074        tb_id => MT->model('trackback'),
1075    };
1076}
1077
1078package MT::Template;
1079
1080sub parents {
1081    my $obj = shift;
1082    {
1083        blog_id => MT->model('blog'),
1084    };
1085}
1086
1087package MT::TemplateMap;
1088
1089sub parents {
1090    my $obj = shift;
1091    {
1092        blog_id => MT->model('blog'),
1093        template_id  => MT->model('template')
1094    };
1095}
1096
1097package MT::Trackback;
1098
1099sub restore_parent_ids {
1100    my $obj = shift;
1101    my ($data, $objects) = @_;
1102
1103    my $result = 0;
1104    my $blog_class = MT->model('blog');
1105    my $new_blog = $objects->{$blog_class . '#' . $data->{blog_id}};
1106    if ($new_blog) {
1107        $data->{blog_id} = $new_blog->id;
1108    } else {
1109        return 0;
1110    }                           
1111    if (my $cid = $data->{category_id}) {
1112        my $cat_class = MT->model('category');
1113        my $new_obj = $objects->{$cat_class . '#' . $cid};
1114        unless ($new_obj) {
1115            $cat_class = MT->model('folder');
1116            $new_obj = $objects->{$cat_class . '#' . $cid};
1117        }
1118        if ($new_obj) {
1119            $data->{category_id} = $new_obj->id;
1120            $result = 1;
1121        }
1122    } elsif (my $eid = $data->{entry_id}) {
1123        my $entry_class = MT->model('entry');
1124        my $new_obj = $objects->{$entry_class . '#' . $eid};
1125        unless ($new_obj) {
1126            $entry_class = MT->model('page');
1127            $new_obj = $objects->{$entry_class . '#' . $eid};
1128        }
1129        if ($new_obj) {
1130            $data->{entry_id} = $new_obj->id;
1131            $result = 1;
1132        }
1133    }
1134    $result;
1135}
1136
1137package MT::ObjectAsset;
1138
1139sub parents {
1140    my $obj = shift;
1141    {
1142        blog_id => MT->model('blog'),
1143        asset_id => MT->model('asset'),
1144        object_id => { relations => {
1145            key => 'object_ds',
1146            entry_id => [ MT->model('entry'), MT->model('page') ],
1147        }}
1148    };
1149}
1150
1151package MT::ObjectScore;
1152
1153sub backup_terms_args {
1154    my $class = shift;
1155    my ($blog_ids) = @_;
1156
1157    return { terms => undef, args => undef };
1158}
1159
1160sub parents {
1161    my $obj = shift;
1162    {
1163        author_id => MT->model('author'),
1164        object_id => { relations => {
1165            key => 'object_ds',
1166            entry_id => [ MT->model('entry'), MT->model('page') ],
1167        }}
1168    };
1169}
1170
11711;
1172__END__
1173
1174=head1 NAME
1175
1176MT::BackupRestore
1177
1178=head1 METHODS
1179
1180=head2 backup
1181
1182TODO Backup I<MT::Tag>, I<MT::Author>, I<MT::Blog>, I<MT::Role>,
1183I<MT::Category>, I<MT::Asset>, and I<MT::Entry>.  Each object will
1184be back up by MT::Object#to_xml call, which will do the actual
1185Object ==>> XML serialization.
1186
1187=head2 restore_file
1188
1189TODO Restore MT system from an XML file which contains MT backup
1190information (created by backup subroutine).
1191
1192=head2 restore_process_single_file
1193
1194TODO A method which will do the actual heavy lifting of the
1195process to restore objects from an XML file.  Returns array of blog_ids
1196which are restored in the very session, and hash of asset_ids.
1197
1198=head2 restore_directory
1199
1200TODO A method which reads specified directory, find a manifest file,
1201and do the multi-file restore operation directed by the manifest file.
1202
1203=head2 restore_object_asset
1204
1205Accepts an asset object just restored, populate associated entries,
1206and scan text and text_more for each entry.  If association marker
1207(<form> tag) is found, replace asset id and URL to the new ones.
1208
1209=head2 restore_asset
1210
1211TODO A method which restores the assets' actual files to the
1212specified directory.  It also creates subdirectory by request.
1213
1214=head2 process_manifest
1215
1216TODO A method which is called from MT::App::CMS to process an uploaded
1217manifest file which is to be the source of the multi-file restore
1218operation in the MT::App::CMS.
1219
1220=head1 Callbacks
1221
1222For plugins which uses MT::Object-derived types, backup and restore
1223operation call callbacks for plugins to inject XMLs so they are
1224also backup, and read XML to restore objects so they are also restored.
1225
1226Callbacks called by the package are as follows:
1227
1228=over 4
1229
1230=item Backup
1231   
1232Calling convention is:
1233
1234    callback($cb, $blog_ids, $progress)
1235
1236The callback is used for MT::Object-derived types used by plugins
1237to be backup.  The callback must return the object's XML representation
1238in a string, or 1 for nothing.  $blog_ids has an ARRAY reference to
1239blog_ids which indicates what weblog a user chose to backup.  It may
1240be an empty array if a user chose Everything.  $progress is a CODEREF
1241used to report progress to the user.
1242
1243If a plugin has an MT::Object derived type, the plugin will register
1244a callback to Backup callback, and Backup process will call the callbacks
1245to give plugins a chance to add their own data to the backup file.
1246Otherwise, plugin's object classes is likely be ignored in backup operation.
1247
1248=item Restore.<element_name>:<xmlnamespace>
1249
1250Restore callbacks are called in convention like below:
1251
1252    callback($cb, $data, $objects, $deferred, $callback);
1253
1254Where $data is a parameter which was passed to XML::SAX::Base's
1255start_element callback method.
1256
1257$objects is an hash reference which contains all the restored objects
1258in the restore session.  The hash keys are stored in the format
1259MT::ObjectClassName#old_id, and hash values are object reference
1260of the actually restored objects (with new id).  Old ids are ids
1261which are stored in the XML files, while new ids are ids which
1262are restored.
1263
1264$deferred is an hash reference which contains information about
1265restore-deferred objects.  Deferred objects are those objects
1266which appeared in the XMl file but could not be restored because
1267any parent objects are missing.  The hash keys are stored in
1268the format MT::ObjectClassName#old_id and hash values are 1.
1269
1270$callback is a code reference which will print out the passed paramter.
1271Callback method can use this to communicate with users.
1272
1273If a plugin has an MT::Object derived type, the plugin will register
1274a callback to Restore.<element_name>:<xmlnamespace> callback,
1275so later the restore operation will call the callback function with
1276parameters described above.  XML Namespace is required to be registered,
1277so an xml node can be resolved into what plugins to be called back,
1278and can be distinguished the same element name with each other.
1279
1280=item restore
1281   
1282Calling convention is:
1283
1284    callback($cb, $objects, $deferred, $errors, $callback);
1285
1286This callback is called when all of the XML files in the particular
1287restore session are restored, thus, when $objects and $deferred
1288would not have any more objects in them.  This callback is useful
1289for object classes which have relationships with other classes,
1290for the kind of classes may not be able to handle relationship
1291correctly until the associated objects would be successfully
1292restored.
1293
1294NOTE that this callback is called BEFORE blogs' site_path and
1295site_url are updated.  Therefore, blog objects and other objects
1296which contains path information such as assets still have old
1297url and path in I<$objects>.
1298
1299$objects is an hash reference which contains all the restored objects
1300in the restore session.  The hash keys are stored in the format
1301MT::ObjectClassName#old_id, and hash values are object reference
1302of the actually restored objects (with new id).  Old ids are ids
1303which are stored in the XML files, while new ids are ids which
1304are restored.
1305
1306$deferred is an hash reference which contains information about
1307restore-deferred objects.  Deferred objects are those objects
1308which appeared in the XMl file but could not be restored because
1309any parent objects are missing.  The hash keys are stored in
1310the format MT::ObjectClassName#old_id and hash values are 1.
1311
1312$callback is a code reference which will print out the passed paramter.
1313Callback method can use this to communicate with users.
1314
1315=item restore_asset
1316   
1317Calling convention is:
1318
1319    callback($cb, $asset, $callback);
1320
1321This callback is called when asset's actual file is restored.
1322$asset has new url and path.
1323
1324$callback is a code reference which will print out the passed paramter.
1325Callback method can use this to communicate with users.
1326
1327=head1 AUTHOR & COPYRIGHT
1328
1329Please see L<MT/AUTHOR & COPYRIGHT>.
1330
1331=cut
Note: See TracBrowser for help on using the browser.