File Coverage

blib/lib/FTN/Message/serialno/File.pm
Criterion Covered Total %
statement 75 82 91.4
branch 29 52 55.7
condition 9 21 42.8
subroutine 12 14 85.7
pod 2 4 50.0
total 127 173 73.4


line stmt bran cond sub pod time code
1             package FTN::Message::serialno::File;
2              
3 2     2   19517 use strict;
  2         3  
  2         72  
4 2     2   8 use warnings FATAL => 'all';
  2         2  
  2         63  
5              
6 2     2   9 use File::Spec ();
  2         3  
  2         36  
7              
8 2     2   873 use parent 'FTN::Message::serialno';
  2         498  
  2         9  
9              
10             use constant {
11 2         1859 FILE_EXTENSION => 'sn',
12             FILENAME_PATTERN => '^([0-9a-fA-F]{1,8})$',
13             FILENAME_FORMAT => '%x',
14             SERIALNO_FORMAT => '%08x',
15             MAX_TRIES => 5,
16 2     2   106 };
  2         3  
17              
18             =encoding utf8
19              
20             =head1 NAME
21              
22             FTN::Message::serialno::File - handles FTN message serialno via file in dedicated directory
23              
24             =head1 VERSION
25              
26             Version 20141120
27              
28             =cut
29              
30             our $VERSION = '20141120';
31              
32             =head1 SYNOPSIS
33              
34             use FTN::Message::serialno::File ();
35              
36             my $serialno = FTN::Message::serialno::File -> new( directory => '/home/user/ftn/_serialno' );
37              
38             my $new_serialno = $serialno -> get_serialno;
39              
40             die 'cannot get new serialno' unless defined $new_serialno;
41              
42             # use $new_serialno value for constructing new message
43              
44             =head1 DESCRIPTION
45              
46             This class is for handling serialno value for new FTN messages. Assigns consecutive unique values.
47              
48             =head1 USAGE
49              
50             This class has the following methods:
51              
52             =head2 new
53              
54             Class constructor. Has the following options:
55              
56             =cut
57              
58             sub initialize {
59 3     3 0 4 my $self = shift;
60              
61 3         11 my %param = @_;
62              
63             =over
64              
65             =item * directory
66              
67             The only mandatory option. Specifies directory where files with serialno are being created and removed. It is recommended to have it as a dedicated directory and not to keep any other files in it.
68              
69             FTN::Message::serialno::File -> new( directory => '/some/dir' );
70              
71             =cut
72              
73             # directory
74 3 50 33     51 die 'directory parameter should be defined!'
75             unless $param{directory}
76             && -d $param{directory};
77              
78 3         101 $self -> {directory} = $param{directory};
79              
80              
81             =item * file_extension
82              
83             Serialno files extension. Dot before its value will be added.
84              
85             FTN::Message::serialno::File -> new( directory => '/some/dir',
86             file_extension => 'seq',
87             );
88              
89             Default value is 'sn'.
90              
91             =cut
92              
93 3 100       10 my $extension = exists $param{file_extension}? $param{file_extension} : FILE_EXTENSION;
94              
95             =item * filename_pattern
96              
97             Defines pattern and matching files in the directory are considered as serialno files and hence can be removed/renamed. File extension shouldn't be specified as it's added automatically.
98              
99             =cut
100              
101 3   50     15 my $pattern = $param{filename_pattern} || FILENAME_PATTERN;
102              
103             =item * filename_format
104              
105             Defines filename format (as in printf) with encoded serialno value. File extension shouldn't be specified as it's added automatically. Filename created with filename_format should match filename_pattern.
106              
107             Default value is '%x'.
108              
109             =cut
110              
111 3   50     13 $self -> {filename_format} = $param{filename_format} || FILENAME_FORMAT;
112 3 50 33     15 if ( defined $extension
113             && length $extension
114             ) {
115 3 50       8 if ( substr( $pattern, -1 ) eq '$' ) {
116 3         12 $pattern = substr( $pattern, 0, -1 ) . '\.' . $extension . '$';
117             } else {
118 0         0 $pattern .= '\.' . $extension;
119             }
120 3         6 $self -> {filename_format} .= '.' . $extension;
121             }
122 3         48 $self -> {filename_pattern} = qr/$pattern/;
123              
124              
125             # validate that filename_pattern will match filename_format
126 3         14 my $t = sprintf $self -> {filename_format}, 1;
127 3 50       23 die 'not matching filename_format ( ' . $self -> {filename_format} . ' ) and filename_pattern ( ' . $self -> {filename_pattern} . ' )'
128             unless $t =~ m/$self->{filename_pattern}/;
129              
130              
131             # decode filename
132             $self -> {decode_filename} = $param{decode_filename} && ref $param{decode_filename} eq 'CODE'?
133             $param{decode_filename}
134             : sub {
135 36 100   36   193 shift =~ m/$self->{filename_pattern}/?
136             hex( $1 )
137             : undef;
138 3 50 33     22 };
139              
140             # encode filename
141             $self -> {encode_filename} = $param{encode_filename} && ref $param{encode_filename} eq 'CODE'?
142             $param{encode_filename}
143             : sub {
144 15     15   65 sprintf $self -> {filename_format}, shift;
145 3 50 33     15 };
146              
147 3 50       7 die 'incorrect decode_filename and/or encode_filename'
148             unless $self -> {decode_filename}( $self -> {encode_filename}( 1 ) ) == 1;
149              
150              
151             =item * max_tries
152              
153             Defines how many times renaming of the file is tried before it is considered failed.
154              
155             Default value is 5.
156              
157             =cut
158              
159             # max_tries for renaming
160 3 50 33     15 $self -> {max_tries} = $param{max_tries} && $param{max_tries} =~ m/^(\d+)$/?
161             $1
162             : MAX_TRIES;
163              
164              
165             =item * very_first_init
166              
167             Defines reference to a function for generating very first serialno value in case there are no matching files in the directory.
168              
169             The possible values:
170              
171             =over 6
172              
173             =item * CURRENT_UNIXTIME
174              
175             use current unixtime as a starting value.
176              
177             FTN::Message::serialno::File -> new( directory => '/some/dir',
178             very_first_init => 'CURRENT_UNIXTIME',
179             );
180            
181              
182             =item * CURRENT_UNIXTIME_MINUS_3_YEARS
183              
184             use current unixtime minus 3 years as a starting value.
185              
186             =item * user defined function
187              
188             FTN::Message::serialno::File -> new( directory => '/some/dir',
189             very_first_init => sub {
190             42; # voices in my head tell me to use 42
191             },
192             );
193              
194             =back
195              
196             Default value is function returning 1.
197              
198             =cut
199              
200             # very_first_init
201 3 100       6 if ( $param{very_first_init} ) {
202 1 50       4 if ( ref $param{very_first_init} eq 'CODE' ) {
    0          
    0          
203 1         2 $self -> {very_first_init} = $param{very_first_init};
204             } elsif ( $param{very_first_init} eq 'CURRENT_UNIXTIME_MINUS_3_YEARS' ) {
205 0     0   0 $self -> {very_first_init} = sub { time - 3 * 365 * 24 * 60 * 60 };
  0         0  
206             } elsif ( $param{very_first_init} eq 'CURRENT_UNIXTIME' ) {
207 0     0   0 $self -> {very_first_init} = sub { time; };
  0         0  
208             }
209             }
210              
211 2     2   4 $self -> {very_first_init} = sub { 1; }
212 3 100       13 unless $self -> {very_first_init};
213              
214             =item * serialno_format
215              
216             serialno format (as in printf) for a return value. Can be changed in case you want another casing ('%08X') or no leading zeroes ('%x') for example.
217              
218             Default value is '%08x'.
219              
220             =back
221              
222             =cut
223              
224 3   100     20 $self -> {serialno_format} = $param{serialno_format} || SERIALNO_FORMAT;
225             }
226              
227              
228             sub new {
229 3 50   3 1 2178 ref( my $class = shift ) and Carp::croak 'I am only a class method!';
230              
231 3         21 my $self = $class -> SUPER::new( @_ );
232              
233 3         9 initialize( $self, @_ ); # not $self -> initialize!
234              
235 3         7 $self;
236             }
237              
238              
239             =head2 get_serialno()
240              
241             Method that does all the work and returns either new valid serialno value or undef.
242              
243             If this is the very first run and no signs of previous serialno values, creates new file with starting value and returns it.
244              
245             If there is just one file with previous serialno value, tries to rename it to a new value (up to defined value (constructor option) times due to parallel execution) and returns that new value on success.
246              
247             If there are more than one file with previous serialno values, sorts them and removes all but last one. Then tries previous approach with last value.
248              
249             If renaming fails more than allowed count, undef is returned.
250              
251             =cut
252              
253             sub get_serialno {
254 12 50   12 1 4221 ref( my $self = shift ) or Carp::croak 'I am only an object method!';
255              
256 12         27 my $try = 1;
257 12         9 my $new_serialno;
258              
259             {
260 12 50       9 opendir my $dh, $self -> {directory}
  12         136  
261             or die "can't opendir $self->{directory}: $!";
262              
263 0         0 my @found_file = sort { $a -> [ 1 ] <=> $b -> [ 1 ] }
  33         68  
264             map {
265 12         509 my $filename = $_;
266 33         33 my @r;
267              
268 33         50 my $t = $self -> {decode_filename}( $filename );
269 33 100       65 if ( defined $t ) {
270 9         19 my $full_name = $self -> full_filename( $filename );
271              
272 9 50       212 push @r,
273             [ $full_name,
274             $t,
275             ]
276             if -f $full_name;
277             }
278              
279 33         72 @r;
280             } readdir $dh;
281              
282 12         76 closedir $dh;
283              
284 12 100       21 if ( @found_file ) {
285             # as old numbers can be deleted by other process, we don't care about errors here, so let's do it in one shot
286 9 50       20 unlink map $_ -> [ 0 ],
287             splice @found_file, 0, -1
288             if @found_file > 1;
289              
290             # here we try to rename
291 9         17 my $new_id = ( $found_file[ 0 ][ 1 ] + 1 ) & 0xffffffff;
292 9         16 my $new_file = $self -> full_filename( $self -> {encode_filename}( $new_id ) );
293              
294 9 50       416 if ( rename $found_file[ 0 ][ 0 ], $new_file ) {
295 9         61 $new_serialno = sprintf $self -> {serialno_format}, $new_id;
296             } else { # might be a parallel request
297 0 0       0 redo if $try++ < $self -> {max_tries};
298             }
299             } else { # nothing in found_file. must be the very first run. create new file and return id
300 3         5 my $id = $self -> {very_first_init}();
301              
302 3         7 my $fn = $self -> full_filename( $self -> {encode_filename}( $id ) );
303 3 50       210 open my $fh, '>', $fn
304             or die 'cannot create file ' . $fn . ': ' . $!;
305 3         34 print $fh $id; # save start value there for a history
306 3         82 close $fh;
307              
308 3         23 $new_serialno = sprintf $self -> {serialno_format}, $id;
309             }
310             }
311              
312 12         47 $new_serialno;
313             }
314              
315              
316             sub full_filename {
317 21 50   21 0 41 ref( my $self = shift ) or Carp::croak 'I am only an object method!';
318              
319 21         27 my $filename = shift;
320              
321 21         167 File::Spec -> catfile( $self -> {directory},
322             $filename,
323             );
324             }
325              
326             =head1 AUTHOR
327              
328             Valery Kalesnik, C<< >>
329              
330             =head1 BUGS
331              
332             Please report any bugs or feature requests to C, or through the web interface at L. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
333              
334             =head1 SUPPORT
335              
336             You can find documentation for this module with the perldoc command.
337              
338             perldoc FTN::Message::serialno::File
339              
340              
341             You can also look for information at:
342              
343             =over 4
344              
345             =item * RT: CPAN's request tracker (report bugs here)
346              
347             L
348              
349             =item * AnnoCPAN: Annotated CPAN documentation
350              
351             L
352              
353             =item * CPAN Ratings
354              
355             L
356              
357             =item * Search CPAN
358              
359             L
360              
361             =back
362              
363              
364             =head1 ACKNOWLEDGEMENTS
365              
366              
367             =head1 LICENSE AND COPYRIGHT
368              
369             Copyright 2014 Valery Kalesnik.
370              
371             This program is free software; you can redistribute it and/or modify it
372             under the terms of the the Artistic License (2.0). You may obtain a
373             copy of the full license at:
374              
375             L
376              
377             Any use, modification, and distribution of the Standard or Modified
378             Versions is governed by this Artistic License. By using, modifying or
379             distributing the Package, you accept this license. Do not use, modify,
380             or distribute the Package, if you do not accept this license.
381              
382             If your Modified Version has been derived from a Modified Version made
383             by someone other than you, you are nevertheless required to ensure that
384             your Modified Version complies with the requirements of this license.
385              
386             This license does not grant you the right to use any trademark, service
387             mark, tradename, or logo of the Copyright Holder.
388              
389             This license includes the non-exclusive, worldwide, free-of-charge
390             patent license to make, have made, use, offer to sell, sell, import and
391             otherwise transfer the Package with respect to any patent claims
392             licensable by the Copyright Holder that are necessarily infringed by the
393             Package. If you institute patent litigation (including a cross-claim or
394             counterclaim) against any party alleging that the Package constitutes
395             direct or contributory patent infringement, then this Artistic License
396             to you shall terminate on the date that such litigation is filed.
397              
398             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
399             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
400             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
401             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
402             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
403             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
404             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
405             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
406              
407             =cut
408              
409             1;