File Coverage

lib/Cache/AgainstFile/Storable.pm
Criterion Covered Total %
statement 171 178 96.0
branch 42 62 67.7
condition 11 14 78.5
subroutine 26 27 96.3
pod 4 6 66.6
total 254 287 88.5


line stmt bran cond sub pod time code
1             ###############################################################################
2             # Purpose : Cache data structures against a file (serialised in files using Storable)
3             # Author : John Alden
4             # Created : 22 Apr 2005 (based on IFL::FileCache)
5             # CVS : $Id: Storable.pm,v 1.22 2006/05/09 09:02:32 mattheww Exp $
6             ###############################################################################
7              
8             package Cache::AgainstFile::Storable;
9              
10 2     2   885 use strict;
  2         4  
  2         55  
11 2     2   40 use Carp;
  2         3  
  2         116  
12 2     2   983 use Cache::AgainstFile::Base;
  2         4  
  2         62  
13 2     2   1958 use Storable qw(store retrieve retrieve_fd lock_store lock_retrieve);
  2         7125  
  2         216  
14 2     2   1676 use File::Spec::Functions qw(canonpath catfile rel2abs);
  2         1492  
  2         163  
15 2     2   2095 use FileHandle;
  2         25684  
  2         14  
16              
17 2 50   2   1347 use constant IS_WINDOWS => ($^O eq 'MSWin32' ? 1 : 0);
  2         4  
  2         228  
18             if (IS_WINDOWS) { require Win32 }
19              
20 2         3 use constant HAVE_FILE_POLICY => eval {
21 2         862 require File::Policy;
22 0         0 import File::Policy qw(check_safe);
23 0         0 1;
24 2     2   16 };
  2         5  
25              
26 2     2   9 use vars qw($VERSION @ISA);
  2         4  
  2         4424  
27             $VERSION = sprintf"%d.%03d", q$Revision: 1.22 $ =~ /: (\d+)\.(\d+)/;
28             @ISA = qw(Cache::AgainstFile::Base);
29              
30             #
31             # Public interface
32             #
33              
34             sub new {
35 9     9 1 247 my $class = shift;
36 9         17 my ($loader, $options) = @_;
37 9         55 my $self = $class->SUPER::new(@_);
38              
39 9   66     383 my $dir = $self->{options}->{CacheDir} || croak("You must supply a cache directory for caching with Storable");
40 8 50       39 check_safe($dir,"w") if(HAVE_FILE_POLICY);
41 8         16 _create_dir_if_required($dir);
42            
43             #Select locking implementation
44 8   100     39 my $locking = $options->{Locking} || 'AtomicWrite';
45 8 100       45 if($locking eq 'Flock') {
    100          
46 1         3 $self->{write} = \&_write_locked;
47 1         4 $self->{read} = \&_read_locked;
48             } elsif ($locking eq 'AtomicWrite') {
49 6         23 $self->{write} = \&_write_atomic;
50 6         19 $self->{read} = \&_read;
51             } else {
52 1         227 croak("Unrecognised locking model '$locking'");
53             }
54              
55 7         32 return $self;
56             }
57              
58             sub get
59             {
60 36     36 1 127 my ($self, $filename, @opts) = @_;
61              
62 36 50       198 check_safe($filename,"r") if(HAVE_FILE_POLICY);
63            
64 36         208 my $cache_dir = $self->{options}{CacheDir};
65 36         148 my $cache_filename = catfile($cache_dir, $self->_filename2cache($filename));
66 36         212 TRACE("cache get - cache filename is '$cache_filename'");
67 36         56 my $stale = 0;
68              
69             # In some (as yet undetermined) circumstances the cachefile directory
70             # can disappear, which causes application errors
71 36         112 _create_dir_if_required($cache_dir);
72              
73             # If cachefile doesn't exist, it won't open, implying staleness.
74 36         324 my $cache_fh = new FileHandle;
75 36 50       2283 check_safe($cache_filename,"r") if(HAVE_FILE_POLICY);
76 36 100       160 unless ($cache_fh->open($cache_filename)) {
77 25         1973 undef $cache_fh;
78 25         113 $stale = 1;
79             }
80              
81             # Compare file mtimes to check staleness
82 36         584 my $file_mtime;
83 36 100 100     202 unless ($self->{options}->{NoStat} && !$stale) {
84 34         763 $file_mtime = (stat($filename))[9];
85 34 100       129 my $cache_mtime = ($cache_fh->stat)[9] if $cache_fh;
86 34   100     630 $stale = (!defined $file_mtime) || (!defined $cache_mtime) || ($file_mtime != $cache_mtime);
87             }
88 36 100       184 TRACE("Cache " . ($stale?"is":"is not") . " stale");
89              
90             #Read from cache
91 36         63 my $data;
92 36 100       101 if (!$stale) {
93 8         14 $data = eval { $self->{read}->($cache_filename, $cache_fh) };
  8         28  
94 8 50       25 if ($@) {
95 0         0 warn "Storable couldn't retrieve $cache_filename: $@";
96 0         0 $stale = 1;
97             }
98             }
99 36 100       131 $cache_fh->close if $cache_fh;
100            
101             #Write to cache
102 36 100       222 if ($stale) {
103 28         84 TRACE("writing cache");
104 28         159 $data = $self->{loader}->($filename, @opts);
105 28 50       10001177 $file_mtime = (stat($filename))[9] unless(defined $file_mtime); #Need mtime now
106 28         114 $self->{write}->($cache_filename, $data, $file_mtime);
107             }
108 36         206 return $data;
109             }
110              
111              
112             sub count {
113 8     8 1 15 my ($self) = shift;
114 8         25 my $files_in_cache = $self->_cache_files;
115 7         43 return scalar @$files_in_cache;
116             }
117              
118             sub size {
119 5     5 1 36 my ($self) = shift;
120 5         11 my $cache_dir = $self->{options}{CacheDir};
121 5         9 my $files_in_cache = $self->_cache_files;
122 5         7 my $sum = 0;
123 5         11 foreach(@$files_in_cache) {$sum += -s catfile($cache_dir, $_)}
  3         51  
124 5         22 return $sum;
125             }
126              
127             #
128             # Protected methods referenced from Base class
129             #
130              
131             sub _remove {
132 7     7   13 my($self, $keys) = @_;
133 7         15 my $cache_dir = $self->{options}{CacheDir};
134 7         22 foreach(@$keys)
135             {
136 12         42 my $filename = $self->_filename2cache($_);
137 12         50 TRACE("Deleting cache for $_ ($filename)");
138 12         1325 unlink catfile($cache_dir, $filename);
139             }
140             }
141              
142             sub _accessed {
143 11     11   19 my($self) = @_;
144 11         32 my $cache_dir = $self->{options}{CacheDir};
145 11         31 my $files_in_cache = $self->_cache_files;
146 17         104 my %accessed = map
147             {
148 11         111 my $cache_file = catfile($cache_dir, $_);
149 17         60 $self->_cache2filename($_) => (stat($cache_file))[8]
150             }
151             @$files_in_cache;
152 11         49 return \%accessed;
153             }
154              
155             sub _stale {
156 1     1   2 my($self) = @_;
157 1         11 my $cache_dir = $self->{options}{CacheDir};
158 1         3 my $files_in_cache = $self->_cache_files;
159 1         3 my @out =
160             map
161             {
162 1         7 $self->_cache2filename($_)
163             }
164             grep
165             {
166 1         2 my $cache_file = catfile($cache_dir, $_);
167 1         4 my $src_mt = (stat ($self->_cache2filename($_)))[9];
168 1         13 my $cache_mt = (stat ($cache_file))[9];
169 1 50 33     16 (!defined $src_mt) || (!defined $cache_mt) || ($src_mt != $cache_mt)
170             } @$files_in_cache;
171 1         5 @out;
172             }
173              
174             #
175             # Private methods
176             #
177              
178             sub _cache_files {
179 25     25   31 my($self) = @_;
180 25         44 my $cache_dir = $self->{options}{CacheDir};
181 25         60 local *FH;
182 25 50       82 check_safe($cache_dir,"r") if(HAVE_FILE_POLICY);
183 25 100       684 opendir (FH, $cache_dir) or die("unable to open directory $cache_dir - $!");
184 24         508 my @files = grep {$_ !~ /^\./} readdir(FH);
  72         280  
185 24         244 closedir FH;
186 24         72 DUMP("cache files", \@files);
187 24         82 return \@files;
188             }
189              
190             #
191             # Subroutines
192             #
193              
194             sub _read_locked {
195 2     2   5 my($cache_filename, $fh) = @_;
196             # we don't want the filehandle. Suppose it might need to be closed
197             # under Win32? Close it anyway
198 2 50       16 $fh->close if $fh;
199 2 50       101 check_safe($cache_filename,"r") if(HAVE_FILE_POLICY);
200 2         10 my $ref_data = lock_retrieve($cache_filename);
201 2         192 TRACE("Fetched from cache file: $cache_filename");
202 2         6 return $$ref_data;
203             }
204              
205             sub _write_locked {
206 2     2   4 my ($cache_filename, $data, $mtime) = @_;
207 2 50       9 check_safe($cache_filename,"w") if(HAVE_FILE_POLICY);
208 2         8 lock_store(\$data, $cache_filename);
209 2         4307 TRACE("wrote cache file: $cache_filename");
210 2         7 _backtouch($cache_filename, $mtime);
211             }
212              
213             sub _write_atomic {
214 26     26   53 my ($cache_filename, $data, $mtime) = @_;
215 26 50       185 check_safe($cache_filename,"w") if(HAVE_FILE_POLICY);
216 26         153 my $temp_filename = $cache_filename . ".tmp$$";
217 26         138 store(\$data, $temp_filename);
218 26         2296393 TRACE("wrote temp file: $temp_filename");
219 26 50       100 (_backtouch($temp_filename, $mtime)) or die "couldn't set utime on $temp_filename: $!";
220 26 50       1764 rename($temp_filename, $cache_filename) or die("Unable to rename temporary file '$temp_filename' to cache file '$cache_filename'");
221 26         115 TRACE("moved to cache file: $cache_filename");
222             }
223              
224             sub _backtouch {
225 28     28   197 my ($file, $utime) = @_;
226 28 50       82 (defined $utime) or confess "need utime";
227             # Might not work in race condition? Exception NOT thrown, returns false on failure.
228 28 50       102 check_safe($file,"w") if(HAVE_FILE_POLICY);
229 28         895 return utime (time(), $utime, $file);
230             }
231              
232             sub _read {
233 6     6   10 my($cache_filename, $fh) = @_;
234 6         5 my $ref_data;
235 6 50       20 check_safe($cache_filename,"r") if(HAVE_FILE_POLICY);
236 6 50       27 if (!$fh) {
237 0         0 TRACE("Reading $cache_filename...");
238 0         0 $ref_data = retrieve($cache_filename);
239             } else {
240 6         21 TRACE("Reading $cache_filename (from filehandle)...");
241 6         21 $ref_data = retrieve_fd($$fh);
242             }
243 6         618 return $$ref_data;
244             }
245              
246              
247             sub _create_dir_if_required {
248 44     44   93 my ($dir) = @_;
249 44 100       917 if(! -d $dir) {
250 4         9 eval {
251 4         26 require File::Path;
252 4         24190 File::Path::mkpath($dir);
253             };
254 4 50       24 croak "Unable to create directory $dir: $@" if $@;
255             }
256             }
257              
258              
259             # escape and normalise filename
260             sub _filename2cache {
261 61     61   463 my ($self, $filename) = @_;
262 61         787 TRACE({Level => 2}, "filename = $filename");
263            
264             #Remove redundant slashes
265 61         363 $filename = canonpath($filename);
266 61         457 TRACE({Level => 2}, " - canonpath => $filename");
267              
268             #Make absolute
269 61         310 my $cache_file = rel2abs($filename);
270 61         2567 TRACE({Level => 2}, " - rel2abs => $cache_file");
271              
272 61         197 if (IS_WINDOWS) {
273             # resolve C:/LONGNA~1 to C:/LongName
274             $cache_file = Win32::GetFullPathName($cache_file);
275             TRACE({Level => 2}, " - fullpathname => $cache_file");
276             # normalise path separator
277             $cache_file =~ tr:\\:/:;
278             TRACE({Level => 2}, " - normalise slashes => $cache_file");
279             }
280              
281             # escape control chars, special characters, etc e.g. '/' -> '%2F'
282 61         633 $cache_file =~ s|([^\w\.\-])| sprintf("%%%02X", ord($1)) |eg;
  360         1813  
283              
284             # normalise case on case-insensitive filesystems
285 61 50       555 $cache_file = lc $cache_file if File::Spec->case_tolerant;
286              
287 61         414 TRACE({Level => 2}," => cache filename = $cache_file");
288 61         492 TRACE("filename2cache $filename -> $cache_file");
289 61         326 return $cache_file;
290             }
291              
292             # unescape filename
293             sub _cache2filename {
294 32     32   243 my $self = shift;
295 32         39 my $cache_file = shift;
296 32         559 (my $filename = $cache_file) =~ s|%([0-9A-Fa-f]{2})| chr(hex($1)) |eg;
  186         627  
297 32         135 TRACE("cache2filename $cache_file -> $filename");
298 32         640 return $filename;
299             }
300              
301             #
302             # Log::Trace stubs
303             #
304              
305 48     48 0 57 sub TRACE {}
306 0     0 0   sub DUMP {}
307              
308             1;
309              
310             =head1 NAME
311              
312             Cache::AgainstFile::Storable - cache data structures parsed from files in Storable files
313              
314             =head1 SYNOPSIS
315              
316             use Cache::AgainstFile;
317             my $cache = new Cache::AgainstFile(
318             \&loader,
319             {
320             Method => 'Storable',
321             CacheDir => '/var/tmp/cache/myapp',
322             # ...
323             }
324             );
325              
326             $data = $cache->get($filename);
327              
328             =head1 DESCRIPTION
329              
330             Data structures parsed from files are cached in "shadow" storable files.
331             If parsing is significantly more expensive than file I/O (e.g. with XML files),
332             then this will offer some benefit.
333              
334             This backend is suitable for non-persistent environments (e.g. CGI scripts)
335             where you want the cache to outlive the process. For persistent environments,
336             the Memory backend may be more suitable as it saves on file I/O.
337              
338             count() and size() are relatively expensive operations involving scanning the cache directory.
339              
340             =head1 OPTIONS
341              
342             =over 4
343              
344             =item CacheDir
345              
346             Directory in which to store cache files. This is mandatory.
347              
348             =item MaxATime
349              
350             Purge items older than this.
351             Value is in seconds (default=undefined=infinity)
352              
353             =item MaxItems
354              
355             Purge oldest items from the cache to reduce the number of items in the cache to be at most this number.
356             Value should be an integer (default=undefined=infinity)
357              
358             =item NoStat
359              
360             Don't stat files to validate the cache - items are served from the cache until they are purged.
361             Valid values are 0|1 (default=0, i.e. files are statted)
362              
363             =item Locking
364              
365             Valid values are Flock and AtomicWrite (default is AtomicWrite).
366             If neither of these are to your taste, consider using Cache::AgainstFile::CacheModule with another caching module.
367             Some other file caching modules on CPAN are:
368              
369             =over 4
370              
371             =item Cache::FileCache
372              
373             This uses atomic writes.
374              
375             =item Cache::File
376              
377             This uses File::NFSLock for locking (no locking is also an option)
378              
379             =back
380              
381             =back
382              
383             =head1 VERSION
384              
385             $Revision: 1.22 $ on $Date: 2006/05/09 09:02:32 $ by $Author: mattheww $
386              
387             =head1 AUTHOR
388              
389             John Alden & Piers Kent
390              
391             =head1 COPYRIGHT
392              
393             (c) BBC 2005. This program is free software; you can redistribute it and/or modify it under the GNU GPL.
394              
395             See the file COPYING in this distribution, or http://www.gnu.org/licenses/gpl.txt
396              
397             =cut