File Coverage

blib/lib/File/Copy/Recursive.pm
Criterion Covered Total %
statement 193 231 83.5
branch 109 200 54.5
condition 65 123 52.8
subroutine 22 22 100.0
pod 12 12 100.0
total 401 588 68.2


line stmt bran cond sub pod time code
1             package File::Copy::Recursive;
2              
3 6     6   449832 use strict;
  6         50  
  6         332  
4              
5             BEGIN {
6             # Keep older versions of Perl from trying to use lexical warnings
7 6 50   6   139 $INC{'warnings.pm'} = "fake warnings entry for < 5.6 perl ($])" if $] < 5.006;
8             }
9 6     6   46 use warnings;
  6         10  
  6         200  
10              
11 6     6   37 use Carp;
  6         11  
  6         367  
12 6     6   2782 use File::Copy;
  6         20167  
  6         327  
13 6     6   42 use File::Spec; #not really needed because File::Copy already gets it, but for good measure :)
  6         12  
  6         106  
14 6     6   26 use Cwd ();
  6         17  
  6         191  
15              
16 6         21326 use vars qw(
17             @ISA @EXPORT_OK $VERSION $MaxDepth $KeepMode $CPRFComp $CopyLink
18             $PFSCheck $RemvBase $NoFtlPth $ForcePth $CopyLoop $RMTrgFil $RMTrgDir
19             $CondCopy $BdTrgWrn $SkipFlop $DirPerms
20 6     6   36 );
  6         18  
21              
22             require Exporter;
23             @ISA = qw(Exporter);
24             @EXPORT_OK = qw(fcopy rcopy dircopy fmove rmove dirmove pathmk pathrm pathempty pathrmdir rcopy_glob rmove_glob);
25              
26             $VERSION = '0.45';
27              
28             $MaxDepth = 0;
29             $KeepMode = 1;
30             $CPRFComp = 0;
31             $CopyLink = eval { local $SIG{'__DIE__'}; symlink '', ''; 1 } || 0;
32             $PFSCheck = 1;
33             $RemvBase = 0;
34             $NoFtlPth = 0;
35             $ForcePth = 0;
36             $CopyLoop = 0;
37             $RMTrgFil = 0;
38             $RMTrgDir = 0;
39             $CondCopy = {};
40             $BdTrgWrn = 0;
41             $SkipFlop = 0;
42             $DirPerms = 0777;
43              
44             my $samecheck = sub {
45             return 1 if $^O eq 'MSWin32'; # need better way to check for this on winders...
46             return if @_ != 2 || !defined $_[0] || !defined $_[1];
47             return if $_[0] eq $_[1];
48              
49             my $one = '';
50             if ($PFSCheck) {
51             $one = join( '-', ( stat $_[0] )[ 0, 1 ] ) || '';
52             my $two = join( '-', ( stat $_[1] )[ 0, 1 ] ) || '';
53             if ( $one eq $two && $one ) {
54             carp "$_[0] and $_[1] are identical";
55             return;
56             }
57             }
58              
59             if ( -d $_[0] && !$CopyLoop ) {
60             $one = join( '-', ( stat $_[0] )[ 0, 1 ] ) if !$one;
61             my $abs = File::Spec->rel2abs( $_[1] );
62             my @pth = File::Spec->splitdir($abs);
63             while (@pth) {
64             if ( $pth[-1] eq '..' ) { # cheaper than Cwd::realpath() plus we don't want to resolve symlinks at this point, right?
65             pop @pth;
66             pop @pth unless -l File::Spec->catdir(@pth);
67             next;
68             }
69             my $cur = File::Spec->catdir(@pth);
70             last if !$cur; # probably not necessary, but nice to have just in case :)
71             my $two = join( '-', ( stat $cur )[ 0, 1 ] ) || '';
72             if ( $one eq $two && $one ) {
73              
74             # $! = 62; # Too many levels of symbolic links
75             carp "Caught Deep Recursion Condition: $_[0] contains $_[1]";
76             return;
77             }
78              
79             pop @pth;
80             }
81             }
82              
83             return 1;
84             };
85              
86             my $glob = sub {
87             my ( $do, $src_glob, @args ) = @_;
88              
89             local $CPRFComp = 1;
90             require File::Glob;
91              
92             my @rt;
93             for my $path ( File::Glob::bsd_glob($src_glob) ) {
94             my @call = [ $do->( $path, @args ) ] or return;
95             push @rt, \@call;
96             }
97              
98             return @rt;
99             };
100              
101             my $move = sub {
102             my $fl = shift;
103             my @x;
104             if ($fl) {
105             @x = fcopy(@_) or return;
106             }
107             else {
108             @x = dircopy(@_) or return;
109             }
110             if (@x) {
111             if ($fl) {
112             unlink $_[0] or return;
113             }
114             else {
115             pathrmdir( $_[0] ) or return;
116             }
117             if ($RemvBase) {
118             my ( $volm, $path ) = File::Spec->splitpath( $_[0] );
119             pathrm( File::Spec->catpath( $volm, $path, '' ), $ForcePth, $NoFtlPth ) or return;
120             }
121             }
122             return wantarray ? @x : $x[0];
123             };
124              
125             my $ok_todo_asper_condcopy = sub {
126             my $org = shift;
127             my $copy = 1;
128             if ( exists $CondCopy->{$org} ) {
129             if ( $CondCopy->{$org}{'md5'} ) {
130              
131             }
132             if ($copy) {
133              
134             }
135             }
136             return $copy;
137             };
138              
139             sub fcopy {
140 147 50   147 1 56285 $samecheck->(@_) or return;
141 147 50 66     391 if ( $RMTrgFil && ( -d $_[1] || -e $_[1] ) ) {
      100        
142 4         13 my $trg = $_[1];
143 4 100       46 if ( -d $trg ) {
144 2         39 my @trgx = File::Spec->splitpath( $_[0] );
145 2         30 $trg = File::Spec->catfile( $_[1], $trgx[$#trgx] );
146             }
147 4 50       22 $samecheck->( $_[0], $trg ) or return;
148 4 50       51 if ( -e $trg ) {
149 4 100       17 if ( $RMTrgFil == 1 ) {
150 2 50       13 unlink $trg or carp "\$RMTrgFil failed: $!";
151             }
152             else {
153 2 50       9 unlink $trg or return;
154             }
155             }
156             }
157 145         2467 my ( $volm, $path ) = File::Spec->splitpath( $_[1] );
158 145 100 66     1952 if ( $path && !-d $path ) {
159 2         27 pathmk( File::Spec->catpath( $volm, $path, '' ), $NoFtlPth );
160             }
161 145 50 33     2888 if ( -l $_[0] && $CopyLink ) {
    100 66        
162 0         0 my $target = readlink( shift() );
163 0         0 ($target) = $target =~ m/(.*)/; # mass-untaint is OK since we have to allow what the file system does
164 0 0 0     0 carp "Copying a symlink ($_[0]) whose target does not exist"
165             if !-e $target && $BdTrgWrn;
166 0         0 my $new = shift();
167 0 0       0 unlink $new if -l $new;
168 0 0       0 symlink( $target, $new ) or return;
169             }
170             elsif ( -d $_[0] && -f $_[1] ) {
171 2         21 return;
172             }
173             else {
174 143 50       1492 return if -d $_[0]; # address File::Copy::copy() bug outlined in https://rt.perl.org/Public/Bug/Display.html?id=132866
175 143 100       661 copy(@_) or return;
176              
177 141         38441 my @base_file = File::Spec->splitpath( $_[0] );
178 141 100       2157 my $mode_trg = -d $_[1] ? File::Spec->catfile( $_[1], $base_file[$#base_file] ) : $_[1];
179              
180 141 100       3189 chmod scalar( ( stat( $_[0] ) )[2] ), $mode_trg if $KeepMode;
181             }
182 141 100       638 return wantarray ? ( 1, 0, 0 ) : 1; # use 0's incase they do math on them and in case rcopy() is called in list context = no uninit val warnings
183             }
184              
185             sub rcopy {
186 9 0 33 9 1 5233 if ( -l $_[0] && $CopyLink ) {
187 0         0 goto &fcopy;
188             }
189              
190 9 100 100     127 goto &dircopy if -d $_[0] || substr( $_[0], ( 1 * -1 ), 1 ) eq '*';
191 7         39 goto &fcopy;
192             }
193              
194             sub rcopy_glob {
195 1     1 1 569 $glob->( \&rcopy, @_ );
196             }
197              
198             sub dircopy {
199 13 50 33 13 1 59950 if ( $RMTrgDir && -d $_[1] ) {
200 0 0       0 if ( $RMTrgDir == 1 ) {
201 0 0       0 pathrmdir( $_[1] ) or carp "\$RMTrgDir failed: $!";
202             }
203             else {
204 0 0       0 pathrmdir( $_[1] ) or return;
205             }
206             }
207 13         41 my $globstar = 0;
208 13         25 my $_zero = $_[0];
209 13         25 my $_one = $_[1];
210 13 100       50 if ( substr( $_zero, ( 1 * -1 ), 1 ) eq '*' ) {
211 2         10 $globstar = 1;
212 2         9 $_zero = substr( $_zero, 0, ( length($_zero) - 1 ) );
213             }
214              
215 13 50       48 $samecheck->( $_zero, $_[1] ) or return;
216 13 100 100     310 if ( !-d $_zero || ( -e $_[1] && !-d $_[1] ) ) {
      100        
217 4         15 $! = 20;
218 4         19 return;
219             }
220              
221 9 100       139 if ( !-d $_[1] ) {
222 5 50       24 pathmk( $_[1], $NoFtlPth ) or return;
223             }
224             else {
225 4 100 100     31 if ( $CPRFComp && !$globstar ) {
226 1         17 my @parts = File::Spec->splitdir($_zero);
227 1         13 while ( $parts[$#parts] eq '' ) { pop @parts; }
  0         0  
228 1         15 $_one = File::Spec->catdir( $_[1], $parts[$#parts] );
229             }
230             }
231 9         33 my $baseend = $_one;
232 9         19 my $level = 0;
233 9         27 my $filen = 0;
234 9         20 my $dirn = 0;
235              
236 9         11 my $recurs; #must be my()ed before sub {} since it calls itself
237             $recurs = sub {
238 45     45   127 my ( $str, $end, $buf ) = @_;
239 45 100       115 $filen++ if $end eq $baseend;
240 45 100       91 $dirn++ if $end eq $baseend;
241              
242 45 50       129 $DirPerms = oct($DirPerms) if substr( $DirPerms, 0, 1 ) eq '0';
243 45 100 50     2123 mkdir( $end, $DirPerms ) or return if !-d $end;
244 45 0 33     178 if ( $MaxDepth && $MaxDepth =~ m/^\d+$/ && $level >= $MaxDepth ) {
      33        
245 0 0       0 chmod scalar( ( stat($str) )[2] ), $end if $KeepMode;
246 0 0       0 return ( $filen, $dirn, $level ) if wantarray;
247 0         0 return $filen;
248             }
249              
250 45         135 $level++;
251              
252 45         74 my @files;
253 45 50       133 if ( $] < 5.006 ) {
254 0 0       0 opendir( STR_DH, $str ) or return;
255 0   0     0 @files = grep( $_ ne '.' && $_ ne '..', readdir(STR_DH) );
256 0         0 closedir STR_DH;
257             }
258             else {
259 45 50       1274 opendir( my $str_dh, $str ) or return;
260 45   100     1694 @files = grep( $_ ne '.' && $_ ne '..', readdir($str_dh) );
261 45         650 closedir $str_dh;
262             }
263              
264 45         128 for my $file (@files) {
265 306         1661 my ($file_ut) = $file =~ m{ (.*) }xms;
266 306         2827 my $org = File::Spec->catfile( $str, $file_ut );
267 306         1612 my $new = File::Spec->catfile( $end, $file_ut );
268 306 100 66     5862 if ( -l $org && $CopyLink ) {
    100          
269 135         1226 my $target = readlink($org);
270 135         675 ($target) = $target =~ m/(.*)/; # mass-untaint is OK since we have to allow what the file system does
271 135 50 66     1323 carp "Copying a symlink ($org) whose target does not exist"
272             if !-e $target && $BdTrgWrn;
273 135 50       2352 unlink $new if -l $new;
274 135 50       3315 symlink( $target, $new ) or return;
275             }
276             elsif ( -d $org ) {
277 36         100 my $rc;
278 36 50 33     439 if ( !-w $org && $KeepMode ) {
279 0         0 local $KeepMode = 0;
280 0 0       0 $rc = $recurs->( $org, $new, $buf ) if defined $buf;
281 0 0       0 $rc = $recurs->( $org, $new ) if !defined $buf;
282 0         0 chmod scalar( ( stat($org) )[2] ), $new;
283             }
284             else {
285 36 50       110 $rc = $recurs->( $org, $new, $buf ) if defined $buf;
286 36 50       226 $rc = $recurs->( $org, $new ) if !defined $buf;
287             }
288 36 50       100 if ( !$rc ) {
289 0 0       0 if ($SkipFlop) {
290 0         0 next;
291             }
292             else {
293 0         0 return;
294             }
295             }
296 36         54 $filen++;
297 36         72 $dirn++;
298             }
299             else {
300 135 50       439 if ( $ok_todo_asper_condcopy->($org) ) {
301 135 50       254 if ($SkipFlop) {
302 0 0 0     0 fcopy( $org, $new, $buf ) or next if defined $buf;
303 0 0 0     0 fcopy( $org, $new ) or next if !defined $buf;
304             }
305             else {
306 135 50 0     257 fcopy( $org, $new, $buf ) or return if defined $buf;
307 135 50 50     360 fcopy( $org, $new ) or return if !defined $buf;
308             }
309 135 100       2336 chmod scalar( ( stat($org) )[2] ), $new if $KeepMode;
310 135         535 $filen++;
311             }
312             }
313             }
314 45         114 $level--;
315 45 100       837 chmod scalar( ( stat($str) )[2] ), $end if $KeepMode;
316 45         245 1;
317              
318 9         88 };
319              
320 9 50       39 $recurs->( $_zero, $_one, $_[2] ) or return;
321 9 100       56 return wantarray ? ( $filen, $dirn, $level ) : $filen;
322             }
323              
324 10     10 1 2294 sub fmove { $move->( 1, @_ ) }
325              
326             sub rmove {
327 9 0 33 9 1 5042 if ( -l $_[0] && $CopyLink ) {
328 0         0 goto &fmove;
329             }
330              
331 9 100 100     120 goto &dirmove if -d $_[0] || substr( $_[0], ( 1 * -1 ), 1 ) eq '*';
332 7         35 goto &fmove;
333             }
334              
335             sub rmove_glob {
336 1     1 1 617 $glob->( \&rmove, @_ );
337             }
338              
339 4     4 1 17705 sub dirmove { $move->( 0, @_ ) }
340              
341             sub pathmk {
342 12     12 1 15966 my ( $vol, $dir, $file ) = File::Spec->splitpath( shift() );
343 12         41 my $nofatal = shift;
344              
345 12 50       45 $DirPerms = oct($DirPerms) if substr( $DirPerms, 0, 1 ) eq '0';
346              
347 12 50       31 if ( defined($dir) ) {
348 12         66 my (@dirs) = File::Spec->splitdir($dir);
349              
350 12         53 for ( my $i = 0; $i < scalar(@dirs); $i++ ) {
351 58         416 my $newdir = File::Spec->catdir( @dirs[ 0 .. $i ] );
352 58         317 my $newpth = File::Spec->catpath( $vol, $newdir, "" );
353              
354 58 50 50     1061 mkdir( $newpth, $DirPerms ) or return if !-d $newpth && !$nofatal;
      66        
355 58 50 33     751 mkdir( $newpth, $DirPerms ) if !-d $newpth && $nofatal;
356             }
357             }
358              
359 12 50       48 if ( defined($file) ) {
360 12         93 my $newpth = File::Spec->catpath( $vol, $dir, $file );
361              
362 12 50 50     605 mkdir( $newpth, $DirPerms ) or return if !-d $newpth && !$nofatal;
      66        
363 12 50 33     184 mkdir( $newpth, $DirPerms ) if !-d $newpth && $nofatal;
364             }
365              
366 12         47 1;
367             }
368              
369             sub pathempty {
370 188     188 1 13725 my $pth = shift;
371              
372 188         1958 my ( $orig_dev, $orig_ino ) = ( lstat $pth )[ 0, 1 ];
373 188 100 66     1806 return 2 if !-d _ || !defined($orig_dev) || ( $^O ne 'MSWin32' && !$orig_ino ); #stat.inode is 0 on Windows
      33        
      66        
374              
375 185         414455 my $starting_point = Cwd::cwd();
376 185         7097 my ( $starting_dev, $starting_ino ) = ( lstat $starting_point )[ 0, 1 ];
377 185 50       3066 chdir($pth) or Carp::croak("Failed to change directory to “$pth”: $!");
378 185         1286 $pth = '.';
379 185         2976 _bail_if_changed( $pth, $orig_dev, $orig_ino );
380              
381 185         433 my @names;
382             my $pth_dh;
383 185 50       1255 if ( $] < 5.006 ) {
384 0 0       0 opendir( PTH_DH, $pth ) or return;
385 0         0 @names = grep !/^\.\.?$/, readdir(PTH_DH);
386 0         0 closedir PTH_DH;
387             }
388             else {
389 185 50       11123 opendir( $pth_dh, $pth ) or return;
390 185         9245 @names = grep !/^\.\.?$/, readdir($pth_dh);
391 185         2973 closedir $pth_dh;
392             }
393 185         769 _bail_if_changed( $pth, $orig_dev, $orig_ino );
394              
395 185         722 for my $name (@names) {
396 389         7422 my ($name_ut) = $name =~ m{ (.*) }xms;
397 389         5385 my $flpth = File::Spec->catdir( $pth, $name_ut );
398              
399 389 100       71129 if ( -l $flpth ) {
    100          
400 72         295 _bail_if_changed( $pth, $orig_dev, $orig_ino );
401 72 50       964 unlink $flpth or return;
402             }
403             elsif ( -d $flpth ) {
404 127         518 _bail_if_changed( $pth, $orig_dev, $orig_ino );
405 127 50       651 pathrmdir($flpth) or return;
406             }
407             else {
408 190         901 _bail_if_changed( $pth, $orig_dev, $orig_ino );
409 180 50       6681 unlink $flpth or return;
410             }
411             }
412              
413 175 50       2580 chdir($starting_point) or Carp::croak("Failed to change directory to “$starting_point”: $!");
414 175         628 _bail_if_changed( ".", $starting_dev, $starting_ino );
415              
416 175         1654 return 1;
417             }
418              
419             sub pathrm {
420 6     6 1 9088 my ( $path, $force, $nofail ) = @_;
421              
422 6         95 my ( $orig_dev, $orig_ino ) = ( lstat $path )[ 0, 1 ];
423 6 50 33     125 return 2 if !-d _ || !defined($orig_dev) || !$orig_ino;
      33        
424              
425             # Manual test (I hate this function :/):
426             # sudo mkdir /foo && perl -MFile::Copy::Recursive=pathrm -le 'print pathrm("/foo",1)' && sudo rm -rf /foo
427 6 50 33     45 if ( $force && File::Spec->file_name_is_absolute($path) ) {
428 0         0 Carp::croak("pathrm() w/ force on abspath is not allowed");
429             }
430              
431 6         142 my @pth = File::Spec->splitdir($path);
432              
433 6         20 my %fs_check;
434             my $aggregate_path;
435 6         42 for my $part (@pth) {
436 16 100       79 $aggregate_path = defined $aggregate_path ? File::Spec->catdir( $aggregate_path, $part ) : $part;
437 16         479 $fs_check{$aggregate_path} = [ ( lstat $aggregate_path )[ 0, 1 ] ];
438             }
439              
440 6         31 while (@pth) {
441 6         31 my $cur = File::Spec->catdir(@pth);
442 6 50       35264 last if !$cur; # necessary ???
443              
444 6 50       36 if ($force) {
445 0         0 _bail_if_changed( $cur, $fs_check{$cur}->[0], $fs_check{$cur}->[1] );
446 0 0       0 if ( !pathempty($cur) ) {
447 0 0       0 return unless $nofail;
448             }
449             }
450 6         91 _bail_if_changed( $cur, $fs_check{$cur}->[0], $fs_check{$cur}->[1] );
451 1 50       5 if ($nofail) {
452 0         0 rmdir $cur;
453             }
454             else {
455 1 50       24 rmdir $cur or return;
456             }
457 0         0 pop @pth;
458             }
459              
460 0         0 return 1;
461             }
462              
463             sub pathrmdir {
464 181     181 1 46517 my $dir = shift;
465 181 50       2015 if ( -e $dir ) {
466 181 50       1933 return if !-d $dir;
467             }
468             else {
469 0         0 return 2;
470             }
471              
472 181         1887 my ( $orig_dev, $orig_ino ) = ( lstat $dir )[ 0, 1 ];
473 181 50 33     3315 return 2 if !defined($orig_dev) || ( $^O ne 'MSWin32' && !$orig_ino );
      33        
474              
475 181 50       926 pathempty($dir) or return;
476 176         653 _bail_if_changed( $dir, $orig_dev, $orig_ino );
477 176 100       6465 rmdir $dir or return;
478              
479 173         1435 return 1;
480             }
481              
482             sub _bail_if_changed {
483 1116     1116   4527 my ( $path, $orig_dev, $orig_ino ) = @_;
484              
485 1116         11820 my ( $cur_dev, $cur_ino ) = ( lstat $path )[ 0, 1 ];
486              
487 1116 100 66     7393 if ( !defined $cur_dev || !defined $cur_ino ) {
488 4   50     75 $cur_dev ||= "undef(path went away?)";
489 4   50     59 $cur_ino ||= "undef(path went away?)";
490             }
491             else {
492 1112         12376 $path = Cwd::abs_path($path);
493             }
494              
495 1116 100 100     7498 if ( $orig_dev ne $cur_dev || $orig_ino ne $cur_ino ) {
496 15         169 local $Carp::CarpLevel += 1;
497 15         7060 Carp::croak("directory $path changed: expected dev=$orig_dev ino=$orig_ino, actual dev=$cur_dev ino=$cur_ino, aborting");
498             }
499             }
500              
501             1;
502              
503             __END__