File Coverage

blib/lib/DataStore/CAS.pm
Criterion Covered Total %
statement 137 197 69.5
branch 46 104 44.2
condition 36 91 39.5
subroutine 37 63 58.7
pod 26 26 100.0
total 282 481 58.6


line stmt bran cond sub pod time code
1             package DataStore::CAS;
2 5     5   12472 use 5.008;
  5         20  
3 5     5   1980 use Moo::Role;
  5         59695  
  5         27  
4 5     5   1909 use Carp;
  5         10  
  5         268  
5 5     5   538 use Try::Tiny;
  5         1392  
  5         8296  
6             require Scalar::Util;
7             require Symbol;
8              
9             our $VERSION= '0.05';
10             our @CARP_NOT= qw( DataStore::CAS::File DataStore::CAS::VirtualHandle );
11              
12             # ABSTRACT: Abstract base class for Content Addressable Storage
13              
14              
15             requires 'digest';
16              
17             has hash_of_null => ( is => 'lazy' );
18              
19             sub _build_hash_of_null {
20 6     6   114 return shift->calculate_hash('');
21             }
22              
23              
24             requires 'get';
25              
26              
27             sub _thing_stringifies_to_filename {
28 18     18   35 my $ref= ref $_[0];
29 18 50 66     234 (!$ref && defined $_[0])
      66        
      66        
30             || $ref->isa('Path::Class::File')
31             || $ref->isa('Path::Tiny')
32             || $ref->isa('File::Temp')
33             }
34              
35             sub put {
36 23     23 1 7749 my $ref= ref $_[1];
37 23 100 66     148 goto $_[0]->can('put_scalar')
38             if !$ref || $ref eq 'SCALAR';
39 5 100 100     63 goto $_[0]->can('put_file')
40             if $ref->isa('DataStore::CAS::File')
41             or _thing_stringifies_to_filename($_[1]);
42 1 50 33     18 goto $_[0]->can('put_handle')
43             if $ref->isa('IO::Handle')
44             or Scalar::Util::reftype($_[1]) eq 'GLOB';
45 0         0 croak("Can't 'put' object of type $ref");
46             }
47              
48              
49             sub put_scalar {
50 17     17 1 44 my ($self, undef, $flags)= @_;
51 17 50       43 my $ref= ref $_[1] eq 'SCALAR'? $_[1] : \$_[1];
52              
53             # Force to plain string if it is an object
54 17 50       45 if (ref $$ref) {
55             # TODO: croak unless object has stringify magic
56 0         0 $ref= \"$$ref";
57             }
58              
59             # Can only 'put' octets, not wide-character unicode strings.
60 17 50       57 utf8::downgrade($$ref, 1)
61             or croak "scalar must be byte string (octets). If storing unicode,"
62             ." you must reduce to a byte encoding first.";
63              
64             my $hash= $flags && $flags->{known_hashes} && $flags->{known_hashes}{$self->digest}
65 17 50 33     71 ? $flags->{known_hashes}{$self->digest}
66             : $self->calculate_hash($ref);
67 17 100       60 if ($self->get($hash)) {
68             # Already have it
69             $flags->{stats}{dup_file_count}++
70 1 50       5 if $flags->{stats};
71 1         7 return $hash;
72             } else {
73 16 100       73 $flags= { ($flags? %$flags : ()), known_hashes => { $self->digest => $hash } };
74 16         62 my $handle= $self->new_write_handle($flags);
75 16         60 $handle->_write_all($$ref);
76 16         50 return $self->commit_write_handle($handle);
77             }
78             }
79              
80              
81             sub put_file {
82 6     6 1 16 my ($self, $file, $flags)= @_;
83 6         14 my $ref= ref $file;
84 6   100     26 my $is_cas_file= $ref && $ref->isa('DataStore::CAS::File');
85 6         13 my $is_filename= _thing_stringifies_to_filename($file);
86 6 50 0     31 croak "Unhandled argument to put_file: ".($file||'(undef)')
      66        
      66        
87             unless defined $file && ($is_cas_file || $is_filename);
88              
89 6 50       22 my %known_hashes= $flags->{known_hashes}? %{$flags->{known_hashes}} : ();
  0         0  
90             # Apply reuse_hash feature, if requested
91 6 50 66     23 if ($is_cas_file && $flags->{reuse_hash}) {
92 0         0 $known_hashes{$file->store->digest}= $file->hash;
93 0         0 $flags= { %$flags, known_hashes => \%known_hashes };
94             }
95             # It is probably better to read a file twice than to write one that
96             # doesn't need to be written.
97             # ...but can't do better than ->put_handle unless the file is a real file.
98 6 50 33     44 my $fname= $is_filename? "$file"
    100          
99             : $is_cas_file && $file->can('local_file')? $file->local_file
100             : undef;
101 6 50 33     77 if ($known_hashes{$self->digest} || (defined $fname && -f $fname)) {
      33        
102             # Calculate the hash if it wasn't given.
103 6   33     39 my $hash= ($known_hashes{$self->digest} ||= $self->calculate_file_hash($fname));
104             # Avoid unnecessary work
105 6 100       707 if ($self->get($hash)) {
106             $flags->{stats}{dup_file_count}++
107 5 50       20 if $flags->{stats};
108             $self->_unlink_source_file($file, $flags)
109 5 50 33     18 if $flags->{move} && defined $fname;
110 5         23 return $hash;
111             }
112             # Save hash for next step
113 1         9 $flags= { %$flags, known_hashes => \%known_hashes };
114             }
115 1         4 my $fh;
116 1 50 0     5 if ($is_cas_file) {
    0          
    0          
117 1 50       3 $fh= $file->open or croak "Can't open '$file': $!";
118             }
119             elsif ($ref && $ref->can('openr')) {
120 0 0       0 $fh= $file->openr or croak "Can't open '$file': $!";
121             }
122             elsif ($is_filename) {
123 0 0       0 open($fh, '<', $fname) or croak "Can't open '$fname': $!";
124             }
125             else {
126 0         0 croak "Don't know how to open '$file'";
127             }
128 1         5 my $hash= $self->put_handle($fh, $flags);
129             $self->_unlink_source_file($file, $flags)
130 1 50 33     99 if $hash && $flags->{move};
131 0         0 return $hash;
132             }
133              
134             sub _unlink_source_file {
135 1     1   4 my ($self, $file, $flags)= @_;
136 1 50       3 return if $flags->{dry_run};
137 1   33     11 my $is_cas_file= ref $file && ref($file)->isa('DataStore::CAS::File');
138 1 50       4 if ($is_cas_file) {
139 1         242 croak "Refusing to delete origin CAS File (this can damage a CAS)\n"
140             ."If you really want to do this, pass \$file->local_name and then"
141             ." delete the cas entry yourself.";
142             } else {
143 0 0 0     0 if (ref $file && ref($file)->isa('File::Temp')) {
144             # The Simple backend closes File::Temp files to ensure they don't
145             # get written to any more. so match that behavior here.
146 0         0 $file->close;
147             }
148 0 0       0 unlink "$file" or croak "unlink($file): $!"
149             }
150             }
151              
152              
153             sub put_handle {
154 2     2 1 8 my ($self, $h_in, $flags)= @_;
155 2         5 binmode $h_in;
156 2         7 my $h_out= $self->new_write_handle($flags);
157 2   50     10 my $buf_size= $flags->{buffer_size} || 1024*1024;
158 2         6 my $buf;
159 2         3 while(1) {
160 4         53 my $got= read($h_in, $buf, $buf_size);
161 4 100       22 if ($got) {
    50          
162 2 50       10 $h_out->_write_all($buf) or croak "write: $!";
163             } elsif (!defined $got) {
164 0 0 0     0 next if ($!{EINTR} || $!{EAGAIN});
165 0         0 croak "read: $!";
166             } else {
167 2         7 last;
168             }
169             }
170 2         8 return $self->commit_write_handle($h_out);
171             }
172              
173              
174             # This implementation probably needs overridden by subclasses.
175             sub new_write_handle {
176 0     0 1 0 my ($self, $flags)= @_;
177 0         0 return DataStore::CAS::FileCreatorHandle->new($self, { flags => $flags });
178             }
179              
180             # This must be implemented by subclasses
181             requires 'commit_write_handle';
182              
183              
184             sub calculate_hash {
185 23     23 1 45 my $self= shift;
186 23 100       78 Digest->new($self->digest)->add(ref $_[0]? ${$_[0]} : $_[0])->hexdigest;
  17         706  
187             }
188              
189             sub calculate_file_hash {
190 9     9 1 31 my ($self, $file)= @_;
191 9 50       334 open my $fh, '<', $file or croak "open($file): $!";
192 9         37 binmode $fh;
193 9         45 Digest->new($self->digest)->addfile($fh)->hexdigest;
194             }
195              
196              
197             sub validate {
198 0     0 1 0 my ($self, $hash, $flags)= @_;
199              
200 0         0 my $file= $self->get($hash);
201 0 0       0 return undef unless defined $file;
202              
203             # Exceptions during 'put' will most likely come from reading $file,
204             # which means that validation fails, and we return false.
205 0         0 my $new_hash;
206             try {
207             # We don't pass flags directly through to get/put, because flags for validate
208             # are not the same as flags for get or put. But, 'stats' is a standard thing.
209 0     0   0 my %args= ( dry_run => 1 );
210 0 0       0 $args{stats}= $flags->{stats} if $flags->{stats};
211 0         0 $new_hash= $self->put_handle($file, \%args);
212             }
213       0     catch {
214 0         0 };
215 0 0 0     0 return (defined $new_hash and $new_hash eq $hash)? 1 : 0;
216             }
217              
218              
219             requires 'delete';
220              
221              
222             requires 'iterator';
223              
224              
225             requires 'open_file';
226              
227             # File and Handle objects have DESTROY methods that call these methods of
228             # their associated CAS. The CAS should implement these for cleanup of
229             # temporary files, or etc.
230       17     sub _file_destroy {}
231       18     sub _handle_destroy {}
232              
233             package DataStore::CAS::File;
234 5     5   42 use strict;
  5         9  
  5         143  
235 5     5   30 use warnings;
  5         11  
  5         1915  
236              
237             our $VERSION= '0.05';
238              
239 4     4 1 599 sub store { $_[0]{store} }
240 5     5 1 51 sub hash { $_[0]{hash} }
241 3     3 1 20 sub size { $_[0]{size} }
242              
243             sub open {
244 4     4 1 10 my $self= shift;
245 4 100       36 return $self->{store}->open_file($self)
246             if @_ == 0;
247 1 50       4 return $self->{store}->open_file($self, { @_ })
248             if @_ > 1;
249 1 50 33     13 return $self->{store}->open_file($self, { layer => $_[0] })
250             if @_ == 1 and !ref $_[0];
251 0         0 Carp::croak "Wrong arguments to 'open'";
252             };
253              
254             sub DESTROY {
255 18     18   4739 $_[0]{store}->_file_destroy(@_);
256             }
257              
258             our $AUTOLOAD;
259             sub AUTOLOAD {
260 4     4   1213 my $attr= substr($AUTOLOAD, rindex($AUTOLOAD, ':')+1);
261 4 100       20 return $_[0]{$attr} if exists $_[0]{$attr};
262 2         7 unshift @_, $_[0]{store};
263             goto (
264 2   66     166 $_[0]->can("_file_$attr")
265             or Carp::croak "Can't locate object method \"_file_$attr\" via package \"".ref($_[0]).'"'
266             );
267             }
268              
269             package DataStore::CAS::VirtualHandle;
270 5     5   55 use strict;
  5         11  
  5         156  
271 5     5   31 use warnings;
  5         10  
  5         4768  
272              
273             our $VERSION= '0.05';
274              
275             sub new {
276 19     19 1 709 my ($class, $cas, $fields)= @_;
277 19         61 my $glob= bless Symbol::gensym(), $class;
278 19         268 ${*$glob}= $cas;
  19         48  
279 19 50       38 %{*$glob}= %{$fields||{}};
  19         67  
  19         75  
280 19         118 tie *$glob, $glob;
281 19         72 $glob;
282             }
283 19     19   51 sub TIEHANDLE { return $_[0]; }
284              
285 19     19   413 sub _cas { ${*${$_[0]}} } # the scalar view of the symbol points to the CAS object
  19         25  
  19         137  
286 39     39   83 sub _data { \%{*${$_[0]}} } # the hashref view of the symbol holds the fields of the handle
  39         56  
  39         146  
287              
288 19     19   722 sub DESTROY { unshift @_, ${*{$_[0]}}; goto $_[0]->can('_handle_destroy') }
  19         25  
  19         93  
  19         112  
289              
290             # By default, any method not defined will call to C<$cas->_handle_$method( $handle, @args );>
291             our $AUTOLOAD;
292             sub AUTOLOAD {
293 4     4   1660 unshift @_, ${*${$_[0]}}; # unshift @_, $self->_cas
  4         7  
  4         18  
294 4         16 my $attr= substr($AUTOLOAD, rindex($AUTOLOAD, ':')+1);
295             goto (
296 4   66     180 $_[0]->can("_handle_$attr")
297             or Carp::croak "Can't locate object method \"_handle_$attr\" via package \"".ref($_[0]).'"'
298             );
299             }
300              
301             #
302             # Tied filehandle API
303             #
304              
305 1     1   1079 sub READ { (shift)->read(@_) }
306 0 0   0   0 sub READLINE { wantarray? (shift)->getlines : (shift)->getline }
307 0     0   0 sub GETC { $_[0]->getc }
308 0     0   0 sub EOF { $_[0]->eof }
309              
310 0     0   0 sub WRITE { (shift)->write(@_) }
311 0     0   0 sub PRINT { (shift)->print(@_) }
312 0     0   0 sub PRINTF { (shift)->printf(@_) }
313              
314 0     0   0 sub SEEK { (shift)->seek(@_) }
315 0     0   0 sub TELL { (shift)->tell(@_) }
316              
317 0     0   0 sub FILENO { $_[0]->fileno }
318 0     0   0 sub CLOSE { $_[0]->close }
319              
320             #
321             # The following are some default implementations to make subclassing less cumbersome.
322             #
323              
324             sub getlines {
325 0     0 1 0 my $self= shift;
326 0 0 0     0 wantarray or !defined wantarray or Carp::croak "getlines called in scalar context";
327 0         0 my (@ret, $line);
328 0         0 push @ret, $line
329             while defined ($line= $self->getline);
330 0         0 @ret;
331             }
332              
333             # I'm not sure why anyone would ever want this function, but I'm adding
334             # it for completeness.
335             sub getc {
336 0     0 1 0 my $c;
337 0 0       0 $_[0]->read($c, 1)? $c : undef;
338             }
339              
340             # 'write' does not guarantee that all bytes get written in one shot.
341             # Needs to be called in a loop to accomplish "print" semantics.
342             sub _write_all {
343 18     18   39 my ($self, $str)= @_;
344 18         32 while (1) {
345 18         49 my $wrote= $self->write($str);
346 18 50 33     122 return 1 if defined $wrote and ($wrote eq length $str);
347 0 0 0     0 return undef unless defined $wrote or $!{EINTR} or $!{EAGAIN};
      0        
348 0         0 substr($str, 0, $wrote)= '';
349             }
350             }
351              
352             # easy to forget that 'print' API involves "$," and "$\"
353             sub print {
354 0     0 1 0 my $self= shift;
355 0 0       0 my $str= join( (defined $, ? $, : ""), @_ );
356 0 0       0 $str .= $\ if defined $\;
357 0         0 $self->_write_all($str);
358             }
359              
360             # as if anyone would want to write their own printf implementation...
361             sub printf {
362 0     0 1 0 my $self= shift;
363 0         0 my $str= sprintf($_[0], $_[1..$#_]);
364 0         0 $self->_write_all($str);
365             }
366              
367             # virtual handles are unlikely to have one, and if they did, they wouldn't
368             # be using this class
369 0     0 1 0 sub fileno { undef; }
370              
371             package DataStore::CAS::FileCreatorHandle;
372 5     5   47 use strict;
  5         9  
  5         137  
373 5     5   26 use warnings;
  5         11  
  5         198  
374 5     5   1414 use parent -norequire => 'DataStore::CAS::VirtualHandle';
  5         930  
  5         45  
375              
376             our $VERSION= '0.05';
377              
378             # For write-handles, commit data to the CAS and return the digest hash for it.
379 0     0 1 0 sub commit { $_[0]->_cas->commit_write_handle(@_) }
380              
381             # These would happen anyway via the AUTOLOAD, but we enumerate them so that
382             # they officially appear as methods of this class.
383 0     0 1 0 sub close { $_[0]->_cas->_handle_close(@_) }
384 0     0 1 0 sub seek { $_[0]->_cas->_handle_seek(@_) }
385 0     0 1 0 sub tell { $_[0]->_cas->_handle_tell(@_) }
386 18     18 1 44 sub write { $_[0]->_cas->_handle_write(@_) }
387              
388             # This is a write-only handle
389 0     0 1   sub eof { return 1; }
390 0     0 1   sub read { return 0; }
391 0     0 1   sub readline { return undef; }
392              
393              
394             1;
395              
396             __END__