Handle hostnames with upper-case letters
[webmin.git] / backup-config / backup-config-lib.pl
1 =head1 backup-config-lib.pl
2
3 Functions for creating configuration file backups. Some example code :
4
5  foreign_require('backup-config', 'backup-config-lib.pl');
6  @backups = backup_config::list_backups();
7  ($apache_backup) = grep { $_->{'mods'} eq 'apache' } @backups;
8  $apache_backup->{'dest'} = '/tmp/apache.tar.gz';
9  &backup_config::save_backup($apache_backup);
10
11 =cut
12
13 BEGIN { push(@INC, ".."); };
14 use strict;
15 use warnings;
16 use WebminCore;
17 our (%text, $module_config_directory, %config);
18 &init_config();
19 &foreign_require("cron", "cron-lib.pl");
20
21 our $cron_cmd = "$module_config_directory/backup.pl";
22 our $backups_dir = "$module_config_directory/backups";
23 our $manifests_dir = "/tmp/backup-config-manifests";
24
25 =head2 list_backup_modules
26
27 Returns details of all modules that allow backups, each of which is a hash
28 ref in the same format as returned by get_module_info.
29
30 =cut
31 sub list_backup_modules
32 {
33 my ($m, @rv);
34 foreach $m (&get_all_module_infos()) {
35         my $mdir = &module_root_directory($m->{'dir'});
36         if (&check_os_support($m) &&
37             -r "$mdir/backup_config.pl") {
38                 push(@rv, $m);
39                 }
40         }
41 return sort { $a->{'desc'} cmp $b->{'desc'} } @rv;
42 }
43
44 =head2 list_backups
45
46 Returns a list of all configured backups, each of which is a hash ref with
47 at least the following keys :
48
49 =item mods - Space-separate list of modules to include.
50
51 =item dest - Destination file, FTP or SSH server.
52
53 =item configfile - Set to 1 if /etc/webmin/modulename files are included.
54
55 =item nofiles - Set to 1 if server config files (like httpd.conf) are NOT included.
56
57 =item others - A tab-separated list of other files to include.
58
59 =item email -Email address to notify.
60
61 =item emode - Set to 0 to send email only on failure, 1 to always send.
62
63 =item sched - Set to 1 if regular scheduled backups are enabled.
64
65 =item mins,hours,days,months,weekdays - Cron-style specification of backup time.
66
67 =cut
68 sub list_backups
69 {
70 my (@rv, $f);
71 opendir(DIR, $backups_dir) || return ();
72 foreach $f (sort { $a cmp $b } readdir(DIR)) {
73         next if ($f !~ /^(\S+)\.backup$/);
74         push(@rv, &get_backup($1));
75         }
76 closedir(DIR);
77 return @rv;
78 }
79
80 =head2 get_backup(id)
81
82 Given a unique backup ID, returns a hash ref containing its details, in the
83 same format as list_backups.
84
85 =cut
86 sub get_backup
87 {
88 my %backup;
89 &read_file("$backups_dir/$_[0].backup", \%backup) || return undef;
90 $backup{'id'} = $_[0];
91 return \%backup;
92 }
93
94 =head2 save_backup(&backup)
95
96 Given a hash ref containing backup details, saves them to disk. Must be in
97 the same format as returned by list_backups, except for the ID which will be
98 randomly assigned if missing.
99
100 =cut
101 sub save_backup
102 {
103 $_[0]->{'id'} ||= time().$$;
104 mkdir($backups_dir, 0700);
105 &lock_file("$backups_dir/$_[0]->{'id'}.backup");
106 &write_file("$backups_dir/$_[0]->{'id'}.backup", $_[0]);
107 &unlock_file("$backups_dir/$_[0]->{'id'}.backup");
108 }
109
110 =head2 delete_backup(&backup)
111
112 Deletes the backup whose details are in the given hash ref.
113
114 =cut
115 sub delete_backup
116 {
117 &unlink_logged("$backups_dir/$_[0]->{'id'}.backup");
118 }
119
120 =head2 parse_backup_url(string)
121
122 Converts a URL like ftp:// or a filename into its components. These are
123 user, pass, host, page, port (optional)
124
125 =cut
126 sub parse_backup_url
127 {
128 if ($_[0] && $_[0] =~ /^ftp:\/\/([^:]*):([^\@]*)\@([^\/:]+)(:(\d+))?(\/.*)$/) {
129         return (1, $1, $2, $3, $6, $5);
130         }
131 elsif ($_[0] &&
132        $_[0] =~ /^ssh:\/\/([^:]*):([^\@]*)\@([^\/:]+)(:(\d+))?(\/.*)$/) {
133         return (2, $1, $2, $3, $6, $5);
134         }
135 elsif ($_[0] && $_[0] =~ /^upload:(.*)$/) {
136         return (3, undef, undef, undef, $1);
137         }
138 elsif ($_[0] && $_[0] =~ /^download:$/) {
139         return (4, undef, undef, undef, undef);
140         }
141 else {
142         return (0, undef, undef, undef, $_[0]);
143         }
144 }
145
146 =head2 show_backup_destination(name, value, [local-mode])
147
148 Returns HTML for a field for selecting a local or FTP file.
149
150 =cut
151 sub show_backup_destination
152 {
153 my ($mode, $user, $pass, $server, $path, $port) = &parse_backup_url($_[1]);
154 my $rv;
155 $rv .= "<table cellpadding=1 cellspacing=0>";
156
157 # Local file field
158 $rv .= "<tr><td>".&ui_oneradio("$_[0]_mode", 0, undef, $mode == 0)."</td>\n";
159 $rv .= "<td colspan=2>$text{'backup_mode0'} ".
160         &ui_textbox("$_[0]_file", $mode == 0 ? $path : "", 40).
161         " ".&file_chooser_button("$_[0]_file")."</td> </tr>\n";
162
163 # FTP file fields
164 $rv .= "<tr><td>".&ui_oneradio("$_[0]_mode", 1, undef, $mode == 1)."</td>\n";
165 $rv .= "<td>$text{'backup_mode1'} ".
166         &ui_textbox("$_[0]_server", $mode == 1 ? $server : undef, 20).
167         "</td>\n";
168 $rv .= "<td>$text{'backup_path'} ".
169         &ui_textbox("$_[0]_path", $mode == 1 ? $path : undef, 40).
170         "</td> </tr>\n";
171 $rv .= "<tr> <td></td>\n";
172 $rv .= "<td>$text{'backup_login'} ".
173         &ui_textbox("$_[0]_user", $mode == 1 ? $user : undef, 15).
174         "</td>\n";
175 $rv .= "<td>$text{'backup_pass'} ".
176         &ui_password("$_[0]_pass", $mode == 1 ? $pass : undef, 15).
177         "</td> </tr>\n";
178 $rv .= "<tr> <td></td>\n";
179 $rv .= "<td>$text{'backup_port'} ".
180         &ui_opt_textbox("$_[0]_port", $mode == 1 ? $port : undef, 5,
181                         $text{'default'})."</td> </tr>\n";
182
183 # SCP file fields
184 $rv .= "<tr><td>".&ui_oneradio("$_[0]_mode", 2, undef, $mode == 2)."</td>\n";
185 $rv .= "<td>$text{'backup_mode2'} ".
186         &ui_textbox("$_[0]_sserver", $mode == 2 ? $server : undef, 20).
187         "</td>\n";
188 $rv .= "<td>$text{'backup_path'} ".
189         &ui_textbox("$_[0]_spath", $mode == 2 ? $path : undef, 40).
190         "</td> </tr>\n";
191 $rv .= "<tr> <td></td>\n";
192 $rv .= "<td>$text{'backup_login'} ".
193         &ui_textbox("$_[0]_suser", $mode == 2 ? $user : undef, 15).
194         "</td>\n";
195 $rv .= "<td>$text{'backup_pass'} ".
196         &ui_password("$_[0]_spass", $mode == 2 ? $pass : undef, 15).
197         "</td> </tr>\n";
198 $rv .= "<tr> <td></td>\n";
199 $rv .= "<td>$text{'backup_port'} ".
200         &ui_opt_textbox("$_[0]_sport", $mode == 2 ? $port : undef, 5,
201                         $text{'default'})."</td> </tr>\n";
202
203 if ($_[2] == 1) {
204         # Uploaded file field
205         $rv .= "<tr><td>".&ui_oneradio("$_[0]_mode", 3, undef, $mode == 3).
206                 "</td>\n";
207         $rv .= "<td colspan=2>$text{'backup_mode3'} ".
208                 &ui_upload("$_[0]_upload", 40).
209                 "</td> </tr>\n";
210         }
211 elsif ($_[2] == 2) {
212         # Output to browser option
213         $rv .= "<tr><td>".&ui_oneradio("$_[0]_mode", 4, undef, $mode == 4).
214                 "</td>\n";
215         $rv .= "<td colspan=2>$text{'backup_mode4'}</td> </tr>\n";
216         }
217
218 $rv .= "</table>\n";
219 return $rv;
220 }
221
222 =head2 parse_backup_destination(name, &in)
223
224 Returns a backup destination string, or calls error.
225
226 =cut
227 sub parse_backup_destination
228 {
229 my %in = %{$_[1]};
230 my $mode = $in{"$_[0]_mode"};
231 if ($mode == 0) {
232         # Local file
233         $in{"$_[0]_file"} =~ /^\/\S/ || &error($text{'backup_edest'});
234         return $in{"$_[0]_file"};
235         }
236 elsif ($mode == 1) {
237         # FTP server
238         &to_ipaddress($in{"$_[0]_server"}) ||
239           &to_ip6address($in{"$_[0]_server"}) ||
240             &error($text{'backup_eserver1'});
241         $in{"$_[0]_path"} =~ /^\/\S/ || &error($text{'backup_epath'});
242         $in{"$_[0]_user"} =~ /^[^:]*$/ || &error($text{'backup_euser'});
243         $in{"$_[0]_pass"} =~ /^[^\@]*$/ || &error($text{'backup_epass'});
244         $in{"$_[0]_port_def"} || $in{"$_[0]_port"} =~ /^\d+$/ ||
245                 &error($text{'backup_eport'});
246         return "ftp://".$in{"$_[0]_user"}.":".$in{"$_[0]_pass"}."\@".
247                $in{"$_[0]_server"}.
248                ($in{"$_[0]_port_def"} ? "" : ":".$in{"$_[0]_port"}).
249                $in{"$_[0]_path"};
250         }
251 elsif ($mode == 2) {
252         # SSH server
253         &to_ipaddress($in{"$_[0]_sserver"}) ||
254           &to_ip6address($in{"$_[0]_sserver"}) ||
255             &error($text{'backup_eserver2'});
256         $in{"$_[0]_spath"} =~ /^\/\S/ || &error($text{'backup_epath2'});
257         $in{"$_[0]_suser"} =~ /^[^:]*$/ || &error($text{'backup_euser'});
258         $in{"$_[0]_spass"} =~ /^[^\@]*$/ || &error($text{'backup_epass'});
259         $in{"$_[0]_sport_def"} || $in{"$_[0]_sport"} =~ /^\d+$/ ||
260                 &error($text{'backup_esport'});
261         return "ssh://".$in{"$_[0]_suser"}.":".$in{"$_[0]_spass"}."\@".
262                $in{"$_[0]_sserver"}.
263                ($in{"$_[0]_sport_def"} ? "" : ":".$in{"$_[0]_sport"}).
264                $in{"$_[0]_spath"};
265         }
266 elsif ($mode == 3) {
267         # Uploaded file .. save as temp file?
268         $in{"$_[0]_upload"} || &error($text{'backup_eupload'});
269         return "upload:$_[0]_upload";
270         }
271 elsif ($mode == 4) {
272         return "download:";
273         }
274 }
275
276 =head2 execute_backup(&modules, dest, &size, &files, include-webmin, exclude-files, &others)
277
278 Backs up the configuration files for the modules to the selected destination.
279 The backup is simply a tar file of config files. Returns undef on success,
280 or an error message on failure.
281
282 =cut
283 sub execute_backup
284 {
285 # Work out modules we can use
286 my @mods;
287 foreach my $m (@{$_[0]}) {
288         my $mdir = &module_root_directory($m);
289         if ($m && &foreign_check($m) && -r "$mdir/backup_config.pl") {
290                 push(@mods, $m);
291                 }
292         }
293
294 # Work out where to write to
295 my ($mode, $user, $pass, $host, $path, $port) = &parse_backup_url($_[1]);
296 my $file;
297 if ($mode == 0) {
298         $file = &date_subs($path);
299         }
300 else {
301         $file = &transname();
302         }
303
304 # Get module descriptions
305 my $m;
306 my %desc;
307 foreach $m (@mods) {
308         my %minfo = &get_module_info($m);
309         $desc{$m} = $minfo{'desc'};
310         }
311
312 my @files;
313 my %manifestfiles;
314 if (!$_[5]) {
315         # Build list of all files to save from modules
316         foreach my $m (@mods) {
317                 &foreign_require($m, "backup_config.pl");
318                 my @mfiles = &foreign_call($m, "backup_config_files");
319                 push(@files, @mfiles);
320                 push(@{$manifestfiles{$m}}, @mfiles);
321                 }
322         }
323
324 # Add module config files
325 if ($_[4]) {
326         foreach $m (@mods) {
327                 my @cfiles = ( "$config_directory/$m/config" );
328                 push(@files, @cfiles);
329                 push(@{$manifestfiles{$m}}, @cfiles);
330                 }
331         }
332
333 # Add other files
334 foreach my $f (@{$_[6]}) {
335         next if (!$f);
336         if (-d $f) {
337                 # A directory .. recursively expand
338                 foreach my $sf (&expand_directory($f)) {
339                         next if (!$sf);
340                         push(@files, $sf);
341                         push(@{$manifestfiles{"other"}}, $sf);
342                         }
343                 }
344         else {
345                 # Just one file
346                 push(@files, $f);
347                 push(@{$manifestfiles{"other"}}, $f);
348                 }
349         }
350
351 # Save the manifest files
352 &execute_command("rm -rf ".quotemeta($manifests_dir));
353 mkdir($manifests_dir, 0755);
354 my @manifests;
355 foreach $m (@mods, "_others") {
356         next if (!defined($manifestfiles{$m}));
357         my $man = "$manifests_dir/$m";
358         my $fh;
359         &open_tempfile($fh, ">$man");
360         &print_tempfile($fh, map { "$_\n" } @{$manifestfiles{$m}});
361         &close_tempfile($fh);
362         push(@manifests, $man);
363         }
364
365 # Make sure we have something to do
366 @files = grep { $_ && -e $_ } @files;
367 @files || (return $text{'backup_enone'});
368
369 if (!$_[5]) {
370         # Call all module pre functions
371         my $m;
372         foreach $m (@mods) {
373                 if (&foreign_defined($m, "pre_backup")) {
374                         my $err = &foreign_call($m, "pre_backup", \@files);
375                         if ($err) {
376                                 return &text('backup_epre', $desc{$m}, $err);
377                                 }
378                         }
379                 }
380         }
381
382 # Make the tar (possibly .gz) file
383 my $filestemp = &transname();
384 my $fh;
385 &open_tempfile($fh, ">$filestemp");
386 foreach my $f (&unique(@files), @manifests) {
387         my $frel = $f;
388         $frel =~ s/^\///;
389         &print_tempfile($fh, $frel."\n");
390         }
391 &close_tempfile($fh);
392 my $qfile = quotemeta($file);
393 my $out;
394 if (&has_command("gzip")) {
395         &execute_command("cd / ; tar cfT - $filestemp | gzip -c >$qfile",
396                          undef, \$out, \$out);
397         }
398 else {
399         &execute_command("cd / ; tar cfT $qfile $filestemp",
400                          undef, \$out, \$out);
401         }
402 my $ex = $?;
403 &unlink_file($filestemp);
404 if ($ex) {
405         &unlink_file($file) if ($mode != 0);
406         return &text('backup_etar', "<pre>$out</pre>");
407         }
408 my @st = stat($file);
409 ${$_[2]} = $st[7] if ($_[2]);
410 @{$_[3]} = &unique(@files) if ($_[3]);
411 &set_ownership_permissions(undef, undef, 0600, $file);
412
413 if (!$_[5]) {
414         # Call all module post functions
415         foreach $m (@mods) {
416                 if (&foreign_defined($m, "post_backup")) {
417                         &foreign_call($m, "post_backup", \@files);
418                         }
419                 }
420         }
421
422 if ($mode == 1) {
423         # FTP upload to destination
424         my $err;
425         &ftp_upload($host, &date_subs($path), $file, \$err, undef,
426                     $user, $pass, $port);
427         &unlink_file($file);
428         return $err if ($err);
429         }
430 elsif ($mode == 2) {
431         # SCP to destination
432         my $err;
433         &scp_copy($file, "$user\@$host:".&date_subs($path), $pass, \$err,$port);
434         &unlink_file($file);
435         return $err if ($err);
436         }
437
438 return undef;
439 }
440
441 =head2 execute_restore(&mods, source, &files, apply, [show-only])
442
443 Restore configuration files from the specified source for the listed modules.
444 Returns undef on success, or an error message.
445
446 =cut
447 sub execute_restore
448 {
449 my ($mods, $src, $files, $apply, $show) = @_;
450
451 # Fetch file if needed
452 my ($mode, $user, $pass, $host, $path, $port) = &parse_backup_url($src);
453 my $file;
454 if ($mode == 0) {
455         $file = $path;
456         }
457 else {
458         $file = &transname();
459         if ($mode == 2) {
460                 # Download with SCP
461                 my $err;
462                 &scp_copy("$user\@$host:$path", $file, $pass, \$err, $port);
463                 if ($err) {
464                         &unlink_file($file);
465                         return $err;
466                         }
467                 }
468         elsif ($mode == 1) {
469                 # Download with FTP
470                 my $err;
471                 &ftp_download($host, $path, $file, \$err, undef,
472                               $user, $pass, $port);
473                 if ($err) {
474                         &unlink_file($file);
475                         return $err;
476                         }
477                 }
478         }
479
480 # Validate archive
481 open(FILE, $file);
482 my $two;
483 read(FILE, $two, 2);
484 close(FILE);
485 my $qfile = quotemeta($file);
486 my $gzipped = ($two eq "\037\213");
487 my $cmd;
488 if ($gzipped) {
489         # Gzipped
490         &has_command("gunzip") || return $text{'backup_egunzip'};
491         $cmd = "gunzip -c $qfile | tar tf -";
492         }
493 else {
494         $cmd = "tar tf $qfile";
495         }
496 my $out;
497 &execute_command($cmd, undef, \$out, \$out, 0, 1);
498 if ($?) {
499         &unlink_file($file) if ($mode != 0);
500         return &text('backup_euntar', "<pre>$out</pre>");
501         }
502 my @tarfiles = map { "/$_" } split(/\r?\n/, $out);
503 my %tarfiles = map { $_, 1 } @tarfiles;
504
505 # Extract manifests for each module
506 my %hasmod = map { $_, 1 } @{$_[0]};
507 $hasmod{"_others"} = 1;
508 &execute_command("rm -rf ".quotemeta($manifests_dir));
509 my $rel_manifests_dir = $manifests_dir;
510 $rel_manifests_dir =~ s/^\///;
511 if ($gzipped) {
512         &execute_command("cd / ; gunzip -c $qfile | tar xf - $rel_manifests_dir", undef, \$out, \$out);
513         }
514 else {
515         &execute_command("cd / ; tar xf $qfile $rel_manifests_dir", undef, \$out, \$out);
516         }
517 opendir(DIR, $manifests_dir);
518 my $m;
519 my %mfiles;
520 my @files;
521 while($m = readdir(DIR)) {
522         next if ($m eq "." || $m eq ".." || !$hasmod{$m});
523         open(MAN, "$manifests_dir/$m");
524         my @mfiles;
525         while(<MAN>) {
526                 s/\r|\n//g;
527                 if ($tarfiles{$_}) {
528                         push(@mfiles, $_);
529                         }
530                 }
531         close(MAN);
532         $mfiles{$m} = \@mfiles;
533         push(@files, @mfiles);
534         }
535 closedir(DIR);
536 if (!@files) {
537         &unlink_file($file) if ($mode != 0);
538         return $text{'backup_enone2'};
539         }
540
541 # Get descriptions for each module
542 my %desc;
543 foreach my $m (@{$_[0]}) {
544         my %minfo = &get_module_info($m);
545         $desc{$m} = $minfo{'desc'};
546         }
547
548 # Call module pre functions
549 foreach my $m (@{$_[0]}) {
550         my $mdir = &module_root_directory($m);
551         if ($m && &foreign_check($m) && !$_[4] &&
552             -r "$mdir/backup_config.pl") {
553                 &foreign_require($m, "backup_config.pl");
554                 if (&foreign_defined($m, "pre_restore")) {
555                         my $err = &foreign_call($m, "pre_restore", \@files);
556                         if ($err) {
557                                 &unlink_file($file) if ($mode != 0);
558                                 return &text('backup_epre2', $desc{$m}, $err);
559                                 }
560                         }
561                 }
562         }
563
564 # Lock all files being extracted
565 if (!$_[4]) {
566         my $f;
567         foreach $f (@files) {
568                 &lock_file($f);
569                 }
570         }
571
572 # Extract contents (only files specified by manifests)
573 my $flag = $_[4] ? "t" : "x";
574 my $qfiles = join(" ", map { s/^\///; quotemeta($_) } &unique(@files));
575 if ($gzipped) {
576         &execute_command("cd / ; gunzip -c $qfile | tar ${flag}f - $qfiles",
577                          undef, \$out, \$out);
578         }
579 else {
580         &execute_command("cd / ; tar ${flag}f $qfile $qfiles",
581                          undef, \$out, \$out);
582         }
583 my $ex = $?;
584
585 # Un-lock all files being extracted
586 if (!$_[4]) {
587         my $f;
588         foreach $f (@files) {
589                 &unlock_file($f);
590                 }
591         }
592
593 # Check for tar error
594 if ($ex) {
595         &unlink_file($file) if ($mode != 0);
596         return &text('backup_euntar', "<pre>$out</pre>");
597         }
598
599 if ($_[3] && !$_[4]) {
600         # Call all module apply functions
601         foreach $m (@{$_[0]}) {
602                 if (&foreign_defined($m, "post_restore")) {
603                         &foreign_call($m, "post_restore", \@files);
604                         }
605                 }
606         }
607
608 @{$_[2]} = @files;
609 return undef;
610 }
611
612 =head2 scp_copy(source, dest, password, &error, [port])
613
614 Copies a file from some source to a destination. One or the other can be
615 a server, like user@foo:/path/to/bar/
616
617 =cut
618 sub scp_copy
619 {
620 &foreign_require("proc", "proc-lib.pl");
621 my $cmd = "scp -r ".($_[4] ? "-P $_[4] " : "").
622           quotemeta($_[0])." ".quotemeta($_[1]);
623 my ($fh, $fpid) = &proc::pty_process_exec($cmd);
624 my $out;
625 while(1) {
626         my $rv = &wait_for($fh, "password:", "yes\\/no", ".*\n");
627         $out .= $wait_for_input;
628         if ($rv == 0) {
629                 syswrite($fh, "$_[2]\n");
630                 }
631         elsif ($rv == 1) {
632                 syswrite($fh, "yes\n");
633                 }
634         elsif ($rv < 0) {
635                 last;
636                 }
637         }
638 close($fh);
639 my $got = waitpid($fpid, 0);
640 if ($? || $out =~ /permission\s+denied/i) {
641         ${$_[3]} = "scp failed : <pre>$out</pre>";
642         }
643 }
644
645 =head2 find_cron_job(&backup)
646
647 MISSING DOCUMENTATION
648
649 =cut
650 sub find_cron_job
651 {
652 my @jobs = &cron::list_cron_jobs();
653 my ($job) = grep { $_->{'user'} eq 'root' &&
654                    $_->{'command'} eq "$cron_cmd $_[0]->{'id'}" } @jobs;
655 return $job;
656 }
657
658 =head2 nice_dest(destination, [subdates])
659
660 Returns a backup filename in a human-readable format, with dates substituted.
661
662 =cut
663 sub nice_dest
664 {
665 my ($url, $subdates) = @_;
666 my ($mode, $user, $pass, $server, $path, $port) = &parse_backup_url($url);
667 if ($subdates) {
668         $path = &date_subs($path);
669         }
670 if ($mode == 0) {
671         return "<tt>$path</tt>";
672         }
673 elsif ($mode == 1) {
674         return &text($port ? 'nice_ftpp' : 'nice_ftp',
675                      "<tt>$server</tt>", "<tt>$path</tt>",
676                      $port ? "<tt>$port</tt>" : "");
677         }
678 elsif ($mode == 2) {
679         return &text($port ? 'nice_sshp' : 'nice_ssh',
680                      "<tt>$server</tt>", "<tt>$path</tt>",
681                      $port ? "<tt>$port</tt>" : "");
682         }
683 elsif ($mode == 3) {
684         return $text{'nice_upload'};
685         }
686 elsif ($mode == 4) {
687         return $text{'nice_download'};
688         }
689 }
690
691 =head2 date_subs(string)
692
693 Given a string with strftime-style format characters in it like %Y and %S, 
694 replaces them with the correct values for the current date and time.
695
696 =cut
697 sub date_subs
698 {
699 if ($config{'date_subs'}) {
700         eval "use POSIX";
701         eval "use posix" if ($@);
702         my @tm = localtime(time());
703         return strftime($_[0], @tm);
704         }
705 else {
706         return $_[0];
707         }
708 }
709
710 =head2 show_backup_what(name, webmin?, nofiles?, others)
711
712 Returns HTML for selecting what gets included in a backup.
713
714 =cut
715 sub show_backup_what
716 {
717 my ($name, $webmin, $nofiles, $others) = @_;
718 $others ||= "";
719 return &ui_checkbox($name."_webmin", 1, $text{'edit_webmin'}, $webmin)."\n".
720        &ui_checkbox($name."_nofiles", 1, $text{'edit_nofiles'}, !$nofiles)."\n".
721        &ui_checkbox($name."_other", 1, $text{'edit_other'}, $others)."<br>".
722        &ui_textarea($name."_files", join("\n", split(/\t+/, $others)), 3, 50);
723 }
724
725 =head2 parse_backup_what(name, &in)
726
727 Returns the webmin and nofiles flags, and a tab-separated list of other
728 files to include.
729
730 =cut
731 sub parse_backup_what
732 {
733 my ($name, $in) = @_;
734 my $webmin = $in->{$name."_webmin"};
735 my $nofiles = !$in->{$name."_nofiles"};
736 $in->{$name."_files"} =~ s/\r//g;
737 my $others = $in->{$name."_other"} ?
738         join("\t", split(/\n+/, $in->{$name."_files"})) : undef;
739 $webmin || !$nofiles || $others || &error($text{'save_ewebmin'});
740 return ($webmin, $nofiles, $others);
741 }
742
743 =head2 expand_directory(directory)
744
745 Given a directory, return a list of full paths to all files within it.
746
747 =cut
748 sub expand_directory
749 {
750 my ($dir) = @_;
751 my @rv;
752 opendir(EXPAND, $dir);
753 my @sf = readdir(EXPAND);
754 closedir(EXPAND);
755 foreach my $sf (@sf) {
756         next if ($sf eq "." || $sf eq "..");
757         my $path = "$dir/$sf";
758         if (-l $path || !-d $path) {
759                 push(@rv, $path);
760                 }
761         elsif (-d $path) {
762                 push(@rv, &expand_directory($path));
763                 }
764         }
765 return @rv;
766 }
767
768 1;
769