File Coverage

lib/Cache/Static.pm
Criterion Covered Total %
statement 217 306 70.9
branch 83 166 50.0
condition 10 27 37.0
subroutine 27 33 81.8
pod 0 11 0.0
total 337 543 62.0


line stmt bran cond sub pod time code
1             ##
2             #
3             # Copyright 2005-2006, Brian Szymanski
4             #
5             # This file is part of Cache::Static
6             #
7             # Cache::Static is free software; you can redistribute it and/or modify
8             # it under the terms of the GNU General Public License as published by
9             # the Free Software Foundation; either version 2 of the License, or
10             # any later version.
11             #
12             # This program is distributed in the hope that it will be useful,
13             # but WITHOUT ANY WARRANTY; without even the implied warranty of
14             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15             # GNU General Public License for more details.
16             #
17             # For more information about Cache::Static, point a web browser at
18             # http://chronicle.allafrica.com/scache/ or read the
19             # documentation included with the Cache::Static distribution in the
20             # doc/ directory
21             #
22             ##
23              
24             package Cache::Static;
25             our $VERSION = '0.9905';
26              
27 6     6   20650 use strict;
  6         14  
  6         201  
28 6     6   32 use warnings;
  6         9  
  6         198  
29              
30 6     6   372431 use Storable;
  6         35502  
  6         610  
31 6     6   63 use Digest::MD5 qw(md5_base64);
  6         39  
  6         4096  
32             #allow serialization of code refs
33             $Storable::Deparse = 1;
34              
35             our $ROOT = '/usr/local/Cache-Static';
36             our $LOGFILE = "$ROOT/log";
37             our $namespace = 'DEFAULT';
38              
39             ### LOG LEVELS:
40             #0 - no output
41             #1 - just hit/miss stats
42             #2 - hit/miss stats and critical errors (production)
43             #3 - his or miss and most error messages (development)
44             #4 - hit or miss and verbose error messages (debugging)
45             my @LOG_LEVEL_NAMES = qw ( NONE STAT CRIT WARN DEBUG );
46             ### /LOG LEVELS
47             my @ILLEGAL_NAMESPACES = qw ( config log timestamps log_level );
48              
49             #used to use a different root directory (used in TEST.pm)
50             sub _rebase {
51 6     6   1985 my $base = shift;
52 6         18 $ROOT = $base;
53 6         23 $LOGFILE = "$ROOT/log";
54 6         31 _mkdir_p("$ROOT/DEFAULT/tmp");
55 6 50       86 die "couldn't create DEFAULT namespace tmp directory: $@" if($@);
56             }
57              
58             #fill %conf with some sane defaults
59             my %CONF = (
60             DEFAULT => {
61             dep_file_not_found_returns => 0,
62             unrecognized_dependency_returns => 0,
63             recursive_unlink => 0,
64             },
65             log_level => 3
66             );
67              
68             #create the tmp directory for the default namespace
69             _mkdir_p("$ROOT/DEFAULT/tmp");
70             die "couldn't create DEFAULT namespace tmp directory: $@" if($@);
71             #create the timestamp directory if it doesn't exist
72             _mkdir_p("$ROOT/timestamps");
73             die "couldn't create timestamp directory: $@" if($@);
74              
75             #read the global config
76             _readconf();
77             _log(3, "conf -- global config --");
78             _print_config();
79              
80             sub _print_config {
81 6     6   175 foreach my $c (keys %CONF) {
82 12 100       253 if(ref($CONF{$c})) {
83 6         75 foreach my $cc (keys %{$CONF{$c}}) {
  6         130  
84 18         616 _log(3, "conf($c): $cc = ".$CONF{$c}->{$cc});
85             }
86             } else {
87 6         171 _log(3, "conf: $c = ".$CONF{$c});
88             }
89             }
90             }
91              
92             #make sure the DEFAULT namespace's directories are there - we don't
93             #call init for these...
94             _mkdir_p("$ROOT/DEFAULT/tmp");
95             die "couldn't create DEFAULT namespace tmp directory: $@" if($@);
96              
97             #just set the default namespace
98             sub init {
99 6     6 0 48 _die_if_invalid_namespace($_[0]);
100 6         27 $namespace = shift;
101             #
102             # _mkdir_p("$ROOT/$namespace/tmp");
103             # die "couldn't make/walk tmp directory: $ROOT/$namespace/tmp: $@" if($@);
104             #
105             # #override conf with namespace-specific values
106             # _readconf("$namespace") unless(defined($CONF{$namespace}));
107             #
108             # _log(3, "conf --init--");
109             # _print_config();
110             }
111              
112             #determine whether we have fcntl and can use locking for native perl
113             #log writes (if not we fall back to invoking echo, which is slower and
114             #more error prone)
115             my $have_fcntl;
116             eval {
117 6     6   46 use Fcntl ':flock';
  6         12  
  6         1573  
118             $have_fcntl = 1;
119             }; if($@) {
120             $have_fcntl = 0;
121             }
122              
123             ###########################
124             ### glue for extensions ###
125             ###########################
126 6     6   4347 use Cache::Static::Configuration;
  6         24  
  6         325  
127             sub get_configuration_data {
128 6     6   37 no strict 'refs';
  6         16  
  6         48145  
129 6     6 0 26 my $fh = *{ "Cache::Static::Configuration::DATA" };
  6         144  
130 6         187 my $block = join ( '', <$fh> );
131 6         1657 my $conf = eval "{ $block }";
132 6         60 return $conf->{$_[0]};
133             }
134              
135             sub find_intersection {
136 12     12 0 31 my ($ref1, $ref2) = @_;
137 12         21 my (%h, @ret);
138 12         42 foreach my $i (@$ref1, @$ref2) { $h{$i}++; };
  42         361  
139 12         43 foreach my $e (keys %h) {
140 30 100       102 push @ret, $e if($h{$e} == 2);
141             }
142 12         61 return @ret;
143             }
144              
145             my @enabled_extensions = @{get_configuration_data("extensions")};
146             sub is_enabled {
147 2     2 0 15 my $module = shift;
148 2         50 return grep(/^$module$/i, @enabled_extensions);
149             }
150              
151             my @POSSIBLE_HELPER_EXTENSIONS = find_intersection(\@enabled_extensions,
152             [ qw ( HTML::Mason ) ] );
153             my @POSSIBLE_TIMESTAMP_EXTENSIONS = find_intersection(\@enabled_extensions,
154             [ qw ( XML::Comma DBI ) ] );
155              
156             my @helper_extensions;
157             foreach my $ext (@POSSIBLE_HELPER_EXTENSIONS) {
158             eval "require $ext;";
159             next if($@);
160             my $util = $ext;
161             $util =~ s/\:\:/_/g;
162             eval "require Cache::Static::${util}_Util";
163             if($@) {
164             _log(2, "$ext exists but Cache::Static::${util}_Util does not\n");
165             } else {
166             push @helper_extensions, $ext;
167             }
168             }
169              
170             my @timestamp_extensions;
171             foreach my $ext (@POSSIBLE_TIMESTAMP_EXTENSIONS) {
172             eval "require $ext;";
173             next if($@);
174             my $util = $ext;
175             $util =~ s/\:\:/_/g;
176             eval "require Cache::Static::${util}_Util";
177             if($@) {
178             _log(2, "$ext exists but Cache::Static::${util}_Util does not, disabling extension\n");
179             } else {
180             push @timestamp_extensions, $ext;
181             }
182             }
183              
184             sub _readconf {
185 10     10   22 my $ns = shift;
186 10 100       36 $ns = '' unless(defined($ns));
187 10 100       36 _die_if_invalid_namespace($ns) if($ns);
188              
189 10         34 my $dir = "$ROOT/$ns";
190 10         15 my @conf;
191 2         3 open(CONF, "$dir/config") &&
192 10 100       444 (@conf = map { my $t = $_; $t = lc($t); $t =~ s/^\s+//; $t =~ s/\s+$//;
  2         5  
  2         5  
  2         5  
193 2         3 my $ar = []; @$ar = split(/\s+/, $t, 2); $ar }
  2         6  
  2         7  
194             grep(/^[^#]/, grep(/./, )));
195 10         42 close(CONF);
196 10         36 foreach my $cr (@conf) {
197 2 50       5 if($cr->[0] eq 'log_level') {
198 0 0 0     0 if(!$ns || $ns eq 'DEFAULT') {
199 0         0 $CONF{log_level} = $cr->[1];
200             } else {
201 0         0 _log(3, "log_level directive in CONF($ns) ignored");
202             }
203             } else {
204 2 50       12 $CONF{$ns ? $ns : 'DEFAULT'}->{$cr->[0]} = $cr->[1];
205             }
206             }
207             }
208              
209             #### useful when adding new modules
210             #warn "time: @timestamp_extensions\n";
211             #warn "help: @helper_extensions\n";
212             #die;
213              
214             sub _has_timestamp {
215 0     0   0 my $mod = shift;
216 0         0 return grep(/^$mod$/, @timestamp_extensions);
217             }
218              
219             sub _has_helper {
220 0     0   0 my $mod = shift;
221 0         0 return grep(/^$mod$/, @helper_extensions);
222             }
223              
224             ############################
225             ### /glue for extensions ###
226             ############################
227              
228             #try to set up the logfile with lenient permissions
229             eval {
230             open(FH, ">>$LOGFILE");
231             close(FH);
232             chmod 0666, $LOGFILE;
233             };
234              
235             #number of levels of directory in cache
236             #TODO: move this to config file
237             my $CACHE_LEVELS = 3;
238              
239             sub get_if_same {
240             ### uncomment the below line to disable Cache::Static
241             # return undef;
242 18     18 0 917 my ($key, $depsref, %args) = @_;
243 18         112 my ($ret, $dep) = _is_same($key, $depsref, %args);
244 18 100       51 if($ret) {
245 8         31 _log(1, "cache hit for key: $key");
246 8         37 return _get($key, %args);
247             } else {
248 10         49 _log(1, "cache miss for key: $key on dep: $dep");
249 10         81 return undef;
250             }
251             }
252              
253             sub _die_if_invalid_namespace {
254 45     45   82 my $ns = shift;
255 45 50 33     957 die "illegal namespace: $namespace" if($namespace =~ /\// ||
256             grep (/^$namespace$/, @ILLEGAL_NAMESPACES));
257             }
258              
259             sub set {
260 9     9 0 51 my ($key, $content, $deps, %args) = @_;
261 9   66     54 my $ns = $args{namespace} || $namespace;
262 9         666 _die_if_invalid_namespace($ns);
263 9         18 eval {
264             #create any necessary directories
265 9         21 my $dir = $key;
266 9         681 $dir =~ s/\/[^\/]*$//;
267 9         57 _mkdir_p("$ROOT/$ns/cache/$dir");
268 9 50       35 die "couldn't make/walk directories: $@" if($@);
269              
270             #if we overrode the namespace, or if the dir got rm -rf'd out
271             #from under us, this comes in handy...
272 9         83 _mkdir_p("$ROOT/$ns/tmp");
273              
274             #write out the content
275 9         22 my $tmpf = $key;
276 9         43 $tmpf =~ s/\///g;
277 9 50       1001 open(FH, ">$ROOT/$ns/tmp/$tmpf") || die "couldn't open $ROOT/$ns/tmp/$tmpf: $!";
278 9 50       85 (print FH $content) || die "couldn't print: $!";
279 9 50       600 close(FH) || die "couldn't close: $!";
280 9         408 chmod 0666, "$ROOT/$ns/tmp/$tmpf";
281              
282             #move the new cache file in place
283 9 50       1206 (rename "$ROOT/$ns/tmp/$tmpf", "$ROOT/$ns/cache/$key") ||
284             die "couldn't rename content to $ROOT/$ns/cache/$key";
285              
286 9 50       32 if($deps) {
287             #write out the deps
288 9         24 my $frozen_deps = join('', map { $a=$_; $a.="\n"; $a } @$deps);
  9         20  
  9         16  
  9         36  
289 9 50       730 open(FH, ">$ROOT/$ns/tmp/$tmpf.dep") || die "couldn't open: $!";
290 9 50       59 (print FH $frozen_deps) || die "couldn't print: $!";
291 9 50       378 close(FH) || die "couldn't close: $!";
292 9         310 chmod 0666, "$ROOT/$ns/tmp/$tmpf.dep";
293              
294             #move the new .dep file in place
295 9 50       1092 (rename "$ROOT/$ns/tmp/$tmpf.dep", "$ROOT/$ns/cache/$key.dep") ||
296             die "couldn't rename deps to $ROOT/$ns/cache/$key.dep: $!";
297             }
298              
299 9 50       29 }; if($@) {
300 0         0 _log(2, "Cache::Static::set couldn't save new value (in namespace: $ns) : $@");
301             } else {
302 9         56 _log(3, "Cache::Static::set refreshed $key in namespace: $ns");
303             }
304             }
305              
306             sub make_friendly_key {
307 3     3 0 9 my ($url, $argsref) = @_;
308              
309             #key for Cache is url + args in deterministic order
310 3         11 my $key = "$url?";
311 3         21 foreach my $arg (sort keys %$argsref) {
312 0         0 my $val = $argsref->{$arg};
313 0 0       0 if(ref($val)) {
314 0 0 0     0 if(ref($val) eq 'ARRAY') {
    0          
315 0         0 $val = join("&$arg=", @$val);
316             } elsif($val->isa('XML::Comma::Doc')
317             && _has_timestamp('XML::Comma')) {
318 0         0 $val = "XML::Comma::Doc:".$val->doc_key;
319             } else {
320 0         0 _log(3, "got a ".ref($val)." and we're just freezing it...");
321 0         0 $val = Storable::freeze($val);
322             }
323             }
324 0         0 $key .= "$arg=$val&";
325             }
326 3         12 $key =~ s/&$//;
327              
328             #fix problem with friendly keys that have a multiple consecutive dashes,
329             #as when they are printed in HTML debugging mode, they can cause SGML
330             #comments to eat what is supposed to be code up to the next literal --
331             #for one-to-one-ness, also map '-' (single dash) to '-1-'
332             #this is really something browsers should work around, but don't. see:
333             # https://bugzilla.mozilla.org/show_bug.cgi?id=214476
334 3 50       15 $key = join("", map { (/-+/) ? "-".length($_)."-" : $_ }
  3         25  
335             split(/(-+)/, $key));
336              
337 3         20 return $key;
338             }
339              
340             sub make_key {
341 3     3 0 1438 return md5_path(make_friendly_key(@_));
342             }
343              
344             sub make_key_from_friendly {
345 0     0 0 0 my $key = shift;
346 0         0 return md5_path($key);
347             }
348              
349             sub md5_path {
350 3     3 0 7 my $key = shift;
351              
352 3         26 $key = md5_base64($key);
353             # base64 is all alphanumeric except + and /
354             # / must be translated
355             # # + is translated for cosmetic reasons
356 3         16 $key =~ s/\//_/g;
357             # $key =~ s/\+/-/g;
358              
359 3         53 $key = join('/', grep(/./, split(/(.)/, $key, $CACHE_LEVELS+1)));
360              
361 3         16 return $key;
362             }
363              
364             sub get_seconds_from_timespec {
365 10     10 0 17 my $arg = shift;
366 10         179 my @args = split(/([a-zA-Z])/, $arg);
367 10 50       41 push @args, 's' if(($#args%2) == 0);
368 10         19 my ($i, $period) = (0, 0);
369 10         35 while($i < $#args) {
370 28         44 my $n = $args[$i];
371 28         40 my $c = $args[$i+1];
372 28         34 my $mult;
373 28 100       186 if(lc($c) eq 'w') { $mult = 7 * 24 * 60 * 60; }
  3 100       7  
    100          
    100          
    50          
374 5         10 elsif(lc($c) eq 'd') { $mult = 24 * 60 * 60; }
375 5         9 elsif(lc($c) eq 'h') { $mult = 60 * 60; }
376 5         9 elsif(lc($c) eq 'm') { $mult = 60; }
377 10         18 elsif(lc($c) eq 's') { $mult = 1; }
378             else {
379 0         0 _log(2, "Cache::Static::get_seconds_from_timespec: unknown multiplier in $arg: $c");
380 0         0 return undef;
381             }
382 28         50 $period += $n * $mult;
383 28         69 $i += 2;
384             }
385 10         36 return $period;
386             }
387              
388             sub _find_bound_before_time {
389 6     6   14 my ($time, $offset, $bound) = @_;
390             #valid bounds: [HMDW]
391 6         191 my @lt = localtime($time);
392              
393 6         11 my ($roffset, $interval);
394             #this would be much nicer with switch/case, grumble.
395 6 100       29 if($bound eq 'M') {
    50          
    50          
    50          
396 4         9 $roffset = $lt[0];
397 4         5 $interval = 60;
398             } elsif($bound eq 'H') {
399 0         0 $roffset = $lt[0] + $lt[1] * 60;
400 0         0 $interval = 60 * 60;
401             } elsif($bound eq 'D') {
402 0         0 $roffset = $lt[0] + $lt[1] * 60 + $lt[2] * 60 * 60;
403 0         0 $interval = 24 * 60 * 60;
404             } elsif($bound eq 'W') {
405 2         8 $roffset = $lt[0] + $lt[1] * 60 + $lt[2] * 60 * 60 +
406             $lt[6] * 24 * 60 * 60;
407 2         3 $interval = 7 * 24 * 60 * 60;
408             } else {
409 0         0 _log(2, "Cache::Static::_find_bound_before_time: unknown time boundary: $bound");
410 0         0 return undef;
411             }
412 6 50       20 if($offset > $interval) {
413 0         0 _log(2, "Cache::Static::_find_bound_before_time: offset ($offset) > interval ($interval)");
414 0         0 return undef;
415             }
416 6 100       27 return $offset + $time - $roffset - ($roffset > $offset ? 0 : $interval);
417             }
418              
419             sub _is_same {
420 18     18   110 my ($key, $depsref, %args) = @_;
421 18   66     107 my $ns = $args{namespace} || $namespace;
422 18         862 _die_if_invalid_namespace($ns);
423              
424             #if no deps argument, find what we've got saved on disk for deps
425 18 50       60 unless($depsref) {
426 0         0 open(F, "$ROOT/$ns/cache/$key.dep");
427 0         0 my $deps_str = ;
428 0         0 close(F);
429 0         0 my @deps = split(/\0/, $deps_str);
430 0         0 $depsref = \@deps;
431 0         0 _log(4, "Cache::Static::_is_same: got ".($#deps+1)." deps for $key");
432             }
433              
434             #get last modified time of the cached version, or 0 if it doesn't exist
435 18         797 my @t = stat("$ROOT/$ns/cache/$key");
436 18 50       66 my $request_modtime = @t ? $t[9] : 0;
437 18 50       46 return (0, "(not yet cached)") unless($request_modtime);
438              
439             # give a chance to add any module specific extra deps
440 18         27 my %extra_deps;
441             ### TODO: this is too slow, at least for XML::Comma (0.02 sec on p4@3GHz)
442             # foreach my $dep (@$depsref) {
443             # my ($type, $spec) = split(/\|/, $dep, 2);
444             # my $dep_modtime;
445             # if($type =~ /^_/) {
446             # #not a builtin - call an extension
447             # my ($module, $type, $spec) = split(/\|/, $dep, 3);
448             # $module =~ s/^_//;
449             # $module =~ s/\:\:/_/g;
450             # my @deps = eval
451             # "Cache::Static::${module}_Util::get_extra_deps(\"$type\", \"$spec\")";
452             # foreach my $d (@deps) {
453             # $extra_deps{$d} = 1 unless($extra_deps{$d});
454             # }
455             # }
456             # }
457 18         45 my @deps = (@$depsref, keys %extra_deps);
458              
459 18         44 my @TRUE = ($key,1);
460 18         54 foreach my $dep (@deps) {
461 18         35 my @FALSE = (0,$dep);
462 18         77 my ($full_type, $spec) = split(/\|/, $dep, 2);
463 18         91 _log(4, "full_type: $full_type, spec: $spec");
464 18         51 my ($type, $modifier) = split(/-/, $full_type, 2);
465 18 50       78 if(defined($modifier)) {
466 0         0 _log(4, "modifier found: full_type: $full_type, type: $type, modifier: $modifier");
467             }
468 18         26 my $dep_modtime;
469 18 50       113 if($type =~ /^_/) {
    100          
    50          
    0          
    0          
470             #not a builtin - call an extension
471 0         0 my ($module, $type, $spec) = split(/\|/, $dep, 3);
472 0         0 $module =~ s/^_//;
473 0         0 $module =~ s/\:\:/_/g;
474              
475 0         0 _log(4, "here we are, extension, module: $module, type: $type spec: $spec");
476              
477 0         0 $dep_modtime = eval "Cache::Static::${module}_Util::modtime(\"$type\", \"$spec\")";
478 0 0       0 if($@) {
    0          
479 0         0 _log(3, "error calling Cache::Static::${module}_Util::modtime(\"$type\", \"$spec\"): $@");
480             } elsif(!$dep_modtime) {
481 0         0 _log(4, "got non-true value from Cache::Static::${module}_Util::modtime(\"$type\", \"$spec\"): $@ $!");
482             }
483             } elsif ($type eq 'file') {
484 8         26 _log(4, "here we are, file spec: $spec");
485 8         177 my @t = stat($spec);
486 8         18 $dep_modtime = $t[9];
487             } elsif ($type eq 'time') {
488 10         17 my $spec_regex = '([0-9]*[hmdsw])+([0-9]*)?';
489 10 50       254 if ($spec =~ /^[0-9]{10}$/) {
    100          
    50          
490             #one-time timestamp expiration
491 0         0 $dep_modtime = $spec;
492             } elsif ($spec =~ /^$spec_regex$/) {
493             #5w4d3h2m1s, e.g. 5 weeks, 4 days, ...
494             #this is a bit backwards: now - spec > time of modification
495 4         15 my $sex = get_seconds_from_timespec($spec);
496 4 50       10 return @FALSE unless(defined($sex));
497 4         15 $dep_modtime = time - $sex;
498             } elsif ($spec =~ /^[HMDW]:$spec_regex$/) {
499             #cron-esque timespecs, e.g. {week|day|hour|min} boundary + $spec
500             #or 3:57 on day 3 of the week (W:3d3h57m)
501             # bound_before(now)+offset <=> request time
502 6         94 my ($bound, $offset) = split(/:/, $spec);
503 6         23 my $sex = get_seconds_from_timespec($offset);
504 6 50       18 return @FALSE unless(defined($sex));
505 6         29 $dep_modtime = _find_bound_before_time(time,
506             $sex, $bound);
507 6 50       22 return @FALSE unless(defined($dep_modtime));
508             } else {
509 0         0 _log(2, "Cache::Static: unrecognized time spec: ($spec), regenerating");
510 0         0 return @FALSE;
511             }
512             } elsif ($type eq 'HIT') {
513 0         0 return @TRUE;
514             } elsif ($type eq 'MISS') {
515 0         0 return @FALSE;
516             } else {
517 0         0 my $ret = _get_conf($ns, 'unrecognized_dependency_returns');
518 0 0       0 _log(2, "Cache::Static: unrecognized dependency ($type)".
519             ($ret ? ", serving anyway" : ", regenerating").
520             " as specified by conf option unrecognized_dependency_returns");
521 0 0       0 return ($ret ? @TRUE : @FALSE);
522             }
523             #always override the default if modifier exists
524 18 50       78 my $bool = defined($modifier) ? $modifier :
525             _get_conf($ns, 'dep_file_not_found_returns');
526 18 100       70 return ($bool ? @TRUE : @FALSE) unless($dep_modtime);
    100          
527 14 100       76 return @FALSE if($dep_modtime > $request_modtime);
528             }
529 7         34 return @TRUE;
530             }
531              
532             sub _get_conf {
533 18     18   32 my ($ns, $var) = @_;
534 18 100       62 _readconf("$ns") unless(defined($CONF{$ns}));
535 18   66     291 return $CONF{$ns}->{$var} || $CONF{DEFAULT}->{$var};
536             }
537              
538             #TODO: this whole function is a race condition...
539             #is doing a regenerate if there was a change since _is_same best?
540             #or should we try to save the version we thought we were gonna use?
541             sub _get {
542 8     8   19 my ($key, %args) = @_;
543 8   66     41 my $ns = $args{namespace} || $namespace;
544 8         17 _die_if_invalid_namespace($ns);
545              
546 8 50       381 open(FH, "$ROOT/$ns/cache/$key") || return undef;
547 8         170 my $t = join('', );
548 8         82 close(FH);
549              
550 8         30 _log(3, "Cache::Static::get read $key");
551              
552 8         59 return $t;
553             }
554              
555             sub _write_spec_timestamp {
556 0     0   0 my $spec = shift;
557 0         0 _mkdirs_and_touch($ROOT.'/timestamps/'.md5_path($spec).'.ts', $spec);
558             }
559              
560             sub _unlink_spec_timestamp {
561 0     0   0 my $spec = shift;
562 0         0 my $file = $ROOT.'/timestamps/'.md5_path($spec).'.ts';
563 0         0 unlink($file);
564 0 0       0 if(_get_conf($namespace, 'recursive_unlink')) {
565 0         0 $file =~ s/\/[^\/]*$//;
566 0 0       0 unless(opendir(DIR, $file)) {
567 0         0 _log(3, "_unlink_spec_timestamp failed to opendir($file): (another process probably rmdir'd it): $!");
568 0         0 return;
569             }
570 0         0 my @files = readdir(DIR);
571 0 0       0 closedir(DIR) if(@files);
572 0         0 while($#files == 1 ) {
573 0 0       0 unless(rmdir $file) {
574 0         0 _log(3, "_unlink_spec_timestamp failed to rmdir($file): (another process probably touched a file in it): $!");
575 0         0 return;
576             }
577 0         0 $file =~ s/\/[^\/]*$//;
578 0 0       0 unless(opendir(DIR, $file)) {
579 0         0 _log(3, "_unlink_spec_timestamp failed to opendir($file): (another process probably rmdir'd it): $!");
580 0         0 return;
581             }
582 0         0 my @files = readdir(DIR);
583 0 0       0 closedir(DIR) if(@files);
584             }
585             }
586             }
587              
588             #optional second argument indicates stuff to squirrel in the file
589             #TODO: the name is misleading given the possibility of the 2nd arg
590             sub _mkdirs_and_touch {
591 0     0   0 my $file = shift;
592 0   0     0 my $output = shift || '';
593              
594             #get rid of double slashes
595 0         0 $file =~ s/\/\//\//g;
596              
597             #split the dir and the filename
598 0         0 my $dir = $file;
599 0         0 $dir =~ s/\/[^\/]*$//;
600              
601 0         0 my $err;
602 0         0 eval {
603             #mkdir -p
604 0         0 _mkdir_p($dir);
605 0 0       0 die "couldn't make/walk directories: $@" if($@);
606              
607             #touch/write to the file
608 0 0       0 open(FH, ">$file") || die "couldn't open $file: $!";
609 0 0       0 if($output) {
610 0   0     0 print FH $output || die "couldn't print $output to $file: $!";
611             }
612 0 0       0 close(FH) || die "couldn't close $file: $!";
613 0         0 chmod 0666, $file;
614 0 0       0 }; if($@) {
615 0         0 _log(2, "Cache::Static::_mkdirs_and_touch: couldn't update timestamps: $@");
616             }
617             }
618              
619             sub _log {
620 91     91   318 my $severity = shift;
621 91 100       338 return unless($severity <= $CONF{log_level});
622 65         226 my $args = join(' ', @_);
623 65         281 $args =~ s/\n/ /mg;
624 65         642 $args =~ s/\s+$//;
625             #we don't need a full stack trace at level 3
626             #TODO: this regexp can be overly greedy
627 65 50       263 $args =~ s/Stack:.*$//sg if($CONF{log_level} == 3);
628 65         4003 my @lt = localtime();
629 65         154 $lt[4]++; #month starts at 0 for perl, 1 for humans
630 65         286 @lt = map { sprintf("%02d", $_) } @lt;
  585         2358  
631 65         871 my $date = ($lt[5]+1900).'/'.$lt[4].'/'.$lt[3].' '.$lt[2].':'.$lt[1].':'.$lt[0];
632 65         165 my $level = $LOG_LEVEL_NAMES[$severity];
633 65         309 $level .= ' ' while(length($level) < 5);
634              
635 65 100       177 if($have_fcntl) {
636             #TODO: we don't need to open/close every time.
637             #just flock(LOG, LOCK_EX), seek, flock(LOG, LOCK_UN);
638             #benchmark and safety test this...
639 35 50       1551 open(LOG, ">>$LOGFILE") || die "can't open log \"$LOGFILE\" $!";
640 35 50       301 flock(LOG, LOCK_EX) || die "can't lock log \"$LOGFILE\" $!";
641 35         174 seek(LOG, 0, 2); #seek to EOF if someone appended while we waited...
642 35   50     374 print LOG "$level $date [$$] $args\n" || die "can't write to log \"$LOGFILE\": $!";
643             #close does implicit unlock
644 35 50       2098 close(LOG) || die "can't close log \"$LOGFILE\": $!";
645             } else {
646             #TODO: there must be a way to escape " such that the shell doesn't puke
647 30         86 $args =~ s/\"/'/g;
648 30         282064 `echo "$level $date [$$] $args" >>$LOGFILE`;
649             }
650             }
651              
652             sub _mkdir_p {
653 44     44   596 my $dir = shift;
654 44         600 my @dirs = grep (/./, split(/\//, $dir));
655 44         127 my $dir_so_far = '/';
656 44         98 foreach my $d (@dirs) {
657 213         578 $dir_so_far .= "$d/";
658 213 100       5527 unless(-e $dir_so_far) {
659 3 50       247 mkdir($dir_so_far) || die "couldn't create $dir_so_far: $!";
660 3 50       99 chmod(0777, $dir_so_far) || die "couldn't change perms on $dir_so_far: $!";
661             }
662             }
663             }
664              
665             1;
666             __END__