【问题标题】:Append to gzipped Tar-Archive附加到 gzipped Tar-Archive
【发布时间】:2012-12-30 19:25:07
【问题描述】:

我写了一个程序,生成一个tarball,它被zlib压缩。
每隔一段时间,同一个程序应该向 tarball 添加一个新文件。

根据定义,tarball 需要empty records(512 字节块)才能在最后正常工作,这已经表明了我的问题。

根据文档gzopen无法在r+模式下打开文件,这意味着我不能简单地跳转到空记录的开头,附加我的文件信息并用空记录再次密封。

现在,我束手无策。只要不涉及空记录,附加就可以在 zlib 中正常工作,但我需要它们来“完成”我的压缩 tarball。

有什么想法吗?

啊,是的,如果我可以避免解压缩整个内容和/或解析整个 tarball,那就太好了。

我也可以使用其他(最好是简单的)文件格式来代替 tar。

【问题讨论】:

    标签: c++ compression append tar zlib


    【解决方案1】:

    这是两个独立的问题,都可以解决。

    首先是如何附加到 tar 文件。您需要做的就是用您的文件覆盖最后两个归零的 512 字节块。您将编写 512 字节的 tar 标头,将文件四舍五入为 512 字节块的整数,然后用 0 填充两个 512 字节块以标记 tar 文件的新结尾。

    第二个是如何频繁地追加到一个 gzip 文件中。最简单的方法是编写单独的 gzip 流并将它们连接起来。在单独的 gzip 流中写入最后两个 512 字节的零块,并记住从哪里开始。然后用带有新 tar 条目的新 gzip 流覆盖它,然后用两个末端块覆盖另一个 gzip 流。这可以通过使用lseek() 在文件中查找然后使用gzdopen() 从那里开始写入来完成。

    对于添加的大文件(至少 10 的 K),这将工作得很好,压缩效果很好。但是,如果您要添加非常小的文件,则简单地连接小的 gzip 流将导致糟糕的压缩,或者更糟的是,扩展。您可以做一些更复杂的事情,将少量数据实际添加到单个 gzip 流中,以便压缩算法可以利用前面的数据进行关联和字符串匹配。为此,请查看zlib 分布中examples/gzlog.hgzlog.c 中的方法。

    下面是一个简单方法的例子:

    /* tapp.c -- Example of how to append to a tar.gz file with concatenated gzip
       streams. Placed in the public domain by Mark Adler, 16 Jan 2013. */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <assert.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include "zlib.h"
    
    #define local static
    
    /* Build an allocated string with the prefix string and the NULL-terminated
       sequence of words strings separated by spaces.  The caller should free the
       returned string when done with it. */
    local char *build_cmd(char *prefix, char **words)
    {
        size_t len;
        char **scan;
        char *str, *next;
    
        len = strlen(prefix) + 1;
        for (scan = words; *scan != NULL; scan++)
            len += strlen(*scan) + 1;
        str = malloc(len);                                  assert(str != NULL);
        next = stpcpy(str, prefix);
        for (scan = words; *scan != NULL; scan++) {
            *next++ = ' ';
            next = stpcpy(next, *scan);
        }
        return str;
    }
    
    /* Usage:
    
          tapp archive.tar.gz addthis.file andthisfile.too
    
       tapp will create a new archive.tar.gz file if it doesn't exist, or it will
       append the files to the existing archive.tar.gz.  tapp must have been used
       to create the archive in the first place.  If it did not, then tapp will
       exit with an error and leave the file unchanged.  Each use of tapp appends a
       new gzip stream whose compression cannot benefit from the files already in
       the archive.  As a result, tapp should not be used to append a small amount
       of data at a time, else the compression will be particularly poor.  Since
       this is just an instructive example, the error checking is done mostly with
       asserts.
     */
    int main(int argc, char **argv)
    {
        int tgz;
        off_t offset;
        char *cmd;
        FILE *pipe;
        gzFile gz;
        int page;
        size_t got;
        int ret;
        ssize_t raw;
        unsigned char buf[3][512];
        const unsigned char z1k[] =     /* gzip stream of 1024 zeros */
            {0x1f, 0x8b, 8, 0, 0, 0, 0, 0, 2, 3, 0x63, 0x60, 0x18, 5, 0xa3, 0x60,
             0x14, 0x8c, 0x54, 0, 0, 0x2e, 0xaf, 0xb5, 0xef, 0, 4, 0, 0};
    
        if (argc < 2)
            return 0;
        tgz = open(argv[1], O_RDWR | O_CREAT, 0644);        assert(tgz != -1);
        offset = lseek(tgz, 0, SEEK_END);                   assert(offset == 0 || offset >= (off_t)sizeof(z1k));
        if (offset) {
            if (argc == 2) {
                close(tgz);
                return 0;
            }
            offset = lseek(tgz, -sizeof(z1k), SEEK_END);    assert(offset != -1);
            raw = read(tgz, buf, sizeof(z1k));              assert(raw == sizeof(z1k));
            if (memcmp(buf, z1k, sizeof(z1k)) != 0) {
                close(tgz);
                fprintf(stderr, "tapp abort: %s was not created by tapp\n", argv[1]);
                return 1;
            }
            offset = lseek(tgz, -sizeof(z1k), SEEK_END);    assert(offset != -1);
        }
        if (argc > 2) {
            gz = gzdopen(tgz, "wb");                        assert(gz != NULL);
            cmd = build_cmd("tar cf - -b 1", argv + 2);
            pipe = popen(cmd, "r");                         assert(pipe != NULL);
            free(cmd);
            got = fread(buf, 1, 1024, pipe);                assert(got == 1024);
            page = 2;
            while ((got = fread(buf[page], 1, 512, pipe)) == 512) {
                if (++page == 3)
                    page = 0;
                ret = gzwrite(gz, buf[page], 512);          assert(ret == 512);
            }                                               assert(got == 0);
            ret = pclose(pipe);                             assert(ret != -1);
            ret = gzclose(gz);                              assert(ret == Z_OK);
            tgz = open(argv[1], O_WRONLY | O_APPEND);       assert(tgz != -1);
        }
        raw = write(tgz, z1k, sizeof(z1k));                 assert(raw == sizeof(z1k));
        close(tgz);
        return 0;
    }
    

    【讨论】:

    • 记住 tar 流的结束位置并使用带有附加标志的 gzseek()gzopen() 不是更容易、更有效吗?我会假设zlib 会自动读取相关的树描述。至少您可以将压缩优化保留或推送到 zlib。我想知道 append 是继续 gzip 流还是开始一个新的流。但我会假设他们让它变得高效。努力似乎是一样的——记住seek 的一些位置。此外,这似乎比滥用 TAR 规范更努力:)
    • 您不能使用 gzseek() 覆盖 gzip 流的一部分。 zlib 不支持。
    • 但是关于@luk32 的建议,我从 tar 文件中截断了终止块,GNU tar 和 BSD tar 都没有抱怨它。因此,GNU tar 文档在这种情况下发出警告是不正确的,或者至少是过时的。我正在使用 GNU tar 1.17 和 BSD tar 2.8.3。
    • 对不起,但我已阅读如下“gzseek [...] 设置给定压缩文件上下一个 gzread 或 gzwrite 的起始位置。”我的答案基于我在文档中阅读的内容。我没有写代码来检查。我从来没有说过你的分析器会打破 TAR 规范。我的可以。是的,但我已经说第三次了。但 gnu 参考解压器将其视为警告,并表示不应依赖它。
    • 您需要继续阅读文档,再多读五行。 “如果打开文件进行写入,则仅支持前向搜索;然后 gzseek 将一系列零压缩到新的起始位置。”
    【解决方案2】:

    在我看来,严格遵守标准的 TAR 是不可能的。我已经阅读了zlib[1] 手册和GNU tar[2] 文件规范。我没有找到任何信息如何附加到 TAR 可以实现。所以我假设它必须通过覆盖空块来完成。

    所以我再次假设您可以使用gzseek() 来实现。但是,您需要知道未压缩存档 (size) 的大小并将 offset 设置为 size-2*512。 请注意,这可能很麻烦,因为“whence 参数在 lseek(2) 中定义;不支持值 SEEK_END。”1 并且您无法同时打开文件进行读取和写入,即反省结束块在哪里。

    然而,应该有可能稍微滥用 TAR 规范。 GNU tar[2] 文档提到了一些有趣的

    " 每个存档的文件都由一个描述文件的标题块表示,然后是零个或多个给出文件内容的块。在存档文件的末尾有两个 512 字节的块,用二进制零填充作为文件结束标记。一个合理的系统应该在档案的末尾写入这样的文件结束标记,但不能假设在读取档案时存在这样的块。特别是 GNU tar 如果没有遇到它,它总是会发出警告。 "

    这意味着,您可以故意不编写这些块。如果您编写了 tarball 压缩器,这很容易。然后你可以在正常的追加模式下使用zlib,记住TAR解压器必须知道"broken" TAR文件。

    [1]http://www.zlib.net/manual.html#Gzip [2]http://www.gnu.org/software/tar/manual/html_node/Standard.html#SEC182

    【讨论】:

    • +1 以获得彻底的答案,尽管这是我自己得出的结论。感谢您批准我的想法。
    • 我确实意识到大部分答案都是您的结论。但我用文档支持它。您还说您需要最后的块,并且您可以使用其他一些简单的文件格式。问题是你不需要它们,甚至 GNU 规范都说它会导致“仅”一个警告。最后,您可以说您的替代文件格式是 TAR,没有这些块,但需要标准库 eof() 或类似的东西。
    猜你喜欢
    • 1970-01-01
    • 2010-11-09
    • 2013-09-13
    • 1970-01-01
    • 2020-07-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多