File Coverage

lib/Sub/Slice/Backend/Filesystem.pm
Criterion Covered Total %
statement 127 129 98.4
branch 40 46 86.9
condition 9 11 81.8
subroutine 28 29 96.5
pod 0 13 0.0
total 204 228 89.4


line stmt bran cond sub pod time code
1             ###############################################################################
2             # Purpose : Filesystem (default) backend for Sub::Slice
3             # Author : John Alden
4             # Created : Nov 2004
5             # CVS : $Header: /home/cvs/software/cvsroot/sub_slice/lib/Sub/Slice/Backend/Filesystem.pm,v 1.13 2005/01/12 16:51:19 simonf Exp $
6             ###############################################################################
7              
8             package Sub::Slice::Backend::Filesystem;
9              
10 2     2   868 use strict;
  2         5  
  2         87  
11 2     2   2302 use Storable();
  2         7322  
  2         56  
12 2     2   1661 use File::Spec::Functions;
  2         1813  
  2         209  
13 2     2   13 use File::Path;
  2         3  
  2         106  
14 2     2   2763 use File::Temp;
  2         51447  
  2         217  
15 2     2   20 use Carp;
  2         5  
  2         163  
16              
17 2     2   11 use constant JOBFILE_PREFIX => 'Sub__Slice__';
  2         4  
  2         118  
18 2     2   10 use constant MASK_LENGTH => 12;
  2         5  
  2         70  
19 2     2   9 use constant TOKEN_DB => 'sub_slice_job.store';
  2         4  
  2         69  
20              
21 2     2   29 use vars qw($VERSION);
  2         3  
  2         3374  
22             $VERSION = sprintf"%d.%03d", q$Revision: 1.13 $ =~ /: (\d+)\.(\d+)/;
23              
24             sub new {
25 27     27 0 1183 my($class, $options) = @_;
26            
27             # Use a subdir within the temp directory by default, so cleanup
28             # can walk the tree beneath it rather than having to match
29             # everything in the temp dir against a mask
30 27         87 my $path = $class->default_path($options->{path});
31 27 100       705 File::Path::mkpath($path) unless (-d $path);
32            
33 27   100     354 my $self = {
      100        
      100        
34             path => $path,
35             prefix => $options->{prefix} || JOBFILE_PREFIX,
36             storable_filename => $options->{job_filename} || TOKEN_DB,
37             mask_length => $options->{unique_key_length} || MASK_LENGTH,
38             lax => $options->{lax}
39             };
40 27         111 return bless($self, $class);
41             }
42              
43             # Given a path (for our temp dir), do any required canonicalization
44             # eg. make sure there is always a trailing /.
45             # Use a default path if one is not specified.
46             sub default_path {
47 29     29 0 68 my ($class, $path) = @_;
48 29   66     68 $path = $path || File::Spec::Functions::tmpdir()."/sub_slice";
49 29         260 $path =~ s!([^/])$!$1/!; # add a trailing slash
50 29         93 $path;
51             }
52              
53             sub new_id {
54 7     7 0 50 my ($self) = @_;
55 7         28 my $mask = "X" x $self->{mask_length};
56 7         38 my ($dir) = File::Temp::mkdtemp($self->{path} . $self->{prefix} . $mask);
57 7         1913 my $id = scalar File::Spec::Functions::splitpath( $dir );
58 7         96 TRACE("Created new ID: $id");
59 7         15 return $id;
60             }
61              
62             sub load_job {
63 21     21 0 1021 my ($self, $id) = @_;
64 21         52 my $filename = $self->_db_from_id( $self->_check_id($id) );
65 18         64 TRACE("loading job '$id' from '$filename'");
66 18         51 return Storable::retrieve( $filename );
67             }
68              
69             sub save_job {
70 20     20 0 307 my ($self, $job) = @_;
71 20 100       189 croak("job should be a Sub::Slice object") unless(UNIVERSAL::isa($job, 'Sub::Slice'));
72 19         54 my $filename = $self->_db_from_id( $job->id );
73 19         46 my $job_id = $job->id;
74 19         102 TRACE("saving job '$job_id' to '$filename' ($$)");
75 19 100       443 TRACE("job_file for '$job_id' already exists and will be overwritten") if (-e $filename);
76 19         72 Storable::store( $job, $filename );
77             }
78              
79             sub delete_job {
80 5     5 0 1065 my ($self, $id) = @_;
81 5         36 my $dir = $self->_dir_from_id( $self->_check_id($id) );
82 4 100       73 die("Job $id does not exist") unless(-d $dir);
83 3         12 TRACE("deleting directory $dir");
84 3         1880 rmtree $dir;
85             }
86              
87             sub store {
88 26     26 0 40 my ($self, $job, $key, $value) = @_;
89 26         120 $job->{'data'}{$key} = $value;
90             }
91              
92             sub fetch {
93 42     42 0 49 my ($self, $job, $key) = @_;
94 42         101 return $job->{'data'}{$key};
95             }
96              
97             sub store_blob {
98 7     7 0 45508 my ($self, $job, $key, $value) = @_;
99 7 100       171 croak("job should be a Sub::Slice object") unless(UNIVERSAL::isa($job, 'Sub::Slice'));
100 6 100       139 croak("you must supply a key to store the blob against") unless(defined $key);
101 5 100       22 if (my $data_file = $job->{'.blobs'}{$key}) {
102 1         7 TRACE("Updating blob for $key in $data_file");
103 1         4 _write_file($data_file, $value);
104             } else {
105 4         16 my $dir = $self->_dir_from_id( $job->id );
106 4         25 my ($fh, $data_file) = File::Temp::tempfile(DIR => $dir, UNLINK => 0);
107 4         1562 TRACE("Writing blob for $key in $data_file");
108 4         33 print $fh $value;
109 4         181 close $fh;
110 4         26 $job->{'.blobs'}{$key} = $data_file;
111             }
112 5         25 return 1;
113             }
114              
115             sub fetch_blob {
116 10     10 0 1101 my ($self, $job, $key) = @_;
117 10 100       176 croak("job should be a Sub::Slice object") unless(UNIVERSAL::isa($job, 'Sub::Slice'));
118 9 100       139 croak("you must supply a key to fetch the blob") unless(defined $key);
119 8 100       43 if (my $data_file = $job->{'.blobs'}{$key}) {
120 6         21 TRACE("Fetching blob for $key from $data_file");
121 6         15 return _read_file($data_file);
122             }
123             }
124              
125             sub cleanup {
126 2     2 0 1011 my ($self, $maxage) = @_;
127 2 100       9 $maxage = 1 if !defined $maxage;
128 2         17 local $^T = time();
129 2         5 my $deleted = 0;
130             my $cleaner = sub {
131 3 100   3   51 return if /^\.{1,2}$/;
132 2         25 my $mtime = -M $_;
133 2         11 TRACE("file $_ mtime $mtime");
134              
135             # it may have *just* disappeared
136 2 50       8 return unless defined $mtime;
137              
138             # only want to clean up if it's old.
139 2 50       6 return unless $mtime >= $maxage;
140 2         5 $deleted++;
141 2 100 50     34 if (-f $_) { unlink $_ || die "can't delete $_: $!" }
  1 50       154  
142 1 50       121 elsif (-d $_) { rmdir $_ || die "can't rmdir $_: $!" }
143 0         0 else { $deleted-- };
144 2         13 };
145 2         6 my $p = $self->{path};
146 2 100       36 return if (!-d $p);
147 1         12 require File::Find;
148 1         5 TRACE ("Cleaning up ".$p);
149 1         285 File::Find::finddepth ($cleaner, $self->{path});
150 1         12 $deleted;
151             }
152              
153             #
154             # Private functions encapsulating:
155             # - creating the dir from an ID
156             # - creating the storable db filename from an ID
157             # - file IO for blob data
158             #
159              
160             sub _dir_from_id {
161 8     8   21 my($self, $id) = @_;
162 8         56 return File::Spec::Functions::catfile($self->{path}, $id);
163             }
164              
165             sub _db_from_id {
166 37     37   48 my($self, $id) = @_;
167 37         216 return File::Spec::Functions::catfile($self->{path}, $id, $self->{storable_filename});
168             }
169              
170             sub _check_id {
171 26     26   41 my($self, $id) = @_;
172 26 100       404 confess("Called without an id") unless(defined $id);
173 24 100       51 unless($self->{lax}) {
174 23         70 my $regex = quotemeta($self->{prefix}) . ('\w' x $self->{mask_length});
175 23 100       801 confess("Format of ID '$id' is invalid") unless($id =~ /\A$regex\Z/);
176             }
177 22         64 return $id;
178             }
179              
180             sub _read_file {
181 6     6   9 my $filename = shift;
182 6 50       205 open (FH, $filename) || die("unable to open $filename - $!");
183 6         28 local $/ = undef;
184 6         123 my $data = ;
185 6         54 close FH;
186 6         36 return $data;
187             }
188              
189             sub _write_file {
190 1     1   3 my ($filename, $data) = @_;
191 1         3 local *FH;
192 1 50       108 open(FH, ">$filename") or die("Unable to open $filename - $!");
193 1         4 binmode FH;
194 1         2 print FH $data;
195 1         14 close FH;
196             }
197              
198              
199             #Log::Trace stubs
200 73     73 0 101 sub TRACE{}
201 0     0 0   sub DUMP{}
202              
203             1;
204              
205             =head1 NAME
206              
207             Sub::Slice::Backend::Filesystem - Default backend for Sub::Slice
208              
209             =head1 SYNOPSIS
210              
211             See L.
212              
213             =head1 DESCRIPTION
214              
215             Implementation of the Sub::Slice::Backend API using Filesystem & Storable.
216             See L and L for more information.
217              
218             Data is stored in one directory per job corresponding to the unique job ID.
219             Within this directory there is a single storable file containing the job data and possibly other uniquely-named files
220             containing BLOB data. The mapping of key to unique filename for BLOBs is stored within the job.
221              
222             =head1 STORAGE OPTIONS
223              
224             =over 4
225              
226             =item path
227              
228             The directory in which Sub::Slice tokens are stored. Default is File::Spec::Functions::tmpdir()."/sub_slice". Sub::Slice will create that directory if it
229             does not exist already.
230              
231             NB. Beware of running Sub::Slice under multiple users using the default
232             path. Unless you are careful with umask settings, you may create a
233             directory that only some Sub::Slice users can write to.
234              
235             =item prefix
236              
237             Prefix for all IDs generated by the module. Default is "Sub__Slice__".
238              
239             =item unique_key_length
240              
241             Length of the unique part of the key. Default is 12 characters.
242              
243             =item job_filename
244              
245             Filename containing the job data. The default is "sub_slice_job.store".
246              
247             =item lax
248              
249             Relaxes the check that enforces that job ids match the prefix and unique key length specified in the constructor.
250             This normally prevents you loading a valid Sub::Slice token from another application if 2 applications
251             share the same $path but use a different prefix.
252              
253             =back
254              
255             =head1 TODO
256              
257             =over 4
258              
259             =item locking functionality
260              
261             This may be added in a future version and should default to something reasonably safe (ie. only one process should be able to work on a job at any point in time)
262              
263             =back
264            
265             =head1 VERSION
266              
267             $Revision: 1.13 $ on $Date: 2005/01/12 16:51:19 $ by $Author: simonf $
268              
269             =head1 AUTHOR
270              
271             John Alden and Simon Flack
272              
273             =head1 COPYRIGHT
274              
275             (c) BBC 2005. This program is free software; you can redistribute it and/or modify it under the GNU GPL.
276              
277             See the file COPYING in this distribution, or http://www.gnu.org/licenses/gpl.txt
278              
279             =cut
280