File Coverage

blib/lib/Directory/Queue/Normal.pm
Criterion Covered Total %
statement 276 326 84.6
branch 104 182 57.1
condition 6 24 25.0
subroutine 32 35 91.4
pod 9 9 100.0
total 427 576 74.1


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