#################################################################################################################################### # C Storage Interface #################################################################################################################################### package pgBackRestTest::Common::StorageRepo; use parent 'pgBackRestTest::Common::StorageBase'; use strict; use warnings FATAL => qw(all); use Carp qw(confess); use English '-no_match_vars'; use Digest::SHA qw(sha1_hex); use Exporter qw(import); our @EXPORT = qw(); use File::Basename qw(dirname); use Fcntl qw(:mode); use File::stat qw{lstat}; use JSON::PP; use pgBackRestDoc::Common::Exception; use pgBackRestDoc::Common::Log; use pgBackRestDoc::ProjectInfo; use pgBackRestTest::Common::Io::Handle; use pgBackRestTest::Common::Io::Process; use pgBackRestTest::Common::StorageBase; #################################################################################################################################### # Temp file extension #################################################################################################################################### use constant STORAGE_TEMP_EXT => PROJECT_EXE . '.tmp'; push @EXPORT, qw(STORAGE_TEMP_EXT); #################################################################################################################################### # 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->{strCommand}, $self->{strType}, $self->{lBufferMax}, $self->{iTimeoutIo}, $self->{strDefaultPathMode}, $self->{strDefaultFileMode}, ) = logDebugParam ( __PACKAGE__ . '->new', \@_, {name => 'strCommand'}, {name => 'strType'}, {name => 'lBufferMax'}, {name => 'iTimeoutIo'}, {name => 'strDefaultPathMode', optional => true, default => '0750'}, {name => 'strDefaultFileMode', optional => true, default => '0640'}, ); # Create JSON object $self->{oJSON} = JSON::PP->new()->allow_nonref(); # Return from function and log return values if any return logDebugReturn ( $strOperation, {name => 'self', value => $self} ); } #################################################################################################################################### # Escape characteres that have special meaning on the command line #################################################################################################################################### sub escape { my $self = shift; # Assign function parameters, defaults, and log debug info my ( $strOperation, $strValue, ) = logDebugParam ( __PACKAGE__ . '->escape', \@_, {name => 'strValue', trace => true}, ); $strValue =~ s/\\/\\\\/g; $strValue =~ s/\/\\\>/g; $strValue =~ s/\!/\\\!/g; $strValue =~ s/\*/\\\*/g; $strValue =~ s/\(/\\\(/g; $strValue =~ s/\)/\\\)/g; $strValue =~ s/\&/\\\&/g; $strValue =~ s/\'/\\\'/g; $strValue =~ s/\;/\\\;/g; $strValue =~ s/\?/\\\?/g; # Return from function and log return values if any return logDebugReturn ( $strOperation, {name => 'strValue', value => $strValue}, ); } #################################################################################################################################### # Execute command and return the output #################################################################################################################################### sub exec { my $self = shift; # Assign function parameters, defaults, and log debug info my ( $strOperation, $strCommand, ) = logDebugParam ( __PACKAGE__ . '->exec', \@_, {name => 'strCommand'}, ); $strCommand = "$self->{strCommand} ${strCommand}"; my $oBuffer = new pgBackRestTest::Common::Io::Buffered( new pgBackRestTest::Common::Io::Handle($strCommand), $self->{iTimeoutIo}, $self->{lBufferMax}); my $oProcess = new pgBackRestTest::Common::Io::Process($oBuffer, $strCommand); my $tResult; while (!$oBuffer->eof()) { $oBuffer->read(\$tResult, $self->{lBufferMax}, false); } $oProcess->close(); # Return from function and log return values if any return logDebugReturn ( $strOperation, {name => 'tResult', value => $tResult}, {name => 'iExitStatus', value => $oProcess->exitStatus()}, ); } #################################################################################################################################### # Create storage #################################################################################################################################### sub create { my $self = shift; # Assign function parameters, defaults, and log debug info my ($strOperation) = logDebugParam(__PACKAGE__ . '->create'); $self->exec("repo-create"); # Return from function and log return values if any return logDebugReturn($strOperation); } #################################################################################################################################### # Check if file exists (not a path) #################################################################################################################################### sub exists { my $self = shift; # Assign function parameters, defaults, and log debug info my ( $strOperation, $strFileExp, ) = logDebugParam ( __PACKAGE__ . '->exists', \@_, {name => 'strFileExp'}, ); # Return from function and log return values if any return logDebugReturn ( $strOperation, {name => 'bExists', value => $self->info($strFileExp, {bIgnoreMissing => true})->{type} eq 'f'} ); } #################################################################################################################################### # Read a buffer from storage all at once #################################################################################################################################### sub get { my $self = shift; # Assign function parameters, defaults, and log debug info my ( $strOperation, $xFile, $strCipherPass, $bRaw, ) = logDebugParam ( __PACKAGE__ . '->get', \@_, {name => 'xFile', required => false}, {name => 'strCipherPass', optional => true, redact => true}, {name => 'bRaw', optional => true, default => false}, ); # If openRead() was called first set values from that call my $strFile = $xFile; my $bIgnoreMissing = false; if (ref($xFile)) { $strFile = $xFile->{strFile}; $bIgnoreMissing = $xFile->{bIgnoreMissing}; $strCipherPass = $xFile->{strCipherPass}; } # Check invalid params if ($bRaw && defined($strCipherPass)) { confess &log(ERROR, 'bRaw and strCipherPass cannot both be set'); } # Get file my ($tResult, $iExitStatus) = $self->exec( (defined($strCipherPass) ? ' --cipher-pass=' . $self->escape($strCipherPass) : '') . ($bRaw ? ' --raw' : '') . ($bIgnoreMissing ? ' --ignore-missing' : '') . ' repo-get ' . $self->escape($strFile)); # Error if missing an not ignored if ($iExitStatus == 1 && !$bIgnoreMissing) { confess &log(ERROR, "unable to open '${strFile}'", ERROR_FILE_OPEN); } # Return from function and log return values if any return logDebugReturn ( $strOperation, {name => 'rtContent', value => $iExitStatus == 0 ? \$tResult : undef, trace => true}, ); } #################################################################################################################################### # Get information for path/file #################################################################################################################################### sub info { my $self = shift; # Assign function parameters, defaults, and log debug info my ( $strOperation, $strPathFileExp, $bIgnoreMissing, ) = logDebugParam ( __PACKAGE__ . '->info', \@_, {name => 'strPathFileExp'}, {name => 'bIgnoreMissing', optional => true, default => false}, ); # Return from function and log return values if any return logDebugReturn ( $strOperation, {name => 'rhInfo', value => $self->manifest($strPathFileExp, {bRecurse => false})->{'.'}, trace => true} ); } #################################################################################################################################### # List all files/paths in path #################################################################################################################################### sub list { my $self = shift; # Assign function parameters, defaults, and log debug info my ( $strOperation, $strPathExp, $strExpression, $strSortOrder, $bIgnoreMissing, ) = logDebugParam ( __PACKAGE__ . '->list', \@_, {name => 'strPathExp', required => false}, {name => 'strExpression', optional => true}, {name => 'strSortOrder', optional => true, default => 'forward'}, {name => 'bIgnoreMissing', optional => true, default => false}, ); # Get file list my $rstryFileList = []; my $rhManifest = $self->manifest($strPathExp, {bRecurse => false}); foreach my $strKey ($strSortOrder eq 'reverse' ? sort {$b cmp $a} keys(%{$rhManifest}) : sort keys(%{$rhManifest})) { next if $strKey eq '.'; next if defined($strExpression) && $strKey !~ $strExpression; push(@{$rstryFileList}, $strKey); } # Return from function and log return values if any return logDebugReturn ( $strOperation, {name => 'stryFileList', value => $rstryFileList} ); } #################################################################################################################################### # Build path/file/link manifest starting with base path and including all subpaths #################################################################################################################################### sub manifest { my $self = shift; # Assign function parameters, defaults, and log debug info my ( $strOperation, $strPathExp, $bRecurse, ) = logDebugParam ( __PACKAGE__ . '->manifest', \@_, {name => 'strPathExp'}, {name => 'bRecurse', optional => true, default => true}, ); my $rhManifest = $self->{oJSON}->decode( $self->exec("--output=json " . ($bRecurse ? ' --recurse' : '') . " repo-ls " . $self->escape($strPathExp))); # Transform the manifest to the old format foreach my $strKey (keys(%{$rhManifest})) { if ($rhManifest->{$strKey}{type} eq 'file') { $rhManifest->{$strKey}{type} = 'f'; if (defined($rhManifest->{$strKey}{time})) { $rhManifest->{$strKey}{modified_time} = $rhManifest->{$strKey}{time}; delete($rhManifest->{$strKey}{time}); } } elsif ($rhManifest->{$strKey}{type} eq 'path') { $rhManifest->{$strKey}{type} = 'd'; } elsif ($rhManifest->{$strKey}{type} eq 'link') { $rhManifest->{$strKey}{type} = 'l'; } elsif ($rhManifest->{$strKey}{type} eq 'special') { $rhManifest->{$strKey}{type} = 's'; } else { confess "invalid file type '$rhManifest->{type}'"; } } # Return from function and log return values if any return logDebugReturn ( $strOperation, {name => 'rhManifest', value => $rhManifest, trace => true} ); } #################################################################################################################################### # Open file for reading #################################################################################################################################### sub openRead { my $self = shift; # Assign function parameters, defaults, and log debug info my ( $strOperation, $strFile, $bIgnoreMissing, $strCipherPass, ) = logDebugParam ( __PACKAGE__ . '->openRead', \@_, {name => 'strFile'}, {name => 'bIgnoreMissing', optional => true, default => false}, {name => 'strCipherPass', optional => true, redact => true}, ); # Return from function and log return values if any return logDebugReturn ( $strOperation, {name => 'rhFileIo', value => {strFile => $strFile, bIgnoreMissing => $bIgnoreMissing, strCipherPass => $strCipherPass}, trace => true}, ); } #################################################################################################################################### # Remove path and all files below it #################################################################################################################################### sub pathRemove { my $self = shift; # Assign function parameters, defaults, and log debug info my ( $strOperation, $strPath, $bRecurse, ) = logDebugParam ( __PACKAGE__ . '->pathRemove', \@_, {name => 'strPath'}, {name => 'bRecurse', optional => true, default => false}, ); $self->exec("repo-rm " . ($bRecurse ? '--recurse ' : '') . $self->escape($strPath)); # Return from function and log return values if any return logDebugReturn($strOperation); } #################################################################################################################################### # put - writes a buffer out to storage all at once #################################################################################################################################### sub put { my $self = shift; # Assign function parameters, defaults, and log debug info my ( $strOperation, $strFile, $tContent, $strCipherPass, $bRaw, ) = logDebugParam ( __PACKAGE__ . '->put', \@_, {name => 'strFile'}, {name => 'tContent', required => false}, {name => 'strCipherPass', optional => true, redact => true}, {name => 'bRaw', optional => true, default => false}, ); # Check invalid params if ($bRaw && defined($strCipherPass)) { confess &log(ERROR, 'bRaw and strCipherPass cannot both be set'); } # Put file my $strCommand = "$self->{strCommand}" . (defined($strCipherPass) ? ' --cipher-pass=' . $self->escape($strCipherPass) : '') . ($bRaw ? ' --raw' : '') . ' repo-put ' . $self->escape($strFile); my $oBuffer = new pgBackRestTest::Common::Io::Buffered( new pgBackRestTest::Common::Io::Handle($strCommand), $self->{iTimeoutIo}, $self->{lBufferMax}); my $oProcess = new pgBackRestTest::Common::Io::Process($oBuffer, $strCommand); if (defined($tContent)) { $oBuffer->write(\$tContent); } close($oBuffer->handleWrite()); my $tResult; while (!$oBuffer->eof()) { $oBuffer->read(\$tResult, $self->{lBufferMax}, false); } close($oBuffer->handleRead()); $oProcess->close(); # Return from function and log return values if any return logDebugReturn($strOperation); } #################################################################################################################################### # Remove file #################################################################################################################################### sub remove { my $self = shift; # Assign function parameters, defaults, and log debug info my ( $strOperation, $strFile, ) = logDebugParam ( __PACKAGE__ . '->remove', \@_, {name => 'xFileExp'}, ); $self->exec("repo-rm " . $self->escape($strFile)); # Return from function and log return values if any return logDebugReturn($strOperation); } #################################################################################################################################### # Cache storage so it can be retrieved quickly #################################################################################################################################### my $oRepoStorage; #################################################################################################################################### # storageRepoCommandSet #################################################################################################################################### my $strStorageRepoCommand; my $strStorageRepoType; sub storageRepoCommandSet { # Assign function parameters, defaults, and log debug info my ( $strOperation, $strCommand, $strStorageType, ) = logDebugParam ( __PACKAGE__ . '::storageRepoCommandSet', \@_, {name => 'strCommand'}, {name => 'strStorageType'}, ); $strStorageRepoCommand = $strCommand; $strStorageRepoType = $strStorageType; # Return from function and log return values if any return logDebugReturn($strOperation); } push @EXPORT, qw(storageRepoCommandSet); #################################################################################################################################### # storageRepo - get repository storage #################################################################################################################################### sub storageRepo { # Assign function parameters, defaults, and log debug info my ( $strOperation, $strStanza, ) = logDebugParam ( __PACKAGE__ . '::storageRepo', \@_, {name => 'strStanza', optional => true, trace => true}, ); # Create storage if not defined if (!defined($oRepoStorage)) { $oRepoStorage = new pgBackRestTest::Common::StorageRepo($strStorageRepoCommand, $strStorageRepoType, 64 * 1024, 60); } # Return from function and log return values if any return logDebugReturn ( $strOperation, {name => 'oStorageRepo', value => $oRepoStorage, trace => true}, ); } push @EXPORT, qw(storageRepo); #################################################################################################################################### # Getters #################################################################################################################################### sub capability {shift->type() eq STORAGE_POSIX} sub type {shift->{strType}} 1;