#!/usr/bin/perl -w # # mythconverg_backup.pl # # Creates a backup of the MythTV database. # # For details, see: # mythconverg_backup.pl --help # Includes use Getopt::Long; use File::Temp qw/ tempfile /; # Script info $NAME = 'MythTV Database Backup Script'; $VERSION = '1.0.13'; # Some variables we'll use here our ($username, $homedir, $mythconfdir, $database_information_file); our ($mysqldump, $compress, $rotate, $rotateglob, $backup_xmltvids); our ($usage, $debug, $show_version, $show_version_script, $dbh); our ($d_db_name, $d_mysqldump, $d_compress, $d_rotate, $d_rotateglob); # This script does not accept a database password on the command-line. # Any packager who enables the functionality should modify the --help output. # our ($db_password); our ($db_hostname, $db_port, $db_username, $db_name, $db_schema_version); our ($backup_directory, $backup_filename); our ($verbose_level_always, $verbose_level_debug, $verbose_level_error); our %mysql_conf = ('db_host' => '', 'db_port' => -1, 'db_user' => '', 'db_pass' => '', 'db_name' => '', 'db_schemaver' => '' ); our %backup_conf = ('directory' => '', 'filename' => '' ); # Variables used to untaint data our $is_env_tainted = 1; our $old_env_path = $ENV{"PATH"}; our @d_allowed_paths = ("/bin", "/usr/bin", "/usr/local/bin", "/sbin", "/usr/sbin", "/usr/local/sbin" ); our @allowed_paths; # Debug levels $verbose_level_always = 0; $verbose_level_debug = 1; $verbose_level_error = 255; # Defaults $d_db_name = 'mythconverg'; $d_mysqldump = 'mysqldump'; $d_compress = 'gzip'; $d_rotate = 5; $d_rotateglob = $d_db_name.'-????-??????????????.sql*'; # Provide default values for GetOptions $mysqldump = $d_mysqldump; $compress = $d_compress; $rotate = $d_rotate; $rotateglob = $d_rotateglob; $debug = 0; # Load the cli options GetOptions('hostname|DBHostName=s' => \$db_hostname, 'port|DBPort=i' => \$db_port, 'username|DBUserName=s' => \$db_username, # This script does not accept a database password on the command-line. # 'password|DBPassword=s' => \$db_password, 'name|DBName=s' => \$db_name, 'schemaver|DBSchemaVer=s' => \$db_schema_version, 'directory|DBBackupDirectory=s' => \$backup_directory, 'filename|DBBackupFilename=s' => \$backup_filename, 'mysqldump=s' => \$mysqldump, 'compress=s' => \$compress, 'rotate=i' => \$rotate, 'rotateglob|glob=s' => \$rotateglob, 'backup_xmltvids|backup-xmltvids|'. 'xmltvids' => \$backup_xmltvids, 'usage|help|h+' => \$usage, 'version' => \$show_version, 'script_version|script-version|v' => \$show_version_script, 'verbose|debug|d+' => \$debug ); # Print version information sub print_version_information { my $script_name = substr $0, rindex($0, '/') + 1; print "$NAME\n$script_name\nversion: $VERSION\n"; } if ($show_version_script) { print "$NAME,$VERSION,,\n"; exit; } elsif ($show_version) { print_version_information; exit; } # Print usage if ($usage) { print_version_information; print < ~/.mythtv/backuprc # $0 --verbose Make sure you keep the backuprc file for next time. Once you have successfully created a backup, the script may be run without the --verbose argument. To backup xmltvids: Ensure you have a ~/.mythtv/backuprc file, as described above, and execute this script with the --backup_xmltvids argument. # $0 --backup_xmltvids EOF if ($usage > 1) { print <= $level); } print { $error ? STDERR : STDOUT } join("\n", @_), "\n"; } sub print_configuration { verbose($verbose_level_debug, '', 'Database Information:', " DBHostName: $mysql_conf{'db_host'}", " DBPort: $mysql_conf{'db_port'}", " DBUserName: $mysql_conf{'db_user'}", ' DBPassword: ' . ( $mysql_conf{'db_pass'} ? 'XXX' : '' ), # "$mysql_conf{'db_pass'}", " DBName: $mysql_conf{'db_name'}", " DBSchemaVer: $mysql_conf{'db_schemaver'}", " DBBackupDirectory: $backup_conf{'directory'}", " DBBackupFilename: $backup_conf{'filename'}"); verbose($verbose_level_debug, '', 'Executables:', " mysqldump: $mysqldump", " compress: $compress"); } sub configure_environment { verbose($verbose_level_debug, '', 'Configuring environment:'); # Get the user's login and home directory, so we can look for config files ($username, $homedir) = (getpwuid $>)[0,7]; $username = $ENV{'USER'} if ($ENV{'USER'}); $homedir = $ENV{'HOME'} if ($ENV{'HOME'}); if ($username && !$homedir) { $homedir = "/home/$username"; if (!-e $homedir && -e "/Users/$username") { $homedir = "/Users/$username"; } } verbose($verbose_level_debug, " - username: $username", " - HOME: $homedir"); # Find the config directory $mythconfdir = $ENV{'MYTHCONFDIR'} ? $ENV{'MYTHCONFDIR'} : "$homedir/.mythtv" ; verbose($verbose_level_debug, " - MYTHCONFDIR: $mythconfdir"); } # Though much of the configuration file parsing could be done by the MythTV # Perl bindings, using them to retrieve database information is not appropriate # for a backup script. The Perl bindings require the backend to be running and # use UPnP for autodiscovery. Also, parsing the files "locally" allows # supporting even the old MythTV database configuration file, mysql.txt. sub parse_database_information { my $file = shift; verbose($verbose_level_debug, " - checking: $file"); return 0 unless ($file && -e $file); verbose($verbose_level_debug, " parsing: $file"); open(CONF, $file) or die("\nERROR: Unable to read $file: $!". ', stopped'); while (my $line = ) { # Cleanup next if ($line =~ m/^\s*#/); $line =~ s/^str //; chomp($line); $line =~ s/^\s+//; $line =~ s/\s+$//; # Split off the var=val pairs my ($var, $val) = split(/ *[\=\: ] */, $line, 2); # Also look for val from config.xml if ($line =~ m/<(\w+)>(.+)<\/(\w+)>$/ && $1 eq $3) { $var = $1; $val = $2; } next unless ($var && $var =~ m/\w/); if (($var eq 'Host') || ($var eq 'DBHostName')) { $mysql_conf{'db_host'} = $val; } elsif (($var eq 'Port') || ($var eq 'DBPort')) { $mysql_conf{'db_port'} = $val; } elsif (($var eq 'UserName') || ($var eq 'DBUserName')) { $mysql_conf{'db_user'} = $val; } elsif (($var eq 'Password') || ($var eq 'DBPassword')) { $mysql_conf{'db_pass'} = $val; $mysql_conf{'db_pass'} =~ s/&/&/sg; $mysql_conf{'db_pass'} =~ s/>/>/sg; $mysql_conf{'db_pass'} =~ s/</connect("dbi:mysql:". "host=$temp_host:". "database=$mysql_conf{'db_name'}", "$mysql_conf{'db_user'}", "$mysql_conf{'db_pass'}", { PrintError => 1 }); } return 1; } sub create_backup_filename { # Create a default backup filename $backup_conf{'filename'} = $mysql_conf{'db_name'}; if (!$backup_conf{'filename'}) { $backup_conf{'filename'} = $d_db_name; } if ((!$mysql_conf{'db_schemaver'}) && ($mysql_conf{'db_host'}) && ($mysql_conf{'db_name'}) && ($mysql_conf{'db_user'}) && ($mysql_conf{'db_pass'})) { # If DBI is available, query the DB for the schema version if (check_database) { verbose($verbose_level_debug, '', 'No DBSchemaVer specified, querying database.'); my $query = 'SELECT data FROM settings WHERE value = ?'; if (defined($dbh)) { my $sth = $dbh->prepare($query); if ($sth->execute('DBSchemaVer')) { while (my @data = $sth->fetchrow_array) { $mysql_conf{'db_schemaver'} = $data[0]; verbose($verbose_level_debug, "Found DBSchemaVer:". " $mysql_conf{'db_schemaver'}."); } } else { verbose($verbose_level_debug, "Unable to retrieve DBSchemaVer from". " database. Filename will not contain ", "DBSchemaVer."); } } } else { verbose($verbose_level_debug, '', 'No DBSchemaVer specified.', 'DBI and/or DBD:mysql is not available. Unable'. ' to query database to determine ', 'DBSchemaVer. DBSchemaVer will not be included'. ' in backup filename.', 'Please ensure DBI and DBD::mysql are'. ' installed.'); } } if ($mysql_conf{'db_schemaver'}) { $backup_conf{'filename'} .= '-'.$mysql_conf{'db_schemaver'}; } # Format the time using localtime data so we don't have to bring in # another dependency. my @timeData = localtime(time); $backup_conf{'filename'} .= sprintf('-%04d%02d%02d%02d%02d%02d.sql', ($timeData[5] + 1900), ($timeData[4] + 1), $timeData[3], $timeData[2], $timeData[1], $timeData[0]); } sub check_backup_directory { my $result = 0; if ($backup_conf{'directory'}) { $result = 1; } elsif (check_database) # If DBI is available, query the DB for the backup directory { verbose($verbose_level_debug, '', 'No DBBackupDirectory specified, querying database.'); my $query = 'SELECT dirname, hostname FROM storagegroup '. ' WHERE groupname = ?'; if (defined($dbh)) { my $directory; my $sth = $dbh->prepare($query); if ($sth->execute('DB Backups')) { # We don't know the hostname associated with this host, and # since it's not worth parsing the mysql.txt/config.xml # LocalHostName (unique identifier), with fallback to the # system hostname, and handling issues along the way, just look # for any available DB Backups directory and, if none are # usable, look for a Default group directory while (my @data = $sth->fetchrow_array) { $directory = $data[0]; if (-d $directory && -w $directory) { $backup_conf{'directory'} = $directory; verbose($verbose_level_debug, "Found DB Backups directory:". " $backup_conf{'directory'}."); $result = 1; $sth->finish; last; } } } if ($result == 0 && $sth->execute('Default')) { while (my @data = $sth->fetchrow_array) { $directory = $data[0]; if (-d $directory && -w $directory) { $backup_conf{'directory'} = $directory; verbose($verbose_level_debug, "Found Default directory:". " $backup_conf{'directory'}."); $result = 1; $sth->finish; last; } } } } if ($result == 0) { verbose($verbose_level_debug, "Unable to retrieve DBBackupDirectory from". " database."); } } return $result; } sub check_config { verbose($verbose_level_debug, '', 'Checking configuration.'); # Check directory/filename if (!check_backup_directory) { print_configuration; die("\nERROR: DBBackupDirectory not specified, stopped"); } if ((!-d $backup_conf{'directory'}) || (!-w $backup_conf{'directory'})) { print_configuration; verbose($verbose_level_error, '', 'ERROR: DBBackupDirectory is not a directory or is '. 'not writable. Please specify', ' a directory in your database information file'. ' using DBBackupDirectory.', ' If not using a database information file,'. ' please specify the ', ' --directory command-line option.'); die("\nInvalid backup directory, stopped"); } if (!$backup_conf{'filename'}) { if ($backup_xmltvids) { my $file = 'mythtv_xmltvid_backup'; # Format the time using localtime data so we don't have to bring in # another dependency. my @timeData = localtime(time); $file .= sprintf('-%04d%02d%02d%02d%02d%02d.sql', ($timeData[5] + 1900), ($timeData[4] + 1), $timeData[3], $timeData[2], $timeData[1], $timeData[0]); $backup_conf{'filename'} = $file; } else { create_backup_filename; } } if ( -e "$backup_conf{'directory'}/$backup_conf{'filename'}") { verbose($verbose_level_error, '', 'ERROR: The specified file already exists.'); die("\nInvalid backup filename, stopped"); } if (!$mysql_conf{'db_name'}) { verbose($verbose_level_debug, '', "WARNING: DBName not specified. Using $d_db_name"); $mysql_conf{'db_name'} = $d_db_name; } # Though the script will attempt a backup even if no other database # information is provided (i.e. using "defaults" from the MySQL options # file, warning the user that some "normally-necessary" information is not # provided may be nice. return if (!$debug); if (!$mysql_conf{'db_host'}) { verbose($verbose_level_debug, '', 'WARNING: DBHostName not specified.', ' Assuming it is specified in the MySQL'. ' options file.'); } if (!$mysql_conf{'db_user'}) { verbose($verbose_level_debug, '', 'WARNING: DBUserName not specified.', ' Assuming it is specified in the MySQL'. ' options file.'); } if (!$mysql_conf{'db_pass'}) { verbose($verbose_level_debug, '', 'WARNING: DBPassword not specified.', ' Assuming it is specified in the MySQL'. ' options file.'); } } sub create_defaults_extra_file { return '' if (!$mysql_conf{'db_pass'}); verbose($verbose_level_debug, '', "Attempting to use supplied password for $mysqldump.", 'Any [client] or [mysqldump] password specified in the MySQL'. ' options file will', 'take precedence.'); # Let tempfile handle unlinking on exit so we don't have to verify that the # file with $filename is the file we created my ($fh, $filename) = tempfile(UNLINK => 1); # Quote the password if it contains # or whitespace or quotes. # Quoting of values in MySQL options files is only supported on MySQL # 4.0.16 and above, so only quote if required. my $quote = ''; my $safe_password = $mysql_conf{'db_pass'}; if ($safe_password =~ /[#'"\s]/) { $quote = "'"; $safe_password =~ s/'/\\'/g; } print $fh "[client]\npassword=${quote}${safe_password}${quote}\n". "[mysqldump]\npassword=${quote}${safe_password}${quote}\n"; return $filename; } sub do_xmltvid_backup { my $exit = 1; if (check_database) { my ($chanid, $channum, $callsign, $name, $xmltvid); my $query = " SELECT chanid, channum, callsign, name, xmltvid". " FROM channel ". "ORDER BY CAST(channum AS SIGNED),". " CAST(SUBSTRING(channum". " FROM (1 +". " LOCATE('_', channum) +". " LOCATE('-', channum) +". " LOCATE('#', channum) +". " LOCATE('.', channum)))". " AS SIGNED)"; my $sth = $dbh->prepare($query); verbose($verbose_level_debug, '', 'Querying database for xmltvid information.'); my $file = "$backup_conf{'directory'}/$backup_conf{'filename'}"; open BACKUP, '>', $file or die("\nERROR: Unable to open". " $file: $!, stopped"); for ($section = 0; $section < 2; $section++) { if ($sth->execute) { while (my @data = $sth->fetchrow_array) { $chanid = $data[0]; $channum = $data[1]; $callsign = $data[2]; $name = $data[3]; $xmltvid = $data[4]; verbose($verbose_level_debug, "Found channel: $chanid, $channum, $callsign,". " $name, $xmltvid.") if ($section == 0); if ($xmltvid && $callsign) { if ($section == 0) { print BACKUP "-- Start Channel Data\n". "-- ID: '$chanid'\n". "-- Number: '$channum'\n". "-- Callsign: '$callsign'\n". "-- Name: '$name'\n". "-- XMLTVID: '$xmltvid'\n". "-- End Channel Data\n"; print BACKUP "UPDATE channel". " SET xmltvid = '$xmltvid'". " WHERE callsign = '$callsign'". ";\n"; } else { print BACKUP "UPDATE channel". " SET xmltvid = '$xmltvid'". " WHERE channum = '$channum'". " AND name = '$name';\n"; } } } if ($section == 0) { verbose($verbose_level_debug, '', 'Successfully backed up xmltvid'. ' information.'. '', '', 'Creating alternate format backup.'); print BACKUP "\n\n/* Alternate format */\n". "/*\n"; } else { print BACKUP "*/\n"; verbose($verbose_level_debug, 'Successfully created alternate format'. ' backup.'); } $exit = 0; } else { verbose($verbose_level_error, '', 'ERROR: Unable to retrieve xmltvid information'. ' from database.'); die("\nError retrieving xmltvid information, stopped"); } } close BACKUP; } else { verbose($verbose_level_error, '', 'ERROR: Unable to backup xmltvids without Perl'. ' database libraries.', ' Please ensure the Perl DBI and DBD::mysql'. ' modules are installed.'); die("\nPerl database libraries missing, stopped"); } return $exit; } # This subroutine performs limited checking of a command and untaints the # command (and the environment) if the command seems to use an absolute path # containing no . or .. references or if it's a simple command name referencing # an executable in a "normal" directory for binaries. It should only be called # after careful consideration of the effects of doing so and of whether it # makes sense to override taint-mode runtime checking of the value. sub untaint_command { my $command = shift; my $allow_untaint = 0; # Only allow directories from @d_allowed_paths that exist in the PATH unless (@allowed_paths) { foreach my $path (split(/:/, $old_env_path)) { if (grep(/^$path$/, @d_allowed_paths)) { push(@allowed_paths, $path); } } verbose($verbose_level_debug + 2, '', 'Allowing paths:', @allowed_paths, 'From PATH: '.$old_env_path); } verbose($verbose_level_debug + 2, '', 'Verifying command: '.$command); if ($command =~ /^\//) { verbose($verbose_level_debug + 2, ' - Command starts with /.'); if (! ($command =~ /\/\.+\//)) { verbose($verbose_level_debug + 2, ' - Command does not contain dir refs.'); if (-e "$command" && -f "$command" && -x "$command") { # Seems to be a valid executable specified with a path starting # with / and having no current/previous directory references verbose($verbose_level_debug + 2, 'Unmodified command meets untaint requirements.', $command); $allow_untaint = 1; } } } else { foreach my $path (@allowed_paths) { if (-e "$path/$command" && -f "$path/$command" && -x "$path/$command") { # Seems to be a valid executable in a "normal" directory for # binaries $command = "$path/$command"; verbose($verbose_level_debug + 2, 'Command seems to be a simple command in a'. ' normal directory for binaries: '.$command); $allow_untaint = 1; } } } if ($allow_untaint) { if ($command =~ /^(.*)$/) { verbose($verbose_level_debug + 1, 'Untainting command: '.$command); $command = $1; $ENV{'PATH'} = ''; $is_env_tainted = 0; } } return $command; } # This subroutine performs limited checking of file or directory paths and # untaints the path if it seems to be an absolute path to a normal file or # directory and contains no . or .. references. It should only be called after # careful consideration of the effects of doing so and of whether it makes # sense to override taint-mode runtime checking of the value. sub untaint_path { my $path = shift; verbose($verbose_level_debug + 2, '', 'Verifying path: '.$path); if ($path =~ /^\//) { verbose($verbose_level_debug + 2, ' - Path starts with /.'); if (! ($path =~ /\/\.+\//)) { verbose($verbose_level_debug + 2, ' - Path contains no dir refs.'); if (-e "$path" && (-f "$path" || -d "$path")) { # Seems to be a file or directory path starting with / and # having no current/previous directory references if ($path =~ /^(.*)$/) { verbose($verbose_level_debug + 1, 'Untainting path: '.$path); $path = $1; } } } } return $path; } # This subroutine does absolutely no data checking. It blindly accepts a # possibly-tainted value and "untaints" it. It should only be called after # careful consideration of the effects of doing so and of whether it makes # sense to override taint-mode runtime checking of the value. sub untaint_data { my $value = shift; if ($value =~ /^(.*)$/) { verbose($verbose_level_debug + 1, 'Untainting data: '.$value); $value = $1; } return $value; } sub reset_environment { if (!$is_env_tainted) { $is_env_tainted = 1; $ENV{'PATH'} = $old_env_path; } } sub do_backup { my $defaults_extra_file = create_defaults_extra_file; my $host_arg = ''; my $port_arg = ''; my $user_arg = ''; if ($defaults_extra_file) { $defaults_arg = " --defaults-extra-file='$defaults_extra_file'"; } else { $defaults_arg = ''; } # For users running in environments where taint mode is activated (i.e. # running mythtv-setup or mythbackend as root), executing a command line # built with tainted data will fail. Therefore, try to untaint data if it # meets certain basic requirements. my $safe_mysqldump = $mysqldump; $safe_mysqldump = untaint_command($safe_mysqldump); $safe_mysqldump =~ s/'/'\\''/sg; $mysql_conf{'db_name'} = untaint_data($mysql_conf{'db_name'}); $mysql_conf{'db_host'} = untaint_data($mysql_conf{'db_host'}); $mysql_conf{'db_port'} = untaint_data($mysql_conf{'db_port'}); $mysql_conf{'db_user'} = untaint_data($mysql_conf{'db_user'}); $backup_conf{'directory'} = untaint_path($backup_conf{'directory'}); # Can't use untaint_path because the filename is not a full path and the # file doesn't yet exist, anyway $backup_conf{'filename'} =~ s/'/'\\''/g; $backup_conf{'filename'} = untaint_data($backup_conf{'filename'}); my $output_file = "$backup_conf{'directory'}/$backup_conf{'filename'}"; $output_file =~ s/'/'\\''/sg; # Create the args for host, port, and user, shell-escaping values, as # necessary. my $safe_db_name = $mysql_conf{'db_name'}; $safe_db_name =~ s/'/'\\''/g; my $safe_string; if ($mysql_conf{'db_host'}) { $safe_string = $mysql_conf{'db_host'}; $safe_string =~ s/'/'\\''/g; $host_arg = " --host='$safe_string'"; } if ($mysql_conf{'db_port'} > 0) { $safe_string = $mysql_conf{'db_port'}; $safe_string =~ s/'/'\\''/g; $port_arg = " --port='$safe_string'"; } if ($mysql_conf{'db_user'}) { $safe_string = $mysql_conf{'db_user'}; $safe_string =~ s/'/'\\''/g; $user_arg = " --user='$safe_string'"; } # Use redirects to capture stderr (for debug) and send stdout (the backup) # to a file my $command = "'${safe_mysqldump}'${defaults_arg}${host_arg}". "${port_arg}${user_arg} --add-drop-table --add-locks ". "--allow-keywords --complete-insert --extended-insert ". "--lock-tables --no-create-db --quick --add-drop-table ". "'$safe_db_name' 2>&1 1>'$output_file'"; verbose($verbose_level_debug, '', 'Executing command:', $command); my $result = `$command`; my $exit = $? >> 8; verbose($verbose_level_debug, '', "$mysqldump exited with status: $exit"); verbose($verbose_level_debug, "$mysqldump output:", $result) if ($exit); reset_environment; return $exit; } sub compress_backup { if (!-e "$backup_conf{'directory'}/$backup_conf{'filename'}") { verbose($verbose_level_debug, '', 'Unable to find backup file to compress'); return 1; } my $result = 0; verbose($verbose_level_debug, '', 'Attempting to compress backup file.'); if ($d_compress eq $compress) { # Try to load the IO::Compress::Gzip library if available (but don't # require it) BEGIN { our $has_compress_gzip = 1; # Though this does nothing, it prevents an invalid "only used # once" warning that occurs for users without IO::Compress # installed. undef $GzipError; eval 'use IO::Compress::Gzip qw(gzip $GzipError);'; if ($@) { $has_compress_gzip = 0; } } if (!$has_compress_gzip) { verbose($verbose_level_debug, " - IO::Compress::Gzip is not installed."); } else { if (-e "$backup_conf{'directory'}/$backup_conf{'filename'}.gz") { verbose($verbose_level_debug, '', 'A file whose name is the backup filename'. ' with the \'.gz\' extension already', 'exists. Leaving backup uncompressed.'); return 1; } verbose($verbose_level_debug, " - Compressing backup file with IO::Compress::Gzip."); $result = gzip( "$backup_conf{'directory'}/$backup_conf{'filename'}" => "$backup_conf{'directory'}/$backup_conf{'filename'}.gz"); if ((defined($result)) && (-e "$backup_conf{'directory'}/". "$backup_conf{'filename'}.gz")) { # For users running in environments where taint mode is # activated (i.e. running mythtv-setup or mythbackend as # root), unlinking a file whose path is built with tainted data # will fail. Therefore, try to untaint the path if it meets # certain basic requirements. my $uncompressed_file = $backup_conf{'directory'}."/". $backup_conf{'filename'}; $uncompressed_file = untaint_path($uncompressed_file); $uncompressed_file =~ s/'/'\\''/sg; verbose($verbose_level_debug + 2, "Unlinking uncompressed file: $uncompressed_file"); unlink "$uncompressed_file"; $backup_conf{'filename'} = "$backup_conf{'filename'}.gz"; verbose($verbose_level_debug, '', 'Successfully compressed backup to file:', "$backup_conf{'directory'}/". "$backup_conf{'filename'}"); return 0; } verbose($verbose_level_debug, " Error: $GzipError"); } } # Try to compress the file with the compress binary. verbose($verbose_level_debug, " - Compressing backup file with $compress."); my $backup_path = "$backup_conf{'directory'}/$backup_conf{'filename'}"; # For users running in environments where taint mode is activated (i.e. # running mythtv-setup or mythbackend as root), executing a command line # built with tainted data will fail. Therefore, try to untaint data if it # meets certain basic requirements. $compress = untaint_command($compress); $compress =~ s/'/'\\''/sg; $backup_path = untaint_path($backup_path); $backup_path =~ s/'/'\\''/sg; my $command = "'$compress' '$backup_path' 2>&1"; verbose($verbose_level_debug, '', 'Executing command:', $command); my $output = `$command`; my $exit = $? >> 8; verbose($verbose_level_debug, '', "$compress exited with status: $exit"); if ($exit) { verbose($verbose_level_debug, "$compress output:", $output); } else { $backup_conf{'filename'} = "$backup_conf{'filename'}.gz"; } reset_environment; return $exit; } sub rotate_backups { if (($rotate < 1) || (!defined($rotateglob)) || (!$rotateglob)) { verbose($verbose_level_debug, '', 'Backup file rotation disabled.'); return 0; } verbose($verbose_level_debug, '', 'Rotating backups.'); verbose($verbose_level_debug, '', 'Searching for files matching pattern:', "$backup_conf{'directory'}/$rotateglob"); my @files = <$backup_conf{'directory'}/$rotateglob>; my @sorted_files = sort { lc($a) cmp lc($b) } @files; my $num_files = @sorted_files; verbose($verbose_level_debug, " - Found $num_files matching files."); $num_files = $num_files - $rotate; $num_files = 0 if ($num_files < 0); verbose($verbose_level_debug, '', "Deleting $num_files and keeping (up to) $rotate backup". ' files.'); my $index = 0; foreach my $file (@sorted_files) { if ($index++ < $num_files) { if ($file eq "$backup_conf{'directory'}/$backup_conf{'filename'}") { # This is the just-created backup. Warn the user that older # backups with newer schema versions may cause rotation to # fail. verbose($verbose_level_debug, '', 'WARNING: You seem to have reverted to an'. ' older database schema version.', 'You should move all backups from newer schema'. ' versions to another directory or', 'delete them to prevent your new backups from'. ' being deleted on rotation.', ''); verbose($verbose_level_debug, " - Keeping backup file: $file"); } else { verbose($verbose_level_debug, " - Deleting old backup file: $file"); # For users running in environments where taint mode is # activated (i.e. running mythtv-setup or mythbackend as # root), unlinking a file whose path is built with tainted data # will fail. Therefore, try to untaint the path if it meets # certain basic requirements. $file = untaint_path($file); $file =~ s/'/'\\''/sg; unlink "$file"; } } else { verbose($verbose_level_debug, " - Keeping backup file: $file"); } } return 1; } ############################################################################## # Main functionality ############################################################################## # The first argument after option parsing, if it exists, should be a database # information file. $database_information_file = shift; configure_environment; read_config; check_config; print_configuration; my $status = 1; if ($backup_xmltvids) { $status = do_xmltvid_backup; } else { $status = do_backup; if (!$status) { compress_backup; rotate_backups; } } $dbh->disconnect if (defined($dbh)); exit $status;