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   2072 use 5.008;
  2         7  
3 2     2   561 use Moo 1.000007;
  2         11031  
  2         11  
4 2     2   1890 use Carp;
  2         4  
  2         143  
5 2     2   14 use Try::Tiny;
  2         4  
  2         125  
6 2     2   479 use Digest 1.16 ();
  2         622  
  2         66  
7 2     2   12 use File::Spec 3.33;
  2         37  
  2         52  
8 2     2   480 use File::Spec::Functions 'catfile', 'catdir', 'canonpath';
  2         792  
  2         149  
9 2     2   1453 use File::Temp 0.22 ();
  2         32237  
  2         10511  
10              
11             our $VERSION = '0.04';
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 23 sub fanout_list { @{ $_[0]->_config->{fanout} } }
  15         149  
22 97     97 1 595 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 112 my ($self, $args)= @_;
31             my ($create, $ignore_version, $digest, $fanout, $_notest)=
32 16         30 delete @{$args}{'create','ignore_version','digest','fanout','_notest'};
  16         66  
33              
34             # Check for invalid params
35 16         51 my @inval= grep { !$self->can($_) } keys %$args;
  16         84  
36 16 50       50 croak "Invalid parameter: ".join(', ', @inval)
37             if @inval;
38              
39             # Path is required, and must be a directory
40 16         37 my $path= $self->path;
41 16 50       272 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         46 my $setup= 0;
50 16 100       328 unless (-f catfile($path, 'conf', 'VERSION')) {
51 13 100       224 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         77 $self->create_store({ digest => $digest, path => $path, fanout => $fanout });
56 9         47 $setup= 1;
57             }
58              
59 12         74 $self->_set__config( $self->_load_config($path, { ignore_version => $ignore_version }) );
60 12         41 my ($tohex, $split)= _get_hex_and_fanout_functions($self->digest, $self->fanout);
61 12         47 $self->_digest_hash_to_hex($tohex);
62 12         23 $self->_digest_hash_split($split);
63              
64 12 100       27 if ($setup) {
65 9         26 $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         918 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 543 my ($self, $hash, $create_dirs)= @_;
84 81         233 my @parts= $self->_digest_hash_split->($hash);
85 81 100       165 if ($create_dirs) {
86 20         43 my $path= $self->path;
87 20         72 for (@parts[0..($#parts-1)]) {
88 42         286 $path= catdir($path, $_);
89 42 100       608 next if -d $path;
90 40 50       1823 mkdir($path) or croak "mkdir($path): $!";
91             }
92 20         278 return catfile($path, $parts[-1]);
93             } else {
94 61         548 return catfile($self->path, @parts);
95             }
96             }
97              
98              
99             sub create_store {
100 12     12 1 27 my $class= shift;
101 12 50       39 $class= ref $class if ref $class;
102 12 50       32 my %params= (@_ == 1? %{$_[0]} : @_);
  12         62  
103            
104 12 50       43 defined $params{path} or croak "Missing required param 'path'";
105 12 50       156 -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       56 unless $class->_is_dir_empty($params{path});
109              
110 11   100     50 $params{digest} ||= 'SHA-1';
111 11         70 $class->_assert_digest_available($params{digest});
112              
113 11   100     89 $params{fanout} ||= [ 1, 2 ];
114             # make sure the fanout isn't insane
115 11         23 $params{fanout}= $class->_parse_fanout(join(' ',@{$params{fanout}}));
  11         60  
116              
117 9         49 my $conf_dir= catdir($params{path}, 'conf');
118 9 50       497 mkdir($conf_dir) or croak "mkdir($conf_dir): $!";
119 9         54 $class->_write_config_setting($params{path}, 'VERSION', $class->_hierarchy_version);
120 9         91 $class->_write_config_setting($params{path}, 'digest', $params{digest}."\n");
121 9         36 $class->_write_config_setting($params{path}, 'fanout', join(' ', @{$params{fanout}})."\n");
  9         74  
122             }
123             sub _hierarchy_version {
124 9   33 9   41 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         34 my $hier= mro::get_linear_isa($class);
128 9         100 for (grep $_->isa(__PACKAGE__), @$hier) {
129 9 50       100 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         85 $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   33 my ($class, $path, $flags)= @_;
142 12 50       35 $class= ref $class if ref $class;
143 12         36 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         59 $class->_parse_version($class->_read_config_setting($path, 'VERSION'));
149 12 50       32 unless ($flags->{ignore_version}) {
150 12         20 while (my ($pkg, $ver)= each %{$params{storage_format_version}}) {
  24         247  
151 12     12   78 my $cur_ver= try { $pkg->VERSION };
  12         413  
152 12 50       173 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   542 (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         78 $class->_assert_digest_available($params{digest});
165             # Get the directory fan-out specification
166 12         36 $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   29 my ($digest, $fanout)= @_;
172 12         59 my $hexlen= length Digest->new($digest)->add('')->hexdigest;
173 12         507 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         418 };
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       61 my $filename_type= $fanout->[-1] =~ /^[0-9]+$/? '*'
189             : pop @$fanout;
190 12         107 my $re= '^'.join('', map "([0-9a-f]{$_})", map /([0-9]+)/, @$fanout);
191 12 50       49 $re .= '([0-9a-f]+)' if $filename_type eq '*';
192 12         139 $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   132 my $hash= $_[0];
201 81 50 50     274 $hash= $tohex->($hash) if $hexlen != (length($hash) || 0);
202 81 50       720 my @dirs= ($hash =~ $re) or croak "can't split hash '$hash' into requested fanout";
203 81         292 return @dirs;
204             }
205 12 50       63 : croak "Unrecognized filename indicator in fanout specification: '$filename_type'";
    50          
206              
207 12         105 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   32 my (undef, $path)= @_;
224 12 50       367 opendir(my $dh, $path)
225             or die "opendir($path): $!";
226 12 100       437 my @entries= grep { $_ ne '.' and $_ ne '..' } readdir($dh);
  26         190  
227 12         234 closedir($dh);
228 12         288 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   79 my (undef, $path, $name, $content)= @_;
238 27         136 $path= catfile($path, 'conf', $name);
239 27 50       1726 open(my $f, '>', $path)
240             or croak "Failed to open '$path' for writing: $!\n";
241 27 50 33     1247 (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         176 $path= catfile($path, 'conf', $name);
247 36 50       1408 open(my $f, '<', $path)
248             or croak "Failed to read '$path' : $!\n";
249 36         225 local $/= undef;
250 36         834 my $str= <$f>;
251 36 50 33     292 defined $str and length $str or croak "Failed to read '$path' : $!\n";
252 36         724 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   80 my (undef, $fanout)= @_;
261 23         60 chomp($fanout);
262 23         155 my @fanout= split /\s+/, $fanout;
263             # Sanity check on the fanout
264 23         46 my $total_digits= 0;
265 23         59 for (@fanout) {
266 55 50 0     221 if ($_ =~ /^(\d+)$/) {
    0          
267 55         129 $total_digits+= $1;
268 55 100       393 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       300 croak "Too many digits of fanout! ($total_digits)" if $total_digits > $max_sane_total_fanout;
278 21         71 return \@fanout;
279             }
280              
281             sub _parse_digest {
282 12     12   54 my (undef, $digest)= @_;
283 12         33 chomp($digest);
284 12 50       80 ($digest =~ /^(\S+)$/)
285             or croak "Invalid digest algorithm name: '$digest'\n";
286 12         56 return $1;
287             }
288              
289             sub _parse_version {
290 12     12   54 my (undef, $version)= @_;
291 12         16 my %versions;
292 12         101 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         57 $versions{$1}= $2;
296             }
297 12         47 return \%versions;
298             }
299              
300              
301             sub get {
302 33     33 1 2069 my ($self, $hash)= @_;
303 33         83 my $fname= $self->path_for_hash($hash);
304             return undef
305 33 100       798 unless (my ($size, $blksize)= (stat $fname)[7,11]);
306 15         182 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 3478 my ($self, $file, $flags)= @_;
320 9         17 my $ref= ref $file;
321 9   100     50 my $is_cas_file= $ref && $ref->isa('DataStore::CAS::File');
322 9         29 my $is_filename= DataStore::CAS::_thing_stringifies_to_filename($file);
323 9 50 0     46 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     41 if ($flags->{hardlink} || ($flags->{move} && !$is_cas_file)) {
      100        
328 4 50 33     28 my $fname= $is_filename? "$file"
    100          
329             : $is_cas_file && $file->can('local_file')? $file->local_file
330             : undef;
331 4 50 33     99 if ($fname && -f $fname) {
332 4 50       21 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     32 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       334 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       4 if $flags->{move};
344 1         8 return $hash;
345             }
346             # Save hash for next step
347 3         23 $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       19 ? $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   5 my ($self, $file, $flags)= @_;
362 1         4 my $hash= $flags->{known_hashes}{$self->digest}; # calculated above
363             # Need to be on same filesystem for this to work.
364 1         5 my $dest= $self->path_for_hash($hash,1);
365 1         7 my $tmp= "$dest.tmp";
366 1 50       40 return 0 unless rename($file, $tmp);
367 1 50 33     7 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   70 my ($mode, $uid, $gid)= (stat $tmp)[2,4,5]
375             or die "stat($tmp): $!\n";
376 1 50       7 if (!$flags->{dry_run}) {
377 1 50 0     39 chown($>, $), $tmp) or die "chown($> $), $tmp): $!\n"
      33        
      33        
      33        
378             if ($uid && $uid != $>) or ($gid && $gid != $) );
379 1 50 50     26 chmod(0444, $tmp) or die "chmod(0444, $tmp): $!\n"
380             if 0444 != ($mode & 0777);
381 1 50       36 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       44 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         9 $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   6 my ($self, $file, $flags)= @_;
400 2         7 my $hash= $flags->{known_hashes}{$self->digest}; # calculated above
401             # Refuse to link a file that is writeable by anyone.
402 2         34 my ($mode, $uid, $gid)= (stat $file)[2,4,5];
403 2 50 33     15 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     22 (!$uid || $uid == $>) and (!$gid || $gid == $))
      33        
      33        
407             or return 0;
408             # looks ok.
409 2         6 my $dest= $self->path_for_hash($hash,1);
410             $flags->{dry_run}
411 2 50 33     86 or link($file, $dest)
412             or return 0;
413             # record that we added a new hash, if stats enabled.
414 2 50       10 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         25 return $hash;
420             }
421              
422              
423             sub new_write_handle {
424 18     18 1 38 my ($self, $flags)= @_;
425 18   100     44 $flags ||= {};
426 18   66     61 my $known_hash= $flags->{known_hashes} && $flags->{known_hashes}{$self->digest};
427 18 100 66     71 $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         79 };
434            
435             $data->{dest_file}= File::Temp->new( TEMPLATE => 'temp-XXXXXXXX', DIR => $self->path )
436 18 50       122 unless $data->{dry_run};
437            
438             $data->{digest}= $self->_new_digest
439 18 100       6963 unless defined $data->{hash};
440            
441 18         139 return DataStore::CAS::FileCreatorHandle->new($self, $data);
442             }
443              
444             sub _handle_write {
445 18     18   51 my ($self, $handle, $buffer, $count, $offset)= @_;
446 18         51 my $data= $handle->_data;
447              
448             # Figure out count and offset, then either write or no-op (dry_run).
449 18   50     93 $offset ||= 0;
450 18   66     100 $count ||= length($buffer)-$offset;
451 18 50 50     408 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     115 if (defined $wrote and $wrote > 0) {
455 9         40 local $!; # just in case
456 9         24 $data->{wrote} += $wrote;
457             $data->{digest}->add(substr($buffer, $offset, $wrote))
458 9 100       53 if defined $data->{digest};
459             }
460 18         83 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 37 my ($self, $handle)= @_;
475 18         41 my $data= $handle->_data;
476            
477             my $hash= defined $data->{hash}?
478             $data->{hash}
479 18 100       65 : $data->{digest}->hexdigest;
480            
481 18         29 my $temp_file= $data->{dest_file};
482 18 50       51 if (defined $temp_file) {
483             # Make sure all data committed
484 18 50       250 close $temp_file
485             or croak "while saving '$temp_file': $!";
486             }
487            
488 18         69 return $self->_commit_file($temp_file, $hash, $data);
489             }
490              
491             sub _commit_file {
492 18     18   41 my ($self, $source_file, $hash, $flags)= @_;
493             # Find the destination file name
494 18         39 my $dest_name= $self->path_for_hash($hash);
495             # Only if we don't have it yet...
496 18 100       224 if (-f $dest_name) {
497 1 50       8 if ($flags->{stats}) {
498 0         0 $flags->{stats}{dup_file_count}++;
499             }
500             }
501             else {
502             # make it read-only
503 17 50       87 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     611 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       1894 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         115 $hash;
519             }
520              
521              
522             sub validate {
523 3     3 1 120 my ($self, $hash)= @_;
524              
525 3         8 my $path= $self->path_for_hash($hash);
526 3 100       307 return undef unless -f $path;
527              
528 2 50       83 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         90  
  0         0  
531 2 100       406 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     7 $mode .= ':'.$flags->{layer} if ($flags && $flags->{layer});
539 2 50       11 open my $fh, $mode, $file->local_file
540             or croak "open: $!";
541 2         20 return $fh;
542             }
543              
544              
545             sub _slurpdir {
546 44     44   245 my ($path, $digits)= @_;
547 44   50     1105 opendir my $dh, $_[0] || die "opendir: $!";
548 44         954 [ sort grep { length($_) eq $digits } readdir $dh ]
  227         1317  
549             }
550             sub iterator {
551 3     3 1 1235 my ($self, $flags)= @_;
552 3   50     19 $flags ||= {};
553 3         12 my @length= ( $self->fanout_list, length($self->hash_of_null) );
554 3         88 $length[-1] -= $_ for @length[0..($#length-1)];
555 3         18 my $path= "".$self->path;
556 3         12 my @dirstack= ( _slurpdir($path, $length[0]) );
557             return sub {
558 15 50   15   766 return undef unless @dirstack;
559 15         26 while (1) {
560             # back out of a directory hierarchy that we have finished
561 56         123 while (!@{$dirstack[-1]}) {
  97         224  
562 44         64 pop @dirstack; # back out of directory
563 44 100       113 return undef unless @dirstack;
564 41         59 shift @{$dirstack[-1]}; # remove directory name
  41         79  
565             }
566             # Build the name of the next file or directory
567 53         104 my @parts= map { $_->[0] } @dirstack;
  104         306  
568 53         368 my $fname= catfile( $path, @parts );
569             # If a dir, descend into it
570 53 100       812 if (-d $fname) {
571 41         183 push @dirstack, _slurpdir($fname, $length[scalar @dirstack]);
572             } else {
573 12         32 shift @{$dirstack[-1]};
  12         27  
574             # If a file at the correct depth, return it
575 12 50 33     189 if ($#dirstack == $#length && -f $fname) {
576 12         172 return join('', @parts);
577             }
578             }
579             }
580 3         56 };
581             }
582              
583              
584             sub delete {
585 1     1 1 749 my ($self, $digest_hash, $flags)= @_;
586 1         4 my $path= $self->path_for_hash($digest_hash);
587 1 50       23 if (-f $path) {
588             unlink $path || die "unlink: $!"
589 1 50 50     68 unless $flags && $flags->{dry_run};
      33        
590             $flags->{stats}{delete_count}++
591 1 0 33     6 if $flags && $flags->{stats};
592 1         9 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   55 my ($self, $digest_name)= @_;
605 26   66     142 Digest->new($digest_name || $self->digest);
606             }
607              
608             sub _assert_digest_available {
609 23     23   59 my ($class, $digest)= @_;
610             try {
611 23     23   1073 $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         177 };
617 23         4983 1;
618             }
619              
620             package DataStore::CAS::Simple::File;
621 2     2   32 use strict;
  2         4  
  2         65  
622 2     2   11 use warnings;
  2         5  
  2         98  
623 2     2   13 use parent 'DataStore::CAS::File';
  2         6  
  2         11  
624              
625              
626 13     13   308 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__