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   15168 use strict;
  5         31  
  5         168  
15 5     5   26 use warnings;
  5         12  
  5         473  
16             our $VERSION = "2.1";
17             our $REVISION = sprintf("%d.%02d", q$Revision: 1.15 $ =~ /(\d+)\.(\d+)/);
18              
19             #
20             # used modules
21             #
22              
23 5     5   1425 use Directory::Queue qw(_create _name _touch /Regexp/ /special/);
  5         15  
  5         29  
24 5     5   3871 use Encode qw(encode decode FB_CROAK LEAVE_SRC);
  5         44299  
  5         500  
25 5     5   45 use No::Worries::Die qw(dief);
  5         12  
  5         53  
26 5     5   2835 use No::Worries::File qw(file_read file_write);
  5         56226  
  5         46  
27 5     5   697 use No::Worries::Stat qw(ST_MTIME ST_NLINK);
  5         13  
  5         68  
28 5     5   3414 use No::Worries::Warn qw(warnf);
  5         4531  
  5         34  
29 5     5   535 use POSIX qw(:errno_h);
  5         13  
  5         39  
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   2156 use constant TEMPORARY_DIRECTORY => "temporary";
  5         13  
  5         509  
43              
44             # name of the directory holding obsolete elements
45 5     5   36 use constant OBSOLETE_DIRECTORY => "obsolete";
  5         10  
  5         264  
46              
47             # name of the directory indicating a locked element
48 5     5   33 use constant LOCKED_DIRECTORY => "locked";
  5         9  
  5         24063  
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   35 my($hash) = @_;
80 16         35 my($value, $string);
81              
82 16         30 $string = "";
83 16         27 foreach my $key (sort(keys(%{ $hash }))) {
  16         425  
84 28         59 $value = $hash->{$key};
85 28 50       66 dief("undefined hash value: %s", $key) unless defined($value);
86 28 50       63 dief("invalid hash scalar: %s", $value) if ref($value);
87 28         100 $key =~ s/([\x5c\x09\x0a])/$_Byte2Esc{$1}/g;
88 28         102 $value =~ s/([\x5c\x09\x0a])/$_Byte2Esc{$1}/g;
89 28         87 $string .= $key . "\x09" . $value . "\x0a";
90             }
91 16         71 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   74 my($stringref) = @_;
103 32         64 my($key, $value, %hash);
104              
105 32         58 foreach my $line (split(/\x0a/, ${ $stringref })) {
  32         199  
106 56 50       312 if ($line =~ /^([^\x09\x0a]*)\x09([^\x09\x0a]*)$/o) {
107 56         186 ($key, $value) = ($1, $2);
108             } else {
109 0         0 dief("unexpected hash line: %s", $line);
110             }
111 56         171 $key =~ s/(\\[\\tn])/$_Esc2Byte{$1}/g;
112 56         178 $value =~ s/(\\[\\tn])/$_Esc2Byte{$1}/g;
113 56         195 $hash{$key} = $value;
114             }
115 32         135 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   6 my($path, $time) = @_;
130 2         5 my(@stat);
131              
132 2         28 @stat = lstat($path);
133 2 50       8 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         12 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   95 my($path) = @_;
154 40         61 my(@stat);
155              
156 40         567 @stat = lstat($path);
157 40 50       151 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         191 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   176 my($element) = @_;
189              
190 95 50       709 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 5919 my($class, %option) = @_;
206 14         61 my($self, $path, $options);
207              
208             # default object
209 14         98 $self = __PACKAGE__->SUPER::_new(%option);
210 14         45 foreach my $name (qw(path maxlock maxtemp rndhex umask)) {
211 70         175 delete($option{$name});
212             }
213             # default options
214 14         91 $self->{maxelts} = 16_000; # maximum number of elements per directory
215             # check maxelts
216 14 100       49 if (defined($option{maxelts})) {
217             dief("invalid maxelts: %s", $option{maxelts})
218 1 50 33     12 unless $option{maxelts} =~ /^\d+$/ and $option{maxelts} > 0;
219 1         3 $self->{maxelts} = delete($option{maxelts});
220             }
221             # check schema
222 14 100       35 if (defined($option{schema})) {
223             dief("invalid schema: %s", $option{schema})
224 8 50       33 unless ref($option{schema}) eq "HASH";
225 8         16 foreach my $name (keys(%{ $option{schema} })) {
  8         33  
226 9 50 33     197 dief("invalid schema name: %s", $name)
227             unless $name =~ /^($_FileRegexp)$/
228             and $name ne LOCKED_DIRECTORY;
229 9 50       65 if ($option{schema}{$name} =~
230             /^(binary|string|table)([\?\*]{0,2})$/) {
231 9         38 $self->{type}{$name} = $1;
232 9         27 $options = $2;
233             } else {
234 0         0 dief("invalid schema type: %s", $option{schema}{$name});
235             }
236 9 100       37 $self->{mandatory}{$name} = 1 unless $options =~ /\?/;
237 9 100       39 $self->{ref}{$name} = 1 if $options =~ /\*/;
238             dief("invalid schema type: %s", $option{schema}{$name})
239 9 50 66     42 if $self->{type}{$name} eq "table" and $self->{ref}{$name};
240             }
241             dief("invalid schema: no mandatory data")
242 8 50       44 unless $self->{mandatory};
243 8         21 delete($option{schema});
244             }
245             # check unexpected options
246 14         40 foreach my $name (keys(%option)) {
247 0         0 dief("unexpected option: %s", $name);
248             }
249             # create directories
250 14         26 foreach my $name (TEMPORARY_DIRECTORY, OBSOLETE_DIRECTORY) {
251 28         100 $path = $self->{path}."/".$name;
252 28 100       492 _special_mkdir($path, $self->{umask}) unless -d $path;
253             }
254             # so far so good...
255 14         127 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 5535 my($self) = @_;
264 10         18 my($count, @list, $subdirs);
265              
266 10         20 $count = 0;
267             # get the list of existing directories
268 10         30 foreach my $name (_special_getdir($self->{path}, "strict")) {
269 33 100       187 push(@list, $1) if $name =~ /^($_DirectoryRegexp)$/o; # untaint
270             }
271             # count sub-directories
272 10         25 foreach my $name (@list) {
273 13         53 $subdirs = $_SubDirs->($self->{path}."/".$name);
274 13 100       39 $count += $subdirs if $subdirs;
275             }
276             # that's all
277 10         66 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   434 my($self, $name, $time) = @_;
288 58         93 my($path, @stat);
289              
290 58         160 $path = $self->{path}."/".$name;
291 58 100       1044 return(0) unless -d $path."/".LOCKED_DIRECTORY;
292 53 100       284 return(1) unless defined($time);
293 2         21 @stat = lstat($path);
294 2 50       7 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         10 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 4113 my($self, $element, $permissive) = @_;
320 37         59 my($path, $oldumask, $success);
321              
322 37         116 _check_element($element);
323 37 50       104 $permissive = 1 unless defined($permissive);
324 37         124 $path = $self->{path}."/".$element."/".LOCKED_DIRECTORY;
325 37 50       112 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         1963 $success = mkdir($path);
331             }
332 37 100       173 unless ($success) {
333 3 50       9 if ($permissive) {
334             # RACE: the locked directory already exists
335 3 50       30 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         132 $path = $self->{path}."/".$element;
343 34 50       453 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         179 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 27 my($self, $element, $permissive) = @_;
373 6         8 my($path);
374              
375 6         16 _check_element($element);
376 6         22 $path = $self->{path}."/".$element."/".LOCKED_DIRECTORY;
377 6 50       322 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         31 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 8 my($self, $element) = @_;
395              
396 1         7 _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 366 my($self, $element) = @_;
405 13         47 my($temp, $path);
406              
407 13         36 _check_element($element);
408 13 100       67 dief("cannot remove %s: not locked", $element)
409             unless _is_locked($self, $element);
410             # move the element out of its intermediate directory
411 12         39 $path = $self->{path}."/".$element;
412 12         21 while (1) {
413             $temp = $self->{path}
414             ."/".OBSOLETE_DIRECTORY
415 12         51 ."/"._name($self->{rndhex});
416 12 50       418 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         57 foreach my $name (_special_getdir($temp, "strict")) {
423 27 100       105 next if $name eq LOCKED_DIRECTORY;
424 15 50       123 if ($name =~ /^($_FileRegexp)$/o) {
425 15         63 $path = $temp."/".$1; # untaint
426             } else {
427 0         0 dief("unexpected file in %s: %s", $temp, $name);
428             }
429 15 50       686 unlink($path) and next;
430 0         0 dief("cannot unlink(%s): %s", $path, $!);
431             }
432             # remove the locked directory
433 12         49 $path = $temp."/".LOCKED_DIRECTORY;
434 12         22 while (1) {
435 12 50       430 rmdir($path) or dief("cannot rmdir(%s): %s", $path, $!);
436 12 50       415 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   10 my($path) = @_;
451 4         5 my($data);
452              
453 4         19 file_read($path, data => \$data);
454 4         607 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   82 my($path) = @_;
463 36         53 my($data, $string);
464              
465 36         195 file_read($path, data => \$data);
466 36         5896 eval {
467 36     0   339 local $SIG{__WARN__} = sub { die($_[0]) };
  0         0  
468 36         167 $string = decode("UTF-8", $data, FB_CROAK);
469             };
470 36 50       2575 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 43798 my($self, $element) = @_;
481 39         80 my(%data, $path, $ref);
482              
483 39 50       129 dief("unknown schema") unless $self->{type};
484 39         121 _check_element($element);
485 39 100       112 dief("cannot get %s: not locked", $element)
486             unless _is_locked($self, $element);
487 38         124 foreach my $name (keys(%{ $self->{type} })) {
  38         187  
488 40         134 $path = "$self->{path}/$element/$name";
489 40 50       460 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       248 if ($self->{type}{$name} =~ /^(binary|string)$/) {
    50          
498 8 100       33 if ($self->{type}{$name} eq "string") {
499 4         12 $ref = _file_read_utf8($path);
500             } else {
501 4         14 $ref = _file_read_bin($path);
502             }
503 8 100       29 $data{$name} = $self->{ref}{$name} ? $ref : ${ $ref };
  7         25  
504             } elsif ($self->{type}{$name} eq "table") {
505 32         93 $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       142 return(\%data) unless wantarray();
511 17         89 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   82 my($self) = @_;
523 28         109 my(@list, $new, $subdirs);
524              
525             # get the list of existing directories
526 28         121 foreach my $name (_special_getdir($self->{path}, "strict")) {
527 86 100       461 push(@list, $1) if $name =~ /^($_DirectoryRegexp)$/o; # untaint
528             }
529             # handle the case with no directories yet
530 28 100       92 unless (@list) {
531 3         8 $new = sprintf("%08x", 0);
532 3         25 _special_mkdir($self->{path}."/".$new, $self->{umask});
533 3         28 return($new);
534             }
535             # check the last directory
536 25         77 @list = sort(@list);
537 25         58 $new = pop(@list);
538 25         137 $subdirs = $_SubDirs->($self->{path}."/".$new);
539 25 50       84 if (defined($subdirs)) {
540 25 100       118 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         7 $new = sprintf("%08x", hex($new) + 1);
549 1         8 _special_mkdir($self->{path}."/".$new, $self->{umask});
550 1         6 return($new);
551             }
552              
553             #
554             # add data to a directory
555             #
556              
557             sub _add_data ($$$) {
558 29     29   96 my($self, $data, $tempdir) = @_;
559 29         56 my($ref, $utf8, $tmp, $path, $fh);
560              
561 29         52 foreach my $name (keys(%{ $data })) {
  29         138  
562 32 50       433 dief("unexpected data: %s", $name) unless $self->{type}{$name};
563 32 100       188 if ($self->{type}{$name} =~ /^(binary|string)$/) {
    50          
564 16 100       41 if ($self->{ref}{$name}) {
565             dief("unexpected %s data in %s: %s",
566             $self->{type}{$name}, $name, $data->{$name})
567 2 100       15 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       33 if ref($data->{$name});
573 14         32 $ref = \$data->{$name};
574             }
575 15         44 $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       63 unless ref($data->{$name}) eq "HASH";
580 16         61 $ref = _hash2string($data->{$name});
581 16         35 $utf8 = 1;
582             } else {
583             dief("unexpected data type in %s: %s",
584 0         0 $name, $self->{type}{$name});
585             }
586 31 100       80 if ($utf8) {
587 23         70 eval {
588 23         40 $tmp = encode("UTF-8", ${ $ref }, FB_CROAK|LEAVE_SRC);
  23         127  
589             };
590 23 50       1890 if ($@) {
591 0         0 $@ =~ s/\s+at\s.+?\sline\s+\d+\.?$//;
592 0         0 dief("unexpected character in %s: %s", $name, $@);
593             }
594 23         52 $ref = \$tmp;
595             }
596 31         85 $path = "$tempdir/$name";
597 31         148 $fh = _create($path, $self->{umask}, "strict");
598 31         165 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 40660 my($self, @data) = @_;
613 29         69 my($data, $tempdir, $dir, $new, $path, $ref, $utf8);
614              
615 29 50       103 dief("unknown schema") unless $self->{type};
616 29 100       88 if (@data == 1) {
617 10         24 $data = $data[0];
618             } else {
619 19         55 $data = { @data };
620             }
621 29         48 foreach my $name (keys(%{ $self->{mandatory} })) {
  29         111  
622             dief("missing mandatory data: %s", $name)
623 29 50       98 unless defined($data->{$name});
624             }
625 29         48 while (1) {
626             $tempdir = $self->{path}
627             ."/".TEMPORARY_DIRECTORY
628 29         165 ."/"._name($self->{rndhex});
629 29 50       129 last if _special_mkdir($tempdir, $self->{umask});
630             }
631 29         159 _add_data($self, $data, $tempdir);
632 28         3743 $dir = _insertion_directory($self);
633 28         70 while (1) {
634 28         128 $new = $dir."/"._name($self->{rndhex});
635 28         88 $path = $self->{path}."/".$new;
636 28 50       1368 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         5 my(@list);
650              
651 2         8 foreach my $name (_special_getdir($self->{path} .
652             "/" . TEMPORARY_DIRECTORY)) {
653 2 50       38 push(@list, TEMPORARY_DIRECTORY."/".$1)
654             if $name =~ /^($_ElementRegexp)$/o; # untaint
655             }
656 2         11 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         8 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 24 my($self, %option) = @_;
693 2         4 my(@list, $path, $subdirs, $oldtime, $locked);
694              
695             # check options
696 2 50       11 $option{maxtemp} = $self->{maxtemp} unless defined($option{maxtemp});
697 2 100       8 $option{maxlock} = $self->{maxlock} unless defined($option{maxlock});
698 2         6 foreach my $name (keys(%option)) {
699 4 50       21 dief("unexpected option: %s", $name)
700             unless $name =~ /^(maxtemp|maxlock)$/;
701             dief("invalid %s: %s", $name, $option{$name})
702 4 50       17 unless $option{$name} =~ /^\d+$/;
703             }
704             # get the list of intermediate directories
705 2         6 @list = ();
706 2         6 foreach my $name (_special_getdir($self->{path}, "strict")) {
707 8 100       62 push(@list, $1) if $name =~ /^($_DirectoryRegexp)$/o; # untaint
708             }
709             # try to purge all but the last intermediate directory
710 2 50       8 if (@list > 1) {
711 2         7 @list = sort(@list);
712 2         15 pop(@list);
713 2         6 foreach my $name (@list) {
714 2         9 $path = $self->{path}."/".$name;
715 2         6 $subdirs = $_SubDirs->($path);
716 2 100 66     14 next if $subdirs or not defined($subdirs);
717 1         6 _special_rmdir($path);
718             }
719             }
720             # remove the volatile directories which are too old
721 2 50       10 if ($option{maxtemp}) {
722 2         4 $oldtime = time() - $option{maxtemp};
723 2         7 foreach my $name (_volatile($self)) {
724 2         8 $path = $self->{path}."/".$name;
725 2 50       9 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       6 if ($option{maxlock}) {
732 2         4 $oldtime = time() - $option{maxlock};
733 2         8 $locked = $self->first();
734 2         10 while ($locked) {
735 3 100       8 next unless _is_locked($self, $locked, $oldtime);
736 1         7 warnf("removing too old locked element: %s", $locked);
737 1         58 $self->unlock($locked, 1);
738             } continue {
739 3         15 $locked = $self->next();
740             }
741             }
742             }
743              
744             1;
745              
746             __END__