package Storage::FileStorage;
use strict;
use bigint;
use Storage::Storage;
use Logging;
use AgentConfig;
use HelpFuncs;
use POSIX;
use IPC::Run;
use Symbol;
use Storage::Splitter;
use Storage::Counter;
use vars qw|@ISA|;
@ISA = qw|Storage::Storage|;
sub _init {
my ($self, %options) = @_;
$self->SUPER::_init(%options);
$self->{split_size} = $options{split_size};
$self->{gzip_bundle} = $options{gzip_bundle};
$self->{output_dir} = $options{output_dir};
$self->{space_reserved} = $options{space_reserved} if $options{space_reserved};
$self->{sign} = 1 if $options{sign};
$self->{last_used_id} = 0;
$self->{unpacked_size} = 0;
$self->{packed_size} = 0;
Logging::info("-" x 60);
Logging::info("FILE storage initialized.");
Logging::info("Base directory: $self->{output_dir}");
Logging::info("Space reserved: $self->{space_reserved}");
Logging::info("Gzip bundles: " . ($self->{gzip_bundle} ? "yes" : "no"));
Logging::info("Bundle split size: " . ($self->{split_size} || "do not split"));
Logging::info("-" x 60);
$self->reserveSpace();
}
sub getFileSize
{
my( $fileName ) = @_;
my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime, $blksize,$blocks) = stat($fileName);
return $size if defined $size;
return 0;
}
sub reserveSpace {
my ($self ) = @_;
if (exists $self->{space_reserved} ) {
my $avail = (HelpFuncs::getMountSpace($self->{output_dir}))[1];
if( $avail < $self->{space_reserved} ) {
my $errmsg = "Available disk space ($avail) is less than required by storage bundle ($self->{space_reserved})";
Logging::error($errmsg,'fatal');
die $errmsg;
}
my $namebase = $self->{output_dir}.'/.fs_'.(0+$self).'_';
my $var = 0;
while( -e "$namebase$var.tmp"){$var++;}
$self->{space_reserver} = "$namebase$var";
Logging::info("Reserve disk space at $self->{space_reserver}");
qx( dd if=/dev/zero of=$self->{space_reserver} bs=$self->{space_reserved} count=1);
}
}
sub unreserveSpace {
my ($self ) = @_;
if (exists $self->{space_reserver} ) {
Logging::info("Free reserved disk space at $self->{space_reserver}");
if( -f $self->{space_reserver}){
unlink $self->{space_reserver} or Logging::debug("Cannot delete file ".$self->{space_reserver} );
}
delete $self->{space_reserver};
}
}
#
# Checks the validity of proposed id: it should not be too long.
#
sub getFileNameIdFromId {
my ($self, $id, $ext, $cansplit ) = @_;
my $maxLength = &POSIX::PATH_MAX;
$maxLength -= length( $ext );
if ( $cansplit && $self->{split_size}) {
$maxLength -= 4;
}
my $destFile = $self->getFullOutputPath() . "/" . $id;
if (length($destFile) > $maxLength) {
$id = $self->{last_used_id}++;
$destFile = $self->getFullOutputPath() . "/" . $id;
}
if ($self->{gzip_bundle}) {
$id .= ":gzipped";
}
my $dstDir = $destFile;
if( $dstDir=~ m/(.*)\/(.*)/ ){
$dstDir = $1;
$destFile = $2;
}
else{
$destFile = 'empty';
}
return ($dstDir,$destFile, $id);
}
sub getBundleExecutor {
my ($bundle) = @_;
return sub {
my $exec = $bundle->run();
binmode STDOUT;
my $block;
my $blocklen;
my $timeWorking = time();
while ($blocklen = sysread($exec, $block, 65536)) {
my $offset = 0;
do {
my $written = syswrite(STDOUT, $block, $blocklen, $offset);
die $! unless defined $written ;
$offset += $written;
$blocklen -= $written;
} while ($blocklen != 0);
# bug 30101. Prevent ssh connection close(when source host has strong security policy) on big content
if ( time() - $timeWorking > 30) {
Logging::info("Tar is working");
$timeWorking = time();
}
}
$bundle->cleanup();
POSIX::_exit(0);
};
}
sub executeAndSave {
my ($self, $destDir, $destFile, $destExt, $bundle, $outunpackedSize, $doNotSplit, $doNotGzip ) = @_;
my $unpackedSize = 0;
system( "mkdir", "-p", $destDir ) if $destDir and not -e $destDir;
#allocating filehandle for creating pipe from subprocess
my $newhandle = POSIX::open("/dev/null", O_RDWR, 0666);
my @cmd;
push @cmd, getBundleExecutor($bundle);
if ($self->{gzip_bundle} and not $doNotGzip ) {
#FIXME check gzip
push @cmd, "|", ["gzip"];
}
my $files;
my $newhandle2 = POSIX::open("/dev/null", O_RDWR, 0666);
my $splitSize = $self->{split_size};
$splitSize = 0 if $doNotSplit;
push @cmd, "|", \&Storage::Splitter::run, "$newhandle2>", \$files,
init => sub {Storage::Splitter::init_process($newhandle2, $splitSize, $destFile, $destDir, $destExt )};
my $h = IPC::Run::harness(@cmd);
if (!$h->run()) {
my ($total, $avail, $mount) = HelpFuncs::getMountSpace($destDir);
Logging::debug("Failed to pack files $destFile in $destDir [ $avail bytes free of $total bytes total on mount point $mount]");
Logging::error("Failed to pack files $destFile in $destDir [ $avail bytes free of $total bytes total on mount point $mount]");
POSIX::close($newhandle);
POSIX::close($newhandle2);
return;
}
POSIX::close($newhandle);
POSIX::close($newhandle2);
if ($unpackedSize =~ /ERR\s(.*)/) {
Logging::error("Unable to pipe data through filter: $1");
return;
}
$self->{unpacked_size} += $unpackedSize;
${$outunpackedSize} = $unpackedSize;
my @ret;
foreach my $line ( split/\n/, $files ) {
my ($file_name, $file_size) = split / /, $line;
$self->{packed_size} += $file_size;
my @filedata;
push @filedata, $file_name;
push @filedata, $file_size;
push @ret, \@filedata;
}
return \@ret if (@ret);
return;
}
sub getFullOutputPath{
my $self = shift;
return "$self->{output_dir}";
}
sub getFilesFromId{
my ($self, $id) = @_;
return $self->{files}->{$id};
}
sub getFilePathFromId{
my ($self, $id) = @_;
return $self->{destdir}->{$id};
}
sub getFilesUnpackSizeFromId{
my ($self, $id) = @_;
return $self->{unpacksize}->{$id};
}
sub regIdFiles{
my ($self, $id, $destDir, $unpackedSize, $files, $shortid ) = @_;
if( $files ){
$destDir = substr( $destDir, length( $self->getFullOutputPath() ) + 1 ) if index( $destDir, $self->getFullOutputPath() )==0;
if( index( $destDir, -1, 1 ) eq '/' ) { $destDir = substr( $destDir, 0, length($destDir)-1 ); }
$id = "$destDir/$id" if $destDir and not $shortid;
$self->{unpacksize}->{$id} = $unpackedSize;
$self->{destdir}->{$id} = "$destDir";
$self->{files}->{$id} = $files;
for my $file( @{$files} ){
chmod S_IRUSR|S_IWUSR|S_IRGRP, $self->getFullOutputPath() . '/' . "$destDir/$file->[0]";
}
return $id;
}
return undef;
}
sub getDumpFiles{
my ($self, $fromPath ) = @_;
my @ret;
while( my( $id, $data ) = each( %{$self->{files}} ) ) {
my $path = $self->getFilePathFromId( $id );
$path = substr( $path, length ($fromPath) ) if $fromPath && index( $path, $fromPath )==0;
$path .= '/' if $path and substr( $path, -1, 1 ) ne '/';
$path = substr( $path, 1 ) if substr ( $path, 0, 1 ) eq '/';
foreach my $filedata( @{$data} ) {
push @ret, "$path$filedata->[0]";
}
}
return @ret;
}
sub CleanupFiles()
{
my $self = shift;
my $pid;
while( ( $pid = wait() ) !=-1 ){
Logging::debug("The child process '$pid' has been terminated" );
}
my $path = $self->getFullOutputPath();
my @files = $self->getDumpFiles();
foreach my $file(@files ){
Logging::debug("Remove file '$file' from repository '$path' ");
unlink "$path/$file" or Logging::debug("Cannot remove file '$path/$file'");
}
if( exists $self->{discovered} ){
foreach my $discovered(@{$self->{discovered}} ){
Logging::debug("Remove discovered '$discovered'");
opendir DIR, $discovered;
my @dirfiles = readdir( DIR );
closedir DIR;
foreach my $file(@dirfiles){
if( $file ne '.' and $file ne '..' ){
unlink "$discovered/$file" or Logging::debug("Cannot remove file '$discovered/$file'");
}
}
rmdir( $discovered ) or Logging::debug("Cannot remove discovered '$discovered'");
}
}
}
sub createRepositoryIndex{
my ( $self, $index ) = @_;
if( $index ){
Logging::debug("Create repository index: $index");
my $destDir = "$self->{output_dir}/.discovered";
system("mkdir", "-p", "$destDir") if not -e $destDir;
open INDEXFILE, "> $destDir/$index";
close INDEXFILE;
}
}
sub writeDiscovered{
my ( $self, $dumpPath, $dumpXmlName, $dumpSize, $ownerGuid, $ownerType, $objectGuid, $objectId ) = @_;
my $idx = rindex( $dumpXmlName, '.xml' );
$dumpXmlName = substr( $dumpXmlName, 0, $idx ) if $idx>0;
my $destDir = $self->getFullOutputPath();
$destDir .= "/$dumpPath" if $dumpPath;
$destDir .= "/.discovered/$dumpXmlName";
push @{$self->{discovered}}, $destDir;
Logging::debug("Create discovered: $destDir");
system("mkdir", "-p", "$destDir") if not -e $destDir;
open SIZEFILE, "> $destDir/size_$dumpSize";
close SIZEFILE;
open OWNERFILE, "> $destDir/owner_$ownerGuid";
close OWNERFILE;
open OWNERTYPEFILE, "> $destDir/ownertype_$ownerType";
close OWNERTYPEFILE;
open GUIDFILE, "> $destDir/GUID_$objectGuid";
close GUIDFILE;
open OBJIDFILE, "> $destDir/objectid_$objectId";
close OBJIDFILE;
my @files;
my $file = ["size_$dumpSize", 0]; push @files, $file;
$file = ["owner_$ownerGuid", 0]; push @files, $file;
$file = ["ownertype_$ownerType", 0]; push @files, $file;
$file = ["GUID_$objectGuid", 0]; push @files, $file;
$file = ["objectid_$objectId", 0]; push @files, $file;
$self->regIdFiles( $destDir, $destDir, 0, \@files );
}
sub getMainDumpXmlFile{
my ($self) = @_;
return $self->{dumpxmlfile};
}
sub getDefExtension{
my ($self) = @_;
return '' if $self->noDefExtension();
return '.tgz' if $self->{gzip_bundle};
return '.tar';
}
sub addDb {
my ($self, $proposedId, %options) = @_;
if ($self->{collectStatistics})
{
$self->{stopWatch}->createMarker("pack");
}
my ($destDir, $destFile, $id) = $self->getFileNameIdFromId($proposedId, $self->{gzip_bundle}, '', 1);
Logging::debug("DB bundle. id=$id, destFile=$destFile");
my $bundle = Storage::Bundle::createDbBundle(%options, 'gzip' => 0 );
return unless $bundle;
my $size = 0;
my $files = $self->executeAndSave($destDir, $destFile, '', $bundle, \$size, 1, 1 );
if ($self->{collectStatistics})
{
$self->{statistics}->{packTime} += $self->{stopWatch}->getDiff("pack");
$self->{stopWatch}->releaseMarker("pack");
}
if( $files and @{$files} ){
my $filename = $files->[0]->[0];
$filename = substr( $filename, length($destDir)+1 ) if index( $filename, $destDir )==0;
my $ret = $self->addTar( $proposedId, "directory" => $destDir, "include" => [$filename] );
foreach my $file( @{$files} ){
$filename = $file->[0];
$filename = substr( $filename, length($destDir)+1 ) if index( $filename, $destDir )==0;
unlink "$destDir/$filename" or Logging::error("Cannot delete temp file '$destDir/$filename'");;
}
return $ret;
}
else{
Logging::error("Failed to execute backup of " . $options{'type'} . " database '" . $options{'name'} . "'");
return undef;
}
}
sub addTar {
my ($self, $proposedId, %options) = @_;
return unless -d $options{'directory'};
if (defined $options{'checkEmptyDir'}) {
return unless checkDirForArchive($options{'directory'}, $options{'exclude'}, $options{'include_hidden_files'});
}
if ($self->{collectStatistics})
{
$self->{stopWatch}->createMarker("pack");
}
my ($destDir, $destFile, $id) = $self->getFileNameIdFromId( $proposedId, $self->getDefExtension(), 1 );
Logging::debug("Tar bundle. id=$id, destFile=$destDir/$destFile");
my $bundle = Storage::Bundle::createTarBundle(%options, 'gzip' => $self->{gzip_bundle});
return unless $bundle;
unless ($bundle)
{
if ($self->{collectStatistics})
{
$self->{statistics}->{packTime} += $self->{stopWatch}->getDiff("pack");
$self->{stopWatch}->releaseMarker("pack");
}
return;
}
my $size = 0;
my $files = $self->executeAndSave($destDir, $destFile, $self->getDefExtension(), $bundle, \$size);
my $ret = $self->regIdFiles( $id, $destDir, $size, $files );
if ($self->{collectStatistics})
{
$self->{statistics}->{packTime} += $self->{stopWatch}->getDiff("pack");
$self->{stopWatch}->releaseMarker("pack");
}
return $ret;
}
sub _getInfoXmlFileName {
my ($self, $fileName ) = @_;
return "$fileName.xml";
#return Storage::Splitter::generateUniqueFileName( $fileName, ".xml" );
}
sub finishXmlFile {
my ($self, $descriptor, $child, $savePath, $fileName) = @_;
if ($self->{collectStatistics})
{
$self->{stopWatch}->createMarker("pack");
}
$fileName = 'dump' if not $fileName;
$savePath = $self->getFullOutputPath() . "/$savePath";
system("mkdir", "-p", "$savePath") if not -e $savePath;
my $dumpFile = $self->_getInfoXmlFileName( $fileName );
Logging::debug("Writing dump file: $savePath/$dumpFile");
open DUMPFILE, "> $savePath/$dumpFile";
if( $child ){
$descriptor->serializeChild(\*DUMPFILE, $child);
} else{
$descriptor->serialize(\*DUMPFILE);
}
close DUMPFILE;
chmod S_IRUSR|S_IWUSR|S_IRGRP, "$savePath/$dumpFile" or Logging::warning("Cannot chmod of '$savePath/$dumpFile'");
my @files;
my @file;
push @file, $dumpFile;
push @file, getFileSize( "$savePath/$dumpFile" );
push @files, \@file;
my $ret = $self->regIdFiles( $dumpFile, $savePath, 0, \@files, $child ? undef : 1 );
$self->{dumpxmlfile} = $dumpFile if not $child;
if ($self->{collectStatistics})
{
$self->{statistics}->{packTime} += $self->{stopWatch}->getDiff("pack");
$self->{stopWatch}->releaseMarker("pack");
}
return $ret;
}
sub finishChild {
my $self = shift;
my $ret = $self->finishXmlFile( @_ );
return $ret;
}
sub finish {
my $self = shift;
my $descriptor = shift;
$self->unreserveSpace();
my $ret = $self->finishXmlFile( $descriptor, undef, @_ );
return 0 if $ret;
return 1;
}
sub createContentList{
my ($self) = @_;
open CONTENT_FILE, ">" . $self->_getContentListFileName();
my @files = $self->getDumpFiles( $self->{output_dir} );
my $fromPath = $self->{output_dir};
my $fullsize = 0;
while( my( $id, $data ) = each( %{$self->{files}} ) ) {
foreach my $filedata( @{$data} ) {
$fullsize += $filedata->[1];
}
}
print CONTENT_FILE "\n";
while( my( $id, $data ) = each( %{$self->{files}} ) ) {
my $path = $self->getFilePathFromId( $id );
$path = substr( $path, length ($fromPath) ) if $fromPath && index( $path, $fromPath )==0;
$path .= '/' if $path and substr( $path, -1, 1 ) ne '/';
$path = substr( $path, 1 ) if substr ( $path, 0, 1 ) eq '/';
foreach my $filedata( @{$data} ) {
print CONTENT_FILE " $path$filedata->[0]\n";
}
}
print CONTENT_FILE " migration.result\n";
print CONTENT_FILE "\n";
close CONTENT_FILE;
}
sub getContentList {
my ($self) = @_;
if ($self->{collectStatistics})
{
$self->{stopWatch}->createMarker("pack");
}
open CONTENT_FILE, $self->_getContentListFileName();
my $s = join "", ;
close CONTENT_FILE;
if ($self->{collectStatistics})
{
$self->{statistics}->{packTime} += $self->{stopWatch}->getDiff("pack");
$self->{stopWatch}->releaseMarker("pack");
}
return $s;
}
sub _getContentListFileName {
my ($self) = @_;
return $self->{output_dir} . "/content-list.xml";
}
sub checkDirForArchive {
my ($srcDir, $exclude, $include_hidden_files) = @_;
# check that directory is not empty
if (!opendir(SRCDIR, $srcDir)) {
return;
}
my $filename;
while (defined ($filename = readdir SRCDIR)) {
my $in_exclude = undef;
next if $filename =~ /^\.\.?$/;
if ( ! $include_hidden_files ) {
next if $filename =~ /^\..*/;
}
if ( ref ($exclude) =~ /ARRAY/ ) {
foreach my $ex (@{$exclude}) {
$in_exclude = 1 if $filename eq $ex;
}
next if defined $in_exclude;
}
# directory is not empty
closedir(SRCDIR);
return 1;
}
# directory is empty
closedir(SRCDIR);
return;
}
1;
# Local Variables:
# mode: cperl
# cperl-indent-level: 2
# indent-tabs-mode: nil
# tab-width: 4
# End: