File Coverage

blib/lib/Directory/Queue/Normal.pm
Criterion Covered Total %
statement 276 329 83.8
branch 104 184 56.5
condition 6 24 25.0
subroutine 33 36 91.6
pod 9 9 100.0
total 428 582 73.5


line stmt bran cond sub pod time code
1             #+##############################################################################
2             # #
3             # File: Directory/Queue/Normal.pm #
4             # #
5             # Description: object oriented interface to a normal directory based queue #
6             # #
7             #-##############################################################################
8              
9             #
10             # module definition
11             #
12              
13             package Directory::Queue::Normal;
14 5     5   11383 use strict;
  5         17  
  5         121  
15 5     5   22 use warnings;
  5         9  
  5         336  
16             our $VERSION = "2.2";
17             our $REVISION = sprintf("%d.%02d", q$Revision: 1.16 $ =~ /(\d+)\.(\d+)/);
18              
19             #
20             # used modules
21             #
22              
23 5     5   1130 use Directory::Queue qw(_create _name _touch /Regexp/ /special/);
  5         12  
  5         24  
24 5     5   3044 use Encode qw(encode decode FB_CROAK LEAVE_SRC);
  5         34482  
  5         422  
25 5     5   31 use No::Worries::Die qw(dief);
  5         11  
  5         40  
26 5     5   2308 use No::Worries::File qw(file_read file_write);
  5         46535  
  5         32  
27 5     5   574 use No::Worries::Stat qw(ST_MTIME ST_NLINK);
  5         10  
  5         47  
28 5     5   2504 use No::Worries::Warn qw(warnf);
  5         3538  
  5         25  
29 5     5   409 use POSIX qw(:errno_h);
  5         12  
  5         46  
30              
31             #
32             # inheritance
33             #
34              
35             our(@ISA) = qw(Directory::Queue);
36              
37             #
38             # constants
39             #
40              
41             # name of the directory holding temporary elements
42 5     5   1578 use constant TEMPORARY_DIRECTORY => "temporary";
  5         10  
  5         384  
43              
44             # name of the directory holding obsolete elements
45 5     5   31 use constant OBSOLETE_DIRECTORY => "obsolete";
  5         8  
  5         217  
46              
47             # name of the directory indicating a locked element
48 5     5   36 use constant LOCKED_DIRECTORY => "locked";
  5         7  
  5         19623  
49              
50             #
51             # global variables
52             #
53              
54             our(
55             $_FileRegexp, # regexp matching a file in an element directory
56             %_Byte2Esc, # byte to escape map
57             %_Esc2Byte, # escape to byte map
58             );
59              
60             $_FileRegexp = qr/[0-9a-zA-Z]+/;
61             %_Byte2Esc = ("\x5c" => "\\\\", "\x09" => "\\t", "\x0a" => "\\n");
62             %_Esc2Byte = reverse(%_Byte2Esc);
63              
64             #+++############################################################################
65             # #
66             # Helper Functions #
67             # #
68             #---############################################################################
69              
70             #
71             # transform a hash of strings into a string (reference)
72             #
73             # note:
74             # - the keys are sorted so that identical hashes yield to identical strings
75             #
76              
77             sub _hash2string ($) {
78 16     16   23 my($hash) = @_;
79 16         20 my($value, $string);
80              
81 16         20 $string = "";
82 16         20 foreach my $key (sort(keys(%{ $hash }))) {
  16         48  
83 28         43 $value = $hash->{$key};
84 28 50       44 dief("undefined hash value: %s", $key) unless defined($value);
85 28 50       41 dief("invalid hash scalar: %s", $value) if ref($value);
86 28         64 $key =~ s/([\x5c\x09\x0a])/$_Byte2Esc{$1}/g;
87 28         72 $value =~ s/([\x5c\x09\x0a])/$_Byte2Esc{$1}/g;
88 28         77 $string .= $key . "\x09" . $value . "\x0a";
89             }
90 16         63 return(\$string);
91             }
92              
93             #
94             # transform a string (reference) into a hash of strings
95             #
96             # note:
97             # - duplicate keys are not checked (the last one wins)
98             #
99              
100             sub _string2hash ($) {
101 32     32   44 my($stringref) = @_;
102 32         43 my($key, $value, %hash);
103              
104 32         39 foreach my $line (split(/\x0a/, ${ $stringref })) {
  32         118  
105 56 50       220 if ($line =~ /^([^\x09\x0a]*)\x09([^\x09\x0a]*)$/o) {
106 56         147 ($key, $value) = ($1, $2);
107             } else {
108 0         0 dief("unexpected hash line: %s", $line);
109             }
110 56         111 $key =~ s/(\\[\\tn])/$_Esc2Byte{$1}/g;
111 56         140 $value =~ s/(\\[\\tn])/$_Esc2Byte{$1}/g;
112 56         137 $hash{$key} = $value;
113             }
114 32         100 return(\%hash);
115             }
116              
117             #
118             # check if a path is old enough:
119             # - return true if the path exists and is (strictly) older than the given time
120             # - return false if it does not exist or it is newer
121             # - die in case of any other error
122             #
123             # note:
124             # - lstat() is used so symlinks are not followed
125             #
126              
127             sub _older ($$) {
128 2     2   7 my($path, $time) = @_;
129 2         3 my(@stat);
130              
131 2         23 @stat = lstat($path);
132 2 50       8 unless (@stat) {
133 0 0       0 dief("cannot lstat(%s): %s", $path, $!) unless $! == ENOENT;
134             # RACE: this path does not exist (anymore)
135 0         0 return(0);
136             }
137 2         11 return($stat[ST_MTIME] < $time);
138             }
139              
140             #
141             # count the number of sub-directories in the given directory:
142             # - return undef if the directory does not exist (anymore)
143             # - die in case of any other error
144             #
145              
146             # stat version (faster):
147             # - lstat() is used so symlinks are not followed
148             # - this only checks the number of hard links
149             # - we do not even check that the given path indeed points to a directory!
150             # - this will return incorrect results on some filesystems like DOS or Btrfs
151              
152             sub _subdirs_stat ($) {
153 0     0   0 my($path) = @_;
154 0         0 my(@stat);
155              
156 0         0 @stat = lstat($path);
157 0 0       0 unless (@stat) {
158 0 0       0 dief("cannot lstat(%s): %s", $path, $!) unless $! == ENOENT;
159             # RACE: this path does not exist (anymore)
160 0         0 return();
161             }
162 0         0 return($stat[ST_NLINK] - 2);
163             }
164              
165             # readdir version (slower):
166             # - we really count the number of entries
167             # - we however do not check that these entries are themselves indeed directories
168             # - this is the default method to favor correctness over speed
169              
170             sub _subdirs_readdir ($) {
171 40     40   63 my($path) = @_;
172              
173 40         81 return(scalar(_special_getdir($path)));
174             }
175              
176             #
177             # wrapper method
178             #
179              
180             sub _subdirs ($$) {
181 40     40   76 my($self, $path) = @_;
182              
183 40 50       98 return($self->{nlink} ? _subdirs_stat($path) : _subdirs_readdir($path));
184             }
185              
186             #
187             # check the given string to make sure it represents a valid element name
188             #
189              
190             sub _check_element ($) {
191 95     95   128 my($element) = @_;
192              
193 95 50       499 dief("invalid element: %s", $element)
194             unless $element =~ m/^(?:$_DirectoryRegexp)\/(?:$_ElementRegexp)$/o;
195             }
196              
197             #+++############################################################################
198             # #
199             # Object Oriented Interface #
200             # #
201             #---############################################################################
202              
203             #
204             # object constructor
205             #
206              
207             sub new : method {
208 14     14 1 4869 my($class, %option) = @_;
209 14         37 my($self, $path, $options);
210              
211             # default object
212 14         74 $self = __PACKAGE__->SUPER::_new(%option);
213 14         35 foreach my $name (qw(path maxlock maxtemp rndhex umask)) {
214 70         102 delete($option{$name});
215             }
216             # default options
217 14         57 $self->{maxelts} = 16_000; # maximum number of elements per directory
218             # check maxelts
219 14 100       52 if (defined($option{maxelts})) {
220             dief("invalid maxelts: %s", $option{maxelts})
221 1 50 33     10 unless $option{maxelts} =~ /^\d+$/ and $option{maxelts} > 0;
222 1         3 $self->{maxelts} = delete($option{maxelts});
223             }
224             # check nlink
225 14         44 $self->{nlink} = delete($option{nlink});
226             # check schema
227 14 100       31 if (defined($option{schema})) {
228             dief("invalid schema: %s", $option{schema})
229 8 50       25 unless ref($option{schema}) eq "HASH";
230 8         13 foreach my $name (keys(%{ $option{schema} })) {
  8         26  
231 9 50 33     158 dief("invalid schema name: %s", $name)
232             unless $name =~ /^($_FileRegexp)$/
233             and $name ne LOCKED_DIRECTORY;
234 9 50       51 if ($option{schema}{$name} =~
235             /^(binary|string|table)([\?\*]{0,2})$/) {
236 9         35 $self->{type}{$name} = $1;
237 9         16 $options = $2;
238             } else {
239 0         0 dief("invalid schema type: %s", $option{schema}{$name});
240             }
241 9 100       31 $self->{mandatory}{$name} = 1 unless $options =~ /\?/;
242 9 100       31 $self->{ref}{$name} = 1 if $options =~ /\*/;
243             dief("invalid schema type: %s", $option{schema}{$name})
244 9 50 66     35 if $self->{type}{$name} eq "table" and $self->{ref}{$name};
245             }
246             dief("invalid schema: no mandatory data")
247 8 50       21 unless $self->{mandatory};
248 8         15 delete($option{schema});
249             }
250             # check unexpected options
251 14         28 foreach my $name (keys(%option)) {
252 0         0 dief("unexpected option: %s", $name);
253             }
254             # create directories
255 14         54 foreach my $name (TEMPORARY_DIRECTORY, OBSOLETE_DIRECTORY) {
256 28         79 $path = $self->{path}."/".$name;
257 28 100       379 _special_mkdir($path, $self->{umask}) unless -d $path;
258             }
259             # so far so good...
260 14         102 return($self);
261             }
262              
263             #
264             # return the number of elements in the queue, regardless of their state
265             #
266              
267             sub count : method {
268 10     10 1 4224 my($self) = @_;
269 10         14 my($count, @list, $subdirs);
270              
271 10         16 $count = 0;
272             # get the list of existing directories
273 10         27 foreach my $name (_special_getdir($self->{path}, "strict")) {
274 33 100       156 push(@list, $1) if $name =~ /^($_DirectoryRegexp)$/o; # untaint
275             }
276             # count sub-directories
277 10         24 foreach my $name (@list) {
278 13         43 $subdirs = _subdirs($self, $self->{path}."/".$name);
279 13 100       35 $count += $subdirs if $subdirs;
280             }
281             # that's all
282 10         58 return($count);
283             }
284              
285             #
286             # check if an element is locked:
287             # - this is best effort only as it may change while we test (only locking is atomic)
288             # - if given a time, only return true on locks older than this time (needed by purge)
289             #
290              
291             sub _is_locked ($$;$) {
292 58     58   374 my($self, $name, $time) = @_;
293 58         90 my($path, @stat);
294              
295 58         125 $path = $self->{path}."/".$name;
296 58 100       692 return(0) unless -d $path."/".LOCKED_DIRECTORY;
297 53 100       207 return(1) unless defined($time);
298 2         18 @stat = lstat($path);
299 2 50       8 unless (@stat) {
300 0 0       0 dief("cannot lstat(%s): %s", $path, $!) unless $! == ENOENT;
301             # RACE: this path does not exist (anymore)
302 0         0 return(0);
303             }
304 2         10 return($stat[ST_MTIME] < $time);
305             }
306              
307             #
308             # lock an element:
309             # - return true on success
310             # - return false in case the element could not be locked (in permissive mode)
311             #
312             # note:
313             # - locking can fail:
314             # - if the element has been locked by somebody else (EEXIST)
315             # - if the element has been removed by somebody else (ENOENT)
316             # - if the optional second argument is true, it is not an error if
317             # the element cannot be locked (= permissive mode), this is the default
318             # as one usually cannot be sure that nobody else will try to lock it
319             # - the directory's mtime will change automatically (after a successful mkdir()),
320             # this will later be used to detect stalled locks
321             #
322              
323             sub lock : method { ## no critic 'ProhibitBuiltinHomonyms'
324 37     37 1 3101 my($self, $element, $permissive) = @_;
325 37         49 my($path, $oldumask, $success);
326              
327 37         81 _check_element($element);
328 37 50       79 $permissive = 1 unless defined($permissive);
329 37         84 $path = $self->{path}."/".$element."/".LOCKED_DIRECTORY;
330 37 50       83 if (defined($self->{umask})) {
331 0         0 $oldumask = umask($self->{umask});
332 0         0 $success = mkdir($path);
333 0         0 umask($oldumask);
334             } else {
335 37         1587 $success = mkdir($path);
336             }
337 37 100       139 unless ($success) {
338 3 50       7 if ($permissive) {
339             # RACE: the locked directory already exists
340 3 50       24 return(0) if $! == EEXIST;
341             # RACE: the element directory does not exist anymore
342 0 0       0 return(0) if $! == ENOENT;
343             }
344             # otherwise this is unexpected...
345 0         0 dief("cannot mkdir(%s): %s", $path, $!);
346             }
347 34         98 $path = $self->{path}."/".$element;
348 34 50       346 unless (lstat($path)) {
349 0 0 0     0 if ($permissive and $! == ENOENT) {
350             # RACE: the element directory does not exist anymore
351             # (this can happen if an other process locked & removed the element
352             # while our mkdir() was in progress... yes, this can happen!)
353 0         0 return(0);
354             }
355             # otherwise this is unexpected...
356 0         0 dief("cannot lstat(%s): %s", $path, $!);
357             }
358             # so far so good
359 34         135 return(1);
360             }
361              
362             #
363             # unlock an element:
364             # - return true on success
365             # - return false in case the element could not be unlocked (in permissive mode)
366             #
367             # note:
368             # - unlocking can fail:
369             # - if the element has been unlocked by somebody else (ENOENT)
370             # - if the element has been removed by somebody else (ENOENT)
371             # - if the optional second argument is true, it is not an error if
372             # the element cannot be unlocked (= permissive mode), this is _not_ the default
373             # as unlock() should normally be called by whoever locked the element
374             #
375              
376             sub unlock : method {
377 6     6 1 22 my($self, $element, $permissive) = @_;
378 6         7 my($path);
379              
380 6         13 _check_element($element);
381 6         16 $path = $self->{path}."/".$element."/".LOCKED_DIRECTORY;
382 6 50       239 unless (rmdir($path)) {
383 0 0       0 if ($permissive) {
384             # RACE: the element directory or its lock does not exist anymore
385 0 0       0 return(0) if $! == ENOENT;
386             }
387             # otherwise this is unexpected...
388 0         0 dief("cannot rmdir(%s): %s", $path, $!);
389             }
390             # so far so good
391 6         27 return(1);
392             }
393              
394             #
395             # touch an element to indicate that it is still being used
396             #
397              
398             sub touch : method {
399 1     1 1 10 my($self, $element) = @_;
400              
401 1         6 _touch($self->{"path"}."/".$element);
402             }
403              
404             #
405             # remove a locked element from the queue
406             #
407              
408             sub remove : method {
409 13     13 1 304 my($self, $element) = @_;
410 13         20 my($temp, $path);
411              
412 13         26 _check_element($element);
413 13 100       53 dief("cannot remove %s: not locked", $element)
414             unless _is_locked($self, $element);
415             # move the element out of its intermediate directory
416 12         36 $path = $self->{path}."/".$element;
417 12         17 while (1) {
418             $temp = $self->{path}
419             ."/".OBSOLETE_DIRECTORY
420 12         40 ."/"._name($self->{rndhex});
421 12 50       359 rename($path, $temp) and last;
422 0 0 0     0 dief("cannot rename(%s, %s): %s", $path, $temp, $!)
423             unless $! == ENOTEMPTY or $! == EEXIST;
424             # RACE: the target directory was already present...
425             }
426             # remove the data files
427 12         46 foreach my $name (_special_getdir($temp, "strict")) {
428 27 100       89 next if $name eq LOCKED_DIRECTORY;
429 15 50       99 if ($name =~ /^($_FileRegexp)$/o) {
430 15         49 $path = $temp."/".$1; # untaint
431             } else {
432 0         0 dief("unexpected file in %s: %s", $temp, $name);
433             }
434 15 50       563 unlink($path) and next;
435 0         0 dief("cannot unlink(%s): %s", $path, $!);
436             }
437             # remove the locked directory
438 12         36 $path = $temp."/".LOCKED_DIRECTORY;
439 12         17 while (1) {
440 12 50       366 rmdir($path) or dief("cannot rmdir(%s): %s", $path, $!);
441 12 50       335 rmdir($temp) and return;
442 0 0 0     0 dief("cannot rmdir(%s): %s", $temp, $!)
443             unless $! == ENOTEMPTY or $! == EEXIST;
444             # RACE: this can happen if an other process managed to lock this element
445             # while it was being removed (see the comment in the lock() method)
446             # so we try to remove the lock again and again...
447             }
448             }
449              
450             #
451             # read a binary file and return a reference to the corresponding data
452             #
453              
454             sub _file_read_bin ($) {
455 4     4   8 my($path) = @_;
456 4         6 my($data);
457              
458 4         15 file_read($path, data => \$data);
459 4         480 return(\$data);
460             }
461              
462             #
463             # read a UTF-8 encoded file and return a reference to the corresponding string
464             #
465              
466             sub _file_read_utf8 ($) {
467 36     36   60 my($path) = @_;
468 36         44 my($data, $string);
469              
470 36         122 file_read($path, data => \$data);
471 36         4082 eval {
472 36     0   249 local $SIG{__WARN__} = sub { die($_[0]) };
  0         0  
473 36         118 $string = decode("UTF-8", $data, FB_CROAK);
474             };
475 36 50       1819 return(\$string) unless $@;
476 0         0 $@ =~ s/\s+at\s.+?\sline\s+\d+\.?$//;
477 0         0 dief("cannot UTF-8 decode %s: %s", $path, $@);
478             }
479              
480             #
481             # get an element from a locked element
482             #
483              
484             sub get : method {
485 39     39 1 29383 my($self, $element) = @_;
486 39         54 my(%data, $path, $ref);
487              
488 39 50       87 dief("unknown schema") unless $self->{type};
489 39         83 _check_element($element);
490 39 100       77 dief("cannot get %s: not locked", $element)
491             unless _is_locked($self, $element);
492 38         60 foreach my $name (keys(%{ $self->{type} })) {
  38         123  
493 40         88 $path = "$self->{path}/$element/$name";
494 40 50       338 unless (lstat($path)) {
495 0 0       0 dief("cannot lstat(%s): %s", $path, $!) unless $! == ENOENT;
496 0 0       0 if ($self->{mandatory}{$name}) {
497 0         0 dief("missing data file: %s", $path);
498             } else {
499 0         0 next;
500             }
501             }
502 40 100       165 if ($self->{type}{$name} =~ /^(binary|string)$/) {
    50          
503 8 100       20 if ($self->{type}{$name} eq "string") {
504 4         9 $ref = _file_read_utf8($path);
505             } else {
506 4         11 $ref = _file_read_bin($path);
507             }
508 8 100       23 $data{$name} = $self->{ref}{$name} ? $ref : ${ $ref };
  7         19  
509             } elsif ($self->{type}{$name} eq "table") {
510 32         65 $data{$name} = _string2hash(_file_read_utf8($path));
511             } else {
512 0         0 dief("unexpected data type: %s", $self->{type}{$name});
513             }
514             }
515 38 100       105 return(\%data) unless wantarray();
516 17         72 return(%data);
517             }
518              
519             #
520             # return the name of the intermediate directory that can be used for insertion:
521             # - if there is none, an initial one will be created
522             # - if it is full, a new one will be created
523             # - in any case the name will match $_DirectoryRegexp
524             #
525              
526             sub _insertion_directory ($) {
527 28     28   52 my($self) = @_;
528 28         40 my(@list, $new, $subdirs);
529              
530             # get the list of existing directories
531 28         86 foreach my $name (_special_getdir($self->{path}, "strict")) {
532 86 100       386 push(@list, $1) if $name =~ /^($_DirectoryRegexp)$/o; # untaint
533             }
534             # handle the case with no directories yet
535 28 100       65 unless (@list) {
536 3         8 $new = sprintf("%08x", 0);
537 3         22 _special_mkdir($self->{path}."/".$new, $self->{umask});
538 3         15 return($new);
539             }
540             # check the last directory
541 25         62 @list = sort(@list);
542 25         37 $new = pop(@list);
543 25         90 $subdirs = _subdirs($self, $self->{path}."/".$new);
544 25 50       69 if (defined($subdirs)) {
545 25 100       111 return($new) if $subdirs < $self->{maxelts};
546             # this last directory is now full... create a new one
547             } else {
548             # RACE: at this point, the directory does not exist anymore, so it
549             # must have been purged after we listed the directory contents...
550             # we do not try to do more and simply create a new directory
551             }
552             # we need a new directory
553 1         6 $new = sprintf("%08x", hex($new) + 1);
554 1         8 _special_mkdir($self->{path}."/".$new, $self->{umask});
555 1         4 return($new);
556             }
557              
558             #
559             # add data to a directory
560             #
561              
562             sub _add_data ($$$) {
563 29     29   66 my($self, $data, $tempdir) = @_;
564 29         41 my($ref, $utf8, $tmp, $path, $fh);
565              
566 29         38 foreach my $name (keys(%{ $data })) {
  29         100  
567 32 50       326 dief("unexpected data: %s", $name) unless $self->{type}{$name};
568 32 100       137 if ($self->{type}{$name} =~ /^(binary|string)$/) {
    50          
569 16 100       36 if ($self->{ref}{$name}) {
570             dief("unexpected %s data in %s: %s",
571             $self->{type}{$name}, $name, $data->{$name})
572 2 100       15 unless ref($data->{$name}) eq "SCALAR";
573 1         4 $ref = $data->{$name};
574             } else {
575             dief("unexpected %s data in %s: %s",
576             $self->{type}{$name}, $name, $data->{$name})
577 14 50       29 if ref($data->{$name});
578 14         24 $ref = \$data->{$name};
579             }
580 15         32 $utf8 = $self->{type}{$name} eq "string";
581             } elsif ($self->{type}{$name} eq "table") {
582             dief("unexpected %s data in %s: %s",
583             $self->{type}{$name}, $name, $data->{$name})
584 16 50       37 unless ref($data->{$name}) eq "HASH";
585 16         35 $ref = _hash2string($data->{$name});
586 16         39 $utf8 = 1;
587             } else {
588             dief("unexpected data type in %s: %s",
589 0         0 $name, $self->{type}{$name});
590             }
591 31 100       54 if ($utf8) {
592 23         33 eval {
593 23         55 $tmp = encode("UTF-8", ${ $ref }, FB_CROAK|LEAVE_SRC);
  23         70  
594             };
595 23 50       1422 if ($@) {
596 0         0 $@ =~ s/\s+at\s.+?\sline\s+\d+\.?$//;
597 0         0 dief("unexpected character in %s: %s", $name, $@);
598             }
599 23         37 $ref = \$tmp;
600             }
601 31         61 $path = "$tempdir/$name";
602 31         106 $fh = _create($path, $self->{umask}, "strict");
603 31         112 file_write($path, handle => $fh, data => $ref);
604             }
605             }
606              
607             #
608             # add a new element to the queue and return its name
609             #
610             # note:
611             # - the destination directory must _not_ be created beforehand as it would
612             # be seen as a valid (but empty) element directory by an other process,
613             # we therefor use rename() from a temporary directory
614             #
615              
616             sub add : method {
617 29     29 1 27710 my($self, @data) = @_;
618 29         45 my($data, $tempdir, $dir, $new, $path, $ref, $utf8);
619              
620 29 50       77 dief("unknown schema") unless $self->{type};
621 29 100       67 if (@data == 1) {
622 10         13 $data = $data[0];
623             } else {
624 19         41 $data = { @data };
625             }
626 29         37 foreach my $name (keys(%{ $self->{mandatory} })) {
  29         82  
627             dief("missing mandatory data: %s", $name)
628 29 50       67 unless defined($data->{$name});
629             }
630 29         36 while (1) {
631             $tempdir = $self->{path}
632             ."/".TEMPORARY_DIRECTORY
633 29         104 ."/"._name($self->{rndhex});
634 29 50       92 last if _special_mkdir($tempdir, $self->{umask});
635             }
636 29         120 _add_data($self, $data, $tempdir);
637 28         2579 $dir = _insertion_directory($self);
638 28         56 while (1) {
639 28         93 $new = $dir."/"._name($self->{rndhex});
640 28         62 $path = $self->{path}."/".$new;
641 28 50       1048 rename($tempdir, $path) and return($new);
642 0 0 0     0 dief("cannot rename(%s, %s): %s", $tempdir, $path, $!)
643             unless $! == ENOTEMPTY or $! == EEXIST;
644             # RACE: the target directory was already present...
645             }
646             }
647              
648             #
649             # return the list of volatile (i.e. temporary or obsolete) directories
650             #
651              
652             sub _volatile ($) {
653 2     2   3 my($self) = @_;
654 2         4 my(@list);
655              
656 2         9 foreach my $name (_special_getdir($self->{path} .
657             "/" . TEMPORARY_DIRECTORY)) {
658 2 50       37 push(@list, TEMPORARY_DIRECTORY."/".$1)
659             if $name =~ /^($_ElementRegexp)$/o; # untaint
660             }
661 2         9 foreach my $name (_special_getdir($self->{path} .
662             "/" . OBSOLETE_DIRECTORY)) {
663 0 0       0 push(@list, OBSOLETE_DIRECTORY."/".$1)
664             if $name =~ /^($_ElementRegexp)$/o; # untaint
665             }
666 2         8 return(@list);
667             }
668              
669             #
670             # destroy a volatile directory
671             #
672              
673             sub _destroy_dir ($) {
674 0     0   0 my($dir) = @_;
675 0         0 my($path);
676              
677 0         0 foreach my $name (_special_getdir($dir)) {
678 0 0       0 next if $name eq LOCKED_DIRECTORY;
679 0         0 $path = $dir."/".$name;
680 0 0       0 unlink($path) and next;
681 0 0       0 dief("cannot unlink(%s): %s", $path, $!) unless $! == ENOENT;
682             }
683 0         0 _special_rmdir($dir."/".LOCKED_DIRECTORY);
684 0         0 _special_rmdir($dir);
685             }
686              
687             #
688             # purge the queue:
689             # - delete unused intermediate directories
690             # - delete too old temporary directories
691             # - unlock too old locked directories
692             #
693             # note: this uses first()/next() to iterate so this will reset the cursor
694             #
695              
696             sub purge : method {
697 2     2 1 21 my($self, %option) = @_;
698 2         5 my(@list, $path, $subdirs, $oldtime, $locked);
699              
700             # check options
701 2 50       8 $option{maxtemp} = $self->{maxtemp} unless defined($option{maxtemp});
702 2 100       8 $option{maxlock} = $self->{maxlock} unless defined($option{maxlock});
703 2         7 foreach my $name (keys(%option)) {
704 4 50       18 dief("unexpected option: %s", $name)
705             unless $name =~ /^(maxtemp|maxlock)$/;
706             dief("invalid %s: %s", $name, $option{$name})
707 4 50       16 unless $option{$name} =~ /^\d+$/;
708             }
709             # get the list of intermediate directories
710 2         5 @list = ();
711 2         7 foreach my $name (_special_getdir($self->{path}, "strict")) {
712 8 100       48 push(@list, $1) if $name =~ /^($_DirectoryRegexp)$/o; # untaint
713             }
714             # try to purge all but the last intermediate directory
715 2 50       10 if (@list > 1) {
716 2         7 @list = sort(@list);
717 2         3 pop(@list);
718 2         5 foreach my $name (@list) {
719 2         14 $path = $self->{path}."/".$name;
720 2         7 $subdirs = _subdirs($self, $path);
721 2 100 66     12 next if $subdirs or not defined($subdirs);
722 1         4 _special_rmdir($path);
723             }
724             }
725             # remove the volatile directories which are too old
726 2 50       6 if ($option{maxtemp}) {
727 2         5 $oldtime = time() - $option{maxtemp};
728 2         6 foreach my $name (_volatile($self)) {
729 2         8 $path = $self->{path}."/".$name;
730 2 50       6 next unless _older($path, $oldtime);
731 0         0 warnf("removing too old volatile element: %s", $name);
732 0         0 _destroy_dir($path);
733             }
734             }
735             # iterate to find abandoned locked entries
736 2 50       6 if ($option{maxlock}) {
737 2         3 $oldtime = time() - $option{maxlock};
738 2         10 $locked = $self->first();
739 2         10 while ($locked) {
740 3 100       6 next unless _is_locked($self, $locked, $oldtime);
741 1         19 warnf("removing too old locked element: %s", $locked);
742 1         48 $self->unlock($locked, 1);
743             } continue {
744 3         10 $locked = $self->next();
745             }
746             }
747             }
748              
749             1;
750              
751             __END__