1#!@PERL_PATH@
2
3# Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License, version 2.0,
7# as published by the Free Software Foundation.
8#
9# This program is also distributed with certain software (including
10# but not limited to OpenSSL) that is licensed under separate terms,
11# as designated in a particular file or component or in included license
12# documentation.  The authors of MySQL hereby grant you an additional
13# permission to link the program and your derivative works with the
14# separately licensed software that they have included with MySQL.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19# GNU General Public License, version 2.0, for more details.
20#
21# You should have received a copy of the GNU Library General Public
22# License along with this library; if not, write to the Free
23# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
24# MA 02110-1301, USA
25
26use strict;
27use Getopt::Long;
28use Data::Dumper;
29use File::Basename;
30use File::Path;
31use DBI;
32use Sys::Hostname;
33use File::Copy;
34use File::Temp qw(tempfile);
35
36=head1 NAME
37
38mysqlhotcopy - fast on-line hot-backup utility for local MySQL databases and tables
39
40=head1 SYNOPSIS
41
42  mysqlhotcopy db_name
43
44  mysqlhotcopy --suffix=_copy db_name_1 ... db_name_n
45
46  mysqlhotcopy db_name_1 ... db_name_n /path/to/new_directory
47
48  mysqlhotcopy db_name./regex/
49
50  mysqlhotcopy db_name./^\(foo\|bar\)/
51
52  mysqlhotcopy db_name./~regex/
53
54  mysqlhotcopy db_name_1./regex_1/ db_name_1./regex_2/ ... db_name_n./regex_n/ /path/to/new_directory
55
56  mysqlhotcopy --method='scp -Bq -i /usr/home/foo/.ssh/identity' --user=root --password=secretpassword \
57         db_1./^nice_table/ user@some.system.dom:~/path/to/new_directory
58
59WARNING: THIS PROGRAM IS STILL IN BETA. Comments/patches welcome.
60
61=cut
62
63# Documentation continued at end of file
64
65my $VERSION = "1.23";
66
67my $opt_tmpdir = $ENV{TMPDIR} || "/tmp";
68
69my $OPTIONS = <<"_OPTIONS";
70
71$0 Ver $VERSION
72
73Usage: $0 db_name[./table_regex/] [new_db_name | directory]
74
75  -?, --help           display this help-screen and exit
76  -u, --user=#         user for database login if not current user
77  -p, --password=#     password to use when connecting to server (if not set
78                       in my.cnf, which is recommended)
79  -h, --host=#         hostname for local server when connecting over TCP/IP
80  -P, --port=#         port to use when connecting to local server with TCP/IP
81  -S, --socket=#       socket to use when connecting to local server
82      --old_server     connect to old MySQL-server (before v5.5) which
83                       doesn't have FLUSH TABLES WITH READ LOCK fully implemented.
84
85  --allowold           don\'t abort if target dir already exists (rename it _old)
86  --addtodest          don\'t rename target dir if it exists, just add files to it
87  --keepold            don\'t delete previous (now renamed) target when done
88  --noindices          don\'t include full index files in copy
89  --method=#           method for copy (only "cp" currently supported)
90
91  -q, --quiet          be silent except for errors
92  --debug              enable debug
93  -n, --dryrun         report actions without doing them
94
95  --regexp=#           copy all databases with names matching regexp
96  --suffix=#           suffix for names of copied databases
97  --checkpoint=#       insert checkpoint entry into specified db.table
98  --flushlog           flush logs once all tables are locked
99  --resetmaster        reset the binlog once all tables are locked
100  --resetslave         reset the master.info once all tables are locked
101  --tmpdir=#	       temporary directory (instead of $opt_tmpdir)
102  --record_log_pos=#   record slave and master status in specified db.table
103  --chroot=#           base directory of chroot jail in which mysqld operates
104
105  Try \'perldoc $0\' for more complete documentation
106_OPTIONS
107
108sub usage {
109    die @_, $OPTIONS;
110}
111
112# Do not initialize user or password options; that way, any user/password
113# options specified in option files will be used.  If no values are specified
114# at all, the defaults will be used (login name, no password).
115
116my %opt = (
117    noindices	=> 0,
118    allowold	=> 0,	# for safety
119    keepold	=> 0,
120    method	=> "cp",
121    flushlog    => 0,
122);
123Getopt::Long::Configure(qw(no_ignore_case)); # disambiguate -p and -P
124GetOptions( \%opt,
125    "help",
126    "host|h=s",
127    "user|u=s",
128    "password|p=s",
129    "port|P=s",
130    "socket|S=s",
131    "old_server",
132    "allowold!",
133    "keepold!",
134    "addtodest!",
135    "noindices!",
136    "method=s",
137    "debug",
138    "quiet|q",
139    "mv!",
140    "regexp=s",
141    "suffix=s",
142    "checkpoint=s",
143    "record_log_pos=s",
144    "flushlog",
145    "resetmaster",
146    "resetslave",
147    "tmpdir|t=s",
148    "dryrun|n",
149    "chroot=s",
150) or usage("Invalid option");
151
152# @db_desc
153# ==========
154# a list of hash-refs containing:
155#
156#   'src'     - name of the db to copy
157#   't_regex' - regex describing tables in src
158#   'target'  - destination directory of the copy
159#   'tables'  - array-ref to list of tables in the db
160#   'files'   - array-ref to list of files to be copied
161#   'index'   - array-ref to list of indexes to be copied
162#
163
164my @db_desc = ();
165my $tgt_name = undef;
166
167print STDERR "Warning: $0 is deprecated and will be removed in a future version.\n";
168
169usage("") if ($opt{help});
170
171if ( $opt{regexp} || $opt{suffix} || @ARGV > 2 ) {
172    $tgt_name   = pop @ARGV unless ( exists $opt{suffix} );
173    @db_desc = map { s{^([^\.]+)\./(.+)/$}{$1}; { 'src' => $_, 't_regex' => ( $2 ? $2 : '.*' ) } } @ARGV;
174}
175else {
176    usage("Database name to hotcopy not specified") unless ( @ARGV );
177
178    $ARGV[0] =~ s{^([^\.]+)\./(.+)/$}{$1};
179    @db_desc = ( { 'src' => $ARGV[0], 't_regex' => ( $2 ? $2 : '.*' ) } );
180
181    if ( @ARGV == 2 ) {
182	$tgt_name   = $ARGV[1];
183    }
184    else {
185	$opt{suffix} = "_copy";
186    }
187}
188
189my %mysqld_vars;
190my $start_time = time;
191$opt_tmpdir= $opt{tmpdir} if $opt{tmpdir};
192$0 = $1 if $0 =~ m:/([^/]+)$:;
193$opt{quiet} = 0 if $opt{debug};
194$opt{allowold} = 1 if $opt{keepold};
195
196# --- connect to the database ---
197my $dsn;
198$dsn  = ";host=" . (defined($opt{host}) ? $opt{host} : "localhost");
199$dsn .= ";port=$opt{port}" if $opt{port};
200$dsn .= ";mysql_socket=$opt{socket}" if $opt{socket};
201
202# use mysql_read_default_group=mysqlhotcopy so that [client] and
203# [mysqlhotcopy] groups will be read from standard options files.
204
205my $dbh = DBI->connect("dbi:mysql:$dsn;mysql_read_default_group=mysqlhotcopy",
206                        $opt{user}, $opt{password},
207{
208    RaiseError => 1,
209    PrintError => 0,
210    AutoCommit => 1,
211});
212
213# --- check that checkpoint table exists if specified ---
214if ( $opt{checkpoint} ) {
215    $opt{checkpoint} = quote_names( $opt{checkpoint} );
216    eval { $dbh->do( qq{ select time_stamp, src, dest, msg
217			 from $opt{checkpoint} where 1 != 1} );
218       };
219
220    die "Error accessing Checkpoint table ($opt{checkpoint}): $@"
221      if ( $@ );
222}
223
224# --- check that log_pos table exists if specified ---
225if ( $opt{record_log_pos} ) {
226    $opt{record_log_pos} = quote_names( $opt{record_log_pos} );
227
228    eval { $dbh->do( qq{ select host, time_stamp, log_file, log_pos, master_host, master_log_file, master_log_pos
229			 from $opt{record_log_pos} where 1 != 1} );
230       };
231
232    die "Error accessing log_pos table ($opt{record_log_pos}): $@"
233      if ( $@ );
234}
235
236# --- get variables from database ---
237my $sth_vars = $dbh->prepare("show variables like 'datadir'");
238$sth_vars->execute;
239while ( my ($var,$value) = $sth_vars->fetchrow_array ) {
240    $mysqld_vars{ $var } = $value;
241}
242my $datadir = $mysqld_vars{'datadir'}
243    || die "datadir not in mysqld variables";
244    $datadir= $opt{chroot}.$datadir if ($opt{chroot});
245$datadir =~ s:/$::;
246
247
248# --- get target path ---
249my ($tgt_dirname, $to_other_database);
250$to_other_database=0;
251if (defined($tgt_name) && $tgt_name =~ m:^\w+$: && @db_desc <= 1)
252{
253    $tgt_dirname = "$datadir/$tgt_name";
254    $to_other_database=1;
255}
256elsif (defined($tgt_name) && ($tgt_name =~ m:/: || $tgt_name eq '.')) {
257    $tgt_dirname = $tgt_name;
258}
259elsif ( $opt{suffix} ) {
260    print "Using copy suffix '$opt{suffix}'\n" unless $opt{quiet};
261}
262else
263{
264  $tgt_name="" if (!defined($tgt_name));
265  die "Target '$tgt_name' doesn't look like a database name or directory path.\n";
266}
267
268# --- resolve database names from regexp ---
269if ( defined $opt{regexp} ) {
270    my $t_regex = '.*';
271    if ( $opt{regexp} =~ s{^/(.+)/\./(.+)/$}{$1} ) {
272        $t_regex = $2;
273    }
274
275    my $sth_dbs = $dbh->prepare("show databases");
276    $sth_dbs->execute;
277    while ( my ($db_name) = $sth_dbs->fetchrow_array ) {
278	next if $db_name =~ m/^information_schema$/i;
279	push @db_desc, { 'src' => $db_name, 't_regex' => $t_regex } if ( $db_name =~ m/$opt{regexp}/o );
280    }
281}
282
283# --- get list of tables and views to hotcopy ---
284
285my $hc_locks = "";
286my $hc_tables = "";
287my $hc_base_tables = "";
288my $hc_views = "";
289my $num_base_tables = 0;
290my $num_views = 0;
291my $num_tables = 0;
292my $num_files = 0;
293
294foreach my $rdb ( @db_desc ) {
295    my $db = $rdb->{src};
296    my @dbh_base_tables = get_list_of_tables( $db );
297    my @dbh_views = get_list_of_views( $db );
298
299    ## filter out certain system non-lockable tables.
300    ## keep in sync with mysqldump.
301    if ($db =~ m/^mysql$/i)
302    {
303      @dbh_base_tables = grep
304        { !/^(apply_status|schema|general_log|slow_log)$/ } @dbh_base_tables
305    }
306
307    ## generate regex for tables/files
308    my $t_regex;
309    my $negated;
310    if ($rdb->{t_regex}) {
311        $t_regex = $rdb->{t_regex};        ## assign temporary regex
312        $negated = $t_regex =~ s/^~//;     ## note and remove negation operator
313
314        $t_regex = qr/$t_regex/;           ## make regex string from
315                                           ## user regex
316
317        ## filter (out) tables specified in t_regex
318        print "Filtering tables with '$t_regex'\n" if $opt{debug};
319        @dbh_base_tables = ( $negated
320                             ? grep { $_ !~ $t_regex } @dbh_base_tables
321                             : grep { $_ =~ $t_regex } @dbh_base_tables );
322
323        ## filter (out) views specified in t_regex
324        print "Filtering tables with '$t_regex'\n" if $opt{debug};
325        @dbh_views = ( $negated
326                       ? grep { $_ !~ $t_regex } @dbh_views
327                       : grep { $_ =~ $t_regex } @dbh_views );
328    }
329
330    ## Now concatenate the base table and view arrays.
331    my @dbh_tables = (@dbh_base_tables, @dbh_views);
332
333    ## get list of files to copy
334    my $db_dir = "$datadir/$db";
335    opendir(DBDIR, $db_dir )
336      or die "Cannot open dir '$db_dir': $!";
337
338    my %db_files;
339
340    while ( defined( my $name = readdir DBDIR ) ) {
341        $db_files{$name} = $1 if ( $name =~ /(.+)\.\w+$/ );
342    }
343    closedir( DBDIR );
344
345    unless( keys %db_files ) {
346	warn "'$db' is an empty database\n";
347    }
348
349    ## filter (out) files specified in t_regex
350    my @db_files;
351    if ($rdb->{t_regex}) {
352        @db_files = ($negated
353                     ? grep { $db_files{$_} !~ $t_regex } keys %db_files
354                     : grep { $db_files{$_} =~ $t_regex } keys %db_files );
355    }
356    else {
357        @db_files = keys %db_files;
358    }
359
360    @db_files = sort @db_files;
361
362    my @index_files=();
363
364    ## remove indices unless we're told to keep them
365    if ($opt{noindices}) {
366        @index_files= grep { /\.(ISM|MYI)$/ } @db_files;
367	@db_files = grep { not /\.(ISM|MYI)$/ } @db_files;
368    }
369
370    $rdb->{files}  = [ @db_files ];
371    $rdb->{index}  = [ @index_files ];
372    my @hc_base_tables = map { quote_names("$db.$_") } @dbh_base_tables;
373    my @hc_views = map { quote_names("$db.$_") } @dbh_views;
374
375    my @hc_tables = (@hc_base_tables, @hc_views);
376    $rdb->{tables} = [ @hc_tables ];
377
378    $hc_locks .= ", "  if ( length $hc_locks && @hc_tables );
379    $hc_locks .= join ", ", map { "$_ READ" } @hc_tables;
380
381    $hc_base_tables .= ", "  if ( length $hc_base_tables && @hc_base_tables );
382    $hc_base_tables .= join ", ", @hc_base_tables;
383    $hc_views .= ", "  if ( length $hc_views && @hc_views );
384    $hc_views .= join " READ, ", @hc_views;
385
386    @hc_tables = (@hc_base_tables, @hc_views);
387
388    $num_base_tables += scalar @hc_base_tables;
389    $num_views += scalar @hc_views;
390    $num_tables += $num_base_tables + $num_views;
391    $num_files  += scalar @{$rdb->{files}};
392}
393
394# --- resolve targets for copies ---
395
396if (defined($tgt_name) && length $tgt_name ) {
397    # explicit destination directory specified
398
399    # GNU `cp -r` error message
400    die "copying multiple databases, but last argument ($tgt_dirname) is not a directory\n"
401      if ( @db_desc > 1 && !(-e $tgt_dirname && -d $tgt_dirname ) );
402
403    if ($to_other_database)
404    {
405      foreach my $rdb ( @db_desc ) {
406	$rdb->{target} = "$tgt_dirname";
407      }
408    }
409    elsif ($opt{method} =~ /^scp\b/)
410    {   # we have to trust scp to hit the target
411	foreach my $rdb ( @db_desc ) {
412	    $rdb->{target} = "$tgt_dirname/$rdb->{src}";
413	}
414    }
415    else
416    {
417      die "Last argument ($tgt_dirname) is not a directory\n"
418	if (!(-e $tgt_dirname && -d $tgt_dirname ) );
419      foreach my $rdb ( @db_desc ) {
420	$rdb->{target} = "$tgt_dirname/$rdb->{src}";
421      }
422    }
423  }
424else {
425  die "Error: expected \$opt{suffix} to exist" unless ( exists $opt{suffix} );
426
427  foreach my $rdb ( @db_desc ) {
428    $rdb->{target} = "$datadir/$rdb->{src}$opt{suffix}";
429  }
430}
431
432print Dumper( \@db_desc ) if ( $opt{debug} );
433
434# --- bail out if all specified databases are empty ---
435
436die "No tables to hot-copy" unless ( length $hc_locks );
437
438# --- create target directories if we are using 'cp' ---
439
440my @existing = ();
441
442if ($opt{method} =~ /^cp\b/)
443{
444  foreach my $rdb ( @db_desc ) {
445    push @existing, $rdb->{target} if ( -d  $rdb->{target} );
446  }
447
448  if ( @existing && !($opt{allowold} || $opt{addtodest}) )
449  {
450    $dbh->disconnect();
451    die "Can't hotcopy to '", join( "','", @existing ), "' because directory\nalready exist and the --allowold or --addtodest options were not given.\n"
452  }
453}
454
455retire_directory( @existing ) if @existing && !$opt{addtodest};
456
457foreach my $rdb ( @db_desc ) {
458    my $tgt_dirpath = "$rdb->{target}";
459    # Remove trailing slashes (needed for Mac OS X)
460    substr($tgt_dirpath, 1) =~ s|/+$||;
461    if ( $opt{dryrun} ) {
462        print "mkdir $tgt_dirpath, 0750\n";
463    }
464    elsif ($opt{method} =~ /^scp\b/) {
465        ## assume it's there?
466        ## ...
467    }
468    else {
469        mkdir($tgt_dirpath, 0750) or die "Can't create '$tgt_dirpath': $!\n"
470            unless -d $tgt_dirpath;
471        my @f_info= stat "$datadir/$rdb->{src}";
472        chown $f_info[4], $f_info[5], $tgt_dirpath;
473    }
474}
475
476##############################
477# --- PERFORM THE HOT-COPY ---
478#
479# Note that we try to keep the time between the LOCK and the UNLOCK
480# as short as possible, and only start when we know that we should
481# be able to complete without error.
482
483# read lock all the tables we'll be copying
484# in order to get a consistent snapshot of the database
485
486if ( $opt{checkpoint} || $opt{record_log_pos} ) {
487  # convert existing READ lock on checkpoint and/or log_pos table into WRITE lock
488  foreach my $table ( grep { defined } ( $opt{checkpoint}, $opt{record_log_pos} ) ) {
489    $hc_locks .= ", $table WRITE"
490	unless ( $hc_locks =~ s/$table\s+READ/$table WRITE/ );
491  }
492}
493
494my $hc_started = time;	# count from time lock is granted
495
496if ( $opt{dryrun} ) {
497    if ( $opt{old_server} ) {
498        print "LOCK TABLES $hc_locks\n";
499        print "FLUSH TABLES /*!32323 $hc_tables */\n";
500    }
501    else {
502        # Lock base tables and views separately.
503        print "FLUSH TABLES $hc_base_tables WITH READ LOCK\n"
504          if ( $hc_base_tables );
505        print "LOCK TABLES $hc_views READ\n" if ( $hc_views );
506    }
507
508    print "FLUSH LOGS\n" if ( $opt{flushlog} );
509    print "RESET MASTER\n" if ( $opt{resetmaster} );
510    print "RESET SLAVE\n" if ( $opt{resetslave} );
511}
512else {
513    my $start = time;
514    if ( $opt{old_server} ) {
515        $dbh->do("LOCK TABLES $hc_locks");
516        printf "Locked $num_tables tables in %d seconds.\n", time-$start unless $opt{quiet};
517        $hc_started = time;	# count from time lock is granted
518
519        # flush tables to make on-disk copy up to date
520        $start = time;
521        $dbh->do("FLUSH TABLES /*!32323 $hc_tables */");
522        printf "Flushed tables ($hc_tables) in %d seconds.\n", time-$start unless $opt{quiet};
523    }
524    else {
525        # Lock base tables and views separately, as 'FLUSH TABLES <tbl_name>
526        # ... WITH READ LOCK' (introduced in 5.5) would fail for views.
527        # Also, flush tables to make on-disk copy up to date
528        $dbh->do("FLUSH TABLES $hc_base_tables WITH READ LOCK")
529          if ( $hc_base_tables );
530        printf "Flushed $num_base_tables tables with read lock ($hc_base_tables) in %d seconds.\n",
531               time-$start unless $opt{quiet};
532
533        $start = time;
534        $dbh->do("LOCK TABLES $hc_views READ") if ( $hc_views );
535        printf "Locked $num_views views ($hc_views) in %d seconds.\n",
536               time-$start unless $opt{quiet};
537
538        $hc_started = time;	# count from time lock is granted
539    }
540    $dbh->do( "FLUSH LOGS" ) if ( $opt{flushlog} );
541    $dbh->do( "RESET MASTER" ) if ( $opt{resetmaster} );
542    $dbh->do( "RESET SLAVE" ) if ( $opt{resetslave} );
543
544    if ( $opt{record_log_pos} ) {
545	record_log_pos( $dbh, $opt{record_log_pos} );
546	$dbh->do("FLUSH TABLES /*!32323 $hc_tables */");
547    }
548}
549
550my @failed = ();
551
552foreach my $rdb ( @db_desc )
553{
554  my @files = map { "$datadir/$rdb->{src}/$_" } @{$rdb->{files}};
555  next unless @files;
556
557  eval { copy_files($opt{method}, \@files, $rdb->{target}); };
558  push @failed, "$rdb->{src} -> $rdb->{target} failed: $@"
559    if ( $@ );
560
561  @files = @{$rdb->{index}};
562  if ($rdb->{index})
563  {
564    copy_index($opt{method}, \@files,
565	       "$datadir/$rdb->{src}", $rdb->{target} );
566  }
567
568  if ( $opt{checkpoint} ) {
569    my $msg = ( $@ ) ? "Failed: $@" : "Succeeded";
570
571    eval {
572      $dbh->do( qq{ insert into $opt{checkpoint} (src, dest, msg)
573		      VALUES ( '$rdb->{src}', '$rdb->{target}', '$msg' )
574		    } );
575    };
576
577    if ( $@ ) {
578      warn "Failed to update checkpoint table: $@\n";
579    }
580  }
581}
582
583if ( $opt{dryrun} ) {
584    print "UNLOCK TABLES\n";
585    if ( @existing && !$opt{keepold} ) {
586	my @oldies = map { $_ . '_old' } @existing;
587	print "rm -rf @oldies\n"
588    }
589    $dbh->disconnect();
590    exit(0);
591}
592else {
593    $dbh->do("UNLOCK TABLES");
594}
595
596my $hc_dur = time - $hc_started;
597printf "Unlocked tables.\n" unless $opt{quiet};
598
599#
600# --- HOT-COPY COMPLETE ---
601###########################
602
603$dbh->disconnect;
604
605if ( @failed ) {
606    # hotcopy failed - cleanup
607    # delete any @targets
608    # rename _old copy back to original
609
610    my @targets = ();
611    foreach my $rdb ( @db_desc ) {
612        push @targets, $rdb->{target} if ( -d  $rdb->{target} );
613    }
614    print "Deleting @targets \n" if $opt{debug};
615
616    print "Deleting @targets \n" if $opt{debug};
617    rmtree([@targets]);
618    if (@existing) {
619	print "Restoring @existing from back-up\n" if $opt{debug};
620        foreach my $dir ( @existing ) {
621	    rename("${dir}_old", $dir )
622	      or warn "Can't rename ${dir}_old to $dir: $!\n";
623	}
624    }
625
626    die join( "\n", @failed );
627}
628else {
629    # hotcopy worked
630    # delete _old unless $opt{keepold}
631
632    if ( @existing && !$opt{keepold} ) {
633	my @oldies = map { $_ . '_old' } @existing;
634	print "Deleting previous copy in @oldies\n" if $opt{debug};
635	rmtree([@oldies]);
636    }
637
638    printf "$0 copied %d tables (%d files) in %d second%s (%d seconds overall).\n",
639	    $num_tables, $num_files,
640	    $hc_dur, ($hc_dur==1)?"":"s", time - $start_time
641	unless $opt{quiet};
642}
643
644exit 0;
645
646
647# ---
648
649sub copy_files {
650    my ($method, $files, $target) = @_;
651    my @cmd;
652    print "Copying ".@$files." files...\n" unless $opt{quiet};
653
654    if ($method =~ /^s?cp\b/)  # cp or scp with optional flags
655    {
656	my $cp = $method;
657	# add option to preserve mod time etc of copied files
658	# not critical, but nice to have
659	$cp.= " -p" if $^O =~ m/^(solaris|linux|freebsd|darwin)$/;
660
661	# add recursive option for scp
662	$cp.= " -r" if $^O =~ /m^(solaris|linux|freebsd|darwin)$/ && $method =~ /^scp\b/;
663
664	# perform the actual copy
665	safe_system( $cp, (map { "'$_'" } @$files), "'$target'" );
666    }
667    else
668    {
669	die "Can't use unsupported method '$method'\n";
670    }
671}
672
673#
674# Copy only the header of the index file
675#
676
677sub copy_index
678{
679  my ($method, $files, $source, $target) = @_;
680
681  print "Copying indices for ".@$files." files...\n" unless $opt{quiet};
682  foreach my $file (@$files)
683  {
684    my $from="$source/$file";
685    my $to="$target/$file";
686    my $buff;
687    open(INPUT, "<$from") || die "Can't open file $from: $!\n";
688    binmode(INPUT, ":raw");
689    my $length=read INPUT, $buff, 2048;
690    die "Can't read index header from $from\n" if ($length < 1024);
691    close INPUT;
692
693    if ( $opt{dryrun} )
694    {
695      print "$opt{method}-header $from $to\n";
696    }
697    elsif ($opt{method} eq 'cp')
698    {
699      open(OUTPUT,">$to")   || die "Can\'t create file $to: $!\n";
700      if (syswrite(OUTPUT,$buff) != length($buff))
701      {
702	die "Error when writing data to $to: $!\n";
703      }
704      close OUTPUT	   || die "Error on close of $to: $!\n";
705    }
706    elsif ($opt{method} =~ /^scp\b/)
707    {
708      my ($fh, $tmp)= tempfile('mysqlhotcopy-XXXXXX', DIR => $opt_tmpdir) or
709	die "Can\'t create/open file in $opt_tmpdir\n";
710      if (syswrite($fh,$buff) != length($buff))
711      {
712	die "Error when writing data to $tmp: $!\n";
713      }
714      close $fh || die "Error on close of $tmp: $!\n";
715      safe_system("$opt{method} $tmp $to");
716      unlink $tmp;
717    }
718    else
719    {
720      die "Can't use unsupported method '$opt{method}'\n";
721    }
722  }
723}
724
725
726sub safe_system {
727  my @sources= @_;
728  my $method= shift @sources;
729  my $target= pop @sources;
730  ## @sources = list of source file names
731
732  ## We have to deal with very long command lines, otherwise they may generate
733  ## "Argument list too long".
734  ## With 10000 tables the command line can be around 1MB, much more than 128kB
735  ## which is the common limit on Linux (can be read from
736  ## /usr/src/linux/include/linux/binfmts.h
737  ## see http://www.linuxjournal.com/article.php?sid=6060).
738
739  my $chunk_limit= 100 * 1024; # 100 kB
740  my @chunk= ();
741  my $chunk_length= 0;
742  foreach (@sources) {
743      push @chunk, $_;
744      $chunk_length+= length($_);
745      if ($chunk_length > $chunk_limit) {
746          safe_simple_system($method, @chunk, $target);
747          @chunk=();
748          $chunk_length= 0;
749      }
750  }
751  if ($chunk_length > 0) { # do not forget last small chunk
752      safe_simple_system($method, @chunk, $target);
753  }
754}
755
756sub safe_simple_system {
757    my @cmd= @_;
758
759    if ( $opt{dryrun} ) {
760        print "@cmd\n";
761    }
762    else {
763        ## for some reason system fails but backticks works ok for scp...
764        print "Executing '@cmd'\n" if $opt{debug};
765        my $cp_status = system "@cmd > /dev/null";
766        if ($cp_status != 0) {
767            warn "Executing command failed ($cp_status). Trying backtick execution...\n";
768            ## try something else
769            `@cmd` || die "Error: @cmd failed ($?) while copying files.\n";
770        }
771    }
772}
773
774sub retire_directory {
775    my ( @dir ) = @_;
776
777    foreach my $dir ( @dir ) {
778	my $tgt_oldpath = $dir . '_old';
779	if ( $opt{dryrun} ) {
780	    print "rmtree $tgt_oldpath\n" if ( -d $tgt_oldpath );
781	    print "rename $dir, $tgt_oldpath\n";
782	    next;
783	}
784
785	if ( -d $tgt_oldpath ) {
786	    print "Deleting previous 'old' hotcopy directory ('$tgt_oldpath')\n" unless $opt{quiet};
787	    rmtree([$tgt_oldpath],0,1);
788	}
789	rename($dir, $tgt_oldpath)
790	  or die "Can't rename $dir=>$tgt_oldpath: $!\n";
791	print "Existing hotcopy directory renamed to '$tgt_oldpath'\n" unless $opt{quiet};
792    }
793}
794
795sub record_log_pos {
796    my ( $dbh, $table_name ) = @_;
797
798    eval {
799	my ($file,$position) = get_row( $dbh, "show master status" );
800	die "master status is undefined" if !defined $file || !defined $position;
801
802	my $row_hash = get_row_hash( $dbh, "show slave status" );
803	my ($master_host, $log_file, $log_pos );
804	if ( $dbh->{mysql_serverinfo} =~ /^3\.23/ ) {
805	    ($master_host, $log_file, $log_pos )
806	      = @{$row_hash}{ qw / Master_Host Log_File Pos / };
807	} else {
808	    ($master_host, $log_file, $log_pos )
809	      = @{$row_hash}{ qw / Master_Host Relay_Master_Log_File Exec_Master_Log_Pos / };
810	}
811	my $hostname = hostname();
812
813	$dbh->do( qq{ replace into $table_name
814			  set host=?, log_file=?, log_pos=?,
815                          master_host=?, master_log_file=?, master_log_pos=? },
816		  undef,
817		  $hostname, $file, $position,
818		  $master_host, $log_file, $log_pos  );
819
820    };
821
822    if ( $@ ) {
823	warn "Failed to store master position: $@\n";
824    }
825}
826
827sub get_row {
828  my ( $dbh, $sql ) = @_;
829
830  my $sth = $dbh->prepare($sql);
831  $sth->execute;
832  return $sth->fetchrow_array();
833}
834
835sub get_row_hash {
836  my ( $dbh, $sql ) = @_;
837
838  my $sth = $dbh->prepare($sql);
839  $sth->execute;
840  return $sth->fetchrow_hashref();
841}
842
843sub get_list_of_tables {
844    my ( $db ) = @_;
845
846    my $tables =
847        eval {
848            $dbh->selectall_arrayref('SHOW FULL TABLES FROM ' .
849                                     $dbh->quote_identifier($db) .
850                                     ' WHERE Table_type = \'BASE TABLE\'')
851        } || [];
852    warn "Unable to retrieve list of tables in $db: $@" if $@;
853
854    return (map { $_->[0] } @$tables);
855}
856
857sub get_list_of_views {
858    my ( $db ) = @_;
859
860    my $views =
861        eval {
862            $dbh->selectall_arrayref('SHOW FULL TABLES FROM ' .
863                                     $dbh->quote_identifier($db) .
864                                     ' WHERE Table_type = \'VIEW\'')
865        } || [];
866    warn "Unable to retrieve list of views in $db: $@" if $@;
867
868    return (map { $_->[0] } @$views);
869}
870
871sub quote_names {
872  my ( $name ) = @_;
873  # given a db.table name, add quotes
874
875  my ($db, $table, @cruft) = split( /\./, $name );
876  die "Invalid db.table name '$name'" if (@cruft || !defined $db || !defined $table );
877
878  # Earlier versions of DBD return table name non-quoted,
879  # such as DBD-2.1012 and the newer ones, such as DBD-2.9002
880  # returns it quoted. Let's have a support for both.
881  $table=~ s/\`//g;
882  return "`$db`.`$table`";
883}
884
885__END__
886
887=head1 DESCRIPTION
888
889mysqlhotcopy is designed to make stable copies of live MySQL databases.
890
891Here "live" means that the database server is running and the database
892may be in active use. And "stable" means that the copy will not have
893any corruptions that could occur if the table files were simply copied
894without first being locked and flushed from within the server.
895
896=head1 OPTIONS
897
898=over 4
899
900=item --checkpoint checkpoint-table
901
902As each database is copied, an entry is written to the specified
903checkpoint-table.  This has the happy side-effect of updating the
904MySQL update-log (if it is switched on) giving a good indication of
905where roll-forward should begin for backup+rollforward schemes.
906
907The name of the checkpoint table should be supplied in database.table format.
908The checkpoint-table must contain at least the following fields:
909
910=over 4
911
912  time_stamp timestamp not null
913  src varchar(32)
914  dest varchar(60)
915  msg varchar(255)
916
917=back
918
919=item --record_log_pos log-pos-table
920
921Just before the database files are copied, update the record in the
922log-pos-table from the values returned from "show master status" and
923"show slave status". The master status values are stored in the
924log_file and log_pos columns, and establish the position in the binary
925logs that any slaves of this host should adopt if initialised from
926this dump.  The slave status values are stored in master_host,
927master_log_file, and master_log_pos, corresponding to the coordinates
928of the next to the last event the slave has executed. The slave or its
929siblings can connect to the master next time and request replication
930starting from the recorded values.
931
932The name of the log-pos table should be supplied in database.table format.
933A sample log-pos table definition:
934
935=over 4
936
937CREATE TABLE log_pos (
938  host            varchar(60) NOT null,
939  time_stamp      timestamp(14) NOT NULL,
940  log_file        varchar(32) default NULL,
941  log_pos         int(11)     default NULL,
942  master_host     varchar(60) NULL,
943  master_log_file varchar(32) NULL,
944  master_log_pos  int NULL,
945
946  PRIMARY KEY  (host)
947);
948
949=back
950
951
952=item --suffix suffix
953
954Each database is copied back into the originating datadir under
955a new name. The new name is the original name with the suffix
956appended.
957
958If only a single db_name is supplied and the --suffix flag is not
959supplied, then "--suffix=_copy" is assumed.
960
961=item --allowold
962
963Move any existing version of the destination to a backup directory for
964the duration of the copy. If the copy successfully completes, the backup
965directory is deleted - unless the --keepold flag is set.  If the copy fails,
966the backup directory is restored.
967
968The backup directory name is the original name with "_old" appended.
969Any existing versions of the backup directory are deleted.
970
971=item --keepold
972
973Behaves as for the --allowold, with the additional feature
974of keeping the backup directory after the copy successfully completes.
975
976=item --addtodest
977
978Don't rename target directory if it already exists, just add the
979copied files into it.
980
981This is most useful when backing up a database with many large
982tables and you don't want to have all the tables locked for the
983whole duration.
984
985In this situation, I<if> you are happy for groups of tables to be
986backed up separately (and thus possibly not be logically consistent
987with one another) then you can run mysqlhotcopy several times on
988the same database each with different db_name./table_regex/.
989All but the first should use the --addtodest option so the tables
990all end up in the same directory.
991
992=item --flushlog
993
994Rotate the log files by executing "FLUSH LOGS" after all tables are
995locked, and before they are copied.
996
997=item --resetmaster
998
999Reset the bin-log by executing "RESET MASTER" after all tables are
1000locked, and before they are copied. Useful if you are recovering a
1001slave in a replication setup.
1002
1003=item --resetslave
1004
1005Reset the master.info by executing "RESET SLAVE" after all tables are
1006locked, and before they are copied. Useful if you are recovering a
1007server in a mutual replication setup.
1008
1009=item --regexp pattern
1010
1011Copy all databases with names matching the pattern.
1012
1013=item --regexp /pattern1/./pattern2/
1014
1015Copy all tables with names matching pattern2 from all databases with
1016names matching pattern1. For example, to select all tables which
1017names begin with 'bar' from all databases which names end with 'foo':
1018
1019   mysqlhotcopy --indices --method=cp --regexp /foo$/./^bar/
1020
1021=item db_name./pattern/
1022
1023Copy only tables matching pattern. Shell metacharacters ( (, ), |, !,
1024etc.) have to be escaped (e.g., \). For example, to select all tables
1025in database db1 whose names begin with 'foo' or 'bar':
1026
1027    mysqlhotcopy --indices --method=cp db1./^\(foo\|bar\)/
1028
1029=item db_name./~pattern/
1030
1031Copy only tables not matching pattern. For example, to copy tables
1032that do not begin with foo nor bar:
1033
1034    mysqlhotcopy --indices --method=cp db1./~^\(foo\|bar\)/
1035
1036=item -?, --help
1037
1038Display help-screen and exit.
1039
1040=item -u, --user=#
1041
1042User for database login if not current user.
1043
1044=item -p, --password=#
1045
1046Password to use when connecting to the server. Note that you are strongly
1047encouraged *not* to use this option as every user would be able to see the
1048password in the process list. Instead use the '[mysqlhotcopy]' section in
1049one of the config files, normally /etc/my.cnf or your personal ~/.my.cnf.
1050(See the chapter 'my.cnf Option Files' in the manual.)
1051
1052=item -h, -h, --host=#
1053
1054Hostname for local server when connecting over TCP/IP.  By specifying this
1055different from 'localhost' will trigger mysqlhotcopy to use TCP/IP connection.
1056
1057=item -P, --port=#
1058
1059Port to use when connecting to MySQL server with TCP/IP.  This is only used
1060when using the --host option.
1061
1062=item -S, --socket=#
1063
1064UNIX domain socket to use when connecting to local server.
1065
1066=item --old_server
1067
1068Use old server (pre v5.5) commands.
1069
1070=item  --noindices
1071
1072Don\'t include index files in copy. Only up to the first 2048 bytes
1073are copied;  You can restore the indexes with isamchk -r or myisamchk -r
1074on the backup.
1075
1076=item  --method=#
1077
1078Method for copy (only "cp" currently supported). Alpha support for
1079"scp" was added in November 2000. Your experience with the scp method
1080will vary with your ability to understand how scp works. 'man scp'
1081and 'man ssh' are your friends.
1082
1083The destination directory _must exist_ on the target machine using the
1084scp method. --keepold and --allowold are meaningless with scp.
1085Liberal use of the --debug option will help you figure out what\'s
1086really going on when you do an scp.
1087
1088Note that using scp will lock your tables for a _long_ time unless
1089your network connection is _fast_. If this is unacceptable to you,
1090use the 'cp' method to copy the tables to some temporary area and then
1091scp or rsync the files at your leisure.
1092
1093=item -q, --quiet
1094
1095Be silent except for errors.
1096
1097=item  --debug
1098
1099Debug messages are displayed.
1100
1101=item -n, --dryrun
1102
1103Display commands without actually doing them.
1104
1105=back
1106
1107=head1 WARRANTY
1108
1109This software is free and comes without warranty of any kind. You
1110should never trust backup software without studying the code yourself.
1111Study the code inside this script and only rely on it if I<you> believe
1112that it does the right thing for you.
1113
1114Patches adding bug fixes, documentation and new features are welcome.
1115Please send these to internals@lists.mysql.com.
1116
1117=head1 TO DO
1118
1119Extend the individual table copy to allow multiple subsets of tables
1120to be specified on the command line:
1121
1122  mysqlhotcopy db newdb  t1 t2 /^foo_/ : t3 /^bar_/ : +
1123
1124where ":" delimits the subsets, the /^foo_/ indicates all tables
1125with names beginning with "foo_" and the "+" indicates all tables
1126not copied by the previous subsets.
1127
1128'newdb' is either the name of the new database, or the full path name
1129of the new database file. The database should not already exist.
1130
1131Add option to lock each table in turn for people who don\'t need
1132cross-table integrity.
1133
1134Add option to FLUSH STATUS just before UNLOCK TABLES.
1135
1136Add support for other copy methods (e.g., tar to single file?).
1137
1138Add support for forthcoming MySQL ``RAID'' table subdirectory layouts.
1139
1140=head1 AUTHOR
1141
1142Tim Bunce
1143
1144Martin Waite - Added checkpoint, flushlog, regexp and dryrun options.
1145               Fixed cleanup of targets when hotcopy fails.
1146               Added --record_log_pos.
1147               RAID tables are now copied (don't know if this works over scp).
1148
1149Ralph Corderoy - Added synonyms for commands.
1150
1151Scott Wiersdorf - Added table regex and scp support.
1152
1153Monty - Working --noindex (copy only first 2048 bytes of index file).
1154        Fixes for --method=scp.
1155
1156Ask Bjoern Hansen - Cleanup code to fix a few bugs and enable -w again.
1157
1158Emil S. Hansen - Added resetslave and resetmaster.
1159
1160Jeremy D. Zawodny - Removed deprecated DBI calls.  Fixed bug which
1161resulted in nothing being copied when a regexp was specified but no
1162database name(s).
1163
1164Martin Waite - Fix to handle database name that contains space.
1165
1166Paul DuBois - Remove end '/' from directory names.
1167