File Coverage

blib/lib/DataStore/CAS/Simple.pm
Criterion Covered Total %
statement 293 341 85.9
branch 112 198 56.5
condition 61 139 43.8
subroutine 47 59 79.6
pod 14 15 93.3
total 527 752 70.0


line stmt bran cond sub pod time code
1             package DataStore::CAS::Simple;
2 2     2   2040 use 5.008;
  2         7  
3 2     2   523 use Moo 1.000007;
  2         10889  
  2         13  
4 2     2   1803 use Carp;
  2         6  
  2         117  
5 2     2   14 use Try::Tiny;
  2         4  
  2         109  
6 2     2   502 use Digest 1.16 ();
  2         568  
  2         64  
7 2     2   13 use File::Spec 3.33;
  2         35  
  2         52  
8 2     2   523 use File::Spec::Functions 'catfile', 'catdir', 'canonpath';
  2         857  
  2         155  
9 2     2   1467 use File::Temp 0.22 ();
  2         24686  
  2         10471  
10              
11             our $VERSION = '0.07';
12             our @CARP_NOT= qw( DataStore::CAS DataStore::CAS::File DataStore::CAS::VirtualHandle );
13              
14             # ABSTRACT: Simple file/directory based CAS implementation
15              
16              
17             has path => ( is => 'ro', required => 1 );
18             has copy_buffer_size => ( is => 'rw', default => sub { 256*1024 } );
19             has _config => ( is => 'rwp', init_arg => undef );
20 12     12 1 43 sub fanout { [ $_[0]->fanout_list ] }
21 15     15 1 28 sub fanout_list { @{ $_[0]->_config->{fanout} } }
  15         151  
22 97     97 1 583 sub digest { $_[0]->_config->{digest} }
23             has _digest_hash_to_hex => ( is => 'rw', init_arg => undef );
24             has _digest_hash_split => ( is => 'rw', init_arg => undef );
25              
26             with 'DataStore::CAS';
27              
28              
29             sub BUILD {
30 16     16 0 110 my ($self, $args)= @_;
31             my ($create, $ignore_version, $digest, $fanout, $_notest)=
32 16         32 delete @{$args}{'create','ignore_version','digest','fanout','_notest'};
  16         62  
33              
34             # Check for invalid params
35 16         52 my @inval= grep { !$self->can($_) } keys %$args;
  16         80  
36 16 50       50 croak "Invalid parameter: ".join(', ', @inval)
37             if @inval;
38              
39             # Path is required, and must be a directory
40 16         38 my $path= $self->path;
41 16 50       264 if (!-d $path) {
42 0 0       0 croak "Path '$path' is not a directory"
43             unless $create;
44 0 0       0 mkdir $path
45             or die "Can't create directory '$path'";
46             }
47              
48             # Check directory
49 16         45 my $setup= 0;
50 16 100       320 unless (-f catfile($path, 'conf', 'VERSION')) {
51 13 100       216 croak "Path does not appear to be a valid CAS : '$path'"
52             unless $create;
53              
54             # Here, we are creating a new CAS directory
55 12         71 $self->create_store({ digest => $digest, path => $path, fanout => $fanout });
56 9         46 $setup= 1;
57             }
58              
59 12         61 $self->_set__config( $self->_load_config($path, { ignore_version => $ignore_version }) );
60 12         44 my ($tohex, $split)= _get_hex_and_fanout_functions($self->digest, $self->fanout);
61 12         45 $self->_digest_hash_to_hex($tohex);
62 12         28 $self->_digest_hash_split($split);
63              
64 12 100       27 if ($setup) {
65 9         33 $self->put('');
66             } else {
67             # Properly initialized CAS will always contain an entry for the empty string
68 3 100       70 croak "CAS dir '$path' is missing a required file"
69             ." (has it been initialized?)"
70             unless $self->validate($self->hash_of_null);
71             }
72              
73 10         895 return $self;
74             }
75              
76              
77             sub path_parts_for_hash {
78 0     0 1 0 my ($self, $hash)= @_;
79 0         0 $self->_digest_hash_split->($hash);
80             }
81              
82             sub path_for_hash {
83 81     81 1 549 my ($self, $hash, $create_dirs)= @_;
84 81         237 my @parts= $self->_digest_hash_split->($hash);
85 81 100       185 if ($create_dirs) {
86 20         50 my $path= $self->path;
87 20         68 for (@parts[0..($#parts-1)]) {
88 42         265 $path= catdir($path, $_);
89 42 100       591 next if -d $path;
90 40 50       1881 mkdir($path) or croak "mkdir($path): $!";
91             }
92 20         270 return catfile($path, $parts[-1]);
93             } else {
94 61         537 return catfile($self->path, @parts);
95             }
96             }
97              
98              
99             sub create_store {
100 12     12 1 23 my $class= shift;
101 12 50       36 $class= ref $class if ref $class;
102 12 50       32 my %params= (@_ == 1? %{$_[0]} : @_);
  12         59  
103            
104 12 50       40 defined $params{path} or croak "Missing required param 'path'";
105 12 50       149 -d $params{path} or croak "Directory '$params{path}' does not exist";
106             # Make sure we are creating in an empty dir
107             croak "Directory '$params{path}' is not empty\n"
108 12 100       59 unless $class->_is_dir_empty($params{path});
109              
110 11   100     45 $params{digest} ||= 'SHA-1';
111 11         84 $class->_assert_digest_available($params{digest});
112              
113 11   100     53 $params{fanout} ||= [ 1, 2 ];
114             # make sure the fanout isn't insane
115 11         21 $params{fanout}= $class->_parse_fanout(join(' ',@{$params{fanout}}));
  11         52  
116              
117 9         52 my $conf_dir= catdir($params{path}, 'conf');
118 9 50       500 mkdir($conf_dir) or croak "mkdir($conf_dir): $!";
119 9         56 $class->_write_config_setting($params{path}, 'VERSION', $class->_hierarchy_version);
120 9         148 $class->_write_config_setting($params{path}, 'digest', $params{digest}."\n");
121 9         35 $class->_write_config_setting($params{path}, 'fanout', join(' ', @{$params{fanout}})."\n");
  9         74  
122             }
123             sub _hierarchy_version {
124 9   33 9   43 my $class= ref $_[0] || $_[0];
125 9         15 my $out= '';
126             # record the version of any class hierarchy which "isa DataStore::CAS::Simple"
127 9 50       25 require MRO::Compat if $] < 5.010;
128 9         41 my $hier= mro::get_linear_isa($class);
129 9         95 for (grep $_->isa(__PACKAGE__), @$hier) {
130 9 50       96 if (!$_->VERSION) {
131 0         0 warn "Package '$_' lacks a VERSION, weakening the protection of DataStore::CAS::Simple's versioned storage directory.";
132             } else {
133 9         88 $out .= $_ . ' ' . $_->VERSION . "\n";
134             }
135             }
136 9         38 return $out;
137             }
138              
139             # This method loads the digest and fanout configuration and validates it
140             # It is called during the constructor.
141             sub _load_config {
142 12     12   34 my ($class, $path, $flags)= @_;
143 12 50       54 $class= ref $class if ref $class;
144 12         26 my %params;
145            
146             # Version str is "$PACKAGE $VERSION\n", where version is a number but might have a
147             # string suffix on it
148             $params{storage_format_version}=
149 12         52 $class->_parse_version($class->_read_config_setting($path, 'VERSION'));
150 12 50       67 unless ($flags->{ignore_version}) {
151 12         16 while (my ($pkg, $ver)= each %{$params{storage_format_version}}) {
  24         316  
152 12     12   76 my $cur_ver= try { $pkg->VERSION };
  12         512  
153 12 50       176 defined $cur_ver
154             or croak "Class mismatch: storage dir was created using $pkg"
155             ." but that package is not loaded now\n";
156 12     0   542 (try { $pkg->VERSION($ver); 1; } catch { 0 })
  12         43  
  0         0  
157 12 50       62 or croak "Version mismatch: storage dir was created using"
158             ." version '$ver' of $pkg but this is only $cur_ver\n";
159             }
160             }
161              
162             # Get the digest algorithm name
163             $params{digest}=
164 12         38 $class->_parse_digest($class->_read_config_setting($path, 'digest'));
165 12         51 $class->_assert_digest_available($params{digest});
166             # Get the directory fan-out specification
167 12         35 $params{fanout}= $class->_parse_fanout($class->_read_config_setting($path, 'fanout'));
168 12         53 return \%params;
169             }
170              
171             sub _get_hex_and_fanout_functions {
172 12     12   74 my ($digest, $fanout)= @_;
173 12         62 my $hexlen= length Digest->new($digest)->add('')->hexdigest;
174 12         585 my $rawlen= length Digest->new($digest)->add('')->digest;
175             # Create a function that coerces the argument into a hex string, or dies.
176             # When given a digest, it can be raw bytes, or hex. The hex one is double the length.
177             my $tohex= sub {
178 0     0   0 my $hash= $_[2];
179 0   0     0 my $len= length($hash) || 0;
180 0 0       0 $len == $hexlen? $hash
    0          
181             : $len == $rawlen? _to_hex($hash)
182             : croak "Invalid length for checksum of $digest: $len "._quoted($hash);
183 12         424 };
184              
185             # Create a function that splits a digest into the path components
186             # for the CAS file.
187 12         40 $fanout= [ @$fanout ];
188             # final component might be a character indicating full-name or remainder-name
189 12 50       65 my $filename_type= $fanout->[-1] =~ /^[0-9]+$/? '*'
190             : pop @$fanout;
191 12         113 my $re= '^'.join('', map "([0-9a-f]{$_})", map /([0-9]+)/, @$fanout);
192 12 50       48 $re .= '([0-9a-f]+)' if $filename_type eq '*';
193 12         143 $re = qr/$re/;
194             my $split= ($filename_type eq '=')? sub {
195 0     0   0 my $hash= $_[0];
196 0 0 0     0 $hash= $tohex->($hash) if $hexlen != (length($hash) || 0);
197 0 0       0 my @dirs= ($hash =~ $re) or croak "can't split hash '$hash' into requested fanout";
198 0         0 return @dirs, $hash;
199             }
200             : ($filename_type eq '*')? sub {
201 81     81   163 my $hash= $_[0];
202 81 50 50     259 $hash= $tohex->($hash) if $hexlen != (length($hash) || 0);
203 81 50       730 my @dirs= ($hash =~ $re) or croak "can't split hash '$hash' into requested fanout";
204 81         313 return @dirs;
205             }
206 12 50       69 : croak "Unrecognized filename indicator in fanout specification: '$filename_type'";
    50          
207              
208 12         46 return ($tohex, $split);
209             }
210              
211             sub _to_hex {
212 0     0   0 my $tmp= shift;
213 0         0 $tmp =~ s/./ sprintf("%02X", $_) /ge;
  0         0  
214 0         0 $tmp;
215             }
216             sub _quoted {
217 0     0   0 my $tmp= shift;
218 0 0       0 return "(undef)" unless defined $tmp;
219 0         0 $tmp =~ s/[\0-\x1F\x7F]/ sprintf("\\x%02X", $_) /ge;
  0         0  
220 0         0 qq{"$tmp"};
221             }
222              
223             sub _is_dir_empty {
224 12     12   32 my (undef, $path)= @_;
225 12 50       339 opendir(my $dh, $path)
226             or die "opendir($path): $!";
227 12 100       255 my @entries= grep { $_ ne '.' and $_ ne '..' } readdir($dh);
  26         175  
228 12         134 closedir($dh);
229 12         274 return @entries == 0;
230             }
231              
232             # In the name of being "Simple", I decided to just read and write
233             # raw files for each parameter instead of using JSON or YAML.
234             # It is not expected that this module will have very many options.
235             # Subclasses will likely use YAML.
236              
237             sub _write_config_setting {
238 27     27   77 my (undef, $path, $name, $content)= @_;
239 27         140 $path= catfile($path, 'conf', $name);
240 27 50       1927 open(my $f, '>', $path)
241             or croak "Failed to open '$path' for writing: $!\n";
242 27 50 33     1194 (print $f $content) && (close $f)
243             or croak "Failed while writing '$path': $!\n";
244             }
245             sub _read_config_setting {
246 36     36   87 my (undef, $path, $name)= @_;
247 36         170 $path= catfile($path, 'conf', $name);
248 36 50       1411 open(my $f, '<', $path)
249             or croak "Failed to read '$path' : $!\n";
250 36         212 local $/= undef;
251 36         846 my $str= <$f>;
252 36 50 33     284 defined $str and length $str or croak "Failed to read '$path' : $!\n";
253 36         775 return $str;
254             }
255              
256             # 4 hex digits makes 65536 subdirectories in a single parent
257             our $max_sane_level_fanout= 4;
258             # 6 hex digits creates 16 million directories, more than that is probably a mistake
259             our $max_sane_total_fanout= 6;
260             sub _parse_fanout {
261 23     23   73 my (undef, $fanout)= @_;
262 23         54 chomp($fanout);
263 23         146 my @fanout= split /\s+/, $fanout;
264             # Sanity check on the fanout
265 23         45 my $total_digits= 0;
266 23         56 for (@fanout) {
267 55 50 0     215 if ($_ =~ /^(\d+)$/) {
    0          
268 55         122 $total_digits+= $1;
269 55 100       370 croak "Too large fanout in one directory ($1)" if $1 > $max_sane_level_fanout;
270             } elsif ($_ eq '=' or $_ eq '*') {
271             # indicates full hash for filename, or partial hash
272             # must be the final element
273 0 0       0 \$_ == \$fanout[-1] or croak "Fanout '+' or '=' can only be final element";
274             } else {
275 0         0 croak "Invalid character in fanout specification: '$_'";
276             }
277             }
278 22 100       244 croak "Too many digits of fanout! ($total_digits)" if $total_digits > $max_sane_total_fanout;
279 21         66 return \@fanout;
280             }
281              
282             sub _parse_digest {
283 12     12   52 my (undef, $digest)= @_;
284 12         32 chomp($digest);
285 12 50       73 ($digest =~ /^(\S+)$/)
286             or croak "Invalid digest algorithm name: '$digest'\n";
287 12         47 return $1;
288             }
289              
290             sub _parse_version {
291 12     12   55 my (undef, $version)= @_;
292 12         19 my %versions;
293 12         97 for my $line (split /\r?\n/, $version) {
294 12 50       67 ($line =~ /^([A-Za-z0-9:_]+) ([0-9.]+)/)
295             or croak "Invalid version string: '$line'\n";
296 12         60 $versions{$1}= $2;
297             }
298 12         44 return \%versions;
299             }
300              
301              
302             sub get {
303 33     33 1 2348 my ($self, $hash)= @_;
304 33         83 my $fname= $self->path_for_hash($hash);
305             return undef
306 33 100       926 unless (my ($size, $blksize)= (stat $fname)[7,11]);
307 15         179 return bless {
308             # required
309             store => $self,
310             hash => $hash,
311             size => $size,
312             # extra info
313             block_size => $blksize,
314             local_file => $fname,
315             }, 'DataStore::CAS::Simple::File';
316             }
317              
318              
319             sub put_file {
320 9     9 1 3599 my ($self, $file, $flags)= @_;
321 9         18 my $ref= ref $file;
322 9   100     49 my $is_cas_file= $ref && $ref->isa('DataStore::CAS::File');
323 9         27 my $is_filename= DataStore::CAS::_thing_stringifies_to_filename($file);
324 9 50 0     53 croak "Unhandled argument to put_file: ".($file||'(undef)')
      66        
      66        
325             unless defined $file && ($is_cas_file || $is_filename);
326            
327             # Can only optimize if source is a real file
328 9 100 100     41 if ($flags->{hardlink} || ($flags->{move} && !$is_cas_file)) {
      100        
329 4 50 33     30 my $fname= $is_filename? "$file"
    100          
330             : $is_cas_file && $file->can('local_file')? $file->local_file
331             : undef;
332 4 50 33     69 if ($fname && -f $fname) {
333 4 50       16 my %known_hashes= $flags->{known_hashes}? %{$flags->{known_hashes}} : ();
  0         0  
334             # Apply reuse_hash feature, if requested
335             $known_hashes{$file->store->digest}= $file->hash
336 4 50 66     28 if $is_cas_file && $flags->{reuse_hash};
337             # Calculate the hash if it wasn't given.
338 4   66     11 my $hash= ($known_hashes{$self->digest} ||= $self->calculate_file_hash($fname));
339             # Have it already?
340 4 100       327 if (-f $self->path_for_hash($hash)) {
341             $flags->{stats}{dup_file_count}++
342 1 50       6 if $flags->{stats};
343             $self->_unlink_source_file($file, $flags)
344 1 50       5 if $flags->{move};
345 1         9 return $hash;
346             }
347             # Save hash for next step
348 3         20 $flags= { %$flags, known_hashes => \%known_hashes };
349             # Try the move or hardlink operation. If it fails, it returns false,
350             # and this falls through to the default implementation that copies the
351             # file.
352             return $hash if $flags->{move}
353 3 100       19 ? $self->_try_put_move($fname, $flags)
    50          
354             : $self->_try_put_hardlink($fname, $flags);
355             }
356             }
357             # Else use the default implementation which opens and reads the file.
358 5         18 return DataStore::CAS::put_file($self, $file, $flags);
359             }
360              
361             sub _try_put_move {
362 1     1   4 my ($self, $file, $flags)= @_;
363 1         5 my $hash= $flags->{known_hashes}{$self->digest}; # calculated above
364             # Need to be on same filesystem for this to work.
365 1         5 my $dest= $self->path_for_hash($hash,1);
366 1         6 my $tmp= "$dest.tmp";
367 1 50       40 return 0 unless rename($file, $tmp);
368 1 50 33     10 if (ref $file && ref($file)->isa('File::Temp')) {
369             # File::Temp creates a writable handle, and operates on the
370             # file using 'fd___' functions, so it needs closed to be safe.
371 0         0 $file->close;
372             }
373             # Need to be able to change ownership to current user and remove write bits.
374             try {
375 1 50   1   70 my ($mode, $uid, $gid)= (stat $tmp)[2,4,5]
376             or die "stat($tmp): $!\n";
377 1 50       41 if (!$flags->{dry_run}) {
378 1 50 0     10 chown($>, $), $tmp) or die "chown($> $), $tmp): $!\n"
      33        
      33        
      33        
379             if ($uid && $uid != $>) or ($gid && $gid != $) );
380 1 50 50     24 chmod(0444, $tmp) or die "chmod(0444, $tmp): $!\n"
381             if 0444 != ($mode & 0777);
382 1 50       74 rename($tmp, $dest)
383             or die "rename($tmp, $dest): $!\n";
384             }
385             # record that we added a new hash, if stats enabled.
386 1 50       9 if ($flags->{stats}) {
387 0         0 $flags->{stats}{new_file_count}++;
388 0   0     0 push @{ $flags->{stats}{new_files} ||= [] }, $hash;
  0         0  
389             }
390 1         5 $hash;
391             }
392             catch {
393 0     0   0 warn "Can't optimize CAS insertion with move: $_";
394 0         0 unlink $tmp;
395 0         0 0;
396 1         13 };
397             }
398              
399             sub _try_put_hardlink {
400 2     2   7 my ($self, $file, $flags)= @_;
401 2         7 my $hash= $flags->{known_hashes}{$self->digest}; # calculated above
402             # Refuse to link a file that is writeable by anyone.
403 2         31 my ($mode, $uid, $gid)= (stat $file)[2,4,5];
404 2 50 33     17 defined $mode && !($mode & 0222)
405             or return 0;
406             # Refuse to link a file owned by anyone else other than root
407 2 50 33     23 (!$uid || $uid == $>) and (!$gid || $gid == $))
      33        
      33        
408             or return 0;
409             # looks ok.
410 2         7 my $dest= $self->path_for_hash($hash,1);
411             $flags->{dry_run}
412 2 50 33     89 or link($file, $dest)
413             or return 0;
414             # record that we added a new hash, if stats enabled.
415 2 50       10 if ($flags->{stats}) {
416 0         0 $flags->{stats}{new_file_count}++;
417 0   0     0 push @{ $flags->{stats}{new_files} ||= [] }, $hash;
  0         0  
418             }
419             # it worked
420 2         25 return $hash;
421             }
422              
423              
424             sub new_write_handle {
425 18     18 1 39 my ($self, $flags)= @_;
426 18   100     40 $flags ||= {};
427 18   66     72 my $known_hash= $flags->{known_hashes} && $flags->{known_hashes}{$self->digest};
428 18 100 66     77 $known_hash= undef unless defined $known_hash && length $known_hash;
429             my $data= {
430             wrote => 0,
431             dry_run => $flags->{dry_run},
432             hash => $known_hash,
433             stats => $flags->{stats},
434 18         70 };
435            
436             $data->{dest_file}= File::Temp->new( TEMPLATE => 'temp-XXXXXXXX', DIR => $self->path )
437 18 50       122 unless $data->{dry_run};
438            
439             $data->{digest}= $self->_new_digest
440 18 100       7114 unless defined $data->{hash};
441            
442 18         152 return DataStore::CAS::FileCreatorHandle->new($self, $data);
443             }
444              
445             sub _handle_write {
446 18     18   52 my ($self, $handle, $buffer, $count, $offset)= @_;
447 18         43 my $data= $handle->_data;
448              
449             # Figure out count and offset, then either write or no-op (dry_run).
450 18   50     94 $offset ||= 0;
451 18   66     100 $count ||= length($buffer)-$offset;
452 18 50 50     723 my $wrote= (defined $data->{dest_file})? syswrite( $data->{dest_file}, $buffer, $count, $offset||0 ) : $count;
453              
454             # digest only the bytes that we wrote
455 18 100 66     113 if (defined $wrote and $wrote > 0) {
456 9         37 local $!; # just in case
457 9         23 $data->{wrote} += $wrote;
458             $data->{digest}->add(substr($buffer, $offset, $wrote))
459 9 100       47 if defined $data->{digest};
460             }
461 18         81 return $wrote;
462             }
463              
464             sub _handle_seek {
465 0     0   0 croak "Seek unsupported (for now)"
466             }
467              
468             sub _handle_tell {
469 0     0   0 my ($self, $handle)= @_;
470 0         0 return $handle->_data->{wrote};
471             }
472              
473              
474             sub commit_write_handle {
475 18     18 1 37 my ($self, $handle)= @_;
476 18         45 my $data= $handle->_data;
477            
478             my $hash= defined $data->{hash}?
479             $data->{hash}
480 18 100       99 : $data->{digest}->hexdigest;
481            
482 18         26 my $temp_file= $data->{dest_file};
483 18 50       40 if (defined $temp_file) {
484             # Make sure all data committed
485 18 50       249 close $temp_file
486             or croak "while saving '$temp_file': $!";
487             }
488            
489 18         59 return $self->_commit_file($temp_file, $hash, $data);
490             }
491              
492             sub _commit_file {
493 18     18   50 my ($self, $source_file, $hash, $flags)= @_;
494             # Find the destination file name
495 18         38 my $dest_name= $self->path_for_hash($hash);
496             # Only if we don't have it yet...
497 18 100       232 if (-f $dest_name) {
498 1 50       6 if ($flags->{stats}) {
499 0         0 $flags->{stats}{dup_file_count}++;
500             }
501             }
502             else {
503             # make it read-only
504 17 50       87 chmod(0444, "$source_file") or croak "chmod(0444, $source_file): $!";
505            
506             # Rename it into place
507             # Check for missing directories after the first failure,
508             # in the spirit of keeping the common case fast.
509             $flags->{dry_run}
510 17 50 33     562 or rename("$source_file", $dest_name)
      33        
      33        
511             or ($self->path_for_hash($hash, 1) and rename($source_file, $dest_name))
512             or croak "rename($source_file => $dest_name): $!";
513             # record that we added a new hash, if stats enabled.
514 17 50       1058 if ($flags->{stats}) {
515 0         0 $flags->{stats}{new_file_count}++;
516 0   0     0 push @{ $flags->{stats}{new_files} ||= [] }, $hash;
  0         0  
517             }
518             }
519 18         110 $hash;
520             }
521              
522              
523             sub validate {
524 3     3 1 163 my ($self, $hash)= @_;
525              
526 3         9 my $path= $self->path_for_hash($hash);
527 3 100       318 return undef unless -f $path;
528              
529 2 50       83 open (my $fh, "<:raw", $path)
530             or return 0; # don't die. Errors mean "not valid", even if it might be a permission issue
531 2     0   22 my $hash2= try { $self->_new_digest->addfile($fh)->hexdigest } catch {''};
  2         90  
  0         0  
532 2 100       462 return ($hash eq $hash2? 1 : 0);
533             }
534              
535              
536             sub open_file {
537 2     2 1 9 my ($self, $file, $flags)= @_;
538 2         4 my $mode= '<';
539 2 0 33     5 $mode .= ':'.$flags->{layer} if ($flags && $flags->{layer});
540 2 50       36 open my $fh, $mode, $file->local_file
541             or croak "open: $!";
542 2         19 return $fh;
543             }
544              
545              
546             sub _slurpdir {
547 44     44   196 my ($path, $digits)= @_;
548 44   50     1110 opendir my $dh, $_[0] || die "opendir: $!";
549 44         1106 [ sort grep { length($_) eq $digits } readdir $dh ]
  227         1307  
550             }
551             sub iterator {
552 3     3 1 1511 my ($self, $flags)= @_;
553 3   50     20 $flags ||= {};
554 3         12 my @length= ( $self->fanout_list, length($self->hash_of_null) );
555 3         88 $length[-1] -= $_ for @length[0..($#length-1)];
556 3         14 my $path= "".$self->path;
557 3         13 my @dirstack= ( _slurpdir($path, $length[0]) );
558             return sub {
559 15 50   15   825 return undef unless @dirstack;
560 15         27 while (1) {
561             # back out of a directory hierarchy that we have finished
562 56         117 while (!@{$dirstack[-1]}) {
  97         278  
563 44         74 pop @dirstack; # back out of directory
564 44 100       110 return undef unless @dirstack;
565 41         56 shift @{$dirstack[-1]}; # remove directory name
  41         79  
566             }
567             # Build the name of the next file or directory
568 53         101 my @parts= map { $_->[0] } @dirstack;
  104         310  
569 53         360 my $fname= catfile( $path, @parts );
570             # If a dir, descend into it
571 53 100       773 if (-d $fname) {
572 41         155 push @dirstack, _slurpdir($fname, $length[scalar @dirstack]);
573             } else {
574 12         31 shift @{$dirstack[-1]};
  12         26  
575             # If a file at the correct depth, return it
576 12 50 33     187 if ($#dirstack == $#length && -f $fname) {
577 12         133 return join('', @parts);
578             }
579             }
580             }
581 3         54 };
582             }
583              
584              
585             sub delete {
586 1     1 1 862 my ($self, $digest_hash, $flags)= @_;
587 1         5 my $path= $self->path_for_hash($digest_hash);
588 1 50       23 if (-f $path) {
589             unlink $path || die "unlink: $!"
590 1 50 50     84 unless $flags && $flags->{dry_run};
      33        
591             $flags->{stats}{delete_count}++
592 1 0 33     7 if $flags && $flags->{stats};
593 1         8 return 1;
594             } else {
595             $flags->{stats}{delete_missing}++
596 0 0 0     0 if $flags && $flags->{stats};
597 0         0 return 0;
598             }
599             }
600              
601             # This can be called as class or instance method.
602             # When called as a class method, '$digest_name' is mandatory,
603             # otherwise it is unneeded.
604             sub _new_digest {
605 26     26   52 my ($self, $digest_name)= @_;
606 26   66     146 Digest->new($digest_name || $self->digest);
607             }
608              
609             sub _assert_digest_available {
610 23     23   54 my ($class, $digest)= @_;
611             try {
612 23     23   1054 $class->_new_digest($digest)
613             }
614             catch {
615 0     0   0 s/^/# /mg;
616 0         0 croak "Digest algorithm '$digest' is not available on this system.\n$_\n"
617 23         170 };
618 23         1191 1;
619             }
620              
621             package DataStore::CAS::Simple::File;
622 2     2   19 use strict;
  2         4  
  2         65  
623 2     2   14 use warnings;
  2         3  
  2         77  
624 2     2   12 use parent 'DataStore::CAS::File';
  2         5  
  2         10  
625              
626              
627 13     13   285 sub local_file { $_[0]{local_file} }
628 0     0     sub block_size { $_[0]{block_size} }
629              
630             1; # End of File::CAS::Store::Simple
631              
632             __END__