File Coverage

blib/lib/CHI/Driver/File.pm
Criterion Covered Total %
statement 129 134 96.2
branch 29 38 76.3
condition 9 12 75.0
subroutine 27 27 100.0
pod 1 11 9.0
total 195 222 87.8


line stmt bran cond sub pod time code
1             package CHI::Driver::File;
2             $CHI::Driver::File::VERSION = '0.61';
3 10     10   6831 use Carp;
  10         38  
  10         1025  
4 10     10   76 use Cwd qw(realpath cwd);
  10         22  
  10         558  
5             use CHI::Util
6 10     10   57 qw(fast_catdir fast_catfile unique_id read_dir read_file write_file);
  10         22  
  10         666  
7 10     10   4852 use Digest::JHash qw(jhash);
  10         6818  
  10         647  
8 10     10   78 use File::Basename qw(basename dirname);
  10         23  
  10         681  
9 10     10   67 use File::Find qw(find);
  10         21  
  10         541  
10 10     10   65 use File::Path qw(mkpath rmtree);
  10         21  
  10         556  
11 10     10   78 use File::Spec::Functions qw(catdir catfile splitdir tmpdir);
  10         26  
  10         617  
12 10     10   74 use Log::Any qw($log);
  10         24  
  10         118  
13 10     10   2728 use Moo;
  10         23  
  10         75  
14 10     10   4355 use MooX::Types::MooseLike::Base qw(:all);
  10         35  
  10         3756  
15 10     10   79 use strict;
  10         32  
  10         312  
16 10     10   60 use warnings;
  10         21  
  10         17084  
17              
18             extends 'CHI::Driver';
19              
20             has '+max_key_length' => ( default => sub { 248 } );
21             has 'depth' => ( is => 'ro', isa => Int, default => sub { 2 } );
22             has 'dir_create_mode' => ( is => 'ro', isa => Int, default => sub { oct(775) } );
23             has 'file_create_mode' => ( is => 'ro', isa => Int, default => sub { oct(666) } );
24             has 'file_extension' => ( is => 'ro', isa => Str, default => sub { '.dat' } );
25             has 'path_to_namespace' => ( is => 'lazy' );
26             has 'root_dir' => ( is => 'ro', isa => Str, default => sub { catdir( tmpdir(), 'chi-driver-file' ) } );
27              
28             sub BUILDARGS {
29 421     421 0 398813 my ( $class, %params ) = @_;
30              
31             # Backward compat
32             #
33 421 50       1536 if ( defined( $params{key_digest} ) ) {
34 0         0 $params{key_digester} = $params{key_digest};
35 0         0 $params{max_key_length} = 0;
36             }
37              
38 421         7341 return \%params;
39             }
40              
41             sub _build_path_to_namespace {
42 378     378   3558 my $self = shift;
43              
44 378         1727 my $namespace = $self->escape_for_filename( $self->namespace );
45 378 50       1552 $namespace = $self->digest_key($namespace)
46             if length($namespace) > $self->max_key_length;
47 378         3258 return catdir( $self->root_dir, $namespace );
48             }
49              
50             # Escape key to make safe for filesystem; if it then grows larger than
51             # max_key_length, digest it.
52             #
53             sub escape_key {
54 9676     9676 0 16534 my ( $self, $key ) = @_;
55              
56 9676         21836 my $new_key = $self->escape_for_filename($key);
57 9676 100 100     29906 if ( length($new_key) > length($key)
58             && length($new_key) > $self->max_key_length() )
59             {
60 96         358 $new_key = $self->digest_key($new_key);
61             }
62 9676         18273 return $new_key;
63             }
64              
65             sub unescape_key {
66 2848     2848 0 13815 my ( $self, $key ) = @_;
67              
68 2848         5732 return $self->unescape_for_filename($key);
69             }
70              
71             sub fetch {
72 6505     6505 0 16987 my ( $self, $key ) = @_;
73              
74 6505         13872 my $file = $self->path_to_key($key);
75 6505 100 66     126617 if ( defined $file && -f $file ) {
76 5414         24348 return read_file($file);
77             }
78             else {
79 1091         8989 return undef;
80             }
81             }
82              
83             sub store {
84 2228     2228 0 9207 my ( $self, $key, $data ) = @_;
85              
86 2228         3353 my $dir;
87 2228 50       5803 my $file = $self->path_to_key( $key, \$dir ) or return undef;
88              
89 2228 100       266730 mkpath( $dir, 0, $self->{dir_create_mode} ) if !-d $dir;
90              
91             # Possibly generate a temporary file - if generate_temporary_filename returns undef,
92             # store to the destination file directly
93             #
94 2228         11240 my $temp_file = $self->generate_temporary_filename( $dir, $file );
95 2228 100       5604 my $store_file = defined($temp_file) ? $temp_file : $file;
96              
97 2228         7590 write_file( $store_file, $data, $self->{file_create_mode} );
98              
99 2226 100       7773 if ( defined($temp_file) ) {
100              
101             # Rename can fail in rare race conditions...try multiple times
102             #
103 2224         6449 for ( my $try = 0 ; $try < 3 ; $try++ ) {
104 2224 50       172967 last if ( rename( $temp_file, $file ) );
105             }
106 2224 50       49927 if ( -f $temp_file ) {
107 0         0 my $error = $!;
108 0         0 unlink($temp_file);
109 0         0 die "could not rename '$temp_file' to '$file': $error";
110             }
111             }
112             }
113              
114             sub remove {
115 536     536 0 7847 my ( $self, $key ) = @_;
116              
117 536 50       1547 my $file = $self->path_to_key($key) or return undef;
118 536         36192 unlink($file);
119             }
120              
121             sub clear {
122 460     460 0 8962 my ($self) = @_;
123              
124 460         8932 my $namespace_dir = $self->path_to_namespace;
125 460 100       13369 return if !-d $namespace_dir;
126 301         1837 my $renamed_dir = join( ".", $namespace_dir, unique_id() );
127 301         13481 rename( $namespace_dir, $renamed_dir );
128 301         371129 rmtree($renamed_dir);
129 301 50       12221 die "could not remove '$renamed_dir'"
130             if -d $renamed_dir;
131             }
132              
133             sub get_keys {
134 492     492 0 1195 my ($self) = @_;
135              
136 492         803 my @filepaths;
137 492         1696 my $re = quotemeta( $self->file_extension );
138 492 100 66 7966   2436 my $wanted = sub { push( @filepaths, $_ ) if -f && /${re}$/ };
  7966         514129  
139 492         1598 my @keys = $self->_collect_keys_via_file_find( \@filepaths, $wanted );
140 492         5202 return @keys;
141             }
142              
143             sub _collect_keys_via_file_find {
144 492     492   1128 my ( $self, $filepaths, $wanted ) = @_;
145              
146 492         11351 my $namespace_dir = $self->path_to_namespace;
147 492 100       11681 return () if !-d $namespace_dir;
148              
149 468         37627 find( { wanted => $wanted, no_chdir => 1 }, $namespace_dir );
150              
151 468         1818 my @keys;
152 468         2372 my $key_start = length($namespace_dir) + 1 + $self->depth * 2;
153 468         1297 my $subtract = -1 * length( $self->file_extension );
154 468         1048 foreach my $filepath (@$filepaths) {
155 2444         4284 my $key = substr( $filepath, $key_start, $subtract );
156 2444         5843 $key = $self->unescape_key( join( "", splitdir($key) ) );
157 2444         5224 push( @keys, $key );
158             }
159 468         1869 return @keys;
160             }
161              
162             sub generate_temporary_filename {
163 2224     2224 0 6102 my ( $self, $dir, $file ) = @_;
164              
165             # Generate a temporary filename using unique_id - faster than tempfile, as long as
166             # we don't need automatic removal.
167             # Note: $file not used here, but might be used in an override.
168             #
169 2224         7149 return fast_catfile( $dir, unique_id() );
170             }
171              
172             sub get_namespaces {
173 46     46 0 832 my ($self) = @_;
174              
175 46         155 my $root_dir = $self->root_dir();
176 46 50       789 return () if !-d $root_dir;
177 46         246 my @contents = read_dir($root_dir);
178             my @namespaces =
179 182         318 map { $self->unescape_for_filename($_) }
180 186         530 grep { $self->is_escaped_for_filename($_) }
181 46         171 grep { -d fast_catdir( $root_dir, $_ ) } @contents;
  186         554  
182 46         222 return @namespaces;
183             }
184              
185             my %hex_strings = map { ( $_, sprintf( "%x", $_ ) ) } ( 0x0 .. 0xf );
186              
187             sub path_to_key {
188 9272     9272 1 16680 my ( $self, $key, $dir_ref ) = @_;
189 9272 50       20040 return undef if !defined($key);
190              
191 9272         188059 my @paths = ( $self->path_to_namespace );
192              
193 9272         74217 my $orig_key = $key;
194 9272         22094 $key = $self->escape_key($key);
195              
196             # Hack: If key is exactly 32 hex chars, assume it's an md5 digest and
197             # take a prefix of it for bucket. Digesting will usually happen in
198             # transform_key and there's no good way for us to know it occurred.
199             #
200 9272 100       19889 if ( $key =~ /^[0-9a-f]{32}$/ ) {
201             push( @paths,
202 159         555 map { substr( $key, $_, 1 ) } ( 0 .. $self->{depth} - 1 ) );
  204         588  
203             }
204             else {
205              
206             # Hash key to a 32-bit integer (using non-escaped key for back compat)
207             #
208 9113         21397 my $bucket = jhash($orig_key);
209              
210             # Create $self->{depth} subdirectories, containing a maximum of 64
211             # subdirectories each, by successively shifting 4 bits off the
212             # bucket and converting to hex.
213             #
214 9113         24481 for ( my $d = $self->{depth} ; $d > 0 ; $d-- ) {
215 14164         31502 push( @paths, $hex_strings{ $bucket & 0xf } );
216 14164         28543 $bucket >>= 4;
217             }
218             }
219              
220             # Join paths together, computing dir separately if $dir_ref was passed.
221             #
222 9272         23611 my $filename = $key . $self->file_extension;
223 9272         12391 my $filepath;
224 9272 100 66     25646 if ( defined $dir_ref && ref($dir_ref) ) {
225 2228         7070 my $dir = fast_catdir(@paths);
226 2228         5615 $filepath = fast_catfile( $dir, $filename );
227 2228         4757 $$dir_ref = $dir;
228             }
229             else {
230 7044         20112 $filepath = fast_catfile( @paths, $filename );
231             }
232              
233 9272         25133 return $filepath;
234             }
235              
236             1;
237              
238             __END__