LoginSignup
11
6

System.Formats.Tarの使い方

Last updated at Posted at 2023-06-09

はじめに

.NET 7から、tarの読み書き機能がベースライブラリに追加されたので、メモ代わりに使い方を書く。
なお、この記事ではtarに関する説明はあまりしないので、下記リンクのような解説ページを参照のこと。
参考URL: https://www.gnu.org/software/tar/manual/html_section/Formats.html

使える条件

  • .NET 7以降であること。
    • .NET 6では扱えない。多分UnixFileModeを使っているため

TARの読み書きはサードパーティ製のものが既にあるので、net6.0以前で使いたい場合はそちらを使う事。

シンプルな使い方

System.Formats.Tar.TarFileクラスのユーティリティを使えば、tarファイルの圧縮、展開ができる。
なお、シンボリックリンク、通常ファイル以外のファイル(キャラクタデバイスファイルとか)も対応している。
生成、展開ともに同期版と非同期版両方が用意してある。

形式はPOSIX.1-2001(pax)フォーマット、通常ファイルの権限は0644になる。

生成

void TarFile.CreateFromDirectoryまたはTask TarFile.CreateFromDirectoryAsyncを使用する。
非同期版はCancellationTokenで途中キャンセルが可能。

どちらも展開先はファイルパスで指定するか、あるいはStreamで指定する。
tgzを作りたい場合はStreamにして、GZipStream等と組み合わせる必要がある。

展開

void TarFile.ExtractToDirectoryまたはTask TarFile.ExtractToDirectoryAsync を使用する。
こちらも非同期版はCancellationTokenで途中キャンセル可能。

ソースはファイルパスで指定するか、あるいはStreamで指定する。
tgzから展開する場合はGZipStream等と組み合わせてStreamで指定する必要がある。

使用例

using System.Formats.Tar;

{
    // アーカイブの生成
    // tgz等を生成したい場合はStream版を使う
    await TarFile.CreateFromDirectoryAsync("srcdir", "result.tar", true);
    // using var stm = File.Create("result.tar");
    // await TarFile.CreateFromDirectoryAsync("srcdir", stm, true);
}
{
    // アーカイブの展開
    // tgz等を展開したい場合はStream版を使う。
    await TarFile.ExtractToDirectoryAsync("result.tar", "dstdir", true);
    // using var stm = File.OpenRead("result.tar");
    // await TarFile.ExtractToDirectoryAsync(stm, "dstdir", true);
}

柔軟な生成、展開をしたい場合

TarFileはあるディレクトリ以下を一律に扱う。しかし、実運用では不都合になる場合もある。
例えば、特定パターンの除外、権限の設定等。
より柔軟な生成・展開を行いたい場合、"TarWriter"と"TarReader"を使う必要がある。

生成

アーカイブ生成にはTarWriterを使用する。
まず最初にnew TarWriter(Stream s, TarEntryFormat f, bool leaveOpen)で書き込み先インスタンスを作成する。
ここでTarEntryFormatを指定することにより、生成するtarファイルの形式が決まる。
基本的にはPaxを選んでおけば間違いはない。V7は最も古い形式で制約も多いのでまず使うことはないだろう。
古いアーカイバーではPaxを認識しないことがたまにあるので、そういう時はUstarを選ぶ必要があるかもしれない。

使用可能なTarEntryFormatは、下記URLを参照のこと。
https://learn.microsoft.com/dotnet/api/system.formats.tar.tarentryformat?view=net-8.0

全てのエントリの書き込みが終わった後にDisposeすれば、書き込みは完了となる。

エントリの追加

TarWriterインスタンスを作成したら、そこに個々のエントリを追加していく。
この時、最初に指定したTarEntryFormatごとに生成するエントリのインスタンスが異なってくるので注意する。
それぞれの対応は以下のようになる。

TarEntryFormat 対応するクラス
Gnu GnuTarEntry
Pax PaxTarEntry
Ustar UstarEntry
V7 V7TarEntry

どのエントリクラスも、TarEntryがベースとなっており、

  1. 名前(ディレクトリ名含む)をコンストラクタで決定
  2. 権限、UID、GID等の各種追加属性の設定
  3. Stream DataStreamプロパティに、データ部分を入れたStreamインスタンスをセット
    • データ部が無いタイプ(ディレクトリエントリ等)あるいは空の場合はnullでも可
  4. TarWriter.WriteEntry(TarEntry)で書き込み

という流れとなる。

展開

TarReaderインスタンスを使用する。
new TarReader(Stream stm, bool leaveOpen)でインスタンスを生成する。
TarEntryFormatは内部で識別する仕様となっているので指定の必要は無い。

エントリの走査と展開

TarEntry? TarReader.GetNextEntry(bool copyData = false)で、エントリを先頭から順番に取り出す仕様となっている。
戻り値にnullが返ってきた時が終端となる。
copyDataをtrueにすると、データ部分を新しく生成したMemoryStreamに乗せてTarEntryを作成する。
大きいデータが来ると予想される場合は注意しよう。
なお、GetNextEntryには時間がかかる場合もあるので、そのような場合にはTask TarReader.GetNextEntryAsync(bool copyData, CancellationToken) を使用する。
TarEntryFormat TarEntry.Formatで形式が判別可能なので、名前とデータ部分以外の情報が欲しい場合は、
個別エントリにキャストを行う。

使用例

// using System.Formats.Tar;
// pax生成
using (var f = File.Create("out-pax.tar"))
using (var tw = new TarWriter(f, TarEntryFormat.Pax, false))
{
    {
        string longname = new string('a', 512);
        var entry = new PaxTarEntry(TarEntryType.RegularFile, longname);
        using (var mstm = new MemoryStream())
        {
            byte[] data = new byte[64];
            data.AsSpan().Fill(1);
            mstm.Write(data.AsSpan());
            mstm.Seek(0, SeekOrigin.Begin);
            entry.DataStream = mstm;
            tw.WriteEntry(entry);
        }
    }
    {
        var entry = new PaxTarEntry(TarEntryType.RegularFile, "b.txt");
        using (var mstm = new MemoryStream())
        {
            byte[] data = new byte[32];
            data.AsSpan().Fill(2);
            mstm.Write(data.AsSpan());
            mstm.Seek(0, SeekOrigin.Begin);
            entry.DataStream = mstm;
            tw.WriteEntry(entry);
        }
    }
}
// 展開
using (var f = File.OpenRead("out-pax.tar"))
using (var tr = new TarReader(f))
{
    while (true)
    {
        var entry = tr.GetNextEntry();
        if (entry == null)
        {
            break;
        }
        // 本体データを読み込まずに次へ行くことも可
        Console.WriteLine($"{entry.Name},{entry.Format}");
    }
}

使用例2: tgzを作成、展開するやり方

tgzを圧縮、展開するやり方も掲載しておく。
今はtar.xz等も出回っているが、圧縮/展開Streamを作るところでxz.netsharpcompress(展開のみ)が追加で必要になる。

using System.Formats.Tar;
using System.IO.Compression;

{
    using var f = File.Create("out.tgz");
    using var fgz = new GZipStream(f, CompressionLevel.Optimal);
    using var tw = new TarWriter(fgz, TarEntryFormat.Pax);
    var entry = new PaxTarEntry(TarEntryType.RegularFile, "x.txt");
    using var mstm = new MemoryStream((new byte[128]).Select(_ => (byte)'a').ToArray());
    entry.DataStream = mstm;
    tw.WriteEntry(entry);
}
{
    using var f = File.OpenRead("out.tgz");
    using var fgz = new GZipStream(f, CompressionMode.Decompress);
    using var tr = new TarReader(fgz);
    var entry = tr.GetNextEntry();
    if(entry == null)
    {
        throw new NullReferenceException();
    }
    Console.WriteLine($"{entry.Name}, {entry.EntryType}");
}

終わりに

tarが標準で使えるようになったことで、依存がより少ないパッケージが作れるようになったのは大きい。
tar.**部分を判別してくれず、別途Streamを分けないといけないのは書いてると煩わしいと思うかもしれないが、そこにリソースを割くよりもAPIの簡潔さを優先させたという判断は納得できる。
非Windows環境で多く使われているTarを正式サポートしたことが、より.NETを普遍的に動かしたいという姿勢を示しているようでいて、興味深い。

11
6
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
6