File Coverage

blib/lib/DataStore/CAS/Simple.pm
Criterion Covered Total %
statement 292 340 85.8
branch 111 196 56.6
condition 61 139 43.8
subroutine 48 59 81.3
pod 14 15 93.3
total 526 749 70.2


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