mirror of
https://github.com/pgbackrest/pgbackrest.git
synced 2024-12-14 10:13:05 +02:00
330 lines
13 KiB
Perl
330 lines
13 KiB
Perl
####################################################################################################################################
|
|
# RESTORE MODULE
|
|
####################################################################################################################################
|
|
package BackRest::Restore;
|
|
|
|
use threads;
|
|
use threads::shared;
|
|
use Thread::Queue;
|
|
use strict;
|
|
use warnings;
|
|
use Carp;
|
|
|
|
use File::Basename;
|
|
|
|
use lib dirname($0);
|
|
use BackRest::Utility;
|
|
use BackRest::ThreadGroup;
|
|
use BackRest::Config;
|
|
use BackRest::File;
|
|
|
|
####################################################################################################################################
|
|
# Module variables
|
|
####################################################################################################################################
|
|
my @oyThreadQueue; # Queues to contain restore files
|
|
my %oManifest; # Manifest from the backup to restore
|
|
my $oFile; # File object to use for operations (also to clone for threads)
|
|
|
|
####################################################################################################################################
|
|
# CONSTRUCTOR
|
|
####################################################################################################################################
|
|
sub new
|
|
{
|
|
my $class = shift; # Class name
|
|
my $strDbClusterPath = shift; # Database cluster path
|
|
my $strBackupPath = shift; # Backup to restore
|
|
my $oFileParam = shift; # Default file object
|
|
my $iThreadTotal = shift; # Total threads to run for restore
|
|
my $bForce = shift; # Force the restore even if files are present
|
|
|
|
# Create the class hash
|
|
my $self = {};
|
|
bless $self, $class;
|
|
|
|
# Initialize variables
|
|
$self->{strDbClusterPath} = $strDbClusterPath;
|
|
$oFile = $oFileParam;
|
|
$self->{thread_total} = $iThreadTotal;
|
|
$self->{bForce} = $bForce;
|
|
|
|
# If backup path is not specified then default to latest
|
|
if (defined($strBackupPath))
|
|
{
|
|
$self->{strBackupPath} = $strBackupPath;
|
|
}
|
|
else
|
|
{
|
|
$self->{strBackupPath} = PATH_LATEST;
|
|
}
|
|
|
|
return $self;
|
|
}
|
|
|
|
####################################################################################################################################
|
|
# RESTORE
|
|
####################################################################################################################################
|
|
sub restore
|
|
{
|
|
my $self = shift; # Class hash
|
|
|
|
# Make sure that Postgres is not running
|
|
if ($oFile->exists(PATH_DB_ABSOLUTE, $self->{strDbClusterPath} . '/' . FILE_POSTMASTER_PID))
|
|
{
|
|
confess &log(ERROR, 'unable to restore while Postgres is running');
|
|
}
|
|
|
|
# Make sure the backup path is valid and load the manifest
|
|
if ($oFile->exists(PATH_BACKUP_CLUSTER, $self->{strBackupPath}))
|
|
{
|
|
# Copy the backup manifest to the db cluster path
|
|
$oFile->copy(PATH_BACKUP_CLUSTER, $self->{strBackupPath} . '/' . FILE_MANIFEST,
|
|
PATH_DB_ABSOLUTE, $self->{strDbClusterPath} . '/' . FILE_MANIFEST);
|
|
|
|
# Load the manifest into a hash
|
|
ini_load($oFile->path_get(PATH_DB_ABSOLUTE, $self->{strDbClusterPath} . '/' . FILE_MANIFEST), \%oManifest);
|
|
|
|
# Remove the manifest now that it is in memory
|
|
$oFile->remove(PATH_DB_ABSOLUTE, $self->{strDbClusterPath} . '/' . FILE_MANIFEST);
|
|
|
|
# Set source compression
|
|
if ($oManifest{'backup:option'}{compress} eq 'y')
|
|
{
|
|
$self->{bSourceCompressed} = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
confess &log(ERROR, 'backup ' . $self->{strBackupPath} . ' does not exist');
|
|
}
|
|
|
|
# Declare thread queues that will be used to process files
|
|
my $iThreadQueueTotal = 0;
|
|
|
|
# Check each restore directory in the manifest and make sure that it exists and is empty.
|
|
# The --force option can be used to override the empty requirement.
|
|
foreach my $strPathKey (sort(keys $oManifest{'backup:path'}))
|
|
{
|
|
my $strPath = $oManifest{'backup:path'}{$strPathKey};
|
|
|
|
&log(INFO, "processing db path ${strPath}");
|
|
|
|
if (!$oFile->exists(PATH_DB_ABSOLUTE, $strPath))
|
|
{
|
|
confess &log(ERROR, "required db path '${strPath}' does not exist");
|
|
}
|
|
|
|
# Load path manifest so it can be compared to delete files/paths/links that are not in the backup
|
|
my %oPathManifest;
|
|
$oFile->manifest(PATH_DB_ABSOLUTE, $strPath, \%oPathManifest);
|
|
|
|
foreach my $strName (sort {$b cmp $a} (keys $oPathManifest{name}))
|
|
{
|
|
# Skip the root path
|
|
if ($strName eq '.')
|
|
{
|
|
next;
|
|
}
|
|
|
|
# If force was not specified then error if any file is found
|
|
if (!$self->{bForce})
|
|
{
|
|
confess &log(ERROR, "db path '${strPath}' contains files");
|
|
}
|
|
|
|
my $strFile = "${strPath}/${strName}";
|
|
|
|
# Determine the file/path/link type
|
|
my $strType = 'file';
|
|
|
|
if ($oPathManifest{name}{$strName}{type} eq 'd')
|
|
{
|
|
$strType = 'path';
|
|
}
|
|
elsif ($oPathManifest{name}{$strName}{type} eq 'l')
|
|
{
|
|
$strType = 'link';
|
|
}
|
|
|
|
# Check to see if the file/path/link exists in the manifest
|
|
if (defined($oManifest{"${strPathKey}:${strType}"}{$strName}))
|
|
{
|
|
my $strMode = $oManifest{"${strPathKey}:${strType}"}{$strName}{permission};
|
|
|
|
# If file/path mode does not match, fix it
|
|
if ($strType ne 'link' && $strMode ne $oPathManifest{name}{$strName}{permission})
|
|
{
|
|
&log(DEBUG, "setting ${strFile} mode to ${strMode}");
|
|
|
|
chmod(oct($strMode), $strFile)
|
|
or confess 'unable to set mode ${strMode} for ${strFile}';
|
|
}
|
|
|
|
my $strUser = $oManifest{"${strPathKey}:${strType}"}{$strName}{user};
|
|
my $strGroup = $oManifest{"${strPathKey}:${strType}"}{$strName}{group};
|
|
|
|
# If ownership does not match, fix it
|
|
if ($strUser ne $oPathManifest{name}{$strName}{user} ||
|
|
$strGroup ne $oPathManifest{name}{$strName}{group})
|
|
{
|
|
&log(DEBUG, "setting ${strFile} ownership to ${strUser}:${strGroup}");
|
|
|
|
# !!! Need to decide if it makes sense to set the user to anything other than the db owner
|
|
}
|
|
|
|
# If a link does not have the same destination, then delete it (it will be recreated later)
|
|
if ($strType eq 'link' && $oManifest{"${strPathKey}:${strType}"}{$strName}{link_destination} ne
|
|
$oPathManifest{name}{$strName}{link_destination})
|
|
{
|
|
&log(DEBUG, "removing link ${strFile} - destination changed");
|
|
unlink($strFile) or confess &log(ERROR, "unable to delete file ${strFile}");
|
|
}
|
|
}
|
|
# If it does not then remove it
|
|
else
|
|
{
|
|
# If a path then remove it, all the files should have already been deleted since we are going in reverse order
|
|
if ($strType eq 'path')
|
|
{
|
|
&log(DEBUG, "removing path ${strFile}");
|
|
rmdir($strFile) or confess &log(ERROR, "unable to delete path ${strFile}, is it empty?");
|
|
}
|
|
# Else delete a file/link
|
|
else
|
|
{
|
|
&log(DEBUG, "removing file ${strFile}");
|
|
unlink($strFile) or confess &log(ERROR, "unable to delete file ${strFile}");
|
|
}
|
|
}
|
|
}
|
|
|
|
# Create all paths in the manifest that do not already exist
|
|
foreach my $strName (sort (keys $oManifest{"${strPathKey}:path"}))
|
|
{
|
|
# Skip the root path
|
|
if ($strName eq '.')
|
|
{
|
|
next;
|
|
}
|
|
|
|
# Create the Path
|
|
$oFile->path_create(PATH_DB_ABSOLUTE, "${strPath}/${strName}",
|
|
$oManifest{"${strPathKey}:path"}{$strName}{permission});
|
|
}
|
|
|
|
# Create all links in the manifest that do not already exist
|
|
if (defined($oManifest{"${strPathKey}:link"}))
|
|
{
|
|
foreach my $strName (sort (keys $oManifest{"${strPathKey}:link"}))
|
|
{
|
|
if (!$oFile->exists(PATH_DB_ABSOLUTE, "${strPath}/${strName}"))
|
|
{
|
|
$oFile->link_create(PATH_DB_ABSOLUTE, $oManifest{"${strPathKey}:link"}{$strName}{link_destination},
|
|
PATH_DB_ABSOLUTE, "${strPath}/${strName}");
|
|
}
|
|
}
|
|
}
|
|
|
|
# Assign the files in each path to a thread queue
|
|
if (defined($oManifest{"${strPathKey}:file"}))
|
|
{
|
|
$oyThreadQueue[$iThreadQueueTotal] = Thread::Queue->new();
|
|
|
|
foreach my $strName (sort (keys $oManifest{"${strPathKey}:file"}))
|
|
{
|
|
$oyThreadQueue[$iThreadQueueTotal]->enqueue("${strPathKey}|${strName}");
|
|
}
|
|
|
|
$oyThreadQueue[$iThreadQueueTotal]->end();
|
|
$iThreadQueueTotal++;
|
|
}
|
|
}
|
|
|
|
# Create threads to process the thread queues
|
|
my $oThreadGroup = new BackRest::ThreadGroup();
|
|
|
|
for (my $iThreadIdx = 0; $iThreadIdx < $self->{thread_total}; $iThreadIdx++)
|
|
{
|
|
$oThreadGroup->add(threads->create(\&restore_thread, $iThreadIdx, $self->{thread_total}, $self->{strBackupPath},
|
|
$self->{bSourceCompressed}));
|
|
}
|
|
|
|
$oThreadGroup->complete();
|
|
}
|
|
|
|
sub restore_thread
|
|
{
|
|
my @args = @_;
|
|
|
|
my $iThreadIdx = $args[0]; # Defines the index of this thread
|
|
my $iThreadTotal = $args[1]; # Total threads running
|
|
my $strBackupPath = $args[2]; # Backup to restore files from
|
|
my $bSourceCompressed = $args[3]; # Backup to restore files from
|
|
|
|
my $iDirection = $iThreadIdx % 2 == 0 ? 1 : -1; # Size of files currently copied by this thread
|
|
my $oFileThread = $oFile->clone($iThreadIdx); # Thread local file object
|
|
|
|
# Initialize the starting and current queue index based in the total number of threads in relation to this thread
|
|
my $iQueueStartIdx = int((@oyThreadQueue / $iThreadTotal) * $iThreadIdx);
|
|
my $iQueueIdx = $iQueueStartIdx;
|
|
|
|
# When a KILL signal is received, immediately abort
|
|
$SIG{'KILL'} = sub {threads->exit();};
|
|
|
|
# Loop through all the queues to restore files (exit when the original queue is reached
|
|
do
|
|
{
|
|
while (my $strMessage = $oyThreadQueue[$iQueueIdx]->dequeue())
|
|
{
|
|
my $strSourcePath = (split(/\|/, $strMessage))[0]; # Source path from backup
|
|
my $strSection = "${strSourcePath}:file"; # Backup section with file info
|
|
my $strDestinationPath = $oManifest{'backup:path'}{$strSourcePath}; # Destination path stored in manifest
|
|
$strSourcePath =~ s/\:/\//g; # Replace : with / in source path
|
|
my $strName = (split(/\|/, $strMessage))[1]; # Name of file to be restored
|
|
|
|
# Generate destination file name
|
|
my $strDestinationFile = $oFileThread->path_get(PATH_DB_ABSOLUTE, "${strDestinationPath}/${strName}");
|
|
|
|
# If checksum is set the destination file already exists, try a checksum before copying
|
|
my $strChecksum = $oManifest{$strSection}{$strName}{checksum};
|
|
|
|
if ($oFileThread->exists(PATH_DB_ABSOLUTE, $strDestinationFile))
|
|
{
|
|
if (defined($strChecksum) && $oFileThread->hash(PATH_DB_ABSOLUTE, $strDestinationFile) eq $strChecksum)
|
|
{
|
|
&log(DEBUG, "${strDestinationFile} exists and matches backup checksum ${strChecksum}");
|
|
next;
|
|
}
|
|
|
|
$oFileThread->remove(PATH_DB_ABSOLUTE, $strDestinationFile);
|
|
}
|
|
|
|
# Copy the file from the backup to the database
|
|
$oFileThread->copy(PATH_BACKUP_CLUSTER, "${strBackupPath}/${strSourcePath}/${strName}" .
|
|
($bSourceCompressed ? '.' . $oFileThread->{strCompressExtension} : ''),
|
|
PATH_DB_ABSOLUTE, $strDestinationFile,
|
|
$bSourceCompressed, # Source is compressed based on backup settings
|
|
undef, undef,
|
|
$oManifest{$strSection}{$strName}{modification_time},
|
|
$oManifest{$strSection}{$strName}{permission});
|
|
}
|
|
|
|
# Even number threads move up when they have finished a queue, odd numbered threads move down
|
|
$iQueueIdx += $iDirection;
|
|
|
|
# Reset the queue index when it goes over or under the number of queues
|
|
if ($iQueueIdx < 0)
|
|
{
|
|
$iQueueIdx = @oyThreadQueue - 1;
|
|
}
|
|
elsif ($iQueueIdx >= @oyThreadQueue)
|
|
{
|
|
$iQueueIdx = 0;
|
|
}
|
|
}
|
|
while ($iQueueIdx != $iQueueStartIdx);
|
|
|
|
&log(DEBUG, "thread ${iThreadIdx} exiting");
|
|
}
|
|
|
|
1;
|