Config-Model
view release on metacpan or search on metacpan
lib/Config/Model/BackendMgr.pm view on Meta::CPAN
#
# This file is part of Config-Model
#
# This software is Copyright (c) 2005-2022 by Dominique Dumont.
#
# This is free software, licensed under:
#
# The GNU Lesser General Public License, Version 2.1, February 1999
#
package Config::Model::BackendMgr 2.162;
use Mouse;
use strict;
use warnings;
use Carp;
use v5.20;
use Config::Model::Exception;
use Data::Dumper;
use Storable qw/dclone/;
use Scalar::Util qw/weaken reftype/;
use Log::Log4perl qw(get_logger :levels);
use Path::Tiny 0.070;
use feature qw/postderef signatures/;
no warnings qw/experimental::postderef experimental::signatures/;
my $logger = get_logger('BackendMgr');
my $user_logger = get_logger('User');
# one BackendMgr per file
has 'node' => (
is => 'ro',
isa => 'Config::Model::Node',
weak_ref => 1,
required => 1
);
has 'file_backup' => ( is => 'rw', isa => 'ArrayRef', default => sub { [] } );
has 'rw_config' => (
is => 'ro',
isa => 'HashRef',
required => 1
);
has 'backend_obj' => (
is => 'rw',
isa => 'Config::Model::Backend::Any',
lazy => 1 ,
builder => '_build_backend_obj',
);
sub _build_backend_obj {
my $self = shift;
my $backend = $self->rw_config->{backend};
$logger->warn("function parameter for a backend is deprecated. Please implement 'read' method in backend $backend")
if $self->rw_config->{function};
# try to load a specific Backend class
my $f = $self->rw_config->{function} || 'read';
my $c = $self->load_backend_class( $backend, $f );
no strict 'refs'; ## no critic (ProhibitNoStrict)
return $c->new(
node => $self->node,
name => $backend,
auto_create => $self->rw_config->{auto_create},
auto_delete => $self->rw_config->{auto_delete},
);
}
has support_annotation => (
is => 'ro',
isa => 'Bool',
default => 0,
);
with "Config::Model::Role::ComputeFunction";
with "Config::Model::Role::FileHandler";
# check if dir is present. May create it in auto_create write mode
sub get_cfg_dir_path ($self, %args) {
my $w = $args{write} || 0;
my $dir = $self->get_tuned_config_dir(%args);
if ( not $dir->is_dir and $w and $args{auto_create} ) {
$logger->info("creating directory $dir");
$dir->mkpath;
}
unless ( $dir->is_dir ) {
my $mode = $w ? 'write' : 'read';
$logger->info( "$args{backend}: missing directory $dir ($mode mode)" );
return ( 0, $dir );
}
$logger->trace( "dir: " . $dir // '<undef>' );
return ( 1, $dir );
}
# return (1, config file path) constructed from arguments or return
# (0). May create directory in auto_create write mode.
sub get_cfg_file_path ($self, %args) {
my $w = $args{write} || 0;
# config file override
my $cfo = $args{config_file};
if ( defined $cfo) {
my $override
= $args{root} ? $args{root}->child($cfo)
: $cfo =~ m!^/! ? path($cfo)
: path('.')->child($cfo);
my $mode = $w ? 'write' : 'read';
$logger->trace("$args{backend} override target file is $override ($mode mode)");
return ( 1, $override );
}
my ( $dir_ok, $dir ) = $self->get_cfg_dir_path(%args);
if ( defined $args{file} ) {
my $file = $args{skip_compute} ? $args{file} : $self->node->compute_string($args{file});
my $res = $dir->child($file);
$logger->trace("get_cfg_file_path: returns $res");
return ( $dir_ok, $res );
}
return 0;
}
sub open_read_file ($self, $file_path) {
if ( $file_path->is_file ) {
$logger->debug("open_read_file: open $file_path for read");
# store a backup in memory in case there's a problem
$self->file_backup( [ $file_path->lines_utf8 ] );
return $file_path->filehandle("<", ":utf8");
}
else {
return;
}
}
# called at configuration node creation
sub load_backend_class ($self, $backend, $function) {
$logger->trace("load_backend_class: called with backend $backend, function $function");
my %c;
my $k = "Config::Model::Backend::" . ucfirst($backend);
my $f = $k . '.pm';
$f =~ s!::!/!g;
$c{$k} = $f;
# try another class
$k =~ s/_(\w)/uc($1)/ge;
$f =~ s/_(\w)/uc($1)/ge;
$c{$k} = $f;
foreach my $c ( sort keys %c ) {
if ( $c->can($function) ) {
# no need to load class
$logger->debug("load_backend_class: $c is already loaded (can $function)");
return $c;
}
}
# look for file to load
my $class_to_load;
foreach my $c ( sort keys %c ) {
$logger->trace("load_backend_class: looking to load class $c");
foreach my $prefix (@INC) {
my $realfilename = "$prefix/$c{$c}";
$class_to_load = $c if -f $realfilename;
}
}
if (not defined $class_to_load) {
Config::Model::Exception::Model->throw(
object => $self->node,
error => "backend error: cannot find Perl class for backend: '$backend'",
);
};
my $file_to_load = $c{$class_to_load};
$logger->trace("load_backend_class: loading class $class_to_load, $file_to_load");
eval { require $file_to_load; };
if ($@) {
die "Error with backend $backend: could not parse $file_to_load: $@\n";
}
return $class_to_load;
}
sub read_config_data ($self, %args) {
$logger->trace( "called for node ", $self->node->location );
lib/Config/Model/BackendMgr.pm view on Meta::CPAN
$error->rethrow ;
}
elsif ( ref $error ) {
die $error ;
}
elsif ( $error ) {
die "Backend $backend failed to read $file_path: $error";
}
# only backend based on C::M::Backend::Any can support annotations
if ($backend_obj->can('annotation')) {
$self->{support_annotation} = $backend_obj->annotation ;
}
return ( $res, $file_path );
}
sub auto_write_init ($self) {
my $rw_config = dclone $self->rw_config ;
my $instance = $self->node->instance();
# root override is passed by the instance
my $root_dir = $instance->root_dir;
my $backend = $rw_config->{backend};
my $write_dir = $self->get_tuned_config_dir(%$rw_config);
$logger->trace( "auto_write_init creating write cb ($backend) for ", $self->node->name );
my @wr_args = (
%$rw_config, # model data
config_dir => $write_dir, # override from instance
write => 1, # for get_cfg_file_path
root => $root_dir, # override from instance
);
# used bby C::M::Dumper and C::M::DumpAsData
$self->{auto_write}{$backend} = 1;
my $wb;
my $f = $rw_config->{function} || 'write';
my $backend_class = $self->load_backend_class( $backend, $f );
my $location = $self->node->name;
my $node = $self->node; # closure
# provide a proper write back function
$wb = sub (%cb_args) {
my $force_delete = delete $cb_args{force_delete} ;
$logger->debug( "write cb ($backend) called for $location ", $force_delete ? '' : ' (deleted)' );
my $backend_obj = $self->backend_obj();
my ($fh, $file_ok, $file_path );
if (not $backend_class->skip_open) {
( $file_ok, $file_path ) = $self->get_cfg_file_path( @wr_args, %cb_args);
}
if ($file_ok) {
$fh = $self->open_file_to_write( $backend, $file_path, delete $cb_args{backup} );
}
# override needed for "save as" button
my %backend_args = (
@wr_args,
file_path => $file_path,
object => $node,
%cb_args # override from user
);
my $res;
if ($force_delete) {
$backend_obj->delete(%backend_args);
}
else {
$res = eval { $backend_obj->$f( %backend_args ); };
my $error = $@;
$logger->error( "write backend $backend $backend_class" . '::' . "$f failed: $error" )
if $error;
$self->close_file_to_write( $error, $file_path, $rw_config->{file_mode} );
$self->auto_delete($file_path, \%backend_args)
if $rw_config->{auto_delete} and not $backend_class->skip_open ;
}
return defined $res ? $res : $@ ? 0 : 1;
};
$logger->trace( "registering write $backend in node " . $self->node->name );
$instance->register_write_back( $self->node->location, $backend, $wb );
return;
}
sub auto_delete ($self, $file_path, $args) {
return unless $file_path;
my $perl_data;
$perl_data = $self->node->dump_as_data( full_dump => $args->{full_dump} // 0)
if defined $self->node;
my $size = ref($perl_data) eq 'HASH' ? scalar keys %$perl_data
: ref($perl_data) eq 'ARRAY' ? scalar @$perl_data
: $perl_data ;
if (not $size) {
$logger->info( "Removing $file_path (no data to store)" );
unlink($file_path);
}
return;
}
sub open_file_to_write ($self, $backend, $file_path, $backup) {
my $do_backup = defined $backup;
$backup ||= 'old'; # use old only if defined
$backup = '.' . $backup unless $backup =~ /^\./;
# make sure that parent dir exists before creating file
$file_path->parent->mkpath;
if ( $do_backup and $file_path->is_file ) {
$file_path->copy( $file_path.$backup ) or die "Backup copy failed: $!";
}
$logger->debug("$backend backend opened file $file_path to write");
return $file_path->filehandle(">",":utf8");
}
sub close_file_to_write ($self, $error, $file_path, $file_mode) {
return unless defined $file_path;
if ($error) {
# restore backup and display error
$logger->warn("Error during write, restoring backup data in $file_path" );
$file_path->append_utf8({ truncate => 1 }, $self->file_backup );
$error->rethrow if ref($error) and $error->can('rethrow');
die $error;
}
# TODO: move chmod in a backend role
$file_path->chmod($file_mode) if $file_mode;
# TODO: move in a backend role
# check file size and remove empty files
$file_path->remove if -z $file_path and not -l $file_path;
return;
}
sub is_auto_write_for_type ($self, $type) {
return $self->{auto_write}{$type} || 0;
}
__PACKAGE__->meta->make_immutable;
1;
# ABSTRACT: Load configuration node on demand
__END__
=pod
=encoding UTF-8
=head1 NAME
Config::Model::BackendMgr - Load configuration node on demand
=head1 VERSION
version 2.162
=head1 SYNOPSIS
# Use BackendMgr to write data in Yaml file
# This example requires Config::Model::Backend::Yaml which is now
# shipped outside of Config::Model. Please get it on CPAN
use Config::Model;
# define configuration tree object
my $model = Config::Model->new;
$model->create_config_class(
name => "Foo",
element => [
[qw/foo bar/] => {
type => 'leaf',
value_type => 'string'
},
]
);
$model->create_config_class(
name => "MyClass",
( run in 0.722 second using v1.01-cache-2.11-cpan-39bf76dae61 )