3 # mythconverg_backup.pl
5 # Creates a backup of the MythTV database.
8 # mythconverg_backup.pl --help
12 use File::Temp qw/ tempfile /;
15 $NAME = 'MythTV Database Backup Script';
18 # Some variables we'll use here
19 our ($username, $homedir, $mythconfdir, $database_information_file);
20 our ($mysqldump, $compress, $rotate, $rotateglob, $backup_xmltvids);
21 our ($usage, $debug, $show_version, $show_version_script, $dbh);
22 our ($d_db_name, $d_mysqldump, $d_compress, $d_rotate, $d_rotateglob);
23 # This script does not accept a database password on the command-line.
24 # Any packager who enables the functionality should modify the --help output.
26 our ($db_hostname, $db_port, $db_username, $db_name, $db_schema_version);
27 our ($backup_directory, $backup_filename);
28 our ($verbose_level_always, $verbose_level_debug, $verbose_level_error);
30 our %mysql_conf = ('db_host' => '',
37 our %backup_conf = ('directory' => '',
41 # Variables used to untaint data
42 our $is_env_tainted = 1;
43 our $old_env_path = $ENV{"PATH"};
44 our @d_allowed_paths = ("/bin",
54 $verbose_level_always = 0;
55 $verbose_level_debug = 1;
56 $verbose_level_error = 255;
59 $d_db_name = 'mythconverg';
60 $d_mysqldump = 'mysqldump';
63 $d_rotateglob = $d_db_name.'-????-??????????????.sql*';
65 # Provide default values for GetOptions
66 $mysqldump = $d_mysqldump;
67 $compress = $d_compress;
69 $rotateglob = $d_rotateglob;
72 # Load the cli options
73 GetOptions('hostname|DBHostName=s' => \$db_hostname,
74 'port|DBPort=i' => \$db_port,
75 'username|DBUserName=s' => \$db_username,
76 # This script does not accept a database password on the command-line.
77 # 'password|DBPassword=s' => \$db_password,
78 'name|DBName=s' => \$db_name,
79 'schemaver|DBSchemaVer=s' => \$db_schema_version,
80 'directory|DBBackupDirectory=s' => \$backup_directory,
81 'filename|DBBackupFilename=s' => \$backup_filename,
82 'mysqldump=s' => \$mysqldump,
83 'compress=s' => \$compress,
84 'rotate=i' => \$rotate,
85 'rotateglob|glob=s' => \$rotateglob,
86 'backup_xmltvids|backup-xmltvids|'.
87 'xmltvids' => \$backup_xmltvids,
88 'usage|help|h+' => \$usage,
89 'version' => \$show_version,
90 'script_version|script-version|v' => \$show_version_script,
91 'verbose|debug|d+' => \$debug
94 # Print version information
95 sub print_version_information
97 my $script_name = substr $0, rindex($0, '/') + 1;
98 print "$NAME\n$script_name\nversion: $VERSION\n";
101 if ($show_version_script)
103 print "$NAME,$VERSION,,\n";
106 elsif ($show_version)
108 print_version_information;
116 print_version_information;
120 $0 [options|database_information_file]
122 Creates a backup of the MythTV database.
126 Create a file ~/.mythtv/backuprc with a single line,
127 "DBBackupDirectory=/home/mythtv" (no quotes), and run this script to create a
128 database backup. Use the --verbose argument to see what is happening.
130 # echo "DBBackupDirectory=/home/mythtv" > ~/.mythtv/backuprc
133 Make sure you keep the backuprc file for next time. Once you have successfully
134 created a backup, the script may be run without the --verbose argument.
138 Ensure you have a ~/.mythtv/backuprc file, as described above, and execute this
139 script with the --backup_xmltvids argument.
141 # $0 --backup_xmltvids
148 DETAILED DESCRIPTION:
150 This script can be called by MythTV for creating automatic database backups.
151 In this mode, it is always exected with a single command-line argument
152 specifying the name of a "database information file" (see DATABASE INFORMATION
153 FILE, below), which contains sufficient information about the database and the
154 backup to allow the script to create a backup without needing any additional
155 configuration files. In this mode, all other MythTV configuration files
156 (including config.xml, mysql.txt) are ignored, but the backup resource file
157 (see RESOURCE FILE, below) and the MySQL option files (i.e. /etc/my.cnf or
158 ~/.my.cnf) will be honored.
160 The script can also be called interactively (i.e. "manually") by the user to
161 create a database backup on demand. Required information may be passed into
162 the script using command-line arguments or with a database information file.
163 If a database information file is specified, all command-line arguments will be
164 ignored. If no database information file is specified, the script will attempt
165 to determine the appropriate configuration by using the MythTV configuration
166 file(s) (preferring config.xml, but falling back to mysql.txt if no config.xml
167 exists). Once the MythTV configuration file has been parsed, the backup
168 resource file (see RESOURCE FILE, below) will be parsed, then command-line
169 arguments will be applied (thus overriding any values determined from the
170 configuration files).
172 The only information required by the script is the directory in which the
173 backup should be created. Therefore, when using a database information file,
174 the DBBackupDirectory should be specified, or if running manually, the
175 --directory command-line argument should be specified. The DBBackupDirectory
176 may be specified in a backup resource file (see RESOURCE FILE, below). Doing
177 so is especially useful for manual backups. If the specified directory is not
178 writable, the script will terminate. Likewise, if a file whose name matches
179 the name to be used for the backup file already exists, the script will
182 If the database name is not specified, the script will attempt to use the
183 MythTV default database name, $d_db_name. Note that the same is not true for
184 the database username and database password. These must be explicitly
185 specified. The password must be specified in a database information file, a
186 backup resource file, or a MySQL options file. The username may be specified
187 the same way or may be specified using a command-line argument if not using a
188 database information file.
190 While this script may be called while MythTV is running, there is a possibility
191 of creating a backup with data integrity errors (i.e. if MythTV updates data in
192 multiple tables between the time the script backs up the first and subsequent
193 tables). Also, depending on your system configuration, performing a backup
194 (which may result in locking a table while it is being backed up) while
195 recording may cause corruption of the recording or inability to properly write
196 recording data (such as the recording seek table) to the database.
197 Therefore, if configuring this script to run in a cron job, try to ensure it
198 runs at a time when recordings are least likely to occur. Alternatively, by
199 choosing to run the script in a system start/shutdown script (i.e. an init
200 script), you may call the script before starting mythbackend or after stopping
201 mythbackend. Note, however, that checking whether to perform the backup is the
202 responsibility of the init script (not this script)--i.e. in a system with
203 multiple frontends/backends, the init script should ensure the backup is
204 created only on the master backend.
206 DATABASE INFORMATION FILE
208 The database information file contains information about the database and the
209 backup. The information within the file is specified as name=value pairs using
210 the same names as used by the MythTV config.xml and mysql.txt configuration
211 files. The following variables are recognized:
213 DBHostName - The hostname (or IP address) which should be used to find the
215 DBPort - The TCP/IP port number to use for the connection. This may have a
216 value of 0, i.e. if the hostname is localhost or if the server is
217 using the default MySQL port or the port specified in a MySQL
219 DBUserName - The database username to use when connecting to the server.
220 DBPassword - The password to use when connecting to the server.
221 DBName - The name of the database that contains the MythTV data.
222 DBSchemaVer - The MythTV schema version of the database. This value will be
223 used to create the backup filename, but only if the filename
224 has not been specified using DBBackupFilename or the --filename
226 DBBackupDirectory - The directory in which the backup file should be
227 created. This directory may have been specially
228 configured by the user as the "DB Backups" storage
229 group. It is recommended that this directory be
230 used--especially in "common-use" scripts such as those
231 provided by distributions.
232 DBBackupFilename - The name of the file in which the backup should be
233 created. Additional extensions may be added by this
234 script as required (i.e. adding an appropriate suffix,
235 such as ".gz", to the file if it is compressed). If the
236 filename recommended by mythbackend is used, it will be
237 displayed in the GUI messages provided for the user. If
238 the recommended filename is not used, the user will not be
239 told where to find the backup file. If no value is
240 provided, a filename using the default filename format
242 mysqldump - The path (including filename) of the mysqldump executable.
243 compress - The command (including path, if necessary) to use to
244 compress the backup. Using gzip is significantly less
245 resource intensive on an SQL backup file than using bzip2,
246 at the cost of a slightly (about 33%) larger compressed
247 filesize, a difference which should be irrelevant at the
248 filesizes involved (especially when compared to the size
249 of recording files). If you decide to use another
250 compression algorithm, please ensure you test it
251 appropriately to verify it does not negatively affect
252 operation of your system. If no value is specified for
253 compress or if the value '$d_compress' is specified, the
254 script will first attempt to use the IO::Compress::Gzip
255 module to compress the backup file, but, if not available,
256 will run the command specified. Therefore, if
257 IO::Compress::Gzip is installed and functional, specifying
258 a value for compress is unnecessary. If neither approach
259 works, the backup file will be left uncompressed.
260 rotate - The number of backups to keep when rotating. To disable
261 rotation, specify -1. Backup rotation is performed by
262 identifying all files in DBBackupDirectory whose names
263 match the glob specified by rotateglob. It is critical
264 that the chosen backup filenames can be sorted properly
265 using an alphabetical sort. If using the default filename
266 format--which contains the DBSchemaVer--and you downgrade
267 MythTV and restore a backup from an older DBSchemaVer,
268 make sure you move the backups from the newer DBSchemaVer
269 out of the DBBackupDirectory or they may cause your new
270 backups to be deleted.
271 rotateglob - The sh-like glob used to identify files within
272 DBBackupDirectory to be considered for rotation. Be
273 very careful with the value--especially if using a
274 DBBackupDirectory that contains any files other than
279 The backup resource file specifies values using the same format as described
280 for the database information file, above, but is intended as a "permanent,"
281 user-created configuration file. The database information file is intended as
282 a "single-use" configuration file, often created automatically (i.e. by a
283 program, such as mythbackend, or a script). The backup resource file should be
284 placed at "~/.mythtv/backuprc" and given appropriate permissions. To be usable
285 by the script, it must be readable. However, it should be protected as
286 required--i.e. if the DBPassword is specified, it should be made readable only
289 When specifying a database information file, the resource file is parsed before
290 the database information file to prevent the resource file from overriding the
291 information in the database information file. When no database information
292 file is specified, the resource file is parsed after the MythTV configuration
293 files, but before the command-line arguments to allow the resource file to
294 override values in the configuration files and to allow command-line arguments
295 to override resource file defaults.
299 --hostname [database hostname]
301 The hostname (or IP address) which should be used to find the MySQL server.
302 See DBHostName, above.
304 --port [database port]
306 The TCP/IP port number to use for connection to the MySQL server. See
309 --username [database username]
311 The MySQL username to use for connection to the MySQL server. See
314 --name [database name]
316 The name of the database containing the MythTV data. See DBName, above.
320 --schemaver [MythTV database schema version]
322 The MythTV schema version. See DBSchemaVer, above.
324 --directory [directory]
326 The directory in which the backup file should be stored. See
327 DBBackupDirectory, above.
329 --filename [database backup filename]
331 The name to use for the database backup file. If not provided, a filename
332 using a default format will be chosen. See DBBackupFilename, above.
336 The path (including filename) of the mysqldump executable. See mysqldump
337 in the DATABASE INFORMATION FILE description, above.
339 Default: $d_mysqldump
343 The command (including path, if necessary) to use to compress the backup.
344 See compress in the DATABASE INFORMATION FILE description, above.
349 The number of backups to keep when rotating. To disable rotation, specify
350 -1. See rotate in the DATABASE INFORMATION FILE description, above.
355 The sh-like glob used to identify files within DBBackupDirectory to be
356 considered for rotation. See rotateglob in the DATABASE INFORMATION FILE
359 Default: $d_rotateglob
362 Rather than creating a backup of the entire database, create a backup of
363 xmltvids. This is useful when doing a full channel scan. The resulting
364 backup is a series of SQL UPDATE statements that can be executed to set
365 the xmltvid for channels whose callsign is the same before and after
366 the scan. Note that the backup file will contain comments with additional
367 channel information, which you can use to identify channels in case the
376 Show version information.
380 Show what is happening.
382 --script_version | -v
384 Show script version information. This is primarily useful for scripts
385 or programs needing to parse the version information.
391 print "For detailed help:\n\n# $0 --help --help\n\n";
400 if ($level == $verbose_level_error)
406 return unless ($debug >= $level);
408 print { $error ? STDERR : STDOUT } join("\n", @_), "\n";
411 sub print_configuration
413 verbose($verbose_level_debug,
415 'Database Information:',
416 " DBHostName: $mysql_conf{'db_host'}",
417 " DBPort: $mysql_conf{'db_port'}",
418 " DBUserName: $mysql_conf{'db_user'}",
420 ( $mysql_conf{'db_pass'} ? 'XXX' : '' ),
421 # "$mysql_conf{'db_pass'}",
422 " DBName: $mysql_conf{'db_name'}",
423 " DBSchemaVer: $mysql_conf{'db_schemaver'}",
424 " DBBackupDirectory: $backup_conf{'directory'}",
425 " DBBackupFilename: $backup_conf{'filename'}");
426 verbose($verbose_level_debug,
429 " mysqldump: $mysqldump",
430 " compress: $compress");
433 sub configure_environment
435 verbose($verbose_level_debug,
436 '', 'Configuring environment:');
438 # Get the user's login and home directory, so we can look for config files
439 ($username, $homedir) = (getpwuid $>)[0,7];
440 $username = $ENV{'USER'} if ($ENV{'USER'});
441 $homedir = $ENV{'HOME'} if ($ENV{'HOME'});
442 if ($username && !$homedir)
444 $homedir = "/home/$username";
445 if (!-e $homedir && -e "/Users/$username")
447 $homedir = "/Users/$username";
450 verbose($verbose_level_debug,
451 " - username: $username",
452 " - HOME: $homedir");
454 # Find the config directory
455 $mythconfdir = $ENV{'MYTHCONFDIR'}
456 ? $ENV{'MYTHCONFDIR'}
460 verbose($verbose_level_debug,
461 " - MYTHCONFDIR: $mythconfdir");
464 # Though much of the configuration file parsing could be done by the MythTV
465 # Perl bindings, using them to retrieve database information is not appropriate
466 # for a backup script. The Perl bindings require the backend to be running and
467 # use UPnP for autodiscovery. Also, parsing the files "locally" allows
468 # supporting even the old MythTV database configuration file, mysql.txt.
469 sub parse_database_information
472 verbose($verbose_level_debug,
473 " - checking: $file");
474 return 0 unless ($file && -e $file);
475 verbose($verbose_level_debug,
477 open(CONF, $file) or die("\nERROR: Unable to read $file: $!".
479 while (my $line = <CONF>)
482 next if ($line =~ m/^\s*#/);
487 # Split off the var=val pairs
488 my ($var, $val) = split(/ *[\=\: ] */, $line, 2);
489 # Also look for <var>val</var> from config.xml
490 if ($line =~ m/<(\w+)>(.+)<\/(\w+)>$/ && $1 eq $3)
492 $var = $1; $val = $2;
494 next unless ($var && $var =~ m/\w/);
495 if (($var eq 'Host') || ($var eq 'DBHostName'))
497 $mysql_conf{'db_host'} = $val;
499 elsif (($var eq 'Port') || ($var eq 'DBPort'))
501 $mysql_conf{'db_port'} = $val;
503 elsif (($var eq 'UserName') || ($var eq 'DBUserName'))
505 $mysql_conf{'db_user'} = $val;
507 elsif (($var eq 'Password') || ($var eq 'DBPassword'))
509 $mysql_conf{'db_pass'} = $val;
510 $mysql_conf{'db_pass'} =~ s/&/&/sg;
511 $mysql_conf{'db_pass'} =~ s/>/>/sg;
512 $mysql_conf{'db_pass'} =~ s/</</sg;
514 elsif (($var eq 'DatabaseName') || ($var eq 'DBName'))
516 $mysql_conf{'db_name'} = $val;
518 elsif ($var eq 'DBSchemaVer')
520 $mysql_conf{'db_schemaver'} = $val;
522 elsif ($var eq 'DBBackupDirectory')
524 $backup_conf{'directory'} = $val;
526 elsif ($var eq 'DBBackupFilename')
528 $backup_conf{'filename'} = $val;
530 elsif ($var eq 'mysqldump')
534 elsif ($var eq 'compress')
538 elsif ($var eq 'rotate')
542 elsif ($var eq 'rotateglob')
553 # Read the "legacy" mysql.txt file in use by MythTV. It could be in a
554 # couple places, so try the usual suspects in the same order that mythtv
555 # does in libs/libmyth/mythcontext.cpp
558 my @mysql = ('/usr/local/share/mythtv/mysql.txt',
559 '/usr/share/mythtv/mysql.txt',
560 '/usr/local/etc/mythtv/mysql.txt',
561 '/etc/mythtv/mysql.txt',
562 $homedir ? "$homedir/.mythtv/mysql.txt" : '',
564 $mythconfdir ? "$mythconfdir/mysql.txt" : '',
566 foreach my $file (@mysql)
568 $found = parse_database_information($file);
569 $result = $result + $found;
574 sub read_resource_file
576 parse_database_information("$mythconfdir/backuprc");
581 verbose($verbose_level_debug,
582 '', 'Applying command-line arguments.');
585 $mysql_conf{'db_host'} = $db_hostname;
589 $mysql_conf{'db_port'} = $db_port;
593 $mysql_conf{'db_user'} = $db_username;
595 # This script does not accept a database password on the command-line.
598 # $mysql_conf{'db_pass'} = $db_password;
602 $mysql_conf{'db_name'} = $db_name;
604 if ($db_schema_version)
606 $mysql_conf{'db_schemaver'} = $db_schema_version;
608 if ($backup_directory)
610 $backup_conf{'directory'} = $backup_directory;
612 if ($backup_filename)
614 $backup_conf{'filename'} = $backup_filename;
621 # If specified, use only the database information file
622 if ($database_information_file)
624 verbose($verbose_level_debug,
625 '', 'Database Information File specified. Ignoring all'.
626 ' command-line arguments');
627 verbose($verbose_level_debug,
628 '', 'Database Information File: '.
629 $database_information_file);
630 unless (-T "$database_information_file")
632 verbose($verbose_level_always,
633 '', 'The argument you supplied for the database'.
634 ' information file is invalid.',
635 'If you were trying to specify a backup filename,'.
636 ' please use the --directory ',
637 'and --filename arguments.');
638 die("\nERROR: Invalid database information file, stopped");
640 # When using a database information file, parse the resource file first
641 # so it cannot override database information file settings
643 $result = parse_database_information($database_information_file);
647 # No database information file, so try the MythTV configuration files.
648 verbose($verbose_level_debug,
649 '', 'Parsing configuration files:');
650 # Prefer the config.xml file
651 my $file = $mythconfdir ? "$mythconfdir/config.xml" : '';
652 $result = parse_database_information($file);
655 # Use the "legacy" mysql.txt file as a fallback
656 $result = read_mysql_txt;
658 # Read the resource file next to override the config file information, but
659 # to allow command-line arguments to override resource file "defaults"
661 # Apply the command-line arguments to override the information provided
662 # by the config file(s).
667 sub check_database_libs
669 # Try to load the DBI library if available (but don't require it)
679 verbose($verbose_level_debug,
680 '', 'DBI is not installed.') if (!$has_dbi);
681 # Try to load the DBD::mysql library if available (but don't
686 eval 'use DBD::mysql;';
692 verbose($verbose_level_debug,
693 '', 'DBD::mysql is not installed.') if (!$has_dbd);
694 return ($has_dbi + $has_dbd);
701 my $have_database_libs = check_database_libs;
702 return 0 if ($have_database_libs < 2);
703 my $temp_host = $mysql_conf{'db_host'};
704 if ($temp_host =~ /:/)
706 if ($temp_host =~ /^(?!\[).*(?!\])$/)
708 $temp_host = "[$temp_host]";
711 $dbh = DBI->connect("dbi:mysql:".
713 "port=$mysql_conf{'db_port'}:".
714 "database=$mysql_conf{'db_name'}",
715 "$mysql_conf{'db_user'}",
716 "$mysql_conf{'db_pass'}",
717 { PrintError => 1 });
722 sub create_backup_filename
724 # Create a default backup filename
725 $backup_conf{'filename'} = $mysql_conf{'db_name'};
726 if (!$backup_conf{'filename'})
728 $backup_conf{'filename'} = $d_db_name;
730 if ((!$mysql_conf{'db_schemaver'}) &&
731 ($mysql_conf{'db_host'}) && ($mysql_conf{'db_name'}) &&
732 ($mysql_conf{'db_user'}) && ($mysql_conf{'db_pass'}))
734 # If DBI is available, query the DB for the schema version
737 verbose($verbose_level_debug,
738 '', 'No DBSchemaVer specified, querying database.');
739 my $query = 'SELECT data FROM settings WHERE value = ?';
742 my $sth = $dbh->prepare($query);
743 if ($sth->execute('DBSchemaVer'))
745 while (my @data = $sth->fetchrow_array)
747 $mysql_conf{'db_schemaver'} = $data[0];
748 verbose($verbose_level_debug,
749 "Found DBSchemaVer:".
750 " $mysql_conf{'db_schemaver'}.");
755 verbose($verbose_level_debug,
756 "Unable to retrieve DBSchemaVer from".
757 " database. Filename will not contain ",
764 verbose($verbose_level_debug,
765 '', 'No DBSchemaVer specified.',
766 'DBI and/or DBD:mysql is not available. Unable'.
767 ' to query database to determine ',
768 'DBSchemaVer. DBSchemaVer will not be included'.
769 ' in backup filename.',
770 'Please ensure DBI and DBD::mysql are'.
774 if ($mysql_conf{'db_schemaver'})
776 $backup_conf{'filename'} .= '-'.$mysql_conf{'db_schemaver'};
778 # Format the time using localtime data so we don't have to bring in
779 # another dependency.
780 my @timeData = localtime(time);
781 $backup_conf{'filename'} .= sprintf('-%04d%02d%02d%02d%02d%02d.sql',
782 ($timeData[5] + 1900),
784 $timeData[3], $timeData[2],
785 $timeData[1], $timeData[0]);
788 sub check_backup_directory
791 if ($backup_conf{'directory'})
795 elsif (check_database)
796 # If DBI is available, query the DB for the backup directory
798 verbose($verbose_level_debug,
799 '', 'No DBBackupDirectory specified, querying database.');
800 my $query = 'SELECT dirname, hostname FROM storagegroup '.
801 ' WHERE groupname = ?';
805 my $sth = $dbh->prepare($query);
806 if ($sth->execute('DB Backups'))
808 # We don't know the hostname associated with this host, and
809 # since it's not worth parsing the mysql.txt/config.xml
810 # LocalHostName (unique identifier), with fallback to the
811 # system hostname, and handling issues along the way, just look
812 # for any available DB Backups directory and, if none are
813 # usable, look for a Default group directory
814 while (my @data = $sth->fetchrow_array)
816 $directory = $data[0];
817 if (-d $directory && -w $directory)
819 $backup_conf{'directory'} = $directory;
820 verbose($verbose_level_debug,
821 "Found DB Backups directory:".
822 " $backup_conf{'directory'}.");
829 if ($result == 0 && $sth->execute('Default'))
831 while (my @data = $sth->fetchrow_array)
833 $directory = $data[0];
834 if (-d $directory && -w $directory)
836 $backup_conf{'directory'} = $directory;
837 verbose($verbose_level_debug,
838 "Found Default directory:".
839 " $backup_conf{'directory'}.");
849 verbose($verbose_level_debug,
850 "Unable to retrieve DBBackupDirectory from".
859 verbose($verbose_level_debug,
860 '', 'Checking configuration.');
861 # Check directory/filename
862 if (!check_backup_directory)
865 die("\nERROR: DBBackupDirectory not specified, stopped");
867 if ((!-d $backup_conf{'directory'}) ||
868 (!-w $backup_conf{'directory'}))
871 verbose($verbose_level_error,
872 '', 'ERROR: DBBackupDirectory is not a directory or is '.
873 'not writable. Please specify',
874 ' a directory in your database information file'.
875 ' using DBBackupDirectory.',
876 ' If not using a database information file,'.
877 ' please specify the ',
878 ' --directory command-line option.');
879 die("\nInvalid backup directory, stopped");
881 if (!$backup_conf{'filename'})
883 if ($backup_xmltvids)
885 my $file = 'mythtv_xmltvid_backup';
886 # Format the time using localtime data so we don't have to bring in
887 # another dependency.
888 my @timeData = localtime(time);
889 $file .= sprintf('-%04d%02d%02d%02d%02d%02d.sql',
890 ($timeData[5] + 1900),
892 $timeData[3], $timeData[2],
893 $timeData[1], $timeData[0]);
894 $backup_conf{'filename'} = $file;
898 create_backup_filename;
901 if ( -e "$backup_conf{'directory'}/$backup_conf{'filename'}")
903 verbose($verbose_level_error,
904 '', 'ERROR: The specified file already exists.');
905 die("\nInvalid backup filename, stopped");
907 if (!$mysql_conf{'db_name'})
909 verbose($verbose_level_debug,
910 '', "WARNING: DBName not specified. Using $d_db_name");
911 $mysql_conf{'db_name'} = $d_db_name;
913 # Though the script will attempt a backup even if no other database
914 # information is provided (i.e. using "defaults" from the MySQL options
915 # file, warning the user that some "normally-necessary" information is not
916 # provided may be nice.
918 if (!$mysql_conf{'db_host'})
920 verbose($verbose_level_debug,
921 '', 'WARNING: DBHostName not specified.',
922 ' Assuming it is specified in the MySQL'.
925 if (!$mysql_conf{'db_user'})
927 verbose($verbose_level_debug,
928 '', 'WARNING: DBUserName not specified.',
929 ' Assuming it is specified in the MySQL'.
932 if (!$mysql_conf{'db_pass'})
934 verbose($verbose_level_debug,
935 '', 'WARNING: DBPassword not specified.',
936 ' Assuming it is specified in the MySQL'.
941 sub create_defaults_extra_file
943 return '' if (!$mysql_conf{'db_pass'});
944 verbose($verbose_level_debug,
945 '', "Attempting to use supplied password for $mysqldump.",
946 'Any [client] or [mysqldump] password specified in the MySQL'.
947 ' options file will',
949 # Let tempfile handle unlinking on exit so we don't have to verify that the
950 # file with $filename is the file we created
951 my ($fh, $filename) = tempfile(UNLINK => 1);
952 # Quote the password if it contains # or whitespace or quotes.
953 # Quoting of values in MySQL options files is only supported on MySQL
954 # 4.0.16 and above, so only quote if required.
956 my $safe_password = $mysql_conf{'db_pass'};
957 if ($safe_password =~ /[#'"\s]/)
960 $safe_password =~ s/'/\\'/g;
962 print $fh "[client]\npassword=${quote}${safe_password}${quote}\n".
963 "[mysqldump]\npassword=${quote}${safe_password}${quote}\n";
967 sub do_xmltvid_backup
972 my ($chanid, $channum, $callsign, $name, $xmltvid);
973 my $query = " SELECT chanid, channum, callsign, name, xmltvid".
975 "ORDER BY CAST(channum AS SIGNED),".
976 " CAST(SUBSTRING(channum".
978 " LOCATE('_', channum) +".
979 " LOCATE('-', channum) +".
980 " LOCATE('#', channum) +".
981 " LOCATE('.', channum)))".
983 my $sth = $dbh->prepare($query);
984 verbose($verbose_level_debug,
985 '', 'Querying database for xmltvid information.');
986 my $file = "$backup_conf{'directory'}/$backup_conf{'filename'}";
987 open BACKUP, '>', $file or die("\nERROR: Unable to open".
988 " $file: $!, stopped");
989 for ($section = 0; $section < 2; $section++)
993 while (my @data = $sth->fetchrow_array)
997 $callsign = $data[2];
1000 verbose($verbose_level_debug,
1001 "Found channel: $chanid, $channum, $callsign,".
1002 " $name, $xmltvid.") if ($section == 0);
1003 if ($xmltvid && $callsign)
1007 print BACKUP "-- Start Channel Data\n".
1008 "-- ID: '$chanid'\n".
1009 "-- Number: '$channum'\n".
1010 "-- Callsign: '$callsign'\n".
1011 "-- Name: '$name'\n".
1012 "-- XMLTVID: '$xmltvid'\n".
1013 "-- End Channel Data\n";
1014 print BACKUP "UPDATE channel".
1015 " SET xmltvid = '$xmltvid'".
1016 " WHERE callsign = '$callsign'".
1021 print BACKUP "UPDATE channel".
1022 " SET xmltvid = '$xmltvid'".
1023 " WHERE channum = '$channum'".
1024 " AND name = '$name';\n";
1030 verbose($verbose_level_debug,
1031 '', 'Successfully backed up xmltvid'.
1033 '', '', 'Creating alternate format backup.');
1034 print BACKUP "\n\n/* Alternate format */\n".
1039 print BACKUP "*/\n";
1040 verbose($verbose_level_debug,
1041 'Successfully created alternate format'.
1048 verbose($verbose_level_error,
1049 '', 'ERROR: Unable to retrieve xmltvid information'.
1051 die("\nError retrieving xmltvid information, stopped");
1058 verbose($verbose_level_error,
1059 '', 'ERROR: Unable to backup xmltvids without Perl'.
1060 ' database libraries.',
1061 ' Please ensure the Perl DBI and DBD::mysql'.
1062 ' modules are installed.');
1063 die("\nPerl database libraries missing, stopped");
1068 # This subroutine performs limited checking of a command and untaints the
1069 # command (and the environment) if the command seems to use an absolute path
1070 # containing no . or .. references or if it's a simple command name referencing
1071 # an executable in a "normal" directory for binaries. It should only be called
1072 # after careful consideration of the effects of doing so and of whether it
1073 # makes sense to override taint-mode runtime checking of the value.
1076 my $command = shift;
1077 my $allow_untaint = 0;
1078 # Only allow directories from @d_allowed_paths that exist in the PATH
1079 unless (@allowed_paths)
1081 foreach my $path (split(/:/, $old_env_path))
1083 if (grep(/^$path$/, @d_allowed_paths))
1085 push(@allowed_paths, $path);
1088 verbose($verbose_level_debug + 2,
1089 '', 'Allowing paths:', @allowed_paths,
1090 'From PATH: '.$old_env_path);
1093 verbose($verbose_level_debug + 2, '', 'Verifying command: '.$command);
1094 if ($command =~ /^\//)
1096 verbose($verbose_level_debug + 2, ' - Command starts with /.');
1097 if (! ($command =~ /\/\.+\//))
1099 verbose($verbose_level_debug + 2,
1100 ' - Command does not contain dir refs.');
1101 if (-e "$command" && -f "$command" && -x "$command")
1103 # Seems to be a valid executable specified with a path starting
1104 # with / and having no current/previous directory references
1105 verbose($verbose_level_debug + 2,
1106 'Unmodified command meets untaint requirements.',
1114 foreach my $path (@allowed_paths)
1116 if (-e "$path/$command" && -f "$path/$command" &&
1117 -x "$path/$command")
1119 # Seems to be a valid executable in a "normal" directory for
1121 $command = "$path/$command";
1122 verbose($verbose_level_debug + 2,
1123 'Command seems to be a simple command in a'.
1124 ' normal directory for binaries: '.$command);
1131 if ($command =~ /^(.*)$/)
1133 verbose($verbose_level_debug + 1,
1134 'Untainting command: '.$command);
1137 $is_env_tainted = 0;
1143 # This subroutine performs limited checking of file or directory paths and
1144 # untaints the path if it seems to be an absolute path to a normal file or
1145 # directory and contains no . or .. references. It should only be called after
1146 # careful consideration of the effects of doing so and of whether it makes
1147 # sense to override taint-mode runtime checking of the value.
1151 verbose($verbose_level_debug + 2, '', 'Verifying path: '.$path);
1154 verbose($verbose_level_debug + 2, ' - Path starts with /.');
1155 if (! ($path =~ /\/\.+\//))
1157 verbose($verbose_level_debug + 2,
1158 ' - Path contains no dir refs.');
1159 if (-e "$path" && (-f "$path" || -d "$path"))
1161 # Seems to be a file or directory path starting with / and
1162 # having no current/previous directory references
1163 if ($path =~ /^(.*)$/)
1165 verbose($verbose_level_debug + 1,
1166 'Untainting path: '.$path);
1175 # This subroutine does absolutely no data checking. It blindly accepts a
1176 # possibly-tainted value and "untaints" it. It should only be called after
1177 # careful consideration of the effects of doing so and of whether it makes
1178 # sense to override taint-mode runtime checking of the value.
1182 if ($value =~ /^(.*)$/)
1184 verbose($verbose_level_debug + 1, 'Untainting data: '.$value);
1190 sub reset_environment
1192 if (!$is_env_tainted)
1194 $is_env_tainted = 1;
1195 $ENV{'PATH'} = $old_env_path;
1201 my $defaults_extra_file = create_defaults_extra_file;
1205 if ($defaults_extra_file)
1207 $defaults_arg = " --defaults-extra-file='$defaults_extra_file'";
1213 # For users running in environments where taint mode is activated (i.e.
1214 # running mythtv-setup or mythbackend as root), executing a command line
1215 # built with tainted data will fail. Therefore, try to untaint data if it
1216 # meets certain basic requirements.
1217 my $safe_mysqldump = $mysqldump;
1218 $safe_mysqldump = untaint_command($safe_mysqldump);
1219 $safe_mysqldump =~ s/'/'\\''/sg;
1220 $mysql_conf{'db_name'} = untaint_data($mysql_conf{'db_name'});
1221 $mysql_conf{'db_host'} = untaint_data($mysql_conf{'db_host'});
1222 $mysql_conf{'db_port'} = untaint_data($mysql_conf{'db_port'});
1223 $mysql_conf{'db_user'} = untaint_data($mysql_conf{'db_user'});
1224 $backup_conf{'directory'} = untaint_path($backup_conf{'directory'});
1225 # Can't use untaint_path because the filename is not a full path and the
1226 # file doesn't yet exist, anyway
1227 $backup_conf{'filename'} =~ s/'/'\\''/g;
1228 $backup_conf{'filename'} = untaint_data($backup_conf{'filename'});
1229 my $output_file = "$backup_conf{'directory'}/$backup_conf{'filename'}";
1230 $output_file =~ s/'/'\\''/sg;
1231 # Create the args for host, port, and user, shell-escaping values, as
1233 my $safe_db_name = $mysql_conf{'db_name'};
1234 $safe_db_name =~ s/'/'\\''/g;
1236 if ($mysql_conf{'db_host'})
1238 $safe_string = $mysql_conf{'db_host'};
1239 $safe_string =~ s/'/'\\''/g;
1240 $host_arg = " --host='$safe_string'";
1242 if ($mysql_conf{'db_port'} > 0)
1244 $safe_string = $mysql_conf{'db_port'};
1245 $safe_string =~ s/'/'\\''/g;
1246 $port_arg = " --port='$safe_string'";
1248 if ($mysql_conf{'db_user'})
1250 $safe_string = $mysql_conf{'db_user'};
1251 $safe_string =~ s/'/'\\''/g;
1252 $user_arg = " --user='$safe_string'";
1255 # Use redirects to capture stderr (for debug) and send stdout (the backup)
1257 my $command = "'${safe_mysqldump}'${defaults_arg}${host_arg}".
1258 "${port_arg}${user_arg} --add-drop-table --add-locks ".
1259 "--allow-keywords --complete-insert --extended-insert ".
1260 "--lock-tables --no-create-db --quick --add-drop-table ".
1261 "'$safe_db_name' 2>&1 1>'$output_file'";
1262 verbose($verbose_level_debug,
1263 '', 'Executing command:', $command);
1264 my $result = `$command`;
1266 verbose($verbose_level_debug,
1267 '', "$mysqldump exited with status: $exit");
1268 verbose($verbose_level_debug,
1269 "$mysqldump output:", $result) if ($exit);
1276 if (!-e "$backup_conf{'directory'}/$backup_conf{'filename'}")
1278 verbose($verbose_level_debug,
1279 '', 'Unable to find backup file to compress');
1283 verbose($verbose_level_debug,
1284 '', 'Attempting to compress backup file.');
1285 if ($d_compress eq $compress)
1287 # Try to load the IO::Compress::Gzip library if available (but don't
1291 our $has_compress_gzip = 1;
1292 # Though this does nothing, it prevents an invalid "only used
1293 # once" warning that occurs for users without IO::Compress
1296 eval 'use IO::Compress::Gzip qw(gzip $GzipError);';
1299 $has_compress_gzip = 0;
1302 if (!$has_compress_gzip)
1304 verbose($verbose_level_debug,
1305 " - IO::Compress::Gzip is not installed.");
1309 if (-e "$backup_conf{'directory'}/$backup_conf{'filename'}.gz")
1311 verbose($verbose_level_debug,
1312 '', 'A file whose name is the backup filename'.
1313 ' with the \'.gz\' extension already',
1314 'exists. Leaving backup uncompressed.');
1317 verbose($verbose_level_debug,
1318 " - Compressing backup file with IO::Compress::Gzip.");
1320 "$backup_conf{'directory'}/$backup_conf{'filename'}" =>
1321 "$backup_conf{'directory'}/$backup_conf{'filename'}.gz");
1322 if ((defined($result)) &&
1323 (-e "$backup_conf{'directory'}/".
1324 "$backup_conf{'filename'}.gz"))
1326 # For users running in environments where taint mode is
1327 # activated (i.e. running mythtv-setup or mythbackend as
1328 # root), unlinking a file whose path is built with tainted data
1329 # will fail. Therefore, try to untaint the path if it meets
1330 # certain basic requirements.
1331 my $uncompressed_file = $backup_conf{'directory'}."/".
1332 $backup_conf{'filename'};
1333 $uncompressed_file = untaint_path($uncompressed_file);
1334 $uncompressed_file =~ s/'/'\\''/sg;
1335 verbose($verbose_level_debug + 2,
1336 "Unlinking uncompressed file: $uncompressed_file");
1337 unlink "$uncompressed_file";
1338 $backup_conf{'filename'} = "$backup_conf{'filename'}.gz";
1339 verbose($verbose_level_debug,
1340 '', 'Successfully compressed backup to file:',
1341 "$backup_conf{'directory'}/".
1342 "$backup_conf{'filename'}");
1345 verbose($verbose_level_debug,
1346 " Error: $GzipError");
1349 # Try to compress the file with the compress binary.
1350 verbose($verbose_level_debug,
1351 " - Compressing backup file with $compress.");
1352 my $backup_path = "$backup_conf{'directory'}/$backup_conf{'filename'}";
1353 # For users running in environments where taint mode is activated (i.e.
1354 # running mythtv-setup or mythbackend as root), executing a command line
1355 # built with tainted data will fail. Therefore, try to untaint data if it
1356 # meets certain basic requirements.
1357 $compress = untaint_command($compress);
1358 $compress =~ s/'/'\\''/sg;
1359 $backup_path = untaint_path($backup_path);
1360 $backup_path =~ s/'/'\\''/sg;
1361 my $command = "'$compress' '$backup_path' 2>&1";
1362 verbose($verbose_level_debug,
1363 '', 'Executing command:', $command);
1364 my $output = `$command`;
1366 verbose($verbose_level_debug,
1367 '', "$compress exited with status: $exit");
1370 verbose($verbose_level_debug,
1371 "$compress output:", $output);
1375 $backup_conf{'filename'} = "$backup_conf{'filename'}.gz";
1383 if (($rotate < 1) || (!defined($rotateglob)) || (!$rotateglob))
1385 verbose($verbose_level_debug,
1386 '', 'Backup file rotation disabled.');
1389 verbose($verbose_level_debug,
1390 '', 'Rotating backups.');
1391 verbose($verbose_level_debug,
1392 '', 'Searching for files matching pattern:',
1393 "$backup_conf{'directory'}/$rotateglob");
1394 my @files = <$backup_conf{'directory'}/$rotateglob>;
1395 my @sorted_files = sort { lc($a) cmp lc($b) } @files;
1396 my $num_files = @sorted_files;
1397 verbose($verbose_level_debug,
1398 " - Found $num_files matching files.");
1399 $num_files = $num_files - $rotate;
1400 $num_files = 0 if ($num_files < 0);
1401 verbose($verbose_level_debug,
1402 '', "Deleting $num_files and keeping (up to) $rotate backup".
1405 foreach my $file (@sorted_files)
1407 if ($index++ < $num_files)
1410 "$backup_conf{'directory'}/$backup_conf{'filename'}")
1412 # This is the just-created backup. Warn the user that older
1413 # backups with newer schema versions may cause rotation to
1415 verbose($verbose_level_debug,
1416 '', 'WARNING: You seem to have reverted to an'.
1417 ' older database schema version.',
1418 'You should move all backups from newer schema'.
1419 ' versions to another directory or',
1420 'delete them to prevent your new backups from'.
1421 ' being deleted on rotation.', '');
1422 verbose($verbose_level_debug,
1423 " - Keeping backup file: $file");
1428 verbose($verbose_level_debug,
1429 " - Deleting old backup file: $file");
1430 # For users running in environments where taint mode is
1431 # activated (i.e. running mythtv-setup or mythbackend as
1432 # root), unlinking a file whose path is built with tainted data
1433 # will fail. Therefore, try to untaint the path if it meets
1434 # certain basic requirements.
1435 $file = untaint_path($file);
1436 $file =~ s/'/'\\''/sg;
1442 verbose($verbose_level_debug,
1443 " - Keeping backup file: $file");
1449 ##############################################################################
1450 # Main functionality
1451 ##############################################################################
1453 # The first argument after option parsing, if it exists, should be a database
1455 $database_information_file = shift;
1457 configure_environment;
1461 print_configuration;
1464 if ($backup_xmltvids)
1466 $status = do_xmltvid_backup;
1470 $status = do_backup;
1478 $dbh->disconnect if (defined($dbh));