File Coverage

blib/lib/CHI/Driver/File.pm
Criterion Covered Total %
statement 132 134 98.5
branch 31 38 81.5
condition 9 12 75.0
subroutine 27 27 100.0
pod 1 11 9.0
total 200 222 90.0


line stmt bran cond sub pod time code
1             package CHI::Driver::File;
2             $CHI::Driver::File::VERSION = '0.60';
3 9     9   5467 use Carp;
  9         15  
  9         902  
4 9     9   89 use Cwd qw(realpath cwd);
  9         13  
  9         485  
5             use CHI::Util
6 9     9   43 qw(fast_catdir fast_catfile unique_id read_dir read_file write_file);
  9         15  
  9         585  
7 9     9   4216 use Digest::JHash qw(jhash);
  9         5124  
  9         593  
8 9     9   60 use File::Basename qw(basename dirname);
  9         16  
  9         551  
9 9     9   48 use File::Find qw(find);
  9         13  
  9         422  
10 9     9   41 use File::Path qw(mkpath rmtree);
  9         14  
  9         416  
11 9     9   42 use File::Spec::Functions qw(catdir catfile splitdir tmpdir);
  9         14  
  9         485  
12 9     9   39 use Log::Any qw($log);
  9         13  
  9         81  
13 9     9   660 use Moo;
  9         14  
  9         74  
14 9     9   4171 use MooX::Types::MooseLike::Base qw(:all);
  9         15  
  9         3560  
15 9     9   53 use strict;
  9         13  
  9         323  
16 9     9   41 use warnings;
  9         13  
  9         12009  
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 417     417 0 314851 my ( $class, %params ) = @_;
30              
31             # Backward compat
32             #
33 417 50       1432 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 417         8603 return \%params;
39             }
40              
41             sub _build_path_to_namespace {
42 364     364   7891 my $self = shift;
43              
44 364         4605 my $namespace = $self->escape_for_filename( $self->namespace );
45 364 50       2691 $namespace = $self->digest_key($namespace)
46             if length($namespace) > $self->max_key_length;
47 364         5023 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 9001     9001 0 10772 my ( $self, $key ) = @_;
55              
56 9001         19635 my $new_key = $self->escape_for_filename($key);
57 9001 100 100     29236 if ( length($new_key) > length($key)
58             && length($new_key) > $self->max_key_length() )
59             {
60 53         360 $new_key = $self->digest_key($new_key);
61             }
62 9001         13716 return $new_key;
63             }
64              
65             sub unescape_key {
66 2295     2295 0 8842 my ( $self, $key ) = @_;
67              
68 2295         4773 return $self->unescape_for_filename($key);
69             }
70              
71             sub fetch {
72 6148     6148 0 12321 my ( $self, $key ) = @_;
73              
74 6148         11306 my $file = $self->path_to_key($key);
75 6148 100 66     133901 if ( defined $file && -f $file ) {
76 5199         14204 return read_file($file);
77             }
78             else {
79 949         5512 return undef;
80             }
81             }
82              
83             sub store {
84 2053     2053 0 6890 my ( $self, $key, $data ) = @_;
85              
86 2053         2036 my $dir;
87 2053 50       5568 my $file = $self->path_to_key( $key, \$dir ) or return undef;
88              
89 2053 100       258373 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 2053         7909 my $temp_file = $self->generate_temporary_filename( $dir, $file );
95 2053 100       4985 my $store_file = defined($temp_file) ? $temp_file : $file;
96              
97 2053         5845 write_file( $store_file, $data, $self->{file_create_mode} );
98              
99 2051 100       4825 if ( defined($temp_file) ) {
100              
101             # Rename can fail in rare race conditions...try multiple times
102             #
103 2049         5503 for ( my $try = 0 ; $try < 3 ; $try++ ) {
104 2077 100       116210 last if ( rename( $temp_file, $file ) );
105             }
106 2049 100       39400 if ( -f $temp_file ) {
107 14         162 my $error = $!;
108 14         1057 unlink($temp_file);
109 14         245 die "could not rename '$temp_file' to '$file': $error";
110             }
111             }
112             }
113              
114             sub remove {
115 477     477 0 6042 my ( $self, $key ) = @_;
116              
117 477 50       1203 my $file = $self->path_to_key($key) or return undef;
118 477         777494 unlink($file);
119             }
120              
121             sub clear {
122 423     423 0 8658 my ($self) = @_;
123              
124 423         10310 my $namespace_dir = $self->path_to_namespace;
125 423 100       13507 return if !-d $namespace_dir;
126 265         1159 my $renamed_dir = join( ".", $namespace_dir, unique_id() );
127 265         18245 rename( $namespace_dir, $renamed_dir );
128 265         759921 rmtree($renamed_dir);
129 265 50       10288 die "could not remove '$renamed_dir'"
130             if -d $renamed_dir;
131             }
132              
133             sub get_keys {
134 378     378 0 714 my ($self) = @_;
135              
136 378         447 my @filepaths;
137 378         1252 my $re = quotemeta( $self->file_extension );
138 378 100 66 6157   1828 my $wanted = sub { push( @filepaths, $_ ) if -f && /${re}$/ };
  6157         370251  
139 378         1324 my @keys = $self->_collect_keys_via_file_find( \@filepaths, $wanted );
140 378         3660 return @keys;
141             }
142              
143             sub _collect_keys_via_file_find {
144 378     378   533 my ( $self, $filepaths, $wanted ) = @_;
145              
146 378         9697 my $namespace_dir = $self->path_to_namespace;
147 378 100       8981 return () if !-d $namespace_dir;
148              
149 364         23257 find( { wanted => $wanted, no_chdir => 1 }, $namespace_dir );
150              
151 364         1117 my @keys;
152 364         2189 my $key_start = length($namespace_dir) + 1 + $self->depth * 2;
153 364         921 my $subtract = -1 * length( $self->file_extension );
154 364         822 foreach my $filepath (@$filepaths) {
155 1975         2443 my $key = substr( $filepath, $key_start, $subtract );
156 1975         4065 $key = $self->unescape_key( join( "", splitdir($key) ) );
157 1975         3448 push( @keys, $key );
158             }
159 364         1601 return @keys;
160             }
161              
162             sub generate_temporary_filename {
163 2049     2049 0 3671 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 2049         5649 return fast_catfile( $dir, unique_id() );
170             }
171              
172             sub get_namespaces {
173 24     24 0 345 my ($self) = @_;
174              
175 24         62 my $root_dir = $self->root_dir();
176 24 50       386 return () if !-d $root_dir;
177 24         72 my @contents = read_dir($root_dir);
178 94         161 my @namespaces =
179 98         224 map { $self->unescape_for_filename($_) }
180 98         174 grep { $self->is_escaped_for_filename($_) }
181 24         58 grep { -d fast_catdir( $root_dir, $_ ) } @contents;
182 24         114 return @namespaces;
183             }
184              
185             my %hex_strings = map { ( $_, sprintf( "%x", $_ ) ) } ( 0x0 .. 0xf );
186              
187             sub path_to_key {
188 8681     8681 1 10600 my ( $self, $key, $dir_ref ) = @_;
189 8681 50       15823 return undef if !defined($key);
190              
191 8681         191126 my @paths = ( $self->path_to_namespace );
192              
193 8681         60628 my $orig_key = $key;
194 8681         17582 $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 8681 100       16916 if ( $key =~ /^[0-9a-f]{32}$/ ) {
201 114         296 push( @paths,
202 113         387 map { substr( $key, $_, 1 ) } ( 0 .. $self->{depth} - 1 ) );
203             }
204             else {
205              
206             # Hash key to a 32-bit integer (using non-escaped key for back compat)
207             #
208 8568         19350 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 8568         22454 for ( my $d = $self->{depth} ; $d > 0 ; $d-- ) {
215 12951         21583 push( @paths, $hex_strings{ $bucket & 0xf } );
216 12951         25816 $bucket >>= 4;
217             }
218             }
219              
220             # Join paths together, computing dir separately if $dir_ref was passed.
221             #
222 8681         18693 my $filename = $key . $self->file_extension;
223 8681         7853 my $filepath;
224 8681 100 66     23122 if ( defined $dir_ref && ref($dir_ref) ) {
225 2053         6563 my $dir = fast_catdir(@paths);
226 2053         4636 $filepath = fast_catfile( $dir, $filename );
227 2053         2981 $$dir_ref = $dir;
228             }
229             else {
230 6628         17920 $filepath = fast_catfile( @paths, $filename );
231             }
232              
233 8681         21190 return $filepath;
234             }
235              
236             1;
237              
238             __END__