File Coverage

blib/lib/FTN/Outbound/BSO.pm
Criterion Covered Total %
statement 90 281 32.0
branch 33 208 15.8
condition 14 114 12.2
subroutine 14 21 66.6
pod 6 6 100.0
total 157 630 24.9


line stmt bran cond sub pod time code
1             package FTN::Outbound::BSO;
2             $FTN::Outbound::BSO::VERSION = '20180823';
3              
4 3     3   243197 use strict;
  3         20  
  3         75  
5 3     3   11 use warnings;
  3         5  
  3         61  
6 3     3   1913 use utf8;
  3         35  
  3         12  
7              
8             # fts-5005.003 BinkleyTerm Style Outbound
9             # except s/Continuous/Crash/g
10              
11 3     3   2272 use Log::Log4perl ();
  3         127360  
  3         68  
12 3     3   27 use Scalar::Util ();
  3         5  
  3         57  
13 3     3   1401 use Encode::Locale ();
  3         37499  
  3         59  
14 3     3   15 use Encode ();
  3         6  
  3         41  
15 3     3   11 use File::Spec ();
  3         4  
  3         43  
16 3     3   11 use Fcntl ();
  3         4  
  3         29  
17 3     3   1370 use FTN::Addr ();
  3         12044  
  3         54  
18 3     3   1475 use FTN::Outbound::Reference_file ();
  3         5025  
  3         12580  
19              
20             my %flavour_extension = ( immediate => [ qw/ iut ilo / ], # Xut (netmail) Xlo (reference file) by fts-5005.003
21             # continuous => [ qw/ c c / ], # except this one
22             crash => [ qw/ cut clo / ],
23             direct => [ qw/ dut dlo / ],
24             normal => [ qw/ out flo / ],
25             hold => [ qw/ hut hlo / ],
26             );
27             # 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.
28              
29             # file_type => extension. both keys and values should be unique in their sets
30             # content notes are from fts-5005.003
31             my %control_file_extension = ( file_request => 'req', # file requests
32             # The format of request files is documented in FTS-0006.
33             busy => 'bsy', # busy control file.
34             # may contain one line of PID information (less than 70 characters).
35             call => 'csy', # call control file
36             # may contain one line of PID information (less than 70 characters).
37             hold => 'hld', # hold control file
38             # must contain a one line string with the expiration of the hold period expressed in UNIX-time.
39             try => 'try', # try control file
40             # A try file (if implemented by a mailer) must contain the following:
41              
42             # NOK - Number of good connects, expressed as a 16-bit, big-endian integer.
43             # NBAD - Number of bad connects, expressed as a 16-bit, big-endian integer.
44             # CLength - Length of comment in bytes, expressed as an 8-bit unsigned integer.
45             # Comment - CLength bytes, detailing the results of the most recent connection attempt.
46              
47             # On completion of a successful session, NOK is incremented and NBAD is reset to zero.
48             # On completion of a failed session, NBAD is incremented.
49             # IF NBAD reaches the mailer's configured limit for failed sessions,
50             # the node is marked undialable, NOK and NBAD are reset to zero,
51             # and a hld control file is created in accordance with section 5.3.
52             );
53              
54             =head1 NAME
55              
56             FTN::Outbound::BSO - working with BinkleyTerm Style Outbound.
57              
58             =head1 VERSION
59              
60             version 20180823
61              
62             =head1 SYNOPSIS
63              
64             use Log::Log4perl ();
65             use Encode ();
66             use FTN::Outbound::BSO ();
67              
68             Log::Log4perl -> easy_init( $Log::Log4perl::INFO );
69              
70             my $bso = FTN::Outbound::BSO
71             -> new( outbound_root => '/var/lib/ftn/outbound',
72             domain => 'fidonet',
73             zone => 2,
74             domain_abbrev => { fidonet => '_out',
75             homenet => 'leftnet',
76             },
77             maximum_session_time => 3600, # one hour
78             ) or die 'cannot create bso object';
79              
80             my $addr = FTN::Addr -> new( '2:451/30' );
81              
82             sub poll {
83             my $addr = shift;
84             my $bso = shift;
85              
86             my $flo = $bso -> addr_file_to_change( $addr,
87             'reference_file',
88             'normal'
89             );
90              
91             open my $fh, '>>', $flo
92             or die sprintf 'cannot open %s: %s', $flo, $!;
93              
94             print $fh '';
95              
96             close $fh;
97             }
98              
99             $bso -> busy_protected_sub( $addr,
100             \ &poll,
101             );
102              
103             =head1 DESCRIPTION
104              
105             FTN::Outbound::BSO module is for working with BinkleyTerm Style Outbound in FTN following specifications from fts-5005.003 document. Figuring out correct file to process might be a tricky process: different casing, few our main domains, other differences. This module helps with this task.
106              
107             =head1 OBJECT CREATION
108              
109             =head2 new
110              
111             Expects parameters as hash:
112              
113             outbound_root - directory path as a character string where the whole outbound is located. Mandatory parameter. This directory should exist.
114              
115             By standard constructor needs our domain and zone. They can be provided as:
116              
117             our_addr - either FTN::Addr object representing our address or our address as a string which will be passed to FTN::Addr constructor.
118              
119             or as a pair:
120              
121             domain - domain part of our address as described in frl-1028.002.
122             zone - our zone in that domain
123              
124             At least one of the ways should be provided. In case both are our_addr has higher priority.
125              
126             domain_abbrev - hash reference where keys are known domains and values are directory names (without extension) in outbound_root for those domains. Mandatory parameter.
127              
128             reference_file_read_line_transform_sub - reference to a function that receives an octet string and returns a character string. Will be passed to FTN::Outbound::Reference_file constructor. If not provided reference file content won't be processed.
129              
130             maximum_session_time - maximum session time in seconds. If provided, all found busy files older than 2 * value will be removed during outbound scan.
131              
132             Returns newly created object on success.
133              
134             =cut
135              
136             sub new {
137 2     2 1 4697 my $logger = Log::Log4perl -> get_logger( __PACKAGE__ );
138              
139 2 50       691 ref( my $class = shift ) and $logger -> logcroak( "I'm only a class method!" );
140              
141             return
142 2 50       7 unless @_;
143              
144 2 50       4 $logger -> logdie( sprintf 'constructor expects even number of arguments, but received %d of them',
145             scalar @_,
146             )
147             if @_ % 2;
148              
149 2         10 my %option = @_;
150 2         4 my %self;
151              
152             # validating
153             # mandatory parameters
154             $logger -> logdie( 'mandatory outbound_root is not provided' )
155 2 50       6 unless exists $option{outbound_root};
156              
157             # outbound_root
158 2         9 my $outbound_root_fs = Encode::encode( locale_fs => $option{outbound_root} );
159              
160 2 50       274 unless ( -e $outbound_root_fs ) {
161             $logger -> warn( sprintf 'outbound_root (%s) directory does not exist',
162             $option{outbound_root},
163 0         0 );
164              
165 0         0 return;
166             }
167              
168 2 50       24 unless ( -d _ ) { # if it exists it should be a directory
169             $logger -> warn( sprintf 'outbound_root (%s) does not point to the directory',
170             $option{outbound_root},
171 0         0 );
172              
173 0         0 return;
174             }
175              
176 2         7 $self{outbound_root} = $option{outbound_root};
177 2         3 $self{outbound_root_fs} = $outbound_root_fs;
178              
179              
180             # our_addr or ( domain + zone )
181 2 50 33     15 if ( exists $option{our_addr}
182             && $option{our_addr}
183             ) {
184 0 0 0     0 if ( ref $option{our_addr}
      0        
185             && Scalar::Util::blessed $option{our_addr}
186             && $option{our_addr} -> isa( 'FTN::Addr' )
187             ) {
188 0         0 $self{our_addr} = $option{our_addr};
189             } else {
190             $self{our_addr} = FTN::Addr -> new( $option{our_addr} )
191             or $logger -> logdie( sprintf 'incorrect value of our_addr: %s',
192             $option{our_addr},
193 0 0       0 );
194             }
195 0         0 $self{domain} = $self{our_addr} -> domain;
196 0         0 $self{zone} = $self{our_addr} -> zone;
197             } else {
198             $logger -> logdie( 'domain is not provided' )
199             unless exists $option{domain}
200 2 50 33     13 && $option{domain};
201              
202             $logger -> logdie( sprintf 'domain (%s) is not valid',
203             $option{domain},
204             )
205 2 50       13 unless $option{domain} =~ m/^[a-z\d_~-]{1,8}$/; # frl-1028.002
206              
207             $logger -> logdie( 'zone is not provided' )
208             unless exists $option{zone}
209 2 50 33     22 && $option{zone};
210              
211             $logger -> logdie( sprintf 'zone (%s) is not valid',
212             $option{zone},
213             )
214             unless $option{zone} =~ m/^\d+$/ # FRL-1002.001, frl-1028.002
215             && 1 <= $option{zone} # FRL-1002.001, frl-1028.002
216 2 50 33     19 && $option{zone} <= 32767; # FRL-1002.001, frl-1028.002
      33        
217              
218 2         5 $self{domain} = $option{domain};
219 2         4 $self{zone} = $option{zone};
220             }
221              
222             # domain abbreviations
223 2 50 33     14 if ( exists $option{domain_abbrev}
      33        
224             && defined $option{domain_abbrev}
225             && ref $option{domain_abbrev} eq 'HASH'
226             ) {
227             $logger -> logdie( sprintf 'our domain (%s) is not in the passed domain_abbrev hash!',
228             $self{domain},
229             )
230 2 50       8 unless exists $option{domain_abbrev}{ $self{domain} };
231              
232 2         3 $self{domain_abbrev} = $option{domain_abbrev};
233             } else {
234 0         0 $logger -> logdie( 'no valid domain_abbrev provided' );
235             }
236              
237             # reference file read line transform sub
238 2 50       11 if ( exists $option{reference_file_read_line_transform_sub} ) {
239             $logger -> logdie( 'incorrect value of reference_file_read_line_transform_sub (should be a sub reference)' )
240             unless defined $option{reference_file_read_line_transform_sub}
241 0 0 0     0 && 'CODE' eq ref $option{reference_file_read_line_transform_sub};
242              
243 0         0 $self{reference_file_read_line_transform_sub} = $option{reference_file_read_line_transform_sub};
244             }
245              
246             # maximum_session_time
247 2 50       6 if ( exists $option{maximum_session_time} ) {
248             $logger -> logdie( sprintf 'incorrect value of maximum_session_time: %s',
249             defined $option{maximum_session_time} ?
250             $option{maximum_session_time}
251             : 'undef'
252             )
253             unless defined $option{maximum_session_time}
254             && $option{maximum_session_time} =~ m/^\d+$/
255 0 0 0     0 && $option{maximum_session_time}; # cannot be 0
    0 0        
256              
257 0         0 $self{maximum_session_time} = $option{maximum_session_time};
258             }
259              
260 2         10 bless \ %self, $class;
261             }
262              
263             sub _store {
264 0     0   0 my $logger = Log::Log4perl -> get_logger( __PACKAGE__ );
265              
266 0 0       0 ref( my $self = shift ) or $logger -> logcroak( "I'm only an object method!" );
267              
268 0         0 my ( $file_prop,
269             $ext,
270             $target,
271             $net,
272             $node,
273             $point,
274             ) = @_;
275              
276 0         0 my %ext_netmail_flavour = map { $flavour_extension{ $_ }[ 0 ] => $_ } keys %flavour_extension;
  0         0  
277 0         0 my %ext_reference_file_flavour = map { $flavour_extension{ $_ }[ 1 ] => $_ } keys %flavour_extension;
  0         0  
278 0         0 my %ext_control_file = reverse %control_file_extension;
279              
280 0         0 my $lc_ext = lc $ext;
281              
282 0 0       0 if ( exists $ext_netmail_flavour{ $lc_ext } ) { # netmail
    0          
    0          
283 0         0 push @{ $target -> { $net }{ $node }{ $point }{netmail}{ $ext_netmail_flavour{ $lc_ext } } },
  0         0  
284             $file_prop;
285             } elsif ( exists $ext_reference_file_flavour{ $lc_ext } ) { # reference file
286 0         0 my $flavour = $ext_reference_file_flavour{ $lc_ext };
287             # referenced files
288 0 0 0     0 if ( $file_prop -> {size} # empty files are empty, right?
289             && exists $self -> {reference_file_read_line_transform_sub}
290             ) {
291             $file_prop -> {referenced_files} =
292             FTN::Outbound::Reference_file
293             -> new( $file_prop -> {full_name},
294             $self -> {reference_file_read_line_transform_sub},
295             )
296 0         0 -> read_existing_file
297             -> referenced_files;
298             }
299              
300 0         0 push @{ $target -> { $net }{ $node }{ $point }{reference_file}{ $flavour } },
  0         0  
301             $file_prop;
302             } elsif ( exists $ext_control_file{ $lc_ext } ) {
303 0 0       0 my $age = $file_prop -> {mstat} ? time - $file_prop -> {mstat} : 0;
304 0 0 0     0 if ( $ext_control_file{ $lc_ext } eq 'busy'
      0        
305             && exists $self -> {maximum_session_time}
306             && $self -> {maximum_session_time} * 2 < $age
307             ) { # try to remove if maximum_session_time is defined and busy is older than it
308             $logger -> info( sprintf 'removing expired busy %s (%d seconds old)',
309             $file_prop -> {full_name},
310 0         0 $age,
311             );
312              
313             unlink Encode::encode( locale_fs => $file_prop -> {full_name} )
314             or $logger -> logdie( sprintf 'could not unlink %s: %s',
315             $file_prop -> {full_name},
316 0 0       0 $!,
317             );
318             } else {
319 0         0 push @{ $target -> { $net }{ $node }{ $point }{ $ext_control_file{ $lc_ext } } },
  0         0  
320             $file_prop;
321             }
322             }
323             }
324              
325             =head1 OBJECT METHODS
326              
327             =head2 scan
328              
329             Scans outbound for all known domains. Old busy files might be removed.
330              
331             Returns itself for chaining.
332              
333             =cut
334              
335             sub scan {
336 0     0 1 0 my $logger = Log::Log4perl -> get_logger( __PACKAGE__ );
337              
338 0 0       0 ref( my $self = shift ) or $logger -> logcroak( "I'm only an object method!" );
339              
340             $logger -> logdie( sprintf 'outbound_root (%s) directory does not exist',
341             $self -> {outbound_root},
342             )
343 0 0       0 unless -e $self -> {outbound_root_fs};
344              
345             # if it exists it should be a directory
346             $logger -> logdie( sprintf 'outbound_root (%s) does not point to the directory',
347             $self -> {outbound_root},
348             )
349 0 0       0 unless -d _;
350              
351             # check outbound_root for all domain abbrevs and zones
352 0         0 my $domain_abbr_re = join '|', values %{ $self -> {domain_abbrev} };
  0         0  
353 0         0 my %result;
354              
355             opendir my $or_dh, $self -> {outbound_root_fs}
356             or $logger -> logdie( sprintf 'cannot opendir %s: %s',
357             $self -> {outbound_root},
358 0 0       0 $!,
359             );
360              
361 0         0 while ( my $dz_out = readdir $or_dh ) { # looking for domain abbreviations directories
362 0         0 $dz_out = Encode::decode( locale_fs => $dz_out );
363              
364             next # skipping hidden files and ..
365 0 0       0 if '.' eq substr $dz_out, 0, 1;
366              
367             my $dir_name = File::Spec -> catdir( $self -> {outbound_root},
368 0         0 $dz_out,
369             );
370              
371 0         0 my $dir_name_fs = Encode::encode( locale_fs => $dir_name );
372              
373             next # looking only for directories
374 0 0       0 unless -d $dir_name_fs;
375              
376             # our_domain_dir, our_domain_dir.7fff, other_domain.1999
377             next
378             unless $dz_out =~ /^($domain_abbr_re)(?:\.([1-7]?[0-9a-f]{3}))?$/i
379             && ( $1 eq $self -> {domain_abbrev}{ $self -> {domain} }
380 0 0 0     0 || defined $2
      0        
381             );
382              
383             my ( $domain ) = grep $self -> {domain_abbrev}{ $_ } eq $1,
384 0         0 keys %{ $self -> {domain_abbrev} };
  0         0  
385              
386 0 0       0 my $zone = defined $2 ? hex $2 : $self -> {zone};
387              
388             next
389 0 0 0     0 unless 1 <= $zone && $zone <= 32767; # FRL-1002.001, frl-1028.002
390              
391 0         0 $logger -> debug( sprintf 'directory %s matches. domain: %s zone: %s',
392             $dz_out,
393             $domain,
394             $zone,
395             );
396              
397 0         0 $result{ $domain }{ $zone }{ $dz_out }{dir} = $dir_name;
398              
399             # now let's traverse found domain_abbrev[.zone] dir
400 0 0       0 opendir my $dz_dh, $dir_name_fs
401             or $logger -> logdie( sprintf 'cannot opendir %s: %s',
402             $dir_name,
403             $!,
404             );
405              
406 0         0 while ( my $dir_entry = readdir $dz_dh ) {
407 0         0 $dir_entry = Encode::decode( locale_fs => $dir_entry );
408              
409             next
410 0 0       0 unless $dir_entry =~ m/^([0-9a-f]{4})([0-9a-f]{4})\.(.+)$/i;
411              
412 0         0 my ( $net, $node ) = map hex, $1, $2;
413 0         0 my $ext = $3;
414              
415 0         0 my $full_name = File::Spec -> catfile( $dir_name,
416             $dir_entry,
417             );
418              
419 0         0 my $full_name_fs = Encode::encode( locale_fs => $full_name );
420              
421 0 0 0     0 if ( lc( $ext ) eq 'pnt'
    0          
422             && -d $full_name_fs
423             ) { # points subdir
424 0         0 $logger -> debug( sprintf 'found %s#%d:%d/%d points subdirectory %s',
425             $domain,
426             $zone,
427             $net,
428             $node,
429             $full_name,
430             );
431              
432 0         0 $result{ $domain }{ $zone }{ $dz_out }{ $net }{ $node }{points_dir}{ $dir_entry } = $full_name; # might be different hex casing for net/node or extension
433              
434 0 0       0 opendir my $p_dh, $full_name_fs
435             or $logger -> logdie( sprintf 'cannot opendir %s: %s',
436             $full_name,
437             $!,
438             );
439              
440 0         0 while ( my $file = readdir $p_dh ) {
441 0         0 $file = Encode::decode( locale_fs => $file );
442              
443             next
444 0 0       0 unless $file =~ m/^([0-9a-f]{8})\.(.+)$/i;
445              
446 0         0 my $point = hex $1;
447 0         0 my $ext = $2;
448              
449 0         0 my $full_name = File::Spec -> catfile( $full_name,
450             $file,
451             );
452              
453             next # in points dir we're interested in files only
454 0 0       0 unless -f Encode::encode( locale_fs => $full_name );
455              
456             $self -> _store( { name => $file,
457             full_name => $full_name,
458             size => -s _,
459             mstat => ( stat _ )[ 9 ],
460             },
461             $ext,
462 0         0 $result{ $domain }{ $zone }{ $dz_out },
463             $net,
464             $node,
465             $point,
466             );
467             }
468 0         0 closedir $p_dh;
469             } elsif ( -f $full_name_fs ) { # node related file
470             $self -> _store( { name => $_,
471             full_name => $full_name,
472             size => -s _,
473             mstat => ( stat _ )[ 9 ],
474             },
475             $ext,
476 0         0 $result{ $domain }{ $zone }{ $dz_out },
477             $net,
478             $node,
479             0, # point
480             );
481             }
482             }
483 0         0 closedir $dz_dh;
484              
485             }
486 0         0 closedir $or_dh;
487              
488 0         0 $self -> {scanned} = \ %result;
489              
490 0         0 $self;
491             }
492              
493             =head2 scanned_hash
494              
495             Returns internal structure representing scanned outbound (hash in list context, hashref in scalar context). If scan method hasn't been called, it will be called implicitly by this method.
496              
497             =cut
498              
499             sub scanned_hash {
500 0     0 1 0 my $logger = Log::Log4perl -> get_logger( __PACKAGE__ );
501              
502 0 0       0 ref( my $self = shift ) or $logger -> logcroak( "I'm only an object method!" );
503              
504             $self -> scan
505 0 0       0 unless exists $self -> {scanned};
506              
507             wantarray ?
508 0         0 %{ $self -> {scanned} }
509 0 0       0 : $self -> {scanned};
510             }
511              
512              
513             sub _validate_addr {
514 0     0   0 my $logger = Log::Log4perl -> get_logger( __PACKAGE__ );
515              
516 0 0       0 ref( my $self = shift ) or $logger -> logcroak( "I'm only an object method!" );
517              
518 0         0 my $addr = shift;
519              
520 0 0 0     0 $logger -> logdie( 'no valid address passed' )
      0        
      0        
521             unless defined $addr
522             && ref $addr
523             && Scalar::Util::blessed $addr
524             && $addr -> isa( 'FTN::Addr' );
525              
526             $logger -> logdie( 'passed address has unknown domain: %s',
527             $addr -> domain,
528             )
529 0 0       0 unless exists $self -> {domain_abbrev}{ $addr -> domain };
530              
531 0         0 $addr;
532             }
533              
534             =head2 is_busy
535              
536             Expects one parameter - address as FTN::Addr object. Returns true if that address is busy (connection session, mail processing, ...).
537              
538             =cut
539              
540             sub is_busy {
541 0     0 1 0 my $logger = Log::Log4perl -> get_logger( __PACKAGE__ );
542              
543 0 0       0 ref( my $self = shift ) or $logger -> logcroak( "I'm only an object method!" );
544              
545 0         0 my $addr = $self -> _validate_addr( shift );
546              
547             $self -> scan
548 0 0       0 unless exists $self -> {scanned};
549              
550             exists $self -> {scanned}{ $addr -> domain }
551             && exists $self -> {scanned}{ $addr -> domain }{ $addr -> zone }
552             && grep { exists $self -> {scanned}{ $addr -> domain }{ $addr -> zone }{ $_ }{ $addr -> net }
553             && exists $self -> {scanned}{ $addr -> domain }{ $addr -> zone }{ $_ }{ $addr -> net }{ $addr -> node }
554             && exists $self -> {scanned}{ $addr -> domain }{ $addr -> zone }{ $_ }{ $addr -> net }{ $addr -> node }{ $addr -> point }
555             && exists $self -> {scanned}{ $addr -> domain }{ $addr -> zone }{ $_ }{ $addr -> net }{ $addr -> node }{ $addr -> point }{busy}
556 0 0 0     0 } keys %{ $self -> {scanned}{ $addr -> domain }{ $addr -> zone } };
  0 0 0     0  
  0   0     0  
557             }
558              
559             sub _select_domain_zone_dir { # best one. for updating. for checking needs a list (another method or direct access to the structure)
560             # and makes one if it doesn't exist or isn't good enough (e.g. our_domain_abbr.our_zone)
561 15     15   503 my $logger = Log::Log4perl -> get_logger( __PACKAGE__ );
562              
563 15 50       319 ref( my $self = shift ) or $logger -> logcroak( "I'm only an object method!" );
564              
565 15         17 my $domain = shift;
566 15         17 my $zone = shift;
567              
568             $logger -> logdie( 'unknown domain: %s',
569             $domain,
570             )
571 15 50       26 unless exists $self -> {domain_abbrev}{ $domain };
572              
573 15         18 my $best_match = $self -> {domain_abbrev}{ $domain };
574              
575             $best_match .= sprintf( '.%03x', $zone )
576             unless $domain eq $self -> {domain}
577 15 100 66     89 && $zone == $self -> {zone};
578              
579             $self -> scan
580 15 50       25 unless exists $self -> {scanned};
581              
582 15 50 33     106 if ( exists $self -> {scanned}{ $domain }
      66        
      33        
583             && exists $self -> {scanned}{ $domain }{ $zone }
584             && ( $domain ne $self -> {domain} # other domains have zones in extensions
585             || $zone != $self -> {zone} # other zones in our domain have zones in extensions
586             || grep length( $_ ) == length( $best_match ),
587             keys %{ $self -> {scanned}{ $domain }{ $zone } }
588             )
589             ) {
590 37 50       81 my @list = sort { length $a <=> length $b
591             || $b cmp $a # we prefer lower case
592             }
593 15         15 keys %{ $self -> {scanned}{ $domain }{ $zone } };
  15         55  
594              
595 15         36 my ( $t ) = grep $_ eq $best_match, @list; # might be exact case
596              
597 15 100       34 $best_match = defined $t ?
598             $t
599             : $list[ 0 ]; # if didn't find the best match, use the very first existing one
600             } else { # need to create
601             my $dir_full_name = File::Spec -> catdir( $self -> {outbound_root},
602 0         0 $best_match,
603             );
604              
605 0         0 $logger -> debug( sprintf 'creating directory for domain %s zone %d: %s',
606             $domain,
607             $zone,
608             $dir_full_name,
609             );
610              
611 0 0       0 mkdir Encode::encode( locale_fs => $dir_full_name )
612             or $logger -> logdie( sprintf 'cannot create domain/zone %s directory: %s',
613             $dir_full_name,
614             $!,
615             );
616              
617 0         0 $self -> {scanned}{ $domain }{ $zone }{ $best_match }{dir} = $dir_full_name;
618             }
619              
620 15         30 $best_match;
621             }
622              
623             sub _select_points_dir { # select best existing. or make it. for updating
624 9     9   4277 my $logger = Log::Log4perl -> get_logger( __PACKAGE__ );
625              
626 9 50       266 ref( my $self = shift ) or $logger -> logcroak( "I'm only an object method!" );
627              
628 9         14 my ( $domain,
629             $zone,
630             $net,
631             $node,
632             ) = @_;
633              
634             $logger -> logdie( 'unknown domain: %s',
635             $domain,
636             )
637 9 50       16 unless exists $self -> {domain_abbrev}{ $domain };
638              
639             # domain zone dir might not exist at all
640 9         14 my $dz_out = $self -> _select_domain_zone_dir( $domain, $zone );
641 9         30 my $points_dir = sprintf( '%04x%04x.pnt',
642             $net,
643             $node,
644             );
645              
646             # what if domain_abbr.zone (perfect one) doesn't have required points dir
647             # but domain_abbr.zOnE has?
648 6         13 my @dz_out_with_existing_points_dir = sort { $b cmp $a } # we prefer lower case of domain[.zone] dir
649             grep length $_ == length $dz_out # to filter out our_domain.our_zone versions
650             && exists $self -> {scanned}{ $domain }{ $zone }{ $_ }{ $net }
651             && exists $self -> {scanned}{ $domain }{ $zone }{ $_ }{ $net }{ $node }
652             && exists $self -> {scanned}{ $domain }{ $zone }{ $_ }{ $net }{ $node }{points_dir},
653 9   33     10 keys %{ $self -> {scanned}{ $domain }{ $zone } };
  9         117  
654              
655 9 50       15 if ( @dz_out_with_existing_points_dir ) { # ok, there is at least one with points dir. how do we select best of them?
656             # let's prioritize domain_abbr[.zone] betterness over points_dir betterness
657 9 100       20 unless ( grep $_ eq $dz_out,
658             @dz_out_with_existing_points_dir
659             ) { # ok, there is no best domain_abbr[.zone]. let's try to find best points_dir
660 7         16 my ( $t ) = grep exists $self -> {scanned}{ $domain }{ $zone }{ $_ }{ $net }{ $node }{points_dir}{ $points_dir },
661             @dz_out_with_existing_points_dir;
662              
663 7 100       14 $dz_out = defined $t ? $t : $dz_out_with_existing_points_dir[ 0 ]; # if no best in either place, just use the very first one
664             }
665              
666             # now we've got best outbound. let's find best points dir. or just the very first
667 6         8 $points_dir = ( sort { $b cmp $a } # we prefer lower case of points dir
668 4         14 keys %{ $self -> {scanned}{ $domain }{ $zone }{ $dz_out }{ $net }{ $node }{points_dir} }
669             )[ 0 ]
670 9 100       18 unless exists $self -> {scanned}{ $domain }{ $zone }{ $dz_out }{ $net }{ $node }{points_dir}{ $points_dir };
671              
672             } else { # doesn't exist. we need to create it using best domain_abbr[.zone] dir
673             my $dir_full_name = File::Spec -> catdir( $self -> {scanned}{ $domain }{ $zone }{ $dz_out }{dir},
674 0         0 $points_dir,
675             );
676              
677 0         0 $logger -> debug( sprintf 'creating %s#%d:%d/%d points directory %s',
678             $domain,
679             $zone,
680             $net,
681             $node,
682             $dir_full_name,
683             );
684              
685 0 0       0 mkdir Encode::encode( locale_fs => $dir_full_name )
686             or $logger -> logdie( sprintf 'cannot create points directory %s: %s',
687             $dir_full_name,
688             $!,
689             );
690              
691 0         0 $self -> {scanned}{ $domain }{ $zone }{ $dz_out }{ $net }{ $node }{points_dir}{ $points_dir } = $dir_full_name;
692             }
693              
694             # return ( dz_out, $points_dir) or full points directory path?
695 9         36 $self -> {scanned}{ $domain }{ $zone }{ $dz_out }{ $net }{ $node }{points_dir}{ $points_dir };
696             }
697              
698             =head2 busy_protected_sub
699              
700             Expects two parameters:
701              
702             address going to be dealt with as a FTN::Addr object
703              
704             function reference that will receive passed address and us ($self) as parameters and which should do all required operations related to the passed address.
705              
706             This method infinitely waits (most likely will be changed in the future) until address is not busy. Then it creates busy flag and calls passed function reference providing itself as an argument for it. After function return removes created busy flag.
707              
708             Returns itself for chaining.
709              
710             =cut
711              
712             sub busy_protected_sub { # address, sub_ref( self ). (order busy, execute sub, remove busy)
713 0     0 1   my $logger = Log::Log4perl -> get_logger( __PACKAGE__ );
714              
715 0 0         ref( my $self = shift ) or $logger -> logcroak( "I'm only an object method!" );
716              
717 0           my $addr = $self -> _validate_addr( shift );
718              
719 0 0 0       $logger -> logdie( 'no valid sub_ref passed' )
      0        
720             unless @_
721             && defined $_[ 0 ]
722             && 'CODE' eq ref $_[ 0 ];
723              
724 0           my $sub_ref = shift;
725              
726             $self -> scan
727 0 0         unless exists $self -> {scanned};
728              
729             # check that it's not already busy
730 0           while ( $self -> is_busy( $addr ) ) {
731 0           sleep( 4 ); # waiting...
732 0           $self -> scan;
733             }
734              
735             # here there is no busy flag for passed address. make it in the best dir then
736 0           my $busy_name;
737              
738 0 0         if ( $addr -> point ) { # possible dir creation
739 0           $busy_name = File::Spec -> catfile( $self -> _select_points_dir( $addr -> domain,
740             $addr -> zone,
741             $addr -> net,
742             $addr -> node,
743             ),
744             sprintf( '%08x',
745             $addr -> point,
746             ),
747             );
748             } else {
749 0           my $dz_out = $self -> _select_domain_zone_dir( $addr -> domain,
750             $addr -> zone,
751             );
752              
753             $busy_name = File::Spec -> catfile( $self -> {scanned}{ $addr -> domain }{ $addr -> zone }{ $dz_out }{dir},
754 0           sprintf( '%04x%04x',
755             $addr -> net,
756             $addr -> node,
757             ),
758             );
759             }
760 0           $busy_name .= '.' . $control_file_extension{busy};
761              
762 0           my $busy_name_fs = Encode::encode( locale_fs => $busy_name );
763              
764 0 0         sysopen my $fh, $busy_name_fs, Fcntl::O_WRONLY | Fcntl::O_CREAT | Fcntl::O_EXCL
765             or $logger -> logdie( 'cannot open %s for writing: %s',
766             $busy_name,
767             $!,
768             );
769              
770 0 0         flock $fh, Fcntl::LOCK_EX
771             or $logger -> logdie( q[can't flock file %s: %s],
772             $busy_name,
773             $!
774             );
775              
776             # For information purposes a bsy file may contain one line of PID information (less than 70 characters).
777 0           printf $fh '%d %s',
778             $$,
779             substr( __FILE__, 0, 70 - 1 - length( $$ ) );
780              
781 0           eval {
782 0           $sub_ref -> ( $addr,
783             $self,
784             );
785             };
786              
787             # remove busy first
788 0           close $fh;
789              
790 0 0         unlink $busy_name_fs
791             or $logger -> logwarn( sprintf 'could not unlink %s: %s',
792             $busy_name,
793             $!,
794             );
795              
796 0 0         if ( $@ ) { # something bad happened
797 0           $logger -> logdie( 'referenced sub execution failed: %s',
798             $@,
799             );
800             }
801              
802 0           $self;
803             }
804              
805             =head2 addr_file_to_change
806              
807             Expects arguments:
808              
809             address is going to be dealt with as a FTN::Addr object
810              
811             file type is one of netmail, reference_file, file_request, busy, call, hold, try.
812              
813             If file type is netmail or reference_file, then next parameter should be its flavour: immediate, crash, direct, normal, hold.
814              
815             If optional function reference passed, then it will be called with one parameter - name of the file to process. After that information in internal structure about that file will be updated.
816              
817             Does not deal with busy flag implicitly. Recommended usage is in the function passed to busy_protected_sub.
818              
819             Returns full name of the file to process (might not exists yet though).
820              
821             =cut
822              
823             sub addr_file_to_change { # addr, type ( netmail, file_reference, .. ), [flavour], [ sub_ref( filename ) ].
824             # figures required filetype name (new or existing) and calls subref with that name.
825             # does not deal with busy implicitly
826             # returns full name of the file to be changed/created
827 0     0 1   my $logger = Log::Log4perl -> get_logger( __PACKAGE__ );
828              
829 0 0         ref( my $self = shift ) or $logger -> logcroak( "I'm only an object method!" );
830              
831 0           my $addr = $self -> _validate_addr( shift );
832              
833 0           my @flavoured = qw/ netmail
834             reference_file
835             /;
836              
837 0 0         $logger -> logdie( 'no type passed' )
838             unless @_;
839              
840 0           my $type = shift;
841              
842 0 0 0       $logger -> logdie( sprintf 'incorrect type: %s',
    0          
843             defined $type ? $type : 'undef',
844             )
845             unless defined $type
846             && grep $type eq $_,
847             @flavoured,
848             keys %control_file_extension;
849              
850 0 0         my $filename = $addr -> point ?
851             sprintf( '%08x.', $addr -> point )
852             : sprintf( '%04x%04x.',
853             $addr -> net,
854             $addr -> node,
855             );
856              
857 0           my $flavoured = grep $type eq $_, @flavoured;
858 0           my $flavour;
859 0 0         if ( $flavoured ) {
860 0 0         $logger -> logdie( 'no flavour passed' )
861             unless @_;
862              
863 0           $flavour = shift;
864              
865             $logger -> logdie( sprintf 'incorrect flavour: %s',
866             defined $flavour ? $flavour : 'undef',
867             )
868             unless defined $flavour
869 0 0 0       && exists $flavour_extension{ $flavour };
    0          
870              
871             $filename .= $type eq $flavoured[ 0 ] ? # netmail
872             $flavour_extension{ $flavour }[ 0 ]
873 0 0         : $flavour_extension{ $flavour }[ 1 ];
874             } else {
875 0           $filename .= $control_file_extension{ $type };
876             }
877              
878 0           my $sub_ref;
879              
880 0 0         if ( @_ ) { # possible sub ref
881 0 0 0       $logger -> logdie( 'no valid sub_ref passed' )
882             unless defined $_[ 0 ]
883             && 'CODE' eq ref $_[ 0 ];
884              
885 0           $sub_ref = shift;
886             }
887              
888              
889             $self -> scan
890 0 0         unless exists $self -> {scanned};
891              
892              
893             # check any outdir except our_domain.our_zone for already existing file
894 0           my $dz_out = $self -> _select_domain_zone_dir( $addr -> domain, $addr -> zone );
895              
896             my @dz_out_with_existing_file = grep length $_ == length $dz_out # to filter out our_domain.our_zone versions
897             && exists $self -> {scanned}{ $addr -> domain }{ $addr -> zone }{ $_ }{ $addr -> net }
898             && exists $self -> {scanned}{ $addr -> domain }{ $addr -> zone }{ $_ }{ $addr -> net }{ $addr -> node }
899             && exists $self -> {scanned}{ $addr -> domain }{ $addr -> zone }{ $_ }{ $addr -> net }{ $addr -> node }{ $addr -> point }
900             && exists $self -> {scanned}{ $addr -> domain }{ $addr -> zone }{ $_ }{ $addr -> net }{ $addr -> node }{ $addr -> point }{ $type }
901             && ( ! $flavoured
902             || exists $self -> {scanned}{ $addr -> domain }{ $addr -> zone }{ $_ }{ $addr -> net }{ $addr -> node }{ $addr -> point }{ $type }{ $flavour }
903             ),
904 0   0       keys %{ $self -> {scanned}{ $addr -> domain }{ $addr -> zone } };
  0            
905              
906 0           my $full_filename;
907              
908 0 0         if ( @dz_out_with_existing_file ) { # file exists
909 0 0         unless ( grep $dz_out eq $_,
910             @dz_out_with_existing_file
911             ) { # best domain.zone does not have existing file. let's select best of the worst
912             # first try to find one with the best formatted file
913             my ( $t ) = grep {
914 0           my $r = $self -> {scanned}{ $addr -> domain }{ $addr -> zone }{ $_ }{ $addr -> net }{ $addr -> node }{ $addr -> point }{ $type };
  0            
915              
916 0 0         $r = $r -> { $flavour }
917             if $flavoured;
918              
919             grep $filename eq $_ -> {name},
920 0           @$r;
921             } @dz_out_with_existing_file;
922              
923 0 0         $dz_out = $t ? $t : $dz_out_with_existing_file[ 0 ]; # or just very first one
924             }
925              
926             # here we've got dz_out with existing file
927 0           my $r = $self -> {scanned}{ $addr -> domain }{ $addr -> zone }{ $dz_out }{ $addr -> net }{ $addr -> node }{ $addr -> point }{ $type };
928              
929 0 0         $r = $r -> { $flavour }
930             if $flavoured;
931              
932             $filename = $r -> [ 0 ]{name}
933             unless grep $filename eq $_ -> {name}, # no best file name
934 0 0         @$r;
935              
936             ( $full_filename ) = map $_ -> {full_name},
937             grep $filename eq $_ -> {name},
938 0           @$r;
939              
940             # and remove it..
941 0           @$r = grep $filename ne $_ -> {name}, @$r;
942             } else { # no file - create it
943             $full_filename = File::Spec -> catfile( $addr -> point ?
944             $self -> _select_points_dir( $addr -> domain,
945             $addr -> zone,
946             $addr -> net,
947             $addr -> node,
948             )
949             : $self -> {scanned}{ $addr -> domain }{ $addr -> zone }{ $dz_out }{dir},
950 0 0         $filename,
951             );
952             }
953              
954 0 0         if ( $sub_ref ) {
955 0           eval {
956 0           $sub_ref -> ( $full_filename );
957             };
958              
959 0 0         if ( $@ ) { # something bad happened
960 0           $logger -> logdie( sprintf 'referenced sub execution failed: %s',
961             $@,
962             );
963             }
964              
965             # update file information in internal structure
966             # first remove existing record about file (if it's known)
967             {
968 0           my $r = $self -> {scanned}{ $addr -> domain }{ $addr -> zone }{ $dz_out }{ $addr -> net }{ $addr -> node }{ $addr -> point }{ $type };
  0            
969              
970 0 0         $r = $r -> { $flavour }
971             if $flavoured;
972              
973 0           my ( $record_idx ) = grep $_ -> {full_name} eq $full_filename, 0 .. $#$r;
974 0 0         splice @$r, $record_idx, 1
975             if defined $record_idx;
976              
977             # might be a good idea to remove empty parents as well if there was just one file
978             }
979              
980 0 0         if ( -e Encode::encode( locale_fs => $full_filename ) ) {
981 0           my $r = $self -> {scanned}{ $addr -> domain }{ $addr -> zone }{ $dz_out }{ $addr -> net }{ $addr -> node }{ $addr -> point }{ $type };
982              
983 0 0         $r = $r -> { $flavour }
984             if $flavoured;
985              
986 0           my %file_prop = ( name => $filename,
987             full_name => $full_filename,
988             mstat => ( stat _ )[ 9 ],
989             size => -s _,
990             );
991              
992             $file_prop{referenced_files} =
993             FTN::Outbound::Reference_file
994             -> new( $file_prop{full_name},
995             $self -> {reference_file_read_line_transform_sub},
996             )
997             -> read_existing_file
998             -> referenced_files
999             if $type eq 'reference_file'
1000             && $file_prop{size} # empty files are empty, right?
1001 0 0 0       && exists $self -> {reference_file_read_line_transform_sub};
      0        
1002              
1003 0           push @$r, \ %file_prop;
1004             }
1005             }
1006              
1007             # what to return - just full name or open handle? handle probably better (can update scanned structure, but buffered/unbuffered? access details?)
1008             # let's try full name first
1009 0           $full_filename;
1010             }
1011              
1012             1;