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 48 59 81.3
pod 14 15 93.3
total 528 752 70.2


line stmt bran cond sub pod time code
1             package DataStore::CAS::Simple;
2 2     2   371205 use 5.008;
  2         8  
3 2     2   466 use Moo 1.000007;
  2         5770  
  2         14  
4 2     2   1836 use Carp;
  2         4  
  2         145  
5 2     2   13 use Try::Tiny;
  2         4  
  2         143  
6 2     2   364 use Digest 1.16 ();
  2         636  
  2         54  
7 2     2   11 use File::Spec 3.33;
  2         36  
  2         62  
8 2     2   618 use File::Spec::Functions 'catfile', 'catdir', 'canonpath';
  2         1049  
  2         158  
9 2     2   1860 use File::Temp 0.22 ();
  2         31251  
  2         13636  
10              
11             our $VERSION = '0.08';
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 48 sub fanout { [ $_[0]->fanout_list ] }
21 15     15 1 36 sub fanout_list { @{ $_[0]->_config->{fanout} } }
  15         249  
22 103     103 1 942 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 189 my ($self, $args)= @_;
31             my ($create, $ignore_version, $digest, $fanout, $_notest)=
32 16         51 delete @{$args}{'create','ignore_version','digest','fanout','_notest'};
  16         97  
33              
34             # Check for invalid params
35 16         62 my @inval= grep { !$self->can($_) } keys %$args;
  16         131  
36 16 50       62 carp "Invalid parameter: ".join(', ', @inval)
37             if @inval;
38              
39             # Path is required, and must be a directory
40 16         92 my $path= $self->path;
41 16 50       436 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       656 unless (-f catfile($path, 'conf', 'VERSION')) {
51 13 100       364 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         123 $self->create_store({ digest => $digest, path => $path, fanout => $fanout });
56 9         52 $setup= 1;
57             }
58              
59 12         93 $self->_set__config( $self->_load_config($path, { ignore_version => $ignore_version }) );
60 12         66 my ($tohex, $split)= _get_hex_and_fanout_functions($self->digest, $self->fanout);
61 12         59 $self->_digest_hash_to_hex($tohex);
62 12         44 $self->_digest_hash_split($split);
63              
64 12 100       29 if ($setup) {
65 9         57 $self->put('');
66             } else {
67             # Properly initialized CAS will always contain an entry for the empty string
68 3 100       121 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         49 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 83     83 1 529 my ($self, $hash, $create_dirs)= @_;
84 83         321 my @parts= $self->_digest_hash_split->($hash);
85 83 100       183 if ($create_dirs) {
86 20         63 my $path= $self->path;
87 20         126 for (@parts[0..($#parts-1)]) {
88 42         263 $path= catdir($path, $_);
89 42 100       1182 next if -d $path;
90 40 50       4639 mkdir($path) or croak "mkdir($path): $!";
91             }
92 20         468 return catfile($path, $parts[-1]);
93             } else {
94 63         785 return catfile($self->path, @parts);
95             }
96             }
97              
98              
99             sub create_store {
100 12     12 1 26 my $class= shift;
101 12 50       47 $class= ref $class if ref $class;
102 12 50       41 my %params= (@_ == 1? %{$_[0]} : @_);
  12         60  
103            
104 12 50       54 defined $params{path} or croak "Missing required param 'path'";
105 12 50       250 -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       73 unless $class->_is_dir_empty($params{path});
109              
110 11   100     55 $params{digest} ||= 'SHA-1';
111 11         102 $class->_assert_digest_available($params{digest});
112              
113 11   100     73 $params{fanout} ||= [ 1, 2 ];
114             # make sure the fanout isn't insane
115 11         23 $params{fanout}= $class->_parse_fanout(join(' ',@{$params{fanout}}));
  11         76  
116              
117 9         84 my $conf_dir= catdir($params{path}, 'conf');
118 9 50       1022 mkdir($conf_dir) or croak "mkdir($conf_dir): $!";
119 9         68 $class->_write_config_setting($params{path}, 'VERSION', $class->_hierarchy_version);
120 9         120 $class->_write_config_setting($params{path}, 'digest', $params{digest}."\n");
121 9         38 $class->_write_config_setting($params{path}, 'fanout', join(' ', @{$params{fanout}})."\n");
  9         106  
122             }
123             sub _hierarchy_version {
124 9   33 9   64 my $class= ref $_[0] || $_[0];
125 9         18 my $out= '';
126             # record the version of any class hierarchy which "isa DataStore::CAS::Simple"
127 9 50       30 require MRO::Compat if $] < 5.010;
128 9         64 my $hier= mro::get_linear_isa($class);
129 9         177 for (grep $_->isa(__PACKAGE__), @$hier) {
130 9 50       145 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         136 $out .= $_ . ' ' . $_->VERSION . "\n";
134             }
135             }
136 9         61 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   45 my ($class, $path, $flags)= @_;
143 12 50       56 $class= ref $class if ref $class;
144 12         23 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         78 $class->_parse_version($class->_read_config_setting($path, 'VERSION'));
150 12 50       47 unless ($flags->{ignore_version}) {
151 12         24 while (my ($pkg, $ver)= each %{$params{storage_format_version}}) {
  24         308  
152 12     12   106 my $cur_ver= try { $pkg->VERSION };
  12         582  
153 12 50       218 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     12   624 (try { $pkg->VERSION($ver); 1; } catch { 0 })
  12         52  
  0         0  
157 12 50       77 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         41 $class->_parse_digest($class->_read_config_setting($path, 'digest'));
165 12         61 $class->_assert_digest_available($params{digest});
166             # Get the directory fan-out specification
167 12         46 $params{fanout}= $class->_parse_fanout($class->_read_config_setting($path, 'fanout'));
168 12         84 return \%params;
169             }
170              
171             sub _get_hex_and_fanout_functions {
172 12     12   45 my ($digest, $fanout)= @_;
173 12         87 my $hexlen= length Digest->new($digest)->add('')->hexdigest;
174 12         665 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         571 };
184              
185             # Create a function that splits a digest into the path components
186             # for the CAS file.
187 12         52 $fanout= [ @$fanout ];
188             # final component might be a character indicating full-name or remainder-name
189 12 50       150 my $filename_type= $fanout->[-1] =~ /^[0-9]+$/? '*'
190             : pop @$fanout;
191 12         180 my $re= '^'.join('', map "([0-9a-f]{$_})", map /([0-9]+)/, @$fanout);
192 12 50       66 $re .= '([0-9a-f]+)' if $filename_type eq '*';
193 12         302 $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 83     83   171 my $hash= $_[0];
202 83 50 50     296 $hash= $tohex->($hash) if $hexlen != (length($hash) || 0);
203 83 50       990 my @dirs= ($hash =~ $re) or croak "can't split hash '$hash' into requested fanout";
204 83         338 return @dirs;
205             }
206 12 50       119 : croak "Unrecognized filename indicator in fanout specification: '$filename_type'";
    50          
207              
208 12         68 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   40 my (undef, $path)= @_;
225 12 50       534 opendir(my $dh, $path)
226             or die "opendir($path): $!";
227 12 100       464 my @entries= grep { $_ ne '.' and $_ ne '..' } readdir($dh);
  26         201  
228 12         174 closedir($dh);
229 12         401 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   92 my (undef, $path, $name, $content)= @_;
239 27         207 $path= catfile($path, 'conf', $name);
240 27 50       5695 open(my $f, '>', $path)
241             or croak "Failed to open '$path' for writing: $!\n";
242 27 50 33     1752 (print $f $content) && (close $f)
243             or croak "Failed while writing '$path': $!\n";
244             }
245             sub _read_config_setting {
246 36     36   111 my (undef, $path, $name)= @_;
247 36         217 $path= catfile($path, 'conf', $name);
248 36 50       1674 open(my $f, '<', $path)
249             or croak "Failed to read '$path' : $!\n";
250 36         210 local $/= undef;
251 36         1002 my $str= <$f>;
252 36 50 33     296 defined $str and length $str or croak "Failed to read '$path' : $!\n";
253 36         791 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   110 my (undef, $fanout)= @_;
262 23         93 chomp($fanout);
263 23         113 my @fanout= split /\s+/, $fanout;
264             # Sanity check on the fanout
265 23         53 my $total_digits= 0;
266 23         69 for (@fanout) {
267 55 50 0     264 if ($_ =~ /^(\d+)$/) {
    0          
268 55         131 $total_digits+= $1;
269 55 100       516 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       397 croak "Too many digits of fanout! ($total_digits)" if $total_digits > $max_sane_total_fanout;
279 21         80 return \@fanout;
280             }
281              
282             sub _parse_digest {
283 12     12   53 my (undef, $digest)= @_;
284 12         37 chomp($digest);
285 12 50       108 ($digest =~ /^(\S+)$/)
286             or croak "Invalid digest algorithm name: '$digest'\n";
287 12         87 return $1;
288             }
289              
290             sub _parse_version {
291 12     12   52 my (undef, $version)= @_;
292 12         22 my %versions;
293 12         130 for my $line (split /\r?\n/, $version) {
294 12 50       85 ($line =~ /^([A-Za-z0-9:_]+) ([0-9.]+)/)
295             or croak "Invalid version string: '$line'\n";
296 12         95 $versions{$1}= $2;
297             }
298 12         60 return \%versions;
299             }
300              
301              
302             sub get {
303 35     35 1 1527 my ($self, $hash)= @_;
304 35         134 my $fname= $self->path_for_hash($hash);
305             return undef
306 35 100       1976 unless (my ($size, $blksize)= (stat $fname)[7,11]);
307 17         290 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 11     11 1 4623 my ($self, $file, $flags)= @_;
321 11         28 my $ref= ref $file;
322 11   100     86 my $is_cas_file= $ref && $ref->isa('DataStore::CAS::File');
323 11         47 my $is_filename= DataStore::CAS::_thing_stringifies_to_filename($file);
324 11 50 0     85 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 11 100 100     68 if ($flags->{hardlink} || ($flags->{move} && !$is_cas_file)) {
      100        
329 4 50 33     49 my $fname= $is_filename? "$file"
    100          
330             : $is_cas_file && $file->can('local_file')? $file->local_file
331             : undef;
332 4 50 33     104 if ($fname && -f $fname) {
333 4 50       24 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     44 if $is_cas_file && $flags->{reuse_hash};
337             # Calculate the hash if it wasn't given.
338 4   66     13 my $hash= ($known_hashes{$self->digest} ||= $self->calculate_file_hash($fname));
339             # Have it already?
340 4 100       437 if (-f $self->path_for_hash($hash)) {
341             $flags->{stats}{dup_file_count}++
342 1 50       7 if $flags->{stats};
343             $self->_unlink_source_file($file, $flags)
344 1 50       6 if $flags->{move};
345 1         14 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       23 ? $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 7         26 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         4 my $hash= $flags->{known_hashes}{$self->digest}; # calculated above
364             # Need to be on same filesystem for this to work.
365 1         4 my $dest= $self->path_for_hash($hash,1);
366 1         3 my $tmp= "$dest.tmp";
367 1 50       97 return 0 unless rename($file, $tmp);
368 1 50 33     6 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   111 my ($mode, $uid, $gid)= (stat $tmp)[2,4,5]
376             or die "stat($tmp): $!\n";
377 1 50       5 if (!$flags->{dry_run}) {
378 1 50 0     9 chown($>, $), $tmp) or die "chown($> $), $tmp): $!\n"
      33        
      33        
      33        
379             if ($uid && $uid != $>) or ($gid && $gid != $) );
380 1 50 50     20 chmod(0444, $tmp) or die "chmod(0444, $tmp): $!\n"
381             if 0444 != ($mode & 0777);
382 1 50       114 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       8 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         9 $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         19 };
397             }
398              
399             sub _try_put_hardlink {
400 2     2   6 my ($self, $file, $flags)= @_;
401 2         8 my $hash= $flags->{known_hashes}{$self->digest}; # calculated above
402             # Refuse to link a file that is writeable by anyone.
403 2         37 my ($mode, $uid, $gid)= (stat $file)[2,4,5];
404 2 50 33     13 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     14 (!$uid || $uid == $>) and (!$gid || $gid == $))
      33        
      33        
408             or return 0;
409             # looks ok.
410 2         6 my $dest= $self->path_for_hash($hash,1);
411             $flags->{dry_run}
412 2 50 33     1327 or link($file, $dest)
413             or return 0;
414             # record that we added a new hash, if stats enabled.
415 2 50       11 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         29 return $hash;
421             }
422              
423              
424             sub new_write_handle {
425 18     18 1 55 my ($self, $flags)= @_;
426 18   100     57 $flags ||= {};
427 18   66     99 my $known_hash= $flags->{known_hashes} && $flags->{known_hashes}{$self->digest};
428 18 100 66     101 $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         122 };
435            
436             $data->{dest_file}= File::Temp->new( TEMPLATE => 'temp-XXXXXXXX', DIR => $self->path )
437 18 50       239 unless $data->{dry_run};
438            
439             $data->{digest}= $self->_new_digest
440 18 100       11599 unless defined $data->{hash};
441            
442 18         432 return DataStore::CAS::FileCreatorHandle->new($self, $data);
443             }
444              
445             sub _handle_write {
446 18     18   63 my ($self, $handle, $buffer, $count, $offset)= @_;
447 18         59 my $data= $handle->_data;
448              
449             # Figure out count and offset, then either write or no-op (dry_run).
450 18   50     114 $offset ||= 0;
451 18   66     139 $count ||= length($buffer)-$offset;
452 18 50 50     648 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     136 if (defined $wrote and $wrote > 0) {
456 9         46 local $!; # just in case
457 9         28 $data->{wrote} += $wrote;
458             $data->{digest}->add(substr($buffer, $offset, $wrote))
459 9 100       69 if defined $data->{digest};
460             }
461 18         92 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 53 my ($self, $handle)= @_;
476 18         49 my $data= $handle->_data;
477            
478             my $hash= defined $data->{hash}?
479             $data->{hash}
480 18 100       103 : $data->{digest}->hexdigest;
481            
482 18         38 my $temp_file= $data->{dest_file};
483 18 50       54 if (defined $temp_file) {
484             # Make sure all data committed
485 18 50       299 close $temp_file
486             or croak "while saving '$temp_file': $!";
487             }
488            
489 18         102 return $self->_commit_file($temp_file, $hash, $data);
490             }
491              
492             sub _commit_file {
493 18     18   62 my ($self, $source_file, $hash, $flags)= @_;
494             # Find the destination file name
495 18         69 my $dest_name= $self->path_for_hash($hash);
496             # Only if we don't have it yet...
497 18 100       255 if (-f $dest_name) {
498 1 50       7 if ($flags->{stats}) {
499 0         0 $flags->{stats}{dup_file_count}++;
500             }
501             }
502             else {
503             # make it read-only
504 17 50       104 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     700 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       19448 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         165 $hash;
520             }
521              
522              
523             sub validate {
524 3     3 1 230 my ($self, $hash)= @_;
525              
526 3         12 my $path= $self->path_for_hash($hash);
527 3 100       486 return undef unless -f $path;
528              
529 2 50       98 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   25 my $hash2= try { $self->_new_digest->addfile($fh)->hexdigest } catch {''};
  2         128  
  0         0  
532 2 100       643 return ($hash eq $hash2? 1 : 0);
533             }
534              
535              
536             sub open_file {
537 2     2 1 7 my ($self, $file, $flags)= @_;
538 2         4 my $mode= '<';
539 2 0 33     8 $mode .= ':'.$flags->{layer} if ($flags && $flags->{layer});
540 2 50       14 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   164 my ($path, $digits)= @_;
548 44   50     1225 opendir my $dh, $_[0] || die "opendir: $!";
549 44         989 [ sort grep { length($_) eq $digits } readdir $dh ]
  227         1249  
550             }
551             sub iterator {
552 3     3 1 2508 my ($self, $flags)= @_;
553 3   50     29 $flags ||= {};
554 3         18 my @length= ( $self->fanout_list, length($self->hash_of_null) );
555 3         115 $length[-1] -= $_ for @length[0..($#length-1)];
556 3         16 my $path= "".$self->path;
557 3         18 my @dirstack= ( _slurpdir($path, $length[0]) );
558             return sub {
559 15 50   15   1397 return undef unless @dirstack;
560 15         26 while (1) {
561             # back out of a directory hierarchy that we have finished
562 56         108 while (!@{$dirstack[-1]}) {
  97         274  
563 44         74 pop @dirstack; # back out of directory
564 44 100       126 return undef unless @dirstack;
565 41         74 shift @{$dirstack[-1]}; # remove directory name
  41         97  
566             }
567             # Build the name of the next file or directory
568 53         114 my @parts= map { $_->[0] } @dirstack;
  104         339  
569 53         415 my $fname= catfile( $path, @parts );
570             # If a dir, descend into it
571 53 100       863 if (-d $fname) {
572 41         132 push @dirstack, _slurpdir($fname, $length[scalar @dirstack]);
573             } else {
574 12         25 shift @{$dirstack[-1]};
  12         29  
575             # If a file at the correct depth, return it
576 12 50 33     247 if ($#dirstack == $#length && -f $fname) {
577 12         177 return join('', @parts);
578             }
579             }
580             }
581 3         69 };
582             }
583              
584              
585             sub delete {
586 1     1 1 1320 my ($self, $digest_hash, $flags)= @_;
587 1         8 my $path= $self->path_for_hash($digest_hash);
588 1 50       30 if (-f $path) {
589             unlink $path || die "unlink: $!"
590 1 50 50     175 unless $flags && $flags->{dry_run};
      33        
591             $flags->{stats}{delete_count}++
592 1 0 33     7 if $flags && $flags->{stats};
593 1         10 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   66 my ($self, $digest_name)= @_;
606 26   66     246 Digest->new($digest_name || $self->digest);
607             }
608              
609             sub _assert_digest_available {
610 23     23   64 my ($class, $digest)= @_;
611             try {
612 23     23   1321 $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         235 };
618 23         1681 1;
619             }
620              
621             package DataStore::CAS::Simple::File;
622 2     2   23 use strict;
  2         6  
  2         67  
623 2     2   11 use warnings;
  2         5  
  2         192  
624 2     2   15 use parent 'DataStore::CAS::File';
  2         5  
  2         29  
625              
626              
627 13     13   356 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__