File Coverage

blib/lib/Catmandu/BagIt.pm
Criterion Covered Total %
statement 685 748 91.5
branch 173 234 73.9
condition 31 58 53.4
subroutine 87 87 100.0
pod 26 30 86.6
total 1002 1157 86.6


line stmt bran cond sub pod time code
1             package Catmandu::BagIt;
2              
3             our $VERSION = '0.260';
4              
5 7     7   264526 use Catmandu::Sane;
  7         362904  
  7         46  
6 7     7   3485 use Catmandu;
  7         453970  
  7         38  
7 7     7   1646 use Moo;
  7         14  
  7         46  
8 7     7   6630 use Encode;
  7         92507  
  7         570  
9 7     7   62 use Digest::MD5;
  7         15  
  7         285  
10 7     7   1521 use Digest::SHA;
  7         8878  
  7         363  
11 7     7   50 use IO::File qw();
  7         13  
  7         127  
12 7     7   52 use IO::Handle qw();
  7         15  
  7         133  
13 7     7   3699 use File::Copy;
  7         17174  
  7         443  
14 7     7   4082 use List::MoreUtils qw(first_index uniq);
  7         64291  
  7         79  
15 7     7   8788 use Path::Tiny;
  7         15  
  7         362  
16 7     7   2490 use Path::Iterator::Rule;
  7         40322  
  7         223  
17 7     7   3533 use Path::Naive;
  7         8185  
  7         358  
18 7     7   3533 use Catmandu::BagIt::Payload;
  7         20  
  7         238  
19 7     7   3191 use Catmandu::BagIt::Fetch;
  7         28  
  7         256  
20 7     7   53 use POSIX qw(strftime);
  7         18  
  7         69  
21 7     7   9149 use LWP::UserAgent;
  7         206581  
  7         259  
22 7     7   57 use utf8;
  7         15  
  7         58  
23 7     7   203 use Catmandu::Util qw(is_string);
  7         23  
  7         426  
24 7     7   48 use namespace::clean;
  7         14  
  7         79  
25              
26             # Flags indicating which operations are needed to create a valid bag
27             use constant {
28 7         79966 FLAG_BAGIT => 0x001 , # Flag indicates updating the bagit.txt file required
29             FLAG_BAG_INFO => 0x002 , # Flag indicates updating the bag-info.txt file required
30             FLAG_FETCH => 0x004 , # Flag indicates updating the fetch.txt file required
31             FLAG_DATA => 0x008 , # Flag indicating new payload data available
32             FLAG_TAG_MANIFEST => 0x016 , # Flag indicates updateing tag-manifest-manifest.txt required
33             FLAG_MANIFEST => 0x032 , # Flag indicates updating manifest-{digest}.txt required
34             FLAG_DIRTY => 0x064 , # Flag indicates payload file that hasn't been serialized
35 7     7   3949 };
  7         18  
36              
37             with 'Catmandu::Logger';
38              
39             # Array containing all errors when reading/writing bags
40             has '_error' => (
41             is => 'rw',
42             default => sub { [] },
43             );
44              
45             # Integer containing a combinatation of FLAG_* set for this bag
46             has 'dirty' => (
47             is => 'ro',
48             writer => '_dirty',
49             default => 0,
50             );
51              
52             # Path to a directory containing a bag
53             has 'path' => (
54             is => 'ro',
55             writer => '_path',
56             init_arg => undef,
57             );
58              
59             # Version number of the bag specification
60             has 'version' => (
61             is => 'ro',
62             writer => '_version',
63             default => '1.0',
64             );
65              
66             # Encoding of all tag manifests
67             has 'encoding' => (
68             is => 'ro',
69             writer => '_encoding',
70             default => 'UTF-8',
71             init_arg => undef,
72             );
73              
74             # User agent used to fetch payloads from the Internet
75             has user_agent => (is => 'lazy');
76              
77             # An array of a tag file names
78             has '_tags' => (
79             is => 'rw',
80             default => sub { [] },
81             init_arg => undef,
82             );
83              
84             # An array of Catmandu::BagIt::Payloads
85             has '_files' => (
86             is => 'rw',
87             default => sub { [] },
88             init_arg => undef,
89             );
90              
91             # An array of Catmandu::BagIt::Fetch
92             has '_fetch' => (
93             is => 'rw',
94             default => sub { [] },
95             init_arg => undef,
96             );
97              
98             # A lookup hash of checksums for the tag files
99             has '_tag_sums' => (
100             is => 'rw',
101             default => sub { {} },
102             init_arg => undef,
103             );
104              
105             # A lookup hahs of checksums for the payload files
106             has '_sums' => (
107             is => 'rw',
108             default => sub { {} },
109             init_arg => undef,
110             );
111              
112             # An array of hashes of all name/value pairs in the bag-info.txt file
113             has '_info' => (
114             is => 'rw',
115             default => sub { [] },
116             init_arg => undef,
117             );
118              
119             # Set the checksum algorithm to use
120             has 'algorithm' => (
121             is => 'ro',
122             default => sub { 'sha512' },
123             trigger => sub {
124             die "need md5|sha1|sha256|sha512" unless $_[1] =~ /^md5|sha1|sha256|sha512$/;
125             },
126             );
127              
128             # Set if we do file escaping
129             has 'escape' => (
130             is => 'ro',
131             default => sub { 1 },
132             init_arg => undef,
133             );
134              
135             sub _build_user_agent {
136 1     1   14 my ($self) = @_;
137 1         16 my $ua = LWP::UserAgent->new;
138 1         3277 $ua->agent('Catmandu-BagIt/' . $Catmandu::BagIt::VERSION);
139 1         76 $ua;
140             }
141              
142             # Settings requires when creating a new bag from scratch
143             sub BUILD {
144 26     26 0 193 my $self = shift;
145              
146 26         422 $self->log->debug("initializing bag");
147              
148             # Intialize the in memory settings of the bag-info
149 26         5883 $self->_update_info;
150              
151             # Initialize the in memory settings of the tag-manifests
152 26         96 $self->_update_tag_manifest;
153              
154             # Intialize the names of the basic tag files
155 26         74 my @tags = qw(bagit.txt bag-info.txt);
156 26         53 my $algorithm = $self->algorithm;
157 26         68 push @tags , "manifest-$algorithm.txt";
158 26         57 $self->_tags(\@tags);
159              
160             # Set this bag as dirty requiring an update of all the files
161 26         221 $self->_dirty($self->dirty | FLAG_BAG_INFO | FLAG_TAG_MANIFEST | FLAG_DATA | FLAG_BAGIT);
162             }
163              
164             # Return all the arrors as an array
165             sub errors {
166 26     26 0 71 my ($self) = @_;
167 26         41 @{$self->_error};
  26         365  
168             }
169              
170             # Return an array of tag file names
171             sub list_tags {
172 39     39 0 64 my ($self) = @_;
173 39         56 @{$self->_tags};
  39         116  
174             }
175              
176             # Return an array of all Catmandu::BagIt::Payload-s
177             sub list_files {
178 273     273 1 820 my ($self) = @_;
179 273         374 @{$self->_files};
  273         834  
180             }
181              
182             # Return a Catmandu::BagIt::Payload given a file name
183             sub get_file {
184 23     23 1 2255 my ($self,$filename) = @_;
185 23 100       64 die "usage: get_file(filename)" unless $filename;
186              
187 22         53 for ($self->list_files) {
188 32 100       123 return $_ if $_->filename eq $filename;
189             }
190 1         18 return undef;
191             }
192              
193             # Return a Catmandu::BagIt::Fetch given a file name
194             sub get_fetch {
195 3     3 1 396 my ($self,$filename) = @_;
196 3 100       20 die "usage: get_fetch(filename)" unless $filename;
197              
198 2         8 for ($self->list_fetch) {
199 2 100       12 return $_ if $_->filename eq $filename;
200             }
201 1         5 return undef;
202             }
203              
204             # Return true when this bag is dirty
205             sub is_dirty {
206 16     16 1 951 my ($self) = @_;
207 16         102 $self->dirty != 0;
208             }
209              
210             # Return true when this bag is holey (and requires fetching data from the Internet
211             # to be made complete)
212             sub is_holey {
213 3     3 1 8 my ($self) = @_;
214 3         6 @{$self->_fetch} > 0;
  3         19  
215             }
216              
217             # Return an array of Catmandu::BagIt::Fetch
218             sub list_fetch {
219 352     352 1 921 my ($self) = @_;
220 352         466 @{$self->_fetch};
  352         1017  
221             }
222              
223             # Return an array of tag file
224             sub list_tagsum {
225 50     50 1 1294 my ($self) = @_;
226 50         86 keys %{$self->_tag_sums};
  50         301  
227             }
228              
229             # Return the checksum of a file
230             sub get_tagsum {
231 119     119 1 1187 my ($self,$file) = @_;
232              
233 119 100       279 die "usage: get_tagsum(file)" unless $file;
234              
235 118         398 $self->_tag_sums->{$file};
236             }
237              
238             # Return an array of payload files
239             sub list_checksum {
240 92     92 1 484 my ($self) = @_;
241 92         148 keys %{$self->_sums};
  92         415  
242             }
243              
244             # Return the checksum of of a file name
245             sub get_checksum {
246 195     195 1 465 my ($self,$file) = @_;
247              
248 195 100       384 die "usage: get_checksum(file)" unless $file;
249              
250 194         923 $self->_sums->{$file};
251             }
252              
253             # Read the content of a bag
254             sub read {
255 11     11 1 4316 my ($class,$path) = @_;
256              
257 11 100       45 die "usage: read(path)" unless $path;
258              
259 10         223 my $self = $class->new;
260              
261 10 100       237 if (! -d $path ) {
262 1         30 $self->log->error("$path doesn't exist");
263 1         21 $self->_push_error("$path doesn't exist");
264 1         12 return;
265             }
266              
267 9         261 $self->log->info("reading: $path");
268              
269 9         167 $self->_path($path);
270              
271 9         16 my $ok = 0;
272              
273 9         33 $ok += $self->_read_version($path);
274 9         41 $ok += $self->_read_info($path);
275 9         40 $ok += $self->_read_manifest($path);
276 9         88 $ok += $self->_read_tag_manifest($path);
277 9         41 $ok += $self->_read_tags($path);
278 9         42 $ok += $self->_read_files($path);
279 9         65 $ok += $self->_read_fetch($path);
280              
281 9         51 $self->_dirty(0);
282              
283 9 50       31 if ( wantarray ) {
284 0 0       0 return $ok == 7 ? ($self) : (undef, $self->errors);
285             }
286             else {
287 9 100       69 return $ok == 7 ? $self : undef;
288             }
289             }
290              
291             # Write the content of a bag back to disk
292             sub write {
293 25     25 1 1454 my ($self,$path,%opts) = @_;
294              
295 25         100 $self->_error([]);
296              
297 25 100       81 die "usage: write(path[, overwrite => 1])" unless $path;
298              
299             # Check if other processes are writing or previous processes died
300 24 50       82 if ($self->locked($path)) {
301 0         0 $self->log->error("$path is locked");
302 0         0 $self->_push_error("$path is locked");
303 0         0 return undef;
304             }
305              
306 24 100 100     415 if (defined($self->path) && $path ne $self->path) {
    100 66        
    100 100        
307             # If the bag is copied from to a new location than all the tag files and
308             # files should be flagged as dirty and need to be overwritten
309 1         31 $self->log->info("copying from old path: " . $self->path);
310 1         20 $self->_dirty($self->dirty | FLAG_BAGIT | FLAG_BAG_INFO | FLAG_TAG_MANIFEST | FLAG_MANIFEST | FLAG_DATA);
311              
312 1         5 foreach my $item ($self->list_files) {
313 0         0 $item->flag($item->flag ^ FLAG_DIRTY);
314             }
315             }
316             elsif (defined($self->path) && $path eq $self->path) {
317             # we are ok the path exists and don't need to remove anything
318             # updates are possible when overwrite => 1
319             }
320             elsif ($opts{overwrite} && -d $path) {
321             # Remove existing bags
322 4         144 $self->log->info("removing: $path");
323 4         85 path($path)->remove_tree;
324             }
325              
326 24 100       3285 if (-f $self->_bagit_file($path)) {
327 14 100       72 if ($opts{overwrite}) {
328 13         391 $self->log->info("overwriting: $path");
329             }
330             else {
331 1         28 $self->log->error("$path already exists");
332 1         18 $self->_push_error("$path already exists");
333 1         6 return undef;
334             }
335             }
336             else {
337 10         348 $self->log->info("creating: $path");
338 10         202 path($path)->mkpath;
339 10         2412 $self->_dirty($self->dirty | FLAG_BAGIT);
340             }
341              
342 23 50       250 unless ($self->touch($self->_lock_file($path))) {
343 0         0 $self->log->error("failed to lock in $path");
344 0         0 return undef;
345             }
346              
347 23         199 $self->_path($path);
348              
349 23         51 my $ok = 0;
350              
351 23         77 $ok += $self->_write_bagit($path);
352 23         80 $ok += $self->_write_info($path);
353 23         89 $ok += $self->_write_data($path);
354 23         101 $ok += $self->_write_fetch($path);
355 23         94 $ok += $self->_write_manifest($path);
356 23         91 $ok += $self->_write_tag_manifest($path);
357              
358 23 50       74 return undef unless $ok == 6;
359              
360 23         57 $self->_dirty(0);
361              
362 23         66 unlink($self->_lock_file($path));
363              
364 23         121 $ok = 0;
365              
366             # Reread the contents of the bag
367 23         89 $ok += $self->_read_version($path);
368 23         82 $ok += $self->_read_info($path);
369 23         87 $ok += $self->_read_manifest($path);
370 23         106 $ok += $self->_read_tag_manifest($path);
371 23         73 $ok += $self->_read_tags($path);
372 23         91 $ok += $self->_read_files($path);
373 23         85 $ok += $self->_read_fetch($path);
374              
375 23         205 $ok == 7;
376             }
377              
378             sub _bagit_file {
379 97     97   240 my ($self,$path) = @_;
380              
381 97         1904 File::Spec->catfile($path,'bagit.txt');
382             }
383              
384             sub _bag_info_file {
385 86     86   250 my ($self,$path) = @_;
386              
387 86         1556 File::Spec->catfile($path,'bag-info.txt');
388             }
389              
390             sub _package_info_file {
391 1     1   14 my ($self,$path) = @_;
392              
393 1         28 File::Spec->catfile($path,'package-info.txt');
394             }
395              
396             sub _manifest_file {
397 32     32   67 my ($self,$path) = @_;
398              
399 32         101 for my $alg (qw(md5 sha512 sha256 sha1)) {
400 53         636 my $p = File::Spec->catfile($path,"manifest-$alg.txt");
401 53 100       1057 return ($p,$alg) if -f $p;
402             }
403              
404 1         5 return (undef,undef);
405             }
406              
407             sub _tagmanifest_file {
408 32     32   80 my ($self,$path) = @_;
409              
410 32         121 for my $alg (qw(md5 sha512 sha256 sha1)) {
411 53         625 my $p = File::Spec->catfile($path,"tagmanifest-$alg.txt");
412 53 100       1053 return ($p,$alg) if -f $p;
413             }
414              
415 1         6 return (undef,undef);
416             }
417              
418             sub _fetch_file {
419 61     61   131 my ($self,$path) = @_;
420              
421 61         1654 File::Spec->catfile($path,'fetch.txt');
422             }
423              
424             sub _tag_file {
425 31     31   76 my ($self,$path,$file) = @_;
426              
427 31         524 File::Spec->catfile($path,$file);
428             }
429              
430             sub _payload_file {
431 130     130   258 my ($self,$path,$file) = @_;
432              
433 130         2847 File::Spec->catfile($path,'data',$file);
434             }
435              
436             sub _lock_file {
437 72     72   151 my ($self,$path) = @_;
438              
439 72         2462 File::Spec->catfile($path,'.lock');
440             }
441              
442             sub locked {
443 26     26 1 62 my ($self,$path) = @_;
444 26   66     92 $path //= $self->path;
445              
446 26 50       67 return undef unless defined($path);
447              
448 26         71 -f $self->_lock_file($path);
449             }
450              
451             sub touch {
452 24     24 0 66 my ($self,$path) = @_;
453              
454 24 50       86 die "usage: touch(path)"
455             unless defined($path);
456              
457 24         101 path("$path")->spew("");
458              
459 24         10410 1;
460             }
461              
462             sub add_file {
463 21     21 1 3263 my ($self, $filename, $data, %opts) = @_;
464              
465 21 50 33     126 die "usage: add_file(filename, data [, overwrite => 1])"
466             unless defined($filename) && defined($data);
467              
468 21         93 $self->_error([]);
469              
470 21 100       65 unless ($self->_is_legal_file_name($filename)) {
471 1         40 $self->log->error("illegal file name $filename");
472 1         28 $self->_push_error("illegal file name $filename");
473 1         6 return;
474             }
475              
476 20         460 $self->log->info("adding file $filename");
477              
478 20 100       277 if ($opts{overwrite}) {
479 7         47 $self->remove_file($filename);
480             }
481              
482 20 100       74 if ($self->get_checksum("$filename")) {
483 1         19 $self->log->error("$filename already exists in bag");
484 1         19 $self->_push_error("$filename already exists in bag");
485 1         5 return;
486             }
487              
488 19         149 my $payload = Catmandu::BagIt::Payload->from_any($filename,$data);
489 19         754 $payload->flag(FLAG_DIRTY);
490              
491 19         40 my $sum;
492              
493 19 100       251 if ( is_string($opts{md5}) ) {
    50          
    50          
    50          
494 2 50       15 if ($self->algorithm ne 'md5') {
    100          
495 0         0 $self->log->error("need a " . $self->algorithm . " checksum not an md5");
496 0         0 $self->_push_error("need a " . $self->algorithm . " checksum not an md5");
497 0         0 return;
498             }
499             elsif ($opts{md5} =~ /^[0-9a-f]{32}$/) {
500 1         4 $sum = $opts{md5};
501             }
502             else {
503 1         22 $self->log->error("supplied md5 sum for $filename does not look like an md5 sum");
504 1         20 $self->_push_error("supplied md5 sum for $filename does not look like an md5 sum");
505 1         6 return;
506             }
507             }
508             elsif ( is_string($opts{sha1}) ) {
509 0 0       0 if ($self->algorithm ne 'sha1') {
    0          
510 0         0 $self->log->error("need a " . $self->algorithm . " checksum not an sha1");
511 0         0 $self->_push_error("need a " . $self->algorithm . " checksum not an sha1");
512 0         0 return;
513             }
514             elsif ($opts{sha1} =~ /^[0-9a-f]{5,40}$/) {
515 0         0 $sum = $opts{sha1};
516             }
517             else {
518 0         0 $self->log->error("supplied sha1 sum for $filename does not look like an sha1 sum");
519 0         0 $self->_push_error("supplied sha1 sum for $filename does not look like an sha1 sum");
520 0         0 return;
521             }
522             }
523             elsif ( is_string($opts{sha256}) ) {
524 0 0       0 if ($self->algorithm ne 'sha256') {
    0          
525 0         0 $self->log->error("need a " . $self->algorithm . " checksum not an sha256");
526 0         0 $self->_push_error("need a " . $self->algorithm . " checksum not an sha256");
527 0         0 return;
528             }
529             elsif ($opts{sha256} =~ /^[A-Fa-f0-9]{64}$/) {
530 0         0 $sum = $opts{sha256};
531             }
532             else {
533 0         0 $self->log->error("supplied sha256 sum for $filename does not look like an sha256 sum");
534 0         0 $self->_push_error("supplied sha256 sum for $filename does not look like an sha256 sum");
535 0         0 return;
536             }
537             }
538             elsif ( is_string($opts{sha512}) ) {
539 0 0       0 if ($self->algorithm ne 'sha512') {
    0          
540 0         0 $self->log->error("need a " . $self->algorithm . " checksum not an sha512");
541 0         0 $self->_push_error("need a " . $self->algorithm . " checksum not an sha512");
542 0         0 return;
543             }
544             elsif ($opts{sha512} =~ /^[A-Fa-f0-9]{128}$/) {
545 0         0 $sum = $opts{sha512};
546             }
547             else {
548 0         0 $self->log->error("supplied sha512 sum for $filename does not look like an sha512 sum");
549 0         0 $self->_push_error("supplied sha512 sum for $filename does not look like an sha512 sum");
550 0         0 return;
551             }
552             }
553             else {
554              
555 17         66 my $fh = $payload->open;
556              
557 17         1579 binmode($fh,":raw");
558              
559 17         86 $sum = $self->_calc_checksum_sum($fh);
560              
561 17         1201 close($fh);
562              
563             }
564              
565 18         95 push @{ $self->_files }, $payload;
  18         98  
566              
567 18         95 $self->_sums->{"$filename"} = $sum;
568              
569             # Total size changes, therefore tag manifest changes
570 18         72 $self->_update_info;
571 18         69 $self->_update_tag_manifest; # Try to update the manifest .. but it is dirty
572             # Until we serialize the bag
573              
574 18         102 $self->_dirty($self->dirty | FLAG_DATA | FLAG_MANIFEST | FLAG_BAG_INFO | FLAG_TAG_MANIFEST);
575              
576 18         143 1;
577             }
578              
579             sub remove_file {
580 14     14 1 941 my ($self, $filename) = @_;
581              
582 14 50       58 die "usage: remove_file(filename)" unless defined($filename);
583              
584 14         69 $self->_error([]);
585              
586 14 100       49 unless ($self->get_checksum($filename)) {
587 5         102 $self->log->error("$filename doesn't exist in bag");
588 5         102 $self->_push_error("$filename doesn't exist in bag");
589 5         18 return;
590             }
591              
592 9         189 $self->log->info("removing file $filename");
593              
594 9     9   158 my $idx = first_index { $_->{filename} eq $filename } @{ $self->_files };
  9         38  
  9         84  
595              
596 9 50       48 unless ($idx != -1) {
597 0         0 $self->_push_error("$filename doesn't exist in bag");
598 0         0 return;
599             }
600              
601 9         16 my @files = grep { $_->{filename} ne $filename } @{ $self->_files };
  14         45  
  9         25  
602              
603 9         47 $self->_files(\@files);
604              
605 9         32 delete $self->_sums->{$filename};
606              
607 9         30 $self->_update_info;
608 9         33 $self->_update_tag_manifest;
609              
610 9         48 $self->_dirty($self->dirty | FLAG_DATA | FLAG_MANIFEST | FLAG_BAG_INFO | FLAG_TAG_MANIFEST);
611              
612 9         44 1;
613             }
614              
615             sub add_fetch {
616 4     4 1 611 my ($self, $url, $size, $filename) = @_;
617              
618 4 50 33     48 die "usage add_fetch(url,size,filename)"
      33        
619             unless defined($url) && $size =~ /^[0-9]+$/ && defined($filename);
620              
621 4 50       13 die "illegal file name $filename"
622             unless $self->_is_legal_file_name($filename);
623              
624 4         91 $self->log->info("adding fetch $url -> $filename");
625              
626 4         52 my (@old) = grep { $_->{filename} ne $filename} @{$self->_fetch};
  0         0  
  4         15  
627              
628 4         16 $self->_fetch(\@old);
629              
630 4         7 push @{$self->_fetch} , Catmandu::BagIt::Fetch->new(url => $url , size => $size , filename => $filename);
  4         77  
631              
632 4         1405 $self->_update_info;
633 4         22 $self->_update_tag_manifest;
634              
635 4         21 $self->_dirty($self->dirty | FLAG_FETCH | FLAG_TAG_MANIFEST);
636              
637 4         16 1;
638             }
639              
640             sub remove_fetch {
641 2     2 1 2008 my ($self, $filename) = @_;
642              
643 2 50       11 die "usage remove_fetch(filename)" unless defined($filename);
644              
645 2         54 $self->log->info("removing fetch for $filename");
646              
647 2         38 my (@old) = grep { $_->filename ne $filename} @{$self->_fetch};
  2         16  
  2         12  
648              
649 2         12 $self->_fetch(\@old);
650 2         7 $self->_update_info;
651 2         8 $self->_update_tag_manifest;
652 2         12 $self->_dirty($self->dirty | FLAG_FETCH | FLAG_TAG_MANIFEST);
653              
654 2         16 1;
655             }
656              
657             sub mirror_fetch {
658 1     1 1 4 my ($self, $fetch) = @_;
659              
660 1 50 33     17 die "usage mirror_fetch(<Catmandu::BagIt::Fetch>)"
      33        
661             unless defined($fetch) && ref($fetch) && ref($fetch) =~ /^Catmandu::BagIt::Fetch/;
662              
663 1         8 my $tmp_filename = Path::Tiny->tempfile;
664              
665 1         696 my $url = $fetch->url;
666 1         6 my $filename = $fetch->filename;
667 1         4 my $path = $self->path;
668              
669 1         25 $self->log->info("mirroring $url -> $tmp_filename...");
670              
671 1         46 my $response = $self->user_agent->mirror($url,$tmp_filename);
672              
673 1 50       10678 if ($response->is_success) {
674 1         34 $self->log->info("mirror is a success");
675             }
676             else {
677 0         0 $self->log->error("mirror $url -> $tmp_filename failed : $response->status_line");
678 0         0 return undef;
679             }
680              
681 1         30 $self->log->info("updating file listing...");
682 1         25 $self->log->debug("add new $filename");
683 1         20 $self->add_file($filename, IO::File->new($tmp_filename,'r'), overwrite => 1);
684             }
685              
686             sub add_info {
687 188     188 1 849 my ($self,$name,$values) = @_;
688              
689 188 50 33     832 die "usage add_info(name,values)"
690             unless defined($name) && defined($values);
691              
692 188 100       992 if ($name =~ /^(Bag-Size|Bagging-Date|Payload-Oxum)$/) {
693 178         274 for my $part (@{$self->_info}) {
  178         529  
694 277 100       693 if ($part->[0] eq $name) {
695 100         196 $part->[1] = $values;
696 100         218 return;
697             }
698             }
699 78         133 push @{$self->_info} , [ $name , $values ];
  78         236  
700 78         159 return;
701             }
702              
703 10         245 $self->log->info("adding info $name");
704              
705 10 50       148 if (ref($values) eq 'ARRAY') {
706 0         0 foreach my $value (@$values) {
707 0         0 push @{$self->_info} , [ $name , $value ];
  0         0  
708             }
709             }
710             else {
711 10         15 push @{$self->_info} , [ $name , $values ];
  10         63  
712             }
713              
714 10         43 $self->_update_tag_manifest;
715              
716 10         50 $self->_dirty($self->dirty | FLAG_BAG_INFO | FLAG_TAG_MANIFEST);
717              
718 10         40 1;
719             }
720              
721             sub remove_info {
722 5     5 1 620 my ($self,$name) = @_;
723              
724 5 50       15 die "usage remove_info(name)"
725             unless defined($name);
726              
727 5 100       28 if ($name =~ /^(Bag-Size|Bagging-Date|Payload-Oxum)$/) {
728 1         25 $self->log->error("removing info $name - is read-only");
729 1         15 return undef;
730             }
731              
732 4         98 $self->log->info("removing info $name");
733              
734 4         54 my (@old) = grep { $_->[0] ne $name } @{$self->_info};
  17         40  
  4         13  
735              
736 4         17 $self->_info(\@old);
737              
738 4         14 $self->_update_tag_manifest;
739              
740 4         27 $self->_dirty($self->dirty | FLAG_BAG_INFO | FLAG_TAG_MANIFEST);
741              
742 4         12 1;
743             }
744              
745             sub list_info_tags {
746 125     125 1 2144 my ($self) = @_;
747 125         182 uniq map { $_->[0] } @{$self->_info};
  452         1784  
  125         303  
748             }
749              
750             sub get_info {
751 447     447 1 1693 my ($self,$field,$join) = @_;
752 447   100     1709 $join //= '; ';
753              
754 447 50       798 die "usage: get_info(field[,$join])" unless $field;
755              
756 447         605 my @res = map { $_->[1] } grep { $_->[0] eq $field } @{$self->_info};
  449         1020  
  1644         3099  
  447         938  
757              
758 447 100       1237 wantarray ? @res : join $join, @res;
759             }
760              
761             sub size {
762 62     62 1 1968 my $self = shift;
763              
764 62         152 my $total = $self->_size;
765              
766 62 50       235 if ($total > 100*1000**3) {
    50          
    100          
767             # 100's of GB
768 0         0 sprintf "%-.3f TB" , $total/(1000**4);
769             }
770             elsif ($total > 100*1024**2) {
771             # 100's of MB
772 0         0 sprintf "%-.3f GB" , $total/(1000**3);
773             }
774             elsif ($total > 100*1024) {
775             # 100's of KB
776 2         28 sprintf "%-.3f MB" , $total/(1000**2);
777             }
778             else {
779 60         710 sprintf "%-.3f KB" , $total/1000;
780             }
781             }
782              
783             sub payload_oxum {
784 64     64 1 969 my $self = shift;
785              
786 64         128 my $size = $self->_size;
787 64         165 my $count = $self->list_files;
788              
789 64   50     115 my $fetches = $self->list_fetch // 0;
790              
791 64         116 $count += $fetches;
792              
793 64         266 return "$size.$count";
794             }
795              
796             sub complete {
797 10     10 1 1694 my $self = shift;
798 10   100     47 my $path = $self->path || '';
799              
800 10         88 $self->_error([]);
801              
802 10         265 $self->log->info("checking complete");
803              
804 10 50 33     204 unless ($self->version and $self->version =~ /^[0-9]+\.[0-9]+$/) {
805 0         0 $self->log->error("Tag 'BagIt-Version' not available in bagit.txt");
806 0         0 $self->_push_error("Tag 'BagIt-Version' not available in bagit.txt");
807             }
808              
809 10 50 33     58 unless ($self->encoding and $self->encoding eq 'UTF-8') {
810 0         0 $self->log->error("Tag 'Tag-File-Character-Encoding' not available in bagit.txt");
811 0         0 $self->_push_error("Tag 'Tag-File-Character-Encoding' not available in bagit.txt");
812             }
813              
814 10         23 my @missing = ();
815              
816 10         38 foreach my $file ($self->list_checksum) {
817 23 100 50     49 unless (grep { (my $filename = $_->{filename} || '') =~ /^$file$/ } $self->list_files) {
  42         417  
818 19         49 push @missing , $file;
819             }
820             }
821              
822 10         32 foreach my $file ($self->list_tagsum) {
823 31 50       69 unless (grep { /^$file$/ } $self->list_tags) {
  100         682  
824 0         0 push @missing , $file;
825             }
826             }
827              
828 10         77 foreach my $file (@missing) {
829 19 50       41 unless (grep { $_->filename =~ /^$file$/ } $self->list_fetch) {
  0         0  
830 19         316 $self->log->error("file $file doesn't exist in bag and fetch.txt");
831 19         212 $self->_push_error("file $file doesn't exist in bag and fetch.txt");
832             }
833             }
834              
835 10 100       35 my $has_fetch = $self->list_fetch > 0 ? 1 : 0;
836              
837 10 100 66     26 $self->errors == 0 && @missing == 0 && $has_fetch == 0;
838             }
839              
840             sub valid {
841 11     11 1 27 my $self = shift;
842              
843 11         279 $self->log->info("checking valid");
844              
845             my $validator = sub {
846 74     74   158 my ($file, $tag) = @_;
847 74         165 my $path = $self->path;
848              
849             # To keep things very simple right now we require at least the
850             # bag to be serialized somewhere before we start our validation process
851 74 50 33     1248 unless (defined $path && -d $path) {
852 0         0 $self->log->error("sorry, only serialized (write) bags allowed when validating");
853 0         0 return (1,"sorry, only serialized (write) bags allowed when validating");
854             }
855              
856 74 100       372 my $sum = $tag == 0 ? $self->get_checksum($file) : $self->get_tagsum($file);
857 74 100       254 my $fh = $tag == 0 ?
858             new IO::File $self->_payload_file($path,$file), "r" :
859             new IO::File $self->_tag_file($path,$file) , "r";
860              
861 74 100       6554 unless ($fh) {
862 38         951 $self->log->error("can't read $file");
863 38         501 return (0,"can't read $file");
864             }
865              
866 36         186 binmode($fh,':raw');
867              
868 36         127 my $sum_check = $self->_calc_checksum_sum($fh);
869              
870 36         997 close($fh);
871              
872 36 100       239 unless (lc($sum) eq lc($sum_check)) {
873 2         55 $self->log->error("$file checksum fails $sum <> $sum_check");
874 2         44 return (0,"$file checksum fails $sum <> $sum_check");
875             }
876              
877 34         192 (1);
878 11         194 };
879              
880 11         48 $self->_error([]);
881              
882 11 100       41 if ($self->dirty) {
883 1         19 $self->log->error("bag is dirty : first serialize (write) then try again");
884 1         11 $self->_push_error("bag is dirty : first serialize (write) then try again");
885 1         19 return 0;
886             }
887              
888 10         37 foreach my $file ($self->list_checksum) {
889 43         89 my ($code,$msg) = $validator->($file,0);
890              
891 43 100       118 if ($code == 0) {
892 38         89 $self->_push_error($msg);
893             }
894             }
895              
896 10         35 foreach my $file ($self->list_tagsum) {
897 31         67 my ($code,$msg) = $validator->($file,1);
898              
899 31 100       107 if ($code == 0) {
900 2         12 $self->_push_error($msg);
901             }
902             }
903              
904 10         54 $self->errors == 0;
905             }
906              
907             #-----------------------------------------
908              
909             sub _push_error {
910 76     76   144 my ($self,$msg) = @_;
911 76   50     274 my $errors = $self->_error // [];
912 76         173 push @$errors , $msg;
913 76         194 $self->_error($errors);
914             }
915              
916             sub _size {
917 126     126   245 my $self = shift;
918 126         286 my $path = $self->path;
919              
920 126         221 my $total = 0;
921              
922 126         270 foreach my $file ($self->list_files) {
923 86         314 my $fh = $file->open;
924 86         6842 my $size = [ $fh->stat ]->[7];
925 86         2070 $fh->close;
926 86         1543 $total += $size;
927             }
928              
929 126         371 foreach my $item ($self->list_fetch) {
930 11         33 my $size = $item->size;
931 11         30 $total += $size;
932             }
933              
934 126         290 $total;
935             }
936              
937             sub _update_info {
938 59     59   128 my $self = shift;
939              
940 59         1162 $self->log->debug("updating the default info");
941              
942             # Add some goodies to the info file...
943 59         3328 $self->add_info('Bagging-Date', strftime "%Y-%m-%d", gmtime);
944 59         230 $self->add_info('Bag-Size',$self->size);
945 59         195 $self->add_info('Payload-Oxum',$self->payload_oxum);
946             }
947              
948             sub _update_tag_manifest {
949 96     96   163 my $self = shift;
950              
951 96         2319 $self->log->debug("updating the tag manifest");
952              
953             {
954 96         247 my $sum = $self->_calc_checksum_sum($self->_bagit_as_string);
955 96         377 $self->_tag_sums->{'bagit.txt'} = $sum;
956             }
957              
958             {
959 96         1021 my $sum = $self->_calc_checksum_sum($self->_baginfo_as_string);
  96         247  
960 96         346 $self->_tag_sums->{'bag-info.txt'} = $sum;
961             }
962              
963             {
964 96         158 my $sum = $self->_calc_checksum_sum($self->_manifest_as_string);
  96         145  
  96         225  
965 96         257 my $algorithm = $self->algorithm;
966 96         360 $self->_tag_sums->{"manifest-$algorithm.txt"} = $sum;
967             }
968              
969 96 100       229 if ($self->list_fetch) {
970 8         22 my $sum = $self->_calc_checksum_sum($self->_fetch_as_string);
971 8         33 $self->_tag_sums->{'fetch.txt'} = $sum;
972              
973 8 100       23 unless (grep {/fetch.txt/} $self->list_tags) {
  28         87  
974 4         8 push @{$self->_tags} , 'fetch.txt';
  4         15  
975             }
976             }
977             else {
978 88         135 my (@new) = grep { $_ ne 'fetch.txt' } @{$self->_tags};
  188         476  
  88         222  
979 88         262 $self->_tags(\@new);
980 88         250 delete $self->_tag_sums->{'fetch.txt'};
981             }
982             }
983              
984             sub _read_fetch {
985 32     32   84 my ($self, $path) = @_;
986              
987 32         111 $self->_fetch([]);
988              
989 32 100       99 return 1 unless -f $self->_fetch_file($path);
990              
991 5         144 $self->log->debug("reading fetch.txt");
992              
993 5         68 foreach my $line (path($self->_fetch_file($path))->lines_utf8) {
994 5         1600 $line =~ s/\r\n$/\n/g;
995 5         13 chomp($line);
996              
997 5         45 my ($url,$size,$filename) = split(/\s+/,$line,3);
998              
999 5         31 $filename =~ s/^data\///;
1000              
1001 5         12 push @{ $self->_fetch } , Catmandu::BagIt::Fetch->new(url => $url , size => $size , filename => $filename);
  5         145  
1002             }
1003              
1004 5         128 1;
1005             }
1006              
1007             sub _read_tag_manifest {
1008 32     32   79 my ($self, $path) = @_;
1009              
1010 32         142 $self->_tag_sums({});
1011              
1012 32         96 my ($manifest,$algorithm) = $self->_tagmanifest_file($path);
1013              
1014 32 100       128 if (! $manifest ) {
1015 1         4 return 1;
1016             }
1017              
1018 31         797 $self->log->debug("reading tagmanifest-$algorithm.txt");
1019              
1020 31         438 foreach my $line (path($manifest)->lines_utf8) {
1021 96         9425 $line =~ s/\r\n$/\n/g;
1022 96         168 chomp($line);
1023 96         513 my ($sum,$file) = split(/\s+/,$line,2);
1024 96         393 $self->_tag_sums->{$file} = $sum;
1025             }
1026              
1027 31 50 33     293 if ($self->algorithm && $self->algorithm ne $algorithm) {
1028 0         0 $self->_push_error("no tagmanifest-$algorithm.txt in $path");
1029 0         0 return 0;
1030             }
1031             else {
1032 31         73 $self->{algorithm} = $algorithm;
1033             }
1034              
1035 31         84 1;
1036             }
1037              
1038             sub _read_manifest {
1039 32     32   68 my ($self, $path) = @_;
1040              
1041 32         113 $self->_sums({});
1042              
1043 32         104 my ($manifest,$algorithm) = $self->_manifest_file($path);
1044              
1045 32 100       133 if (! $manifest ) {
1046 1         5 $self->_push_error("no manifest-{digest}.txt in $path");
1047 1         3 return 0;
1048             }
1049              
1050 31         870 $self->log->debug("reading manifest-$algorithm.txt");
1051              
1052 31         476 foreach my $line (path($manifest)->lines_utf8) {
1053 66         6430 $line =~ s/\r\n$/\n/g;
1054 66         112 chomp($line);
1055 66         363 my ($sum,$file) = split(/\s+/,$line,2);
1056 66         301 $file =~ s/^data\///;
1057             # Unescape LF,CR,% when needed
1058 66 50       204 if ($self->escape) {
1059 66         125 $file =~ s{%0A}{\n}mg;
1060 66         101 $file =~ s{%0D}{\r}mg;
1061 66         100 $file =~ s{%25}{%}mg;
1062             }
1063 66         232 $self->_sums->{$file} = $sum;
1064             }
1065              
1066             # The algorithm of the bag will be set by the algorithm of the manifest
1067 31         2923 $self->{algorithm} = $algorithm;
1068              
1069 31         74 1;
1070             }
1071              
1072             sub _read_tags {
1073 32     32   89 my ($self, $path) = @_;
1074              
1075 32         730 $self->log->debug("reading tag files");
1076              
1077 32         446 $self->_tags([]);
1078              
1079 32         232 my $rule = Path::Iterator::Rule->new;
1080 32         309 $rule->max_depth(1);
1081 32         1197 $rule->file;
1082 32         730 my $iter = $rule->iter($path);
1083              
1084 32         2940 while(my $file = $iter->()) {
1085 129         48238 $file =~ s/^$path.//;
1086              
1087 129 100       508 next if $file =~ /^tagmanifest-\w+.txt$/;
1088              
1089 98         148 push @{ $self->_tags } , $file;
  98         515  
1090             }
1091              
1092 32         1302 1;
1093             }
1094              
1095             sub _read_files {
1096 32     32   79 my ($self, $path) = @_;
1097              
1098 32         713 $self->log->debug("reading data files");
1099              
1100 32         481 $self->_files([]);
1101              
1102 32 100       603 if (! -d "$path/data" ) {
1103 3         73 $self->log->error("payload directory $path/data doesn't exist");
1104 3         48 $self->_push_error("payload directory $path/data doesn't exist");
1105 3         7 return 1;
1106             }
1107              
1108 29         232 my $rule = Path::Iterator::Rule->new;
1109 29         320 $rule->file;
1110 29         893 my $iter = $rule->iter("$path/data");
1111              
1112 29         2666 while(my $file = $iter->()) {
1113 32         13992 my $filename = $file;
1114 32         365 $filename =~ s/^$path\/data\///;
1115 32         788 my $payload = Catmandu::BagIt::Payload->new(filename => $filename, path => $file);
1116 32         1928 push @{ $self->_files } , $payload;
  32         182  
1117             }
1118              
1119 29         3407 1;
1120             }
1121              
1122             sub _read_info {
1123 32     32   75 my ($self, $path) = @_;
1124              
1125 32         758 $self->log->debug("reading the tag info file");
1126              
1127 32         462 $self->_info([]);
1128              
1129 32 100       91 my $info_file = -f $self->_bag_info_file($path) ?
1130             $self->_bag_info_file($path) :
1131             $self->_package_info_file($path);
1132              
1133 32 100       855 if (! -f $info_file) {
1134 1         26 $self->log->error("no package-info.txt or bag-info.txt in $path");
1135 1         17 $self->_push_error("no package-info.txt or bag-info.txt in $path");
1136 1         3 return 0;
1137             }
1138              
1139 31         177 foreach my $line (path($info_file)->lines_utf8) {
1140 127         10202 $line =~ s/\r\n$/\n/g;
1141 127         220 chomp($line);
1142              
1143 127 100       406 if ($line =~ /^\s+/) {
1144 12         46 $line =~ s/^\s*//;
1145 12         36 $self->_info->[-1]->[1] .= $line;
1146 12         21 next;
1147             }
1148              
1149 115         543 my ($n,$v) = split(/\s*:\s*/,$line,2);
1150              
1151 115         204 push @{ $self->_info } , [ $n , $v ];
  115         435  
1152             }
1153              
1154 31         104 1;
1155             }
1156              
1157             sub _read_version {
1158 32     32   71 my ($self, $path) = @_;
1159              
1160 32         832 $self->log->debug("reading the version file");
1161              
1162 32 100       446 if (! -f $self->_bagit_file($path) ) {
1163 1         26 $self->log->error("no bagit.txt in $path");
1164 1         18 $self->_push_error("no bagit.txt in $path");
1165 1         3 return 0;
1166             }
1167              
1168 31         170 foreach my $line (path($self->_bagit_file($path))->lines_utf8) {
1169 62         16789 $line =~ s/\r\n$/\n/g;
1170 62         127 chomp($line);
1171 62         434 my ($n,$v) = split(/\s*:\s*/,$line,2);
1172              
1173 62 100       223 if ($n eq 'BagIt-Version') {
    50          
1174 31         132 $self->_version($v);
1175             }
1176             elsif ($n eq 'Tag-File-Character-Encoding') {
1177 31         107 $self->_encoding($v);
1178             }
1179             }
1180              
1181 31         127 1;
1182             }
1183              
1184             sub _write_bagit {
1185 23     23   54 my ($self,$path) = @_;
1186              
1187 23 100       106 return 1 unless $self->dirty & FLAG_BAGIT;
1188              
1189 10         289 $self->log->info("writing the version file");
1190              
1191 10         137 path($self->_bagit_file($path))->spew_utf8($self->_bagit_as_string);
1192              
1193 10         6343 $self->_dirty($self->dirty ^ FLAG_BAGIT);
1194              
1195 10         32 1;
1196             }
1197              
1198             sub _bagit_as_string {
1199 106     106   447 my $self = shift;
1200              
1201 106         268 my $version = $self->version;
1202 106         229 my $encoding = $self->encoding;
1203              
1204 106         456 return <<EOF;
1205             BagIt-Version: $version
1206             Tag-File-Character-Encoding: $encoding
1207             EOF
1208             }
1209              
1210             sub _write_info {
1211 23     23   47 my ($self,$path) = @_;
1212              
1213 23 50       75 return 1 unless $self->dirty & FLAG_BAG_INFO;
1214              
1215 23         618 $self->log->info("writing the tag info file");
1216              
1217 23         293 path($self->_bag_info_file($path))->spew_utf8($self->_baginfo_as_string);
1218              
1219 23         9777 $self->_dirty($self->dirty ^ FLAG_BAG_INFO);
1220              
1221 23         57 1;
1222             }
1223              
1224             sub _baginfo_as_string {
1225 119     119   827 my $self = shift;
1226              
1227 119         195 my $str = '';
1228              
1229 119         312 foreach my $tag ($self->list_info_tags) {
1230 425         934 my @values = $self->get_info($tag);
1231 425         703 foreach my $val (@values) {
1232 427         1977 my @msg = split //, "$tag: $val";
1233              
1234 427         669 my $cnt = 0;
1235 427 100       1378 while (my (@chunk) = splice(@msg,0,$cnt == 0 ? 79 : 78)) {
1236 427 50       1553 $str .= ($cnt == 0 ? '' : ' ') . join('',@chunk) . "\n";
1237 427         1852 $cnt++;
1238             }
1239             }
1240             }
1241              
1242 119         382 $str;
1243             }
1244              
1245             # Write BagIt data payloads to disk
1246             sub _write_data {
1247 23     23   59 my ($self,$path) = @_;
1248              
1249             # Return immediately when no files need to be written
1250 23 100       89 return 1 unless $self->dirty & FLAG_DATA;
1251              
1252 22         648 $self->log->info("writing the data files");
1253              
1254             # Create a data/ directory for payloads
1255 22 100       698 unless (-d "$path/data") {
1256 10 50       564 unless (mkdir "$path/data") {
1257 0         0 $self->log->error("can't create payload directory $path/data: $!");
1258 0         0 $self->_push_error("can't create payload directory $path/data: $!");
1259 0         0 return;
1260             }
1261             }
1262              
1263             # Create a list of all files written to the payload directory
1264             # Compare this list later with files found in the payload directory
1265             # This difference are the files that can be deleted
1266 22         86 my @all_names_in_bag = ();
1267              
1268 22         71 foreach my $item ($self->list_files) {
1269 24         90 my $filename = 'data/' . $item->{filename};
1270 24         55 push @all_names_in_bag , $filename;
1271              
1272             # Only process files that are dirty
1273 24 100       87 next unless $item->flag & FLAG_DIRTY;
1274              
1275             # Check for deep directories that need to be stored
1276 14         34 my $dir = $filename; $dir =~ s/\/[^\/]+$//;
  14         134  
1277              
1278 14         324 $self->log->info("serializing $filename");
1279              
1280 14 50       404 path("$path/$dir")->mkpath unless -d "$path/$dir";
1281              
1282 14         78 my $old_path = $item->path;
1283 14         47 my $new_path = "$path/$filename";
1284              
1285 14 50       65 if ($item->is_new) {
1286 14         66 File::Copy::move($old_path,$new_path);
1287             }
1288             else {
1289 0         0 File::Copy::copy($old_path,$new_path);
1290             }
1291              
1292 14         1268 $item->flag($item->flag ^ FLAG_DIRTY);
1293             }
1294              
1295             # Check deleted files. Delete all files not in the @all_names_in_bag list
1296 22         178 my $rule = Path::Iterator::Rule->new;
1297 22         278 $rule->file;
1298 22         847 my $iter = $rule->iter("$path/data");
1299              
1300 22         2234 while(my $file = $iter->()) {
1301 28         13013 my $filename = $file;
1302 28         326 $filename =~ s/^$path\///;
1303              
1304 28 100       97 unless (grep {$filename eq $_} @all_names_in_bag) {
  51         208  
1305 4         116 $self->log->info("deleting $path/$filename");
1306 4         359 unlink "$path/$filename";
1307             }
1308             }
1309              
1310 22         2078 $self->_dirty($self->dirty ^ FLAG_DATA);
1311              
1312 22         385 1;
1313             }
1314              
1315             sub _write_fetch {
1316 23     23   57 my ($self,$path) = @_;
1317              
1318 23 50       76 return 1 unless $self->dirty & FLAG_FETCH;
1319              
1320 23         63 my $fetch_str = $self->_fetch_as_string;
1321              
1322 23 100 66     150 unless (defined($fetch_str) && length($fetch_str)) {
1323 20         490 $self->log->info("removing fetch.txt");
1324 20 100       281 unlink $self->_fetch_file($path) if -f $self->_fetch_file($path);
1325 20         111 return 1;
1326             }
1327              
1328 3         84 $self->log->info("writing the fetch file");
1329              
1330 3 50       55 if ($self->_fetch == 0) {
1331 0 0       0 unlink $self->_fetch_file($path) if -r $self->_fetch_file($path);
1332 0         0 $self->_dirty($self->dirty ^ FLAG_FETCH);
1333 0         0 return 1;
1334             }
1335              
1336 3         26 path($self->_fetch_file($path))->spew_utf8($fetch_str);
1337              
1338 3         1342 $self->_dirty($self->dirty ^ FLAG_FETCH);
1339              
1340 3         9 1;
1341             }
1342              
1343             sub _fetch_as_string {
1344 31     31   61 my $self = shift;
1345              
1346 31         65 my $str = '';
1347              
1348 31         78 foreach my $f ($self->list_fetch) {
1349 11         93 $str .= sprintf "%s %s data/%s\n" , $f->url, $f->size, $f->filename;
1350             }
1351              
1352 31         81 $str;
1353             }
1354              
1355             sub _write_manifest {
1356 23     23   56 my ($self,$path) = @_;
1357              
1358 23 50       89 return 1 unless $self->dirty & FLAG_MANIFEST;
1359              
1360 23         549 $self->log->info("writing the manifest file");
1361              
1362 23         313 my $algorithm = $self->algorithm;
1363 23         241 my $manifest = File::Spec->catfile($path,"manifest-$algorithm.txt");
1364              
1365 23         147 path($manifest)->spew_utf8($self->_manifest_as_string);
1366              
1367 23         9138 $self->_dirty($self->dirty ^ FLAG_MANIFEST);
1368              
1369 23         68 1;
1370             }
1371              
1372             sub _manifest_as_string {
1373 119     119   850 my $self = shift;
1374 119         260 my $path = $self->path;
1375              
1376 119 100       345 return undef unless defined $path;
1377              
1378 67         120 my $str = '';
1379              
1380 67         154 foreach my $file ($self->list_checksum) {
1381 87 100       220 next unless -f $self->_payload_file($path,$file);
1382 75         437 my $sum = $self->get_checksum($file);
1383             # Escape LF, CR and % (when needed)
1384 75 50       228 if ($self->escape) {
1385 75         206 $file =~ s{%}{%25}mg;
1386 75         135 $file =~ s{\n}{%0A}mg;
1387 75         128 $file =~ s{\r}{%0D}mg;
1388             }
1389 75         305 $str .= "$sum data/$file\n";
1390             }
1391              
1392 67         258 $str;
1393             }
1394              
1395             sub _write_tag_manifest {
1396 23     23   59 my ($self,$path) = @_;
1397              
1398 23 50       78 return 1 unless $self->dirty & FLAG_TAG_MANIFEST;
1399              
1400             # The tag manifest can be dirty when writing new files
1401 23         70 $self->_update_tag_manifest;
1402              
1403 23         586 $self->log->info("writing the tagmanifest file");
1404              
1405 23         275 my $algorithm = $self->algorithm;
1406 23         257 my $manifest = File::Spec->catfile($path,"tagmanifest-$algorithm.txt");
1407              
1408 23         98 path($manifest)->spew_utf8($self->_tag_manifest_as_string);
1409              
1410 23         9925 $self->_dirty($self->dirty ^ FLAG_MANIFEST);
1411              
1412 23         71 1;
1413             }
1414              
1415             sub _tag_manifest_as_string {
1416 23     23   744 my $self = shift;
1417              
1418 23         39 my $str = '';
1419              
1420 23         72 foreach my $file ($self->list_tagsum) {
1421 72         143 my $sum = $self->get_tagsum($file);
1422 72         204 $str .= "$sum $file\n";
1423             }
1424              
1425 23         105 $str;
1426             }
1427              
1428             sub _calc_checksum_sum {
1429 349     349   680 my ($self, $data) = @_;
1430              
1431 349         700 my $algorithm = $self->algorithm;
1432              
1433 349         501 my $ctx;
1434              
1435 349 100       1154 if ($algorithm =~ /^sha/) {
1436 259         987 $ctx = Digest::SHA->new($algorithm);
1437             }
1438             else {
1439 90         451 $ctx = Digest::MD5->new;
1440             }
1441              
1442 349 100       4746 if (!defined $data) {
    100          
    50          
    50          
1443 52         558 return $ctx->add(Encode::encode_utf8(''))->hexdigest;
1444             }
1445             elsif (! ref $data) {
1446 244         2788 return $ctx->add(Encode::encode_utf8($data))->hexdigest;
1447             }
1448             elsif (ref($data) eq 'SCALAR') {
1449 0         0 return $ctx->add(Encode::encode_utf8($$data))->hexdigest;
1450             }
1451             elsif (ref($data) =~ /^IO/) {
1452 53         1463 return $ctx->addfile($data)->hexdigest;
1453             }
1454             else {
1455 0         0 die "unknown data type: `" . ref($data) . "`";
1456             }
1457             }
1458              
1459             sub _is_legal_file_name {
1460 25     25   51 my ($self, $filename) = @_;
1461              
1462             # Adding some security measures to stop people writing data
1463             # outside the data directory of the bagit..
1464 25         96 my $normal = Path::Naive::normalize_path($filename);
1465 25         1162 my $abs = Path::Naive::abs_path($filename,'/');
1466              
1467 25 50       1916 return undef unless ($filename eq $normal);
1468 25 100       109 return undef unless ("/$filename" eq $abs);
1469 24         71 return 1;
1470             }
1471              
1472             1;
1473              
1474             __END__
1475              
1476             =encoding utf-8
1477              
1478             =head1 NAME
1479              
1480             Catmandu::BagIt - Low level Catmandu interface to the BagIt packages.
1481              
1482             =begin markdown
1483              
1484             # STATUS
1485              
1486             [![Build Status](https://travis-ci.org/LibreCat/Catmandu-BagIt.svg?branch=master)](https://travis-ci.org/LibreCat/Catmandu-BagIt)
1487             [![Coverage Status](https://coveralls.io/repos/LibreCat/Catmandu-BagIt/badge.svg?branch=master&service=github)](https://coveralls.io/github/LibreCat/Catmandu-BagIt?branch=master)
1488              
1489             =end markdown
1490              
1491             =head1 SYNOPSIS
1492              
1493             use Catmandu::BagIt;
1494              
1495             # Assemble a new bag
1496             my $bagit = Catmandu::BagIt->new;
1497              
1498             # Force a previous version and checksum algorithm
1499             my $bagit = Catmandu::BagIt->new(version => '0.97' , algorithm => 'md5');
1500              
1501             # Read an existing
1502             my $bagit = Catmandu::BagIt->read($directory);
1503              
1504             $bag->read('t/bag');
1505              
1506             printf "path: %s\n", $bagit->path;
1507             printf "version: %s\n" , $bagit->version;
1508             printf "encoding: %s\n" , $bagit->encoding;
1509             printf "size: %s\n", $bagit->size;
1510             printf "payload-oxum: %s\n", $bagit->payload_oxum;
1511              
1512             printf "tags:\n";
1513             for my $tag ($bagit->list_info_tags) {
1514             my @values = $bagit->get_info($tag);
1515             printf " $tag: %s\n" , join(", ",@values);
1516             }
1517              
1518             printf "tag-sums:\n";
1519             for my $file ($bagit->list_tagsum) {
1520             my $sum = $bagit->get_tagsum($file);
1521             printf " $file: %s\n" , $sum;
1522             }
1523              
1524             # Read the file listing as found in the manifest file
1525             printf "file-sums:\n";
1526             for my $file ($bagit->list_checksum) {
1527             my $sum = $bagit->get_checksum($file);
1528             printf " $file: %s\n" , $sum;
1529             }
1530              
1531             # Read the real listing of files as found on the disk
1532             printf "files:\n";
1533             for my $file ($bagit->list_files) {
1534             my $stat = [stat($file->path)];
1535             printf " name: %s\n", $file->filename;
1536             printf " size: %s\n", $stat->[7];
1537             printf " last-mod: %s\n", scalar(localtime($stat->[9]));
1538             }
1539              
1540             my $file = $bagit->get_file("mydata.txt");
1541             my $fh = $file->open;
1542              
1543             while (<$fh>) {
1544             ....
1545             }
1546              
1547             close($fh);
1548              
1549             print "dirty?\n" if $bagit->is_dirty;
1550              
1551             if ($bagit->complete) {
1552             print "bag is complete\n";
1553             }
1554             else {
1555             print "bag is not complete!\n";
1556             }
1557              
1558             if ($bagit->valid) {
1559             print "bag is valid\n";
1560             }
1561             else {
1562             print "bag is not valid!\n";
1563             }
1564              
1565             if ($bagit->is_holey) {
1566             print "bag is holey\n";
1567             }
1568             else {
1569             print "bag isn't holey\n";
1570             }
1571              
1572             if ($bagit->errors) {
1573             print join("\n",$bagit->errors);
1574             }
1575              
1576             # Write operations
1577             $bagit->add_info('My-Tag','fsdfsdfsdf');
1578             $bagit->add_info('My-Tag',['dfdsf','dfsfsf','dfdsf']);
1579             $bagit->remove_info('My-Tag');
1580              
1581             $bagit->add_file("test.txt","my text");
1582             $bagit->add_file("data.pdf", IO::File->new("/tmp/data.pdf"));
1583             $bagit->remove_file("test.txt");
1584              
1585             $bagit->add_fetch("http://www.gutenberg.org/cache/epub/1980/pg1980.txt","290000","shortstories.txt");
1586             $bagit->remove_fetch("shortstories.txt");
1587              
1588             if ($bagit->errors) {
1589             print join("\n",$bagit->errors);
1590             exit;
1591             }
1592              
1593             unless ($bagit->locked) {
1594             $bagit->write("bags/demo04"); # fails when the bag already exists
1595             $bagit->write("bags/demo04", new => 1); # recreate the bag when it already existed
1596             $bagit->write("bags/demo04", overwrite => 1); # overwrites an exiting bag
1597             }
1598              
1599             =head1 CATMANDU MODULES
1600              
1601             =over
1602              
1603             =item * L<Catmandu::Importer::BagIt>
1604              
1605             =item * L<Catmandu::Exporter::BagIt>
1606              
1607             =item * L<Catmandu::Store::File::BagIt>
1608              
1609             =back
1610              
1611             =head1 LARGE FILE SUPPORT
1612              
1613             Streaming large files into a BagIt requires a large /tmp directory. The location
1614             of the temp directory can be set with the TMPDIR environmental variable.
1615              
1616             =head1 METHODS
1617              
1618             =head2 new()
1619              
1620             =head2 new(version => ... , algorithm => 'md5|sha1|sha256|sha512')
1621              
1622             Create a new BagIt object
1623              
1624             =head2 read($directory)
1625              
1626             Open an exiting BagIt object and return an instance of BagIt or undef on failure.
1627             In array context the read method also returns all errors as an array:
1628              
1629             my $bagit = Catmandu::BagIt->read("/data/my-bag");
1630              
1631             my ($bagit,@errors) = Catmandu::BagIt->read("/data/my-bag");
1632              
1633             =head2 write($directory, [%options])
1634              
1635             Write a BagIt to disk. Options: new => 1 recreate the bag when it already existed, overwrite => 1 overwrite
1636             and existing bag (updating the changed tags/files);
1637              
1638             =head2 locked
1639              
1640             Check if a process has locked the BagIt. Or, a previous process didn't complete the write operations.
1641              
1642             =head2 path()
1643              
1644             Return the path to the BagIt.
1645              
1646             =head2 version()
1647              
1648             Return the version of the BagIt.
1649              
1650             =head2 encoding()
1651              
1652             Return the encoding of the BagIt.
1653              
1654             =head2 size()
1655              
1656             Return a human readble string of the expected size of the BagIt (adding the actual sizes found on disk plus
1657             the files that need to be fetched from the network).
1658              
1659             =head2 payload_oxum()
1660              
1661             Return the actual payload oxum of files found in the package.
1662              
1663             =head2 is_dirty()
1664              
1665             Return true when the BagIt contains changes not yet written to disk.
1666              
1667             =head2 is_holey()
1668              
1669             Return true when the BagIt contains a non emtpy fetch configuration.
1670              
1671             =head2 is_error()
1672              
1673             Return an ARRAY of errors when checking complete, valid and write.
1674              
1675             =head2 complete()
1676              
1677             Return true when the BagIt is complete (all files and manifest files are consistent).
1678              
1679             =head2 valid()
1680              
1681             Returns true when the BagIt is complete and all checkums match the files on disk.
1682              
1683             =head2 list_info_tags()
1684              
1685             Return an ARRAY of tag names found in bagit-info.txt.
1686              
1687             =head2 add_info($tag,$value)
1688              
1689             =head2 add_info($tag,[$values])
1690              
1691             Add an info $tag with a $value.
1692              
1693             =head2 remove_info($tag)
1694              
1695             Remove an info $tag.
1696              
1697             =head2 get_info($tag, [$delim])
1698              
1699             Return an ARRAY of values found for the $tag name. Or, in scalar context, return a string of
1700             all values optionally delimeted by $delim.
1701              
1702             =head2 list_tagsum()
1703              
1704             Return a ARRAY of all checkums of tag files.
1705              
1706             =head2 get_tagsum($filename)
1707              
1708             Return the checksum of the tag file $filename.
1709              
1710             =head2 list_checksum()
1711              
1712             Return an ARRAY of files found in the manifest file.
1713              
1714             =head2 get_checksum($filename)
1715              
1716             Return the checksum of the file $filname.
1717              
1718             =head2 list_files()
1719              
1720             Return an ARRAY of real payload files found on disk as Catmandu::BagIt::Payload.
1721              
1722             =head2 get_file($filename)
1723              
1724             Get a Catmandu::BagIt::Payload object for the file $filename.
1725              
1726             =head2 add_file($filename, $string, %opts)
1727              
1728             =head2 add_file($filename, IO::File->new(...), %opts)
1729              
1730             =head2 add_file($filaname, sub { my $io = shift; .... }, %opts)
1731              
1732             Add a new file to the BagIt. Possible options:
1733              
1734             overwrite => 1 - remove the old file
1735             md5 => "" - supply an MD5 (don't recalculate it)
1736             sha1 => "" - supply an SHA1 (don't recalculate it)
1737             sha256 => "" - supply an SHA256 (don't recalculate it)
1738             sha512 => "" - supply an SHA512 (don't recalculate it)
1739              
1740             =head2 remove_file($filename)
1741              
1742             Remove a file from the BagIt.
1743              
1744             =head2 list_fetch()
1745              
1746             Return an ARRAY of fetch payloads as Catmandu::BagIt::Fetch.
1747              
1748             =head2 get_fetch($filename)
1749              
1750             Get a Catmandu::BagIt::Fetch object for the file $filename.
1751              
1752             =head2 add_fetch($url,$size,$filename)
1753              
1754             Add a fetch entry to the BagIt.
1755              
1756             =head2 remove_fetch($filename)
1757              
1758             Remove a fetch entry from the BagIt.
1759              
1760             =head2 mirror_fetch($fetch)
1761              
1762             Mirror a Catmandu::BagIt::Fetch object to local disk.
1763              
1764             =head1 KNOWN LIMITATIONS
1765              
1766             This module only supports one manifest-algorithm.txt file per bag.
1767              
1768             =head1 SEE ALSO
1769              
1770             L<Catmandu::Importer::BagIt> , L<Catmandu::Exporter::BagIt> , L<Catmandu::Store::File::BagIt>
1771              
1772             =head1 AUTHOR
1773              
1774             Patrick Hochstenbach <Patrick.Hochstenbach@UGent.be>
1775              
1776             =head1 CONTRIBUTORS
1777              
1778             Nicolas Franck, C<< nicolas.franck at ugent.be >>
1779              
1780             =head1 COPYRIGHT AND LICENSE
1781              
1782             This software is copyright (c) 2015 by Patrick Hochstenbach.
1783              
1784             This is free software; you can redistribute it and/or modify it under
1785             the same terms as the Perl 5 programming language system itself.
1786              
1787             =cut