1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2024-12-14 10:13:05 +02:00
pgbackrest/lib/pgBackRest/File.pm
David Steele d0b6f78b20 More flexible configuration for databases
Master and standby can both be configured on the backup server and pgBackRest will automatically determine which is the master. This means no configuration changes for backup are required after failing over from a master to standby when a separate backup server is used.
2016-08-24 12:39:27 -04:00

1781 lines
56 KiB
Perl

####################################################################################################################################
# FILE MODULE
####################################################################################################################################
package pgBackRest::File;
use strict;
use warnings FATAL => qw(all);
use Carp qw(confess);
use Exporter qw(import);
our @EXPORT = qw();
use Fcntl qw(:mode :flock O_RDONLY O_WRONLY O_CREAT);
use File::Basename qw(dirname basename);
use File::Copy qw(cp);
use File::Path qw(make_path remove_tree);
use File::stat;
use IO::Handle;
use Scalar::Util qw(blessed);
use lib dirname($0) . '/../lib';
use pgBackRest::Common::Exception;
use pgBackRest::Common::Log;
use pgBackRest::Common::String;
use pgBackRest::Common::Wait;
use pgBackRest::FileCommon;
use pgBackRest::Protocol::Common;
####################################################################################################################################
# Remote operation constants
####################################################################################################################################
use constant OP_FILE => 'File';
use constant OP_FILE_COPY => OP_FILE . '->copy';
push @EXPORT, qw(OP_FILE_COPY);
use constant OP_FILE_COPY_IN => OP_FILE . '->copyIn';
push @EXPORT, qw(OP_FILE_COPY_IN);
use constant OP_FILE_COPY_OUT => OP_FILE . '->copyOut';
push @EXPORT, qw(OP_FILE_COPY_OUT);
use constant OP_FILE_EXISTS => OP_FILE . '->exists';
push @EXPORT, qw(OP_FILE_EXISTS);
use constant OP_FILE_LIST => OP_FILE . '->list';
push @EXPORT, qw(OP_FILE_LIST);
use constant OP_FILE_MANIFEST => OP_FILE . '->manifest';
push @EXPORT, qw(OP_FILE_MANIFEST);
use constant OP_FILE_PATH_CREATE => OP_FILE . '->pathCreate';
push @EXPORT, qw(OP_FILE_PATH_CREATE);
use constant OP_FILE_WAIT => OP_FILE . '->wait';
push @EXPORT, qw(OP_FILE_WAIT);
####################################################################################################################################
# COMMAND error constants [DEPRECATED - TO BE REPLACED BY CONSTANTS IN EXCEPTION.PM]
####################################################################################################################################
use constant COMMAND_ERR_FILE_MISSING => 1;
use constant COMMAND_ERR_FILE_READ => 2;
use constant COMMAND_ERR_FILE_MOVE => 3;
use constant COMMAND_ERR_FILE_TYPE => 4;
use constant COMMAND_ERR_LINK_READ => 5;
use constant COMMAND_ERR_PATH_MISSING => 6;
use constant COMMAND_ERR_PATH_CREATE => 7;
use constant COMMAND_ERR_PATH_READ => 8;
####################################################################################################################################
# PATH_GET constants
####################################################################################################################################
use constant PATH_ABSOLUTE => 'absolute';
push @EXPORT, qw(PATH_ABSOLUTE);
use constant PATH_DB => 'db';
push @EXPORT, qw(PATH_DB);
use constant PATH_DB_ABSOLUTE => 'db:absolute';
push @EXPORT, qw(PATH_DB_ABSOLUTE);
use constant PATH_BACKUP => 'backup';
push @EXPORT, qw(PATH_BACKUP);
use constant PATH_BACKUP_ABSOLUTE => 'backup:absolute';
push @EXPORT, qw(PATH_BACKUP_ABSOLUTE);
use constant PATH_BACKUP_CLUSTER => 'backup:cluster';
push @EXPORT, qw(PATH_BACKUP_CLUSTER);
use constant PATH_BACKUP_TMP => 'backup:tmp';
push @EXPORT, qw(PATH_BACKUP_TMP);
use constant PATH_BACKUP_ARCHIVE => 'backup:archive';
push @EXPORT, qw(PATH_BACKUP_ARCHIVE);
use constant PATH_BACKUP_ARCHIVE_OUT => 'backup:archive:out';
push @EXPORT, qw(PATH_BACKUP_ARCHIVE_OUT);
####################################################################################################################################
# STD pipe constants
####################################################################################################################################
use constant PIPE_STDIN => '<STDIN>';
push @EXPORT, qw(PIPE_STDIN);
use constant PIPE_STDOUT => '<STDOUT>';
push @EXPORT, qw(PIPE_STDOUT);
use constant PIPE_STDERR => '<STDERR>';
push @EXPORT, qw(PIPE_STDERR);
####################################################################################################################################
# new
####################################################################################################################################
sub new
{
my $class = shift;
# Create the class hash
my $self = {};
bless $self, $class;
# Assign function parameters, defaults, and log debug info
(
my $strOperation,
$self->{strStanza},
$self->{strBackupPath},
$self->{oProtocol},
$self->{strDefaultPathMode},
$self->{strDefaultFileMode},
$self->{iThreadIdx}
) =
logDebugParam
(
__PACKAGE__ . '->new', \@_,
{name => 'strStanza', required => false},
{name => 'strBackupPath'},
{name => 'oProtocol'},
{name => 'strDefaultPathMode', default => '0750'},
{name => 'strDefaultFileMode', default => '0640'},
{name => 'iThreadIdx', required => false}
);
# Default compression extension to gz
$self->{strCompressExtension} = 'gz';
# Remote object must be set
if (!defined($self->{oProtocol}))
{
confess &log(ASSERT, 'oProtocol must be defined');
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'self', value => $self}
);
}
####################################################################################################################################
# DESTROY
####################################################################################################################################
sub DESTROY
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my ($strOperation) = logDebugParam(__PACKAGE__ . '->DESTROY');
if (defined($self->{oProtocol}))
{
$self->{oProtocol} = undef;
}
# Return from function and log return values if any
return logDebugReturn($strOperation);
}
####################################################################################################################################
# clone
####################################################################################################################################
sub clone
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$iThreadIdx
) =
logDebugParam
(
__PACKAGE__ . '->clone', \@_,
{name => 'iThreadidx', required => false}
);
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{
name => 'self',
value => pgBackRest::File->new(
$self->{strStanza},
$self->{strBackupPath},
$self->{oProtocol},
$self->{strDefaultPathMode},
$self->{strDefaultFileMode},
$iThreadIdx)
}
);
}
####################################################################################################################################
# stanza
####################################################################################################################################
sub stanza
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my ($strOperation) = logDebugParam(__PACKAGE__ . '->stanza');
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'strStanza', $self->{strStanza}, trace => true}
);
}
####################################################################################################################################
# pathTypeGet
####################################################################################################################################
sub pathTypeGet
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strType
) =
logDebugParam
(
__PACKAGE__ . '->pathTypeGet', \@_,
{name => 'strType', trace => true}
);
my $strPath;
# If absolute type
if ($strType eq PATH_ABSOLUTE)
{
$strPath = PATH_ABSOLUTE;
}
# If db type
elsif ($strType =~ /^db(\:.*){0,1}/)
{
$strPath = PATH_DB;
}
# Else if backup type
elsif ($strType =~ /^backup(\:.*){0,1}/)
{
$strPath = PATH_BACKUP;
}
# Else error when path type not recognized
else
{
confess &log(ASSERT, "no known path types in '${strType}'");
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'strPath', value => $strPath, trace => true}
);
}
####################################################################################################################################
# pathGet
# ??? Need to tackle the return paths in this function (i.e. there are to many ways to return)
####################################################################################################################################
sub pathGet
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strType, # Base type of the path to get (PATH_DB_ABSOLUTE, PATH_BACKUP_TMP, etc)
$strFile, # File to append to the base path (can include a path as well)
$bTemp # Return the temp file for this path type - only some types have temp files
) =
logDebugParam
(
__PACKAGE__ . '->pathGet', \@_,
{name => 'strType', trace => true},
{name => 'strFile', required => false, trace => true},
{name => 'bTemp', default => false, trace => true}
);
# Is this an absolute path type?
my $bAbsolute = $strType =~ /.*absolute.*/;
# Make sure a temp file is valid for this type and file
if ($bTemp)
{
# Only allow temp files for PATH_BACKUP_ARCHIVE, PATH_BACKUP_ARCHIVE_OUT, PATH_BACKUP_TMP and any absolute path
if (!($strType eq PATH_BACKUP_ARCHIVE || $strType eq PATH_BACKUP_ARCHIVE_OUT || $strType eq PATH_BACKUP_TMP || $bAbsolute))
{
confess &log(ASSERT, 'temp file not supported for path type ' . $strType);
}
# The file must be defined
if (!defined($strFile))
{
confess &log(ASSERT, 'strFile must be defined when temp file requested');
}
}
# Get absolute path
if ($bAbsolute)
{
# Make sure that any absolute path starts with /, otherwise it will actually be relative
if ($strFile !~ /^\/.*/)
{
confess &log(ASSERT, "absolute path ${strType}:${strFile} must start with /");
}
if (defined($bTemp) && $bTemp)
{
return $strFile . '.backrest.tmp';
}
return $strFile;
}
# Make sure the base backup path is defined (since all other path types are backup)
if (!defined($self->{strBackupPath}))
{
confess &log(ASSERT, 'strBackupPath not defined');
}
# Get base backup path
if ($strType eq PATH_BACKUP)
{
return $self->{strBackupPath} . (defined($strFile) ? "/${strFile}" : '');
}
# Make sure the cluster is defined
if (!defined($self->{strStanza}))
{
confess &log(ASSERT, 'strStanza not defined');
}
# Get the backup tmp path
if ($strType eq PATH_BACKUP_TMP)
{
return
"$self->{strBackupPath}/temp/$self->{strStanza}.tmp" . (defined($strFile) ? "/${strFile}" : '') .
($bTemp ? (defined($self->{iThreadIdx}) ? ".$self->{iThreadIdx}" : '') . '.tmp' : '');
}
# Get the backup archive path
if ($strType eq PATH_BACKUP_ARCHIVE_OUT || $strType eq PATH_BACKUP_ARCHIVE)
{
my $strArchivePath = "$self->{strBackupPath}/archive/$self->{strStanza}";
if (!defined($strFile))
{
return $strArchivePath;
}
if ($strType eq PATH_BACKUP_ARCHIVE)
{
my $strArchiveId = (split('/', $strFile))[0];
my $strArchiveFile = (split('/', $strFile))[1];
if (!defined($strArchiveFile))
{
return "${strArchivePath}/${strFile}";
}
my $strArchive = substr(basename($strArchiveFile), 0, 24);
if ($strArchive !~ /^([0-F]){24}$/)
{
return "${strArchivePath}/${strFile}";
}
$strArchivePath = "${strArchivePath}/${strArchiveId}/" . substr($strArchive, 0, 16) . "/${strArchiveFile}";
}
else
{
$strArchivePath = "${strArchivePath}/out" . (defined($strFile) ? '/' . $strFile : '');
}
if ($bTemp)
{
if (!defined($strFile))
{
confess &log(ASSERT, 'archive temp must have strFile defined');
}
$strArchivePath = "${strArchivePath}.tmp";
}
return $strArchivePath;
}
if ($strType eq PATH_BACKUP_CLUSTER)
{
return $self->{strBackupPath} . "/backup/$self->{strStanza}" . (defined($strFile) ? "/${strFile}" : '');
}
# Error when path type not recognized
confess &log(ASSERT, "no known path types in '${strType}'");
}
####################################################################################################################################
# isRemote
#
# Determine whether the path type is remote
####################################################################################################################################
sub isRemote
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strPathType
) =
logDebugParam
(
__PACKAGE__ . '->isRemote', \@_,
{name => 'strPathType', trace => true}
);
my $bRemote = $self->{oProtocol}->isRemote() && $self->{oProtocol}->remoteTypeTest($self->pathTypeGet($strPathType));
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'bRemote', value => $bRemote, trace => true}
);
}
####################################################################################################################################
# linkCreate
####################################################################################################################################
sub linkCreate
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strSourcePathType,
$strSourceFile,
$strDestinationPathType,
$strDestinationFile,
$bHard,
$bRelative,
$bPathCreate
) =
logDebugParam
(
__PACKAGE__ . '->linkCreate', \@_,
{name => 'strSourcePathType'},
{name => 'strSourceFile'},
{name => 'strDestinationPathType'},
{name => 'strDestinationFile'},
{name => 'bHard', default => false},
{name => 'bRelative', default => false},
{name => 'bPathCreate', default => true}
);
# Source and destination path types must be the same (e.g. both PATH_DB or both PATH_BACKUP, etc.)
if ($self->pathTypeGet($strSourcePathType) ne $self->pathTypeGet($strDestinationPathType))
{
confess &log(ASSERT, 'path types must be equal in link create');
}
# Generate source and destination files
my $strSource = $self->pathGet($strSourcePathType, $strSourceFile);
my $strDestination = $self->pathGet($strDestinationPathType, $strDestinationFile);
# Run remotely
if ($self->isRemote($strSourcePathType))
{
confess &log(ASSERT, "${strOperation}: remote operation not supported");
}
# Run locally
else
{
# If the destination path is backup and does not exist, create it
# ??? This should only happen when the link create errors
if ($bPathCreate && $self->pathTypeGet($strDestinationPathType) eq PATH_BACKUP)
{
filePathCreate(dirname($strDestination));
}
unless (-e $strSource)
{
if (-e $strSource . ".$self->{strCompressExtension}")
{
$strSource .= ".$self->{strCompressExtension}";
$strDestination .= ".$self->{strCompressExtension}";
}
else
{
# Error when a hardlink will be created on a missing file
if ($bHard)
{
confess &log(ASSERT, "unable to find ${strSource}(.$self->{strCompressExtension}) for link");
}
}
}
# Generate relative path if requested
if ($bRelative)
{
my $iCommonLen = commonPrefix($strSource, $strDestination);
if ($iCommonLen != 0)
{
$strSource = ('../' x substr($strDestination, $iCommonLen) =~ tr/\///) . substr($strSource, $iCommonLen);
}
logDebugMisc
(
$strOperation, 'apply relative path',
{name => 'strSource', value => $strSource, trace => true}
);
}
if ($bHard)
{
link($strSource, $strDestination)
or confess &log(ERROR, "unable to create hardlink from ${strSource} to ${strDestination}");
}
else
{
symlink($strSource, $strDestination)
or confess &log(ERROR, "unable to create symlink from ${strSource} to ${strDestination}");
}
}
# Return from function and log return values if any
return logDebugReturn($strOperation);
}
####################################################################################################################################
# move
#
# Moves a file locally or remotely.
####################################################################################################################################
sub move
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strSourcePathType,
$strSourceFile,
$strDestinationPathType,
$strDestinationFile,
$bDestinationPathCreate
) =
logDebugParam
(
__PACKAGE__ . '->move', \@_,
{name => 'strSourcePathType'},
{name => 'strSourceFile', required => false},
{name => 'strDestinationPathType'},
{name => 'strDestinationFile'},
{name => 'bDestinationPathCreate', default => false}
);
# Source and destination path types must be the same
if ($self->pathTypeGet($strSourcePathType) ne $self->pathTypeGet($strSourcePathType))
{
confess &log(ASSERT, 'source and destination path types must be equal');
}
# Set operation variables
my $strPathOpSource = $self->pathGet($strSourcePathType, $strSourceFile);
my $strPathOpDestination = $self->pathGet($strDestinationPathType, $strDestinationFile);
# Run remotely
if ($self->isRemote($strSourcePathType))
{
confess &log(ASSERT, "${strOperation}: remote operation not supported");
}
# Run locally
else
{
fileMove($strPathOpSource, $strPathOpDestination, $bDestinationPathCreate);
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation
);
}
####################################################################################################################################
# compress
####################################################################################################################################
sub compress
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strPathType,
$strFile,
$bRemoveSource
) =
logDebugParam
(
__PACKAGE__ . '->compress', \@_,
{name => 'strPathType'},
{name => 'strFile'},
{name => 'bRemoveSource', default => true}
);
# Set operation variables
my $strPathOp = $self->pathGet($strPathType, $strFile);
# Run remotely
if ($self->isRemote($strPathType))
{
confess &log(ASSERT, "${strOperation}: remote operation not supported");
}
# Run locally
else
{
# Use copy to compress the file
$self->copy($strPathType, $strFile, $strPathType, "${strFile}.gz", false, true);
# Remove the old file
if ($bRemoveSource)
{
fileRemove($strPathOp);
}
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation
);
}
####################################################################################################################################
# pathCreate
#
# Creates a path locally or remotely.
####################################################################################################################################
sub pathCreate
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strPathType,
$strPath,
$strMode,
$bIgnoreExists,
$bCreateParents
) =
logDebugParam
(
__PACKAGE__ . '->pathCreate', \@_,
{name => 'strPathType'},
{name => 'strPath', required => false},
{name => 'strMode', default => '0750'},
{name => 'bIgnoreExists', default => false},
{name => 'bCreateParents', default => false}
);
# Set operation variables
my $strPathOp = $self->pathGet($strPathType, $strPath);
if ($self->isRemote($strPathType))
{
# Build param hash
my %oParamHash;
$oParamHash{path} = ${strPathOp};
if (defined($strMode))
{
$oParamHash{mode} = ${strMode};
}
# Execute the command
$self->{oProtocol}->cmdExecute(OP_FILE_PATH_CREATE, \%oParamHash);
}
else
{
filePathCreate($strPathOp, $strMode, $bIgnoreExists, $bCreateParents);
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation
);
}
####################################################################################################################################
# exists
#
# Checks for the existence of a file, but does not imply that the file is readable/writeable.
#
# Return: true if file exists, false otherwise
####################################################################################################################################
sub exists
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strPathType,
$strPath
) =
logDebugParam
(
__PACKAGE__ . '->exists', \@_,
{name => 'strPathType'},
{name => 'strPath', required => false}
);
# Set operation variables
my $strPathOp = $self->pathGet($strPathType, $strPath);
my $bExists = true;
# Run remotely
if ($self->isRemote($strPathType))
{
# Build param hash
my %oParamHash;
$oParamHash{path} = $strPathOp;
# Execute the command
$bExists = $self->{oProtocol}->cmdExecute(OP_FILE_EXISTS, \%oParamHash, true) eq 'Y' ? true : false;
}
# Run locally
else
{
$bExists = fileExists($strPathOp);
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'bExists', value => $bExists}
);
}
####################################################################################################################################
# remove
####################################################################################################################################
sub remove
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strPathType,
$strPath,
$bTemp,
$bIgnoreMissing
) =
logDebugParam
(
__PACKAGE__ . '->remove', \@_,
{name => 'strPathType'},
{name => 'strPath'},
{name => 'bTemp', required => false},
{name => 'bIgnoreMissing', default => true}
);
# Set operation variables
my $strPathOp = $self->pathGet($strPathType, $strPath, $bTemp);
my $bRemoved = true;
# Run remotely
if ($self->isRemote($strPathType))
{
confess &log(ASSERT, "${strOperation}: remote operation not supported");
}
# Run locally
else
{
$bRemoved = fileRemove($strPathOp, $bIgnoreMissing);
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'bRemoved', value => $bRemoved}
);
}
####################################################################################################################################
# hash
####################################################################################################################################
sub hash
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strPathType,
$strFile,
$bCompressed,
$strHashType
) =
logDebugParam
(
__PACKAGE__ . '->hash', \@_,
{name => 'strPathType'},
{name => 'strFile'},
{name => 'bCompressed', required => false},
{name => 'strHashType', required => false}
);
my ($strHash) = $self->hashSize($strPathType, $strFile, $bCompressed, $strHashType);
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'strHash', value => $strHash, trace => true}
);
}
####################################################################################################################################
# hashSize
####################################################################################################################################
sub hashSize
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strPathType,
$strFile,
$bCompressed,
$strHashType
) =
logDebugParam
(
__PACKAGE__ . '->hashSize', \@_,
{name => 'strPathType'},
{name => 'strFile'},
{name => 'bCompressed', default => false},
{name => 'strHashType', default => 'sha1'}
);
# Set operation variables
my $strFileOp = $self->pathGet($strPathType, $strFile);
my $strHash;
my $iSize = 0;
if ($self->isRemote($strPathType))
{
confess &log(ASSERT, "${strOperation}: remote operation not supported");
}
else
{
($strHash, $iSize) = fileHashSize($strFileOp, $bCompressed, $strHashType, $self->{oProtocol});
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'strHash', value => $strHash},
{name => 'iSize', value => $iSize}
);
}
####################################################################################################################################
# owner
####################################################################################################################################
sub owner
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strPathType,
$strFile,
$strUser,
$strGroup
) =
logDebugParam
(
__PACKAGE__ . '->owner', \@_,
{name => 'strPathType'},
{name => 'strFile'},
{name => 'strUser'},
{name => 'strGroup'}
);
# Set operation variables
my $strFileOp = $self->pathGet($strPathType, $strFile);
if ($self->isRemote($strPathType))
{
confess &log(ASSERT, "${strOperation}: remote operation not supported");
}
else
{
my $iUserId;
my $iGroupId;
my $oStat;
if (!defined($strUser) || !defined($strGroup))
{
$oStat = stat($strFileOp);
if (!defined($oStat))
{
confess &log(ERROR, 'unable to stat ${strFileOp}');
}
}
if (defined($strUser))
{
$iUserId = getpwnam($strUser);
}
else
{
$iUserId = $oStat->uid;
}
if (defined($strGroup))
{
$iGroupId = getgrnam($strGroup);
}
else
{
$iGroupId = $oStat->gid;
}
chown($iUserId, $iGroupId, $strFileOp)
or confess &log(ERROR, "unable to set ownership for ${strFileOp}");
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation
);
}
####################################################################################################################################
# list
####################################################################################################################################
sub list
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strPathType,
$strPath,
$strExpression,
$strSortOrder,
$bIgnoreMissing
) =
logDebugParam
(
__PACKAGE__ . '->list', \@_,
{name => 'strPathType'},
{name => 'strPath', required => false},
{name => 'strExpression', required => false},
{name => 'strSortOrder', default => 'forward'},
{name => 'bIgnoreMissing', default => false}
);
# Set operation variables
my $strPathOp = $self->pathGet($strPathType, $strPath);
my @stryFileList;
# Run remotely
if ($self->isRemote($strPathType))
{
# Build param hash
my %oParamHash;
$oParamHash{path} = $strPathOp;
$oParamHash{sort_order} = $strSortOrder;
$oParamHash{ignore_missing} = ${bIgnoreMissing};
if (defined($strExpression))
{
$oParamHash{expression} = $strExpression;
}
# Execute the command
my $strOutput = $self->{oProtocol}->cmdExecute(OP_FILE_LIST, \%oParamHash);
if (defined($strOutput))
{
@stryFileList = split(/\n/, $strOutput);
}
}
# Run locally
else
{
@stryFileList = fileList($strPathOp, $strExpression, $strSortOrder, $bIgnoreMissing);
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'stryFileList', value => \@stryFileList}
);
}
####################################################################################################################################
# wait
#
# Wait until the next second. This is done in the file object because it must be performed on whichever side the db is on, local or
# remote. This function is used to make sure that no files are copied in the same second as the manifest is created. The reason is
# that the db might modify they file again in the same second as the copy and that change will not be visible to a subsequent
# incremental backup using timestamp/size to determine deltas.
####################################################################################################################################
sub wait
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strPathType,
$bWait
) =
logDebugParam
(
__PACKAGE__ . '->wait', \@_,
{name => 'strPathType'},
{name => 'bWait', default => true}
);
# Second when the function was called
my $lTimeBegin;
# Run remotely
if ($self->isRemote($strPathType))
{
# Build param hash
my %oParamHash;
$oParamHash{wait} = $bWait;
# Execute the command
$lTimeBegin = $self->{oProtocol}->cmdExecute(OP_FILE_WAIT, \%oParamHash, true);
}
# Run locally
else
{
# Wait the remainder of the current second
$lTimeBegin = waitRemainder($bWait);
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'lTimeBegin', value => $lTimeBegin, trace => true}
);
}
####################################################################################################################################
# manifest
#
# Builds a path/file manifest starting with the base path and including all subpaths. The manifest contains all the information
# needed to perform a backup or a delta with a previous backup.
####################################################################################################################################
sub manifest
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strPathType,
$strPath,
$oManifestHashRef
) =
logDebugParam
(
__PACKAGE__ . '->manifest', \@_,
{name => 'strPathType'},
{name => 'strPath', required => false},
{name => 'oManifestHashRef'}
);
# Set operation variables
my $strPathOp = $self->pathGet($strPathType, $strPath);
# Run remotely
if ($self->isRemote($strPathType))
{
# Build param hash
my %oParamHash;
$oParamHash{path} = $strPathOp;
# Execute the command
dataHashBuild($oManifestHashRef, $self->{oProtocol}->cmdExecute(OP_FILE_MANIFEST, \%oParamHash, true), "\t");
}
# Run locally
else
{
$self->manifestRecurse($strPathType, $strPathOp, undef, 0, $oManifestHashRef);
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation
);
}
sub manifestRecurse
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strPathType,
$strPathOp,
$strPathFileOp,
$iDepth,
$oManifestHashRef
) =
logDebugParam
(
__PACKAGE__ . '->manifestRecurse', \@_,
{name => 'strPathType'},
{name => 'strPathOp'},
{name => 'strPathFileOp', required => false},
{name => 'iDepth'},
{name => 'oManifestHashRef', required => false}
);
# Set operation and debug strings
my $strPathRead = $strPathOp . (defined($strPathFileOp) ? "/${strPathFileOp}" : '');
my $hPath;
my $strFilter;
# If this is the top level stat the path to discover if it is actually a file
if ($iDepth == 0 && !S_ISDIR((fileStat($strPathRead))->mode))
{
$strFilter = basename($strPathRead);
$strPathRead = dirname($strPathRead);
}
# Open the path
if (!opendir($hPath, $strPathRead))
{
my $strError = "${strPathRead} could not be read: " . $!;
my $iErrorCode = COMMAND_ERR_PATH_READ;
# If the path does not exist and is not the root path requested then return, else error
# It's OK for paths to go away during execution (databases are a dynamic thing!)
if (!$self->exists(PATH_ABSOLUTE, $strPathRead))
{
if ($iDepth != 0)
{
return;
}
$strError = "${strPathRead} does not exist";
$iErrorCode = COMMAND_ERR_PATH_MISSING;
}
if ($strPathType eq PATH_ABSOLUTE)
{
confess &log(ERROR, $strError, $iErrorCode);
}
confess &log(ERROR, $strError);
}
# Get a list of all files in the path (except ..)
my @stryFileList = grep(!/^\..$/i, readdir($hPath));
close($hPath);
# Loop through all subpaths/files in the path
foreach my $strFile (sort(@stryFileList))
{
# Skip this file if it does not match the filter
if (defined($strFilter) && $strFile ne $strFilter)
{
next;
}
my $strPathFile = "${strPathRead}/$strFile";
my $bCurrentDir = $strFile eq '.';
# Create the file and path names
if ($iDepth != 0)
{
if ($bCurrentDir)
{
$strFile = $strPathFileOp;
$strPathFile = $strPathRead;
}
else
{
$strFile = "${strPathFileOp}/${strFile}";
}
}
# Stat the path/file
my $oStat = lstat($strPathFile);
# Check for errors in stat
if (!defined($oStat))
{
my $strError = "${strPathFile} could not be read: " . $!;
my $iErrorCode = COMMAND_ERR_FILE_READ;
# If the file does not exist then go to the next file, else error
# It's OK for files to go away during execution (databases are a dynamic thing!)
if (!$self->exists(PATH_ABSOLUTE, $strPathFile))
{
next;
}
if ($strPathType eq PATH_ABSOLUTE)
{
confess &log(ERROR, $strError, $iErrorCode);
}
confess &log(ERROR, $strError);
}
# Check for regular file
if (S_ISREG($oStat->mode))
{
${$oManifestHashRef}{name}{"${strFile}"}{type} = 'f';
# Get inode
${$oManifestHashRef}{name}{"${strFile}"}{inode} = $oStat->ino;
# Get size
${$oManifestHashRef}{name}{"${strFile}"}{size} = $oStat->size;
# Get modification time
${$oManifestHashRef}{name}{"${strFile}"}{modification_time} = $oStat->mtime;
}
# Check for directory
elsif (S_ISDIR($oStat->mode))
{
${$oManifestHashRef}{name}{"${strFile}"}{type} = 'd';
}
# Check for link
elsif (S_ISLNK($oStat->mode))
{
${$oManifestHashRef}{name}{"${strFile}"}{type} = 'l';
# Get link destination
${$oManifestHashRef}{name}{"${strFile}"}{link_destination} = readlink($strPathFile);
if (!defined(${$oManifestHashRef}{name}{"${strFile}"}{link_destination}))
{
if (-e $strPathFile)
{
my $strError = "${strPathFile} error reading link: " . $!;
if ($strPathType eq PATH_ABSOLUTE)
{
print $strError;
exit COMMAND_ERR_LINK_READ;
}
confess &log(ERROR, $strError);
}
}
}
# Not a recognized type
else
{
my $strError = "${strPathFile} is not of type directory, file, or link";
if ($strPathType eq PATH_ABSOLUTE)
{
print $strError;
exit COMMAND_ERR_FILE_TYPE;
}
confess &log(ERROR, $strError);
}
# Get user name
${$oManifestHashRef}{name}{"${strFile}"}{user} = getpwuid($oStat->uid);
# Get group name
${$oManifestHashRef}{name}{"${strFile}"}{group} = getgrgid($oStat->gid);
# Get mode
if (${$oManifestHashRef}{name}{"${strFile}"}{type} ne 'l')
{
${$oManifestHashRef}{name}{"${strFile}"}{mode} = sprintf('%04o', S_IMODE($oStat->mode));
}
# Recurse into directories
if (${$oManifestHashRef}{name}{"${strFile}"}{type} eq 'd' && !$bCurrentDir)
{
$self->manifestRecurse($strPathType, $strPathOp, $strFile, $iDepth + 1, $oManifestHashRef);
}
}
# Return from function and log return values if any
return logDebugReturn($strOperation);
}
####################################################################################################################################
# copy
#
# Copies a file from one location to another:
#
# * source and destination can be local or remote
# * wire and output compression/decompression are supported
# * intermediate temp files are used to prevent partial copies
# * modification time, mode, and ownership can be set on destination file
# * destination path can optionally be created
####################################################################################################################################
sub copy
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strSourcePathType,
$strSourceFile,
$strDestinationPathType,
$strDestinationFile,
$bSourceCompressed,
$bDestinationCompress,
$bIgnoreMissingSource,
$lModificationTime,
$strMode,
$bDestinationPathCreate,
$strUser,
$strGroup,
$bAppendChecksum
) =
logDebugParam
(
__PACKAGE__ . '->copy', \@_,
{name => 'strSourcePathType'},
{name => 'strSourceFile', required => false},
{name => 'strDestinationPathType'},
{name => 'strDestinationFile', required => false},
{name => 'bSourceCompressed', default => false},
{name => 'bDestinationCompress', default => false},
{name => 'bIgnoreMissingSource', default => false},
{name => 'lModificationTime', required => false},
{name => 'strMode', default => '0640'},
{name => 'bDestinationPathCreate', default => false},
{name => 'strUser', required => false},
{name => 'strGroup', required => false},
{name => 'bAppendChecksum', default => false}
);
# Set working variables
my $bSourceRemote = $self->isRemote($strSourcePathType) || $strSourcePathType eq PIPE_STDIN;
my $bDestinationRemote = $self->isRemote($strDestinationPathType) || $strDestinationPathType eq PIPE_STDOUT;
my $strSourceOp = $strSourcePathType eq PIPE_STDIN ?
$strSourcePathType : $self->pathGet($strSourcePathType, $strSourceFile);
my $strDestinationOp = $strDestinationPathType eq PIPE_STDOUT ?
$strDestinationPathType : $self->pathGet($strDestinationPathType, $strDestinationFile);
my $strDestinationTmpOp = $strDestinationPathType eq PIPE_STDOUT ?
undef : $self->pathGet($strDestinationPathType, $strDestinationFile, true);
# Checksum and size variables
my $strChecksum = undef;
my $iFileSize = undef;
my $bResult = true;
# Open the source and destination files (if needed)
my $hSourceFile;
my $hDestinationFile;
if (!$bSourceRemote)
{
if (!sysopen($hSourceFile, $strSourceOp, O_RDONLY))
{
my $strError = $!;
my $iErrorCode = COMMAND_ERR_FILE_READ;
if ($!{ENOENT})
{
# $strError = 'file is missing';
$iErrorCode = COMMAND_ERR_FILE_MISSING;
if ($bIgnoreMissingSource && $strDestinationPathType ne PIPE_STDOUT)
{
return false, undef, undef;
}
}
$strError = "cannot open source file ${strSourceOp}: " . $strError;
if ($strSourcePathType eq PATH_ABSOLUTE)
{
if ($strDestinationPathType eq PIPE_STDOUT)
{
$self->{oProtocol}->binaryXferAbort();
}
confess &log(ERROR, $strError, $iErrorCode);
}
confess &log(ERROR, $strError, $iErrorCode);
}
}
if (!$bDestinationRemote)
{
my $iCreateFlag = O_WRONLY | O_CREAT;
# Open the destination temp file
if (!sysopen($hDestinationFile, $strDestinationTmpOp, $iCreateFlag))
{
if ($bDestinationPathCreate)
{
filePathCreate(dirname($strDestinationTmpOp), undef, true, true);
}
if (!$bDestinationPathCreate || !sysopen($hDestinationFile, $strDestinationTmpOp, $iCreateFlag))
{
my $strError = "unable to open ${strDestinationTmpOp}: " . $!;
my $iErrorCode = COMMAND_ERR_FILE_READ;
if (!fileExists(dirname($strDestinationTmpOp)))
{
$strError = dirname($strDestinationTmpOp) . ' destination path does not exist';
$iErrorCode = COMMAND_ERR_FILE_MISSING;
}
if (!($bDestinationPathCreate && $iErrorCode == COMMAND_ERR_FILE_MISSING))
{
if ($strSourcePathType eq PATH_ABSOLUTE)
{
confess &log(ERROR, $strError, $iErrorCode);
}
confess &log(ERROR, $strError);
}
}
}
# Now lock the file to be sure nobody else is operating on it
if (!flock($hDestinationFile, LOCK_EX | LOCK_NB))
{
confess &log(ERROR, "unable to acquire exclusive lock on ${strDestinationTmpOp}", ERROR_LOCK_ACQUIRE);
}
}
# If source or destination are remote
if ($bSourceRemote || $bDestinationRemote)
{
# Build the command and open the local file
my $hFile;
my %oParamHash;
my $hIn,
my $hOut;
my $strRemote;
my $strRemoteOp;
# If source is remote and destination is local
if ($bSourceRemote && !$bDestinationRemote)
{
$hOut = $hDestinationFile;
$strRemoteOp = OP_FILE_COPY_OUT;
$strRemote = 'in';
if ($strSourcePathType ne PIPE_STDIN)
{
$oParamHash{source_file} = $strSourceOp;
$oParamHash{source_compressed} = $bSourceCompressed;
$oParamHash{destination_compress} = $bDestinationCompress;
}
}
# Else if source is local and destination is remote
elsif (!$bSourceRemote && $bDestinationRemote)
{
$hIn = $hSourceFile;
$strRemoteOp = OP_FILE_COPY_IN;
$strRemote = 'out';
if ($strDestinationPathType ne PIPE_STDOUT)
{
$oParamHash{destination_file} = $strDestinationOp;
$oParamHash{source_compressed} = $bSourceCompressed;
$oParamHash{destination_compress} = $bDestinationCompress;
$oParamHash{destination_path_create} = $bDestinationPathCreate;
if (defined($strMode))
{
$oParamHash{mode} = $strMode;
}
if (defined($strUser))
{
$oParamHash{user} = $strUser;
}
if (defined($strGroup))
{
$oParamHash{group} = $strGroup;
}
if ($bAppendChecksum)
{
$oParamHash{append_checksum} = true;
}
}
}
# Else source and destination are remote
else
{
$strRemoteOp = OP_FILE_COPY;
$oParamHash{source_file} = $strSourceOp;
$oParamHash{source_compressed} = $bSourceCompressed;
$oParamHash{destination_file} = $strDestinationOp;
$oParamHash{destination_compress} = $bDestinationCompress;
$oParamHash{destination_path_create} = $bDestinationPathCreate;
if (defined($strMode))
{
$oParamHash{mode} = $strMode;
}
if (defined($strUser))
{
$oParamHash{user} = $strUser;
}
if (defined($strGroup))
{
$oParamHash{group} = $strGroup;
}
if ($bIgnoreMissingSource)
{
$oParamHash{ignore_missing_source} = $bIgnoreMissingSource;
}
if ($bAppendChecksum)
{
$oParamHash{append_checksum} = true;
}
}
# If an operation is defined then write it
if (%oParamHash)
{
$self->{oProtocol}->cmdWrite($strRemoteOp, \%oParamHash);
}
# Transfer the file (skip this for copies where both sides are remote)
if ($strRemoteOp ne OP_FILE_COPY)
{
($strChecksum, $iFileSize) =
$self->{oProtocol}->binaryXfer($hIn, $hOut, $strRemote, $bSourceCompressed, $bDestinationCompress);
}
# If this is the controlling process then wait for OK from remote
if (%oParamHash)
{
# Test for an error when reading output
my $strOutput;
eval
{
$strOutput = $self->{oProtocol}->outputRead(true, $bIgnoreMissingSource);
# Check the result of the remote call
if (substr($strOutput, 0, 1) eq 'Y')
{
# If the operation was purely remote, get checksum/size
if ($strRemoteOp eq OP_FILE_COPY ||
$strRemoteOp eq OP_FILE_COPY_IN && $bSourceCompressed && !$bDestinationCompress)
{
# Checksum shouldn't already be set
if (defined($strChecksum) || defined($iFileSize))
{
confess &log(ASSERT, "checksum and size are already defined, but shouldn't be");
}
# Parse output and check to make sure tokens are defined
my @stryToken = split(/ /, $strOutput);
if (!defined($stryToken[1]) || !defined($stryToken[2]) ||
$stryToken[1] eq '?' && $stryToken[2] eq '?')
{
confess &log(ERROR, "invalid return from copy" . (defined($strOutput) ? ": ${strOutput}" : ''));
}
# Read the checksum and size
if ($stryToken[1] ne '?')
{
$strChecksum = $stryToken[1];
}
if ($stryToken[2] ne '?')
{
$iFileSize = $stryToken[2];
}
}
}
# Remote called returned false
else
{
$bResult = false;
}
};
# If there is an error then evaluate
if ($@)
{
my $oMessage = $@;
# We'll ignore this error if the source file was missing and missing file exception was returned
# and bIgnoreMissingSource is set
if ($bIgnoreMissingSource && $strRemote eq 'in' && blessed($oMessage) && $oMessage->isa('pgBackRest::Common::Exception') &&
$oMessage->code() == COMMAND_ERR_FILE_MISSING)
{
close($hDestinationFile)
or confess &log(ERROR, "cannot close file ${strDestinationTmpOp}");
fileRemove($strDestinationTmpOp);
return false, undef, undef;
}
confess $oMessage;
}
}
}
# Else this is a local operation
else
{
# If the source is not compressed and the destination is then compress
if (!$bSourceCompressed && $bDestinationCompress)
{
($strChecksum, $iFileSize) =
$self->{oProtocol}->binaryXfer($hSourceFile, $hDestinationFile, 'out', false, true, false);
}
# If the source is compressed and the destination is not then decompress
elsif ($bSourceCompressed && !$bDestinationCompress)
{
($strChecksum, $iFileSize) =
$self->{oProtocol}->binaryXfer($hSourceFile, $hDestinationFile, 'in', true, false, false);
}
# Else both side are compressed, so copy capturing checksum
elsif ($bSourceCompressed)
{
($strChecksum, $iFileSize) =
$self->{oProtocol}->binaryXfer($hSourceFile, $hDestinationFile, 'out', true, true, false);
}
else
{
($strChecksum, $iFileSize) =
$self->{oProtocol}->binaryXfer($hSourceFile, $hDestinationFile, 'in', false, true, false);
}
}
# Close the source file (if local)
if (defined($hSourceFile))
{
close($hSourceFile) or confess &log(ERROR, "cannot close file ${strSourceOp}");
}
# Sync and close the destination file (if local)
if (defined($hDestinationFile))
{
$hDestinationFile->sync()
or confess &log(ERROR, "unable to sync ${strDestinationTmpOp}", ERROR_FILE_SYNC);
close($hDestinationFile)
or confess &log(ERROR, "cannot close file ${strDestinationTmpOp}");
}
# Checksum and file size should be set if the destination is not remote
if ($bResult &&
!(!$bSourceRemote && $bDestinationRemote && $bSourceCompressed) &&
(!defined($strChecksum) || !defined($iFileSize)))
{
confess &log(ASSERT, 'checksum or file size not set');
}
# Where the destination is local, set mode, modification time, and perform move to final location
if ($bResult && !$bDestinationRemote)
{
# Set the file Mode if required
if (defined($strMode))
{
chmod(oct($strMode), $strDestinationTmpOp)
or confess &log(ERROR, "unable to set mode for local ${strDestinationTmpOp}");
}
# Set the file modification time if required
if (defined($lModificationTime))
{
utime($lModificationTime, $lModificationTime, $strDestinationTmpOp)
or confess &log(ERROR, "unable to set time for local ${strDestinationTmpOp}");
}
# set user and/or group if required
if (defined($strUser) || defined($strGroup))
{
$self->owner(PATH_ABSOLUTE, $strDestinationTmpOp, $strUser, $strGroup);
}
# Replace checksum in destination filename (if exists)
if ($bAppendChecksum)
{
# Replace destination filename
if ($bDestinationCompress)
{
$strDestinationOp =
substr($strDestinationOp, 0, length($strDestinationOp) - length($self->{strCompressExtension}) - 1) .
'-' . $strChecksum . '.' . $self->{strCompressExtension};
}
else
{
$strDestinationOp .= '-' . $strChecksum;
}
}
# Move the file from tmp to final destination
fileMove($strDestinationTmpOp, $strDestinationOp, $bDestinationPathCreate);
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'bResult', value => $bResult, trace => true},
{name => 'strChecksum', value => $strChecksum, trace => true},
{name => 'iFileSize', value => $iFileSize, trace => true}
);
}
1;