00001 #!/usr/bin/perl -w
00002 #
00003 # mythconverg_backup.pl
00004 #
00005 # Creates a backup of the MythTV database.
00006 #
00007 # For details, see:
00008 # mythconverg_backup.pl --help
00009
00010 # Includes
00011 use Getopt::Long;
00012 use File::Temp qw/ tempfile /;
00013
00014 # Script info
00015 $NAME = 'MythTV Database Backup Script';
00016 $VERSION = '1.0.11';
00017
00018 # Some variables we'll use here
00019 our ($username, $homedir, $mythconfdir, $database_information_file);
00020 our ($mysqldump, $compress, $rotate, $rotateglob, $backup_xmltvids);
00021 our ($usage, $debug, $show_version, $show_version_script, $dbh);
00022 our ($d_db_name, $d_mysqldump, $d_compress, $d_rotate, $d_rotateglob);
00023 # This script does not accept a database password on the command-line.
00024 # Any packager who enables the functionality should modify the --help output.
00025 # our ($db_password);
00026 our ($db_hostname, $db_port, $db_username, $db_name, $db_schema_version);
00027 our ($backup_directory, $backup_filename);
00028 our ($verbose_level_always, $verbose_level_debug, $verbose_level_error);
00029
00030 our %mysql_conf = ('db_host' => '',
00031 'db_port' => -1,
00032 'db_user' => '',
00033 'db_pass' => '',
00034 'db_name' => '',
00035 'db_schemaver' => ''
00036 );
00037 our %backup_conf = ('directory' => '',
00038 'filename' => ''
00039 );
00040
00041 # Variables used to untaint data
00042 our $is_env_tainted = 1;
00043 our $old_env_path = $ENV{"PATH"};
00044 our @d_allowed_paths = ("/bin",
00045 "/usr/bin",
00046 "/usr/local/bin",
00047 "/sbin",
00048 "/usr/sbin",
00049 "/usr/local/sbin"
00050 );
00051 our @allowed_paths;
00052
00053 # Debug levels
00054 $verbose_level_always = 0;
00055 $verbose_level_debug = 1;
00056 $verbose_level_error = 255;
00057
00058 # Defaults
00059 $d_db_name = 'mythconverg';
00060 $d_mysqldump = 'mysqldump';
00061 $d_compress = 'gzip';
00062 $d_rotate = 5;
00063 $d_rotateglob = $d_db_name.'-????-??????????????.sql*';
00064
00065 # Provide default values for GetOptions
00066 $mysqldump = $d_mysqldump;
00067 $compress = $d_compress;
00068 $rotate = $d_rotate;
00069 $rotateglob = $d_rotateglob;
00070 $debug = 0;
00071
00072 # Load the cli options
00073 GetOptions('hostname|DBHostName=s' => \$db_hostname,
00074 'port|DBPort=i' => \$db_port,
00075 'username|DBUserName=s' => \$db_username,
00076 # This script does not accept a database password on the command-line.
00077 # 'password|DBPassword=s' => \$db_password,
00078 'name|DBName=s' => \$db_name,
00079 'schemaver|DBSchemaVer=s' => \$db_schema_version,
00080 'directory|DBBackupDirectory=s' => \$backup_directory,
00081 'filename|DBBackupFilename=s' => \$backup_filename,
00082 'mysqldump=s' => \$mysqldump,
00083 'compress=s' => \$compress,
00084 'rotate=i' => \$rotate,
00085 'rotateglob|glob=s' => \$rotateglob,
00086 'backup_xmltvids|backup-xmltvids|'.
00087 'xmltvids' => \$backup_xmltvids,
00088 'usage|help|h+' => \$usage,
00089 'version' => \$show_version,
00090 'script_version|script-version|v' => \$show_version_script,
00091 'verbose|debug|d+' => \$debug
00092 );
00093
00094 # Print version information
00095 sub print_version_information
00096 {
00097 my $script_name = substr $0, rindex($0, '/') + 1;
00098 print "$NAME\n$script_name\nversion: $VERSION\n";
00099 }
00100
00101 if ($show_version_script)
00102 {
00103 print "$NAME,$VERSION,,\n";
00104 exit;
00105 }
00106 elsif ($show_version)
00107 {
00108 print_version_information;
00109 exit;
00110 }
00111
00112
00113 # Print usage
00114 if ($usage)
00115 {
00116 print_version_information;
00117 print <<EOF;
00118
00119 Usage:
00120 $0 [options|database_information_file]
00121
00122 Creates a backup of the MythTV database.
00123
00124 QUICK START:
00125
00126 Create a file ~/.mythtv/backuprc with a single line,
00127 "DBBackupDirectory=/home/mythtv" (no quotes), and run this script to create a
00128 database backup. Use the --verbose argument to see what is happening.
00129
00130 # echo "DBBackupDirectory=/home/mythtv" > ~/.mythtv/backuprc
00131 # $0 --verbose
00132
00133 Make sure you keep the backuprc file for next time. Once you have successfully
00134 created a backup, the script may be run without the --verbose argument.
00135
00136 To backup xmltvids:
00137
00138 Ensure you have a ~/.mythtv/backuprc file, as described above, and execute this
00139 script with the --backup_xmltvids argument.
00140
00141 # $0 --backup_xmltvids
00142
00143 EOF
00144
00145 if ($usage > 1)
00146 {
00147 print <<EOF;
00148 DETAILED DESCRIPTION:
00149
00150 This script can be called by MythTV for creating automatic database backups.
00151 In this mode, it is always exected with a single command-line argument
00152 specifying the name of a "database information file" (see DATABASE INFORMATION
00153 FILE, below), which contains sufficient information about the database and the
00154 backup to allow the script to create a backup without needing any additional
00155 configuration files. In this mode, all other MythTV configuration files
00156 (including config.xml, mysql.txt) are ignored, but the backup resource file
00157 (see RESOURCE FILE, below) and the MySQL option files (i.e. /etc/my.cnf or
00158 ~/.my.cnf) will be honored.
00159
00160 The script can also be called interactively (i.e. "manually") by the user to
00161 create a database backup on demand. Required information may be passed into
00162 the script using command-line arguments or with a database information file.
00163 If a database information file is specified, all command-line arguments will be
00164 ignored. If no database information file is specified, the script will attempt
00165 to determine the appropriate configuration by using the MythTV configuration
00166 file(s) (prefering config.xml, but falling back to mysql.txt if no config.xml
00167 exists). Once the MythTV configuration file has been parsed, the backup
00168 resource file (see RESOURCE FILE, below) will be parsed, then command-line
00169 arguments will be applied (thus overriding any values determined from the
00170 configuration files).
00171
00172 The only information required by the script is the directory in which the
00173 backup should be created. Therefore, when using a database information file,
00174 the DBBackupDirectory should be specified, or if running manually, the
00175 --directory command-line argument should be specified. The DBBackupDirectory
00176 may be specified in a backup resource file (see RESOURCE FILE, below). Doing
00177 so is especially useful for manual backups. If the specified directory is not
00178 writable, the script will terminate. Likewise, if a file whose name matches
00179 the name to be used for the backup file already exists, the script will
00180 terminate.
00181
00182 If the database name is not specified, the script will attempt to use the
00183 MythTV default database name, $d_db_name. Note that the same is not true for
00184 the database username and database password. These must be explicitly
00185 specified. The password must be specified in a database information file, a
00186 backup resource file, or a MySQL options file. The username may be specified
00187 the same way or may be specified using a command-line argument if not using a
00188 database information file.
00189
00190 While this script may be called while MythTV is running, there is a possibility
00191 of creating a backup with data integrity errors (i.e. if MythTV updates data in
00192 multiple tables between the time the script backs up the first and subsequent
00193 tables). Also, depending on your system configuration, performing a backup
00194 (which may result in locking a table while it is being backed up) while
00195 recording may cause corruption of the recording or inability to properly write
00196 recording data (such as the recording seek table) to the database.
00197 Therefore, if configuring this script to run in a cron job, try to ensure it
00198 runs at a time when recordings are least likely to occur. Alternatively, by
00199 choosing to run the script in a system start/shutdown script (i.e. an init
00200 script), you may call the script before starting mythbackend or after stopping
00201 mythbackend. Note, however, that checking whether to perform the backup is the
00202 responsibility of the init script (not this script)--i.e. in a system with
00203 multiple frontends/backends, the init script should ensure the backup is
00204 created only on the master backend.
00205
00206 DATABASE INFORMATION FILE
00207
00208 The database information file contains information about the database and the
00209 backup. The information within the file is specified as name=value pairs using
00210 the same names as used by the MythTV config.xml and mysql.txt configuration
00211 files. The following variables are recognized:
00212
00213 DBHostName - The hostname (or IP address) which should be used to find the
00214 MySQL server.
00215 DBPort - The TCP/IP port number to use for the connection. This may have a
00216 value of 0, i.e. if the hostname is localhost or if the server is
00217 using the default MySQL port or the port specified in a MySQL
00218 options file.
00219 DBUserName - The database username to use when connecting to the server.
00220 DBPassword - The password to use when connecting to the server.
00221 DBName - The name of the database that contains the MythTV data.
00222 DBSchemaVer - The MythTV schema version of the database. This value will be
00223 used to create the backup filename, but only if the filename
00224 has not been specified using DBBackupFilename or the --filename
00225 argument.
00226 DBBackupDirectory - The directory in which the backup file should be
00227 created. This directory may have been specially
00228 configured by the user as the "DB Backups" storage
00229 group. It is recommended that this directory be
00230 used--especially in "common-use" scripts such as those
00231 provided by distributions.
00232 DBBackupFilename - The name of the file in which the backup should be
00233 created. Additional extensions may be added by this
00234 script as required (i.e. adding an appropriate suffix,
00235 such as ".gz", to the file if it is compressed). If the
00236 filename recommended by mythbackend is used, it will be
00237 displayed in the GUI messages provided for the user. If
00238 the recommended filename is not used, the user will not be
00239 told where to find the backup file. If no value is
00240 provided, a filename using the default filename format
00241 will be chosen.
00242 mysqldump - The path (including filename) of the mysqldump executable.
00243 compress - The command (including path, if necessary) to use to
00244 compress the backup. Using gzip is significantly less
00245 resource intensive on an SQL backup file than using bzip2,
00246 at the cost of a slightly (about 33%) larger compressed
00247 filesize, a difference which should be irrelevant at the
00248 filesizes involved (especially when compared to the size
00249 of recording files). If you decide to use another
00250 compression algorithm, please ensure you test it
00251 appropriately to verify it does not negatively affect
00252 operation of your system. If no value is specified for
00253 compress or if the value '$d_compress' is specified, the
00254 script will first attempt to use the IO::Compress::Gzip
00255 module to compress the backup file, but, if not available,
00256 will run the command specified. Therefore, if
00257 IO::Compress::Gzip is installed and functional, specifying
00258 a value for compress is unnecessary. If neither approach
00259 works, the backup file will be left uncompressed.
00260 rotate - The number of backups to keep when rotating. To disable
00261 rotation, specify -1. Backup rotation is performed by
00262 identifying all files in DBBackupDirectory whose names
00263 match the glob specified by rotateglob. It is critical
00264 that the chosen backup filenames can be sorted properly
00265 using an alphabetical sort. If using the default filename
00266 format--which contains the DBSchemaVer--and you downgrade
00267 MythTV and restore a backup from an older DBSchemaVer,
00268 make sure you move the backups from the newer DBSchemaVer
00269 out of the DBBackupDirectory or they may cause your new
00270 backups to be deleted.
00271 rotateglob - The sh-like glob used to identify files within
00272 DBBackupDirectory to be considered for rotation. Be
00273 very careful with the value--especially if using a
00274 DBBackupDirectory that contains any files other than
00275 backups.
00276
00277 RESOURCE FILE
00278
00279 The backup resource file specifies values using the same format as described
00280 for the database information file, above, but is intended as a "permanent,"
00281 user-created configuration file. The database information file is intended as
00282 a "single-use" configuration file, often created automatically (i.e. by a
00283 program, such as mythbackend, or a script). The backup resource file should be
00284 placed at "~/.mythtv/backuprc" and given appropriate permissions. To be usable
00285 by the script, it must be readable. However, it should be protected as
00286 required--i.e. if the DBPassword is specified, it should be made readable only
00287 by the owner.
00288
00289 When specifying a database information file, the resource file is parsed before
00290 the database information file to prevent the resource file from overriding the
00291 information in the database information file. When no database information
00292 file is specified, the resource file is parsed after the MythTV configuration
00293 files, but before the command-line arguments to allow the resource file to
00294 override values in the configuration files and to allow command-line arguments
00295 to override resource file defaults.
00296
00297 options:
00298
00299 --hostname [database hostname]
00300
00301 The hostname (or IP address) which should be used to find the MySQL server.
00302 See DBHostName, above.
00303
00304 --port [database port]
00305
00306 The TCP/IP port number to use for connection to the MySQL server. See
00307 DBPort, above.
00308
00309 --username [database username]
00310
00311 The MySQL username to use for connection to the MySQL server. See
00312 DBUserName, above.
00313
00314 --name [database name]
00315
00316 The name of the database containing the MythTV data. See DBName, above.
00317
00318 Default: $d_db_name
00319
00320 --schemaver [MythTV database schema version]
00321
00322 The MythTV schema version. See DBSchemaVer, above.
00323
00324 --directory [directory]
00325
00326 The directory in which the backup file should be stored. See
00327 DBBackupDirectory, above.
00328
00329 --filename [database backup filename]
00330
00331 The name to use for the database backup file. If not provided, a filename
00332 using a default format will be chosen. See DBBackupFilename, above.
00333
00334 --mysqldump [path]
00335
00336 The path (including filename) of the mysqldump executable. See mysqldump
00337 in the DATABASE INFORMATION FILE description, above.
00338
00339 Default: $d_mysqldump
00340
00341 --compress [path]
00342
00343 The command (including path, if necessary) to use to compress the backup.
00344 See compress in the DATABASE INFORMATION FILE description, above.
00345
00346 Default: $d_compress
00347
00348 --rotate [number]
00349 The number of backups to keep when rotating. To disable rotation, specify
00350 -1. See rotate in the DATABASE INFORMATION FILE description, above.
00351
00352 Default: $d_rotate
00353
00354 --rotateglob [glob]
00355 The sh-like glob used to identify files within DBBackupDirectory to be
00356 considered for rotation. See rotateglob in the DATABASE INFORMATION FILE
00357 description, above.
00358
00359 Default: $d_rotateglob
00360
00361 --backup_xmltvids
00362 Rather than creating a backup of the entire database, create a backup of
00363 xmltvids. This is useful when doing a full channel scan. The resulting
00364 backup is a series of SQL UPDATE statements that can be executed to set
00365 the xmltvid for channels whose callsign is the same before and after
00366 the scan. Note that the backup file will contain comments with additional
00367 channel information, which you can use to identify channels in case the
00368 callsign changes.
00369
00370 --help
00371
00372 Show this help text.
00373
00374 --version
00375
00376 Show version information.
00377
00378 --verbose
00379
00380 Show what is happening.
00381
00382 --script_version | -v
00383
00384 Show script version information. This is primarily useful for scripts
00385 or programs needing to parse the version information.
00386
00387 EOF
00388 }
00389 else
00390 {
00391 print "For detailed help:\n\n# $0 --help --help\n\n";
00392 }
00393 exit;
00394 }
00395
00396 sub verbose
00397 {
00398 my $level = shift;
00399 my $error = 0;
00400 if ($level == $verbose_level_error)
00401 {
00402 $error = 1;
00403 }
00404 else
00405 {
00406 return unless ($debug >= $level);
00407 }
00408 print { $error ? STDERR : STDOUT } join("\n", @_), "\n";
00409 }
00410
00411 sub print_configuration
00412 {
00413 verbose($verbose_level_debug,
00414 '',
00415 'Database Information:',
00416 " DBHostName: $mysql_conf{'db_host'}",
00417 " DBPort: $mysql_conf{'db_port'}",
00418 " DBUserName: $mysql_conf{'db_user'}",
00419 ' DBPassword: ' .
00420 ( $mysql_conf{'db_pass'} ? 'XXX' : '' ),
00421 # "$mysql_conf{'db_pass'}",
00422 " DBName: $mysql_conf{'db_name'}",
00423 " DBSchemaVer: $mysql_conf{'db_schemaver'}",
00424 " DBBackupDirectory: $backup_conf{'directory'}",
00425 " DBBackupFilename: $backup_conf{'filename'}");
00426 verbose($verbose_level_debug,
00427 '',
00428 'Executables:',
00429 " mysqldump: $mysqldump",
00430 " compress: $compress");
00431 }
00432
00433 sub configure_environment
00434 {
00435 verbose($verbose_level_debug,
00436 '', 'Configuring environment:');
00437
00438 # Get the user's login and home directory, so we can look for config files
00439 ($username, $homedir) = (getpwuid $>)[0,7];
00440 $username = $ENV{'USER'} if ($ENV{'USER'});
00441 $homedir = $ENV{'HOME'} if ($ENV{'HOME'});
00442 if ($username && !$homedir)
00443 {
00444 $homedir = "/home/$username";
00445 if (!-e $homedir && -e "/Users/$username")
00446 {
00447 $homedir = "/Users/$username";
00448 }
00449 }
00450 verbose($verbose_level_debug,
00451 " - username: $username",
00452 " - HOME: $homedir");
00453
00454 # Find the config directory
00455 $mythconfdir = $ENV{'MYTHCONFDIR'}
00456 ? $ENV{'MYTHCONFDIR'}
00457 : "$homedir/.mythtv"
00458 ;
00459
00460 verbose($verbose_level_debug,
00461 " - MYTHCONFDIR: $mythconfdir");
00462 }
00463
00464 # Though much of the configuration file parsing could be done by the MythTV
00465 # Perl bindings, using them to retrieve database information is not appropriate
00466 # for a backup script. The Perl bindings require the backend to be running and
00467 # use UPnP for autodiscovery. Also, parsing the files "locally" allows
00468 # supporting even the old MythTV database configuration file, mysql.txt.
00469 sub parse_database_information
00470 {
00471 my $file = shift;
00472 verbose($verbose_level_debug,
00473 " - checking: $file");
00474 return 0 unless ($file && -e $file);
00475 verbose($verbose_level_debug,
00476 " parsing: $file");
00477 open(CONF, $file) or die("\nERROR: Unable to read $file: $!".
00478 ', stopped');
00479 while (my $line = <CONF>)
00480 {
00481 # Cleanup
00482 next if ($line =~ m/^\s*#/);
00483 $line =~ s/^str
00484 chomp($line);
00485 $line =~ s/^\s+
00486 $line =~ s/\s+$
00487 # Split off the var=val pairs
00488 my ($var, $val) = split(/ *[\=\: ] */, $line, 2);
00489 # Also look for <var>val</var> from config.xml
00490 if ($line =~ m/<(\w+)>(.+)<\/(\w+)>$/ && $1 eq $3)
00491 {
00492 $var = $1; $val = $2;
00493 }
00494 next unless ($var && $var =~ m/\w/);
00495 if (($var eq 'Host') || ($var eq 'DBHostName'))
00496 {
00497 $mysql_conf{'db_host'} = $val;
00498 }
00499 elsif (($var eq 'Port') || ($var eq 'DBPort'))
00500 {
00501 $mysql_conf{'db_port'} = $val;
00502 }
00503 elsif (($var eq 'UserName') || ($var eq 'DBUserName'))
00504 {
00505 $mysql_conf{'db_user'} = $val;
00506 }
00507 elsif (($var eq 'Password') || ($var eq 'DBPassword'))
00508 {
00509 $mysql_conf{'db_pass'} = $val;
00510 $mysql_conf{'db_pass'} =~ s/&/&/sg;
00511 $mysql_conf{'db_pass'} =~ s/>/>/sg;
00512 $mysql_conf{'db_pass'} =~ s/</</sg;
00513 }
00514 elsif (($var eq 'DatabaseName') || ($var eq 'DBName'))
00515 {
00516 $mysql_conf{'db_name'} = $val;
00517 }
00518 elsif ($var eq 'DBSchemaVer')
00519 {
00520 $mysql_conf{'db_schemaver'} = $val;
00521 }
00522 elsif ($var eq 'DBBackupDirectory')
00523 {
00524 $backup_conf{'directory'} = $val;
00525 }
00526 elsif ($var eq 'DBBackupFilename')
00527 {
00528 $backup_conf{'filename'} = $val;
00529 }
00530 elsif ($var eq 'mysqldump')
00531 {
00532 $mysqldump = $val;
00533 }
00534 elsif ($var eq 'compress')
00535 {
00536 $compress = $val;
00537 }
00538 elsif ($var eq 'rotate')
00539 {
00540 $rotate = $val;
00541 }
00542 elsif ($var eq 'rotateglob')
00543 {
00544 $rotateglob = $val;
00545 }
00546 }
00547 close CONF;
00548 return 1;
00549 }
00550
00551 sub read_mysql_txt
00552 {
00553 # Read the "legacy" mysql.txt file in use by MythTV. It could be in a
00554 # couple places, so try the usual suspects in the same order that mythtv
00555 # does in libs/libmyth/mythcontext.cpp
00556 my $found = 0;
00557 my $result = 0;
00558 my @mysql = ('/usr/local/share/mythtv/mysql.txt',
00559 '/usr/share/mythtv/mysql.txt',
00560 '/usr/local/etc/mythtv/mysql.txt',
00561 '/etc/mythtv/mysql.txt',
00562 $homedir ? "$homedir/.mythtv/mysql.txt" : '',
00563 'mysql.txt',
00564 $mythconfdir ? "$mythconfdir/mysql.txt" : '',
00565 );
00566 foreach my $file (@mysql)
00567 {
00568 $found = parse_database_information($file);
00569 $result = $result + $found;
00570 }
00571 return $result;
00572 }
00573
00574 sub read_resource_file
00575 {
00576 parse_database_information("$mythconfdir/backuprc");
00577 }
00578
00579 sub apply_arguments
00580 {
00581 verbose($verbose_level_debug,
00582 '', 'Applying command-line arguments.');
00583 if ($db_hostname)
00584 {
00585 $mysql_conf{'db_host'} = $db_hostname;
00586 }
00587 if ($db_port)
00588 {
00589 $mysql_conf{'db_port'} = $db_port;
00590 }
00591 if ($db_username)
00592 {
00593 $mysql_conf{'db_user'} = $db_username;
00594 }
00595 # This script does not accept a database password on the command-line.
00596 # if ($db_password)
00597 # {
00598 # $mysql_conf{'db_pass'} = $db_password;
00599 # }
00600 if ($db_name)
00601 {
00602 $mysql_conf{'db_name'} = $db_name;
00603 }
00604 if ($db_schema_version)
00605 {
00606 $mysql_conf{'db_schemaver'} = $db_schema_version;
00607 }
00608 if ($backup_directory)
00609 {
00610 $backup_conf{'directory'} = $backup_directory;
00611 }
00612 if ($backup_filename)
00613 {
00614 $backup_conf{'filename'} = $backup_filename;
00615 }
00616 }
00617
00618 sub read_config
00619 {
00620 my $result = 0;
00621 # If specified, use only the database information file
00622 if ($database_information_file)
00623 {
00624 verbose($verbose_level_debug,
00625 '', 'Database Information File specified. Ignoring all'.
00626 ' command-line arguments');
00627 verbose($verbose_level_debug,
00628 '', 'Database Information File: '.
00629 $database_information_file);
00630 unless (-T "$database_information_file")
00631 {
00632 verbose($verbose_level_always,
00633 '', 'The argument you supplied for the database'.
00634 ' information file is invalid.',
00635 'If you were trying to specify a backup filename,'.
00636 ' please use the --directory ',
00637 'and --filename arguments.');
00638 die("\nERROR: Invalid database information file, stopped");
00639 }
00640 # When using a database information file, parse the resource file first
00641 # so it cannot override database information file settings
00642 read_resource_file;
00643 $result = parse_database_information($database_information_file);
00644 return $result;
00645 }
00646
00647 # No database information file, so try the MythTV configuration files.
00648 verbose($verbose_level_debug,
00649 '', 'Parsing configuration files:');
00650 # Prefer the config.xml file
00651 my $file = $mythconfdir ? "$mythconfdir/config.xml" : '';
00652 $result = parse_database_information($file);
00653 if (!$result)
00654 {
00655 # Use the "legacy" mysql.txt file as a fallback
00656 $result = read_mysql_txt;
00657 }
00658 # Read the resource file next to override the config file information, but
00659 # to allow command-line arguments to override resource file "defaults"
00660 read_resource_file;
00661 # Apply the command-line arguments to override the information provided
00662 # by the config file(s).
00663 apply_arguments;
00664 return $result;
00665 }
00666
00667 sub check_database_libs
00668 {
00669 # Try to load the DBI library if available (but don't require it)
00670 BEGIN
00671 {
00672 our $has_dbi = 1;
00673 eval 'use DBI;';
00674 if ($@)
00675 {
00676 $has_dbi = 0;
00677 }
00678 }
00679 verbose($verbose_level_debug,
00680 '', 'DBI is not installed.') if (!$has_dbi);
00681 # Try to load the DBD::mysql library if available (but don't
00682 # require it)
00683 BEGIN
00684 {
00685 our $has_dbd = 1;
00686 eval 'use DBD::mysql;';
00687 if ($@)
00688 {
00689 $has_dbd = 0;
00690 }
00691 }
00692 verbose($verbose_level_debug,
00693 '', 'DBD::mysql is not installed.') if (!$has_dbd);
00694 return ($has_dbi + $has_dbd);
00695 }
00696
00697 sub check_database
00698 {
00699 if (!defined($dbh))
00700 {
00701 my $have_database_libs = check_database_libs;
00702 return 0 if ($have_database_libs < 2);
00703 $dbh = DBI->connect("dbi:mysql:".
00704 "database=$mysql_conf{'db_name'}:".
00705 "host=$mysql_conf{'db_host'}",
00706 "$mysql_conf{'db_user'}",
00707 "$mysql_conf{'db_pass'}",
00708 { PrintError => 0 });
00709 }
00710 return 1;
00711 }
00712
00713 sub create_backup_filename
00714 {
00715 # Create a default backup filename
00716 $backup_conf{'filename'} = $mysql_conf{'db_name'};
00717 if (!$backup_conf{'filename'})
00718 {
00719 $backup_conf{'filename'} = $d_db_name;
00720 }
00721 if ((!$mysql_conf{'db_schemaver'}) &&
00722 ($mysql_conf{'db_host'}) && ($mysql_conf{'db_name'}) &&
00723 ($mysql_conf{'db_user'}) && ($mysql_conf{'db_pass'}))
00724 {
00725 # If DBI is available, query the DB for the schema version
00726 if (check_database)
00727 {
00728 verbose($verbose_level_debug,
00729 '', 'No DBSchemaVer specified, querying database.');
00730 my $query = 'SELECT data FROM settings WHERE value = ?';
00731 if (defined($dbh))
00732 {
00733 my $sth = $dbh->prepare($query);
00734 if ($sth->execute('DBSchemaVer'))
00735 {
00736 while (my @data = $sth->fetchrow_array)
00737 {
00738 $mysql_conf{'db_schemaver'} = $data[0];
00739 verbose($verbose_level_debug,
00740 "Found DBSchemaVer:".
00741 " $mysql_conf{'db_schemaver'}.");
00742 }
00743 }
00744 else
00745 {
00746 verbose($verbose_level_debug,
00747 "Unable to retrieve DBSchemaVer from".
00748 " database. Filename will not contain ",
00749 "DBSchemaVer.");
00750 }
00751 }
00752 }
00753 else
00754 {
00755 verbose($verbose_level_debug,
00756 '', 'No DBSchemaVer specified.',
00757 'DBI and/or DBD:mysql is not available. Unable'.
00758 ' to query database to determine ',
00759 'DBSchemaVer. DBSchemaVer will not be included'.
00760 ' in backup filename.',
00761 'Please ensure DBI and DBD::mysql are'.
00762 ' installed.');
00763 }
00764 }
00765 if ($mysql_conf{'db_schemaver'})
00766 {
00767 $backup_conf{'filename'} .= '-'.$mysql_conf{'db_schemaver'};
00768 }
00769 # Format the time using localtime data so we don't have to bring in
00770 # another dependency.
00771 my @timeData = localtime(time);
00772 $backup_conf{'filename'} .= sprintf('-%04d%02d%02d%02d%02d%02d.sql',
00773 ($timeData[5] + 1900),
00774 ($timeData[4] + 1),
00775 $timeData[3], $timeData[2],
00776 $timeData[1], $timeData[0]);
00777 }
00778
00779 sub check_backup_directory
00780 {
00781 my $result = 0;
00782 if ($backup_conf{'directory'})
00783 {
00784 $result = 1;
00785 }
00786 elsif (check_database)
00787 # If DBI is available, query the DB for the backup directory
00788 {
00789 verbose($verbose_level_debug,
00790 '', 'No DBBackupDirectory specified, querying database.');
00791 my $query = 'SELECT dirname, hostname FROM storagegroup '.
00792 ' WHERE groupname = ?';
00793 if (defined($dbh))
00794 {
00795 my $directory;
00796 my $sth = $dbh->prepare($query);
00797 if ($sth->execute('DB Backups'))
00798 {
00799 # We don't know the hostname associated with this host, and
00800 # since it's not worth parsing the mysql.txt/config.xml
00801 # LocalHostName (unique identifier), with fallback to the
00802 # system hostname, and handling issues along the way, just look
00803 # for any available DB Backups directory and, if none are
00804 # usable, look for a Default group directory
00805 while (my @data = $sth->fetchrow_array)
00806 {
00807 $directory = $data[0];
00808 if (-d $directory && -w $directory)
00809 {
00810 $backup_conf{'directory'} = $directory;
00811 verbose($verbose_level_debug,
00812 "Found DB Backups directory:".
00813 " $backup_conf{'directory'}.");
00814 $result = 1;
00815 $sth->finish;
00816 last;
00817 }
00818 }
00819 }
00820 if ($result == 0 && $sth->execute('Default'))
00821 {
00822 while (my @data = $sth->fetchrow_array)
00823 {
00824 $directory = $data[0];
00825 if (-d $directory && -w $directory)
00826 {
00827 $backup_conf{'directory'} = $directory;
00828 verbose($verbose_level_debug,
00829 "Found Default directory:".
00830 " $backup_conf{'directory'}.");
00831 $result = 1;
00832 $sth->finish;
00833 last;
00834 }
00835 }
00836 }
00837 }
00838 if ($result == 0)
00839 {
00840 verbose($verbose_level_debug,
00841 "Unable to retrieve DBBackupDirectory from".
00842 " database.");
00843 }
00844 }
00845 return $result;
00846 }
00847
00848 sub check_config
00849 {
00850 verbose($verbose_level_debug,
00851 '', 'Checking configuration.');
00852 # Check directory/filename
00853 if (!check_backup_directory)
00854 {
00855 print_configuration;
00856 die("\nERROR: DBBackupDirectory not specified, stopped");
00857 }
00858 if ((!-d $backup_conf{'directory'}) ||
00859 (!-w $backup_conf{'directory'}))
00860 {
00861 print_configuration;
00862 verbose($verbose_level_error,
00863 '', 'ERROR: DBBackupDirectory is not a directory or is '.
00864 'not writable. Please specify',
00865 ' a directory in your database information file'.
00866 ' using DBBackupDirectory.',
00867 ' If not using a database information file,'.
00868 ' please specify the ',
00869 ' --directory command-line option.');
00870 die("\nInvalid backup directory, stopped");
00871 }
00872 if (!$backup_conf{'filename'})
00873 {
00874 if ($backup_xmltvids)
00875 {
00876 my $file = 'mythtv_xmltvid_backup';
00877 # Format the time using localtime data so we don't have to bring in
00878 # another dependency.
00879 my @timeData = localtime(time);
00880 $file .= sprintf('-%04d%02d%02d%02d%02d%02d.sql',
00881 ($timeData[5] + 1900),
00882 ($timeData[4] + 1),
00883 $timeData[3], $timeData[2],
00884 $timeData[1], $timeData[0]);
00885 $backup_conf{'filename'} = $file;
00886 }
00887 else
00888 {
00889 create_backup_filename;
00890 }
00891 }
00892 if ( -e "$backup_conf{'directory'}/$backup_conf{'filename'}")
00893 {
00894 verbose($verbose_level_error,
00895 '', 'ERROR: The specified file already exists.');
00896 die("\nInvalid backup filename, stopped");
00897 }
00898 if (!$mysql_conf{'db_name'})
00899 {
00900 verbose($verbose_level_debug,
00901 '', "WARNING: DBName not specified. Using $d_db_name");
00902 $mysql_conf{'db_name'} = $d_db_name;
00903 }
00904 # Though the script will attempt a backup even if no other database
00905 # information is provided (i.e. using "defaults" from the MySQL options
00906 # file, warning the user that some "normally-necessary" information is not
00907 # provided may be nice.
00908 return if (!$debug);
00909 if (!$mysql_conf{'db_host'})
00910 {
00911 verbose($verbose_level_debug,
00912 '', 'WARNING: DBHostName not specified.',
00913 ' Assuming it is specified in the MySQL'.
00914 ' options file.');
00915 }
00916 if (!$mysql_conf{'db_user'})
00917 {
00918 verbose($verbose_level_debug,
00919 '', 'WARNING: DBUserName not specified.',
00920 ' Assuming it is specified in the MySQL'.
00921 ' options file.');
00922 }
00923 if (!$mysql_conf{'db_pass'})
00924 {
00925 verbose($verbose_level_debug,
00926 '', 'WARNING: DBPassword not specified.',
00927 ' Assuming it is specified in the MySQL'.
00928 ' options file.');
00929 }
00930 }
00931
00932 sub create_defaults_extra_file
00933 {
00934 return '' if (!$mysql_conf{'db_pass'});
00935 verbose($verbose_level_debug,
00936 '', "Attempting to use supplied password for $mysqldump.",
00937 'Any [client] or [mysqldump] password specified in the MySQL'.
00938 ' options file will',
00939 'take precedence.');
00940 # Let tempfile handle unlinking on exit so we don't have to verify that the
00941 # file with $filename is the file we created
00942 my ($fh, $filename) = tempfile(UNLINK => 1);
00943 # Quote the password if it contains # or whitespace or quotes.
00944 # Quoting of values in MySQL options files is only supported on MySQL
00945 # 4.0.16 and above, so only quote if required.
00946 my $quote = '';
00947 my $safe_password = $mysql_conf{'db_pass'};
00948 if ($safe_password =~ /[#'"\s]/)
00949 {
00950 $quote = "'";
00951 $safe_password =~ s/'/\\'/g;
00952 }
00953 print $fh "[client]\npassword=${quote}${safe_password}${quote}\n".
00954 "[mysqldump]\npassword=${quote}${safe_password}${quote}\n";
00955 return $filename;
00956 }
00957
00958 sub do_xmltvid_backup
00959 {
00960 my $exit = 1;
00961 if (check_database)
00962 {
00963 my ($chanid, $channum, $callsign, $name, $xmltvid);
00964 my $query = " SELECT chanid, channum, callsign, name, xmltvid".
00965 " FROM channel ".
00966 "ORDER BY CAST(channum AS SIGNED),".
00967 " CAST(SUBSTRING(channum".
00968 " FROM (1 +".
00969 " LOCATE('_', channum) +".
00970 " LOCATE('-', channum) +".
00971 " LOCATE('#', channum) +".
00972 " LOCATE('.', channum)))".
00973 " AS SIGNED)";
00974 my $sth = $dbh->prepare($query);
00975 verbose($verbose_level_debug,
00976 '', 'Querying database for xmltvid information.');
00977 my $file = "$backup_conf{'directory'}/$backup_conf{'filename'}";
00978 open BACKUP, '>', $file or die("\nERROR: Unable to open".
00979 " $file: $!, stopped");
00980 for ($section = 0; $section < 2; $section++)
00981 {
00982 if ($sth->execute)
00983 {
00984 while (my @data = $sth->fetchrow_array)
00985 {
00986 $chanid = $data[0];
00987 $channum = $data[1];
00988 $callsign = $data[2];
00989 $name = $data[3];
00990 $xmltvid = $data[4];
00991 verbose($verbose_level_debug,
00992 "Found channel: $chanid, $channum, $callsign,".
00993 " $name, $xmltvid.") if ($section == 0);
00994 if ($xmltvid && $callsign)
00995 {
00996 if ($section == 0)
00997 {
00998 print BACKUP "-- Start Channel Data\n".
00999 "-- ID: '$chanid'\n".
01000 "-- Number: '$channum'\n".
01001 "-- Callsign: '$callsign'\n".
01002 "-- Name: '$name'\n".
01003 "-- XMLTVID: '$xmltvid'\n".
01004 "-- End Channel Data\n";
01005 print BACKUP "UPDATE channel".
01006 " SET xmltvid = '$xmltvid'".
01007 " WHERE callsign = '$callsign'".
01008 ";\n";
01009 }
01010 else
01011 {
01012 print BACKUP "UPDATE channel".
01013 " SET xmltvid = '$xmltvid'".
01014 " WHERE channum = '$channum'".
01015 " AND name = '$name';\n";
01016 }
01017 }
01018 }
01019 if ($section == 0)
01020 {
01021 verbose($verbose_level_debug,
01022 '', 'Successfully backed up xmltvid'.
01023 ' information.'.
01024 '', '', 'Creating alternate format backup.');
01025 print BACKUP "\n\n\n".
01026 "
01027
01028
01029
01030 \n";
01031 verbose($verbose_level_debug,
01032 'Successfully created alternate format'.
01033 ' backup.');
01034 }
01035 $exit = 0;
01036 }
01037 else
01038 {
01039 verbose($verbose_level_error,
01040 '', 'ERROR: Unable to retrieve xmltvid information'.
01041 ' from database.');
01042 die("\nError retrieving xmltvid information, stopped");
01043 }
01044 }
01045 close BACKUP;
01046 }
01047 else
01048 {
01049 verbose($verbose_level_error,
01050 '', 'ERROR: Unable to backup xmltvids without Perl'.
01051 ' database libraries.',
01052 ' Please ensure the Perl DBI and DBD::mysql'.
01053 ' modules are installed.');
01054 die("\nPerl database libraries missing, stopped");
01055 }
01056 return $exit;
01057 }
01058
01059 # This subroutine performs limited checking of a command and untaints the
01060 # command (and the environment) if the command seems to use an absolute path
01061 # containing no . or .. references or if it's a simple command name referencing
01062 # an executable in a "normal" directory for binaries. It should only be called
01063 # after careful consideration of the effects of doing so and of whether it
01064 # makes sense to override taint-mode runtime checking of the value.
01065 sub untaint_command
01066 {
01067 my $command = shift;
01068 my $allow_untaint = 0;
01069 # Only allow directories from @d_allowed_paths that exist in the PATH
01070 if (!defined(@allowed_paths))
01071 {
01072 foreach my $path (split(/:/, $old_env_path))
01073 {
01074 if (grep(/^$path$/, @d_allowed_paths))
01075 {
01076 push(@allowed_paths, $path);
01077 }
01078 }
01079 verbose($verbose_level_debug + 2,
01080 '', 'Allowing paths:', @allowed_paths,
01081 'From PATH: '.$old_env_path);
01082 }
01083
01084 verbose($verbose_level_debug + 2, '', 'Verifying command: '.$command);
01085 if ($command =~ /^\//)
01086 {
01087 verbose($verbose_level_debug + 2, ' - Command starts with /.');
01088 if (! ($command =~ /\/\.+\//))
01089 {
01090 verbose($verbose_level_debug + 2,
01091 ' - Command does not contain dir refs.');
01092 if (-e "$command" && -f "$command" && -x "$command")
01093 {
01094 # Seems to be a valid executable specified with a path starting
01095 # with / and having no current/previous directory references
01096 verbose($verbose_level_debug + 2,
01097 'Unmodified command meets untaint requirements.',
01098 $command);
01099 $allow_untaint = 1;
01100 }
01101 }
01102 }
01103 else
01104 {
01105 foreach my $path (@allowed_paths)
01106 {
01107 if (-e "$path/$command" && -f "$path/$command" &&
01108 -x "$path/$command")
01109 {
01110 # Seems to be a valid executable in a "normal" directory for
01111 # binaries
01112 $command = "$path/$command";
01113 verbose($verbose_level_debug + 2,
01114 'Command seems to be a simple command in a'.
01115 ' normal directory for binaries: '.$command);
01116 $allow_untaint = 1;
01117 }
01118 }
01119 }
01120 if ($allow_untaint)
01121 {
01122 if ($command =~ /^(.*)$/)
01123 {
01124 verbose($verbose_level_debug + 1,
01125 'Untainting command: '.$command);
01126 $command = $1;
01127 $ENV{'PATH'} = '';
01128 $is_env_tainted = 0;
01129 }
01130 }
01131 return $command;
01132 }
01133
01134 # This subroutine performs limited checking of file or directory paths and
01135 # untaints the path if it seems to be an absolute path to a normal file or
01136 # directory and contains no . or .. references. It should only be called after
01137 # careful consideration of the effects of doing so and of whether it makes
01138 # sense to override taint-mode runtime checking of the value.
01139 sub untaint_path
01140 {
01141 my $path = shift;
01142 verbose($verbose_level_debug + 2, '', 'Verifying path: '.$path);
01143 if ($path =~ /^\//)
01144 {
01145 verbose($verbose_level_debug + 2, ' - Path starts with /.');
01146 if (! ($path =~ /\/\.+\//))
01147 {
01148 verbose($verbose_level_debug + 2,
01149 ' - Path contains no dir refs.');
01150 if (-e "$path" && (-f "$path" || -d "$path"))
01151 {
01152 # Seems to be a file or directory path starting with / and
01153 # having no current/previous directory references
01154 if ($path =~ /^(.*)$/)
01155 {
01156 verbose($verbose_level_debug + 1,
01157 'Untainting path: '.$path);
01158 $path = $1;
01159 }
01160 }
01161 }
01162 }
01163 return $path;
01164 }
01165
01166 # This subroutine does absolutely no data checking. It blindly accepts a
01167 # possibly-tainted value and "untaints" it. It should only be called after
01168 # careful consideration of the effects of doing so and of whether it makes
01169 # sense to override taint-mode runtime checking of the value.
01170 sub untaint_data
01171 {
01172 my $value = shift;
01173 if ($value =~ /^(.*)$/)
01174 {
01175 verbose($verbose_level_debug + 1, 'Untainting data: '.$value);
01176 $value = $1;
01177 }
01178 return $value;
01179 }
01180
01181 sub reset_environment
01182 {
01183 if (!$is_env_tainted)
01184 {
01185 $is_env_tainted = 1;
01186 $ENV{'PATH'} = $old_env_path;
01187 }
01188 }
01189
01190 sub do_backup
01191 {
01192 my $defaults_extra_file = create_defaults_extra_file;
01193 my $host_arg = '';
01194 my $port_arg = '';
01195 my $user_arg = '';
01196 if ($defaults_extra_file)
01197 {
01198 $defaults_arg = " --defaults-extra-file='$defaults_extra_file'";
01199 }
01200 else
01201 {
01202 $defaults_arg = '';
01203 }
01204 # For users running in environments where taint mode is activated (i.e.
01205 # running mythtv-setup or mythbackend as root), executing a command line
01206 # built with tainted data will fail. Therefore, try to untaint data if it
01207 # meets certain basic requirements.
01208 my $safe_mysqldump = $mysqldump;
01209 $safe_mysqldump = untaint_command($safe_mysqldump);
01210 $safe_mysqldump =~ s/'/'\\''/sg;
01211 $mysql_conf{'db_name'} = untaint_data($mysql_conf{'db_name'});
01212 $mysql_conf{'db_host'} = untaint_data($mysql_conf{'db_host'});
01213 $mysql_conf{'db_port'} = untaint_data($mysql_conf{'db_port'});
01214 $mysql_conf{'db_user'} = untaint_data($mysql_conf{'db_user'});
01215 $backup_conf{'directory'} = untaint_path($backup_conf{'directory'});
01216 # Can't use untaint_path because the filename is not a full path and the
01217 # file doesn't yet exist, anyway
01218 $backup_conf{'filename'} =~ s/'/'\\''/g;
01219 $backup_conf{'filename'} = untaint_data($backup_conf{'filename'});
01220 my $output_file = "$backup_conf{'directory'}/$backup_conf{'filename'}";
01221 $output_file =~ s/'/'\\''/sg;
01222 # Create the args for host, port, and user, shell-escaping values, as
01223 # necessary.
01224 my $safe_db_name = $mysql_conf{'db_name'};
01225 $safe_db_name =~ s/'/'\\''/g;
01226 my $safe_string;
01227 if ($mysql_conf{'db_host'})
01228 {
01229 $safe_string = $mysql_conf{'db_host'};
01230 $safe_string =~ s/'/'\\''/g;
01231 $host_arg = " --host='$safe_string'";
01232 }
01233 if ($mysql_conf{'db_port'} > 0)
01234 {
01235 $safe_string = $mysql_conf{'db_port'};
01236 $safe_string =~ s/'/'\\''/g;
01237 $port_arg = " --port='$safe_string'";
01238 }
01239 if ($mysql_conf{'db_user'})
01240 {
01241 $safe_string = $mysql_conf{'db_user'};
01242 $safe_string =~ s/'/'\\''/g;
01243 $user_arg = " --user='$safe_string'";
01244 }
01245
01246 # Use redirects to capture stderr (for debug) and send stdout (the backup)
01247 # to a file
01248 my $command = "'${safe_mysqldump}'${defaults_arg}${host_arg}".
01249 "${port_arg}${user_arg} --add-drop-table --add-locks ".
01250 "--allow-keywords --complete-insert --extended-insert ".
01251 "--lock-tables --no-create-db --quick --add-drop-table ".
01252 "'$safe_db_name' 2>&1 1>'$output_file'";
01253 verbose($verbose_level_debug,
01254 '', 'Executing command:', $command);
01255 my $result = `$command`;
01256 my $exit = $? >> 8;
01257 verbose($verbose_level_debug,
01258 '', "$mysqldump exited with status: $exit");
01259 verbose($verbose_level_debug,
01260 "$mysqldump output:", $result) if ($exit);
01261 reset_environment;
01262 return $exit;
01263 }
01264
01265 sub compress_backup
01266 {
01267 if (!-e "$backup_conf{'directory'}/$backup_conf{'filename'}")
01268 {
01269 verbose($verbose_level_debug,
01270 '', 'Unable to find backup file to compress');
01271 return 1;
01272 }
01273 my $result = 0;
01274 verbose($verbose_level_debug,
01275 '', 'Attempting to compress backup file.');
01276 if ($d_compress eq $compress)
01277 {
01278 # Try to load the IO::Compress::Gzip library if available (but don't
01279 # require it)
01280 BEGIN
01281 {
01282 our $has_compress_gzip = 1;
01283 # Though this does nothing, it prevents an invalid "only used
01284 # once" warning that occurs for users without IO::Compress
01285 # installed.
01286 undef $GzipError;
01287 eval 'use IO::Compress::Gzip qw(gzip $GzipError);';
01288 if ($@)
01289 {
01290 $has_compress_gzip = 0;
01291 }
01292 }
01293 if (!$has_compress_gzip)
01294 {
01295 verbose($verbose_level_debug,
01296 " - IO::Compress::Gzip is not installed.");
01297 }
01298 else
01299 {
01300 if (-e "$backup_conf{'directory'}/$backup_conf{'filename'}.gz")
01301 {
01302 verbose($verbose_level_debug,
01303 '', 'A file whose name is the backup filename'.
01304 ' with the \'.gz\' extension already',
01305 'exists. Leaving backup uncompressed.');
01306 return 1;
01307 }
01308 verbose($verbose_level_debug,
01309 " - Compressing backup file with IO::Compress::Gzip.");
01310 $result = gzip(
01311 "$backup_conf{'directory'}/$backup_conf{'filename'}" =>
01312 "$backup_conf{'directory'}/$backup_conf{'filename'}.gz");
01313 if ((defined($result)) &&
01314 (-e "$backup_conf{'directory'}/".
01315 "$backup_conf{'filename'}.gz"))
01316 {
01317 # For users running in environments where taint mode is
01318 # activated (i.e. running mythtv-setup or mythbackend as
01319 # root), unlinking a file whose path is built with tainted data
01320 # will fail. Therefore, try to untaint the path if it meets
01321 # certain basic requirements.
01322 my $uncompressed_file = $backup_conf{'directory'}."/".
01323 $backup_conf{'filename'};
01324 $uncompressed_file = untaint_path($uncompressed_file);
01325 $uncompressed_file =~ s/'/'\\''/sg;
01326 verbose($verbose_level_debug + 2,
01327 "Unlinking uncompressed file: $uncompressed_file");
01328 unlink "$uncompressed_file";
01329 $backup_conf{'filename'} = "$backup_conf{'filename'}.gz";
01330 verbose($verbose_level_debug,
01331 '', 'Successfully compressed backup to file:',
01332 "$backup_conf{'directory'}/".
01333 "$backup_conf{'filename'}");
01334 return 0;
01335 }
01336 verbose($verbose_level_debug,
01337 " Error: $GzipError");
01338 }
01339 }
01340 # Try to compress the file with the compress binary.
01341 verbose($verbose_level_debug,
01342 " - Compressing backup file with $compress.");
01343 my $backup_path = "$backup_conf{'directory'}/$backup_conf{'filename'}";
01344 # For users running in environments where taint mode is activated (i.e.
01345 # running mythtv-setup or mythbackend as root), executing a command line
01346 # built with tainted data will fail. Therefore, try to untaint data if it
01347 # meets certain basic requirements.
01348 $compress = untaint_command($compress);
01349 $compress =~ s/'/'\\''/sg;
01350 $backup_path = untaint_path($backup_path);
01351 $backup_path =~ s/'/'\\''/sg;
01352 my $command = "'$compress' '$backup_path' 2>&1";
01353 verbose($verbose_level_debug,
01354 '', 'Executing command:', $command);
01355 my $output = `$command`;
01356 my $exit = $? >> 8;
01357 verbose($verbose_level_debug,
01358 '', "$compress exited with status: $exit");
01359 if ($exit)
01360 {
01361 verbose($verbose_level_debug,
01362 "$compress output:", $output);
01363 }
01364 else
01365 {
01366 $backup_conf{'filename'} = "$backup_conf{'filename'}.gz";
01367 }
01368 reset_environment;
01369 return $exit;
01370 }
01371
01372 sub rotate_backups
01373 {
01374 if (($rotate < 1) || (!defined($rotateglob)) || (!$rotateglob))
01375 {
01376 verbose($verbose_level_debug,
01377 '', 'Backup file rotation disabled.');
01378 return 0;
01379 }
01380 verbose($verbose_level_debug,
01381 '', 'Rotating backups.');
01382 verbose($verbose_level_debug,
01383 '', 'Searching for files matching pattern:',
01384 "$backup_conf{'directory'}/$rotateglob");
01385 my @files = <$backup_conf{'directory'}/$rotateglob>;
01386 my @sorted_files = sort { lc($a) cmp lc($b) } @files;
01387 my $num_files = @sorted_files;
01388 verbose($verbose_level_debug,
01389 " - Found $num_files matching files.");
01390 $num_files = $num_files - $rotate;
01391 $num_files = 0 if ($num_files < 0);
01392 verbose($verbose_level_debug,
01393 '', "Deleting $num_files and keeping (up to) $rotate backup".
01394 ' files.');
01395 my $index = 0;
01396 foreach my $file (@sorted_files)
01397 {
01398 if ($index++ < $num_files)
01399 {
01400 if ($file eq
01401 "$backup_conf{'directory'}/$backup_conf{'filename'}")
01402 {
01403 # This is the just-created backup. Warn the user that older
01404 # backups with newer schema versions may cause rotation to
01405 # fail.
01406 verbose($verbose_level_debug,
01407 '', 'WARNING: You seem to have reverted to an'.
01408 ' older database schema version.',
01409 'You should move all backups from newer schema'.
01410 ' versions to another directory or',
01411 'delete them to prevent your new backups from'.
01412 ' being deleted on rotation.', '');
01413 verbose($verbose_level_debug,
01414 " - Keeping backup file: $file");
01415
01416 }
01417 else
01418 {
01419 verbose($verbose_level_debug,
01420 " - Deleting old backup file: $file");
01421 # For users running in environments where taint mode is
01422 # activated (i.e. running mythtv-setup or mythbackend as
01423 # root), unlinking a file whose path is built with tainted data
01424 # will fail. Therefore, try to untaint the path if it meets
01425 # certain basic requirements.
01426 $file = untaint_path($file);
01427 $file =~ s/'/'\\''/sg;
01428 unlink "$file";
01429 }
01430 }
01431 else
01432 {
01433 verbose($verbose_level_debug,
01434 " - Keeping backup file: $file");
01435 }
01436 }
01437 return 1;
01438 }
01439
01440 ##############################################################################
01441 # Main functionality
01442 ##############################################################################
01443
01444 # The first argument after option parsing, if it exists, should be a database
01445 # information file.
01446 $database_information_file = shift;
01447
01448 configure_environment;
01449 read_config;
01450 check_config;
01451
01452 print_configuration;
01453
01454 my $status = 1;
01455 if ($backup_xmltvids)
01456 {
01457 $status = do_xmltvid_backup;
01458 }
01459 else
01460 {
01461 $status = do_backup;
01462 if (!$status)
01463 {
01464 compress_backup;
01465 rotate_backups;
01466 }
01467 }
01468
01469 $dbh->disconnect if (defined($dbh));
01470
01471 exit $status;
01472