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.250';
4              
5 7     7   257167 use Catmandu::Sane;
  7         365433  
  7         47  
6 7     7   3691 use Catmandu;
  7         446707  
  7         35  
7 7     7   2037 use Moo;
  7         16  
  7         42  
8 7     7   6280 use Encode;
  7         91547  
  7         552  
9 7     7   57 use Digest::MD5;
  7         15  
  7         279  
10 7     7   1543 use Digest::SHA;
  7         8607  
  7         321  
11 7     7   46 use IO::File qw();
  7         16  
  7         133  
12 7     7   38 use IO::Handle qw();
  7         13  
  7         134  
13 7     7   3527 use File::Copy;
  7         16636  
  7         434  
14 7     7   3925 use List::MoreUtils qw(first_index uniq);
  7         62955  
  7         71  
15 7     7   8518 use Path::Tiny;
  7         15  
  7         356  
16 7     7   2538 use Path::Iterator::Rule;
  7         39413  
  7         218  
17 7     7   3724 use Path::Naive;
  7         7657  
  7         339  
18 7     7   3520 use Catmandu::BagIt::Payload;
  7         20  
  7         232  
19 7     7   3219 use Catmandu::BagIt::Fetch;
  7         21  
  7         260  
20 7     7   55 use POSIX qw(strftime);
  7         16  
  7         60  
21 7     7   8911 use LWP::UserAgent;
  7         202239  
  7         262  
22 7     7   55 use utf8;
  7         33  
  7         57  
23 7     7   213 use Catmandu::Util qw(is_string);
  7         17  
  7         447  
24 7     7   54 use namespace::clean;
  7         17  
  7         65  
25              
26             # Flags indicating which operations are needed to create a valid bag
27             use constant {
28 7         77628 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   3884 };
  7         19  
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   13 my ($self) = @_;
137 1         13 my $ua = LWP::UserAgent->new;
138 1         3133 $ua->agent('Catmandu-BagIt/' . $Catmandu::BagIt::VERSION);
139 1         73 $ua;
140             }
141              
142             # Settings requires when creating a new bag from scratch
143             sub BUILD {
144 26     26 0 198 my $self = shift;
145              
146 26         428 $self->log->debug("initializing bag");
147              
148             # Intialize the in memory settings of the bag-info
149 26         5921 $self->_update_info;
150              
151             # Initialize the in memory settings of the tag-manifests
152 26         93 $self->_update_tag_manifest;
153              
154             # Intialize the names of the basic tag files
155 26         72 my @tags = qw(bagit.txt bag-info.txt);
156 26         56 my $algorithm = $self->algorithm;
157 26         67 push @tags , "manifest-$algorithm.txt";
158 26         65 $self->_tags(\@tags);
159              
160             # Set this bag as dirty requiring an update of all the files
161 26         226 $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 52 my ($self) = @_;
167 26         40 @{$self->_error};
  26         308  
168             }
169              
170             # Return an array of tag file names
171             sub list_tags {
172 39     39 0 73 my ($self) = @_;
173 39         46 @{$self->_tags};
  39         110  
174             }
175              
176             # Return an array of all Catmandu::BagIt::Payload-s
177             sub list_files {
178 273     273 1 810 my ($self) = @_;
179 273         351 @{$self->_files};
  273         843  
180             }
181              
182             # Return a Catmandu::BagIt::Payload given a file name
183             sub get_file {
184 23     23 1 2223 my ($self,$filename) = @_;
185 23 100       65 die "usage: get_file(filename)" unless $filename;
186              
187 22         48 for ($self->list_files) {
188 32 100       118 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 383 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       11 return $_ if $_->filename eq $filename;
200             }
201 1         6 return undef;
202             }
203              
204             # Return true when this bag is dirty
205             sub is_dirty {
206 16     16 1 1365 my ($self) = @_;
207 16         100 $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 9 my ($self) = @_;
214 3         5 @{$self->_fetch} > 0;
  3         23  
215             }
216              
217             # Return an array of Catmandu::BagIt::Fetch
218             sub list_fetch {
219 352     352 1 927 my ($self) = @_;
220 352         483 @{$self->_fetch};
  352         994  
221             }
222              
223             # Return an array of tag file
224             sub list_tagsum {
225 50     50 1 1232 my ($self) = @_;
226 50         91 keys %{$self->_tag_sums};
  50         273  
227             }
228              
229             # Return the checksum of a file
230             sub get_tagsum {
231 119     119 1 1190 my ($self,$file) = @_;
232              
233 119 100       261 die "usage: get_tagsum(file)" unless $file;
234              
235 118         390 $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         134 keys %{$self->_sums};
  92         402  
242             }
243              
244             # Return the checksum of of a file name
245             sub get_checksum {
246 195     195 1 463 my ($self,$file) = @_;
247              
248 195 100       438 die "usage: get_checksum(file)" unless $file;
249              
250 194         952 $self->_sums->{$file};
251             }
252              
253             # Read the content of a bag
254             sub read {
255 11     11 1 4334 my ($class,$path) = @_;
256              
257 11 100       46 die "usage: read(path)" unless $path;
258              
259 10         238 my $self = $class->new;
260              
261 10 100       255 if (! -d $path ) {
262 1         29 $self->log->error("$path doesn't exist");
263 1         16 $self->_push_error("$path doesn't exist");
264 1         11 return;
265             }
266              
267 9         255 $self->log->info("reading: $path");
268              
269 9         146 $self->_path($path);
270              
271 9         16 my $ok = 0;
272              
273 9         34 $ok += $self->_read_version($path);
274 9         35 $ok += $self->_read_info($path);
275 9         35 $ok += $self->_read_manifest($path);
276 9         36 $ok += $self->_read_tag_manifest($path);
277 9         68 $ok += $self->_read_tags($path);
278 9         40 $ok += $self->_read_files($path);
279 9         48 $ok += $self->_read_fetch($path);
280              
281 9         51 $self->_dirty(0);
282              
283 9 50       30 if ( wantarray ) {
284 0 0       0 return $ok == 7 ? ($self) : (undef, $self->errors);
285             }
286             else {
287 9 100       56 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 1313 my ($self,$path,%opts) = @_;
294              
295 25         94 $self->_error([]);
296              
297 25 100       82 die "usage: write(path[, overwrite => 1])" unless $path;
298              
299             # Check if other processes are writing or previous processes died
300 24 50       86 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     364 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         29 $self->log->info("copying from old path: " . $self->path);
310 1         19 $self->_dirty($self->dirty | FLAG_BAGIT | FLAG_BAG_INFO | FLAG_TAG_MANIFEST | FLAG_MANIFEST | FLAG_DATA);
311              
312 1         2 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         131 $self->log->info("removing: $path");
323 4         69 path($path)->remove_tree;
324             }
325              
326 24 100       2765 if (-f $self->_bagit_file($path)) {
327 14 100       60 if ($opts{overwrite}) {
328 13         351 $self->log->info("overwriting: $path");
329             }
330             else {
331 1         29 $self->log->error("$path already exists");
332 1         18 $self->_push_error("$path already exists");
333 1         7 return undef;
334             }
335             }
336             else {
337 10         314 $self->log->info("creating: $path");
338 10         151 path($path)->mkpath;
339 10         2129 $self->_dirty($self->dirty | FLAG_BAGIT);
340             }
341              
342 23 50       216 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         217 $self->_path($path);
348              
349 23         52 my $ok = 0;
350              
351 23         81 $ok += $self->_write_bagit($path);
352 23         72 $ok += $self->_write_info($path);
353 23         78 $ok += $self->_write_data($path);
354 23         110 $ok += $self->_write_fetch($path);
355 23         85 $ok += $self->_write_manifest($path);
356 23         85 $ok += $self->_write_tag_manifest($path);
357              
358 23 50       118 return undef unless $ok == 6;
359              
360 23         56 $self->_dirty(0);
361              
362 23         69 unlink($self->_lock_file($path));
363              
364 23         116 $ok = 0;
365              
366             # Reread the contents of the bag
367 23         124 $ok += $self->_read_version($path);
368 23         85 $ok += $self->_read_info($path);
369 23         87 $ok += $self->_read_manifest($path);
370 23         79 $ok += $self->_read_tag_manifest($path);
371 23         76 $ok += $self->_read_tags($path);
372 23         89 $ok += $self->_read_files($path);
373 23         91 $ok += $self->_read_fetch($path);
374              
375 23         240 $ok == 7;
376             }
377              
378             sub _bagit_file {
379 97     97   221 my ($self,$path) = @_;
380              
381 97         1880 File::Spec->catfile($path,'bagit.txt');
382             }
383              
384             sub _bag_info_file {
385 86     86   218 my ($self,$path) = @_;
386              
387 86         1531 File::Spec->catfile($path,'bag-info.txt');
388             }
389              
390             sub _package_info_file {
391 1     1   3 my ($self,$path) = @_;
392              
393 1         10 File::Spec->catfile($path,'package-info.txt');
394             }
395              
396             sub _manifest_file {
397 32     32   68 my ($self,$path) = @_;
398              
399 32         73 for my $alg (qw(md5 sha512 sha256 sha1)) {
400 53         562 my $p = File::Spec->catfile($path,"manifest-$alg.txt");
401 53 100       1023 return ($p,$alg) if -f $p;
402             }
403              
404 1         6 return (undef,undef);
405             }
406              
407             sub _tagmanifest_file {
408 32     32   71 my ($self,$path) = @_;
409              
410 32         121 for my $alg (qw(md5 sha512 sha256 sha1)) {
411 53         570 my $p = File::Spec->catfile($path,"tagmanifest-$alg.txt");
412 53 100       990 return ($p,$alg) if -f $p;
413             }
414              
415 1         5 return (undef,undef);
416             }
417              
418             sub _fetch_file {
419 61     61   138 my ($self,$path) = @_;
420              
421 61         1546 File::Spec->catfile($path,'fetch.txt');
422             }
423              
424             sub _tag_file {
425 31     31   67 my ($self,$path,$file) = @_;
426              
427 31         464 File::Spec->catfile($path,$file);
428             }
429              
430             sub _payload_file {
431 130     130   270 my ($self,$path,$file) = @_;
432              
433 130         2668 File::Spec->catfile($path,'data',$file);
434             }
435              
436             sub _lock_file {
437 72     72   146 my ($self,$path) = @_;
438              
439 72         2311 File::Spec->catfile($path,'.lock');
440             }
441              
442             sub locked {
443 26     26 1 65 my ($self,$path) = @_;
444 26   66     71 $path //= $self->path;
445              
446 26 50       70 return undef unless defined($path);
447              
448 26         67 -f $self->_lock_file($path);
449             }
450              
451             sub touch {
452 24     24 0 63 my ($self,$path) = @_;
453              
454 24 50       66 die "usage: touch(path)"
455             unless defined($path);
456              
457 24         94 path("$path")->spew("");
458              
459 24         10100 1;
460             }
461              
462             sub add_file {
463 21     21 1 3214 my ($self, $filename, $data, %opts) = @_;
464              
465 21 50 33     127 die "usage: add_file(filename, data [, overwrite => 1])"
466             unless defined($filename) && defined($data);
467              
468 21         86 $self->_error([]);
469              
470 21 100       70 unless ($self->_is_legal_file_name($filename)) {
471 1         26 $self->log->error("illegal file name $filename");
472 1         16 $self->_push_error("illegal file name $filename");
473 1         6 return;
474             }
475              
476 20         468 $self->log->info("adding file $filename");
477              
478 20 100       277 if ($opts{overwrite}) {
479 7         28 $self->remove_file($filename);
480             }
481              
482 20 100       66 if ($self->get_checksum("$filename")) {
483 1         18 $self->log->error("$filename already exists in bag");
484 1         17 $self->_push_error("$filename already exists in bag");
485 1         5 return;
486             }
487              
488 19         144 my $payload = Catmandu::BagIt::Payload->from_any($filename,$data);
489 19         727 $payload->flag(FLAG_DIRTY);
490              
491 19         35 my $sum;
492              
493 19 100       247 if ( is_string($opts{md5}) ) {
    50          
    50          
    50          
494 2 50       17 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         19 $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         68 my $fh = $payload->open;
556              
557 17         1461 binmode($fh,":raw");
558              
559 17         68 $sum = $self->_calc_checksum_sum($fh);
560              
561 17         1111 close($fh);
562              
563             }
564              
565 18         79 push @{ $self->_files }, $payload;
  18         96  
566              
567 18         87 $self->_sums->{"$filename"} = $sum;
568              
569             # Total size changes, therefore tag manifest changes
570 18         68 $self->_update_info;
571 18         62 $self->_update_tag_manifest; # Try to update the manifest .. but it is dirty
572             # Until we serialize the bag
573              
574 18         120 $self->_dirty($self->dirty | FLAG_DATA | FLAG_MANIFEST | FLAG_BAG_INFO | FLAG_TAG_MANIFEST);
575              
576 18         89 1;
577             }
578              
579             sub remove_file {
580 14     14 1 872 my ($self, $filename) = @_;
581              
582 14 50       42 die "usage: remove_file(filename)" unless defined($filename);
583              
584 14         47 $self->_error([]);
585              
586 14 100       44 unless ($self->get_checksum($filename)) {
587 5         101 $self->log->error("$filename doesn't exist in bag");
588 5         79 $self->_push_error("$filename doesn't exist in bag");
589 5         14 return;
590             }
591              
592 9         184 $self->log->info("removing file $filename");
593              
594 9     9   144 my $idx = first_index { $_->{filename} eq $filename } @{ $self->_files };
  9         32  
  9         77  
595              
596 9 50       49 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         44  
  9         24  
602              
603 9         40 $self->_files(\@files);
604              
605 9         28 delete $self->_sums->{$filename};
606              
607 9         32 $self->_update_info;
608 9         31 $self->_update_tag_manifest;
609              
610 9         40 $self->_dirty($self->dirty | FLAG_DATA | FLAG_MANIFEST | FLAG_BAG_INFO | FLAG_TAG_MANIFEST);
611              
612 9         32 1;
613             }
614              
615             sub add_fetch {
616 4     4 1 610 my ($self, $url, $size, $filename) = @_;
617              
618 4 50 33     55 die "usage add_fetch(url,size,filename)"
      33        
619             unless defined($url) && $size =~ /^[0-9]+$/ && defined($filename);
620              
621 4 50       14 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         16  
627              
628 4         16 $self->_fetch(\@old);
629              
630 4         6 push @{$self->_fetch} , Catmandu::BagIt::Fetch->new(url => $url , size => $size , filename => $filename);
  4         80  
631              
632 4         1523 $self->_update_info;
633 4         12 $self->_update_tag_manifest;
634              
635 4         18 $self->_dirty($self->dirty | FLAG_FETCH | FLAG_TAG_MANIFEST);
636              
637 4         16 1;
638             }
639              
640             sub remove_fetch {
641 2     2 1 1972 my ($self, $filename) = @_;
642              
643 2 50       8 die "usage remove_fetch(filename)" unless defined($filename);
644              
645 2         52 $self->log->info("removing fetch for $filename");
646              
647 2         27 my (@old) = grep { $_->filename ne $filename} @{$self->_fetch};
  2         10  
  2         10  
648              
649 2         10 $self->_fetch(\@old);
650 2         8 $self->_update_info;
651 2         7 $self->_update_tag_manifest;
652 2         11 $self->_dirty($self->dirty | FLAG_FETCH | FLAG_TAG_MANIFEST);
653              
654 2         10 1;
655             }
656              
657             sub mirror_fetch {
658 1     1 1 4 my ($self, $fetch) = @_;
659              
660 1 50 33     14 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         638 my $url = $fetch->url;
666 1         4 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         36 my $response = $self->user_agent->mirror($url,$tmp_filename);
672              
673 1 50       10185 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         29 $self->log->info("updating file listing...");
682 1         23 $self->log->debug("add new $filename");
683 1         24 $self->add_file($filename, IO::File->new($tmp_filename,'r'), overwrite => 1);
684             }
685              
686             sub add_info {
687 188     188 1 805 my ($self,$name,$values) = @_;
688              
689 188 50 33     864 die "usage add_info(name,values)"
690             unless defined($name) && defined($values);
691              
692 188 100       957 if ($name =~ /^(Bag-Size|Bagging-Date|Payload-Oxum)$/) {
693 178         263 for my $part (@{$self->_info}) {
  178         480  
694 277 100       651 if ($part->[0] eq $name) {
695 100         172 $part->[1] = $values;
696 100         202 return;
697             }
698             }
699 78         129 push @{$self->_info} , [ $name , $values ];
  78         237  
700 78         159 return;
701             }
702              
703 10         231 $self->log->info("adding info $name");
704              
705 10 50       140 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         21 push @{$self->_info} , [ $name , $values ];
  10         44  
712             }
713              
714 10         49 $self->_update_tag_manifest;
715              
716 10         47 $self->_dirty($self->dirty | FLAG_BAG_INFO | FLAG_TAG_MANIFEST);
717              
718 10         35 1;
719             }
720              
721             sub remove_info {
722 5     5 1 549 my ($self,$name) = @_;
723              
724 5 50       15 die "usage remove_info(name)"
725             unless defined($name);
726              
727 5 100       30 if ($name =~ /^(Bag-Size|Bagging-Date|Payload-Oxum)$/) {
728 1         23 $self->log->error("removing info $name - is read-only");
729 1         15 return undef;
730             }
731              
732 4         96 $self->log->info("removing info $name");
733              
734 4         52 my (@old) = grep { $_->[0] ne $name } @{$self->_info};
  17         40  
  4         14  
735              
736 4         18 $self->_info(\@old);
737              
738 4         13 $self->_update_tag_manifest;
739              
740 4         19 $self->_dirty($self->dirty | FLAG_BAG_INFO | FLAG_TAG_MANIFEST);
741              
742 4         15 1;
743             }
744              
745             sub list_info_tags {
746 125     125 1 2137 my ($self) = @_;
747 125         202 uniq map { $_->[0] } @{$self->_info};
  452         1714  
  125         325  
748             }
749              
750             sub get_info {
751 447     447 1 1680 my ($self,$field,$join) = @_;
752 447   100     1661 $join //= '; ';
753              
754 447 50       794 die "usage: get_info(field[,$join])" unless $field;
755              
756 447         614 my @res = map { $_->[1] } grep { $_->[0] eq $field } @{$self->_info};
  449         942  
  1644         3106  
  447         885  
757              
758 447 100       1234 wantarray ? @res : join $join, @res;
759             }
760              
761             sub size {
762 62     62 1 1983 my $self = shift;
763              
764 62         150 my $total = $self->_size;
765              
766 62 50       243 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         25 sprintf "%-.3f MB" , $total/(1000**2);
777             }
778             else {
779 60         723 sprintf "%-.3f KB" , $total/1000;
780             }
781             }
782              
783             sub payload_oxum {
784 64     64 1 970 my $self = shift;
785              
786 64         140 my $size = $self->_size;
787 64         137 my $count = $self->list_files;
788              
789 64   50     134 my $fetches = $self->list_fetch // 0;
790              
791 64         104 $count += $fetches;
792              
793 64         245 return "$size.$count";
794             }
795              
796             sub complete {
797 10     10 1 1675 my $self = shift;
798 10   100     56 my $path = $self->path || '';
799              
800 10         83 $self->_error([]);
801              
802 10         254 $self->log->info("checking complete");
803              
804 10 50 33     200 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     64 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         25 my @missing = ();
815              
816 10         31 foreach my $file ($self->list_checksum) {
817 23 100 50     46 unless (grep { (my $filename = $_->{filename} || '') =~ /^$file$/ } $self->list_files) {
  42         409  
818 19         49 push @missing , $file;
819             }
820             }
821              
822 10         42 foreach my $file ($self->list_tagsum) {
823 31 50       74 unless (grep { /^$file$/ } $self->list_tags) {
  100         732  
824 0         0 push @missing , $file;
825             }
826             }
827              
828 10         73 foreach my $file (@missing) {
829 19 50       36 unless (grep { $_->filename =~ /^$file$/ } $self->list_fetch) {
  0         0  
830 19         355 $self->log->error("file $file doesn't exist in bag and fetch.txt");
831 19         216 $self->_push_error("file $file doesn't exist in bag and fetch.txt");
832             }
833             }
834              
835 10 100       25 my $has_fetch = $self->list_fetch > 0 ? 1 : 0;
836              
837 10 100 66     31 $self->errors == 0 && @missing == 0 && $has_fetch == 0;
838             }
839              
840             sub valid {
841 11     11 1 30 my $self = shift;
842              
843 11         270 $self->log->info("checking valid");
844              
845             my $validator = sub {
846 74     74   157 my ($file, $tag) = @_;
847 74         171 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       330 my $sum = $tag == 0 ? $self->get_checksum($file) : $self->get_tagsum($file);
857 74 100       249 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       6498 unless ($fh) {
862 38         923 $self->log->error("can't read $file");
863 38         496 return (0,"can't read $file");
864             }
865              
866 36         182 binmode($fh,':raw');
867              
868 36         115 my $sum_check = $self->_calc_checksum_sum($fh);
869              
870 36         1016 close($fh);
871              
872 36 100       247 unless (lc($sum) eq lc($sum_check)) {
873 2         53 $self->log->error("$file checksum fails $sum <> $sum_check");
874 2         41 return (0,"$file checksum fails $sum <> $sum_check");
875             }
876              
877 34         182 (1);
878 11         179 };
879              
880 11         52 $self->_error([]);
881              
882 11 100       47 if ($self->dirty) {
883 1         18 $self->log->error("bag is dirty : first serialize (write) then try again");
884 1         13 $self->_push_error("bag is dirty : first serialize (write) then try again");
885 1         17 return 0;
886             }
887              
888 10         31 foreach my $file ($self->list_checksum) {
889 43         86 my ($code,$msg) = $validator->($file,0);
890              
891 43 100       111 if ($code == 0) {
892 38         90 $self->_push_error($msg);
893             }
894             }
895              
896 10         38 foreach my $file ($self->list_tagsum) {
897 31         72 my ($code,$msg) = $validator->($file,1);
898              
899 31 100       156 if ($code == 0) {
900 2         12 $self->_push_error($msg);
901             }
902             }
903              
904 10         43 $self->errors == 0;
905             }
906              
907             #-----------------------------------------
908              
909             sub _push_error {
910 76     76   151 my ($self,$msg) = @_;
911 76   50     258 my $errors = $self->_error // [];
912 76         161 push @$errors , $msg;
913 76         191 $self->_error($errors);
914             }
915              
916             sub _size {
917 126     126   193 my $self = shift;
918 126         266 my $path = $self->path;
919              
920 126         188 my $total = 0;
921              
922 126         273 foreach my $file ($self->list_files) {
923 86         286 my $fh = $file->open;
924 86         6629 my $size = [ $fh->stat ]->[7];
925 86         1627 $fh->close;
926 86         1556 $total += $size;
927             }
928              
929 126         318 foreach my $item ($self->list_fetch) {
930 11         33 my $size = $item->size;
931 11         27 $total += $size;
932             }
933              
934 126         245 $total;
935             }
936              
937             sub _update_info {
938 59     59   115 my $self = shift;
939              
940 59         1164 $self->log->debug("updating the default info");
941              
942             # Add some goodies to the info file...
943 59         3511 $self->add_info('Bagging-Date', strftime "%Y-%m-%d", gmtime);
944 59         213 $self->add_info('Bag-Size',$self->size);
945 59         167 $self->add_info('Payload-Oxum',$self->payload_oxum);
946             }
947              
948             sub _update_tag_manifest {
949 96     96   172 my $self = shift;
950              
951 96         2234 $self->log->debug("updating the tag manifest");
952              
953             {
954 96         263 my $sum = $self->_calc_checksum_sum($self->_bagit_as_string);
955 96         368 $self->_tag_sums->{'bagit.txt'} = $sum;
956             }
957              
958             {
959 96         1021 my $sum = $self->_calc_checksum_sum($self->_baginfo_as_string);
  96         252  
960 96         317 $self->_tag_sums->{'bag-info.txt'} = $sum;
961             }
962              
963             {
964 96         154 my $sum = $self->_calc_checksum_sum($self->_manifest_as_string);
  96         176  
  96         230  
965 96         224 my $algorithm = $self->algorithm;
966 96         366 $self->_tag_sums->{"manifest-$algorithm.txt"} = $sum;
967             }
968              
969 96 100       217 if ($self->list_fetch) {
970 8         27 my $sum = $self->_calc_checksum_sum($self->_fetch_as_string);
971 8         29 $self->_tag_sums->{'fetch.txt'} = $sum;
972              
973 8 100       20 unless (grep {/fetch.txt/} $self->list_tags) {
  28         84  
974 4         7 push @{$self->_tags} , 'fetch.txt';
  4         12  
975             }
976             }
977             else {
978 88         161 my (@new) = grep { $_ ne 'fetch.txt' } @{$self->_tags};
  188         446  
  88         234  
979 88         240 $self->_tags(\@new);
980 88         249 delete $self->_tag_sums->{'fetch.txt'};
981             }
982             }
983              
984             sub _read_fetch {
985 32     32   82 my ($self, $path) = @_;
986              
987 32         108 $self->_fetch([]);
988              
989 32 100       93 return 1 unless -f $self->_fetch_file($path);
990              
991 5         139 $self->log->debug("reading fetch.txt");
992              
993 5         63 foreach my $line (path($self->_fetch_file($path))->lines_utf8) {
994 5         1517 $line =~ s/\r\n$/\n/g;
995 5         15 chomp($line);
996              
997 5         48 my ($url,$size,$filename) = split(/\s+/,$line,3);
998              
999 5         30 $filename =~ s/^data\///;
1000              
1001 5         13 push @{ $self->_fetch } , Catmandu::BagIt::Fetch->new(url => $url , size => $size , filename => $filename);
  5         150  
1002             }
1003              
1004 5         136 1;
1005             }
1006              
1007             sub _read_tag_manifest {
1008 32     32   81 my ($self, $path) = @_;
1009              
1010 32         154 $self->_tag_sums({});
1011              
1012 32         124 my ($manifest,$algorithm) = $self->_tagmanifest_file($path);
1013              
1014 32 100       177 if (! $manifest ) {
1015 1         3 return 1;
1016             }
1017              
1018 31         783 $self->log->debug("reading tagmanifest-$algorithm.txt");
1019              
1020 31         428 foreach my $line (path($manifest)->lines_utf8) {
1021 96         9335 $line =~ s/\r\n$/\n/g;
1022 96         171 chomp($line);
1023 96         526 my ($sum,$file) = split(/\s+/,$line,2);
1024 96         391 $self->_tag_sums->{$file} = $sum;
1025             }
1026              
1027 31 50 33     256 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         75 $self->{algorithm} = $algorithm;
1033             }
1034              
1035 31         70 1;
1036             }
1037              
1038             sub _read_manifest {
1039 32     32   79 my ($self, $path) = @_;
1040              
1041 32         112 $self->_sums({});
1042              
1043 32         107 my ($manifest,$algorithm) = $self->_manifest_file($path);
1044              
1045 32 100       139 if (! $manifest ) {
1046 1         7 $self->_push_error("no manifest-{digest}.txt in $path");
1047 1         3 return 0;
1048             }
1049              
1050 31         793 $self->log->debug("reading manifest-$algorithm.txt");
1051              
1052 31         455 foreach my $line (path($manifest)->lines_utf8) {
1053 66         6362 $line =~ s/\r\n$/\n/g;
1054 66         123 chomp($line);
1055 66         348 my ($sum,$file) = split(/\s+/,$line,2);
1056 66         265 $file =~ s/^data\///;
1057             # Unescape LF,CR,% when needed
1058 66 50       213 if ($self->escape) {
1059 66         132 $file =~ s{%0A}{\n}mg;
1060 66         106 $file =~ s{%0D}{\r}mg;
1061 66         109 $file =~ s{%25}{%}mg;
1062             }
1063 66         242 $self->_sums->{$file} = $sum;
1064             }
1065              
1066             # The algorithm of the bag will be set by the algorithm of the manifest
1067 31         2903 $self->{algorithm} = $algorithm;
1068              
1069 31         81 1;
1070             }
1071              
1072             sub _read_tags {
1073 32     32   88 my ($self, $path) = @_;
1074              
1075 32         730 $self->log->debug("reading tag files");
1076              
1077 32         408 $self->_tags([]);
1078              
1079 32         211 my $rule = Path::Iterator::Rule->new;
1080 32         329 $rule->max_depth(1);
1081 32         1254 $rule->file;
1082 32         751 my $iter = $rule->iter($path);
1083              
1084 32         3094 while(my $file = $iter->()) {
1085 129         48607 $file =~ s/^$path.//;
1086              
1087 129 100       518 next if $file =~ /^tagmanifest-\w+.txt$/;
1088              
1089 98         145 push @{ $self->_tags } , $file;
  98         506  
1090             }
1091              
1092 32         1309 1;
1093             }
1094              
1095             sub _read_files {
1096 32     32   89 my ($self, $path) = @_;
1097              
1098 32         695 $self->log->debug("reading data files");
1099              
1100 32         472 $self->_files([]);
1101              
1102 32 100       640 if (! -d "$path/data" ) {
1103 3         78 $self->log->error("payload directory $path/data doesn't exist");
1104 3         52 $self->_push_error("payload directory $path/data doesn't exist");
1105 3         9 return 1;
1106             }
1107              
1108 29         233 my $rule = Path::Iterator::Rule->new;
1109 29         320 $rule->file;
1110 29         838 my $iter = $rule->iter("$path/data");
1111              
1112 29         2663 while(my $file = $iter->()) {
1113 32         14096 my $filename = $file;
1114 32         329 $filename =~ s/^$path\/data\///;
1115 32         784 my $payload = Catmandu::BagIt::Payload->new(filename => $filename, path => $file);
1116 32         1963 push @{ $self->_files } , $payload;
  32         179  
1117             }
1118              
1119 29         3420 1;
1120             }
1121              
1122             sub _read_info {
1123 32     32   78 my ($self, $path) = @_;
1124              
1125 32         732 $self->log->debug("reading the tag info file");
1126              
1127 32         519 $self->_info([]);
1128              
1129 32 100       108 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       517 if (! -f $info_file) {
1134 1         25 $self->log->error("no package-info.txt or bag-info.txt in $path");
1135 1         23 $self->_push_error("no package-info.txt or bag-info.txt in $path");
1136 1         2 return 0;
1137             }
1138              
1139 31         199 foreach my $line (path($info_file)->lines_utf8) {
1140 127         9917 $line =~ s/\r\n$/\n/g;
1141 127         198 chomp($line);
1142              
1143 127 100       381 if ($line =~ /^\s+/) {
1144 12         47 $line =~ s/^\s*//;
1145 12         33 $self->_info->[-1]->[1] .= $line;
1146 12         23 next;
1147             }
1148              
1149 115         543 my ($n,$v) = split(/\s*:\s*/,$line,2);
1150              
1151 115         190 push @{ $self->_info } , [ $n , $v ];
  115         409  
1152             }
1153              
1154 31         114 1;
1155             }
1156              
1157             sub _read_version {
1158 32     32   98 my ($self, $path) = @_;
1159              
1160 32         784 $self->log->debug("reading the version file");
1161              
1162 32 100       380 if (! -f $self->_bagit_file($path) ) {
1163 1         25 $self->log->error("no bagit.txt in $path");
1164 1         18 $self->_push_error("no bagit.txt in $path");
1165 1         4 return 0;
1166             }
1167              
1168 31         216 foreach my $line (path($self->_bagit_file($path))->lines_utf8) {
1169 62         16688 $line =~ s/\r\n$/\n/g;
1170 62         133 chomp($line);
1171 62         455 my ($n,$v) = split(/\s*:\s*/,$line,2);
1172              
1173 62 100       242 if ($n eq 'BagIt-Version') {
    50          
1174 31         150 $self->_version($v);
1175             }
1176             elsif ($n eq 'Tag-File-Character-Encoding') {
1177 31         113 $self->_encoding($v);
1178             }
1179             }
1180              
1181 31         112 1;
1182             }
1183              
1184             sub _write_bagit {
1185 23     23   65 my ($self,$path) = @_;
1186              
1187 23 100       101 return 1 unless $self->dirty & FLAG_BAGIT;
1188              
1189 10         350 $self->log->info("writing the version file");
1190              
1191 10         138 path($self->_bagit_file($path))->spew_utf8($self->_bagit_as_string);
1192              
1193 10         6182 $self->_dirty($self->dirty ^ FLAG_BAGIT);
1194              
1195 10         27 1;
1196             }
1197              
1198             sub _bagit_as_string {
1199 106     106   470 my $self = shift;
1200              
1201 106         248 my $version = $self->version;
1202 106         255 my $encoding = $self->encoding;
1203              
1204 106         454 return <<EOF;
1205             BagIt-Version: $version
1206             Tag-File-Character-Encoding: $encoding
1207             EOF
1208             }
1209              
1210             sub _write_info {
1211 23     23   57 my ($self,$path) = @_;
1212              
1213 23 50       73 return 1 unless $self->dirty & FLAG_BAG_INFO;
1214              
1215 23         651 $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         9625 $self->_dirty($self->dirty ^ FLAG_BAG_INFO);
1220              
1221 23         65 1;
1222             }
1223              
1224             sub _baginfo_as_string {
1225 119     119   841 my $self = shift;
1226              
1227 119         187 my $str = '';
1228              
1229 119         274 foreach my $tag ($self->list_info_tags) {
1230 425         916 my @values = $self->get_info($tag);
1231 425         782 foreach my $val (@values) {
1232 427         1997 my @msg = split //, "$tag: $val";
1233              
1234 427         703 my $cnt = 0;
1235 427 100       1392 while (my (@chunk) = splice(@msg,0,$cnt == 0 ? 79 : 78)) {
1236 427 50       1612 $str .= ($cnt == 0 ? '' : ' ') . join('',@chunk) . "\n";
1237 427         1862 $cnt++;
1238             }
1239             }
1240             }
1241              
1242 119         410 $str;
1243             }
1244              
1245             # Write BagIt data payloads to disk
1246             sub _write_data {
1247 23     23   64 my ($self,$path) = @_;
1248              
1249             # Return immediately when no files need to be written
1250 23 100       96 return 1 unless $self->dirty & FLAG_DATA;
1251              
1252 22         636 $self->log->info("writing the data files");
1253              
1254             # Create a data/ directory for payloads
1255 22 100       660 unless (-d "$path/data") {
1256 10 50       501 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         85 my @all_names_in_bag = ();
1267              
1268 22         96 foreach my $item ($self->list_files) {
1269 24         88 my $filename = 'data/' . $item->{filename};
1270 24         54 push @all_names_in_bag , $filename;
1271              
1272             # Only process files that are dirty
1273 24 100       108 next unless $item->flag & FLAG_DIRTY;
1274              
1275             # Check for deep directories that need to be stored
1276 14         49 my $dir = $filename; $dir =~ s/\/[^\/]+$//;
  14         96  
1277              
1278 14         368 $self->log->info("serializing $filename");
1279              
1280 14 50       400 path("$path/$dir")->mkpath unless -d "$path/$dir";
1281              
1282 14         72 my $old_path = $item->path;
1283 14         61 my $new_path = "$path/$filename";
1284              
1285 14 50       65 if ($item->is_new) {
1286 14         59 File::Copy::move($old_path,$new_path);
1287             }
1288             else {
1289 0         0 File::Copy::copy($old_path,$new_path);
1290             }
1291              
1292 14         1269 $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         183 my $rule = Path::Iterator::Rule->new;
1297 22         261 $rule->file;
1298 22         705 my $iter = $rule->iter("$path/data");
1299              
1300 22         2153 while(my $file = $iter->()) {
1301 28         12851 my $filename = $file;
1302 28         330 $filename =~ s/^$path\///;
1303              
1304 28 100       88 unless (grep {$filename eq $_} @all_names_in_bag) {
  51         181  
1305 4         87 $self->log->info("deleting $path/$filename");
1306 4         280 unlink "$path/$filename";
1307             }
1308             }
1309              
1310 22         2140 $self->_dirty($self->dirty ^ FLAG_DATA);
1311              
1312 22         394 1;
1313             }
1314              
1315             sub _write_fetch {
1316 23     23   64 my ($self,$path) = @_;
1317              
1318 23 50       82 return 1 unless $self->dirty & FLAG_FETCH;
1319              
1320 23         63 my $fetch_str = $self->_fetch_as_string;
1321              
1322 23 100 66     131 unless (defined($fetch_str) && length($fetch_str)) {
1323 20         484 $self->log->info("removing fetch.txt");
1324 20 100       252 unlink $self->_fetch_file($path) if -f $self->_fetch_file($path);
1325 20         104 return 1;
1326             }
1327              
1328 3         75 $self->log->info("writing the fetch file");
1329              
1330 3 50       43 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         11 path($self->_fetch_file($path))->spew_utf8($fetch_str);
1337              
1338 3         1330 $self->_dirty($self->dirty ^ FLAG_FETCH);
1339              
1340 3         8 1;
1341             }
1342              
1343             sub _fetch_as_string {
1344 31     31   56 my $self = shift;
1345              
1346 31         54 my $str = '';
1347              
1348 31         76 foreach my $f ($self->list_fetch) {
1349 11         92 $str .= sprintf "%s %s data/%s\n" , $f->url, $f->size, $f->filename;
1350             }
1351              
1352 31         77 $str;
1353             }
1354              
1355             sub _write_manifest {
1356 23     23   62 my ($self,$path) = @_;
1357              
1358 23 50       91 return 1 unless $self->dirty & FLAG_MANIFEST;
1359              
1360 23         591 $self->log->info("writing the manifest file");
1361              
1362 23         268 my $algorithm = $self->algorithm;
1363 23         218 my $manifest = File::Spec->catfile($path,"manifest-$algorithm.txt");
1364              
1365 23         106 path($manifest)->spew_utf8($self->_manifest_as_string);
1366              
1367 23         9072 $self->_dirty($self->dirty ^ FLAG_MANIFEST);
1368              
1369 23         67 1;
1370             }
1371              
1372             sub _manifest_as_string {
1373 119     119   866 my $self = shift;
1374 119         255 my $path = $self->path;
1375              
1376 119 100       332 return undef unless defined $path;
1377              
1378 67         104 my $str = '';
1379              
1380 67         158 foreach my $file ($self->list_checksum) {
1381 87 100       230 next unless -f $self->_payload_file($path,$file);
1382 75         342 my $sum = $self->get_checksum($file);
1383             # Escape LF, CR and % (when needed)
1384 75 50       219 if ($self->escape) {
1385 75         184 $file =~ s{%}{%25}mg;
1386 75         142 $file =~ s{\n}{%0A}mg;
1387 75         129 $file =~ s{\r}{%0D}mg;
1388             }
1389 75         313 $str .= "$sum data/$file\n";
1390             }
1391              
1392 67         249 $str;
1393             }
1394              
1395             sub _write_tag_manifest {
1396 23     23   70 my ($self,$path) = @_;
1397              
1398 23 50       85 return 1 unless $self->dirty & FLAG_TAG_MANIFEST;
1399              
1400             # The tag manifest can be dirty when writing new files
1401 23         78 $self->_update_tag_manifest;
1402              
1403 23         519 $self->log->info("writing the tagmanifest file");
1404              
1405 23         293 my $algorithm = $self->algorithm;
1406 23         238 my $manifest = File::Spec->catfile($path,"tagmanifest-$algorithm.txt");
1407              
1408 23         102 path($manifest)->spew_utf8($self->_tag_manifest_as_string);
1409              
1410 23         9685 $self->_dirty($self->dirty ^ FLAG_MANIFEST);
1411              
1412 23         68 1;
1413             }
1414              
1415             sub _tag_manifest_as_string {
1416 23     23   716 my $self = shift;
1417              
1418 23         81 my $str = '';
1419              
1420 23         71 foreach my $file ($self->list_tagsum) {
1421 72         140 my $sum = $self->get_tagsum($file);
1422 72         215 $str .= "$sum $file\n";
1423             }
1424              
1425 23         453 $str;
1426             }
1427              
1428             sub _calc_checksum_sum {
1429 349     349   688 my ($self, $data) = @_;
1430              
1431 349         662 my $algorithm = $self->algorithm;
1432              
1433 349         507 my $ctx;
1434              
1435 349 100       1073 if ($algorithm =~ /^sha/) {
1436 259         963 $ctx = Digest::SHA->new($algorithm);
1437             }
1438             else {
1439 90         440 $ctx = Digest::MD5->new;
1440             }
1441              
1442 349 100       4660 if (!defined $data) {
    100          
    50          
    50          
1443 52         489 return $ctx->add(Encode::encode_utf8(''))->hexdigest;
1444             }
1445             elsif (! ref $data) {
1446 244         2673 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         1406 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   53 my ($self, $filename) = @_;
1461              
1462             # Adding some security measures to stop people writing data
1463             # outside the data directory of the bagit..
1464 25         101 my $normal = Path::Naive::normalize_path($filename);
1465 25         1154 my $abs = Path::Naive::abs_path($filename,'/');
1466              
1467 25 50       1927 return undef unless ($filename eq $normal);
1468 25 100       74 return undef unless ("/$filename" eq $abs);
1469 24         74 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