1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2024-12-14 10:13:05 +02:00
pgbackrest/pg_backrest_file.pm

1049 lines
38 KiB
Perl
Raw Normal View History

2014-02-03 03:03:05 +03:00
####################################################################################################################################
# FILE MODULE
####################################################################################################################################
package pg_backrest_file;
use threads;
2014-02-06 05:39:08 +03:00
use Moose;
2014-02-03 03:03:05 +03:00
use strict;
use warnings;
use Carp;
use Net::OpenSSH;
use IPC::Open3;
use File::Basename;
use IPC::System::Simple qw(capture);
use lib dirname($0);
use pg_backrest_utility;
use Exporter qw(import);
2014-02-06 05:39:08 +03:00
our @EXPORT = qw(PATH_DB PATH_DB_ABSOLUTE PATH_BACKUP PATH_BACKUP_ABSOLUTE PATH_BACKUP_CLUSTER PATH_BACKUP_TMP PATH_BACKUP_ARCHIVE);
2014-02-03 03:03:05 +03:00
# Extension and permissions
2014-02-06 05:39:08 +03:00
has strCompressExtension => (is => 'ro', default => 'gz');
has strDefaultPathPermission => (is => 'bare', default => '0750');
has strDefaultFilePermission => (is => 'ro', default => '0640');
2014-02-03 03:03:05 +03:00
# Command strings
2014-02-06 05:39:08 +03:00
has strCommandChecksum => (is => 'bare');
has strCommandCompress => (is => 'bare');
has strCommandDecompress => (is => 'bare');
has strCommandCat => (is => 'bare', default => 'cat %file%');
has strCommandManifest => (is => 'bare');
2014-02-04 04:48:02 +03:00
2014-04-28 16:13:25 +03:00
# Lock path
has strLockPath => (is => 'bare');
# Files to hold stderr
#has strBackupStdErrFile => (is => 'bare');
#has strDbStdErrFile => (is => 'bare');
2014-02-04 04:48:02 +03:00
# Module variables
2014-02-06 05:39:08 +03:00
has strDbUser => (is => 'bare'); # Database user
has strDbHost => (is => 'bare'); # Database host
has oDbSSH => (is => 'bare'); # Database SSH object
2014-02-04 04:48:02 +03:00
2014-02-06 05:39:08 +03:00
has strBackupUser => (is => 'bare'); # Backup user
has strBackupHost => (is => 'bare'); # Backup host
has oBackupSSH => (is => 'bare'); # Backup SSH object
has strBackupPath => (is => 'bare'); # Backup base path
has strBackupClusterPath => (is => 'bare'); # Backup cluster path
2014-02-03 03:03:05 +03:00
# Process flags
2014-02-06 05:39:08 +03:00
has bNoCompression => (is => 'bare');
has strStanza => (is => 'bare');
2014-02-11 23:31:16 +03:00
has iThreadIdx => (is => 'bare');
2014-02-03 03:03:05 +03:00
2014-04-28 16:13:25 +03:00
####################################################################################################################################
# PATH_GET Constants
####################################################################################################################################
use constant
{
PATH_DB => 'db',
PATH_DB_ABSOLUTE => 'db:absolute',
PATH_BACKUP => 'backup',
PATH_BACKUP_ABSOLUTE => 'backup:absolute',
PATH_BACKUP_CLUSTER => 'backup:cluster',
PATH_BACKUP_TMP => 'backup:tmp',
PATH_BACKUP_ARCHIVE => 'backup:archive',
PATH_LOCK_ERR => 'lock:err'
};
2014-02-07 00:37:37 +03:00
####################################################################################################################################
# CONSTRUCTOR
####################################################################################################################################
sub BUILD
2014-02-03 03:03:05 +03:00
{
2014-02-06 05:39:08 +03:00
my $self = shift;
2014-02-03 22:50:23 +03:00
# Make sure the backup path is defined
2014-02-06 05:39:08 +03:00
if (!defined($self->{strBackupPath}))
2014-02-03 22:50:23 +03:00
{
2014-02-04 03:03:17 +03:00
confess &log(ERROR, "common:backup_path undefined");
2014-02-03 22:50:23 +03:00
}
2014-02-06 05:39:08 +03:00
2014-02-03 22:50:23 +03:00
# Create the backup cluster path
$self->{strBackupClusterPath} = $self->{strBackupPath} . "/" . $self->{strStanza};
2014-02-03 22:50:23 +03:00
2014-02-17 03:01:06 +03:00
# Create the ssh options string
if (defined($self->{strBackupHost}) || defined($self->{strDbHost}))
2014-02-03 22:50:23 +03:00
{
2014-05-27 16:00:24 +03:00
# if (defined($self->{strBackupHost}) && defined($self->{strDbHost}))
# {
# confess &log(ASSERT, "backup and db hosts cannot both be remote");
# }
2014-04-01 15:59:09 +03:00
my $strOptionSSHRequestTTY = "RequestTTY=yes";
my $strOptionSSHCompression = "Compression=no";
2014-02-17 03:01:06 +03:00
if ($self->{bNoCompression})
{
2014-04-01 15:59:09 +03:00
$strOptionSSHCompression = "Compression=yes";
2014-02-17 03:01:06 +03:00
}
# Connect SSH object if backup host is defined
if (!defined($self->{oBackupSSH}) && defined($self->{strBackupHost}))
2014-02-17 03:01:06 +03:00
{
&log(TRACE, "connecting to backup ssh host " . $self->{strBackupHost});
2014-04-28 16:13:25 +03:00
2014-04-01 15:59:09 +03:00
$self->{oBackupSSH} = Net::OpenSSH->new($self->{strBackupHost}, timeout => 300, user => $self->{strBackupUser},
2014-05-27 16:00:24 +03:00
# default_stderr_file => $self->path_get(PATH_LOCK_ERR, "file"),
2014-04-01 15:59:09 +03:00
master_opts => [-o => $strOptionSSHCompression, -o => $strOptionSSHRequestTTY]);
2014-02-17 03:01:06 +03:00
$self->{oBackupSSH}->error and confess &log(ERROR, "unable to connect to $self->{strBackupHost}: " . $self->{oBackupSSH}->error);
}
# Connect SSH object if db host is defined
if (!defined($self->{oDbSSH}) && defined($self->{strDbHost}))
2014-02-17 03:01:06 +03:00
{
&log(TRACE, "connecting to database ssh host $self->{strDbHost}");
2014-04-01 15:59:09 +03:00
$self->{oDbSSH} = Net::OpenSSH->new($self->{strDbHost}, timeout => 300, user => $self->{strDbUser},
2014-05-27 16:00:24 +03:00
# default_stderr_file => $self->path_get(PATH_LOCK_ERR, "file"),
2014-04-01 15:59:09 +03:00
master_opts => [-o => $strOptionSSHCompression, -o => $strOptionSSHRequestTTY]);
2014-02-17 03:01:06 +03:00
$self->{oDbSSH}->error and confess &log(ERROR, "unable to connect to $self->{strDbHost}: " . $self->{oDbSSH}->error);
}
2014-02-03 22:50:23 +03:00
}
2014-02-03 03:03:05 +03:00
}
2014-02-11 23:31:16 +03:00
####################################################################################################################################
# CLONE
####################################################################################################################################
sub clone
{
my $self = shift;
my $iThreadIdx = shift;
return pg_backrest_file->new
(
strCompressExtension => $self->{strCompressExtension},
strDefaultPathPermission => $self->{strDefaultPathPermission},
strDefaultFilePermission => $self->{strDefaultFilePermission},
strCommandChecksum => $self->{strCommandChecksum},
strCommandCompress => $self->{strCommandCompress},
strCommandDecompress => $self->{strCommandDecompress},
strCommandCat => $self->{strCommandCat},
strCommandManifest => $self->{strCommandManifest},
2014-02-19 21:37:18 +03:00
# oDbSSH => $self->{strDbSSH},
2014-02-11 23:31:16 +03:00
strDbUser => $self->{strDbUser},
strDbHost => $self->{strDbHost},
2014-02-19 21:37:18 +03:00
# oBackupSSH => $self->{strBackupSSH},
2014-02-11 23:31:16 +03:00
strBackupUser => $self->{strBackupUser},
strBackupHost => $self->{strBackupHost},
strBackupPath => $self->{strBackupPath},
strBackupClusterPath => $self->{strBackupClusterPath},
bNoCompression => $self->{bNoCompression},
strStanza => $self->{strStanza},
2014-04-28 16:13:25 +03:00
iThreadIdx => $iThreadIdx,
strLockPath => $self->{strLockPath}
2014-02-11 23:31:16 +03:00
);
}
2014-02-03 03:03:05 +03:00
####################################################################################################################################
2014-04-28 16:13:25 +03:00
# ERROR_GET
2014-02-03 03:03:05 +03:00
####################################################################################################################################
2014-04-28 16:13:25 +03:00
sub error_get
2014-02-03 03:03:05 +03:00
{
2014-04-28 16:13:25 +03:00
my $self = shift;
my $strErrorFile = $self->path_get(PATH_LOCK_ERR, "file");
open my $hFile, '<', $strErrorFile or return "error opening ${strErrorFile} to read STDERR output";
my $strError = do {local $/; <$hFile>};
close $hFile;
return trim($strError);
}
2014-02-03 03:03:05 +03:00
2014-05-27 16:00:24 +03:00
####################################################################################################################################
# ERROR_CLEAR
####################################################################################################################################
sub error_clear
{
my $self = shift;
unlink($self->path_get(PATH_LOCK_ERR, "file"));
}
2014-04-28 16:13:25 +03:00
####################################################################################################################################
# PATH_GET
####################################################################################################################################
2014-02-03 03:03:05 +03:00
sub path_type_get
{
2014-02-06 05:39:08 +03:00
my $self = shift;
2014-02-03 03:03:05 +03:00
my $strType = shift;
2014-02-07 00:37:37 +03:00
2014-02-03 03:03:05 +03:00
# If db type
if ($strType =~ /^db(\:.*){0,1}/)
{
return PATH_DB;
}
# Else if backup type
elsif ($strType =~ /^backup(\:.*){0,1}/)
{
return PATH_BACKUP;
}
2014-02-07 00:37:37 +03:00
2014-02-03 03:03:05 +03:00
# Error when path type not recognized
confess &log(ASSERT, "no known path types in '${strType}'");
}
sub path_get
{
2014-02-06 05:39:08 +03:00
my $self = shift;
2014-02-05 23:56:05 +03:00
my $strType = shift; # Base type of the path to get (PATH_DB_ABSOLUTE, PATH_BACKUP_TMP, etc)
my $strFile = shift; # File to append to the base path (can include a path as well)
my $bTemp = shift; # Return the temp file for this path type - only some types have temp files
2014-02-03 03:03:05 +03:00
2014-02-05 23:56:05 +03:00
# Only allow temp files for PATH_BACKUP_ARCHIVE and PATH_BACKUP_TMP
if (defined($bTemp) && $bTemp && !($strType eq PATH_BACKUP_ARCHIVE || $strType eq PATH_BACKUP_TMP ||
$strType eq PATH_DB_ABSOLUTE || $strType eq PATH_BACKUP_ABSOLUTE))
2014-02-03 03:03:05 +03:00
{
confess &log(ASSERT, "temp file not supported on path " . $strType);
}
2014-02-05 23:56:05 +03:00
# Get absolute db path
2014-02-03 03:03:05 +03:00
if ($strType eq PATH_DB_ABSOLUTE)
{
if (defined($bTemp) && $bTemp)
{
return $strFile . ".backrest.tmp";
}
2014-02-03 03:03:05 +03:00
return $strFile;
}
2014-02-05 23:56:05 +03:00
# Make sure the base backup path is defined
2014-02-06 05:39:08 +03:00
if (!defined($self->{strBackupPath}))
2014-02-05 21:10:36 +03:00
{
2014-02-05 23:56:05 +03:00
confess &log(ASSERT, "\$strBackupPath not yet defined");
2014-02-05 21:10:36 +03:00
}
2014-02-05 23:56:05 +03:00
# Get absolute backup path
if ($strType eq PATH_BACKUP_ABSOLUTE)
2014-02-03 03:03:05 +03:00
{
if (defined($bTemp) && $bTemp)
{
return "${strFile}.backrest.tmp";
}
2014-02-04 03:03:17 +03:00
2014-02-05 23:56:05 +03:00
return $strFile;
}
2014-02-05 21:10:36 +03:00
2014-02-05 23:56:05 +03:00
# Get base backup path
if ($strType eq PATH_BACKUP)
{
2014-02-06 05:39:08 +03:00
return $self->{strBackupPath} . (defined($strFile) ? "/${strFile}" : "");
2014-02-05 23:56:05 +03:00
}
2014-02-05 21:10:36 +03:00
2014-02-05 23:56:05 +03:00
# Make sure the cluster is defined
if (!defined($self->{strStanza}))
2014-02-05 23:56:05 +03:00
{
confess &log(ASSERT, "\$strStanza not yet defined");
2014-02-05 23:56:05 +03:00
}
2014-02-05 21:10:36 +03:00
2014-05-13 18:23:15 +03:00
# Get the lock error path
2014-04-28 16:13:25 +03:00
if ($strType eq PATH_LOCK_ERR)
{
2014-05-27 16:00:24 +03:00
my $strTempPath = $self->{strLockPath};
if (!defined($strTempPath))
{
return undef;
}
2014-04-28 16:13:25 +03:00
return ${strTempPath} . (defined($strFile) ? "/${strFile}" .
(defined($self->{iThreadIdx}) ? ".$self->{iThreadIdx}" : "") . ".err" : "");
}
2014-05-13 18:23:15 +03:00
# Get the backup tmp path
2014-02-05 23:56:05 +03:00
if ($strType eq PATH_BACKUP_TMP)
{
2014-02-11 23:31:16 +03:00
my $strTempPath = "$self->{strBackupPath}/temp/$self->{strStanza}.tmp";
2014-02-05 21:10:36 +03:00
2014-02-05 23:56:05 +03:00
if (defined($bTemp) && $bTemp)
{
2014-02-11 23:31:16 +03:00
return "${strTempPath}/file.tmp" . (defined($self->{iThreadIdx}) ? ".$self->{iThreadIdx}" : "");
2014-02-05 21:10:36 +03:00
}
2014-02-05 23:56:05 +03:00
return "${strTempPath}" . (defined($strFile) ? "/${strFile}" : "");
2014-02-04 03:03:17 +03:00
}
2014-02-05 23:56:05 +03:00
# Get the backup archive path
if ($strType eq PATH_BACKUP_ARCHIVE)
2014-02-03 03:03:05 +03:00
{
my $strArchivePath = "$self->{strBackupPath}/archive/$self->{strStanza}";
2014-02-05 23:56:05 +03:00
my $strArchive;
2014-02-04 03:03:17 +03:00
2014-02-13 23:26:07 +03:00
if (defined($bTemp) && $bTemp)
{
return "${strArchivePath}/file.tmp" . (defined($self->{iThreadIdx}) ? ".$self->{iThreadIdx}" : "");
}
2014-02-05 23:56:05 +03:00
if (defined($strFile))
2014-02-03 03:03:05 +03:00
{
2014-02-05 23:56:05 +03:00
$strArchive = substr(basename($strFile), 0, 24);
if ($strArchive !~ /^([0-F]){24}$/)
2014-02-03 03:03:05 +03:00
{
2014-02-05 23:56:05 +03:00
return "${strArchivePath}/${strFile}";
2014-02-03 03:03:05 +03:00
}
2014-02-05 23:56:05 +03:00
}
2014-02-04 03:03:17 +03:00
2014-02-05 23:56:05 +03:00
return $strArchivePath . (defined($strArchive) ? "/" . substr($strArchive, 0, 16) : "") .
(defined($strFile) ? "/" . $strFile : "");
}
2014-02-04 03:03:17 +03:00
2014-02-05 23:56:05 +03:00
if ($strType eq PATH_BACKUP_CLUSTER)
{
return $self->{strBackupPath} . "/backup/$self->{strStanza}" . (defined($strFile) ? "/${strFile}" : "");
2014-02-03 03:03:05 +03:00
}
# Error when path type not recognized
confess &log(ASSERT, "no known path types in '${strType}'");
}
####################################################################################################################################
# LINK_CREATE
####################################################################################################################################
sub link_create
{
2014-02-06 05:39:08 +03:00
my $self = shift;
2014-02-03 03:03:05 +03:00
my $strSourcePathType = shift;
my $strSourceFile = shift;
my $strDestinationPathType = shift;
my $strDestinationFile = shift;
my $bHard = shift;
my $bRelative = shift;
my $bPathCreate = shift;
2014-02-07 00:37:37 +03:00
# if bHard is not defined default to false
$bHard = defined($bHard) ? $bHard : false;
2014-02-07 00:37:37 +03:00
# if bRelative is not defined or bHard is true, default to false
$bRelative = !defined($bRelative) || $bHard ? false : $bRelative;
# if bPathCreate is not defined, default to true
$bPathCreate = defined($bPathCreate) ? $bPathCreate : true;
# Source and destination path types must be the same (both PATH_DB or both PATH_BACKUP)
2014-02-06 05:39:08 +03:00
if ($self->path_type_get($strSourcePathType) ne $self->path_type_get($strDestinationPathType))
2014-02-04 03:03:17 +03:00
{
confess &log(ASSERT, "path types must be equal in link create");
}
# Generate source and destination files
2014-02-06 05:39:08 +03:00
my $strSource = $self->path_get($strSourcePathType, $strSourceFile);
my $strDestination = $self->path_get($strDestinationPathType, $strDestinationFile);
2014-02-03 03:03:05 +03:00
2014-02-04 03:03:17 +03:00
# If the destination path is backup and does not exist, create it
if ($bPathCreate && $self->path_type_get($strDestinationPathType) eq PATH_BACKUP)
2014-02-03 03:03:05 +03:00
{
2014-02-06 05:39:08 +03:00
$self->path_create(PATH_BACKUP_ABSOLUTE, dirname($strDestination));
2014-02-03 03:03:05 +03:00
}
2014-02-07 00:37:37 +03:00
2014-02-03 03:03:05 +03:00
unless (-e $strSource)
{
2014-02-06 05:39:08 +03:00
if (-e $strSource . ".$self->{strCompressExtension}")
2014-02-03 03:03:05 +03:00
{
2014-02-06 05:39:08 +03:00
$strSource .= ".$self->{strCompressExtension}";
$strDestination .= ".$self->{strCompressExtension}";
2014-02-03 03:03:05 +03:00
}
else
{
# Error when a hardlink will be created on a missing file
if ($bHard)
{
2014-02-06 05:39:08 +03:00
confess &log(ASSERT, "unable to find ${strSource}(.$self->{strCompressExtension}) for link");
}
2014-02-03 03:03:05 +03:00
}
}
2014-02-07 00:37:37 +03:00
# Generate relative path if requested
if ($bRelative)
2014-02-03 03:03:05 +03:00
{
my $iCommonLen = common_prefix($strSource, $strDestination);
if ($iCommonLen != 0)
{
$strSource = ("../" x substr($strDestination, $iCommonLen) =~ tr/\///) . substr($strSource, $iCommonLen);
}
2014-02-03 03:03:05 +03:00
}
# Create the command
my $strCommand = "ln" . (!$bHard ? " -s" : "") . " ${strSource} ${strDestination}";
2014-02-07 00:37:37 +03:00
2014-02-04 03:03:17 +03:00
# Run remotely
2014-02-06 05:39:08 +03:00
if ($self->is_remote($strSourcePathType))
2014-02-04 03:03:17 +03:00
{
2014-02-13 21:54:43 +03:00
&log(TRACE, "link_create: remote ${strSourcePathType} '${strCommand}'");
2014-02-04 03:03:17 +03:00
2014-02-06 05:39:08 +03:00
my $oSSH = $self->remote_get($strSourcePathType);
2014-02-04 03:03:17 +03:00
$oSSH->system($strCommand) or confess &log("unable to create link from ${strSource} to ${strDestination}");
}
# Run locally
else
{
2014-02-13 21:54:43 +03:00
&log(TRACE, "link_create: local '${strCommand}'");
2014-02-04 03:03:17 +03:00
system($strCommand) == 0 or confess &log("unable to create link from ${strSource} to ${strDestination}");
}
2014-02-03 03:03:05 +03:00
}
####################################################################################################################################
# PATH_CREATE
2014-02-04 03:03:17 +03:00
#
# Creates a path locally or remotely. Currently does not error if the path already exists. Also does not set permissions if the
# path aleady exists.
2014-02-03 03:03:05 +03:00
####################################################################################################################################
sub path_create
{
2014-02-06 05:39:08 +03:00
my $self = shift;
2014-02-03 03:03:05 +03:00
my $strPathType = shift;
my $strPath = shift;
my $strPermission = shift;
2014-02-07 00:37:37 +03:00
2014-02-04 03:03:17 +03:00
# If no permissions are given then use the default
2014-02-03 03:03:05 +03:00
if (!defined($strPermission))
{
2014-02-06 05:39:08 +03:00
$strPermission = $self->{strDefaultPathPermission};
2014-02-03 03:03:05 +03:00
}
# Get the path to create
my $strPathCreate = $strPath;
if (defined($strPathType))
{
2014-02-06 05:39:08 +03:00
$strPathCreate = $self->path_get($strPathType, $strPath);
2014-02-03 03:03:05 +03:00
}
2014-02-04 03:03:17 +03:00
my $strCommand = "mkdir -p -m ${strPermission} ${strPathCreate}";
# Run remotely
2014-02-06 05:39:08 +03:00
if ($self->is_remote($strPathType))
2014-02-04 03:03:17 +03:00
{
2014-02-13 21:54:43 +03:00
&log(TRACE, "path_create: remote ${strPathType} '${strCommand}'");
2014-02-04 03:03:17 +03:00
2014-02-06 05:39:08 +03:00
my $oSSH = $self->remote_get($strPathType);
2014-02-04 03:03:17 +03:00
$oSSH->system($strCommand) or confess &log("unable to create remote path ${strPathType}:${strPath}");
}
# Run locally
else
{
2014-02-13 21:54:43 +03:00
&log(TRACE, "path_create: local '${strCommand}'");
2014-02-04 03:03:17 +03:00
system($strCommand) == 0 or confess &log(ERROR, "unable to create path ${strPath}");
}
2014-02-03 03:03:05 +03:00
}
####################################################################################################################################
# IS_REMOTE
#
# Determine whether any operations are being performed remotely. If $strPathType is defined, the function will return true if that
# path is remote. If $strPathType is not defined, then function will return true if any path is remote.
####################################################################################################################################
sub is_remote
{
2014-02-06 05:39:08 +03:00
my $self = shift;
2014-02-03 03:03:05 +03:00
my $strPathType = shift;
2014-02-07 00:37:37 +03:00
2014-02-03 03:03:05 +03:00
# If the SSH object is defined then some paths are remote
2014-02-06 05:39:08 +03:00
if (defined($self->{oDbSSH}) || defined($self->{oBackupSSH}))
2014-02-03 03:03:05 +03:00
{
# If path type is not defined but the SSH object is, then some paths are remote
if (!defined($strPathType))
{
return true;
}
2014-02-07 00:37:37 +03:00
2014-02-03 03:03:05 +03:00
# If a host is defined for the path then it is remote
2014-02-06 05:39:08 +03:00
if (defined($self->{strBackupHost}) && $self->path_type_get($strPathType) eq PATH_BACKUP ||
defined($self->{strDbHost}) && $self->path_type_get($strPathType) eq PATH_DB)
2014-02-03 03:03:05 +03:00
{
return true;
}
}
return false;
}
####################################################################################################################################
# REMOTE_GET
#
# Get remote SSH object depending on the path type.
####################################################################################################################################
sub remote_get
{
2014-02-06 05:39:08 +03:00
my $self = shift;
2014-02-03 03:03:05 +03:00
my $strPathType = shift;
2014-02-04 03:03:17 +03:00
# Get the db SSH object
2014-02-06 05:39:08 +03:00
if ($self->path_type_get($strPathType) eq PATH_DB && defined($self->{oDbSSH}))
2014-02-03 03:03:05 +03:00
{
2014-02-06 05:39:08 +03:00
return $self->{oDbSSH};
2014-02-03 03:03:05 +03:00
}
2014-02-04 03:03:17 +03:00
# Get the backup SSH object
2014-02-06 05:39:08 +03:00
if ($self->path_type_get($strPathType) eq PATH_BACKUP && defined($self->{oBackupSSH}))
2014-02-03 03:03:05 +03:00
{
2014-02-06 05:39:08 +03:00
return $self->{oBackupSSH}
2014-02-03 03:03:05 +03:00
}
2014-02-04 03:03:17 +03:00
# Error when no ssh object is found
2014-02-03 03:03:05 +03:00
confess &log(ASSERT, "path type ${strPathType} does not have a defined ssh object");
}
2014-02-05 02:48:39 +03:00
####################################################################################################################################
# FILE_MOVE
#
# Moves a file locally or remotely.
####################################################################################################################################
sub file_move
{
2014-02-06 05:39:08 +03:00
my $self = shift;
2014-02-05 21:10:36 +03:00
my $strSourcePathType = shift;
2014-02-05 02:48:39 +03:00
my $strSourceFile = shift;
2014-02-05 21:10:36 +03:00
my $strDestinationPathType = shift;
2014-02-05 02:48:39 +03:00
my $strDestinationFile = shift;
my $bPathCreate = shift;
# if bPathCreate is not defined, default to true
$bPathCreate = defined($bPathCreate) ? $bPathCreate : true;
2014-02-05 21:10:36 +03:00
&log(TRACE, "file_move: ${strSourcePathType}: " . (defined($strSourceFile) ? ":${strSourceFile}" : "") .
" to ${strDestinationPathType}" . (defined($strDestinationFile) ? ":${strDestinationFile}" : ""));
# Get source and desination files
2014-02-06 05:39:08 +03:00
if ($self->path_type_get($strSourcePathType) ne $self->path_type_get($strSourcePathType))
2014-02-05 21:10:36 +03:00
{
confess &log(ASSERT, "source and destination path types must be equal");
}
2014-02-06 05:39:08 +03:00
my $strSource = $self->path_get($strSourcePathType, $strSourceFile);
my $strDestination = $self->path_get($strDestinationPathType, $strDestinationFile);
2014-02-05 02:48:39 +03:00
# If the destination path is backup and does not exist, create it
if ($bPathCreate && $self->path_type_get($strDestinationPathType) eq PATH_BACKUP)
2014-02-05 02:48:39 +03:00
{
2014-02-06 05:39:08 +03:00
$self->path_create(PATH_BACKUP_ABSOLUTE, dirname($strDestination));
2014-02-05 02:48:39 +03:00
}
my $strCommand = "mv ${strSource} ${strDestination}";
# Run remotely
2014-02-06 05:39:08 +03:00
if ($self->is_remote($strDestinationPathType))
2014-02-05 02:48:39 +03:00
{
2014-02-13 21:54:43 +03:00
&log(TRACE, "file_move: remote ${strDestinationPathType} '${strCommand}'");
2014-02-05 02:48:39 +03:00
2014-02-06 05:39:08 +03:00
my $oSSH = $self->remote_get($strDestinationPathType);
2014-02-05 21:10:36 +03:00
$oSSH->system($strCommand)
or confess &log("unable to move remote ${strDestinationPathType}:${strSourceFile} to ${strDestinationFile}");
2014-02-05 02:48:39 +03:00
}
# Run locally
else
{
2014-02-13 21:54:43 +03:00
&log(TRACE, "file_move: '${strCommand}'");
2014-02-05 02:48:39 +03:00
system($strCommand) == 0 or confess &log("unable to move local ${strSourceFile} to ${strDestinationFile}");
}
}
2014-02-03 03:03:05 +03:00
####################################################################################################################################
# FILE_COPY
####################################################################################################################################
sub file_copy
{
2014-02-06 05:39:08 +03:00
my $self = shift;
2014-02-03 03:03:05 +03:00
my $strSourcePathType = shift;
my $strSourceFile = shift;
my $strDestinationPathType = shift;
my $strDestinationFile = shift;
my $bNoCompressionOverride = shift;
2014-02-05 19:35:09 +03:00
my $lModificationTime = shift;
my $strPermission = shift;
my $bPathCreate = shift;
my $bConfessCopyError = shift;
# if bPathCreate is not defined, default to true
$bPathCreate = defined($bPathCreate) ? $bPathCreate : true;
2014-05-13 18:23:15 +03:00
$bConfessCopyError = defined($bConfessCopyError) ? $bConfessCopyError : true;
2014-02-05 19:35:09 +03:00
&log(TRACE, "file_copy: ${strSourcePathType}: " . (defined($strSourceFile) ? ":${strSourceFile}" : "") .
" to ${strDestinationPathType}" . (defined($strDestinationFile) ? ":${strDestinationFile}" : ""));
2014-02-05 19:35:09 +03:00
# Modification time and permissions cannot be set remotely
2014-02-06 05:39:08 +03:00
if ((defined($lModificationTime) || defined($strPermission)) && $self->is_remote($strDestinationPathType))
2014-02-05 19:35:09 +03:00
{
confess &log(ASSERT, "modification time and permissions cannot be set on remote destination file");
}
2014-02-03 03:03:05 +03:00
# Generate source, destination and tmp filenames
2014-02-06 05:39:08 +03:00
my $strSource = $self->path_get($strSourcePathType, $strSourceFile);
my $strDestination = $self->path_get($strDestinationPathType, $strDestinationFile);
my $strDestinationTmp = $self->path_get($strDestinationPathType, $strDestinationFile, true);
2014-02-03 03:03:05 +03:00
# Is this already a compressed file?
2014-02-06 05:39:08 +03:00
my $bAlreadyCompressed = $strSource =~ "^.*\.$self->{strCompressExtension}\$";
2014-02-03 03:03:05 +03:00
2014-02-06 05:39:08 +03:00
if ($bAlreadyCompressed && $strDestination !~ "^.*\.$self->{strCompressExtension}\$")
2014-02-03 03:03:05 +03:00
{
2014-02-06 05:39:08 +03:00
$strDestination .= ".$self->{strCompressExtension}";
2014-02-03 03:03:05 +03:00
}
# Does the file need compression?
2014-02-05 16:21:27 +03:00
my $bCompress = !((defined($bNoCompressionOverride) && $bNoCompressionOverride) ||
2014-02-06 05:39:08 +03:00
(!defined($bNoCompressionOverride) && $self->{bNoCompression}));
2014-02-03 03:03:05 +03:00
# If the destination path is backup and does not exist, create it
if ($bPathCreate && $self->path_type_get($strDestinationPathType) eq PATH_BACKUP)
2014-02-03 03:03:05 +03:00
{
2014-02-06 05:39:08 +03:00
$self->path_create(PATH_BACKUP_ABSOLUTE, dirname($strDestination));
2014-02-03 03:03:05 +03:00
}
2014-02-05 16:21:27 +03:00
# Generate the command string depending on compression/decompression/cat
2014-02-06 05:39:08 +03:00
my $strCommand = $self->{strCommandCat};
2014-02-05 16:21:27 +03:00
if (!$bAlreadyCompressed && $bCompress)
2014-02-05 16:21:27 +03:00
{
2014-02-06 05:39:08 +03:00
$strCommand = $self->{strCommandCompress};
2014-02-05 16:21:27 +03:00
$strDestination .= ".gz";
}
elsif ($bAlreadyCompressed && !$bCompress)
{
2014-02-06 05:39:08 +03:00
$strCommand = $self->{strCommandDecompress};
$strDestination = substr($strDestination, 0, length($strDestination) - length($self->{strCompressExtension}) - 1);
2014-02-05 16:21:27 +03:00
}
2014-02-03 03:03:05 +03:00
$strCommand =~ s/\%file\%/${strSource}/g;
# If this command is remote on only one side
2014-02-06 05:39:08 +03:00
if ($self->is_remote($strSourcePathType) && !$self->is_remote($strDestinationPathType) ||
!$self->is_remote($strSourcePathType) && $self->is_remote($strDestinationPathType))
2014-02-03 03:03:05 +03:00
{
# Else if the source is remote
2014-02-06 05:39:08 +03:00
if ($self->is_remote($strSourcePathType))
2014-02-03 03:03:05 +03:00
{
2014-02-13 21:54:43 +03:00
&log(TRACE, "file_copy: remote ${strSource} to local ${strDestination}");
2014-02-03 03:03:05 +03:00
# Open the destination file for writing (will be streamed from the ssh session)
my $hFile;
open($hFile, ">", $strDestinationTmp) or confess &log(ERROR, "cannot open ${strDestination}");
# Execute the command through ssh
2014-02-06 05:39:08 +03:00
my $oSSH = $self->remote_get($strSourcePathType);
2014-05-27 16:00:24 +03:00
unless ($oSSH->system({stderr_file => $self->path_get(PATH_LOCK_ERR, "file"), stdout_fh => $hFile}, $strCommand))
{
close($hFile) or confess &log(ERROR, "cannot close file ${strDestinationTmp}");
2014-05-27 16:00:24 +03:00
my $strResult = "unable to execute ssh '${strCommand}': " . $self->error_get();
$bConfessCopyError ? confess &log(ERROR, $strResult) : return false;
}
2014-02-03 03:03:05 +03:00
# Close the destination file handle
close($hFile) or confess &log(ERROR, "cannot close file ${strDestinationTmp}");
2014-02-03 03:03:05 +03:00
}
# Else if the destination is remote
2014-02-06 05:39:08 +03:00
elsif ($self->is_remote($strDestinationPathType))
2014-02-03 03:03:05 +03:00
{
2014-02-13 21:54:43 +03:00
&log(TRACE, "file_copy: local ${strSource} ($strCommand) to remote ${strDestination}");
2014-02-03 03:03:05 +03:00
2014-05-27 16:00:24 +03:00
if (defined($self->path_get(PATH_LOCK_ERR, "file")))
{
$strCommand .= " 2> " . $self->path_get(PATH_LOCK_ERR, "file");
}
2014-02-03 03:03:05 +03:00
# Open the input command as a stream
my $hOut;
my $pId = open3(undef, $hOut, undef, $strCommand) or confess(ERROR, "unable to execute '${strCommand}'");
# Execute the command though ssh
2014-02-06 05:39:08 +03:00
my $oSSH = $self->remote_get($strDestinationPathType);
2014-05-27 16:00:24 +03:00
$oSSH->system({stderr_file => $self->path_get(PATH_LOCK_ERR, "file"), stdin_fh => $hOut}, "cat > ${strDestinationTmp}") or confess &log(ERROR, "unable to execute ssh 'cat'");
2014-02-03 03:03:05 +03:00
# Wait for the stream process to finish
waitpid($pId, 0);
my $iExitStatus = ${^CHILD_ERROR_NATIVE} >> 8;
2014-05-27 16:00:24 +03:00
2014-02-03 03:03:05 +03:00
if ($iExitStatus != 0)
{
2014-05-27 16:00:24 +03:00
my $strResult = "command '${strCommand}' returned " . $iExitStatus . ": " . $self->error_get();
$bConfessCopyError ? confess &log(ERROR, $strResult) : return false;
2014-02-03 03:03:05 +03:00
}
}
}
# If the source and destination are both remote but not the same remote
2014-02-06 05:39:08 +03:00
elsif ($self->is_remote($strSourcePathType) && $self->is_remote($strDestinationPathType) &&
$self->path_type_get($strSourcePathType) ne $self->path_type_get($strDestinationPathType))
2014-02-03 03:03:05 +03:00
{
2014-02-13 21:54:43 +03:00
&log(TRACE, "file_copy: remote ${strSource} to remote ${strDestination}");
2014-02-03 03:03:05 +03:00
confess &log(ASSERT, "remote source and destination not supported");
}
# Else this is a local command or remote where both sides are the same remote
else
{
# Complete the command by redirecting to the destination tmp file
$strCommand .= " > ${strDestinationTmp}";
if ($self->is_remote($strSourcePathType))
2014-02-03 03:03:05 +03:00
{
2014-02-13 21:54:43 +03:00
&log(TRACE, "file_copy: remote ${strSourcePathType} '${strCommand}'");
2014-02-03 03:03:05 +03:00
2014-02-06 05:39:08 +03:00
my $oSSH = $self->remote_get($strSourcePathType);
2014-05-27 16:00:24 +03:00
$oSSH->system({stderr_file => $self->path_get(PATH_LOCK_ERR, "file")}, $strCommand);
if ($oSSH->error)
{
my $strResult = "unable to execute copy ${strCommand}: " . $oSSH->error;
$bConfessCopyError ? confess &log(ERROR, $strResult) : return false;
}
2014-02-03 03:03:05 +03:00
}
else
{
2014-02-13 21:54:43 +03:00
&log(TRACE, "file_copy: local '${strCommand}'");
2014-05-27 16:00:24 +03:00
if (defined($self->path_get(PATH_LOCK_ERR, "file")))
{
$strCommand .= " 2> " . $self->path_get(PATH_LOCK_ERR, "file");
}
unless(system($strCommand) == 0)
{
my $strResult = "unable to copy local ${strSource} to local ${strDestinationTmp}";
$bConfessCopyError ? confess &log(ERROR, $strResult) : return false;
}
2014-02-03 03:03:05 +03:00
}
}
2014-02-05 19:35:09 +03:00
# Set the file permission if required (this only works locally for now)
if (defined($strPermission))
{
2014-02-13 21:54:43 +03:00
&log(TRACE, "file_copy: chmod ${strPermission}");
2014-02-05 19:35:09 +03:00
system("chmod ${strPermission} ${strDestinationTmp}") == 0
or confess &log(ERROR, "unable to set permissions for local ${strDestinationTmp}");
}
2014-02-07 00:37:37 +03:00
2014-02-05 19:35:09 +03:00
# Set the file modification time if required (this only works locally for now)
if (defined($lModificationTime))
{
2014-02-13 21:54:43 +03:00
&log(TRACE, "file_copy: time ${lModificationTime}");
2014-02-05 19:35:09 +03:00
utime($lModificationTime, $lModificationTime, $strDestinationTmp)
or confess &log(ERROR, "unable to set time for local ${strDestinationTmp}");
}
2014-02-07 00:37:37 +03:00
2014-02-03 03:03:05 +03:00
# Move the file from tmp to final destination
2014-02-06 05:39:08 +03:00
$self->file_move($self->path_type_get($strSourcePathType) . ":absolute", $strDestinationTmp,
$self->path_type_get($strDestinationPathType) . ":absolute", $strDestination, $bPathCreate);
return true;
2014-02-03 03:03:05 +03:00
}
####################################################################################################################################
# FILE_HASH_GET
####################################################################################################################################
sub file_hash_get
{
2014-02-06 05:39:08 +03:00
my $self = shift;
2014-02-03 03:03:05 +03:00
my $strPathType = shift;
my $strFile = shift;
2014-02-07 00:37:37 +03:00
2014-02-04 03:03:17 +03:00
# For now this operation is not supported remotely. Not currently needed.
2014-02-06 05:39:08 +03:00
if ($self->is_remote($strPathType))
2014-02-04 03:03:17 +03:00
{
confess &log(ASSERT, "remote operation not supported");
}
2014-02-07 00:37:37 +03:00
2014-02-06 05:39:08 +03:00
if (!defined($self->{strCommandChecksum}))
2014-02-03 03:03:05 +03:00
{
confess &log(ASSERT, "\$strCommandChecksum not defined");
}
2014-02-07 00:37:37 +03:00
2014-02-06 05:39:08 +03:00
my $strPath = $self->path_get($strPathType, $strFile);
2014-02-03 03:03:05 +03:00
my $strCommand;
2014-02-07 00:37:37 +03:00
2014-02-03 03:03:05 +03:00
if (-e $strPath)
{
2014-02-06 05:39:08 +03:00
$strCommand = $self->{strCommandChecksum};
2014-02-05 16:21:27 +03:00
$strCommand =~ s/\%file\%/${strPath}/g;
2014-02-03 03:03:05 +03:00
}
2014-02-06 05:39:08 +03:00
elsif (-e $strPath . ".$self->{strCompressExtension}")
2014-02-03 03:03:05 +03:00
{
2014-02-06 05:39:08 +03:00
$strCommand = $self->{strCommandDecompress};
2014-02-03 03:03:05 +03:00
$strCommand =~ s/\%file\%/${strPath}/g;
2014-02-06 05:39:08 +03:00
$strCommand .= " | " . $self->{strCommandChecksum};
2014-02-03 03:03:05 +03:00
$strCommand =~ s/\%file\%//g;
}
else
{
2014-02-06 05:39:08 +03:00
confess &log(ASSERT, "unable to find $strPath(.$self->{strCompressExtension}) for checksum");
2014-02-03 03:03:05 +03:00
}
2014-02-07 00:37:37 +03:00
2014-02-03 03:03:05 +03:00
return trim(capture($strCommand)) or confess &log(ERROR, "unable to checksum ${strPath}");
}
####################################################################################################################################
# FILE_COMPRESS
####################################################################################################################################
sub file_compress
{
my $self = shift;
my $strPathType = shift;
my $strFile = shift;
# For now this operation is not supported remotely. Not currently needed.
if ($self->is_remote($strPathType))
{
confess &log(ASSERT, "remote operation not supported");
}
if (!defined($self->{strCommandCompress}))
{
2014-03-26 01:56:05 +03:00
confess &log(ASSERT, "\$strCommandCompress not defined");
}
my $strPath = $self->path_get($strPathType, $strFile);
# Build the command
my $strCommand = $self->{strCommandCompress};
$strCommand =~ s/\%file\%/${strPath}/g;
$strCommand =~ s/\ \-\-stdout//g;
system($strCommand) == 0 or confess &log(ERROR, "unable to compress ${strPath}: ${strCommand}");
}
2014-02-03 03:03:05 +03:00
####################################################################################################################################
# FILE_LIST_GET
####################################################################################################################################
sub file_list_get
{
2014-02-06 05:39:08 +03:00
my $self = shift;
2014-02-04 03:03:17 +03:00
my $strPathType = shift;
2014-02-03 03:03:05 +03:00
my $strPath = shift;
my $strExpression = shift;
my $strSortOrder = shift;
2014-02-04 03:03:17 +03:00
# Get the root path for the file list
2014-02-06 05:39:08 +03:00
my $strPathList = $self->path_get($strPathType, $strPath);
2014-02-07 00:37:37 +03:00
# Builds the file list command
2014-04-28 16:13:25 +03:00
# my $strCommand = "ls ${strPathList} | egrep \"$strExpression\"";
my $strCommand = "ls -1 ${strPathList}";
2014-03-26 01:56:05 +03:00
# Run the file list command
my $strFileList = "";
2014-02-03 03:03:05 +03:00
2014-03-26 01:56:05 +03:00
# Run remotely
if ($self->is_remote($strPathType))
2014-02-03 03:03:05 +03:00
{
&log(TRACE, "file_list_get: remote ${strPathType}:${strPathList} ${strCommand}");
2014-02-07 00:37:37 +03:00
2014-03-26 01:56:05 +03:00
my $oSSH = $self->remote_get($strPathType);
$strFileList = $oSSH->capture($strCommand);
2014-04-28 16:13:25 +03:00
if ($oSSH->error)
{
confess &log(ERROR, "unable to execute file list (${strCommand}): " . $self->error_get());
}
2014-03-26 01:56:05 +03:00
}
# Run locally
else
2014-02-03 03:03:05 +03:00
{
&log(TRACE, "file_list_get: local ${strPathType}:${strPathList} ${strCommand}");
2014-04-28 16:13:25 +03:00
$strFileList = capture($strCommand);
2014-02-03 03:03:05 +03:00
}
2014-02-07 00:37:37 +03:00
# Split the files into an array
2014-04-28 16:13:25 +03:00
my @stryFileList;
if (defined($strExpression))
{
@stryFileList = grep(/$strExpression/i, split(/\n/, $strFileList));
}
else
{
@stryFileList = split(/\n/, $strFileList);
}
2014-03-26 01:56:05 +03:00
# Return the array in reverse order if specified
if (defined($strSortOrder) && $strSortOrder eq "reverse")
{
return sort {$b cmp $a} @stryFileList;
}
# Return in normal sorted order
return sort @stryFileList;
2014-02-03 03:03:05 +03:00
}
####################################################################################################################################
# FILE_EXISTS
####################################################################################################################################
sub file_exists
{
my $self = shift;
my $strPathType = shift;
my $strPath = shift;
# Get the root path for the manifest
my $strPathExists = $self->path_get($strPathType, $strPath);
2014-05-27 16:00:24 +03:00
&log(TRACE, "file_exists: " . ($self->is_remote($strPathType) ? "remote" : "local") . " ${strPathType}:${strPathExists}");
# Run remotely
if ($self->is_remote($strPathType))
{
2014-05-27 16:00:24 +03:00
# Builds the exists command
my $strCommand = "ls ${strPathExists}";
# Run the file exists command
my $strExists;
my $strError;
&log(TRACE, "file_exists: command: ${strCommand}");
my $oSSH = $self->remote_get($strPathType);
2014-05-27 16:00:24 +03:00
$strExists = $oSSH->capture({stderr_file => $self->path_get(PATH_LOCK_ERR, "file")}, $strCommand);
&log(TRACE, "file_exists: search = ${strPathExists}, result = " . (defined($strExists) ? $strExists : "<undef>"));
2014-05-13 18:23:15 +03:00
if ($oSSH->error)
{
2014-05-27 16:00:24 +03:00
my $strError = $self->error_get();
&log(TRACE, "error detected: $strError");
if ($strError =~ /.*ls.*No such file or directory.*/)
{
return(false);
}
confess &log(ERROR, "unable to execute file exists (${strCommand}): " . $strError);
2014-05-13 18:23:15 +03:00
}
2014-05-27 16:00:24 +03:00
# If the return from ls eq strPathExists then true
return trim($strExists) eq $strPathExists;
}
# Run locally
else
{
2014-05-27 16:00:24 +03:00
if (-e $strPathExists)
{
return true;
}
else
{
return false;
}
}
2014-05-27 16:00:24 +03:00
confess &log(ASSERT, "file_exists: true or false should have been returned");
}
####################################################################################################################################
# FILE_REMOVE
####################################################################################################################################
sub file_remove
{
my $self = shift;
my $strPathType = shift;
my $strPath = shift;
my $bTemp = shift;
my $bErrorIfNotExists = shift;
if (!defined($bErrorIfNotExists))
{
$bErrorIfNotExists = false;
}
# Get the root path for the manifest
my $strPathRemove = $self->path_get($strPathType, $strPath, $bTemp);
# Builds the exists command
my $strCommand = "rm -f ${strPathRemove}";
# Run remotely
if ($self->is_remote($strPathType))
{
&log(TRACE, "file_remove: remote ${strPathType}:${strPathRemove}");
my $oSSH = $self->remote_get($strPathType);
2014-02-22 00:09:34 +03:00
$oSSH->system($strCommand) or $bErrorIfNotExists ? confess &log(ERROR, "unable to remove remote ${strPathType}:${strPathRemove}") : true;
2014-05-13 18:23:15 +03:00
if ($oSSH->error)
{
confess &log(ERROR, "unable to execute file_remove (${strCommand}): " . $self->error_get());
}
}
# Run locally
else
{
2014-05-13 18:23:15 +03:00
&log(TRACE, "file_remove: local ${strPathType}:${strPathRemove}");
system($strCommand) == 0 or $bErrorIfNotExists ? confess &log(ERROR, "unable to remove local ${strPathType}:${strPathRemove}") : true;
}
}
2014-02-03 03:03:05 +03:00
####################################################################################################################################
# MANIFEST_GET
#
# 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_get
{
2014-02-06 05:39:08 +03:00
my $self = shift;
2014-02-03 03:03:05 +03:00
my $strPathType = shift;
my $strPath = shift;
2014-02-13 21:54:43 +03:00
&log(TRACE, "manifest: " . $self->{strCommandManifest});
2014-02-03 03:03:05 +03:00
# Get the root path for the manifest
2014-02-06 05:39:08 +03:00
my $strPathManifest = $self->path_get($strPathType, $strPath);
2014-02-03 03:03:05 +03:00
# Builds the manifest command
2014-02-06 05:39:08 +03:00
my $strCommand = $self->{strCommandManifest};
2014-02-03 03:03:05 +03:00
$strCommand =~ s/\%path\%/${strPathManifest}/g;
# Run the manifest command
my $strManifest;
# Run remotely
2014-02-06 05:39:08 +03:00
if ($self->is_remote($strPathType))
2014-02-03 03:03:05 +03:00
{
2014-02-13 21:54:43 +03:00
&log(TRACE, "manifest_get: remote ${strPathType}:${strPathManifest}");
2014-02-03 03:03:05 +03:00
2014-02-06 05:39:08 +03:00
my $oSSH = $self->remote_get($strPathType);
2014-04-28 16:13:25 +03:00
$strManifest = $oSSH->capture($strCommand) or
confess &log(ERROR, "unable to execute remote manifest (${strCommand}): " . $self->error_get());
2014-02-03 03:03:05 +03:00
}
# Run locally
else
{
2014-02-13 21:54:43 +03:00
&log(TRACE, "manifest_get: local ${strPathType}:${strPathManifest}");
2014-02-03 03:03:05 +03:00
$strManifest = capture($strCommand) or confess &log(ERROR, "unable to execute local command '${strCommand}'");
}
# Load the manifest into a hash
return data_hash_build("name\ttype\tuser\tgroup\tpermission\tmodification_time\tinode\tsize\tlink_destination\n" .
$strManifest, "\t", ".");
}
2014-02-06 05:39:08 +03:00
no Moose;
__PACKAGE__->meta->make_immutable;