File Coverage

blib/lib/FTN/Outbound/Reference_file.pm
Criterion Covered Total %
statement 82 119 68.9
branch 31 84 36.9
condition 4 15 26.6
subroutine 12 13 92.3
pod 6 6 100.0
total 135 237 56.9


line stmt bran cond sub pod time code
1 2     2   101611 use strict;
  2         3  
  2         48  
2 2     2   8 use warnings;
  2         2  
  2         46  
3 2     2   920 use utf8;
  2         18  
  2         7  
4              
5             package FTN::Outbound::Reference_file;
6             $FTN::Outbound::Reference_file::VERSION = '20170409';
7              
8             # fts-5005.002 BinkleyTerm Style Outbound
9              
10             # Reference files consist of a number of lines (terminated by 0x0a or 0x0d,0x0a) each consisting of the name of the file to transfer to the remote system.
11              
12 2     2   712 use Log::Log4perl ();
  2         34342  
  2         31  
13 2     2   764 use Encode::Locale ();
  2         12188  
  2         38  
14 2     2   11 use Encode ();
  2         3  
  2         2850  
15              
16             # use File::Basename ();
17              
18              
19             my @line_joiner = ( "\x0a",
20             "\x0d\x0a",
21             );
22              
23             my $prefix_re = qr/[-#^~!@]/; # fts-5005.002
24              
25             =head1 NAME
26              
27             FTN::Outbound::Reference_file - Object-oriented module for working with FTN reference files.
28              
29             =head1 VERSION
30              
31             version 20170409
32              
33             =head1 SYNOPSIS
34              
35             use Log::Log4perl ();
36             use Encode ();
37             use FTN::Outbound::Reference_file ();
38              
39             Log::Log4perl -> easy_init( $Log::Log4perl::INFO );
40              
41             my $reference_file = FTN::Outbound::Reference_file -> new( '/var/lib/ftn/outbound/fidonet/00010001.flo',
42             sub {
43             Encode::decode( 'cp866', shift );
44             },
45             sub {
46             Encode::encode( 'cp866', shift );
47             },
48             "\x0d\x0a",
49             );
50              
51             $reference_file
52             -> read_existing_file
53             -> push_reference( '#', '/tmp/file_to_transfer' )
54             -> write_file;
55              
56             =head1 DESCRIPTION
57              
58             FTN::Outbound::Reference_file module is for working with reference files in FTN following specifications from fts-5005.002 document.
59              
60             =head1 OBJECT CREATION
61              
62             =head2 new
63              
64             my $reference_file = FTN::Outbound::Reference_file -> new( 'filename',
65             sub {
66             Encode::decode( 'UTF-8', shift );
67             },
68             sub {
69             Encode::encode( 'UTF-8', shift );
70             },
71             chr( 0x0a ),
72             );
73              
74             First parameter is a filename as a character string.
75              
76             Second parameter is either undef (in case no reading from the file expected (means file does not exist)) or sub reference that takes octet string (read from the existing reference file) and returns character string. In simplest case does just decoding from some predefined character set used by your software. Also might do other transformations. For example if other software uses relative path, this is the place where you transform it to absolute path by some rules. Output result used only in memory processing and won't be written to the file.
77              
78             Third parameter is either undef (in case no updates expected) or sub reference that takes character string and returns octet stream that will be written to the file. Used only by push_reference method.
79              
80             Forth parameter defines line joiner as standard allows two of them. If not defined or omitted will be either figured out from existing file (if possible) or character with code 0x0a will be used.
81              
82             =cut
83              
84             sub new {
85 2     2 1 3022 my $logger = Log::Log4perl -> get_logger( __PACKAGE__ );
86              
87 2 50       222 ref( my $class = shift ) and $logger -> logcroak( "I'm only a class method!" );
88              
89 2         3 my ( $reference_file,
90             $reference_file_read_line_transform_sub,
91             $reference_file_write_line_transform_sub,
92             $line_joiner,
93             ) = @_;
94              
95 2 50       6 $logger -> logdie( 'reference file name cannot be undefined' )
96             unless defined $reference_file;
97              
98 2         11 $logger -> debug( sprintf 'reference file name %s',
99             $reference_file,
100             );
101              
102 2         17 my %self = ( reference_file => $reference_file );
103              
104 2 50 33     17 $logger -> logdie( 'not valid reference file read line transform subroutine reference was passed as second argument' )
105             if defined $reference_file_read_line_transform_sub
106             && ref $reference_file_read_line_transform_sub ne 'CODE';
107              
108 2 50       12 $logger -> debug( sprintf 'reference file read line transform sub reference was%s passed',
109             defined $reference_file_read_line_transform_sub ?
110             ''
111             : ' not'
112             );
113              
114 2 50       13 $self{reference_file_read_line_transform_sub} = $reference_file_read_line_transform_sub
115             if defined $reference_file_read_line_transform_sub;
116              
117 2 50 66     8 $logger -> logdie( 'not valid reference file write line transform subroutine reference was passed as third argument' )
118             if defined $reference_file_write_line_transform_sub
119             && ref $reference_file_write_line_transform_sub ne 'CODE';
120              
121 2 100       10 $logger -> debug( sprintf 'reference file write line transform sub reference was%s passed',
122             defined $reference_file_write_line_transform_sub ?
123             ''
124             : ' not'
125             );
126              
127 2 100       10 $self{reference_file_write_line_transform_sub} = $reference_file_write_line_transform_sub
128             if defined $reference_file_write_line_transform_sub;
129              
130 2 100       4 if ( defined $line_joiner ) {
131 1 50       5 $logger -> logdie( 'incorrect line joiner: ', $line_joiner )
132             unless grep $line_joiner eq $_, @line_joiner;
133              
134 1         2 $self{line_joiner} = $line_joiner;
135             } else {
136 1         4 $logger -> debug( 'line joiner undefined' );
137             }
138              
139 2         10 bless \ %self, $class;
140             }
141              
142             sub _file_info {
143 1     1   2 my $filename = shift;
144 1         1 my $hashref = shift;
145              
146 1         3 my $logger = Log::Log4perl -> get_logger( __PACKAGE__ );
147              
148 1         14 $hashref -> {full_name} = $filename;
149             # $hashref -> {name} = File::Basename::basename( $filename );
150              
151 1 50       4 if ( -e Encode::encode( locale_fs => $filename ) ) {
152 1 50       34 if ( -f _ ) {
153 1         18 $hashref -> {size} = -s _;
154 1         7 $hashref -> {mstat} = ( stat _ )[ 9 ];
155             } else {
156 0         0 $logger -> warn( sprintf 'referenced file %s is not actually a file',
157             $filename,
158             );
159             }
160             } else {
161 0         0 $logger -> warn( sprintf 'referenced file %s does not exist',
162             $filename,
163             );
164             }
165             }
166              
167             =head1 FILE READ/WRITE
168              
169             =head2 read_existing_file
170              
171             Method for explicit reading of existing file. If file exists, this method has not been called and you're trying to update or write file it will be called implicitly before that.
172              
173             Does not expect any arguments.
174              
175             If file exists and isn't empty it will be read and each line will be passed to the sub reference which was passed as second parameter to the constructor.
176              
177             Returns itself for method chaining.
178              
179             =cut
180              
181             sub read_existing_file {
182 1     1 1 380 my $logger = Log::Log4perl -> get_logger( __PACKAGE__ );
183              
184 1 50       20 ref( my $self = shift ) or $logger -> logcroak( "I'm only an object method!" );
185              
186 1         7 my $reference_file_fs = Encode::encode( locale_fs => $self -> {reference_file} );
187              
188 1 50       119 if ( -e $reference_file_fs ) {
189             $logger -> logdie( sprintf '% is not a file',
190             $self -> {reference_file}
191             )
192 0 0       0 unless -f _;
193              
194 0 0       0 if ( -s _ ) { # non empty file
195             $logger -> logdie( 'reference file exists, but reference file read line transform subroutine reference needed for reading its content was not provided to constructor' )
196 0 0       0 unless exists $self -> {reference_file_read_line_transform_sub};
197              
198             $logger -> logdie( sprintf 'reference file %s is not readable',
199             $self -> {reference_file},
200             )
201 0 0       0 unless -r _;
202              
203             open my $fh, '<', $reference_file_fs
204             or $logger -> logdie( sprintf 'cannot open file %s for reading: %s',
205             $self -> {reference_file},
206 0 0       0 $!,
207             );
208 0         0 binmode $fh;
209              
210 0         0 my $read_result = read $fh, ( my $t ), -s _;
211              
212             $logger -> logdie( sprintf 'reading from %s failed: %s',
213             $self -> {reference_file},
214 0 0       0 $!,
215             )
216             unless defined $read_result;
217              
218             $logger -> logdie( sprintf 'errors while reading %s: expected to read %d bytes, but read %d',
219             $self -> {reference_file},
220 0 0       0 -s _,
221             $read_result,
222             )
223             unless $read_result == -s _;
224              
225             $self -> {line_joiner} = $line_joiner[ 1 ]
226             unless exists $self -> {line_joiner}
227 0 0 0     0 || -1 == index $t, $line_joiner[ 1 ];
228              
229 0         0 for my $l ( split /\x0d?\x0a/, $t ) { # Reference files consist of a number of lines (terminated by 0x0a or 0x0d,0x0a) each consisting of the name of the file to transfer to the remote system.
230 0         0 $logger -> debug( sprintf 'read octet line from reference file: %s',
231             $l,
232             );
233              
234             my %referenced_file = ( octet_line_in_reference_file => $l,
235 0         0 character_line_in_reference_file => $self -> {reference_file_read_line_transform_sub} -> ( $l ),
236             );
237              
238 0         0 my $full_name = $referenced_file{character_line_in_reference_file};
239 0 0       0 $referenced_file{prefix} = $1
240             if $full_name =~ s/^($prefix_re)//; # fts-5005.002
241              
242 0         0 _file_info( $full_name, \ %referenced_file );
243              
244 0         0 push @{ $self -> {referenced_files} },
  0         0  
245             \ %referenced_file;
246             }
247 0         0 close $fh;
248             } else { # file is empty
249 0         0 $self -> {referenced_files} = [];
250             }
251             } else { # file does not exist
252 1         7 $self -> {referenced_files} = [];
253             }
254              
255 1         5 $self;
256             }
257              
258             =head2 write_file
259              
260             Method for writing content from memory to the file. Does not need any parameters.
261             If file exists and its content in memory is empty, it will be deleted.
262              
263             Returns itself for method chaining.
264              
265             =cut
266              
267             sub write_file {
268 1     1 1 10 my $logger = Log::Log4perl -> get_logger( __PACKAGE__ );
269              
270 1 50       35 ref( my $self = shift ) or $logger -> logcroak( "I'm only an object method!" );
271              
272             $self -> read_existing_file
273 1 50       4 unless exists $self -> {referenced_files};
274              
275 1         4 my $reference_file_fs = Encode::encode( locale_fs => $self -> {reference_file} );
276              
277 1 50       32 if ( @{ $self -> {referenced_files} } ) { # update the file
  1 0       4  
278             # simple overwriting for now. later try File::Temp for new file and then File::Copy::move for moving over existing one
279             my $line_joiner = exists $self -> {line_joiner} ?
280             $self -> {line_joiner}
281 1 50       5 : $line_joiner[ 0 ];
282              
283             open my $fh, '>', $reference_file_fs
284             or $logger -> logdie( sprintf 'cannot open %s: %s',
285             $self -> {reference_file},
286 1 50       77 $!,
287             );
288              
289 1         4 binmode $fh;
290              
291             print $fh join $line_joiner,
292             map $_ -> {octet_line_in_reference_file},
293 1         3 @{ $self -> {referenced_files} };
  1         16  
294              
295 1         45 close $fh;
296              
297             } elsif ( -e $reference_file_fs ) { # remove the file as it's empty
298 0         0 $logger -> debug( 'removing empty ', $self -> {reference_file} );
299              
300             unlink $self -> {reference_file}
301             or $logger -> logdie( sprintf 'could not unlink %s: %s',
302             $self -> {reference_file},
303 0 0       0 $!,
304             );
305             }
306              
307 1         4 $self;
308             }
309              
310             =head1 CONTENT ACCESS
311              
312             =head2 referenced_files
313              
314             Returns list of hash references describing referenced files in list content.
315             In scalar content returns array reference.
316              
317             Each hash has fields:
318              
319             octet_line_in_reference_file - original line from the file or result returned by third parameter (sub reference) for constructor during push_reference method call. This is the value that will be written by write_file method call
320              
321             character_line_in_reference_file - line that was returned by second parameter (sub reference) for constructor during existing file read or possibly prefixed second argument for push_reference
322              
323             full_name - character line without prefix
324              
325             There might be other fields:
326              
327             prefix - if there is one
328              
329             size - size in bytes if file existed during read_existing_file or push_reference method call
330              
331             mstat - last modify time in seconds since the epoch if file existed during read_existing_file or push_reference method call
332              
333             =cut
334              
335             sub referenced_files {
336 1     1 1 356 my $logger = Log::Log4perl -> get_logger( __PACKAGE__ );
337              
338 1 50       27 ref( my $self = shift ) or $logger -> logcroak( "I'm only an object method!" );
339              
340             $self -> read_existing_file
341 1 50       4 unless exists $self -> {referenced_files};
342              
343             wantarray ?
344 1         4 @{ $self -> {referenced_files} }
345 1 50       3 : $self -> {referenced_files};
346             }
347              
348             =head1 CONTENT MODIFICATION
349              
350             =head2 process_lines
351              
352             Method expects one parameter - function reference. That function will be called for each line in reference file with one parameter - hash reference with all details about the referenced file.
353             Function can change/update fields - they are actual values, not a copy.
354              
355             Function return value is very important.
356             If it is false then this line will be removed from the memory and after write_file call from the actual file.
357             If return value is true then line stays.
358              
359             Method returns number of lines removed.
360              
361             =cut
362              
363             sub process_lines {
364 0     0 1 0 my $logger = Log::Log4perl -> get_logger( __PACKAGE__ );
365              
366 0 0       0 ref( my $self = shift ) or $logger -> logcroak( "I'm only an object method!" );
367              
368 0         0 my $sub_ref = shift; # gets line hash ref and should return boolean (keep the line or not)
369              
370 0 0 0     0 $logger -> logdie( 'not valid condition subroutine reference was passed' )
371             unless defined $sub_ref
372             && ref $sub_ref eq 'CODE';
373              
374             $self -> read_existing_file
375 0 0       0 unless exists $self -> {referenced_files};
376              
377             my @idx_to_remove = grep ! $sub_ref -> ( $self -> {referenced_files}[ $_ ] ),
378 0         0 0 .. $#{ $self -> {referenced_files} };
  0         0  
379              
380 0         0 for ( reverse @idx_to_remove ) {
381             $logger -> info( sprintf 'remove %s from %s',
382             $self -> {referenced_files}[ $_ ]{full_name},
383             $self -> {reference_file},
384 0         0 );
385 0         0 splice @{ $self -> {referenced_files} }, $_, 1;
  0         0  
386             }
387              
388 0         0 scalar @idx_to_remove;
389             }
390              
391             =head2 push_reference
392              
393             Expects referenced filename as a character string. If prefix [-#^~!@] needed, it should be defined as first parameter and filename as second parameter.
394              
395             Returns itself for method chaining.
396              
397             =cut
398              
399             sub push_reference {
400 1     1 1 10 my $logger = Log::Log4perl -> get_logger( __PACKAGE__ );
401              
402 1 50       18 ref( my $self = shift ) or $logger -> logcroak( "I'm only an object method!" );
403              
404             $logger -> logdie( 'reference file write line transform subroutine reference needed for update was not passed to constructor' )
405 1 50       9 unless exists $self -> {reference_file_write_line_transform_sub};
406              
407             $self -> read_existing_file
408 1 50       4 unless exists $self -> {referenced_files};
409              
410 1 50       3 my ( $prefix, $filename ) = ( @_ == 1 ? undef : (),
411             @_,
412             );
413              
414 1 50 33     10 $logger -> logdie( 'Incorrect prefix: ' . $prefix )
415             if defined $prefix
416             && $prefix !~ m/$prefix_re/;
417              
418 1         2 my %new;
419              
420 1         3 _file_info( $filename, \ %new );
421              
422 1 50       3 if ( defined $prefix ) {
423 1         2 $new{prefix} = $prefix;
424 1         3 $new{character_line_in_reference_file} = $prefix . $filename;
425             } else {
426 0         0 $new{character_line_in_reference_file} = $filename;
427             }
428 1         4 $new{octet_line_in_reference_file} = $self -> {reference_file_write_line_transform_sub} -> ( $new{character_line_in_reference_file} );
429              
430 1         2295 push @{ $self -> {referenced_files} },
  1         4  
431             \ %new;
432              
433 1         5 $self;
434             }
435              
436             1;