File Coverage

lib/Archive/Par.pm
Criterion Covered Total %
statement 28 30 93.3
branch n/a
condition n/a
subroutine 10 10 100.0
pod n/a
total 38 40 95.0


line stmt bran cond sub pod time code
1             # (X)Emacs mode: -*- cperl -*-
2              
3             package Archive::Par;
4              
5             =head1 NAME
6              
7             Archive::Par - use & manipulate par files
8              
9             =head1 SYNOPSIS
10              
11             use Archive::Par qw( $PACKAGE $VERSION );
12              
13             =head1 DESCRIPTION
14              
15             Z<>
16              
17             =cut
18              
19             # ----------------------------------------------------------------------------
20              
21             # Pragmas -----------------------------
22              
23             require 5.005_62;
24 9     9   1171757 use strict;
  9         23  
  9         456  
25 9     9   55 use warnings;
  9         16  
  9         395  
26              
27             # Inheritance -------------------------
28              
29 9     9   50 use base qw( Exporter );
  9         23  
  9         1747  
30             our @EXPORT_OK = qw( $PACKAGE $VERSION );
31              
32             # Utility -----------------------------
33              
34 9     9   49 use Carp qw( carp croak );
  9         18  
  9         623  
35 9     9   13240 use Class::MethodMaker 1.02 qw( );
  9         278983  
  9         497  
36 9     9   3371 use Fatal 1.02 qw( :void close open seek sysopen );
  9         46378  
  9         83  
37 9     9   21134 use File::Basename 2.6 qw( dirname );
  9         282  
  9         759  
38 9     9   52 use File::Spec::Functions qw( catfile );
  9         17  
  9         841  
39 9     9   4108 use IPC::Run 0.44 qw( harness run );
  9         139521  
  9         568  
40 9     9   16822 use Log::Info 1.03 qw( :DEFAULT :log_levels :default_channels );
  0            
  0            
41              
42             # ----------------------------------------------------------------------------
43              
44             # CLASS METHODS --------------------------------------------------------------
45              
46             # -------------------------------------
47             # CLASS CONSTANTS
48             # -------------------------------------
49              
50             =head1 CLASS CONSTANTS
51              
52             Z<>
53              
54             =cut
55              
56             # Bits used in status bitmask
57              
58             # File statuses:
59             # FOUND RESTORABLE CORRUPT OK
60             # OK x x
61             # Moved x
62             # Corrupt (Recover) x x x
63             # Corrupt (Buggered) x x
64             # Not Found (Recover) x
65             # Not Found (Buggered)
66              
67             use constant FILE_FOUND => 1;
68             use constant FILE_RESTORABLE => 2;
69             use constant FILE_CORRUPT => 4;
70             use constant FILE_OK => 8;
71              
72             # -------------------------------------
73              
74             our $PACKAGE = 'Archive-Par';
75             our $VERSION = '1.01';
76              
77             # -------------------------------------
78             # CLASS CONSTRUCTION
79             # -------------------------------------
80              
81             # -------------------------------------
82             # CLASS COMPONENTS
83             # -------------------------------------
84              
85             =head1 CLASS COMPONENTS
86              
87             Z<>
88              
89             =cut
90              
91             # -------------------------------------
92             # CLASS HIGHER-LEVEL FUNCTIONS
93             # -------------------------------------
94              
95             =head1 CLASS HIGHER-LEVEL FUNCTIONS
96              
97             Z<>
98              
99             =cut
100              
101             ## _parse_par_output
102             #
103             # Args:
104             # -) text
105             # Text to parse
106             # -) fn
107             # Name of file submitted to par (for sanity checking).
108             #
109             # Returns:
110             # -) status
111             # hashref from file name to status
112             # -) file_name
113             # hashref from file name, as it should be as per par, to file found
114             # -) bad_old_files
115             # Where new files have been created containing bad data (e.g., old corrupt
116             # files being moved out of the way), these files are enumerated here.
117              
118             sub _parse_par_output {
119             my $class = shift;
120             my ($text, $fn) = @_;
121              
122             my @lines = split /\n/, $text;
123              
124             my $lineno = 0;
125             croak sprintf("Bad start format on par line %d:\n-->%s<--\n" .
126             "Expected:\n-->%s<--\n",
127             $lineno, $lines[$lineno], "Checking $fn")
128             unless $lines[$lineno] eq "Checking $fn";
129             $lineno++;
130              
131             my (%status, %file_name, @bad_old_files);
132              
133             LINE:
134             for ( ; substr($lines[$lineno], 0, 2) eq ' '; $lineno++ ) {
135             my ($file, $found, $foundfile);
136              
137             if ( ($file, $found, $foundfile) =
138             ($lines[$lineno] =~
139             /^ (.{40,}) - (OK|NOT FOUND|CORRUPT|FOUND: (.*))$/) ) {
140             # Corrupt files are handled by ERROR: RE below
141             next LINE
142             if $found eq 'CORRUPT';
143              
144             $file =~ s! +$!!;
145              
146             if ( $found eq 'OK' ) {
147             $status{$file} = FILE_FOUND | FILE_OK;
148             } elsif ( substr($found, 0, 5) eq 'FOUND' ) {
149             # If file is already marked with a status, let the presence of
150             # file_name be the only marker of finding it elsewhere
151             $status{$file} = FILE_FOUND
152             unless exists $status{$file};
153             $file_name{$file} = $foundfile;
154             } else {
155             $status{$file} = 0;
156             }
157             } elsif ( ($file) =
158             ($lines[$lineno] =~ /^ {6}ERROR: (.*): Failed md5 sum$/) ) {
159             $status{$file} = FILE_FOUND | FILE_CORRUPT;
160             } elsif ( my ($from, $to) =
161             ($lines[$lineno] =~ /^ {4}Rename: (.*) -> (.*)$/) ) {
162             if ( exists $file_name{$to} ) {
163             if ( $file_name{$to} eq $from ) { # If $to is real name (as per par)
164             # of from file, all is well
165             delete $file_name{$to};
166             $status{$to} = FILE_FOUND | FILE_OK;
167             } else { # Else we know nothing about the incoming file. Eek!
168             croak("Nothing known about incoming file: $from (renaming to $to):" .
169             "\n$lines[$lineno]\n");
170             }
171             } elsif ( exists $status{$from} ) {
172             if ( $status{$from} & FILE_CORRUPT ) {
173             # If file is corrupt, we're moving it to make way
174             $status{$from} = FILE_RESTORABLE;
175             push @bad_old_files, $to;
176             } else { # Else file is not corrupt; why are we moving it?
177             croak("Par is moving file $from to $to; I don't understand why..." .
178             "\n$lines[$lineno]\n");
179             }
180             } else {
181             croak("Par is moving file $from to $to; I know not why..." .
182             "\n$lines[$lineno]\n");
183             }
184             } else {
185             croak
186             sprintf("Don't know how to handle this (on par line %d):\n %s\n",
187             $lineno, $lines[$lineno]);
188             }
189             }
190              
191             if ( $lines[$lineno] eq '' ) {
192             # Break into list of PXX volumes and file statuses
193             # Getting here is indication of a problem (of the order of a missing or
194             # broken source file).
195             $lineno++;
196              
197             croak "Bad looking format on par c line $lineno: $lines[$lineno]\n"
198             unless $lines[$lineno] eq 'Looking for PXX volumes:';
199             $lineno++;
200              
201             # for ( ; substr($lines[$lineno], 0, 2) eq ' '; $lineno++ ) {
202             for ( ; $lines[$lineno] ne ''; $lineno++ ) {
203             if ( my ($file) =
204             ( $lines[$lineno] =~ /^ (.{40,}) - (OK)$/) ) {
205             $file =~ s! +$!!;
206             # push @volumes, $file;
207             } elsif ( $lines[$lineno] =~ /^(.*)$/ ) {
208             } else {
209             Log(CHAN_DEBUG, LOG_INFO, "Ignoring line: $lines[$lineno]");
210             }
211             }
212              
213             croak "Bad format on par line $lineno: $lines[$lineno]\n"
214             unless $lines[$lineno] eq '';
215             $lineno++;
216              
217             if ( $lines[$lineno] eq 'Restorable:' ) {
218             $lineno++;
219             while ( $lineno <= $#lines and
220             my ($file) =
221             ($lines[$lineno] =~ /^ (.{40,}) - (can be restored)$/) ) {
222             $file =~ s! +$!!;
223             $status{$file} |= FILE_RESTORABLE;
224             $lineno++;
225             }
226             } elsif ( $lines[$lineno] eq 'Too many missing files:' ) {
227             $lineno++;
228             while ( $lineno <= $#lines and
229             my ($file) =
230             ($lines[$lineno] =~ /^ (.*)$/) ) {
231             $file =~ s! +$!!;
232             $lineno++;
233             }
234             } elsif ( $lines[$lineno] eq 'Restoring:' ) {
235             $lineno++;
236             $lineno++
237             if $lines[$lineno] eq '0%100%';
238             RECOVER_LINE:
239             while ( $lineno <= $#lines ) {
240             if ( my ($file, $status) =
241             ($lines[$lineno] =~ /^ (.{40,}) - (RECOVERED)$/) ) {
242             $file =~ s! +$!!;
243             $status{$file} = FILE_FOUND | FILE_OK
244             if $status eq 'RECOVERED';
245             } elsif ( $lines[$lineno] eq '0%100%' ) {
246             # Ignore
247             } elsif ( my ($from, $to) =
248             ($lines[$lineno] =~ /^ Rename: (.*) -> (.*)$/) ) {
249             if ( $status{$from} & FILE_CORRUPT ) {
250             # If file is corrupt, we're moving it to make way
251             $status{$from} = FILE_RESTORABLE;
252             push @bad_old_files, $to;
253             } else { # Else file is not corrupt; why are we moving it?
254             croak
255             ("Par is moving file $from to $to; I do not understand why..." .
256             "\n$lines[$lineno]\n");
257             }
258             } else {
259             last RECOVER_LINE;
260             }
261             } continue {
262             $lineno++;
263             }
264             } else {
265             croak "Bad restorable format on par line $lineno: $lines[$lineno]\n";
266             }
267             } else {
268             croak "Bad end format on par line $lineno: $lines[$lineno]\n"
269             unless $lines[$lineno] eq 'All files found';
270             }
271              
272             croak sprintf("Junk after end of par:\n%s\n",
273             join("\n", @lines[$lineno+1..$#lines]))
274             unless $lineno >= $#lines;
275              
276             return \%status, \%file_name, \@bad_old_files;
277             }
278              
279             # -------------------------------------
280             # CLASS HIGHER-LEVEL PROCEDURES
281             # -------------------------------------
282              
283             =head1 CLASS HIGHER-LEVEL PROCEDURES
284              
285             Z<>
286              
287             =cut
288              
289             # INSTANCE METHODS -----------------------------------------------------------
290              
291             # -------------------------------------
292             # INSTANCE CONSTRUCTION
293             # -------------------------------------
294              
295             =head1 INSTANCE CONSTRUCTION
296              
297             Z<>
298              
299             =cut
300              
301             =head2 new
302              
303             Create & return a new thing.
304              
305             =cut
306              
307             Class::MethodMaker->import (new_with_init => 'new',
308             new_hash_init => 'hash_init',);
309              
310             sub init {
311             my $self = shift;
312             my ($fn) = @_;
313              
314             $self->hash_init (fn => $fn);
315             }
316              
317             # -------------------------------------
318             # INSTANCE FINALIZATION
319             # -------------------------------------
320              
321             # -------------------------------------
322             # INSTANCE COMPONENTS
323             # -------------------------------------
324              
325             =head1 INSTANCE COMPONENTS
326              
327             Z<>
328              
329             =cut
330              
331             Class::MethodMaker->import
332             (
333             get_set => [qw/ fn /],
334             # status is a map from filename to a bitmask.
335             hash => [qw/ status _file_name /],
336             boolean => [qw/ _checked /],
337             );
338              
339              
340             # -------------------------------------
341             # INSTANCE HIGHER-LEVEL FUNCTIONS
342             # -------------------------------------
343              
344             =head1 INSTANCE HIGHER-LEVEL FUNCTIONS
345              
346             Z<>
347              
348             =cut
349              
350             =head2 files
351              
352             =over 4
353              
354             =item PRECONDITION
355              
356             $self->checked
357              
358             =item ARGUMENTS
359              
360             I
361              
362             =item RETURNS
363              
364             =over 4
365              
366             =item files
367              
368             List of files known by par, by their names as par believes they should be.
369              
370             =back
371              
372             =back
373              
374             =cut
375              
376             sub files {
377             my $self = shift;
378             return $self->status_keys;
379             }
380              
381             # -------------------------------------
382              
383             =head2 files
384              
385             =over 4
386              
387             =item PRECONDITION
388              
389             $self->checked
390              
391             =item ARGUMENTS
392              
393             I
394              
395             =item RETURNS
396              
397             =over 4
398              
399             =item files
400              
401             List of files known by par, by their names as found on the filesystem. Files
402             not found are not included in the list. File names are prefixed by the
403             directory portion of the par filename, so -e should work.
404              
405             =back
406              
407             =back
408              
409             =cut
410              
411             sub fs_files {
412             my $self = shift;
413              
414             my $par_dir = dirname($self->fn);
415              
416             return
417             map catfile($par_dir, $_),
418             map(($self->file_moved($_) || $_), grep ($self->status($_) & FILE_FOUND,
419             $self->files));
420             }
421              
422             # -------------------------------------
423              
424             =head2 file_known
425              
426             =over 4
427              
428             =item PRECONDITION
429              
430             $self->checked
431              
432             =item ARGUMENTS
433              
434             =over 4
435              
436             =item fn
437              
438             Name of file to look up. This is the name as expected by par, not any
439             suitable substitute found by par.
440              
441             =back
442              
443             =item RETURNS
444              
445             =over 4
446              
447             =item known
448              
449             Whether this file name is known by par.
450              
451             =back
452              
453             =back
454              
455             =cut
456              
457             sub file_known {
458             my $self = shift;
459             my ($fn) = @_;
460              
461             return $self->status_exists($fn);
462             }
463              
464             # -------------------------------------
465              
466             =head2 file_found
467              
468             =over 4
469              
470             =item PRECONDITION
471              
472             $self->file_known($fn)
473              
474             =item ARGUMENTS
475              
476             =over 4
477              
478             =item fn
479              
480             Name of file to look up. This is the name as expected by par, not any
481             suitable substitute found by par.
482              
483             =back
484              
485             =item RETURNS
486              
487             =over 4
488              
489             =item found
490              
491             Whether this file name is found by par.
492              
493             =back
494              
495             =back
496              
497             =cut
498              
499             sub file_found {
500             my $self = shift;
501             my ($fn) = @_;
502              
503             return $self->status($fn) & FILE_FOUND;
504             }
505              
506             # -------------------------------------
507              
508             =head2 file_restorable
509              
510             =over 4
511              
512             =item PRECONDITION
513              
514             $self->file_known($fn)
515              
516             =item ARGUMENTS
517              
518             =over 4
519              
520             =item fn
521              
522             Name of file to look up. This is the name as expected by par, not any
523             suitable substitute found by par.
524              
525             =back
526              
527             =item RETURNS
528              
529             =over 4
530              
531             =item found
532              
533             Whether this file name is thought by par to be restorable.
534              
535             =back
536              
537             =back
538              
539             =cut
540              
541             sub file_restorable {
542             my $self = shift;
543             my ($fn) = @_;
544              
545             return $self->status($fn) & FILE_RESTORABLE;
546             }
547              
548             # -------------------------------------
549              
550             =head2 file_moved
551              
552             =over 4
553              
554             =item PRECONDITION
555              
556             $self->file_known($fn)
557              
558             =item ARGUMENTS
559              
560             =over 4
561              
562             =item fn
563              
564             Name of file to look up. This is the name as expected by par, not any
565             suitable substitute found by par.
566              
567             =back
568              
569             =item RETURNS
570              
571             =over 4
572              
573             =item found
574              
575             The name this file has apparently moved to as per par; undef if the file has
576             not moved.
577              
578             =back
579              
580             =back
581              
582             =cut
583              
584             sub file_moved {
585             my $self = shift;
586             my ($fn) = @_;
587              
588             return $self->_file_name($fn);
589             }
590              
591             # -------------------------------------
592              
593             =head2 file_ok
594              
595             =over 4
596              
597             =item PRECONDITION
598              
599             $self->file_known($fn)
600              
601             =item ARGUMENTS
602              
603             =over 4
604              
605             =item fn
606              
607             Name of file to look up. This is the name as expected by par, not any
608             suitable substitute found by par.
609              
610             =back
611              
612             =item RETURNS
613              
614             =over 4
615              
616             =item found
617              
618             Whether this file name is thought by par to be in tip-top condition.
619              
620             =back
621              
622             =back
623              
624             =cut
625              
626             sub file_ok {
627             my $self = shift;
628             my ($fn) = @_;
629              
630             return $self->status($fn) & FILE_OK;
631             }
632              
633             # -------------------------------------
634              
635             =head2 file_corrupt
636              
637             =over 4
638              
639             =item PRECONDITION
640              
641             $self->file_known($fn)
642              
643             =item ARGUMENTS
644              
645             =over 4
646              
647             =item fn
648              
649             Name of file to look up. This is the name as expected by par, not any
650             suitable substitute found by par.
651              
652             =back
653              
654             =item RETURNS
655              
656             =over 4
657              
658             =item found
659              
660             Whether this file name is thought by par to be corrupt
661              
662             =back
663              
664             =back
665              
666             =cut
667              
668             sub file_corrupt {
669             my $self = shift;
670             my ($fn) = @_;
671              
672             return $self->status($fn) & FILE_CORRUPT;
673             }
674              
675             # -------------------------------------
676              
677             =head2 file_recoverable
678              
679             =over 4
680              
681             =item PRECONDITION
682              
683             ! $self->file_ok($fn)
684              
685             =item ARGUMENTS
686              
687             =over 4
688              
689             =item fn
690              
691             =back
692              
693             =item RETURNS
694              
695             Whether the file may be regenerated somehow
696              
697             =back
698              
699             =cut
700              
701             sub file_recoverable {
702             my $self = shift;
703             my ($fn) = @_;
704              
705             return $self->file_moved($fn) || $self->file_restorable($fn);
706             }
707              
708             # -------------------------------------
709              
710             =head2 recoverable
711              
712             =over 4
713              
714             =item PRECONDITIONS
715              
716             $self->checked
717              
718             ! $self->ok
719              
720             =item ARGUMENTS
721              
722             I
723              
724             =item RETURNS
725              
726             =over 4
727              
728             =item recoverable
729              
730             true if the files can be recovered, false if not
731              
732             =back
733              
734             =back
735              
736             =cut
737              
738             sub recoverable {
739             my $self = shift;
740              
741             croak sprintf("PRECONDITION on %s:%s: failed; not checked\n",
742             (caller(0))[0,3])
743             unless $self->checked;
744             croak sprintf("PRECONDITION on %s:%s: failed; par ok\n",
745             (caller(0))[0,3])
746             if $self->ok;
747              
748             grep(! ($self->file_ok($_) || $self->file_recoverable($_)),
749             $self->status_keys) == 0
750             }
751              
752             # -------------------------------------
753              
754             =head2 dump_file_status
755              
756             Convenience method for returning status of files in par.
757              
758             =cut
759              
760             sub dump_file_status {
761             my $self = shift;
762              
763             for my $fn ($self->status_keys) {
764             my $status = $self->status($fn);
765             my @flags;
766             for my $flag (sort grep(substr($_, 0, 5) eq 'FILE_',
767             keys %{*Archive::Par::})) {
768             no strict 'refs';
769             my $val = &$flag();
770             push @flags, substr($flag, 5)
771             if $status & $val;
772             }
773             printf STDERR "FILE:%-20s: (S%2d); %s\n", $fn, $status, join ' ', @flags;
774             if ( $self->_file_name_exists($fn) ) {
775             printf STDERR " (found as %s)\n", $self->_file_name($fn);
776             }
777             }
778             }
779              
780             # -------------------------------------
781              
782             =head2 checked
783              
784             =over 4
785              
786             =item ARGUMENTS
787              
788             I
789              
790             =item RETURNS
791              
792             =over 4
793              
794             =item checked
795              
796             Whether the status flags for this instance are meaningful.
797              
798             =back
799              
800             =back
801              
802             =cut
803              
804             sub checked { $_[0]->_checked }
805              
806             # -------------------------------------
807              
808             =head2 ok
809              
810             =over 4
811              
812             =item PRECONDITIONS
813              
814             $self->checked
815              
816             =item ARGUMENTS
817              
818             I
819              
820             =item RETURNS
821              
822             =over 4
823              
824             =item ok
825              
826             True if there are no fixes for par to make.
827              
828             =back
829              
830             =back
831              
832             =cut
833              
834             sub ok { grep(($_ & FILE_OK) == 0, $_[0]->status_values) == 0 }
835              
836             # -------------------------------------
837             # INSTANCE HIGHER-LEVEL PROCEDURES
838             # -------------------------------------
839              
840             =head1 INSTANCE HIGHER-LEVEL PROCEDURES
841              
842             Z<>
843              
844             =cut
845              
846             sub check {
847             my $self = shift; my $class = ref $self;
848              
849             my $out;
850             # OK, there is (possibly) some arguments. A filename forces that file to be
851             # used for the unrar command. A filehandle argument reads from that
852             # filehandle to parse, rather than invoking unrar. If the filehandle isn't
853             # a ref, it's treated purely as a text string. This is for testing.
854              
855             my ($fn, $fh) = @_;
856             if ( defined $fh ) {
857             if ( ref $fh ) {
858             local $/ = undef;
859             $out = <$fh>;
860             } else {
861             $out = $fh;
862             }
863             } else {
864             $fn = $self->fn
865             unless defined $fn;
866             run([par => 'check', $fn], '&>', \$out);
867             }
868              
869             my ($status, $file_name) = $class->_parse_par_output($out, $fn);
870             $self->status_clear;
871             $self->_file_name_clear;
872             $self->status($status);
873             $self->_file_name($file_name);
874             $self->_checked(1);
875             }
876              
877             # -------------------------------------
878              
879             =head2 restore
880              
881             =over 4
882              
883             =item PRECONDITIONS
884              
885             $self->recoverable
886              
887             =item ARGUMENTS
888              
889             =over 4
890              
891             =item remove_old_files
892              
893             I If true, remove (corrupt) old files created by the restore.
894              
895             =back
896              
897             =back
898              
899             =cut
900              
901             sub restore {
902             my $self = shift; my $class = ref $self;
903             my ($remove_old_files) = @_;
904              
905             croak sprintf("PRECONDITION on %s:%s: failed; not recoverable\n",
906             (caller(0))[0,3])
907             unless $self->recoverable;
908              
909             my $fn = $self->fn;
910             my $out;
911             run([qw( par -m -f restore), $fn], '&>', \$out);
912              
913             my ($status, $file_name, $old_files) = $class->_parse_par_output($out, $fn);
914             $self->status_clear;
915             $self->_file_name_clear;
916             $self->status($status);
917             $self->_file_name($file_name);
918             if ( $remove_old_files ) {
919             for ( @$old_files ) {
920             my $target = catfile(dirname($self->fn), $_);
921             unlink $target
922             or croak "Failed to remove corrupt old file: $target: $!\n";
923             }
924             }
925             $self->_checked(1);
926             }
927              
928             # ----------------------------------------------------------------------------
929              
930             =head1 EXAMPLES
931              
932             Z<>
933              
934             =head1 BUGS
935              
936             Z<>
937              
938             =head1 REPORTING BUGS
939              
940             Email the author.
941              
942             =head1 AUTHOR
943              
944             Martyn J. Pearce C
945              
946             =head1 COPYRIGHT
947              
948             Copyright (c) 2002 Martyn J. Pearce. This program is free software; you can
949             redistribute it and/or modify it under the same terms as Perl itself.
950              
951             =head1 SEE ALSO
952              
953             Z<>
954              
955             =cut
956              
957             1; # keep require happy.
958              
959             __END__