3 # Unix SMB/CIFS implementation.
4 # Test suite for the tar backup mode of smbclient.
5 # Copyright (C) Aurélien Aptel 2013
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 C<test_smbclient_tarmode.pl> - Test for smbclient tar backup feature
29 no warnings 'experimental::smartmatch';
33 use Digest::MD5 qw/md5_hex/;
34 use File::Path qw/make_path remove_tree/;
41 sub d {print Dumper @_;}
44 # 'our' to make them available in the File package
47 our $HOST = 'localhost';
49 our $SHARE = 'public';
50 our $DIR = 'tar_test_dir';
51 our $LOCALPATH = '/media/data/smb-test';
52 our $TMP = File::Temp->newdir();
53 our $BIN = 'smbclient';
56 my $SELECTED_TEST = '';
69 # ['test helper', \&test_helper],
70 ['create, normal files (no attributes)', \&test_creation_normal, 'normal'],
71 ['create, normal nested files (no attributes)', \&test_creation_normal, 'nested'],
72 ['create, normal files (interactive)', \&test_creation_normal, 'inter'],
73 ['create, large file', \&test_creation_large_file],
74 ['create, long path', \&test_creation_long_path],
75 ['create, incremental with -g', \&test_creation_incremental, '-g'],
76 ['create, incremental with tarmode', \&test_creation_incremental, 'tarmode inc'],
77 ['create, reset archived files with -a', \&test_creation_reset, '-a'],
78 ['create, reset archived files with tarmode', \&test_creation_reset, 'tarmode reset'],
79 ['create, files newer than a file', \&test_creation_newer],
80 ['create, combination of tarmode filter', \&test_creation_attr],
81 ['create, explicit include', \&test_creation_include],
82 # ['create, explicit exclude', \&test_creation_exclude],
83 ['create, include w/ filelist (F)', \&test_creation_list],
84 ['create, wildcard simple', \&test_creation_wildcard_simple],
85 ['create, regex', \&test_creation_regex],
86 ['create, multiple backup in session', \&test_creation_multiple],
87 ['extract, normal files', \&test_extraction_normal],
88 ['extract, explicit include', \&test_extraction_include],
89 ['extract, explicit exclude', \&test_extraction_exclude],
90 ['extract, include w/ filelist (F)', \&test_extraction_list],
91 ['extract, regex', \&test_extraction_regex],
96 test_smbclient_tarmode.pl [options] -- [smbclient options]
99 -h, --help brief help message
100 --man full documentation
109 sub-path to use on the share
111 -l, --local-path PATH
112 path to the root of the samba share on the machine.
115 path to the smbclient binary to use
124 only run certain tests (accept list and intervals of numbers)
130 print command and their output
134 GetOptions('u|user=s' => \$USER,
135 'p|password=s' => \$PW,
136 'h|host=s' => \$HOST,
138 's|share=s' => \$SHARE,
140 'l|local-path=s' => \$LOCALPATH,
143 'test=s' => \$SELECTED_TEST,
144 'list' => \$LIST_TEST,
147 'subunit' => \$SUBUNIT,
149 'v|verbose' => \$VERBOSE,
151 'man' => \$MAN) or pod2usage(2);
153 pod2usage(0) if $HELP;
154 pod2usage(-exitval => 0, -verbose => 2) if $MAN;
155 list_test(), exit 0 if $LIST_TEST;
158 die "Need both user and password when one is provided\n";
160 elsif ($USER and $PW) {
161 push @SMBARGS, '-U'.$USER.'%'.$PW;
168 push @SMBARGS, '-I', $IP;
171 # remaining arguments are passed to smbclient
172 push @SMBARGS, @ARGV;
174 # path to store the downloaded tarball
175 my $TAR = "$TMP/tarmode.tar";
181 # remove all final slashes from input paths
182 $LOCALPATH =~ s{[/\\]+$}{}g;
183 $SHARE =~ s{[/\\]+$}{}g;
184 $HOST =~ s{[/\\]+$}{}g;
185 $DIR =~ s{[/\\]+$}{}g;
187 if (!-d $LOCALPATH) {
188 die "Local path '$LOCALPATH' is not a directory.\n";
192 # clean the whole root first
193 remove_tree($LOCALPATH, { keep_root => 1 });
204 my @selection = parse_test_string($SELECTED_TEST);
206 if ($SELECTED_TEST eq '') {
208 } elsif (@selection > 0) {
209 run_test(@selection);
211 die "Test selection '$SELECTED_TEST' is invalid\n";
214 #################################
218 =head2 Defining a test
222 =item * Create a function C<test_yourtest>
224 =item * Use the File module, documented below
226 =item * Use C<smb_tar>, C<smb_client>, C<check_tar> or C<check_remote>
228 =item * Return number of error
230 =item * Add function to C<@TESTS>
234 The function must be placed in the C<@TESTS> list along with a short
235 description and optional arguments.
239 sub test_creation_newer {
243 # create oldest file at - DT
244 my $oldest = File->new_remote('oldest');
245 $oldest->set_time(time - $dt);
248 my $limit = File->new_local("$TMP/limit");
250 # create newA file at + DT
251 my $newA = File->new_remote('newA');
252 $newA->set_time(time + $dt);
254 # create newB file at + DT
255 my $newB = File->new_remote('newB');
256 $newB->set_time(time + $dt);
258 # get files newer than limit_file
259 push @files, $newA, $newB;
261 smb_tar('', '-TcN', $limit->localpath, $TAR, $DIR);
262 return check_tar($TAR, \@files);
265 sub test_creation_attr {
266 my @attr = qw/r h s a/;
272 my $f = File->new_remote("file-n.txt");
275 # combinations of attributes
276 for my $n (1..@attr) {
277 for (combine(\@attr, $n)) {
279 my $fn = "file-" . join('+', @t) . ".txt";
280 my $f = File->new_remote($fn);
286 @inc = grep { !$_->attr('s') } @all;
287 smb_tar('tarmode nosystem', '-Tc', $TAR, $DIR);
288 $err += check_tar($TAR, \@inc);
290 @inc = grep { !$_->attr('h') } @all;
291 smb_tar('tarmode nohidden', '-Tc', $TAR, $DIR);
292 $err += check_tar($TAR, \@inc);
294 @inc = grep { !$_->attr_any('h', 's') } @all;
295 smb_tar('tarmode nohidden nosystem', '-Tc', $TAR, $DIR);
296 $err += check_tar($TAR, \@inc);
298 @inc = grep { $_->attr('a') && !$_->attr_any('h', 's') } @all;
299 smb_tar('tarmode inc nohidden nosystem', '-Tc', $TAR, $DIR);
300 $err += check_tar($TAR, \@inc);
305 sub test_creation_reset {
311 my $f = File->new_remote("file-$_");
316 if ($mode =~ /reset/) {
317 smb_tar('tarmode full reset', '-Tc', $TAR, $DIR);
319 smb_tar('', '-Tca', $TAR, $DIR);
322 my $err = check_tar($TAR, \@files);
323 return $err if ($err > 0);
325 for my $f (File::list($DIR)) {
327 printf " ! %s %s\n", $f->attr_str, $f->remotepath;
334 sub test_creation_large_file {
335 my $size = int(15e6); # 15MB
336 my $f = File->new_remote("fat.jpg", 0, $size);
338 smb_tar('', '-Tc', $TAR, $DIR);
339 return check_tar($TAR, [$f]);
342 sub test_creation_long_path {
346 for(qw( foo/a bar/b )) {
347 push @all, File->new_remote("$d/$_");
350 smb_tar('', '-Tc', $TAR, $DIR);
351 return check_tar($TAR, \@all);
354 sub test_creation_normal {
357 my $prefix = ($mode =~ /nest/) ? "/foo/bar/bar/" : '';
361 my $f = File->new_remote($prefix."file-$_");
365 if ($mode =~ /inter/) {
366 smb_tar("tar c $TAR $DIR", '');
368 smb_tar('tarmode full', '-Tc', $TAR, $DIR);
370 return check_tar($TAR, \@files);
373 sub test_creation_incremental {
379 my $f = File->new_remote("file-$_");
381 # set archive bit on ~half of them
387 $f->set_attr((qw/n r s h/)[$_ % 4]);
391 if ($mode =~ /inc/) {
392 smb_tar('tarmode inc', '-Tc', $TAR, $DIR);
394 smb_tar('', '-Tcg', $TAR, $DIR);
396 return check_tar($TAR, \@files);
400 sub test_extraction_normal {
404 my $f = File->new_remote("file-$_");
409 smb_tar('', '-Tc', $TAR, $DIR);
410 my $err = check_tar($TAR, \@files);
411 return $err if $err > 0;
415 smb_tar('', '-Tx', $TAR);
416 check_remote($DIR, \@files);
419 sub test_extraction_include {
423 for (qw(file_inc inc/b inc/c inc/dir/foo dir_ex/d zob)) {
424 my $f = File->new_remote($_);
426 push @inc_files, $f if /inc/;
430 smb_tar('', '-Tc', $TAR, $DIR);
431 my $err = check_tar($TAR, \@all_files);
432 return $err if $err > 0;
436 smb_tar('', '-TxI', $TAR, "$DIR/file_inc", "$DIR/inc");
437 check_remote($DIR, \@inc_files);
440 sub test_extraction_exclude {
444 for (qw(file_exc exc/b exc/c exc/dir/foo dir_ex/d zob)) {
445 my $f = File->new_remote($_);
447 push @inc_files, $f if !/exc/;
451 smb_tar('', '-Tc', $TAR, $DIR);
452 my $err = check_tar($TAR, \@all_files);
453 return $err if $err > 0;
457 smb_tar('', '-TxX', $TAR, "$DIR/file_exc", "$DIR/exc");
458 check_remote($DIR, \@inc_files);
462 sub test_creation_include {
465 for (qw(file_inc inc/b inc/c inc/dir/foo dir_ex/d zob)) {
466 my $f = File->new_remote($_);
467 push @files, $f if /inc/;
470 smb_tar('', '-TcI', $TAR, "$DIR/file_inc", "$DIR/inc");
471 return check_tar($TAR, \@files);
474 sub test_creation_exclude {
477 for (qw(file_ex ex/b ex/c ex/dir/foo foo/bar zob)) {
478 my $f = File->new_remote($_);
479 push @files, $f if !/ex/;
482 smb_tar('', '-TcX', $TAR, "$DIR/file_ex", "$DIR/ex");
483 return check_tar($TAR, \@files);
486 sub test_creation_list {
489 for (qw(file_inc inc/b inc/c inc/dir/foo foo/bar zob)) {
490 my $f = File->new_remote($_);
491 push @inc_files, $f if /inc/;
494 my $flist = File->new_local("$TMP/list", file_list(@inc_files));
495 smb_tar('', '-TcF', $TAR, $flist->localpath);
496 return check_tar($TAR, \@inc_files);
499 sub test_creation_regex {
500 my @exts = qw(jpg exe);
501 my @dirs = ('', "$DIR/");
502 my @all = make_env(\@exts, \@dirs);
510 @inc = grep { $_->remotepath !~ m{exe$} } @all;
511 smb_tar('', '-TcrX', $TAR, '*.exe');
512 $err += check_tar($TAR, \@inc);
514 # if the pattern is a path, it doesn't skip anything
515 smb_tar('', '-TcrX', $TAR, "$DIR/*.exe");
516 $err += check_tar($TAR, \@all);
517 smb_tar('', '-TcrX', $TAR, "$DIR/*");
518 $err += check_tar($TAR, \@all);
519 smb_tar('', '-TcrX', $TAR, "$DIR");
520 $err += check_tar($TAR, \@all);
522 # no paths => include everything
523 smb_tar('', '-TcrX', $TAR);
524 $err += check_tar($TAR, \@all);
528 smb_tar('', '-TcrX', $TAR, "*.*");
529 $err += check_tar($TAR, []);
530 smb_tar('', '-TcrX', $TAR, "*");
531 $err += check_tar($TAR, []);
535 # no paths => include everything
536 smb_tar('', '-Tcr', $TAR);
537 $err += check_tar($TAR, \@all);
540 smb_tar('', '-Tcr', $TAR, '*');
541 $err += check_tar($TAR, \@all);
543 # include only .exe at root
544 @inc = grep { $_->remotepath =~ m{^[^/]+exe$}} @all;
545 smb_tar('', '-Tcr', $TAR, '*.exe');
546 $err += check_tar($TAR, \@inc);
548 # smb_tar('', '-Tcr', $TAR, "$DIR/*");
549 ## in old version (bug?)
550 # $err += check_tar($TAR, []);
552 # @inc = grep { $_->remotepath =~ /^$DIR/ } @all;
553 # $err += check_tar($TAR, \@inc);
558 sub test_creation_wildcard_simple {
559 my @exts = qw(jpg exe);
560 my @dirs = ('', "$DIR/");
561 my @all = make_env(\@exts, \@dirs);
566 @inc = grep { $_->remotepath =~ m{^[^/]+exe$} } @all;
567 smb_tar('', '-Tc', $TAR, "*.exe");
568 $err += check_tar($TAR, \@inc);
570 @inc = grep { $_->remotepath =~ m{$DIR/.+exe$} } @all;
571 smb_tar('', '-Tc', $TAR, "$DIR/*.exe");
572 $err += check_tar($TAR, \@inc);
578 # helper to test tests
580 my @exts = qw(txt jpg exe);
581 my @dirs = ('', "$DIR/", "$DIR/dir/");
582 my @all = make_env(\@exts, \@dirs);
587 smb_tar('', '-Tc', $TAR);
588 return 1 if check_tar($TAR, \@all);
591 my @exc = grep { $_->remotepath =~ m!/dir/.+exe!} @all;
592 @inc = grep { $_->remotepath !~ m!/dir/.+exe!} @all;
593 smb_tar('', '-TxXr', $TAR, "/$DIR/dir/*.exe");
594 $err += check_remote('/', \@all); # BUG: should be \@inc
600 sub test_creation_multiple {
601 my @exts = qw(jpg exe);
602 my @dirs = ('', "$DIR/");
603 my @all = make_env(\@exts, \@dirs);
608 my ($tarA, $tarB) = ("$TMP/a.tar", "$TMP/b.tar");
609 my @incA = grep { $_->remotepath =~ m{^[^/]+exe$} } @all;
610 my @incB = grep { $_->remotepath =~ m{^[^/]+jpg$} } @all;
612 my $flistA = File->new_local("$TMP/listA", file_list(@incA))->localpath;
613 my $flistB = File->new_local("$TMP/listB", file_list(@incB))->localpath;
615 smb_tar("tar cF $tarA $flistA ; tar cF $tarB $flistB ; quit");
616 $err += check_tar($tarA, \@incA);
617 $err += check_tar($tarB, \@incB);
622 sub test_extraction_regex {
623 my @exts = qw(txt jpg exe);
624 my @dirs = ('', "$DIR/", "$DIR/dir/");
625 my @all = make_env(\@exts, \@dirs);
630 smb_tar('', '-Tc', $TAR);
631 return 1 if check_tar($TAR, \@all);
636 # only include file at root
637 @inc = grep { $_->remotepath =~ m!exe!} @all;
638 smb_tar('', '-Txr', $TAR, "*.exe");
639 $err += check_remote('/', \@inc);
642 @inc = grep { $_->remotepath =~ m!/dir/.+exe!} @all;
643 smb_tar('', '-Txr', $TAR, "/$DIR/dir/*.exe");
644 $err += check_remote('/', []); # BUG: should be \@inc
649 # exclude file not directly at root
650 @inc = grep { $_->remotepath =~ m!^[^/]+$!} @all;
651 @exc = grep { $_->remotepath !~ m!^[^/]+$!} @all;
652 smb_tar('', '-TxrX', $TAR, map {$_->remotepath} @exc);
653 $err += check_remote('/', \@all); # BUG: should be @inc...
656 # exclude only $DIR/dir/*exe
657 @exc = grep { $_->remotepath =~ m!/dir/.+exe!} @all;
658 @inc = grep { $_->remotepath !~ m!/dir/.+exe!} @all;
659 smb_tar('', '-TxXr', $TAR, "/$DIR/dir/*.exe");
660 $err += check_remote('/', \@all); # BUG: should be \@inc
666 sub test_extraction_wildcard {
667 my @exts = qw(txt jpg exe);
668 my @dirs = ('', "$DIR/", "$DIR/dir/");
672 for my $dir (@dirs) {
677 for my $dir (@dirs) {
679 my $fn = $dir . "file$nb." . $_;
680 my $f = File->new_remote($fn, 'ABSPATH');
681 $f->delete_on_destruction(1);
690 my $fn = $dir."file$nb.".$ext;
691 my $pattern = $dir.'*.'.$ext;
696 $flist = File->new_local("$TMP/list", "$pattern\n");
699 my $re = '^'.$dir.'.*file';
700 @inc = grep { $dir eq '' or $_->remotepath =~ m{$re} } @all;
701 smb_tar('', '-Tc', $TAR, $dir);
702 $err += check_tar($TAR, \@inc);
705 my $re2 = '^'.$dir.'file.+exe';
706 @inc = grep { $_->remotepath =~ /$re2/ } @all;
707 smb_tar('', '-TxrF', $TAR, $flist->localpath);
708 $err += check_remote($dir, \@inc);
716 sub test_extraction_list {
720 for (qw(file_inc inc/b inc/c inc/dir/foo foo/bar zob)) {
721 my $f = File->new_remote($_);
723 push @inc_files, $f if /inc/;
727 smb_tar('', '-Tc', $TAR, $DIR);
728 my $err = check_tar($TAR, \@all_files);
729 return $err if $err > 0;
733 my $flist = File->new_local("$TMP/list", file_list(@inc_files));
734 smb_tar('', '-TxF', $TAR, $flist->localpath);
735 return check_remote($DIR, \@inc_files);
738 #################################
742 =head2 Useful functions
744 Here are a list of useful functions and helpers to define tests.
748 # list test number and description
752 my ($desc, $f, @args) = @$_;
753 printf "%2d.\t%s\n", $i++, $desc;
759 run_test_subunit(@_);
765 sub run_test_normal {
767 my ($desc, $f, @args) = @$_;
776 open my $saveout, ">&STDOUT";
777 open STDOUT, '>', File::Spec->devnull();
779 open STDOUT, ">&", $saveout;
787 sub run_test_subunit {
789 my ($desc, $f, @args) = @$_;
796 # capture output in $buf
798 open my $handle, '>', \$buf;
801 # check for die() calls
814 # result string is output + eventual exception message
817 printf "%s: %s [\n%s]\n", ($err > 0 ? "failure" : "success"), $desc, $str;
822 sub parse_test_string {
830 for (split /,/, $s) {
835 push @tests, $TESTS[$_];
837 when (/^(\d+)-(\d+)$/) {
838 my ($min, $max) = sort ($1, $2);
839 if ($max >= @TESTS) {
844 push @tests, $TESTS[$_];
858 printf " RES: %s%d ERR%s\n", color('bold red'), $err, color 'reset';
860 printf " RES: %sOK%s\n", color('bold green'), color 'reset';
865 my ($exts, $dirs) = @_;
868 for my $dir (@$dirs) {
870 my $fn = $dir . "file$nb." . $_;
871 my $f = File->new_remote($fn, 'ABSPATH');
872 $f->delete_on_destruction(1);
881 =head3 C<combine ( \@set, $n )>
883 =head3 C<combine ( ['a', 'b', 'c'], 2 )>
885 Return a list of all possible I<n>-uplet (or combination of C<$n> element) of C<@set>.
890 die "Insufficient list members" if $n > @$list;
892 return map [$_], @$list if $n <= 1;
896 for (my $i = 0; $i+$n <= @$list; $i++) {
897 my $val = $list->[$i];
898 my @rest = @$list[$i+1..$#$list];
899 push @comb, [$val, @$_] for combine(\@rest, $n-1);
906 =head3 C<reset_remote( )>
908 Remove all files in the server C<$DIR> (not root)
912 # remove_tree($LOCALPATH . '/'. $DIR);
913 # make_path($LOCALPATH . '/'. $DIR);
914 remove_tree($LOCALPATH, {keep_root => 1});
915 make_path($LOCALPATH, {keep_root => 1});
918 =head3 C<reset_tmp( )>
920 Remove all files in the temp directory C<$TMP>
924 remove_tree($TMP, { keep_root => 1 });
928 =head3 C<reset_env( )>
930 Remove both temp and remote (C<$DIR>) files
938 =head3 C<file_list ( @files )>
940 Make a multiline string of all the files remote path, one path per line.
942 C<@files> must be a list of C<File> instance.
949 $s .= $_->remotepath."\n";
954 =head3 C<check_remote( $remotepath, \@files )>
956 Check if C<$remotepath> has B<exactly> all the C<@files>.
958 Print a summary on STDOUT.
960 C<@files> must be a list of C<File> instance.
964 my ($subpath, $files) = @_;
965 my (%done, %expected);
966 my (@less, @more, @diff);
969 $expected{$_->remotepath} = $_;
970 $done{$_->remotepath} = 0;
974 File::walk(sub { $remote{$_->remotepath} = $_ }, File::tree($subpath));
976 for my $rfile (sort keys %remote) {
978 # files that shouldn't be there
979 if (!exists $expected{$rfile}) {
985 # same file multiple times
986 if ($done{$rfile} > 0) {
989 printf " +%3d %s\n", $done{$rfile}, $rfile;
996 my $rmd5 = $remote{$rfile}->md5;
997 if ($expected{$rfile}->md5 ne $rmd5) {
998 say " ! $rfile ($rmd5)";
1006 # file that should have been in tar
1007 @less = grep { $done{$_} == 0 } sort keys %done;
1013 printf("\t%d files, +%d, -%d, !%d\n",
1018 return (@more + @less + @diff); # nb of errors
1021 =head3 C<check_tar( $path_to_tar, \@files )>
1023 Check if the archive C<$path_to_tar> has B<exactly> all the C<@files>.
1025 Print a summary on C<STDOUT>;
1027 C<@files> must be a list of C<File> instance.
1031 my ($tar, $files) = @_;
1033 my (@less, @more, @diff);
1039 say "no tar file $tar";
1044 $h{$_->tarpath} = $_;
1045 $done{$_->tarpath} = 0;
1049 my $i = Archive::Tar->iter($tar, 1, {md5 => 1});
1050 while (my $f = $i->()) {
1051 if ($f->has_content) {
1053 my $p = $f->full_path =~ s{^\./+}{}r;
1055 # file that shouldn't be there
1056 if (!exists $done{$p}) {
1062 # same file multiple times
1063 if ($done{$p} > 0) {
1066 printf " +%3d %s\n", $done{$p}, $p;
1076 $md5 = md5_hex($md5);
1079 if ($md5 ne $h{$p}->md5) {
1089 # file that should have been in tar
1090 @less = grep { $done{$_} == 0 } keys %done;
1096 printf("\t%d files, +%d, -%d, !%d\n",
1101 return (@more + @less + @diff); # nb of errors
1104 =head3 C<smb_client ( @args )>
1106 Run smbclient with C<@args> passed as argument and return output.
1108 Each element of C<@args> becomes one escaped argument of smbclient.
1110 Host, share, user, password and the additionnal arguments provided on
1111 the command-line are already inserted.
1113 The output contains both the C<STDOUT> and C<STDERR>.
1115 Die if smbclient crashes or exits with an error code.
1121 my $fullpath = "//$HOST/$SHARE";
1122 my $cmd = sprintf("%s %s %s",
1124 quotemeta($fullpath),
1125 join(' ', map {quotemeta} (@SMBARGS, @args)));
1128 say color('bold yellow'),$cmd =~ s{\\([./+-])}{$1}gr,color('reset');
1131 my $out = `$cmd 2>&1`;
1134 # handle abnormal exit
1136 $errstr = "failed to execute $cmd: $!\n";
1138 elsif ($err & 127) {
1139 $errstr = sprintf "child died with signal %d (%s)\n", ($err & 127), $cmd;
1142 $errstr = sprintf "child exited with value %d (%s)\n", ($err >> 8), $cmd;
1150 die "ERROR: $errstr";
1156 return smb_client('-c', join(' ', @_));
1159 =head3 C<smb_tar( $cmd, @args )>
1161 =head3 C<smb_tar( 'tarmode inc', '-Tc', $TAR, $DIR )>
1163 Run C<$cmd> command and use C<@args> as argument and return output.
1165 Wrapper around C<smb_client> for tar calls.
1169 my ($cmd, @rest) = @_;
1170 printf " CMD: %s\n ARG: %s\n", $cmd, join(' ', @rest);
1171 smb_client((length($cmd) ? ('-c', $cmd) : ()), @rest);
1174 =head3 C<random( $min, $max )>
1176 Return integer in C<[ $min ; $max ]>
1180 my ($min, $max) = @_;
1181 ($min, $max) = ($max, $min) if ($min > $max);
1182 $min + int(rand($max - $min));
1185 #################################
1189 =head2 The File module
1191 All the test should use the C<File> class. It has nice functions and
1192 methods to deal with paths, to create random files, to list the
1193 content of the server, to change attributes, etc.
1195 There are 2 kinds of C<File>: remote and local.
1199 =item * Remote files are accessible on the server.
1201 =item * Local files are not.
1205 Thus, some methods only works on remote files. If they do not make
1206 sense for local ones, they always return undef.
1210 use File::Path qw/make_path remove_tree/;
1211 use Digest::MD5 qw/md5_hex/;
1212 use Scalar::Util 'blessed';
1216 =head4 C<< File->new_remote($path [, $abs, $size]) >>
1218 Creates a file accessible on the server at C<$DIR/$path> ie. not at the
1219 root of the share and write C<$size> random bytes.
1221 If no size is provided, a random size is chosen.
1223 If you want to remove the automatic prefix C<$DIR>, set C<$abs> to 1.
1225 The file is created without any DOS attributes.
1227 If C<$path> contains non-existent directories, they are automatically
1232 my ($class, $path, $abs, $size) = @_;
1233 my ($file, $dir) = fileparse($path);
1235 $dir = '' if $dir eq './';
1239 $loc = cleanpath($main::LOCALPATH.'/'.$dir);
1241 $dir = cleanpath($main::DIR.'/'.$dir);
1242 $loc = cleanpath($main::LOCALPATH.'/'.$dir);
1248 'attr' => {qw/r 0 s 0 h 0 a 0 d 0 n 0/},
1251 'md5' => create_file($loc.'/'.$file, $size),
1260 =head4 C<< File->new_local($abs_path [, $data]) >>
1262 Creates a file at C<$abs_path> with $data in it on the system.
1263 If $data is not provided, fill it with random bytes.
1267 my ($class, $path, $data) = @_;
1268 my ($file, $dir) = fileparse($path);
1274 if (defined $data) {
1275 open my $f, '>', $path or die "can't write in $path: $!";
1278 $md5 = md5_hex($data);
1280 $md5 = create_file($path);
1284 'attr' => {qw/r 0 s 0 h 0 a 0 d 0 n 0/},
1291 bless $self, $class;
1296 =head4 C<< $f->localpath >>
1298 Return path on the system eg. F</opt/samba/share/test_tar_mode/file>
1304 return cleanpath($main::LOCALPATH.'/'.$s->remotepath);
1307 return cleanpath($s->{dir}.'/'.$s->{name});
1311 =head4 C<< $f->remotepath >>
1313 Return path on the server share.
1315 Return C<undef> if the file is local.
1320 return undef if !$s->{remote};
1321 cleanpath(($s->{dir}.'/'.$s->{name}) =~ s{^/}{}r);
1325 =head4 C<< $f->remotedir >>
1327 Return the directory path of the file on the server.
1329 Like C<< $f->remotepath >> but without the final file name.
1334 return undef if !$s->{remote};
1335 cleanpath($s->{dir});
1338 =head4 C<< $f->tarpath >>
1340 Return path as it would appear in a tar archive.
1342 Like C<< $f->remotepath >> but prefixed with F<./>
1347 return undef if !$s->{remote};
1348 $s->remotepath =~ s{^\./+}{}r;
1351 =head4 C<< $f->delete_on_destruction( 0 ) >>
1353 =head4 C<< $f->delete_on_destruction( 1 ) >>
1355 By default, a C<File> is not deleted on the filesystem when it is not
1356 referenced anymore in Perl memory.
1358 When set to 1, the destructor unlink the file if it is not already removed.
1359 If the C<File> created directories when constructed, it does not remove them.
1362 sub delete_on_destruction {
1363 my ($s, $delete) = @_;
1364 $s->{delete_on_destruction} = $delete;
1367 =head4 C<< $f->set_attr( ) >>
1369 =head4 C<< $f->set_attr( 'a' ) >>
1371 =head4 C<< $f->set_attr( 'a', 'r', 's', 'h' ) >>
1373 Remove all DOS attributes and only set the one provided.
1377 my ($s, @flags) = @_;
1378 return undef if !$s->{remote};
1380 $s->{attr} = {qw/r 0 s 0 h 0 a 0 d 0 n 0/};
1383 $s->{attr}{lc($_)} = 1;
1386 my $file = $s->{name};
1388 if ($s->remotedir) {
1389 push @args, '-D', $s->remotedir;
1391 main::smb_client(@args, '-c', qq{setmode "$file" -rsha});
1392 if (@flags && $flags[0] !~ /n/i) {
1393 main::smb_client(@args, '-c', qq{setmode "$file" +}.join('', @flags));
1397 =head4 C<< $f->attr_any( 'a' ) >>
1399 =head4 C<< $f->attr_any( 'a', 's', ... ) >>
1401 Return 1 if the file has any of the DOS attributes provided.
1405 my ($s, @flags) = @_;
1407 return 1 if $s->{attr}{$_};
1413 =head4 C<< $f->attr( 'a' ) >>
1415 =head4 C<< $f->attr( 'a', 's', ... ) >>
1417 Return 1 if the file has all the DOS attributes provided.
1421 my ($s, @flags) = @_;
1423 return 0 if !$s->{attr}{$_};
1428 =head4 C<< $f->attr_str >>
1430 Return DOS attributes as a compact string.
1432 Read-only, hiden, system, archive => "rhsa"
1437 return undef if !$s->{remote};
1438 join('', map {$_ if $s->{attr}{$_}} qw/r h s a d n/);
1441 =head4 C<< $f->set_time($t) >>
1443 Set modification and access time of the file to C<$t>.
1445 C<$t> must be in Epoch time (number of seconds since 1970/1/1).
1450 utime $t, $t, $s->localpath;
1453 =head4 C<< $f->md5 >>
1455 Return md5 sum of the file.
1457 The result is cached.
1464 open my $h, '<', $s->localpath() or die "can't read ".$s->localpath.": $!";
1466 $s->{md5} = Digest::MD5->new->addfile($h)->hexdigest;
1475 if ($s->{delete_on_destruction} && -f $s->localpath) {
1477 say "DESTROY ".$s->localpath;
1479 unlink $s->localpath;
1485 =head4 C<< File::walk( \&function, @files) >>
1487 =head4 C<< File::walk( sub { ... }, @files) >>
1489 Iterate on file hierachy in C<@files> and return accumulated results.
1491 Use C<$_> in the sub to access the current C<File>.
1493 The C<@files> must come from a call to the C<File::tree> function.
1497 my $fun = \&{shift @_};
1502 if ($_->{attr}{d}) {
1503 push @res, walk($fun, @{$_->{content}});
1505 push @res, $fun->($_);
1512 =head4 C<< File::list( $remotepath ) >>
1514 Return list of file (C<File> instance) in C<$remotepath>.
1516 C<$remotepath> must be a directory.
1523 my $out = main::smb_client('-D', $path, '-c', 'ls');
1525 for (split /\n/, $out) {
1526 next if !/^ (.+?)\s+([AHSRDN]*)\s+(\d+)\s+(.+)/o;
1527 my ($fn, $attr, $size, $date) = ($1, $2, $3, $4);
1528 next if $fn =~ /^\.{1,2}$/;
1530 push @files, bless {
1532 'dir' => $path =~ s{^/}{}r,
1534 'size' => int($size),
1537 # list context returns something different than the
1538 # boolean matching result => force scalar context
1539 'a' => scalar ($attr =~ /A/),
1540 'h' => scalar ($attr =~ /H/),
1541 's' => scalar ($attr =~ /S/),
1542 'r' => scalar ($attr =~ /R/),
1543 'd' => scalar ($attr =~ /D/),
1544 'n' => scalar ($attr =~ /N/),
1551 =head4 C<< File::tree( $remotepath ) >>
1553 Return recursive list of file in C<$remotepath>.
1555 C<$remotepath> must be a directory.
1557 Use C<File::walk()> to iterate over all the files.
1566 } elsif (blessed $d) {
1567 @files = list($d->remotepath);
1572 for my $f (@files) {
1573 if ($f->{attr}{d}) {
1574 $f->{content} = [tree($f)];
1581 # remove trailing or duplicated slash
1589 # create random file at path local path $fn
1591 my ($fn, $size) = @_;
1593 unlink $fn if -e $fn;
1594 $size ||= main::random(512, 1024);
1598 # try /dev/urandom, faster
1599 if (-e '/dev/urandom') {
1600 my $cmd = sprintf('head -c %d /dev/urandom | tee %s | md5sum',
1601 $size, quotemeta($fn));
1602 $md5 = (split / /, `$cmd`)[0];
1604 open my $out, '>', $fn or die "can't open $fn: $!\n";
1607 $buf .= pack('C', main::random(0, 256));
1611 $md5 = md5_hex($buf);
1619 # create remote file in $DIR/foo/bar
1620 my $f = File->new_remote("foo/bar/myfile");
1621 say $f->localpath; # /opt/share/$DIR/foo/bar/myfile
1622 say $f->remotepath; # $DIR/foo/bar/myfile
1623 say $f->remotedir; # $DIR/foo/bar
1626 # same but in root dir
1627 my $f = File->new_remote("myfile", 1);
1628 say $f->localpath; # /opt/share/myfile
1629 say $f->remotepath; # myfile
1630 say $f->remotedir; #
1633 # create local random temp file in $TMP
1634 my $f = File->new_local("$TMP/temp");
1635 say $f->remotepath; # undef because it's not on the server
1638 # same but file contains "hello"
1639 my $f = File->new_local("$TMP/temp", "hello");
1642 # list of files in $DIR (1 level)
1643 for (File::list($DIR)) {
1648 # list of all files in dir and subdir of $DIR
1649 File::walk(sub { say $_->remotepath }, File::tree($DIR));