File Coverage

blib/lib/File/Copy/Recursive.pm
Criterion Covered Total %
statement 193 232 83.1
branch 109 200 54.5
condition 65 123 52.8
subroutine 22 22 100.0
pod 12 12 100.0
total 401 589 68.0


line stmt bran cond sub pod time code
1             package File::Copy::Recursive;
2              
3 6     6   474120 use strict;
  6         60  
  6         324  
4              
5             BEGIN {
6             # Keep older versions of Perl from trying to use lexical warnings
7 6 50   6   133 $INC{'warnings.pm'} = "fake warnings entry for < 5.6 perl ($])" if $] < 5.006;
8             }
9 6     6   45 use warnings;
  6         13  
  6         185  
10              
11 6     6   40 use Carp;
  6         12  
  6         386  
12 6     6   3197 use File::Copy;
  6         21046  
  6         326  
13 6     6   42 use File::Spec; #not really needed because File::Copy already gets it, but for good measure :)
  6         12  
  6         103  
14 6     6   27 use Cwd ();
  6         11  
  6         207  
15              
16 6         22339 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   38 );
  6         13  
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.44';
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 62583 $samecheck->(@_) or return;
141 147 50 66     411 if ( $RMTrgFil && ( -d $_[1] || -e $_[1] ) ) {
      100        
142 4         20 my $trg = $_[1];
143 4 100       58 if ( -d $trg ) {
144 2         38 my @trgx = File::Spec->splitpath( $_[0] );
145 2         36 $trg = File::Spec->catfile( $_[1], $trgx[$#trgx] );
146             }
147 4 50       15 $samecheck->( $_[0], $trg ) or return;
148 4 50       57 if ( -e $trg ) {
149 4 100       23 if ( $RMTrgFil == 1 ) {
150 2 50       9 unlink $trg or carp "\$RMTrgFil failed: $!";
151             }
152             else {
153 2 50       10 unlink $trg or return;
154             }
155             }
156             }
157 145         2464 my ( $volm, $path ) = File::Spec->splitpath( $_[1] );
158 145 100 66     2183 if ( $path && !-d $path ) {
159 2         30 pathmk( File::Spec->catpath( $volm, $path, '' ), $NoFtlPth );
160             }
161 145 50 33     3182 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       1561 return if -d $_[0]; # address File::Copy::copy() bug outlined in https://rt.perl.org/Public/Bug/Display.html?id=132866
175 143 100       632 copy(@_) or return;
176              
177 141         48160 my @base_file = File::Spec->splitpath( $_[0] );
178 141 100       2331 my $mode_trg = -d $_[1] ? File::Spec->catfile( $_[1], $base_file[$#base_file] ) : $_[1];
179              
180 141 100       3578 chmod scalar( ( stat( $_[0] ) )[2] ), $mode_trg if $KeepMode;
181             }
182 141 100       649 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 5331 if ( -l $_[0] && $CopyLink ) {
187 0         0 goto &fcopy;
188             }
189              
190 9 100 100     133 goto &dircopy if -d $_[0] || substr( $_[0], ( 1 * -1 ), 1 ) eq '*';
191 7         44 goto &fcopy;
192             }
193              
194             sub rcopy_glob {
195 1     1 1 611 $glob->( \&rcopy, @_ );
196             }
197              
198             sub dircopy {
199 13 50 33 13 1 67313 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         24 my $globstar = 0;
208 13         39 my $_zero = $_[0];
209 13         21 my $_one = $_[1];
210 13 100       63 if ( substr( $_zero, ( 1 * -1 ), 1 ) eq '*' ) {
211 2         4 $globstar = 1;
212 2         9 $_zero = substr( $_zero, 0, ( length($_zero) - 1 ) );
213             }
214              
215 13 50       58 $samecheck->( $_zero, $_[1] ) or return;
216 13 100 100     331 if ( !-d $_zero || ( -e $_[1] && !-d $_[1] ) ) {
      100        
217 4         18 $! = 20;
218 4         24 return;
219             }
220              
221 9 100       105 if ( !-d $_[1] ) {
222 5 50       23 pathmk( $_[1], $NoFtlPth ) or return;
223             }
224             else {
225 4 100 100     93 if ( $CPRFComp && !$globstar ) {
226 1         22 my @parts = File::Spec->splitdir($_zero);
227 1         18 while ( $parts[$#parts] eq '' ) { pop @parts; }
  0         0  
228 1         12 $_one = File::Spec->catdir( $_[1], $parts[$#parts] );
229             }
230             }
231 9         25 my $baseend = $_one;
232 9         16 my $level = 0;
233 9         19 my $filen = 0;
234 9         13 my $dirn = 0;
235              
236 9         14 my $recurs; #must be my()ed before sub {} since it calls itself
237             $recurs = sub {
238 45     45   138 my ( $str, $end, $buf ) = @_;
239 45 100       117 $filen++ if $end eq $baseend;
240 45 100       91 $dirn++ if $end eq $baseend;
241              
242 45 50       125 $DirPerms = oct($DirPerms) if substr( $DirPerms, 0, 1 ) eq '0';
243 45 100 50     2395 mkdir( $end, $DirPerms ) or return if !-d $end;
244 45 0 33     179 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         86 $level++;
251              
252 45         70 my @files;
253 45 50       115 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       1465 opendir( my $str_dh, $str ) or return;
260 45   100     1860 @files = grep( $_ ne '.' && $_ ne '..', readdir($str_dh) );
261 45         662 closedir $str_dh;
262             }
263              
264 45         143 for my $file (@files) {
265 306         1707 my ($file_ut) = $file =~ m{ (.*) }xms;
266 306         2930 my $org = File::Spec->catfile( $str, $file_ut );
267 306         1731 my $new = File::Spec->catfile( $end, $file_ut );
268 306 100 66     6608 if ( -l $org && $CopyLink ) {
    100          
269 135         1505 my $target = readlink($org);
270 135         676 ($target) = $target =~ m/(.*)/; # mass-untaint is OK since we have to allow what the file system does
271 135 50 66     1494 carp "Copying a symlink ($org) whose target does not exist"
272             if !-e $target && $BdTrgWrn;
273 135 50       2861 unlink $new if -l $new;
274 135 50       4112 symlink( $target, $new ) or return;
275             }
276             elsif ( -d $org ) {
277 36         109 my $rc;
278 36 50 33     556 if ( !-w $org && $KeepMode ) {
279 0         0 local $KeepMode = 0;
280 0         0 carp "Copying readonly directory ($org); mode of its contents may not be preserved.";
281 0 0       0 $rc = $recurs->( $org, $new, $buf ) if defined $buf;
282 0 0       0 $rc = $recurs->( $org, $new ) if !defined $buf;
283 0         0 chmod scalar( ( stat($org) )[2] ), $new;
284             }
285             else {
286 36 50       107 $rc = $recurs->( $org, $new, $buf ) if defined $buf;
287 36 50       225 $rc = $recurs->( $org, $new ) if !defined $buf;
288             }
289 36 50       101 if ( !$rc ) {
290 0 0       0 if ($SkipFlop) {
291 0         0 next;
292             }
293             else {
294 0         0 return;
295             }
296             }
297 36         49 $filen++;
298 36         97 $dirn++;
299             }
300             else {
301 135 50       433 if ( $ok_todo_asper_condcopy->($org) ) {
302 135 50       245 if ($SkipFlop) {
303 0 0 0     0 fcopy( $org, $new, $buf ) or next if defined $buf;
304 0 0 0     0 fcopy( $org, $new ) or next if !defined $buf;
305             }
306             else {
307 135 50 0     256 fcopy( $org, $new, $buf ) or return if defined $buf;
308 135 50 50     365 fcopy( $org, $new ) or return if !defined $buf;
309             }
310 135 100       2684 chmod scalar( ( stat($org) )[2] ), $new if $KeepMode;
311 135         519 $filen++;
312             }
313             }
314             }
315 45         115 $level--;
316 45 100       1029 chmod scalar( ( stat($str) )[2] ), $end if $KeepMode;
317 45         285 1;
318              
319 9         102 };
320              
321 9 50       42 $recurs->( $_zero, $_one, $_[2] ) or return;
322 9 100       98 return wantarray ? ( $filen, $dirn, $level ) : $filen;
323             }
324              
325 10     10 1 2364 sub fmove { $move->( 1, @_ ) }
326              
327             sub rmove {
328 9 0 33 9 1 5176 if ( -l $_[0] && $CopyLink ) {
329 0         0 goto &fmove;
330             }
331              
332 9 100 100     130 goto &dirmove if -d $_[0] || substr( $_[0], ( 1 * -1 ), 1 ) eq '*';
333 7         39 goto &fmove;
334             }
335              
336             sub rmove_glob {
337 1     1 1 552 $glob->( \&rmove, @_ );
338             }
339              
340 4     4 1 18999 sub dirmove { $move->( 0, @_ ) }
341              
342             sub pathmk {
343 12     12 1 17152 my ( $vol, $dir, $file ) = File::Spec->splitpath( shift() );
344 12         35 my $nofatal = shift;
345              
346 12 50       79 $DirPerms = oct($DirPerms) if substr( $DirPerms, 0, 1 ) eq '0';
347              
348 12 50       43 if ( defined($dir) ) {
349 12         70 my (@dirs) = File::Spec->splitdir($dir);
350              
351 12         48 for ( my $i = 0; $i < scalar(@dirs); $i++ ) {
352 58         450 my $newdir = File::Spec->catdir( @dirs[ 0 .. $i ] );
353 58         350 my $newpth = File::Spec->catpath( $vol, $newdir, "" );
354              
355 58 50 50     1177 mkdir( $newpth, $DirPerms ) or return if !-d $newpth && !$nofatal;
      66        
356 58 50 33     837 mkdir( $newpth, $DirPerms ) if !-d $newpth && $nofatal;
357             }
358             }
359              
360 12 50       52 if ( defined($file) ) {
361 12         91 my $newpth = File::Spec->catpath( $vol, $dir, $file );
362              
363 12 50 50     682 mkdir( $newpth, $DirPerms ) or return if !-d $newpth && !$nofatal;
      66        
364 12 50 33     193 mkdir( $newpth, $DirPerms ) if !-d $newpth && $nofatal;
365             }
366              
367 12         51 1;
368             }
369              
370             sub pathempty {
371 188     188 1 15232 my $pth = shift;
372              
373 188         2147 my ( $orig_dev, $orig_ino ) = ( lstat $pth )[ 0, 1 ];
374 188 100 66     3369 return 2 if !-d _ || !$orig_dev || ( $^O ne 'MSWin32' && !$orig_ino ); #stat.inode is 0 on Windows
      33        
      66        
375              
376 185         521878 my $starting_point = Cwd::cwd();
377 185         7522 my ( $starting_dev, $starting_ino ) = ( lstat $starting_point )[ 0, 1 ];
378 185 50       3670 chdir($pth) or Carp::croak("Failed to change directory to “$pth”: $!");
379 185         1571 $pth = '.';
380 185         3425 _bail_if_changed( $pth, $orig_dev, $orig_ino );
381              
382 185         472 my @names;
383             my $pth_dh;
384 185 50       1174 if ( $] < 5.006 ) {
385 0 0       0 opendir( PTH_DH, $pth ) or return;
386 0         0 @names = grep !/^\.\.?$/, readdir(PTH_DH);
387 0         0 closedir PTH_DH;
388             }
389             else {
390 185 50       12411 opendir( $pth_dh, $pth ) or return;
391 185         11599 @names = grep !/^\.\.?$/, readdir($pth_dh);
392 185         3179 closedir $pth_dh;
393             }
394 185         1484 _bail_if_changed( $pth, $orig_dev, $orig_ino );
395              
396 185         842 for my $name (@names) {
397 389         8573 my ($name_ut) = $name =~ m{ (.*) }xms;
398 389         6117 my $flpth = File::Spec->catdir( $pth, $name_ut );
399              
400 389 100       82343 if ( -l $flpth ) {
    100          
401 72         312 _bail_if_changed( $pth, $orig_dev, $orig_ino );
402 72 50       950 unlink $flpth or return;
403             }
404             elsif ( -d $flpth ) {
405 127         533 _bail_if_changed( $pth, $orig_dev, $orig_ino );
406 127 50       694 pathrmdir($flpth) or return;
407             }
408             else {
409 190         896 _bail_if_changed( $pth, $orig_dev, $orig_ino );
410 180 50       7051 unlink $flpth or return;
411             }
412             }
413              
414 175 50       3031 chdir($starting_point) or Carp::croak("Failed to change directory to “$starting_point”: $!");
415 175         683 _bail_if_changed( ".", $starting_dev, $starting_ino );
416              
417 175         1768 return 1;
418             }
419              
420             sub pathrm {
421 6     6 1 10331 my ( $path, $force, $nofail ) = @_;
422              
423 6         126 my ( $orig_dev, $orig_ino ) = ( lstat $path )[ 0, 1 ];
424 6 50 33     123 return 2 if !-d _ || !$orig_dev || !$orig_ino;
      33        
425              
426             # Manual test (I hate this function :/):
427             # sudo mkdir /foo && perl -MFile::Copy::Recursive=pathrm -le 'print pathrm("/foo",1)' && sudo rm -rf /foo
428 6 50 33     47 if ( $force && File::Spec->file_name_is_absolute($path) ) {
429 0         0 Carp::croak("pathrm() w/ force on abspath is not allowed");
430             }
431              
432 6         145 my @pth = File::Spec->splitdir($path);
433              
434 6         29 my %fs_check;
435             my $aggregate_path;
436 6         40 for my $part (@pth) {
437 16 100       106 $aggregate_path = defined $aggregate_path ? File::Spec->catdir( $aggregate_path, $part ) : $part;
438 16         592 $fs_check{$aggregate_path} = [ ( lstat $aggregate_path )[ 0, 1 ] ];
439             }
440              
441 6         45 while (@pth) {
442 6         39 my $cur = File::Spec->catdir(@pth);
443 6 50       38273 last if !$cur; # necessary ???
444              
445 6 50       29 if ($force) {
446 0         0 _bail_if_changed( $cur, $fs_check{$cur}->[0], $fs_check{$cur}->[1] );
447 0 0       0 if ( !pathempty($cur) ) {
448 0 0       0 return unless $nofail;
449             }
450             }
451 6         109 _bail_if_changed( $cur, $fs_check{$cur}->[0], $fs_check{$cur}->[1] );
452 1 50       5 if ($nofail) {
453 0         0 rmdir $cur;
454             }
455             else {
456 1 50       32 rmdir $cur or return;
457             }
458 0         0 pop @pth;
459             }
460              
461 0         0 return 1;
462             }
463              
464             sub pathrmdir {
465 181     181 1 48762 my $dir = shift;
466 181 50       2261 if ( -e $dir ) {
467 181 50       2096 return if !-d $dir;
468             }
469             else {
470 0         0 return 2;
471             }
472              
473 181         2256 my ( $orig_dev, $orig_ino ) = ( lstat $dir )[ 0, 1 ];
474 181 50 33     3800 return 2 if !$orig_dev || ( $^O ne 'MSWin32' && !$orig_ino );
      33        
475              
476 181 50       882 pathempty($dir) or return;
477 176         649 _bail_if_changed( $dir, $orig_dev, $orig_ino );
478 176 100       7211 rmdir $dir or return;
479              
480 173         1596 return 1;
481             }
482              
483             sub _bail_if_changed {
484 1116     1116   4722 my ( $path, $orig_dev, $orig_ino ) = @_;
485              
486 1116         13181 my ( $cur_dev, $cur_ino ) = ( lstat $path )[ 0, 1 ];
487              
488 1116 100 66     7734 if ( !defined $cur_dev || !defined $cur_ino ) {
489 4   50     89 $cur_dev ||= "undef(path went away?)";
490 4   50     66 $cur_ino ||= "undef(path went away?)";
491             }
492             else {
493 1112         15909 $path = Cwd::abs_path($path);
494             }
495              
496 1116 100 100     8221 if ( $orig_dev ne $cur_dev || $orig_ino ne $cur_ino ) {
497 15         175 local $Carp::CarpLevel += 1;
498 15         7204 Carp::croak("directory $path changed: expected dev=$orig_dev ino=$orig_ino, actual dev=$cur_dev ino=$cur_ino, aborting");
499             }
500             }
501              
502             1;
503              
504             __END__