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
|
|
|
|
|
|
|
|