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