File Coverage

blib/lib/File/Properties/Regular.pm
Criterion Covered Total %
statement 96 129 74.4
branch 20 48 41.6
condition 2 6 33.3
subroutine 17 19 89.4
pod 5 5 100.0
total 140 207 67.6


line stmt bran cond sub pod time code
1             # ----------------------------------------------------------------------------
2             #
3             # This module supports computing and caching of file SHA-2 digests and
4             # mime types.
5             #
6             # Copyright © 2010,2011 Brendt Wohlberg
7             # See distribution LICENSE file for license details.
8             #
9             # Most recent modification: 18 December 2011
10             #
11             # ----------------------------------------------------------------------------
12              
13             package File::Properties::Regular;
14             our $VERSION = 0.02;
15              
16 4     4   1991 use File::Properties::Error;
  4         9  
  4         35  
17 4     4   2830 use File::Properties::Generic;
  4         12  
  4         155  
18 4     4   26 use base qw(File::Properties::Generic);
  4         8  
  4         466  
19              
20             require 5.005;
21 4     4   24 use strict;
  4         8  
  4         167  
22 4     4   22 use warnings;
  4         8  
  4         141  
23 4     4   33 use Error qw(:try);
  4         7  
  4         37  
24 4     4   8102 use File::Type; # Perhaps use File::MMagic::XS, File::MimeInfo
  4         160475  
  4         332  
25 4     4   5767 use Digest::SHA;
  4         20092  
  4         18983  
26              
27             our $SHADigestType = 512;
28             our $CacheTableName = 'RegFileCache';
29              
30              
31             # ----------------------------------------------------------------------------
32             # Initialiser
33             # ----------------------------------------------------------------------------
34             sub _init {
35 6     6   15 my $self = shift;
36 6         17 my $path = shift; # File path
37 6         12 my $fpcr = shift; # File::Properties::Cache reference
38              
39             # Call base class _init method
40 6         45 $self->SUPER::_init($path, $fpcr);
41             ## Remainder of initialisation only necessary for regular files (in
42             ## particular, it should not be performed for directories)
43 6 50       37 if ($self->isreg) {
44             # Set flag indicating whether file path is cached
45 6 50       55 my $cptf = defined $fpcr?
46             $fpcr->cproperties($CacheTableName, 'CachedPath'):1;
47             # Initialisation for _fromcache method
48 6         35 $self->{'rfcf'} = {};
49 6         10 my $cent;
50             ## If File::Properties::Cache reference specified and the cache
51             ## contains an entry matching the base Generic object, set the mime
52             ## type and file digest from the cache entry, otherwise compute
53             ## these values.
54 6 100 66     53 if (defined $fpcr and ($cent = $fpcr->cretrieve($CacheTableName,
55             $self->cachekey($cptf)))) {
56 3         23 $self->mimetype($cent->{'MimeType'});
57 3         16 $self->digest($cent->{'Digest'});
58             # Set flag indicating that this entry was obtained from the cache
59 3         14 $self->_fromcache($CacheTableName, 1);
60             } else {
61 3         19 $self->mimetype(_mimetype($self->path));
62 3         13 $self->digest(_digest($self->path));
63             # Set flag indicating that this entry was not obtained from the cache
64 3         24 $self->_fromcache($CacheTableName, 0);
65             ## If a File::Properties::Cache reference is specified, record the
66             ## mime type and digest in the cache
67 3 50       13 if (defined $fpcr) {
68 3         21 my $row = $self->cachekey($cptf);
69 3         16 $row->{'MimeType'} = $self->mimetype;
70 3         12 $row->{'Digest'} = $self->digest;
71 3         32 $fpcr->cinsert($CacheTableName, $row);
72             }
73             }
74             }
75             }
76              
77              
78             # ----------------------------------------------------------------------------
79             # Get (or set) file mime type
80             # ----------------------------------------------------------------------------
81             sub mimetype {
82 16     16 1 2587 my $self = shift;
83              
84 16 100       59 $self->{'fmtp'} = shift if (@_);
85 16         246 return $self->{'fmtp'};
86             }
87              
88              
89             # ----------------------------------------------------------------------------
90             # Get (or set) file digest
91             # ----------------------------------------------------------------------------
92             sub digest {
93 16     16 1 138 my $self = shift;
94              
95 16 100       114 $self->{'fdgs'} = shift if (@_);
96 16         87 return $self->{'fdgs'};
97             }
98              
99              
100             # ----------------------------------------------------------------------------
101             # Construct string representing properties hash
102             # ----------------------------------------------------------------------------
103             sub string {
104 0     0 1 0 my $self = shift;
105 0         0 my $levl = shift;
106              
107 0 0       0 $levl = 0 if (!defined $levl);
108 0         0 my $lpfx = ' ' x (2*$levl);
109 0         0 my $s = $self->SUPER::string($levl);
110              
111 0 0       0 if (not $self->isdir) {
112 0         0 $s .= $lpfx . " File Mime Type: ".$self->mimetype."\n";
113 0         0 $s .= $lpfx . " File Digest: ".substr($self->digest,0,40)."...\n";
114             }
115              
116 0         0 return $s;
117             }
118              
119              
120             # ----------------------------------------------------------------------------
121             # Compute cache key array for this object
122             # ----------------------------------------------------------------------------
123             sub cachekey {
124 9     9 1 20 my $self = shift;
125 9         18 my $cptf = shift; # Cache path flag
126              
127 9         47 my $key = {'Size' => $self->size, 'ModTime' => $self->mtime};
128             # Insert path into key data if path is cached
129 9 50       60 $key->{'Path'} = $self->path if ($cptf);
130             # Insert device and inode numbers into key data if stat is fully supported
131 9 50       34 if ($File::Properties::Generic::FullStatSupport) {
132 9         49 $key->{'Device'} = $self->device;
133 9         37 $key->{'Inode'} = $self->inode;
134             }
135 9         41 return $key;
136             }
137              
138              
139             # ----------------------------------------------------------------------------
140             # Create initialised cache object
141             # ----------------------------------------------------------------------------
142             sub cache {
143 2     2 1 1992 my $self = shift;
144 2         5 my $dbfp = shift; # Database file path
145 2         5 my $opts = shift; # Options hash
146              
147             # Construct a File::Properties::Cache object attached to the
148             # specified database file
149 2         36 my $fpcr = File::Properties::Cache->new($dbfp, $opts);
150             # Initialise the cache table for this class
151 2         21 $self->_cacheinit($fpcr, $opts);
152 2         30 return $fpcr;
153             }
154              
155              
156             # ----------------------------------------------------------------------------
157             # Initialise cache table for File::Properties::Regular data
158             # ----------------------------------------------------------------------------
159             sub _cacheinit {
160 2     2   7 my $self = shift;
161 2         7 my $fpcr = shift; # File::Properties::Cache reference
162 2         5 my $opts = shift; # Options hash
163              
164             ## Flag determining caching of file path
165 2         7 my $cptf = 1;
166 2 50       15 $cptf = $opts->{'CachedPath'} if (defined $opts->{'CachedPath'});
167 2 50       10 $cptf = 1 if (not $File::Properties::Generic::FullStatSupport);
168             ## If cache table exists, check that it is compatible with current
169             ## options, otherwise initialise it according to options
170 2 50       21 if ($fpcr->tableexists($CacheTableName)) {
171             # Get array of cache table column names
172 0         0 my $cnam = $fpcr->columns($CacheTableName);
173             # Determine whether cache table column names include 'Path'
174 0         0 my $ecpt = File::Properties::Cache::_inarray($cnam, 'Path');
175             # Throw error if file path caching is requested in options but not
176             # configured in cache database
177 0 0 0     0 throw File::Properties::Error("Path caching requested but is not " .
178             "configured in existing cache database")
179             if ($cptf and not $ecpt);
180             # Set file path caching flag if configured in cache database
181 0 0       0 $cptf = 1 if ($ecpt);
182             # Determine whether cache table column names include 'Device' and 'Inode'
183 0 0       0 my $ecsf = File::Properties::Cache::_inarray($cnam, 'Device') and
184             File::Properties::Cache::_inarray($cnam, 'Inode');
185             # Throw error if File::Properties::Generic::FullStatSupport flag
186             # value does not match flag indicating inclusion of 'Device' and
187             # 'Inode' entries in cache table
188 0 0       0 throw File::Properties::Error("Mismatch between stat support in current ".
189             "architecture and existing database")
190             if (!!$File::Properties::Generic::FullStatSupport != !!$ecsf);
191             }
192 2         7 my $ctbk = []; # Cache table keys
193 2         7 my $ctbc = []; # Cache table columns
194             ## Construct array describing cache table definition
195 2         5 my $cols = [];
196 2 50       13 push @$cols, 'Path TEXT' if ($cptf);
197 2 50       12 push @$cols, ('Device INTEGER','Inode INTEGER')
198             if ($File::Properties::Generic::FullStatSupport);
199 2         8 push @$cols,('Size INTEGER','ModTime INTEGER');
200             # Construct cache table keys array by removing type specifications
201             # from a copy of $cols array
202 2         8 $ctbk = [map { /^[^\s]+/; $& } @$cols];
  10         54  
  10         33  
203             ## Construct final cache table specification array by appending
204             ## column definitions for MimeType and Digest to copy of $cols array
205 2         10 $ctbc = [@$cols];
206 2         9 push @$ctbc, ('MimeType TEXT','Digest TEXT');
207             # Define cache table
208 2         47 $fpcr->define($CacheTableName, $ctbc, {'IncludeInsertDate' => 1,
209             'TableVersion' => [__PACKAGE__.'::Version', $VERSION]});
210             # Make temporary record of path caching flag
211 2         37 $fpcr->cproperties($CacheTableName, 'CachedPath', $cptf);
212             }
213              
214              
215             # ----------------------------------------------------------------------------
216             # Clear invalid entries in cache table for File::Properties::Regular data
217             # ----------------------------------------------------------------------------
218             sub _cacheclean {
219 0     0   0 my $self = shift;
220 0         0 my $fpcr = shift; # File::Properties::Cache reference
221              
222             # This operation is only possible if file paths are included in the
223             # cache
224 0 0       0 if ($fpcr->cproperties($CacheTableName, 'CachedPath')) {
225             # Initialise array of paths found not to exist
226 0         0 my $clst = [];
227             # Set the number of database rows retrieved in each database access
228 0         0 my $nblk = 100;
229             # Get the total number of rows in the database
230 0         0 my $nrow = $fpcr->numrows($CacheTableName);
231 0         0 my ($k, $l);
232             ## Iterate over all database rows, accessing them in blocks of
233             ## $nblk rows at a time. Within each block of rows, push any paths
234             ## that no longer exist onto the array of paths to be cleared from
235             ## the cache.
236 0         0 for ($k = 0; $k < $nrow / $nblk; $k++) {
237             # Compute the number of rows in the current block
238 0 0       0 my $lmax = (int($nrow / $nblk) == $k)?($nrow % $nblk):$nblk;
239             # Compute the row index of the first row in the current block
240 0         0 my $ridx = $k * $nblk;
241             # Retrieve the current block of rows from the database
242 0         0 my $rdat = $fpcr->retrieve($CacheTableName,
243             {'ReturnType' => 'Array',
244             'Suffix' => "LIMIT $ridx, $lmax"});
245             ## Push any paths that no longer exist onto the clean list
246 0         0 for ($l = 0; $l < $lmax; $l++) {
247 0 0       0 push @$clst, $rdat->[$l]->[0] if (! -f $rdat->[$l]->[0]);
248             }
249             }
250 0         0 my $pth;
251             ## Iterate over array of non-existent paths and remove the
252             ## corresponding cache entry
253 0         0 foreach $pth (@$clst) {
254 0         0 $fpcr->remove($CacheTableName, {'Where' => {'Path' => $pth}});
255             }
256 0         0 return 1;
257             } else {
258 0         0 return 0;
259             }
260             }
261              
262              
263             # ----------------------------------------------------------------------------
264             # Get or set flag indicating whether data was retrieved from the cache
265             # ----------------------------------------------------------------------------
266             sub _fromcache {
267 14     14   956 my $self = shift;
268              
269 14         33 my $mkey = shift;
270 14 100       746 $mkey = eval('$' . ref($self) . "::CacheTableName") if not defined $mkey;
271 14 100       557 $self->{'rfcf'}->{$mkey} = shift if (@_);
272 14         69 return $self->{'rfcf'}->{$mkey};
273             }
274              
275              
276             # ----------------------------------------------------------------------------
277             # Compute mime type of file specified by path
278             # ----------------------------------------------------------------------------
279             sub _mimetype {
280 4     4   26 my $path = shift; # File path
281              
282 4         44 my $ft = File::Type->new();
283 4         62 return $ft->checktype_filename($path);
284             }
285              
286              
287             # ----------------------------------------------------------------------------
288             # Compute digest of file specified by path
289             # ----------------------------------------------------------------------------
290             sub _digest {
291 6     6   3634842 my $path = shift; # File path
292              
293 6         90 my $sha = Digest::SHA->new($SHADigestType);
294 6         234 $sha->addfile($path, 'b');
295 6         172253 return $sha->hexdigest;
296             }
297              
298              
299             # ----------------------------------------------------------------------------
300             # End of method definitions
301             # ----------------------------------------------------------------------------
302              
303              
304             1;
305             __END__