1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2024-12-12 10:04:14 +02:00
pgbackrest/lib/pgBackRest/Common/Ini.pm
David Steele de7fc37f88 Storage and IO layer refactor:
Refactor storage layer to allow for new repository filesystems using drivers. (Reviewed by Cynthia Shang.)
Refactor IO layer to allow for new compression formats, checksum types, and other capabilities using filters. (Reviewed by Cynthia Shang.)
2017-06-09 17:51:41 -04:00

825 lines
27 KiB
Perl

####################################################################################################################################
# COMMON INI MODULE
####################################################################################################################################
package pgBackRest::Common::Ini;
use strict;
use warnings FATAL => qw(all);
use Carp qw(confess);
use English '-no_match_vars';
use Digest::SHA;
use Exporter qw(import);
our @EXPORT = qw();
use Fcntl qw(:mode O_WRONLY O_CREAT O_TRUNC);
use File::Basename qw(dirname basename);
use IO::Handle;
use JSON::PP;
use Storable qw(dclone);
use pgBackRest::Common::Exception;
use pgBackRest::Common::Log;
use pgBackRest::Common::String;
use pgBackRest::Version;
####################################################################################################################################
# Boolean constants
####################################################################################################################################
use constant INI_TRUE => JSON::PP::true;
push @EXPORT, qw(INI_TRUE);
use constant INI_FALSE => JSON::PP::false;
push @EXPORT, qw(INI_FALSE);
####################################################################################################################################
# Ini control constants
####################################################################################################################################
use constant INI_SECTION_BACKREST => 'backrest';
push @EXPORT, qw(INI_SECTION_BACKREST);
use constant INI_KEY_CHECKSUM => 'backrest-checksum';
push @EXPORT, qw(INI_KEY_CHECKSUM);
use constant INI_KEY_FORMAT => 'backrest-format';
push @EXPORT, qw(INI_KEY_FORMAT);
use constant INI_KEY_VERSION => 'backrest-version';
push @EXPORT, qw(INI_KEY_VERSION);
####################################################################################################################################
# Ini file copy extension
####################################################################################################################################
use constant INI_COPY_EXT => '.copy';
push @EXPORT, qw(INI_COPY_EXT);
####################################################################################################################################
# Ini sort orders
####################################################################################################################################
use constant INI_SORT_FORWARD => 'forward';
push @EXPORT, qw(INI_SORT_FORWARD);
use constant INI_SORT_REVERSE => 'reverse';
push @EXPORT, qw(INI_SORT_REVERSE);
use constant INI_SORT_NONE => 'none';
push @EXPORT, qw(INI_SORT_NONE);
####################################################################################################################################
# new()
####################################################################################################################################
sub new
{
my $class = shift; # Class name
# Create the class hash
my $self = {};
bless $self, $class;
# Load Storage::Helper module
require pgBackRest::Storage::Helper;
pgBackRest::Storage::Helper->import();
# Assign function parameters, defaults, and log debug info
(
my $strOperation,
$self->{strFileName},
my $bLoad,
my $strContent,
$self->{oStorage},
$self->{iInitFormat},
$self->{strInitVersion},
my $bIgnoreMissing,
) =
logDebugParam
(
__PACKAGE__ . '->new', \@_,
{name => 'strFileName', trace => true},
{name => 'bLoad', optional => true, default => true, trace => true},
{name => 'strContent', optional => true, trace => true},
{name => 'oStorage', optional => true, default => storageLocal(), trace => true},
{name => 'iInitFormat', optional => true, default => BACKREST_FORMAT, trace => true},
{name => 'strInitVersion', optional => true, default => BACKREST_VERSION, trace => true},
{name => 'bIgnoreMissing', optional => true, default => false, trace => true},
);
# Set changed to false
$self->{bModified} = false;
# Set exists to false
$self->{bExists} = false;
# Load the file if requested
if ($bLoad)
{
$self->load($bIgnoreMissing);
}
# Load from a string if provided
elsif (defined($strContent))
{
$self->{oContent} = iniParse($strContent);
$self->headerCheck();
}
# Initialize if not loading from string and file does not exist
if (!$self->{bExists} && !defined($strContent))
{
$self->numericSet(INI_SECTION_BACKREST, INI_KEY_FORMAT, undef, $self->{iInitFormat});
$self->set(INI_SECTION_BACKREST, INI_KEY_VERSION, undef, $self->{strInitVersion});
}
return $self;
}
####################################################################################################################################
# loadVersion() - load a version (main or copy) of the ini file
####################################################################################################################################
sub loadVersion
{
my $self = shift;
my $bCopy = shift;
my $bIgnoreError = shift;
# Load main
my $rstrContent = $self->{oStorage}->get(
$self->{oStorage}->openRead($self->{strFileName} . ($bCopy ? INI_COPY_EXT : ''), {bIgnoreMissing => $bIgnoreError}));
# If the file exists then attempt to parse it
if (defined($rstrContent))
{
my $rhContent = iniParse($$rstrContent, {bIgnoreInvalid => $bIgnoreError});
# If the content is valid then check the header
if (defined($rhContent))
{
$self->{oContent} = $rhContent;
# If the header is invalid then undef content
if (!$self->headerCheck({bIgnoreInvalid => $bIgnoreError}))
{
delete($self->{oContent});
}
}
}
return defined($self->{oContent});
}
####################################################################################################################################
# load() - load the ini
####################################################################################################################################
sub load
{
my $self = shift;
my $bIgnoreMissing = shift;
# If main was not loaded then try the copy
if (!$self->loadVersion(false, true))
{
if (!$self->loadVersion(true, true))
{
return if $bIgnoreMissing;
confess &log(ERROR, "unable to open $self->{strFileName} or $self->{strFileName}" . INI_COPY_EXT, ERROR_FILE_MISSING);
}
}
$self->{bExists} = true;
}
####################################################################################################################################
# headerCheck() - check that version and checksum in header are as expected
####################################################################################################################################
sub headerCheck
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$bIgnoreInvalid,
) =
logDebugParam
(
__PACKAGE__ . '->headerCheck', \@_,
{name => 'bIgnoreInvalid', optional => true, default => false, trace => true},
);
# Eval so exceptions can be ignored on bIgnoreInvalid
my $bValid = true;
eval
{
# Make sure the ini is valid by testing checksum
my $strChecksum = $self->get(INI_SECTION_BACKREST, INI_KEY_CHECKSUM, undef, false);
my $strTestChecksum = $self->hash();
if (!defined($strChecksum) || $strChecksum ne $strTestChecksum)
{
confess &log(ERROR,
"invalid checksum in '$self->{strFileName}', expected '${strTestChecksum}' but found " .
(defined($strChecksum) ? "'${strChecksum}'" : '[undef]'),
ERROR_CHECKSUM);
}
# Make sure that the format is current, otherwise error
my $iFormat = $self->get(INI_SECTION_BACKREST, INI_KEY_FORMAT, undef, false, 0);
if ($iFormat != $self->{iInitFormat})
{
confess &log(ERROR,
"invalid format in '$self->{strFileName}', expected $self->{iInitFormat} but found ${iFormat}", ERROR_FORMAT);
}
# Check if the version has changed
if (!$self->test(INI_SECTION_BACKREST, INI_KEY_VERSION, undef, $self->{strInitVersion}))
{
$self->set(INI_SECTION_BACKREST, INI_KEY_VERSION, undef, $self->{strInitVersion});
}
return true;
}
or do
{
# Confess the error if it should not be ignored
if (!$bIgnoreInvalid)
{
confess $EVAL_ERROR;
}
# Return false when errors are ignored
$bValid = false;
};
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'bValid', value => $bValid, trace => true}
);
}
####################################################################################################################################
# iniParse() - parse from standard INI format to a hash.
####################################################################################################################################
push @EXPORT, qw(iniParse);
sub iniParse
{
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strContent,
$bRelaxed,
$bIgnoreInvalid,
) =
logDebugParam
(
__PACKAGE__ . '::iniParse', \@_,
{name => 'strContent', required => false, trace => true},
{name => 'bRelaxed', optional => true, default => false, trace => true},
{name => 'bIgnoreInvalid', optional => true, default => false, trace => true},
);
# Ini content
my $oContent = undef;
my $strSection;
# Create the JSON object
my $oJSON = JSON::PP->new()->allow_nonref();
# Eval so exceptions can be ignored on bIgnoreInvalid
eval
{
# Read the INI file
foreach my $strLine (split("\n", defined($strContent) ? $strContent : ''))
{
$strLine = trim($strLine);
# Skip lines that are blank or comments
if ($strLine ne '' && $strLine !~ '^[ ]*#.*')
{
# Get the section
if (index($strLine, '[') == 0)
{
$strSection = substr($strLine, 1, length($strLine) - 2);
}
else
{
if (!defined($strSection))
{
confess &log(ERROR, "key/value pair '${strLine}' found outside of a section", ERROR_CONFIG);
}
# Get key and value
my $iIndex = index($strLine, '=');
if ($iIndex == -1)
{
confess &log(ERROR, "unable to find '=' in '${strLine}'", ERROR_CONFIG);
}
my $strKey = substr($strLine, 0, $iIndex);
my $strValue = substr($strLine, $iIndex + 1);
# If relaxed then read the value directly
if ($bRelaxed)
{
if (defined($oContent->{$strSection}{$strKey}))
{
if (ref($oContent->{$strSection}{$strKey}) ne 'ARRAY')
{
$oContent->{$strSection}{$strKey} = [$oContent->{$strSection}{$strKey}];
}
push(@{$oContent->{$strSection}{$strKey}}, $strValue);
}
else
{
$oContent->{$strSection}{$strKey} = $strValue;
}
}
# Else read the value as stricter JSON
else
{
${$oContent}{$strSection}{$strKey} = $oJSON->decode($strValue);
}
}
}
}
# Error if the file is empty
if (!($bRelaxed || defined($oContent)))
{
confess &log(ERROR, 'no key/value pairs found', ERROR_CONFIG);
}
return true;
}
or do
{
# Confess the error if it should not be ignored
if (!$bIgnoreInvalid)
{
confess $EVAL_ERROR;
}
# Undef content when errors are ignored
undef($oContent);
};
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'oContent', value => $oContent, trace => true}
);
}
####################################################################################################################################
# save() - save the file.
####################################################################################################################################
sub save
{
my $self = shift;
# Save only if modified
if ($self->{bModified})
{
# Calculate the hash
$self->hash();
# Save the file
$self->{oStorage}->put($self->{strFileName}, iniRender($self->{oContent}));
$self->{oStorage}->pathSync(dirname($self->{strFileName}));
$self->{oStorage}->put($self->{strFileName} . INI_COPY_EXT, iniRender($self->{oContent}));
$self->{oStorage}->pathSync(dirname($self->{strFileName}));
$self->{bModified} = false;
# Indicate the file now exists
$self->{bExists} = true;
# File was saved
return true;
}
# File was not saved
return false;
}
####################################################################################################################################
# saveCopy - save only a copy of the file.
####################################################################################################################################
sub saveCopy
{
my $self = shift;
if ($self->{oStorage}->exists($self->{strFileName}))
{
confess &log(ASSERT, "cannot save copy only when '$self->{strFileName}' exists");
}
$self->hash();
$self->{oStorage}->put($self->{strFileName} . INI_COPY_EXT, iniRender($self->{oContent}));
}
####################################################################################################################################
# iniRender() - render hash to standard INI format.
####################################################################################################################################
push @EXPORT, qw(iniRender);
sub iniRender
{
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$oContent,
$bRelaxed,
) =
logDebugParam
(
__PACKAGE__ . '::iniRender', \@_,
{name => 'oContent', trace => true},
{name => 'bRelaxed', default => false, trace => true},
);
# Open the ini file for writing
my $strContent = '';
my $bFirst = true;
# Create the JSON object canonical so that fields are alpha ordered to pass unit tests
my $oJSON = JSON::PP->new()->canonical()->allow_nonref();
# Write the INI file
foreach my $strSection (sort(keys(%$oContent)))
{
# Add a linefeed between sections
if (!$bFirst)
{
$strContent .= "\n";
}
# Write the section
$strContent .= "[${strSection}]\n";
# Iterate through all keys in the section
foreach my $strKey (sort(keys(%{$oContent->{$strSection}})))
{
# If the value is a hash then convert it to JSON, otherwise store as is
my $strValue = ${$oContent}{$strSection}{$strKey};
# If relaxed then store as old-style config
if ($bRelaxed)
{
# If the value is an array then save each element to a separate key/value pair
if (ref($strValue) eq 'ARRAY')
{
foreach my $strArrayValue (@{$strValue})
{
$strContent .= "${strKey}=${strArrayValue}\n";
}
}
# Else write a standard key/value pair
else
{
$strContent .= "${strKey}=${strValue}\n";
}
}
# Else write as stricter JSON
else
{
$strContent .= "${strKey}=" . $oJSON->encode($strValue) . "\n";
}
}
$bFirst = false;
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'strContent', value => $strContent, trace => true}
);
}
####################################################################################################################################
# hash() - generate hash for the manifest.
####################################################################################################################################
sub hash
{
my $self = shift;
# Remove the old checksum
delete($self->{oContent}{&INI_SECTION_BACKREST}{&INI_KEY_CHECKSUM});
# Calculate the checksum
my $oSHA = Digest::SHA->new('sha1');
my $oJSON = JSON::PP->new()->canonical()->allow_nonref();
$oSHA->add($oJSON->encode($self->{oContent}));
# Set the new checksum
$self->{oContent}{&INI_SECTION_BACKREST}{&INI_KEY_CHECKSUM} = $oSHA->hexdigest();
return $self->{oContent}{&INI_SECTION_BACKREST}{&INI_KEY_CHECKSUM};
}
####################################################################################################################################
# get() - get a value.
####################################################################################################################################
sub get
{
my $self = shift;
my $strSection = shift;
my $strKey = shift;
my $strSubKey = shift;
my $bRequired = shift;
my $oDefault = shift;
# Parameter constraints
if (!defined($strSection))
{
confess &log(ASSERT, 'strSection is required');
}
if (defined($strSubKey) && !defined($strKey))
{
confess &log(ASSERT, "strKey is required when strSubKey '${strSubKey}' is requested");
}
# Get the result
my $oResult = $self->{oContent}->{$strSection};
if (defined($strKey) && defined($oResult))
{
$oResult = $oResult->{$strKey};
if (defined($strSubKey) && defined($oResult))
{
$oResult = $oResult->{$strSubKey};
}
}
# When result is not defined
if (!defined($oResult))
{
# Error if a result is required
if (!defined($bRequired) || $bRequired)
{
confess &log(ASSERT, "strSection '$strSection'" . (defined($strKey) ? ", strKey '$strKey'" : '') .
(defined($strSubKey) ? ", strSubKey '$strSubKey'" : '') . ' is required but not defined');
}
# Return default if specified
if (defined($oDefault))
{
return $oDefault;
}
}
return $oResult
}
####################################################################################################################################
# boolGet() - get a boolean value.
####################################################################################################################################
sub boolGet
{
my $self = shift;
my $strSection = shift;
my $strKey = shift;
my $strSubKey = shift;
my $bRequired = shift;
my $bDefault = shift;
return $self->get(
$strSection, $strKey, $strSubKey, $bRequired,
defined($bDefault) ? ($bDefault ? INI_TRUE : INI_FALSE) : undef) ? true : false;
}
####################################################################################################################################
# numericGet() - get a numeric value.
####################################################################################################################################
sub numericGet
{
my $self = shift;
my $strSection = shift;
my $strKey = shift;
my $strSubKey = shift;
my $bRequired = shift;
my $nDefault = shift;
return $self->get($strSection, $strKey, $strSubKey, $bRequired, defined($nDefault) ? $nDefault + 0 : undef) + 0;
}
####################################################################################################################################
# set - set a value.
####################################################################################################################################
sub set
{
my $self = shift;
my $strSection = shift;
my $strKey = shift;
my $strSubKey = shift;
my $oValue = shift;
# Parameter constraints
if (!(defined($strSection) && defined($strKey)))
{
confess &log(ASSERT, 'strSection and strKey are required');
}
my $oCurrentValue;
if (defined($strSubKey))
{
$oCurrentValue = \$self->{oContent}{$strSection}{$strKey}{$strSubKey};
}
else
{
$oCurrentValue = \$self->{oContent}{$strSection}{$strKey};
}
if (!defined($$oCurrentValue) ||
defined($oCurrentValue) != defined($oValue) ||
${dclone($oCurrentValue)} ne ${dclone(\$oValue)})
{
$$oCurrentValue = $oValue;
if (!$self->{bModified})
{
$self->{bModified} = true;
}
return true;
}
return false;
}
####################################################################################################################################
# boolSet - set a boolean value.
####################################################################################################################################
sub boolSet
{
my $self = shift;
my $strSection = shift;
my $strKey = shift;
my $strSubKey = shift;
my $bValue = shift;
$self->set($strSection, $strKey, $strSubKey, $bValue ? INI_TRUE : INI_FALSE);
}
####################################################################################################################################
# numericSet - set a numeric value.
####################################################################################################################################
sub numericSet
{
my $self = shift;
my $strSection = shift;
my $strKey = shift;
my $strSubKey = shift;
my $nValue = shift;
$self->set($strSection, $strKey, $strSubKey, defined($nValue) ? $nValue + 0 : undef);
}
####################################################################################################################################
# remove - remove a value.
####################################################################################################################################
sub remove
{
my $self = shift;
my $strSection = shift;
my $strKey = shift;
my $strSubKey = shift;
# Test if the value exists
if ($self->test($strSection, $strKey, $strSubKey))
{
# Remove a subkey
if (defined($strSubKey))
{
delete($self->{oContent}{$strSection}{$strKey}{$strSubKey});
}
# Remove a key
if (defined($strKey))
{
if (!defined($strSubKey))
{
delete($self->{oContent}{$strSection}{$strKey});
}
# Remove the section if it is now empty
if (keys(%{$self->{oContent}{$strSection}}) == 0)
{
delete($self->{oContent}{$strSection});
}
}
# Remove a section
if (!defined($strKey))
{
delete($self->{oContent}{$strSection});
}
# Record changes
if (!$self->{bModified})
{
$self->{bModified} = true;
}
return true;
}
return false;
}
####################################################################################################################################
# keys - get the list of keys in a section.
####################################################################################################################################
sub keys
{
my $self = shift;
my $strSection = shift;
my $strSortOrder = shift;
if ($self->test($strSection))
{
if (!defined($strSortOrder) || $strSortOrder eq INI_SORT_FORWARD)
{
return (sort(keys(%{$self->get($strSection)})));
}
elsif ($strSortOrder eq INI_SORT_REVERSE)
{
return (sort {$b cmp $a} (keys(%{$self->get($strSection)})));
}
elsif ($strSortOrder eq INI_SORT_NONE)
{
return (keys(%{$self->get($strSection)}));
}
else
{
confess &log(ASSERT, "invalid strSortOrder '${strSortOrder}'");
}
}
my @stryEmptyArray;
return @stryEmptyArray;
}
####################################################################################################################################
# test - test a value.
#
# Test a value to see if it equals the supplied test value. If no test value is given, tests that the section, key, or subkey
# is defined.
####################################################################################################################################
sub test
{
my $self = shift;
my $strSection = shift;
my $strValue = shift;
my $strSubValue = shift;
my $strTest = shift;
# Get the value
my $strResult = $self->get($strSection, $strValue, $strSubValue, false);
# Is there a result
if (defined($strResult))
{
# Is there a value to test against?
if (defined($strTest))
{
return $strResult eq $strTest ? true : false;
}
return true;
}
return false;
}
####################################################################################################################################
# boolTest - test a boolean value, see test().
####################################################################################################################################
sub boolTest
{
my $self = shift;
my $strSection = shift;
my $strValue = shift;
my $strSubValue = shift;
my $bTest = shift;
return $self->test($strSection, $strValue, $strSubValue, defined($bTest) ? ($bTest ? INI_TRUE : INI_FALSE) : undef);
}
####################################################################################################################################
# Properties.
####################################################################################################################################
sub modified {shift->{bModified}} # Has the data been modified since last load/save?
sub exists {shift->{bExists}} # Is the data persisted to file?
1;