File Coverage

blib/lib/File/VMSVersions.pm
Criterion Covered Total %
statement 119 134 88.8
branch 42 80 52.5
condition 11 26 42.3
subroutine 16 16 100.0
pod 4 5 80.0
total 192 261 73.5


line stmt bran cond sub pod time code
1             package File::VMSVersions;
2            
3 2     2   1508 use 5.6.0;
  2         7  
  2         91  
4 2     2   10 use strict;
  2         2  
  2         57  
5 2     2   17 use warnings;
  2         4  
  2         58  
6            
7 2     2   2491 use IO::Handle;
  2         16101  
  2         111  
8 2     2   16 use File::Basename;
  2         4  
  2         168  
9 2     2   1903 use File::Spec::Functions;
  2         1612  
  2         172  
10 2     2   17 use Carp;
  2         4  
  2         118  
11 2     2   12 use Fcntl qw(:DEFAULT :flock);
  2         3  
  2         1230  
12 2     2   25401 use Data::Dumper;
  2         17122  
  2         4273  
13            
14             our $VERSION = '0.1';
15            
16             my $vcfilename = '.vcntl';
17            
18             =head1 NAME
19            
20             File::VMSVersions - Perl extension for opening files in a directory with
21             VMS like versioning
22            
23             =head1 SYNOPSIS
24            
25             use File::VMSVersions;
26            
27             my $vdir = File::VMSVersions->new(
28             -name => "./mydir",
29             -mode => 'versions',
30             -limit => 3,
31             );
32            
33             foreach my $i (1..6) {
34             my($fh, $fn) = $vdir->open('bla.dat', '>');
35             die $fn unless $fh;
36             print $fh "file number $i\n";
37             print "created $fn\n";
38             $fh->close;
39             }
40            
41             Now you should have the following files in ./mydir:
42            
43             .vcntl
44             bla.dat;lck
45             bla.dat;4
46             bla.dat;5
47             bla.dat;6
48            
49             =head1 DESCRIPTION
50            
51             The B module was developed for maintaining automatic
52             versioning of files in a directory. When you are using the module's routines for
53             opening files, it will keep a configurable amount of old versions. The versions
54             will be identified by a number that is added at the end of the filename after a
55             semicolon (i. e. F<'myfile.dat;7'>).
56            
57             The configured options for a directory are saved in the file F<'.vcntl'>. They
58             are read each time the B method is called and written when the B
59             constructor or the B method are called with according options.
60            
61             F<'.vcntl'> consists of only one line with limit and mode separated by an '#'.
62             For example:
63            
64             20#days
65            
66             10#versions
67            
68             It is ok to edit F<'.vcntl'> manually
69            
70             =cut
71            
72             =head1 CONSTRUCTOR
73            
74             To create a new B call the B contructor
75            
76             $obj = File::VMSVersions->new(
77             -name => ,
78             [ -mode => <'versions'|'days'>,
79             -limit => , ]
80             );
81            
82             You have to specify both B<-limit> and B<-mode> or none of them. If both
83             evaluate to false the file F<.vcntl> is read. Otherwise it will be replaced with
84             the new values. If the file doesn't exist when the configuration is read, there
85             is no version limit at all.
86            
87             =cut
88            
89             sub new {
90 2     2 0 1798 my($caller) = shift;
91 2   33     21 my($class) = ref($caller) || $caller;
92            
93 2         14 my %cfg = @_;
94            
95 2 50       10 $cfg{-name} or
96             croak << ' END';
97             usage: File::VMSVersions->new(
98             -name => ,
99             [-mode => <"days"|"versions">,
100             -limit => ,]
101             );
102             END
103            
104 2 50 25     20 if ($cfg{-mode} xor $cfg{-limit}) {
105 0 0       0 $cfg{-mode} ?
106             croak("-limit not specified") :
107             croak("-mode not specified");
108             }
109            
110 2         11 %cfg = _config(%cfg);
111            
112 2         12 return(bless(\%cfg, $class));
113             }
114            
115            
116             =head1 METHODS
117            
118             =over 4
119            
120             =item B<<< $obj->open( [, |>>']> [, ]) >>>
121            
122             Opens a version of a file. The default mode is '<' (read).
123            
124             If version is not specified when reading, the last version will be opened.
125            
126             If mode equals '>' (write) or '>>' (append), the specified version of the
127             desired file will be created or appended (append will create a new file if the
128             version doesn't exist).
129            
130             If there is no version specified, the highest existing version will be
131             incremented by 1.
132            
133             If the specified version is negative the nth last version will be opened.
134            
135             B returns a list with an indirect filehandle and the filename. On errors
136             the filehandle is undefined and the filename contains an error message.
137            
138             =cut
139            
140             sub open {
141 11     11 1 3880 my($self) = shift;
142 11         20 my($fn, $mode, $ver) = @_;
143            
144 11   50     32 $mode ||= '<';
145 11 50       54 croak("illegal mode '$mode'") unless $mode =~ /^<|>|>>$/;
146            
147 11   50     45 $ver ||= 0;
148            
149 11         59 my $fullfn = catfile($self->{-name}, $fn);
150            
151             # waiting for lock on lockfile in write mode
152 11         15 my $lck;
153 11 50       34 if ($mode =~ />/) {
154 11         30 $lck = _getlock("$fullfn;lck");
155             }
156            
157             # get version info
158 11         32 my $info = $self->info($fn);
159            
160 11         12 my $purge = 0;
161            
162 11 50       21 if ($mode eq '>') {
163            
164             # negative versions in write mode make no sense
165             # increase version anyway (like VMS)
166 11 50       35 $ver = $info->{max} + 1 if $ver <= 0;
167 11         14 $purge = 1;
168            
169             } else {
170 0 0       0 if ($ver) {
171 0 0       0 if ($ver > 0) {
172             # ver too small -> set to minimum
173 0 0       0 $ver = $info->{min} if $ver < $info->{min};
174             } else {
175             # get the desired version with negative array index
176 0         0 $ver = $info->{$self->{-mode}}->[$ver-1];
177 0 0       0 return(undef, "version >>$ver<< not found") unless defined($ver);
178             }
179 0 0 0     0 if ( !exists($info->{$ver}) and $mode eq '>>') {
180 0         0 $purge = 1;
181             }
182             } else {
183 0         0 $ver = $info->{max};
184             }
185             }
186            
187 11 50       779 CORE::open(my $fh, $mode, "$fullfn;$ver") or
188             return(undef, "error opening $fullfn version $ver in mode $mode, $!");
189            
190 11 50       62 $self->purge($fn, $self->config()) if $purge;
191            
192             # releasing lock
193 11 50       87 $lck->close if $lck;
194            
195 11         286 return($fh, "$fullfn;$ver");
196             }
197            
198            
199             =item B<<< $obj->purge(, [-mode => , -limit => ] >>>
200            
201             purges the versions of a file to the specified limit. When limit and mode are
202             not specified all but the last versions are purged. There is no need to call
203             B for normal versioning.
204            
205             =cut
206            
207             sub purge {
208 12     12 1 895 my($self) = shift;
209 12         29 my($fn, %cfg) = @_;
210            
211 12 50       25 croak("purge: no filename specified") unless $fn;
212            
213 12         66 my $fullfn = catfile($self->{-name}, $fn);
214            
215 12 50 50     76 if ($cfg{-mode} xor $cfg{-limit}) {
216 0 0       0 $cfg{-mode} ?
217             croak("-limit not specified") :
218             croak("-mode not specified");
219             }
220            
221 12 100       36 ($cfg{-limit}, $cfg{-mode}) = (1, 'versions') unless $cfg{-mode};
222            
223 12         28 my $info = $self->info($fn);
224            
225 12         36 print Dumper($info);
226 12         12141 print Dumper($self);
227 12         3380 print Dumper(\%cfg);
228            
229 12         2656 foreach my $v ( @{$info->{$self->{-mode}}} ) {
  12         48  
230            
231 31 100       71 if ($cfg{-mode} eq 'versions') {
232            
233 10 100       76 last if ( $info->{count} <= $cfg{-limit} );
234            
235 4 50       648 if ( unlink("$fullfn;$v") ) {
236 4         15 delete($info->{$v});
237             } else {
238 0         0 carp("couldn't purge $fullfn;$v");
239             }
240            
241 4         9 $info->{count}--;
242            
243             } else {
244            
245 21 100       103 if ( $info->{$v} - $info->{d_max} > $cfg{-limit} ) {
246 2 50       82 if ( unlink("$fullfn;$v") ) {
247 2         8 delete($info->{$v});
248             } else {
249 0         0 carp("couldn't purge $fullfn;$v");
250             }
251             }
252            
253             }
254             }
255             }
256            
257            
258             =item B<<< $obj->config([-mode => , -limit => ]) >>>
259            
260             Sets and/or returns limit and mode of the directory
261            
262             =cut
263            
264             sub config {
265 11     11 1 14 my $self = shift;
266            
267 11         22 my %cfg = @_;
268            
269 11 50 25     73 if ($cfg{-limit} xor $cfg{-mode}) {
270 0         0 croak('please specify both -limit and -mode or none of them!');
271             }
272            
273 11         32 $cfg{-name} = $self->{-name};
274            
275 11         30 return(_config(%cfg));
276             }
277            
278            
279             sub _config {
280 13     13   28 my(%cfg) = @_;
281            
282 13         63 my $vfn = catfile($cfg{-name}, $vcfilename);
283            
284 13 100       42 if ( $cfg{-limit} ) {
285            
286 2 50       12 croak("illegal mode >>$cfg{-mode}<<") unless $cfg{-mode} =~ /^days|versions$/;
287 2 50       14 croak("illegal limit >>$cfg{-limit}<<") unless $cfg{-limit} =~ /^\d+$/;
288            
289 2 50       166 CORE::open(my $vfh, ">", $vfn) or croak("could not write $vfn, $!");
290 2         20 print $vfh join('#', $cfg{-limit}, $cfg{-mode});
291 2         31 $vfh->close;
292            
293             } else {
294            
295 11 50       118 if ( -f $vfn ) {
296 11 50       288 CORE::open(my $vfh, "<", $vfn) or croak("couldn't read $vfn, $!");
297 11         216 ( $cfg{-limit}, $cfg{-mode} ) = split(/#/, <$vfh>);
298 11         41 $vfh->close;
299            
300 11 50       191 croak("illegal mode >>$cfg{-mode}<< from $vfn")
301             unless $cfg{-mode} =~ /^days|versions$/;
302 11 50       63 croak("illegal limit >>$cfg{-limit}<< from $vfn")
303             unless $cfg{-limit} =~ /^\d+$/;
304            
305             } else {
306 0         0 ( $cfg{-limit}, $cfg{-mode} ) = (999999999999999, 'versions');
307             }
308            
309             }
310            
311 13         179 return(%cfg);
312             }
313            
314            
315             =item B<<< $obj->info() >>>
316            
317             returns a hashref with version information for
318            
319             =cut
320            
321             sub info {
322 26     26 1 1321 my($self) = shift;
323 26         35 my($fn) = @_;
324            
325 26 50       45 $fn or croak "usage: info()";
326            
327 26         112 my $fullfn = catfile($self->{-name}, $fn);
328            
329 26         36 my(%info, @tmp, $ver);
330            
331 26         2590 foreach my $f (glob("$fullfn;*")) {
332 96         224 $ver = (split(/;/, $f))[-1];
333 96 100       358 next unless $ver =~ /^\d+$/;
334 70         697 $info{$ver} = -M $f;
335             }
336            
337 26         121 @tmp = sort {$a <=> $b} keys(%info);
  69         128  
338 26         67 $info{versions} = [@tmp];
339 26         62 $info{count} = @tmp;
340 26   100     72 $info{min} = $tmp[0] || 0;
341 26   100     82 $info{max} = $tmp[-1] || 0;
342 26         65 @tmp = sort {$info{$b} <=> $info{$a}} grep {/^\d+$/} keys(%info);
  72         135  
  174         373  
343 26         73 $info{days} = [@tmp];
344 26 100       144 $info{d_min} = $tmp[0] ? $info{$tmp[0]} : 0;
345 26 100       56 $info{d_max} = $tmp[-1] ? $info{$tmp[-1]} : 0;
346            
347 26         77 return(\%info);
348             }
349            
350            
351             sub _getlock {
352 11     11   15 my($fn) = @_;
353            
354 11 100       170 my $mode = -e $fn ? '<' : '>';
355 11 50       420 CORE::open(my $lck, $mode, $fn) or croak "couldn't open lock file $fn, $!";
356            
357 11 50       73 unless (flock($lck, LOCK_EX | LOCK_NB)) {
358 0         0 flock($lck, LOCK_EX);
359             }
360            
361 11         42 return($lck);
362             }
363            
364            
365             =head1 AUTHOR
366            
367             Thomas Kratz, EThomasKratz@web.deE
368            
369             =head1 COPYRIGHT AND LICENSE
370            
371             Copyright 2003 by Thomas Kratz
372            
373             This library is free software; you can redistribute it and/or modify
374             it under the same terms as Perl itself.
375            
376             =cut