File Coverage

blib/lib/Brackup/File.pm
Criterion Covered Total %
statement 138 151 91.3
branch 33 56 58.9
condition 10 18 55.5
subroutine 31 33 93.9
pod 0 21 0.0
total 212 279 75.9


line stmt bran cond sub pod time code
1             package Brackup::File;
2             # "everything is a file"
3             # ... this class includes symlinks and directories
4              
5 13     13   470 use strict;
  13         33  
  13         569  
6 13     13   79 use warnings;
  13         26  
  13         413  
7 13     13   132 use Carp qw(croak);
  13         34  
  13         675  
8 13     13   676459 use File::stat ();
  13         518320  
  13         287  
9 13     13   68 use Fcntl qw(S_ISREG S_ISDIR S_ISLNK S_ISFIFO O_RDONLY);
  13         26  
  13         980  
10 13     13   923 use Digest::SHA1;
  13         1679  
  13         541  
11 13     13   42038 use String::Escape qw(printable);
  13         128278  
  13         1561  
12 13     13   10116 use Brackup::PositionedChunk;
  13         54  
  13         529  
13 13     13   19952 use Brackup::Chunker::Default;
  13         37  
  13         376  
14 13     13   9049 use Brackup::Chunker::MP3;
  13         35  
  13         30075  
15              
16             sub new {
17 117     117 0 446 my ($class, %opts) = @_;
18 117         716 my $self = bless {}, $class;
19              
20 117         8270 $self->{root} = delete $opts{root};
21 117         690 $self->{path} = delete $opts{path};
22 117         244 $self->{stat} = delete $opts{stat}; # File::stat object
23 117 50       371 croak("Unknown options: " . join(', ', keys %opts)) if %opts;
24              
25 117 50 33     2346 die "No root object provided." unless $self->{root} && $self->{root}->isa("Brackup::Root");
26 117 50       290 die "No path provided." unless defined($self->{path}); # note: permit "0"
27 117         245 $self->{path} =~ s!^\./!!;
28              
29 117         389 return $self;
30             }
31              
32             sub root {
33 797     797 0 1799 my $self = shift;
34 797         7392 return $self->{root};
35             }
36              
37             # returns File::stat object
38             sub stat {
39 2316     2316 0 3633 my $self = shift;
40 2316 100       115003 return $self->{stat} if $self->{stat};
41 8         149 my $path = $self->fullpath;
42 8         84 my $stat = File::stat::lstat($path);
43 8         2584 return $self->{stat} = $stat;
44             }
45              
46             sub size {
47 477     477 0 1182 my $self = shift;
48 477         1332 return $self->stat->size;
49             }
50              
51             sub is_dir {
52 150     150 0 405 my $self = shift;
53 150         1743 return S_ISDIR($self->stat->mode);
54             }
55              
56             sub is_link {
57 130     130 0 236 my $self = shift;
58 130         231 my $result = eval { S_ISLNK($self->stat->mode) };
  130         315  
59 130 50       1582 $result = -l $self->fullpath unless defined($result);
60 130         714 return $result;
61             }
62              
63             sub is_file {
64 611     611 0 1094 my $self = shift;
65 611         2991 return S_ISREG($self->stat->mode);
66             }
67              
68             sub is_fifo {
69 0     0 0 0 my $self = shift;
70 0         0 return S_ISFIFO($self->stat->mode);
71             }
72              
73             # Returns file type like find's -type
74             sub type {
75 301     301 0 649 my $self = shift;
76 301 100       1467 return "f" if $self->is_file;
77 40 50       671 return "d" if $self->is_dir;
78 0 0       0 return "l" if $self->is_link;
79 0 0       0 return "p" if $self->is_fifo;
80 0         0 return "";
81             }
82              
83             sub fullpath {
84 143     143 0 265 my $self = shift;
85 143         1820 return $self->{root}->path . "/" . $self->{path};
86             }
87              
88             # a scalar that hopefully uniquely represents a single version of a file in time.
89             sub cachekey {
90 178     178 0 493 my $self = shift;
91 178         645 my $st = $self->stat;
92 178         1454 return "[" . $self->{root}->name . "]" . $self->{path} . ":" . join(",", $st->ctime, $st->mtime, $st->size, $st->ino);
93             }
94              
95             # Returns the appropriate FileChunker class for the provided file's
96             # type. In most cases this FileChunker will be very dumb, just making
97             # equal-sized chunks for, say, 5MB, but in specialized cases (like mp3
98             # files), the chunks will be one (or two) small ones for the ID3v1/v2
99             # chunks, and one big chunk for the audio bytes (which might be cut
100             # into its own small chunks). This way the mp3 metadata can be
101             # changed without needing to back up the entire file again ... just
102             # the changed metadata.
103             sub file_chunker {
104 90     90 0 164 my $self = shift;
105 90 50 33     1278 if ($self->{path} =~ /\.mp3$/i && $self->{root}->smart_mp3_chunking) {
106 0         0 return "Brackup::Chunker::MP3";
107             }
108 90         2179 return "Brackup::Chunker::Default";
109             }
110              
111             sub chunks {
112 396     396 0 1106 my $self = shift;
113             # memoized:
114 396 100       1890 return @{ $self->{chunks} } if $self->{chunks};
  286         2022  
115              
116             # non-files don't have chunks
117 110 100       1082 if (!$self->is_file) {
118 20         325 $self->{chunks} = [];
119 20         85 return ();
120             }
121              
122             # Get the appropriate FileChunker for this file type,
123             # then pass ourselves to it to get our chunks.
124 90         2879 my @chunk_list = $self->file_chunker->chunks($self);
125              
126 90         5816 $self->{chunks} = \@chunk_list;
127 90         770 return @chunk_list;
128             }
129              
130             sub full_digest {
131 167     167 0 413 my $self = shift;
132 167   66     1904 return $self->{_full_digest} ||= $self->_calc_full_digest;
133             }
134              
135             sub _calc_full_digest {
136 90     90   166 my $self = shift;
137 90 50       255 return "" unless $self->is_file;
138              
139 90         9084 my $cache = $self->{root}->digest_cache;
140 90         394 my $key = $self->cachekey;
141              
142 90         11045 my $dig = $cache->get($key);
143 90 100       708 return $dig if $dig;
144              
145             # legacy migration thing... we used to more often store
146             # the chunk digests, not the file digests. so try that
147             # first...
148 69 100       571 if ($self->chunks == 1) {
149 57         663 my ($chunk) = $self->chunks;
150 57         499 $dig = $cache->get($chunk->cachekey);
151             }
152              
153 69 50       682 unless ($dig) {
154 69         1666 my $sha1 = Digest::SHA1->new;
155 69         278 my $path = $self->fullpath;
156 69 50       9563 sysopen(my $fh, $path, O_RDONLY) or die "Failed to open $path: $!";
157 69         766 binmode($fh);
158 69         5743 $sha1->addfile($fh);
159 69         962 close($fh);
160              
161 69         1609 $dig = "sha1:" . $sha1->hexdigest;
162             }
163              
164 69         1056 $cache->set($key => $dig);
165 69         1987 return $dig;
166             }
167              
168             sub link_target {
169 0     0 0 0 my $self = shift;
170 0 0       0 return $self->{linktarget} if $self->{linktarget};
171 0 0       0 return undef unless $self->is_link;
172 0         0 return $self->{linktarget} = readlink($self->fullpath);
173             }
174              
175             sub path {
176 196     196 0 409 my $self = shift;
177 196         4830 return $self->{path};
178             }
179              
180             sub as_string {
181 81     81 0 151 my $self = shift;
182 81         502 my $type = $self->type;
183 81         2643 return "[" . $self->{root}->as_string . "] t=$type $self->{path}";
184             }
185              
186             sub mode {
187 220     220 0 5078 my $self = shift;
188 220         508 return sprintf('%#o', $self->stat->mode & 0777);
189             }
190              
191             sub uid {
192 220     220 0 1048 my $self = shift;
193 220         476 return $self->stat->uid;
194             }
195              
196             sub gid {
197 220     220 0 428 my $self = shift;
198 220         655 return $self->stat->gid;
199             }
200              
201             sub as_rfc822 {
202 110     110 0 245 my ($self, $schunk_list, $backup) = @_;
203 110         5907 my $ret = "";
204             my $set = sub {
205 648     648   5159 my ($key, $val) = @_;
206 648 100       2434 return unless length $val;
207 628         2279 $ret .= "$key: $val\n";
208 110         1075 };
209 110         818 my $st = $self->stat;
210              
211 110         1043 $set->("Path", printable($self->{path}));
212 110         755 my $type = $self->type;
213 110 100       2089 if ($self->is_file) {
214 90         1856 my $size = $self->size;
215 90         892 $set->("Size", $size);
216 90 50       448 $set->("Digest", $self->full_digest) if $size;
217             } else {
218 20         205 $set->("Type", $type);
219 20 50       116 if ($self->is_link) {
220 0         0 $set->("Link", $self->link_target);
221             }
222             }
223 110         823 $set->("Chunks", join("\n ", map { $_->to_meta } @$schunk_list));
  108         1026  
224              
225 110 50       14234 unless ($self->is_link) {
226 110         4200 $set->("Mtime", $st->mtime);
227 110 50       391 $set->("Atime", $st->atime) unless $self->root->noatime;
228              
229 110         502 my $mode = $self->mode;
230 110 100 66     4286 unless (($type eq "d" && $mode eq $backup->default_directory_mode) ||
      66        
      66        
231             ($type eq "f" && $mode eq $backup->default_file_mode)) {
232 8         43 $set->("Mode", $mode);
233             }
234             }
235              
236 110         7886 my $uid = $self->uid;
237 110 50       2009 unless ($uid eq $backup->default_uid) {
238 0         0 $set->("UID", $uid);
239             }
240 110         385 my $gid = $self->gid;
241 110 50       2008 unless ($gid eq $backup->default_gid) {
242 0         0 $set->("GID", $gid);
243             }
244              
245 110         1439 return $ret . "\n";
246             }
247              
248             1;