File Coverage

blib/lib/Tie/File/Indexed.pm
Criterion Covered Total %
statement 299 335 89.2
branch 143 284 50.3
condition 51 110 46.3
subroutine 53 63 84.1
pod 23 38 60.5
total 569 830 68.5


line stmt bran cond sub pod time code
1             ##-*- Mode: CPerl -*-
2             ##
3             ## File: Tie/File/Indexed.pm
4             ## Author: Bryan Jurish
5             ## Description: tied array access to indexed data files
6              
7             package Tie::File::Indexed;
8 5     5   29751 use 5.10.0; ##-- for // operator
  5         11  
9 5     5   2401 use Tie::Array;
  5         4265  
  5         101  
10 5     5   2869 use JSON qw();
  5         51349  
  5         125  
11 5     5   28 use Fcntl qw(:DEFAULT :seek :flock);
  5         6  
  5         1749  
12 5     5   2156 use File::Copy qw();
  5         10704  
  5         98  
13 5     5   1971 use IO::File;
  5         31935  
  5         888  
14 5     5   28 use Carp qw(confess);
  5         5  
  5         176  
15 5     5   16 use strict;
  5         6  
  5         3820  
16              
17             ##======================================================================
18             ## Globals
19              
20             our @ISA = qw(Tie::Array);
21             our $VERSION = '0.07';
22              
23             ##======================================================================
24             ## Constructors etc.
25              
26             ## $tied = CLASS->new(%opts)
27             ## $tied = CLASS->new($file,%opts)
28             ## + %opts, object structure:
29             ## (
30             ## file => $file, ##-- file basename; uses files "${file}", "${file}.idx", "${file}.hdr"
31             ## mode => $mode, ##-- open mode (fcntl flags or perl-style; default='rwa')
32             ## perms => $perms, ##-- default: 0666 & ~umask
33             ## pack_o => $pack_o, ##-- file offset pack template (default='J')
34             ## pack_l => $pack_l, ##-- string-length pack template (default='J')
35             ## bsize => $bsize, ##-- block-size in bytes for index batch-operations (default=2**21 = 2MB)
36             ## temp => $bool, ##-- if true, call unlink() on object destruction (default=false)
37             ## ##
38             ## ##-- pack lengths (after open())
39             ## len_o => $len_o, ##-- packsize($pack_o)
40             ## len_l => $len_l, ##-- packsize($pack_l)
41             ## len_ix => $len_ix, ##-- packsize($pack_ix) == $len_o + $len_l
42             ## pack_ix=> $pack_ix, ##-- "${pack_o}${pack_l}"
43             ## ##
44             ## ##-- guts (after open())
45             ## idxfh => $idxfh, ##-- $file.idx : [$i] => pack("${pack_o}${pack_l}", $offset_in_datfh_of_item_i, $len_in_datfh_of_item_i)
46             ## datfh => $datfh, ##-- $file : raw data (concatenated)
47             ## #size => $nrecords, ##-- cached number of records for faster FETCHSIZE() ##-- potentially UNSAFE for concurrent access: DON'T USE
48             ## )
49             sub new {
50 17     17 1 430 my $that = shift;
51 17 50       45 my $file = (@_ % 2)==0 ? undef : shift;
52 17         34 my %opts = @_;
53 17   66     47 my $tied = bless({
54             $that->defaults(),
55             file => $file,
56             @_,
57             }, ref($that)||$that);
58 17 50       93 return $tied->open() if (defined($tied->{file}));
59 0         0 return $tied;
60             }
61              
62             ## %defaults = CLASS_OR_OBJECT->defaults()
63             ## + default attributes for constructor
64             sub defaults {
65             return (
66             #file => $file,
67 17     17 1 179 perms => (0666 & ~umask),
68             mode => 'rwa',
69             pack_o => 'J',
70             pack_l => 'J',
71             block => 2**21,
72             );
73             }
74              
75             ## undef = $tied->DESTROY()
76             ## + implicitly calls unlink() if 'temp' attribute is set to a true value
77             ## + implicitly calls close()
78             sub DESTROY {
79 16 100   16   1579 $_[0]->unlink() if ($_[0]{temp});
80 16         33 $_[0]->close();
81             }
82              
83             ##======================================================================
84             ## Utilities
85              
86             ##--------------------------------------------------------------
87             ## Utilities: fcntl
88              
89             ## $flags = CLASS_OR_OBJECT->fcflags($mode)
90             ## + returns Fcntl flags for symbolic string $mode
91             sub fcflags {
92 384 50   384 0 717 shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
93 384         287 my $mode = shift;
94 384   50     449 $mode //= 'r';
95 384 100       958 return $mode if ($mode =~ /^[0-9]+$/); ##-- numeric mode is interpreted as Fcntl bitmask
96 17         41 my $fread = $mode =~ /[r<]/;
97 17         23 my $fwrite = $mode =~ /[wa>\+]/;
98 17   66     59 my $fappend = ($mode =~ /[a]/ || $mode =~ />>/);
99 17 100       35 my $flags = ($fread
    0          
    50          
100             ? ($fwrite ? (O_RDWR|O_CREAT) : O_RDONLY)
101             : ($fwrite ? (O_WRONLY|O_CREAT) : 0)
102             );
103 17 100 100     59 $flags |= O_TRUNC if ($fwrite && !$fappend);
104 17         54 return $flags;
105             }
106              
107             ## $bool = CLASS_OR_OBJECT->fcread($mode)
108             ## + returns true if any read-bits are set for $mode
109             sub fcread {
110 65 50   65 0 147 shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
111 65         67 my $flags = fcflags(shift);
112 65   33     207 return ($flags&O_RDONLY)==O_RDONLY || ($flags&O_RDWR)==O_RDWR;
113             }
114              
115             ## $bool = CLASS_OR_OBJECT->fcwrite($mode)
116             ## + returns true if any write-bits are set for $mode
117             sub fcwrite {
118 110 50   110 0 303 shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
119 110         111 my $flags = fcflags(shift);
120 110   66     482 return ($flags&O_WRONLY)==O_WRONLY || ($flags&O_RDWR)==O_RDWR;
121             }
122              
123             ## $bool = CLASS_OR_OBJECT->fctrunc($mode)
124             ## + returns true if truncate-bits are set for $mode
125             sub fctrunc {
126 97 50   97 0 208 shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
127 97         95 my $flags = fcflags(shift);
128 97         228 return ($flags&O_TRUNC)==O_TRUNC;
129             }
130              
131             ## $bool = CLASS_OR_OBJECT->fccreat($mode)
132             sub fccreat {
133 0 0   0 0 0 shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
134 0         0 my $flags = fcflags(shift);
135 0         0 return ($flags&O_CREAT)==O_CREAT;
136             }
137              
138             ## $str = CLASS_OR_OBJECT->fcperl($mode)
139             ## + return perl mode-string for $mode
140             sub fcperl {
141 44 50   44 0 101 shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
142 44         43 my $flags = fcflags(shift);
143 44 100       55 return (fcread($flags)
    100          
    0          
    0          
    50          
144             ? (fcwrite($flags) ##-- +read
145             ? (fctrunc($flags) ##-- +read,+write
146             ? '+>' : '+<') ##-- +read,+write,+/-trunc
147             : '<')
148             : (fcwrite($flags) ##-- -read
149             ? (fctrunc($flags) ##-- -read,+write
150             ? '>' : '>>') ##-- -read,+write,+/-trunc
151             : '<') ##-- -read,-write : default
152             );
153             }
154              
155             ## $fh_or_undef = CLASS_OR_OBJECT->fcopen($file,$mode)
156             ## $fh_or_undef = CLASS_OR_OBJECT->fcopen($file,$mode,$perms)
157             ## + opens $file with fcntl- or perl-style mode $mode
158             sub fcopen {
159 44 50   44 0 139 shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
160 44         50 my ($file,$flags,$perms) = @_;
161 44         50 $flags = fcflags($flags);
162 44   33     62 $perms //= (0666 & ~umask);
163 44         54 my $mode = fcperl($flags);
164              
165 44         43 my ($sysfh);
166 44 50       56 if (ref($file)) {
167             ##-- dup an existing filehandle
168 0         0 $sysfh = $file;
169             }
170             else {
171             ##-- use sysopen() to honor O_CREAT and O_TRUNC
172 44 50       1673 sysopen($sysfh, $file, $flags, $perms) or return undef;
173             }
174              
175             ##-- now open perl-fh from system fh
176 44 50       414 open(my $fh, "${mode}&=", fileno($sysfh)) or return undef;
177 44 100 100     62 if (fcwrite($flags) && !fctrunc($flags)) {
178             ##-- append mode: seek to end of file
179 11 50       27 seek($fh, 0, SEEK_END) or return undef;
180             }
181 44         228 return $fh;
182             }
183              
184             ##--------------------------------------------------------------
185             ## Utilities: pack sizes
186              
187             ## $len = CLASS->packsize($packfmt)
188             ## $len = CLASS->packsize($packfmt,@args)
189             ## + get pack-size for $packfmt with args @args
190             sub packsize {
191 5     5   2455 use bytes; ##-- deprecated in perl v5.18.2
  5         38  
  5         38  
192 5     5   121 no warnings;
  5         6  
  5         11050  
193 42     42 0 175 return bytes::length(pack($_[0],@_[1..$#_]));
194             }
195              
196              
197             ##--------------------------------------------------------------
198             ## Utilities: JSON
199              
200             ## $data = CLASS->loadJsonString( $string,%opts)
201             ## $data = CLASS->loadJsonString(\$string,%opts)
202             ## + %opts passed to JSON::from_json(), e.g. (relaxed=>0)
203             ## + supports $opts{json} = $json_obj
204             sub loadJsonString {
205 8 50   8 0 36 my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
206 8 50       14 my $bufr = ref($_[0]) ? $_[0] : \$_[0];
207 8         18 my %opts = @_[1..$#_];
208 8 50       15 return $opts{json}->decode($$bufr) if ($opts{json});
209 8         47 return JSON::from_json($$bufr, {utf8=>!utf8::is_utf8($$bufr), relaxed=>1, allow_nonref=>1, %opts});
210             }
211              
212             ## $data = CLASS->loadJsonFile($filename_or_handle,%opts)
213             sub loadJsonFile {
214 8 50   8 0 19 my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
215 8         8 my $file = shift;
216 8 50       37 my $fh = ref($file) ? $file : IO::File->new("<$file");
217 8 50       346 return undef if (!$fh);
218 8         22 binmode($fh,':raw');
219 8         24 local $/=undef;
220 8         84 my $buf = <$fh>;
221 8 50       40 close($fh) if (!ref($file));
222 8         18 return $that->loadJsonString(\$buf,@_);
223             }
224              
225             ## $str = CLASS->saveJsonString($data)
226             ## $str = CLASS->saveJsonString($data,%opts)
227             ## + %opts passed to JSON::to_json(), e.g. (pretty=>0, canonical=>0)'
228             ## + supports $opts{json} = $json_obj
229             sub saveJsonString {
230 30 50   30 0 333 my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
231 30         34 my $data = shift;
232 30         37 my %opts = @_;
233 30 50       50 return $opts{json}->encode($data) if ($opts{json});
234 30         139 return JSON::to_json($data, {utf8=>1, allow_nonref=>1, allow_unknown=>1, allow_blessed=>1, convert_blessed=>1, pretty=>1, canonical=>1, %opts});
235             }
236              
237             ## $bool = CLASS->saveJsonFile($data,$filename_or_handle,%opts)
238             sub saveJsonFile {
239 18 50   18 0 51 my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
240 18         27 my $data = shift;
241 18         19 my $file = shift;
242 18 50       105 my $fh = ref($file) ? $file : IO::File->new(">$file");
243 18 50 0     1414 logconfess((ref($that)||$that)."::saveJsonFile() failed to open file '$file': $!") if (!$fh);
244 18         66 binmode($fh,':raw');
245 18 50       61 $fh->print($that->saveJsonString($data,@_)) or return undef;
246 18 50       909 if (!ref($file)) { close($fh) || return undef; }
  18 50       474  
247 18         78 return 1;
248             }
249              
250             ##--------------------------------------------------------------
251             ## Utilities: debugging
252              
253             ## $idxbuf = $tied->slurpIndex()
254             ## + slurps whole raw index-file into a string-buffer (for debugging)
255             sub slurpIndex {
256 0     0 0 0 my $tied = shift;
257 0 0       0 return undef if (!$tied->opened);
258 0         0 my $fh = $tied->{idxfh};
259 0 0       0 CORE::seek($fh, 0, SEEK_SET) or return undef;
260 0         0 local $/ = undef;
261 0         0 return <$fh>;
262             }
263              
264             ## $idxtxt = $tied->indexText()
265             ## @idxtxt = $tied->indexText()
266             ## + slurps whole index file and returns it as a text-buffer (for debugging)
267             sub indexText {
268 0     0 0 0 my $tied = shift;
269 0   0     0 my @idx = map {join(' ',unpack($tied->{pack_ix},$_))} unpack("(A[$tied->{len_ix}])*", $tied->slurpIndex//'');
  0         0  
270 0 0       0 return wantarray ? @idx : join("\n",@idx)."\n";
271             }
272              
273             ## $datbuf = $tied->slurpData()
274             ## + slurps whole raw data-file into a string-buffer (for debugging)
275             sub slurpData {
276 0     0 0 0 my $tied = shift;
277 0 0       0 return undef if (!$tied->opened);
278 0         0 my $fh = $tied->{datfh};
279 0 0       0 CORE::seek($fh, 0, SEEK_SET) or return undef;
280 0         0 local $/ = undef;
281 0         0 return <$fh>;
282             }
283              
284              
285             ##======================================================================
286             ## Subclass API: Data I/O
287              
288             ## $bool = $tied->writeData($data)
289             ## + write item $data to $tied->{datfh} at its current position
290             ## + after writing, $tied->{datfh} should be positioned to the first byte following the written item
291             ## + $tied is assumed to be opened in write-mode
292             ## + default implementation just writes $data as a byte-string (undef is written as the empty string)
293             ## + can be overridden by subclasses to perform transparent encoding of complex data
294             sub writeData {
295 24   50 24 1 65 return $_[0]{datfh}->print($_[1]//'');
296             }
297              
298             ## $data_or_undef = $tied->readData($length)
299             ## + read item data record of length $length from $tied->{datfh} at its current position
300             ## + default implementation just reads a byte-string of length $length
301             sub readData {
302 106 50   106 1 386 CORE::read($_[0]{datfh}, my $buf, $_[1])==$_[1] or return undef;
303 106         271 return $buf;
304             }
305              
306             ##======================================================================
307             ## Subclass API: Index I/O
308              
309             ## ($off,$len) = $tied->readIndex($index)
310             ## ($off,$len) = $tied->readIndex(undef)
311             ## + gets index-record for item at logical index $index from $tied->{idxfh}
312             ## + if $index is undef, read from the current position of $tied->{idxfh}
313             ## + $index is assumed to exist in the array
314             ## + returns the empty list on error
315             sub readIndex {
316 133 50 33 133 1 530 !defined($_[1]) or CORE::seek($_[0]{idxfh}, $_[1]*$_[0]{len_ix}, SEEK_SET) or return qw();
317 133 50       573 CORE::read($_[0]{idxfh}, my $buf, $_[0]{len_ix})==$_[0]{len_ix} or return qw();
318 133         368 return unpack($_[0]{pack_ix}, $buf);
319             }
320              
321             ## $tied_or_undef = $tied->writeIndex($index,$off,$len)
322             ## $tied_or_undef = $tied->writeIndex(undef,$off,$len)
323             ## + writes index-record for item at logical index $index to $tied->{idxfh}
324             ## + if $index is undef, write at the current position of $tied->{idxfh}
325             ## + returns undef list on error
326             sub writeIndex {
327 75 50 66 75 1 705 !defined($_[1]) or CORE::seek($_[0]{idxfh}, $_[1]*$_[0]{len_ix}, SEEK_SET) or return undef;
328 75 50       267 $_[0]{idxfh}->print(pack($_[0]{pack_ix}, $_[2], $_[3])) or return undef;
329 75         418 return $_[0];
330             }
331              
332             ## $tied_or_undef = $tied->shiftIndex($start,$n,$shift)
333             ## + moves $n index records starting from $start by $shift positions (may be negative)
334             ## + operates directly on $tied->{idxfh}
335             ## + doesn't change old values unless they are overwritten
336             sub shiftIndex {
337 4     4 1 5 my ($tied,$start,$n,$shift) = @_;
338              
339             ##-- common variables
340 4         4 my $idxfh = $tied->{idxfh};
341 4         4 my $len_ix = $tied->{len_ix};
342 4   50     12 my $bsize = $tied->{bsize} // 2**21;
343 4         5 my $bstart = $len_ix * $start;
344 4         4 my $bn = $len_ix * $n;
345 4         3 my $bshift = $len_ix * $shift;
346 4         2 my ($buf,$boff,$blen);
347              
348             ##-- dispatch by shift direction
349 4 100       8 if ($shift > 0) {
350             ##-- shift right (copy right-to-left)
351 2 50       6 CORE::seek($tied->{idxfh}, $bstart+$bn, SEEK_SET) or return undef;
352 2         5 while ($bn > 0) {
353 2 50       4 $blen = $bn > $bsize ? $bsize : $bn;
354 2 50       5 CORE::seek($idxfh, -$blen, SEEK_CUR) or return undef;
355 2 50       11 CORE::read($idxfh, $buf, $blen)==$blen or return undef;
356 2 50       5 CORE::seek($idxfh, $bshift-$blen, SEEK_CUR) or return undef;
357 2 50       6 $idxfh->print($buf) or return undef;
358 2 50       27 CORE::seek($idxfh, -$bshift, SEEK_CUR) or return undef;
359 2         4 $bn -= $blen;
360             }
361             } else {
362             ##-- shift left (copy left-to-right)
363 2 50       7 CORE::seek($tied->{idxfh}, $bstart, SEEK_SET) or return undef;
364 2         4 while ($bn > 0) {
365 2 50       4 $blen = $bn > $bsize ? $bsize : $bn;
366 2 50       12 CORE::read($idxfh, $buf, $blen)==$blen or return undef;
367 2 50       6 CORE::seek($idxfh, $bshift-$blen, SEEK_CUR) or return undef;
368 2 50       6 $idxfh->print($buf) or return undef;
369 2 50       34 CORE::seek($idxfh, -$bshift, SEEK_CUR) or return undef;
370 2         3 $bn -= $blen;
371             }
372             }
373              
374 4         6 return $tied;
375             }
376              
377              
378              
379             ##======================================================================
380             ## Object API
381              
382             ##--------------------------------------------------------------
383             ## Object API: header
384              
385             ## @keys = $tied->headerKeys()
386             ## + keys to save as header
387             sub headerKeys {
388 18   100 18 1 18 return grep {!ref($_[0]{$_}) && $_ !~ m{^(?:file|mode|perms)$}} keys %{$_[0]};
  225         680  
  18         68  
389             }
390              
391             ## \%header = $tied->headerData()
392             ## + data to save as header
393             sub headerData {
394 18     18 1 31 my $tied = shift;
395 18         35 return {(map {($_=>$tied->{$_})} $tied->headerKeys), class=>ref($tied)};
  134         232  
396             }
397              
398             ## $tied_or_undef = $tied->loadHeader()
399             ## $tied_or_undef = $tied->loadHeader($headerFile,%opts)
400             ## + loads header from "$tied->{file}.hdr"
401             ## + %opts are passed to loadJsonFile()
402             sub loadHeader {
403 8     8 1 13 my ($tied,$hfile,%opts) = @_;
404 8 50 33     33 $hfile //= $tied->{file}.".hdr" if (defined($tied->{file}));
405 8 50       12 confess(ref($tied)."::loadHeader(): no header-file specified and no 'file' attribute defined") if (!defined($hfile));
406 8 50       18 my $hdata = $tied->loadJsonFile($hfile,%opts)
407             or confess(ref($tied)."::loadHeader(): failed to load header data from '$hfile'");
408 8         275 @$tied{keys %$hdata} = values %$hdata;
409 8         39 return $tied;
410             }
411              
412             ## $tied_or_undef = $tied->saveHeader()
413             ## $tied_or_undef = $tied->saveHeader($headerFile)
414             ## + saves header data to $headerFile
415             ## + %opts are passed to saveJsonFile()
416             sub saveHeader {
417 18     18 1 27 my ($tied,$hfile,%opts) = @_;
418 18 50 33     84 $hfile //= $tied->{file}.".hdr" if (defined($tied->{file}));
419 18 50       28 confess(ref($tied)."::saveHeader(): no header-file specified and no 'file' attribute defined") if (!defined($hfile));
420 18         36 return $tied->saveJsonFile($tied->headerData(), $hfile, %opts);
421             }
422              
423             ##--------------------------------------------------------------
424             ## Object API: open/close
425              
426             ## $tied_or_undef = $tied->open($file,$mode)
427             ## $tied_or_undef = $tied->open($file)
428             ## $tied_or_undef = $tied->open()
429             ## + opens file(s)
430             sub open {
431 21     21 1 24 my ($tied,$file,$mode) = @_;
432 21   66     67 $file //= $tied->{file};
433 21   66     50 $mode //= $tied->{mode};
434 21 50       44 $tied->close() if ($tied->opened);
435 21         26 $tied->{file} = $file;
436 21         42 $tied->{mode} = $mode = fcflags($mode);
437              
438 21 100 66     39 if (fcread($mode) && !fctrunc($mode)) {
439 8 50 33     97 (!-e "$file.hdr" && fccreat($mode))
      33        
440             or $tied->loadHeader()
441             or confess(ref($tied)."::failed to load header from '$tied->{file}.hdr': $!");
442             }
443              
444             $tied->{idxfh} = fcopen("$file.idx", $mode, $tied->{perms})
445 21 50       60 or confess(ref($tied)."::open failed for index-file $file.idx: $!");
446             $tied->{datfh} = fcopen("$file", $mode, $tied->{perms})
447 21 50       47 or confess(ref($tied)."::open failed for data-file $file: $!");
448 21         81 binmode($_) foreach (@$tied{qw(idxfh datfh)});
449              
450             ##-- pack lengths
451             #use bytes; ##-- deprecated in perl v5.18.2
452 21         36 $tied->{len_o} = packsize($tied->{pack_o});
453 21         2308 $tied->{len_l} = packsize($tied->{pack_l});
454 21         61 $tied->{len_ix} = $tied->{len_o} + $tied->{len_l};
455 21         36 $tied->{pack_ix} = $tied->{pack_o}.$tied->{pack_l};
456              
457 21         91 return $tied;
458             }
459              
460             ## $tied_or_undef = $tied->close()
461             ## + close any opened file, writes header if opened in write mode
462             sub close {
463 31     31 1 33 my $tied = shift;
464 31 100       39 return $tied if (!$tied->opened);
465 20 100 66     29 if ($tied->opened && fcwrite($tied->{mode})) {
466 18 50       42 $tied->saveHeader() or
467             confess(ref($tied)."::close(): failed to save header file");
468             }
469 20         195 delete @$tied{qw(idxfh datfh)}; ##-- should auto-close if not shared
470 20         32 undef $tied->{file};
471 20         78 return $tied;
472             }
473              
474             ## $bool = $tied->reopen()
475             ## + closes and re-opens underlying filehandles
476             ## + should cause a "real" flush even on systems without a working IO::Handle::flush
477             sub reopen {
478 2     2 1 2 my $tied = shift;
479 2         3 my ($file,$mode) = @$tied{qw(file mode)};
480 2   33     4 return $tied->opened() && $tied->close() && $tied->open($file,$mode);
481             }
482              
483             ## $bool = $tied->opened()
484             ## + returns true iff object is opened
485             sub opened {
486 85     85 1 85 my $tied = shift;
487             return (ref($tied)
488             && defined($tied->{idxfh})
489             && defined($tied->{datfh})
490 85   66     569 );
491             }
492              
493             ## $tied_or_undef = $tied->flush()
494             ## $tied_or_undef = $tied->flush($flushHeader)
495             ## + attempts to flush underlying filehandles using underlying filehandles' flush() method
496             ## (ususally IO::Handle::flush)
497             ## + also writes header file
498             ## + calls reopen() if underlying filehandles don't support a flush() method
499             sub flush {
500 5     5 1 7 my ($tied,$flushHeader) = @_;
501 5         8 my $rc = $tied->opened;
502 5 50 33     44 if ($rc && UNIVERSAL::can($tied->{idxfh},'flush') && UNIVERSAL::can($tied,'flush')) {
      33        
503             ##-- use fh flush() method
504 5   33     55 $rc = $tied->{idxfh}->flush() && $tied->{datfh}->flush() && (!$flushHeader || $tied->saveHeaderFile());
505             }
506             else {
507             ##-- use reopen()
508 0         0 $rc = $tied->reopen();
509             }
510 5 50       18 return $rc ? $tied : undef;
511             }
512              
513             ##--------------------------------------------------------------
514             ## Object API: file operations
515              
516             ## $tied_or_undef = $tied->unlink()
517             ## $tied_or_undef = $tied->unlink($file)
518             ## + attempts to unlink underlying files
519             ## + implicitly calls close()
520             sub unlink {
521 11     11 1 1655 my ($tied,$file) = @_;
522 11   66     54 $file //= $tied->{file};
523 11         33 $tied->close();
524 11 100       23 return undef if (!defined($file));
525 10         19 foreach ('','.idx','.hdr') {
526 30 50       1060 CORE::unlink("${file}$_") or return undef;
527             }
528 10         38 return $tied;
529             }
530              
531             ## $tied_or_undef = $tied->rename($newname)
532             ## + renames underlying file(s) using CORE::rename()
533             ## + implicitly close()s and re-open()s $tied
534             ## + object must be opened in write-mode
535             sub rename {
536 1     1 1 6 my ($tied,$newfile) = @_;
537 1         3 my $flags = fcflags($tied->{mode});
538 1         1 my $oldfile = $tied->{file};
539 1 50 33     3 return undef if (!$tied->opened() || !fcwrite($flags) || !$tied->close);
      33        
540 1         3 foreach ('','.idx','.hdr') {
541 3 50       69 CORE::rename("${oldfile}$_","${newfile}$_") or return undef;
542             }
543 1         4 return $tied->open($newfile, ($flags & ~O_TRUNC));
544             }
545              
546             ## $dst_object_or_undef = $tied_src->copy($dst_filename, %dst_opts)
547             ## $dst_object_or_undef = $tied_src->copy($dst_object)
548             ## + copies underlying file(s) using File::Copy::copy()
549             ## + source object must be opened
550             ## + implicitly calls flush() on both source and destination objects
551             ## + if a destination object is specified, it must be opened in write-mode
552             sub copy {
553 2     2 1 11 my ($src,$dst,%opts) = @_;
554 2 50 33     4 return undef if (!$src->opened || !$src->flush);
555 2 100       8 $dst = $src->new($dst, %opts, mode=>'rw') if (!ref($dst));
556 2 0 33     4 return undef if (!$dst->opened && !$dst->open($opts{file}, 'rw'));
557              
558 2         3 foreach (qw(idxfh datfh)) {
559 4 50       220 CORE::seek($src->{$_}, 0, SEEK_SET) or return undef;
560 4 50       8 CORE::seek($dst->{$_}, 0, SEEK_SET) or return undef;
561 4 50       8 File::Copy::copy($src->{$_}, $dst->{$_}) or return undef;
562             }
563 2         209 return $dst->flush();
564             }
565              
566             ## $tied_or_undef = $tied->move($newname)
567             ## + renames underlying file(s) using File::Copy::move()
568             ## + implicitly close()s and re-open()s $tied
569             ## + object must be opened in write-mode
570             sub move {
571 1     1 1 2 my ($tied,$newfile) = @_;
572 1         3 my $flags = fcflags($tied->{mode});
573 1         2 my $oldfile = $tied->{file};
574 1 50 33     3 return undef if (!$tied->opened() || !fcwrite($flags) || !$tied->close);
      33        
575 1         4 foreach ('','.idx','.hdr') {
576 3 50       99 File::Copy::move("${oldfile}$_","${newfile}$_") or return undef;
577             }
578 1         35 return $tied->open($newfile, ($flags & ~O_TRUNC));
579             }
580              
581              
582              
583             ##--------------------------------------------------------------
584             ## Object API: consolidate
585              
586             ## $tied_or_undef = $tied->consolidate()
587             ## $tied_or_undef = $tied->consolidate($tmpfile)
588             ## + consolidates file data: ensures data in $tied->{datfh} are in index-order and contain no gaps or unused blocks
589             ## + object must be opened in write-mode
590             ## + uses $tmpfile as a temporary file for consolidation (default="$tied->{file}.tmp")
591             sub consolidate {
592 1     1 1 296 my ($tied,$tmpfile) = @_;
593              
594             ##-- open tempfile
595 1   33     7 $tmpfile //= "$tied->{file}.tmp";
596             my $tmpfh = fcopen($tmpfile, $tied->{mode}, $tied->{perms})
597 1 50       3 or confess(ref($tied)."::open failed for temporary data-file $tmpfile: $!");
598 1         3 binmode($tmpfh);
599              
600             ##-- copy data
601 1         4 my ($file,$idxfh,$datfh,$len_ix,$pack_ix) = @$tied{qw(file idxfh datfh len_ix pack_ix)};
602 1         1 my ($buf,$off,$len);
603 1         3 my $size = $tied->size;
604 1 50       3 CORE::seek($idxfh, 0, SEEK_SET) or return undef;
605 1 50       3 CORE::seek($tmpfh, 0, SEEK_SET) or return undef;
606 1         3 for (my $i=0; $i < $size; ++$i) {
607 3 50       36 CORE::read($idxfh, $buf, $len_ix)==$len_ix or return undef;
608 3         6 ($off,$len) = unpack($pack_ix, $buf);
609              
610             ##-- update index record (in-place)
611 3 50       8 CORE::seek($idxfh, $i*$len_ix, SEEK_SET) or return undef;
612 3 50       10 $idxfh->print(pack($pack_ix, CORE::tell($tmpfh),$len)) or return undef;
613              
614             ##-- copy data record
615 3 50       17 next if ($len == 0);
616 3 50       7 CORE::seek($datfh, $off, SEEK_SET) or return undef;
617 3 50       9 CORE::read($datfh, $buf, $len)==$len or return undef;
618 3 50       5 $tmpfh->print($buf) or return undef;
619             }
620              
621             ##-- close data-filehandles
622 1 50       27 CORE::close($tmpfh)
623             or confess(ref($tied)."::consolidate(): failed to close temp-file '$tmpfile': $!");
624 1 50       6 CORE::close($datfh)
625             or confess(ref($tied)."::consolidate(): failed to close old data-file '$file': $!");
626              
627             ##-- replace old datafile
628 1         2 undef $tmpfh;
629 1         1 undef $datfh;
630 1         2 delete $tied->{datfh};
631 1 50       55 CORE::unlink($file)
632             or confess(ref($tied)."::consolidate(): failed to unlink old data-file '$tied->{file}': $!");
633             #CORE::rename($tmpfile, $file) ##-- win32 chokes here with "Permission denied"
634 1 50       6 File::Copy::move($tmpfile, $file)
635             or confess(ref($tied)."::consolidate(): failed to move temp-file '$tmpfile' to '$file': $!");
636              
637             ##-- re-open
638             $tied->{datfh} = fcopen("$file", (fcflags($tied->{mode}) & ~O_TRUNC), $tied->{perms})
639 1 50       65 or confess(ref($tied)."::consolidate(): failed to re-open data-file $file: $!");
640              
641 1         4 return $tied;
642             }
643              
644             ##--------------------------------------------------------------
645             ## Object API: advisory locking
646              
647             ## $bool = $tied->flock()
648             ## $bool = $tied->flock($lock)
649             ## + get an advisory lock of type $lock (default=LOCK_EX) on $tied->{datfh}, using perl's flock() function
650             ## + implicitly calls flush() prior to locking
651             sub flock {
652 0     0 1 0 my ($tied,$op) = @_;
653 0 0       0 return undef if (!$tied->opened);
654 0         0 $tied->flush();
655 0   0     0 return CORE::flock($tied->{datfh}, ($op // LOCK_EX));
656             }
657              
658             ## $bool = $tied->funlock()
659             ## $bool = $tied->funlock($lock)
660             ## + unlock $tied->{datfh} using perl's flock() function; $lock defaults to LOCK_UN
661             sub funlock {
662 0   0 0 1 0 return $_[0]->flock( LOCK_UN | ($_[1]//0) );
663             }
664              
665              
666             ##======================================================================
667             ## API: Tied Array
668              
669             ##--------------------------------------------------------------
670             ## API: Tied Array: mandatory methods
671              
672             ## $tied = tie(@array, $tieClass, $file,%opts)
673             ## $tied = TIEARRAY($tieClass, $file,%opts)
674 5     5   128 BEGIN { *TIEARRAY = \&new; }
675              
676             ## $count = $tied->FETCHSIZE()
677             ## + like scalar(@array)
678             ## + re-positions $tied->{idxfh} to eof
679 5     5   3990 BEGIN { *size = \&FETCHSIZE; }
680             sub FETCHSIZE {
681 288 50   288   12938 return undef if (!$_[0]{idxfh});
682             #return ((-s $_[0]{idxfh}) / $_[0]{len_ix}); ##-- doesn't handle recent writes correctly (probably due to perl i/o buffering)
683             ##
684 288 50       711 CORE::seek($_[0]{idxfh},0,SEEK_END) or return undef;
685 288         612 return CORE::tell($_[0]{idxfh}) / $_[0]{len_ix};
686             }
687              
688             ## $val = $tied->FETCH($index)
689             ## $val = $tied->FETCH($index)
690             sub FETCH {
691             ##-- sanity check
692 131 50   131   1489 return undef if ($_[1] >= $_[0]->size);
693              
694             ##-- get index record from $idxfh
695 131 50       192 my ($off,$len) = $_[0]->readIndex($_[1]) or return undef;
696              
697             ##-- get data record from $datfh
698 131 50       378 CORE::seek($_[0]{datfh}, $off, SEEK_SET) or return undef;
699 131         209 return $_[0]->readData($len);
700             }
701              
702             ## $val = $tied->STORE($index,$val)
703             ## + no consistency checking or optimization; just appends a new record to the end of $datfh and updates $idxfh
704             sub STORE {
705             ##-- append encoded record to $datfh
706 74 50   74   579 CORE::seek($_[0]{datfh}, 0, SEEK_END) or return undef;
707 74         106 my $off0 = CORE::tell($_[0]{datfh});
708 74 50       140 $_[0]->writeData($_[2]) or return undef;
709 74         1026 my $off1 = CORE::tell($_[0]{datfh});
710              
711             ##-- update index record in $idxfh
712 74 50       130 $_[0]->writeIndex($_[1], $off0, ($off1-$off0)) or return undef;
713              
714             ##-- return
715 74         177 return $_[2];
716             }
717              
718             ## $count = $tied->STORESIZE($count)
719             ## $count = $tied->STORESIZE($count) ##-- local extension
720             ## + modifies only $idxfh
721             sub STORESIZE {
722 0     0   0 my $oldsize = $_[0]->size;
723 0 0       0 if ($_[1] < $oldsize) {
    0          
724             ##-- shrink
725 0 0       0 CORE::truncate($_[0]{idxfh}, $_[1]*$_[0]{len_ix}) or return undef;
726             } elsif ($_[1] > $oldsize) {
727             ##-- grow (idxfh only)
728 0 0       0 CORE::seek($_[0]{idxfh}, $_[1]*$_[0]{len_ix}-1, SEEK_SET) or return undef;
729 0         0 $_[0]{idxfh}->print("\0");
730             }
731 0         0 return $_[1];
732             }
733              
734             ## $bool = $tied->EXISTS($index)
735             sub EXISTS {
736 0     0   0 return $_[1] < $_[0]->size;
737             }
738              
739             ## undef = $tied->DELETE($index)
740             ## + really just wraps $tied->STORE($index,undef)
741             sub DELETE {
742 0     0   0 return $_[0]->STORE($_[1],undef);
743             }
744              
745             ##--------------------------------------------------------------
746             ## API: Tied Array: optional methods
747              
748             ## undef = $tied->CLEAR()
749             sub CLEAR {
750 10 50   10   268 CORE::truncate($_[0]{idxfh}, 0) or return undef;
751 10 50       111 CORE::truncate($_[0]{datfh}, 0) or return undef;
752 10         65 return $_[0];
753             }
754              
755             ## $newsize = $tied->PUSH(@vals)
756             sub PUSH {
757 1     1   1 my $tied = shift;
758              
759 1 50       5 CORE::seek($tied->{datfh}, 0, SEEK_END) or return undef;
760 1 50       3 CORE::seek($tied->{idxfh}, 0, SEEK_END) or return undef;
761 1         1 my ($off0,$off1);
762 1         3 foreach (@_) {
763 1         2 my $off0 = CORE::tell($tied->{datfh});
764 1 50       2 $tied->writeData($_) or return undef;
765 1         9 my $off1 = CORE::tell($tied->{datfh});
766              
767             ##-- update index record in $idxfh
768 1 50       2 $tied->writeIndex(undef, $off0, ($off1-$off0)) or return undef;
769             }
770              
771 1 50       4 return $tied->size if (defined(wantarray));
772             }
773              
774             ## $val = $tied->POP()
775             ## + truncates data-file if we're popping the final data-record
776             sub POP {
777 1 50   1   4 return undef if (!(my $size=$_[0]->size));
778              
779             ##-- get final index record (& truncate it)
780 1 50       5 my ($off,$len) = $_[0]->readIndex($size-1) or return undef;
781 1 50       24 CORE::truncate($_[0]{idxfh}, ($size-1)*$_[0]{len_ix}) or return undef;
782              
783             ##-- get corresponding data-record
784 1 50       4 CORE::seek($_[0]{datfh}, $off, SEEK_SET) or return undef;
785 1         3 my $val = $_[0]->readData($len);
786              
787             ##-- maybe trim data-file
788 1 50       20 CORE::truncate($_[0]{datfh}, $off) if (($off+$len) == (-s $_[0]{datfh}));
789 1         5 return $val;
790             }
791              
792             ## $val = $tied->SHIFT()
793             ## + truncates data-file if we're shifting the final data-record
794             sub SHIFT {
795             ##-- get first index record
796 1 50   1   3 my ($off,$len) = $_[0]->readIndex(0) or return undef;
797              
798             ##-- defer to SPLICE
799 1         4 my $val = $_[0]->SPLICE(0,1);
800              
801             ##-- maybe trim data-file
802 1 50       8 CORE::truncate($_[0]{datfh}, $off) if (($off+$len) == (-s $_[0]{datfh}));
803 1         4 return $val;
804             }
805              
806             ## @removed = $tied->SPLICE($offset, $length, @newvals)
807             ## $last_removed = $tied->SPLICE($offset, $length, @newvals)
808             sub SPLICE {
809 4     4   8 my $tied = shift;
810 4         7 my $size = $tied->size();
811 4 50       8 my $off = (@_) ? shift : 0;
812 4 50       7 $off += $size if ($off < 0);
813 4 50       5 my $len = (@_) ? shift : ($size-$off);
814 4 50       7 $len += $size-$off if ($len < 0);
815              
816             ##-- get result-list
817 4         3 my ($i,@result);
818 4 100       8 if (wantarray) {
    50          
819 3         7 for ($i=$off; $i <= $len; ++$i) {
820 4         8 push(@result, $tied->FETCH($i));
821             }
822             } elsif ($len > 0) {
823 1         2 @result = ($tied->FETCH($off+$len-1));
824             }
825              
826             ##-- shift post-splice index records (expensive, but generally not so bad as default Tie::Array iterated FETCH()+STORE())
827 4         6 my $shift = scalar(@_) - $len;
828 4 50       14 $tied->shiftIndex($off+$len, $size-($off+$len), $shift) if ($shift != 0);
829              
830             ##-- store new values
831 4         9 for ($i=0; $i < @_; ++$i) {
832 4         6 $tied->STORE($off+$i, $_[$i]);
833             }
834              
835             ##-- maybe shrink array
836 4 100 50     43 CORE::truncate($tied->{idxfh}, ($size+$shift)*$tied->{len_ix}) or return undef if ($shift < 0);
837              
838             ##-- return
839 4 100       25 return wantarray ? @result : $result[0];
840             }
841              
842             ## @vals = $tied->UNSHIFT(@vals)
843             ## + just defers to SPLICE
844             sub UNSHIFT {
845 0     0     return scalar shift->SPLICE(0,0,@_);
846             }
847              
848             ## ? = $tied->EXTEND($newcount)
849              
850             1; ##-- be happpy