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 2 100.0
total 127 171 74.2


line stmt bran cond sub pod time code
1             package FTN::Message::serialno::File;
2              
3 2     2   18220 use strict;
  2         4  
  2         74  
4 2     2   8 use warnings FATAL => 'all';
  2         4  
  2         80  
5              
6 2     2   10 use File::Spec ();
  2         3  
  2         39  
7              
8 2     2   1025 use parent 'FTN::Message::serialno';
  2         522  
  2         10  
9              
10             use constant {
11 2         1834 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   99 };
  2         2  
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 20141121
27              
28             =cut
29              
30             our $VERSION = '20141121';
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   2 my $self = shift;
60              
61 3         8 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     40 die 'directory parameter should be defined!'
75             unless $param{directory}
76             && -d $param{directory};
77              
78 3         80 $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       8 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     14 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     57 $self -> {filename_format} = $param{filename_format} || FILENAME_FORMAT;
112 3 50 33     13 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         7 $self -> {filename_format} .= '.' . $extension;
121             }
122 3         47 $self -> {filename_pattern} = qr/$pattern/;
123              
124              
125             # validate that filename_pattern will match filename_format
126 3         13 my $t = sprintf $self -> {filename_format}, 1;
127 3 50       20 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   213 shift =~ m/$self->{filename_pattern}/?
136             ( $1 . '.' . $extension,
137             hex( $1 )
138             )
139             : ();
140 3 50 33     19 };
141              
142             # encode filename
143             $self -> {encode_filename} = $param{encode_filename} && ref $param{encode_filename} eq 'CODE'?
144             $param{encode_filename}
145             : sub {
146 15     15   65 sprintf $self -> {filename_format}, shift;
147 3 50 33     15 };
148              
149 3 50       7 die 'incorrect decode_filename and/or encode_filename'
150             unless $self -> {decode_filename}( $self -> {encode_filename}( 1 ) ) == 1;
151              
152              
153             =item * max_tries
154              
155             Defines how many times renaming of the file is tried before it is considered failed.
156              
157             Default value is 5.
158              
159             =cut
160              
161             # max_tries for renaming
162 3 50 33     12 $self -> {max_tries} = $param{max_tries} && $param{max_tries} =~ m/^(\d+)$/?
163             $1
164             : MAX_TRIES;
165              
166              
167             =item * very_first_init
168              
169             Defines reference to a function for generating very first serialno value in case there are no matching files in the directory.
170              
171             The possible values:
172              
173             =over 6
174              
175             =item * CURRENT_UNIXTIME
176              
177             use current unixtime as a starting value.
178              
179             FTN::Message::serialno::File -> new( directory => '/some/dir',
180             very_first_init => 'CURRENT_UNIXTIME',
181             );
182            
183              
184             =item * CURRENT_UNIXTIME_MINUS_3_YEARS
185              
186             use current unixtime minus 3 years as a starting value.
187              
188             =item * user defined function
189              
190             FTN::Message::serialno::File -> new( directory => '/some/dir',
191             very_first_init => sub {
192             42; # voices in my head tell me to use 42
193             },
194             );
195              
196             =back
197              
198             Default value is function returning 1.
199              
200             =cut
201              
202             # very_first_init
203 3 100       6 if ( $param{very_first_init} ) {
204 1 50       4 if ( ref $param{very_first_init} eq 'CODE' ) {
    0          
    0          
205 1         2 $self -> {very_first_init} = $param{very_first_init};
206             } elsif ( $param{very_first_init} eq 'CURRENT_UNIXTIME_MINUS_3_YEARS' ) {
207 0     0   0 $self -> {very_first_init} = sub { time - 3 * 365 * 24 * 60 * 60 };
  0         0  
208             } elsif ( $param{very_first_init} eq 'CURRENT_UNIXTIME' ) {
209 0     0   0 $self -> {very_first_init} = sub { time; };
  0         0  
210             }
211             }
212              
213 2     2   2 $self -> {very_first_init} = sub { 1; }
214 3 100       11 unless $self -> {very_first_init};
215              
216             =item * serialno_format
217              
218             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.
219              
220             Default value is '%08x'.
221              
222             =back
223              
224             =cut
225              
226 3   100     17 $self -> {serialno_format} = $param{serialno_format} || SERIALNO_FORMAT;
227             }
228              
229              
230             sub new {
231 3 50   3 1 1634 ref( my $class = shift ) and Carp::croak 'I am only a class method!';
232              
233 3         17 my $self = $class -> SUPER::new( @_ );
234              
235 3         6 _initialize( $self, @_ ); # not $self -> _initialize!
236              
237 3         10 $self;
238             }
239              
240              
241             =head2 get_serialno()
242              
243             Method that does all the work and returns either new valid serialno value or undef.
244              
245             If this is the very first run and no signs of previous serialno values, creates new file with starting value and returns it.
246              
247             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.
248              
249             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.
250              
251             If renaming fails more than allowed count, undef is returned.
252              
253             =cut
254              
255             sub get_serialno {
256 12 50   12 1 2924 ref( my $self = shift ) or Carp::croak 'I am only an object method!';
257              
258 12         25 my $try = 1;
259 12         11 my $new_serialno;
260              
261             {
262 12 50       8 opendir my $dh, $self -> {directory}
  12         57  
263             or die "can't opendir $self->{directory}: $!";
264              
265 0         0 my @found_file = sort { $a -> [ 1 ] <=> $b -> [ 1 ] }
  33         55  
266             map {
267 12         464 my $filename = $_;
268 33         29 my @r;
269              
270 33         54 my @t = $self -> {decode_filename}( $filename );
271              
272 33 100       55 if ( @t ) {
273             # my $full_name = $self -> _full_filename( $filename ); tainted on win
274 9         20 my $full_name = $self -> _full_filename( $t[ 0 ] );
275              
276 9 50       206 push @r,
277             [ $full_name,
278             $t[ 1 ],
279             ]
280             if -f $full_name;
281             }
282              
283 33         68 @r;
284             } readdir $dh;
285              
286 12         75 closedir $dh;
287              
288 12 100       21 if ( @found_file ) {
289             # as old numbers can be deleted by other process, we don't care about errors here, so let's do it in one shot
290 9 50       17 unlink map $_ -> [ 0 ],
291             splice @found_file, 0, -1
292             if @found_file > 1;
293              
294             # here we try to rename
295 9         14 my $new_id = ( $found_file[ 0 ][ 1 ] + 1 ) & 0xffffffff;
296 9         16 my $new_file = $self -> _full_filename( $self -> {encode_filename}( $new_id ) );
297              
298 9 50       415 if ( rename $found_file[ 0 ][ 0 ], $new_file ) {
299 9         62 $new_serialno = sprintf $self -> {serialno_format}, $new_id;
300             } else { # might be a parallel request
301 0 0       0 redo if $try++ < $self -> {max_tries};
302             }
303             } else { # nothing in found_file. must be the very first run. create new file and return id
304 3         7 my $id = $self -> {very_first_init}();
305              
306 3         6 my $fn = $self -> _full_filename( $self -> {encode_filename}( $id ) );
307 3 50       185 open my $fh, '>', $fn
308             or die 'cannot create file ' . $fn . ': ' . $!;
309 3         33 print $fh $id; # save start value there for a history
310 3         73 close $fh;
311              
312 3         22 $new_serialno = sprintf $self -> {serialno_format}, $id;
313             }
314             }
315              
316 12         42 $new_serialno;
317             }
318              
319              
320             sub _full_filename {
321 21 50   21   64 ref( my $self = shift ) or Carp::croak 'I am only an object method!';
322              
323 21         16 my $filename = shift;
324              
325 21         146 File::Spec -> catfile( $self -> {directory},
326             $filename,
327             );
328             }
329              
330             =head1 AUTHOR
331              
332             Valery Kalesnik, C<< >>
333              
334             =head1 BUGS
335              
336             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.
337              
338             =head1 SUPPORT
339              
340             You can find documentation for this module with the perldoc command.
341              
342             perldoc FTN::Message::serialno::File
343              
344              
345             You can also look for information at:
346              
347             =over 4
348              
349             =item * RT: CPAN's request tracker (report bugs here)
350              
351             L
352              
353             =item * AnnoCPAN: Annotated CPAN documentation
354              
355             L
356              
357             =item * CPAN Ratings
358              
359             L
360              
361             =item * Search CPAN
362              
363             L
364              
365             =back
366              
367              
368             =head1 ACKNOWLEDGEMENTS
369              
370              
371             =head1 LICENSE AND COPYRIGHT
372              
373             Copyright 2014 Valery Kalesnik.
374              
375             This program is free software; you can redistribute it and/or modify it
376             under the terms of the the Artistic License (2.0). You may obtain a
377             copy of the full license at:
378              
379             L
380              
381             Any use, modification, and distribution of the Standard or Modified
382             Versions is governed by this Artistic License. By using, modifying or
383             distributing the Package, you accept this license. Do not use, modify,
384             or distribute the Package, if you do not accept this license.
385              
386             If your Modified Version has been derived from a Modified Version made
387             by someone other than you, you are nevertheless required to ensure that
388             your Modified Version complies with the requirements of this license.
389              
390             This license does not grant you the right to use any trademark, service
391             mark, tradename, or logo of the Copyright Holder.
392              
393             This license includes the non-exclusive, worldwide, free-of-charge
394             patent license to make, have made, use, offer to sell, sell, import and
395             otherwise transfer the Package with respect to any patent claims
396             licensable by the Copyright Holder that are necessarily infringed by the
397             Package. If you institute patent litigation (including a cross-claim or
398             counterclaim) against any party alleging that the Package constitutes
399             direct or contributory patent infringement, then this Artistic License
400             to you shall terminate on the date that such litigation is filed.
401              
402             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
403             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
404             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
405             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
406             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
407             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
408             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
409             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
410              
411             =cut
412              
413             1;