File Coverage

blib/lib/File/DirSync.pm
Criterion Covered Total %
statement 83 418 19.8
branch 9 340 2.6
condition 5 99 5.0
subroutine 21 43 48.8
pod 19 21 90.4
total 137 921 14.8


line stmt bran cond sub pod time code
1             package File::DirSync;
2              
3 2     2   19584 use strict;
  2         4  
  2         79  
4 2     2   11 use Exporter;
  2         5  
  2         225  
5 2     2   10 use Fcntl qw(O_CREAT O_RDONLY O_WRONLY O_EXCL);
  2         6  
  2         107  
6 2     2   23 use Carp qw(croak);
  2         3  
  2         88  
7              
8 2     2   10 use vars qw( $VERSION @ISA $PROC );
  2         3  
  2         282  
9             $VERSION = '1.22';
10             @ISA = qw(Exporter);
11             $PROC = join " ", $0, @ARGV;
12              
13             # Whether or not symlinks are supported
14 2   50 2   11 use constant HAS_SYMLINKS => ($^O !~ /Win32/i) || 0;
  2         3  
  2         289  
15              
16             # Wallclock percent spent sleeping
17 2     2   12 use constant GENTLE_PERCENT_DEFAULT => 50;
  2         10  
  2         82  
18 2     2   10 use constant GENTLE_PERCENT_MIN => 0;
  2         4  
  2         103  
19 2     2   21 use constant GENTLE_PERCENT_MAX => 99;
  2         3  
  2         89  
20              
21             # Disk Operations to run without sleeping
22 2     2   17 use constant GENTLE_OPS_DEFAULT => 1_000;
  2         4  
  2         92  
23 2     2   10 use constant GENTLE_OPS_MIN => 10;
  2         2  
  2         77  
24 2     2   260 use constant GENTLE_OPS_MAX => 20_000_000;
  2         4  
  2         112  
25              
26             # Automatically increase maxops by GENTLE_CHEWINCFACTOR
27             # whenever realtime spent syncing is under GENTLE_CHEWMINTIME
28             # or sleeptime is under GENTLE_SLEEPMINTIME.
29 2     2   9 use constant GENTLE_CHEWINCFACTOR => 0.25;
  2         11  
  2         90  
30 2     2   15 use constant GENTLE_CHEWMINTIME => 2;
  2         4  
  2         89  
31 2     2   10 use constant GENTLE_SLEEPMINTIME => 2;
  2         4  
  2         78  
32 2     2   10 use constant GENTLE_SLEEPMAXTIME => 600;
  2         2  
  2         96  
33              
34             # Number of bytes that can read and write to and from a local file in one syscall
35 2     2   16 use constant BUFSIZE => 8192;
  2         3  
  2         100  
36              
37             # Number of bytes written to consider as one "disk op"
38 2     2   11 use constant BLKSIZE => 1024;
  2         3  
  2         251  
39              
40             # Number of ops to be considered for each iteration during a file copy.
41             # (This counts both the read and write ops for the buffer.)
42 2     2   11 use constant BUFFER_OPS => BUFSIZE / BLKSIZE * 2;
  2         2  
  2         19530  
43              
44             sub new {
45 1     1 1 1445 my $class = shift;
46 1   50     9 my $self = shift || {};
47 1   50     9 $self->{only} ||= [];
48 1 50       4 $| = 1 if $self->{verbose};
49 1         2 bless $self, $class;
50 1         3 return $self;
51             }
52              
53             sub proctitle {
54 0     0 1 0 my $self = shift;
55 0   0     0 $self->{proctitle} ||= shift || $0;
      0        
56 0         0 return $self->{proctitle};
57             }
58              
59             sub gentle {
60 0     0 1 0 my $self = shift;
61              
62 0   0     0 $self->{_gentle_percent} = shift || GENTLE_PERCENT_DEFAULT;
63 0 0       0 $self->{_gentle_percent} = GENTLE_PERCENT_MIN if $self->{_gentle_percent} < GENTLE_PERCENT_MIN;
64 0 0       0 $self->{_gentle_percent} = GENTLE_PERCENT_MAX if $self->{_gentle_percent} > GENTLE_PERCENT_MAX;
65              
66 0   0     0 $self->{_gentle_maxops} = shift || GENTLE_OPS_DEFAULT;
67 0 0       0 $self->{_gentle_maxops} = GENTLE_OPS_MIN if $self->{_gentle_maxops} < GENTLE_OPS_MIN;
68 0 0       0 $self->{_gentle_maxops} = GENTLE_OPS_MAX if $self->{_gentle_maxops} > GENTLE_OPS_MAX;
69              
70 0         0 $self->{_gentle_started} = time;
71 0         0 $self->{_gentle_ops} = 0;
72              
73 0         0 return $self->{_gentle_percent};
74             }
75              
76             sub _op {
77 0     0   0 my $self = shift;
78 0 0 0     0 if (($self->{_gentle_ops} += (shift || 1) ) >= $self->{_gentle_maxops}) {
79             # Reached maximum operations
80 0         0 my $elapsed = time - $self->{_gentle_started};
81 0   0     0 my $delay = int ($elapsed / (100/$self->{_gentle_percent} - 1)) || 1;
82 0 0 0     0 if ($self->{_gentle_maxops} < GENTLE_OPS_MAX and
    0 0        
83             $elapsed < GENTLE_CHEWMINTIME ||
84             $delay < GENTLE_SLEEPMINTIME) {
85 0         0 $self->{_gentle_maxops} += int ($self->{_gentle_maxops} * GENTLE_CHEWINCFACTOR);
86 0 0       0 $self->{_gentle_maxops} = GENTLE_OPS_MAX if $self->{_gentle_maxops} > GENTLE_OPS_MAX;
87             } elsif ($delay > GENTLE_SLEEPMAXTIME) {
88 0         0 $self->{_gentle_maxops} -= int ($self->{_gentle_maxops} * GENTLE_CHEWINCFACTOR);
89 0         0 $delay = GENTLE_SLEEPMAXTIME;
90             }
91 0         0 my $prevproc = $0;
92 0 0       0 $0 = "$self->{proctitle} - [$self->{_gentle_percent}% gentle on $self->{_gentle_maxops} ops]: SLEEPING $delay UNTIL: ".scalar(localtime (time() + $delay)) if $self->{proctitle};
93 0         0 sleep $delay;
94 0         0 $0 = $prevproc;
95 0         0 $self->{_gentle_started} = time;
96 0         0 $self->{_gentle_ops} = 0;
97             }
98 0         0 return 1;
99             }
100              
101             sub rebuild {
102 0     0 1 0 my $self = shift;
103 0   0     0 my $dir = shift || $self->{src};
104              
105 0 0       0 croak 'Source directory must be specified: $obj->rebuild($directory) or define $obj->src($directory)'
106             unless defined $dir;
107              
108             # Remove trailing / if accidently supplied
109 0         0 $dir =~ s%/$%%;
110 0 0       0 -d $dir or
111             croak 'Source must be a directory';
112              
113 0 0       0 if (@{ $self->{only} }) {
  0         0  
114 0         0 foreach my $only (@{ $self->{only} }) {
  0         0  
115 0 0       0 if ($only =~ /^$dir/) {
116 0         0 $self->_rebuild( $only );
117             } else {
118 0         0 croak "$only is not a subdirectory of $dir";
119             }
120 0         0 local $self->{localmode} = 1;
121 0   0     0 while ($only =~ s%/[^/]*$%% && $only =~ /^$dir/) {
122 0         0 $self->_rebuild( $only );
123             }
124             }
125             } else {
126 0         0 $self->_rebuild( $dir );
127             }
128 0 0       0 $0 = $PROC if $self->{proctitle};
129 0 0       0 print "Rebuild cache complete.\n" if $self->{verbose};
130             }
131              
132             sub _rebuild {
133 0     0   0 my $self = shift;
134 0         0 my $dir = shift;
135              
136             # Hack to snab a scoped file handle.
137 0         0 my $handle = do { local *FH; };
  0         0  
138 0 0       0 $dir = $1 if $dir =~ m%^(.*)$%;
139 0 0       0 $self->_op if $self->{_gentle_percent};
140 0 0       0 return unless opendir($handle, $dir);
141 0 0       0 $0 = "$self->{proctitle} - rebuild: $dir" if $self->{proctitle};
142 0 0       0 $self->_op if $self->{_gentle_percent};
143 0         0 my $current = (lstat $dir)[9];
144 0         0 my $most_current = $current;
145 0         0 my $node;
146 0         0 my $skew = $self->{maxskew};
147 0 0       0 if (defined $skew) {
148 0         0 $skew += time;
149 0 0       0 if ($current > $skew) {
150 0         0 $most_current = $current = $skew;
151             }
152             }
153 0 0       0 $self->_op if $self->{_gentle_percent};
154 0         0 while (defined ($node = readdir($handle))) {
155 0 0       0 next if $node =~ /^\.\.?$/;
156 0 0       0 next if $self->{ignore}->{$node};
157 0         0 my $path = "$dir/$node";
158             # Recurse into directories to make sure they
159             # are updated before comparing time stamps
160 0 0       0 $self->_op if $self->{_gentle_percent};
161 0 0 0     0 !$self->{localmode} && !-l $path && -d _ && $self->_rebuild( $path );
      0        
162 0         0 my $this_stamp = (lstat $path)[9];
163 0 0       0 next if -l _;
164 0 0       0 if (defined $skew) {
165 0 0       0 $self->_op if $self->{_gentle_percent};
166 0 0 0     0 if ($this_stamp > $skew and !-l $path) {
167 0 0       0 print "Clock skew detected [$path] ".($this_stamp-$skew)." seconds in the future? Repairing...\n" if $self->{verbose};
168 0         0 utime($skew, $skew, $path);
169 0         0 $this_stamp = $skew;
170             }
171             }
172 0 0       0 if ($this_stamp > $most_current) {
173 0 0       0 print "Found a newer node [$path]\n" if $self->{verbose};
174 0         0 $most_current = $this_stamp;
175             }
176             }
177 0         0 closedir($handle);
178 0 0       0 if ($most_current > $current) {
179 0 0       0 print "Adjusting [$dir]...\n" if $self->{verbose};
180 0 0       0 $most_current = $1 if $most_current =~ /^(\d+)$/;
181 0 0       0 $self->_op if $self->{_gentle_percent};
182 0         0 utime($most_current, $most_current, $dir);
183             }
184 0         0 return;
185             }
186              
187             sub tracking {
188 0     0 1 0 my $self = shift;
189 0 0       0 if (@_) {
190 0 0       0 if (shift) {
191 0         0 $self->{_tracking} = {
192             removed => [],
193             updated => [],
194             skipped => [],
195             failed => [],
196             };
197             } else {
198 0         0 delete $self->{_tracking};
199             }
200             }
201 0 0       0 return ($self->{_tracking} ? 1 : 0);
202             }
203              
204             sub dirsync {
205 0     0 1 0 my $self = shift;
206 0   0     0 my $src = shift || $self->{src};
207 0   0     0 my $dst = shift || $self->{dst};
208 0 0 0     0 croak 'Source and destination directories must be specified: $obj->dirsync($source_directory, $destination_directory) or specify $obj->to($source_directory) and $obj->src($destination_directory)'
209             unless (defined $src) && (defined $dst);
210              
211             # Remove trailing / if accidently supplied
212 0         0 $src =~ s%/$%%;
213 0 0       0 -d $src or
214             croak 'Source must be a directory';
215             # Remove trailing / if accidently supplied
216 0         0 $dst =~ s%/$%%;
217 0         0 my $upper_dst = $dst;
218 0         0 $upper_dst =~ s%/?[^/]+$%%;
219 0 0 0     0 if ($upper_dst && !-d $upper_dst) {
220 0         0 croak "Destination root [$upper_dst] must exist: Aborting dirsync";
221             }
222 0         0 $self->_dirsync( $src, $dst );
223 0 0       0 $0 = $PROC if $self->{proctitle};
224 0         0 return;
225             }
226              
227             sub _dirsync {
228 0     0   0 my $self = shift;
229 0         0 my $src = shift;
230 0         0 my $dst = shift;
231              
232 0 0       0 $self->_op(2) if $self->{_gentle_percent};
233 0         0 my $when_dst = (lstat $dst)[9];
234 0         0 my $size_dst = -s _;
235 0         0 my @stat_src = lstat $src;
236 0         0 my $when_src = $stat_src[9];
237 0         0 my $size_src = $stat_src[7];
238              
239 0         0 if (HAS_SYMLINKS) {
240             # Symlink Check must be first because
241             # I could not figure out how to preserve
242             # timestamps (without root privileges).
243 0 0       0 if (-l _) {
244             # Source is a symlink
245 0 0       0 $self->_op(2) if $self->{_gentle_percent};
246 0         0 my $point = readlink($src);
247 0 0       0 if (-l $dst) {
248             # Dest is a symlink, too
249 0 0       0 if ($point eq (readlink $dst)) {
250             # Symlinks match, nothing to do.
251 0 0       0 $self->_op if $self->{_gentle_percent};
252 0         0 return;
253             }
254             # Remove incorrect symlink
255 0 0       0 print "$dst: Removing symlink\n" if $self->{verbose};
256 0 0       0 unlink $dst or warn "$dst: Failed to remove symlink: $!\n";
257 0 0       0 $self->_op(2) if $self->{_gentle_percent};
258             }
259 0 0       0 if (-d $dst) {
    0          
260             # Wipe directory
261 0 0       0 print "$dst: Removing tree\n" if $self->{verbose};
262 0 0       0 $self->rmtree($dst) or warn "$dst: Failed to rmtree!\n";
263             } elsif (-e $dst) {
264             # Regular file (or something else) needs to go
265 0 0       0 print "$dst: Removing\n" if $self->{verbose};
266 0 0       0 unlink $dst or warn "$dst: Failed to purge: $!\n";
267             }
268 0 0 0     0 if (-l $dst || -e $dst) {
269 0         0 warn "$dst: Still exists after wipe?!!!\n";
270             }
271 0 0       0 $point = $1 if $point =~ /^(.+)$/; # Taint
272             # Point to the same place that $src points to
273 0 0       0 print "$dst -> $point\n" if $self->{verbose};
274 0 0       0 symlink $point, $dst or warn "$dst: Failed to create symlink: $!\n";
275 0 0       0 $self->_op(5) if $self->{_gentle_percent};
276 0         0 return;
277             }
278             }
279              
280 0 0 0     0 if ($self->{nocache} && -d _) {
281 0         0 $size_dst = -1;
282             }
283             # Short circuit and kick out the common case:
284             # Nothing to do if the timestamp and size match
285 0 0 0     0 if ( defined ( $when_src && $when_dst && $size_src && $size_dst) &&
      0        
      0        
286             $when_src == $when_dst && $size_src == $size_dst ) {
287 0 0       0 push @{ $self->{_tracking}->{skipped} }, $dst if $self->{_tracking};
  0         0  
288 0         0 return;
289             }
290              
291             # Regular File Check
292 0 0       0 if (-f _) {
293             # Source is a plain file
294 0 0       0 if (-l $dst) {
    0          
295             # Dest is a symlink
296 0 0       0 print "$dst: Removing symlink\n" if $self->{verbose};
297 0 0       0 unlink $dst or warn "$dst: Failed to remove symlink: $!\n";
298 0 0       0 $self->_op if $self->{_gentle_percent};
299             } elsif (-d _) {
300             # Wipe directory
301 0 0       0 print "$dst: Removing tree\n" if $self->{verbose};
302 0 0       0 $self->rmtree($dst) or warn "$dst: Failed to rmtree: $!\n";
303             }
304 0 0       0 $self->_op if $self->{_gentle_percent};
305 0 0       0 $0 = "$self->{proctitle} - copying: $src => $dst" if $self->{proctitle};
306 0 0       0 if ($self->copy($src, $dst)) {
307 0 0       0 print "$dst: Updated\n" if $self->{verbose};
308 0 0       0 push @{ $self->{_tracking}->{updated} }, $dst if $self->{_tracking};
  0         0  
309             } else {
310 0         0 warn "$dst: Failed to copy: $!\n";
311             }
312 0 0       0 if (!-e $dst) {
313 0         0 warn "$dst: Never created?!!!\n";
314 0 0       0 push @{ $self->{_tracking}->{failed} }, $dst if $self->{_tracking};
  0         0  
315 0 0       0 $self->_op if $self->{_gentle_percent};
316 0         0 return;
317             }
318             # Force permissions to match the source
319 0 0       0 chmod( $stat_src[2] & 0777, $dst) or warn "$dst: Failed to chmod: $!\n";
320             # Force user and group ownership to match the source
321 0 0       0 chown( $stat_src[4], $stat_src[5], $dst) or warn "$dst: Failed to chown: $!\n";
322             # Force timestamp to match the source.
323 0 0       0 utime $when_src, $when_src, $dst or warn "$dst: Failed to utime: $!\n";
324 0 0       0 $self->_op(4) if $self->{_gentle_percent};
325 0         0 return;
326             }
327              
328             # Missing Check
329 0 0       0 if (!-e _) {
330             # The source does not exist
331             # The destination must also not exist
332 0 0       0 print "$dst: Removing\n" if $self->{verbose};
333 0 0       0 $0 = "$self->{proctitle} - removing: $dst" if $self->{proctitle};
334 0 0       0 if ( $self->rmtree($dst) ) {
335 0 0       0 push @{ $self->{_tracking}->{removed} }, $dst if $self->{_tracking};
  0         0  
336             } else {
337 0 0       0 push @{ $self->{_tracking}->{failed} }, $dst if $self->{_tracking};
  0         0  
338 0         0 warn "$dst: Failed to rmtree!\n";
339             }
340 0         0 return;
341             }
342              
343             # Finally, the recursive Directory Check
344 0 0       0 if (-d _) {
345             # Source is a directory
346 0 0       0 if (-l $dst) {
347             # Dest is a symlink
348 0 0       0 print "$dst: Removing symlink\n" if $self->{verbose};
349 0 0       0 unlink $dst or warn "$dst: Failed to remove symlink: $!\n";
350 0 0       0 $self->_op if $self->{_gentle_percent};
351             }
352 0 0       0 if (-f $dst) {
353             # Dest is a plain file
354             # It must be wiped
355 0 0       0 print "$dst: Removing file\n" if $self->{verbose};
356 0 0       0 if ( unlink($dst) ) {
357 0 0       0 push @{ $self->{_tracking}->{removed} }, $dst if $self->{_tracking};
  0         0  
358             } else {
359 0 0       0 push @{ $self->{_tracking}->{failed} }, $dst if $self->{_tracking};
  0         0  
360 0         0 warn "$dst: Failed to unlink file: $!\n";
361             }
362 0 0       0 $self->_op if $self->{_gentle_percent};
363             }
364 0 0       0 if (!-d $dst) {
365 0 0       0 if ( mkdir $dst, 0755 ) {
366 0 0       0 push @{ $self->{_tracking}->{updated} }, $dst if $self->{_tracking};
  0         0  
367             } else {
368 0 0       0 push @{ $self->{_tracking}->{failed} }, $dst if $self->{_tracking};
  0         0  
369 0         0 warn "$dst: Failed to mkdir: $!\n";
370             }
371 0 0       0 $self->_op if $self->{_gentle_percent};
372             }
373 0 0       0 -d $dst or warn "$dst: Destination directory cannot exist?\n";
374 0 0       0 $self->_op(4) if $self->{_gentle_percent};
375              
376             # If nocache() was not specified, then it is okay
377             # skip this directory if the timestamps match.
378 0 0       0 if (!$self->{nocache}) {
379             # (The directory sizes do not really matter.)
380             # If the timestamps are the same, nothing to do
381             # because rebuild() will ensure that the directory
382             # timestamp is the most recent within its
383             # entire descent.
384 0 0 0     0 if ( defined ( $when_src && $when_dst) &&
      0        
385             $when_src == $when_dst ) {
386 0 0       0 push @{ $self->{_tracking}->{skipped} }, $dst if $self->{_tracking};
  0         0  
387 0         0 return;
388             }
389             }
390              
391 0 0       0 print "$dst: Scanning...\n" if $self->{verbose};
392              
393             # I know the source is a directory.
394             # I know the destination is also a directory
395             # which has a different timestamp than the
396             # source. All nodes within both directories
397             # must be scanned and updated accordingly.
398              
399 0         0 my ($handle, $node, %nodes);
400              
401 0         0 $handle = do { local *FH; };
  0         0  
402 0 0       0 $0 = "$self->{proctitle} - src: $src" if $self->{proctitle};
403 0 0       0 return unless opendir($handle, $src);
404 0         0 while (defined ($node = readdir($handle))) {
405 0 0       0 next if $node =~ /^\.\.?$/;
406 0 0       0 next if $self->{ignore}->{$node};
407 0 0 0     0 next if ($self->{localmode} &&
      0        
408             !-l "$src/$node" &&
409             -d _);
410 0         0 $nodes{$node} = 1;
411 0 0       0 $self->_op if $self->{_gentle_percent};
412             }
413 0         0 closedir($handle);
414              
415 0         0 $handle = do { local *FH; };
  0         0  
416 0 0       0 $0 = "$self->{proctitle} - dst: $dst" if $self->{proctitle};
417 0 0       0 return unless opendir($handle, $dst);
418 0         0 while (defined ($node = readdir($handle))) {
419 0 0       0 next if $node =~ /^\.\.?$/;
420 0 0       0 next if $self->{ignore}->{$node};
421 0 0 0     0 next if ($self->{localmode} &&
      0        
422             !-l "$src/$node" &&
423             -d _);
424 0         0 $nodes{$node} = 1;
425 0 0       0 $self->_op if $self->{_gentle_percent};
426             }
427 0         0 closedir($handle);
428              
429 0 0       0 $0 = "$self->{proctitle} - syncing: $src => $dst" if $self->{proctitle};
430             # %nodes is now a union set of all nodes
431             # in both the source and destination.
432             # Recursively call myself for each node.
433 0         0 foreach $node (keys %nodes) {
434 0         0 $self->_dirsync("$src/$node", "$dst/$node");
435             }
436             # Force permissions to match the source
437 0 0       0 chmod( $stat_src[2] & 0777, $dst) or warn "$dst: Failed to chmod: $!\n";
438             # Force user and group ownership to match the source
439 0 0       0 chown( $stat_src[4], $stat_src[5], $dst) or warn "$dst: Failed to chown: $!\n";
440             # Force timestamp to match the source.
441 0 0       0 utime $when_src, $when_src, $dst or warn "$dst: Failed to utime: $!\n";
442 0 0       0 $self->_op(5) if $self->{_gentle_percent};
443 0         0 return;
444             }
445              
446 0 0       0 print "$src: Unimplemented weird type of file! Skipping...\n" if $self->{verbose};
447             }
448              
449             sub only {
450 0     0 1 0 my $self = shift;
451 0         0 push (@{ $self->{only} }, @_);
  0         0  
452             }
453              
454             sub maxskew {
455 0     0 1 0 my $self = shift;
456 0   0     0 $self->{maxskew} = shift || 0;
457             }
458              
459             sub dst {
460 0     0 1 0 my $self = shift;
461 0         0 $self->{dst} = shift;
462             }
463              
464             sub src {
465 0     0 1 0 my $self = shift;
466 0         0 $self->{src} = shift;
467             }
468              
469             sub ignore {
470 0     0 1 0 my $self = shift;
471 0   0     0 $self->{ignore} ||= {};
472             # Load ignore into a hash
473 0         0 foreach my $node (@_) {
474 0         0 $self->{ignore}->{$node} = 1;
475             }
476             }
477              
478             sub lockfile {
479 0     0 1 0 my $self = shift;
480 0 0       0 my $lockfile = shift or return;
481 0 0       0 open (LOCK, ">$lockfile") or return;
482 0 0       0 if (!flock(LOCK, 6)) { # (LOCK_EX | LOCK_NB)
483 0 0       0 print "Skipping due to concurrent process already running.\n" if $self->{verbose};
484 0         0 exit;
485             }
486             }
487              
488             sub verbose {
489 0     0 1 0 my $self = shift;
490 0 0       0 if (@_) {
491 0         0 $self->{verbose} = shift;
492             }
493 0         0 return $self->{verbose};
494             }
495              
496             sub localmode {
497 0     0 1 0 my $self = shift;
498 0 0       0 if (@_) {
499 0         0 $self->{localmode} = shift;
500             }
501 0         0 return $self->{localmode};
502             }
503              
504             sub nocache {
505 0     0 1 0 my $self = shift;
506 0 0       0 if (@_) {
507 0         0 $self->{nocache} = shift;
508             }
509 0         0 return $self->{nocache};
510             }
511              
512              
513             sub entries_updated {
514 0     0 1 0 my $self = shift;
515 0 0       0 return () unless ( ref $self->{_tracking} eq 'HASH' );
516 0         0 return @{ $self->{_tracking}->{updated} };
  0         0  
517             }
518              
519             sub entries_removed {
520 0     0 1 0 my $self = shift;
521 0 0       0 return () unless ( ref $self->{_tracking} eq 'HASH' );
522 0         0 return @{ $self->{_tracking}->{removed} };
  0         0  
523             }
524              
525             sub entries_skipped {
526 0     0 1 0 my $self = shift;
527 0 0       0 return () unless ( ref $self->{_tracking} eq 'HASH' );
528 0         0 return @{ $self->{_tracking}->{skipped} };
  0         0  
529             }
530              
531             sub entries_failed {
532 0     0 1 0 my $self = shift;
533 0 0       0 return () unless ( ref $self->{_tracking} eq 'HASH' );
534 0         0 return @{ $self->{_tracking}->{failed} };
  0         0  
535             }
536              
537             sub rmtree {
538 4     4 0 883 my $self = shift;
539 4         7 my $restore = {};
540 4         9 foreach my $node (@_) {
541 4 50       10 $self->_op if $self->{_gentle_percent};
542 4         58 my (undef,undef,$mode) = lstat $node;
543 4 100       11 if (-d _) {
544 2         3 my @files = ();
545 2 50       94 if (opendir my $d, $node) {
546 2         56 @files = readdir $d;
547 2         22 closedir $d;
548             } else {
549 0 0       0 unless ($mode & 0200) {
550             # Make directory writable
551 0 0       0 chmod 0777, $node or warn "$node: Failed to chmod: $!\n";
552 0 0       0 $self->_op(2) if $self->{_gentle_percent};
553             # Try to opendir one last time
554 0 0       0 if (opendir my $d, $node) {
555 0         0 @files = readdir $d;
556 0         0 closedir $d;
557             } else {
558 0         0 warn "$node: Failed to opendir: $!\n";
559             }
560             }
561             }
562 2         14 $self->rmtree( map { "$node/$_" } grep !/^\.\.?$/, @files );
  2         33  
563 2         162 rmdir $node;
564 2 50       15 $self->_op(3 + scalar(@files)) if $self->{_gentle_percent};
565             } else {
566 2 50 33     130 if (!unlink $node and lstat $node) {
567             # Tried to unlink it but it still exists
568 0         0 my $dir = $node;
569 0         0 $dir =~ s%[^/]*$%.%;
570 0         0 my (undef,undef, $dmode) = lstat $dir;
571 0 0       0 unless ($dmode & 0200) {
572             # Make directory writable
573 0 0       0 chmod 0777, $dir or warn "$dir: Failed to chmod: $!\n";
574 0 0       0 $self->_op if $self->{_gentle_percent};
575             }
576             # Try one last time to remove
577 0 0       0 unlink $node or warn "$node: Failed to unlink: $!\n";
578 0 0       0 $self->_op(4) if $self->{_gentle_percent};
579             # Don't forget to restore this guy back to how he was
580 0 0       0 $restore->{$dir} = $dmode & 07777 unless exists $restore->{$dir};
581             } else {
582 2 50       12 $self->_op if $self->{_gentle_percent};
583             }
584             }
585             }
586 4         7 foreach my $dir (keys %{ $restore }) {
  4         10  
587 0         0 chmod $restore->{$dir}, $dir;
588             }
589 4 50       10 $self->_op(1 + scalar(keys %{ $restore }) ) if $self->{_gentle_percent};
  0         0  
590 4   33     63 return $_[0] && !lstat $_[0];
591             }
592              
593             # Create copy of $src as $dst in one atomic operation.
594             # (The $dst file will never be partial.)
595             sub copy {
596 0     0 0   my $self = shift;
597 0           my $src = shift;
598 0           my $dst = shift;
599 0           my $temp_dst = $dst;
600 0           $temp_dst =~ s%/([^/]+)$%/.\#$1.dirsync.tmp%;
601 0           my $errno = 0;
602 0 0         if (sysopen FROM, $src, O_RDONLY) {
603 0 0         if (sysopen TO, $temp_dst, O_WRONLY | O_CREAT | O_EXCL, 0600) {
604 0           my $buffer;
605 0           while (sysread(FROM, $buffer, BUFSIZE)) {
606 0 0         $self->_op(BUFFER_OPS) if $self->{_gentle_percent};
607 0 0         if (!syswrite(TO, $buffer, length $buffer)) {
608 0           $errno = $!;
609 0           last;
610             }
611             }
612 0           close TO;
613             } else {
614 0           $errno = $!;
615             }
616 0           close FROM;
617             } else {
618 0           $errno = $!;
619             }
620             # XXX - Should we consider this operation as many thousands of ops?
621             # XXX - Depending on fs type, such as reiserfs, could this grind?
622 0 0 0       if (!$errno and !rename $temp_dst, $dst) {
623 0           $errno = $!;
624             }
625 0 0         $self->_op(6) if $self->{_gentle_percent};
626 0 0         if ($errno) {
627 0           unlink $temp_dst;
628 0           $! = $errno;
629 0           return undef;
630             }
631 0           return 1;
632             }
633              
634             1;
635             __END__