Как добавить полную древовидную структуру в файл .tar.bz2 с помощью Perl?

Я хочу сжать много данных, распределенных по множеству подкаталогов, в архив. Я не могу просто использовать встроенные функции tar, потому что мне нужен сценарий Perl для работы как в среде Windows, так и в среде Linux. Я нашел модуль Archive::Tar, но их документация выдает предупреждение:

Обратите внимание, что этот метод [create_archive()] не записывает on the fly как бы; он по-прежнему считывает все файлы в память перед записью архива. Если это проблема, обратитесь к приведенным ниже часто задаваемым вопросам.

Из-за огромного размера моих данных я хочу писать «на лету». Но я не могу найти в FAQ полезной информации о записи файлов. Предлагают использовать итератор iter():

Возвращает функцию итератора, которая читает файл tar, не загружая его в память. Каждый раз, когда вызывается функция, она возвращает следующий файл в архиве.

my $next = Archive::Tar->iter( "example.tar.gz", 1, {filter => qr/\.pm$/} );
while( my $f = $next->() ) {
    print $f->name, "\n";
    $f->extract or warn "Extraction failed";
    # ....
}

Но здесь обсуждается только чтение файлов, а не запись сжатого архива. Итак, у меня вопрос: как я могу взять каталог $dir и рекурсивно добавить его в архив archive.tar.bz2 со сжатием bzip2 в удобной для памяти манере, то есть без предварительной загрузки всего дерева в память?

Я попытался создать свой собственный сценарий с предложениями в комментариях, используя Archive::Tar::Streamed и IO::Compress::Bzip2, но безуспешно.

use strict;
use warnings;

use Archive::Tar::Streamed;
use File::Spec qw(catfile);
use IO::Compress::Bzip2 qw(bzip2 $Bzip2Error);

my ($in_d, $out_tar, $out_bz2) = @ARGV;

open(my $out_fh,'>', $out_tar) or die "Couldn't create archive";
binmode $out_fh;

my $tar = Archive::Tar::Streamed->new($out_fh);

opendir(my $in_dh, $in_d) or die "Could not opendir '$in_d': $!";
while (my $in_f = readdir $in_dh) {
  next unless ($in_f =~ /\.xml$/);
  print STDOUT "Processing $in_f\r";
  $in_f = File::Spec->catfile($in_d, $in_f);
  $tar->add($in_f);
}

print STDOUT "\nBzip'ing $out_tar\r";

 bzip2 $out_tar => $out_bz2
    or die "Bzip2 failed: $Bzip2Error\n";

Очень быстро в моей системе заканчивается память. В моей текущей системе доступно 32 ГБ, но они почти сразу переполняются. Размер некоторых файлов в каталоге, который я пытаюсь добавить в архив, превышает 32 ГБ.

Память превышена

Поэтому мне интересно, должен ли каждый файл быть полностью прочитан в памяти даже в классе Streamed перед добавлением в архив? Я предполагал, что сами файлы будут передаваться в буфере в архив, но, возможно, просто вместо того, чтобы сначала сохранять ВСЕ файлы в памяти, Streamed позволяет полностью использовать только один файл в памяти, а затем добавлять его в архив один за другим. ?


person Bram Vanroy    schedule 30.07.2017    source источник
comment
По теме: stackoverflow.com/questions/653127/   -  person melpomene    schedule 30.07.2017
comment
Что означает "разные платформы"? Вам нужно вытащить эти файлы из нескольких систем?   -  person Borodin    schedule 30.07.2017
comment
@Borodin Я имел в виду, что скрипт должен работать как в Windows, так и в Linux. Я отредактировал первый абзац, чтобы отразить это.   -  person Bram Vanroy    schedule 30.07.2017
comment
Разве нельзя просто установить tar программу в Windows? В долгосрочной перспективе может быть проще.   -  person melpomene    schedule 30.07.2017
comment
@melpomene Я мог бы. Но как мне тогда написать сценарий, который был бы достаточно общим, чтобы мне не нужно было ничего менять, чтобы он работал под Linux (встроенный tar) и Windows (не встроенный)? (taring не является автономным и является частью более крупного Perl-скрипта.)   -  person Bram Vanroy    schedule 30.07.2017
comment
@BramVanroy: Хорошо, спасибо. Вы смотрели Archive::Tar::Streamed, как описано в вопрос, с которым мелпомена связана? Вопреки принятому ответу, для него не требуется tar служебная программа командной строки, и это должно подойти в ваших системах Windows. В документации говорится, что он также нацелен на переносимость и доступность на платформах без собственного tar.   -  person Borodin    schedule 30.07.2017
comment
@Borodin Спасибо, что изучили это. Я посмотрел на это, но, насколько я могу судить, невозможно добавить метод сжатия в качестве аргумента в класс Streamed. Означает ли это, что я должен «архивировать» созданный файл tar, в любом случае используя Archive :: Tar? Но разве это не означает, что все tar (возможно, сотни гигабайт) должны быть прочитаны в памяти?   -  person Bram Vanroy    schedule 30.07.2017
comment
Возможно, вы могли бы использовать IO :: Compress :: Bzip2 для bzip-архива.   -  person melpomene    schedule 30.07.2017
comment
@ Бородин и Мельпомена, пожалуйста, посмотрите мою правку.   -  person Bram Vanroy    schedule 30.07.2017
comment
Streamed позволяет полностью разместить только один файл в памяти, а затем добавлять его в архив по одному? Основное отличие от Archive::Tar в том, что tar-файл создается постепенно на диске, а не в памяти. Добавление файла или списка файлов в архив потребует, чтобы все данные этих файлов находились в памяти, какой бы модуль ни использовался. Это можно свести к минимуму, добавляя только один файл за раз. Включают ли ваши данные какие-либо отдельные файлы размером в несколько гигабайт? Я написал короткое решение и опубликую его завтра, если у вас нет файлов, которые не умещаются в памяти.   -  person Borodin    schedule 30.07.2017
comment
@Borodin К сожалению, как я написал в своем посте , в моей текущей системе доступно 32 ГБ, но они почти сразу переполняются. Размер некоторых файлов в каталоге, который я пытаюсь добавить в архив, превышает 32 ГБ. Так что да, некоторые файлы больше, чем моя доступная память. Тем не менее, пожалуйста, опубликуйте свое решение, потому что мне понадобится что-то подобное для каталога, который состоит из множества подкаталогов, содержащих все множество маленьких файлов. Кстати, для ясности: то, что это невозможно в Perl, не означает, что я не могу сделать это в простой командной строке (оператор обратного апострофа), верно?   -  person Bram Vanroy    schedule 30.07.2017
comment
@BramVanroy: Завтра я подумаю о написании варианта Archive::Tar, который направляет на вывод отдельные файлы, а также весь архив. Это не должно быть сложно. Между тем, да, вы можете использовать system или обратные кавычки. Windows не поставляется с архиватором командной строки tar или bzip2, но с GnuWin предоставляет и то, и другое.   -  person Borodin    schedule 30.07.2017
comment
@Borodin Звучит интересно. Возможно, цитата из ответа Синана Унура поможет. Если вы планируете создать свой собственный модуль архива и разместить его на CPAN, это было бы круто! Я могу представить, что многие люди, работающие с большими данными и Perl (а также использующие Windows-машину для тестирования некоторых вещей), тоже найдут это полезным!   -  person Bram Vanroy    schedule 31.07.2017
comment
@BramVanroy: Он может оказаться на CPAN, но потребуется много работы, чтобы сделать его достойным этого. Я собираюсь написать что-то, что будет работать именно для вашей ситуации в настоящее время. Поддержка таких вещей, как символические ссылки, может подождать.   -  person Borodin    schedule 31.07.2017


Ответы (2)


К сожалению, то, что вы хотите, невозможно в Perl:

Я согласен, было бы неплохо, если бы этот модуль мог записывать файлы по частям, а затем переписывать заголовки (чтобы поддерживать связь Archive :: Tar, выполняющую запись). Возможно, вы могли бы пройти по архиву в обратном направлении, зная, что вы разбили файл на N записи, удалили лишние заголовки и обновили первый заголовок, указав сумму их размеров.

На данный момент единственные варианты: использовать Archive::Tar::File, разделить данные на управляемые размеры вне perl или использовать команду tar напрямую (чтобы использовать ее из perl, на CPAN есть хорошая оболочка: Archive::Tar::Wrapper).

Я не думаю, что у нас когда-либо будет действительно нерезидентная tar реализация на Perl, основанная на Archive::Tar. Если честно, сам Archive::Tar должен быть переписан или заменен чем-то другим.

person Sinan Ünür    schedule 30.07.2017

Это исходная версия моего решения, которая по-прежнему хранит в памяти целый файл. У меня, наверное, сегодня не будет времени добавить обновление, которое хранит только частичные файлы, так как модуль Archive::Tar не имеет самого удобного API

use strict;
use warnings 'all';
use autodie; # Remove need for checks on IO calls

use File::Find 'find';
use Archive::Tar::Streamed ();
use Compress::Raw::Bzip2;
use Time::HiRes qw/ gettimeofday tv_interval /;

# Set a default root directory for testing
#
BEGIN {
    our @ARGV;
    @ARGV = 'E:\test' unless @ARGV;
}

use constant ROOT_DIR => shift;

use constant KB => 1024;
use constant MB => KB * KB;
use constant GB => MB * KB;

STDOUT->autoflush; # Make sure console output isn't buffered

my $t0 = [ gettimeofday ];

# Create a pipe, and fork a child that will build a tar archive
# from the files and pass the result to the pipe as it is built
#
# The parent reads from the pipe and passes each chunk to the
# module for compression. The result of zipping each block is
# written directly to the bzip2 file
#
pipe( my $pipe_from_tar, my $pipe_to_parent );  # Make our pipe
my $pid  = fork;                      # fork the process

if ( $pid == 0 ) {    # child builds tar and writes it to the pipe

    $pipe_from_tar->close;    # Close the parent side of the pipe
    $pipe_to_parent->binmode;
    $pipe_to_parent->autoflush; 

    # Create the ATS object, specifiying that the tarred output
    # will be passed straight to the pipe
    #
    my $tar = Archive::Tar::Streamed->new( $pipe_to_parent );

    find(sub {

        my $file = File::Spec->canonpath( $File::Find::name );
        $tar->add( $file );

        print "Processing $file\n" if -d;

    }, ROOT_DIR );

    $tar->writeeof; # This is undocumented but essential

    $pipe_to_parent->close;
}
else {    # parent reads the tarred data, bzips it, and writes it to the file

    $pipe_to_parent->close; # Close the child side of the pipe
    $pipe_from_tar->binmode;

    open my $bz2_fh, '>:raw', 'T:\test.tar.bz2';
    $bz2_fh->autoflush;

    # The first parameter *must* have a value of zero. The default
    # is to accumulate each zipped chunnk into the output variable,
    # whereas we want to write each chunk to a file
    #
    my ( $bz, $status ) = Compress::Raw::Bzip2->new( 0 );
    defined $bz or die "Cannot create bunzip2 object: $status\n";

    my $zipped;

    while ( my $len = read $pipe_from_tar, my $buff, 8 * MB ) {

        $status = $bz->bzdeflate( $buff, $zipped );
        $bz2_fh->print( $zipped ) if length $zipped;
    }

    $pipe_from_tar->close;

    $status = $bz->bzclose( $zipped );
    $bz2_fh->print( $zipped ) if length $zipped;

    $bz2_fh->close;

    my $elapsed = tv_interval( $t0 );

    printf "\nProcessing took %s\n", hms($elapsed);
}


use constant MINUTE => 60;
use constant HOUR   => MINUTE * 60;

sub hms {
    my ($s) = @_;

    my @ret;

    if ( $s > HOUR ) {
        my $h = int($s / HOUR);
        $s -= $h * HOUR;
        push @ret, "${h}h";
    }

    if ( $s > MINUTE or @ret ) {
        my $m = int($s / MINUTE);
        $s -= $m * MINUTE;
        push @ret, "${m}m";
    }

    push @ret, sprintf "%.1fs", $s;

    "@ret";
}
person Borodin    schedule 31.07.2017