v9.10.2019.0101  全面巩固批量Insert/Update/Upsert,支持数据备份、恢复和同步,支持实体列表保存到文件以及加载
大石头 authored at 2019-01-01 13:38:33
12.08 KiB
X
using System.Text;
using NewLife;
using NewLife.Compression;
using NewLife.Log;
using Xunit;

namespace XUnitTest.Compression;

//[CollectionDefinition("TarArchiver", DisableParallelization = true)]
//public class TarArchiverTestsCollection
//{
//    // 这个类不需要包含任何代码
//    // 它的作用是定义一个测试集合,并禁用并行化
//}

//[Collection("TarArchiver")]
[TestCaseOrderer("NewLife.UnitTest.DefaultOrderer", "NewLife.UnitTest")]
public class TarFileTests
{
    private static String _testDir;
    private static String _outputDir;
    private static String _tarFile;

    static TarFileTests()
    {
        XTrace.WriteLine("TarArchiverTests");

        var root = NewLife.Setting.Current.DataPath.GetFullPath();

        // 准备测试目录和文件
        _testDir = Path.Combine(root, "TarTest_Source");
        _outputDir = Path.Combine(root, "TarTest_Extract");
        _tarFile = Path.Combine(root, "test.tar");

        // 确保目录存在且为空
        if (Directory.Exists(_testDir)) Directory.Delete(_testDir, true);
        if (Directory.Exists(_outputDir)) Directory.Delete(_outputDir, true);
        if (File.Exists(_tarFile)) File.Delete(_tarFile);

        Directory.CreateDirectory(_testDir);
        Directory.CreateDirectory(_outputDir);

        // 创建测试文件
        CreateTestFile(Path.Combine(_testDir, "test1.txt"), "这是测试文件1的内容");
        CreateTestFile(Path.Combine(_testDir, "test2.txt"), "这是测试文件2的内容,包含一些中文字符");

        // 创建子目录和文件
        var subDir = Path.Combine(_testDir, "subdir");
        Directory.CreateDirectory(subDir);
        CreateTestFile(Path.Combine(subDir, "test3.txt"), "子目录中的测试文件");
    }

    static void CreateTestFile(String path, String content)
    {
        File.WriteAllText(path, content);
    }

    [Fact(DisplayName = "测试创建Tar文件")]
    public void TestCreateTar()
    {
        if (File.Exists(_tarFile)) File.Delete(_tarFile);
        using var archiver = new TarFile(_tarFile, true);

        // 创建tar文件
        archiver.CreateFromDirectory(_testDir);

        // 验证tar文件已创建
        Assert.True(File.Exists(_tarFile));

        // 验证文件大小大于0
        var fileInfo = new FileInfo(_tarFile);
        Assert.True(fileInfo.Length > 0);

        // 验证文件清单
        var fileList = archiver.Entries.Select(e => e.FileName).ToList();
        Assert.Equal(3, fileList.Count);
        Assert.Contains("test1.txt", fileList);
        Assert.Contains("test2.txt", fileList);
        Assert.Contains("subdir/test3.txt", fileList);
    }

    [Fact(DisplayName = "测试解压Tar文件")]
    public void TestExtractTar()
    {
        if (File.Exists(_tarFile)) File.Delete(_tarFile);
        {
            using var archiver = new TarFile(_tarFile, true);

            // 先创建tar文件
            archiver.CreateFromDirectory(_testDir);
        }

        // 解压tar文件
        using var extractArchiver = new TarFile(_tarFile);
        extractArchiver.ExtractToDirectory(_outputDir, true);

        // 验证文件已解压
        Assert.True(File.Exists(Path.Combine(_outputDir, "test1.txt")));
        Assert.True(File.Exists(Path.Combine(_outputDir, "test2.txt")));
        Assert.True(File.Exists(Path.Combine(_outputDir, "subdir", "test3.txt")));

        // 验证文件内容
        Assert.Equal("这是测试文件1的内容", File.ReadAllText(Path.Combine(_outputDir, "test1.txt")));
        Assert.Equal("这是测试文件2的内容,包含一些中文字符", File.ReadAllText(Path.Combine(_outputDir, "test2.txt")));
        Assert.Equal("子目录中的测试文件", File.ReadAllText(Path.Combine(_outputDir, "subdir", "test3.txt")));

        // 验证文件清单
        var fileList = extractArchiver.Entries.Select(e => e.FileName).ToList();
        Assert.Equal(3, fileList.Count);
        Assert.Contains("test1.txt", fileList);
        Assert.Contains("test2.txt", fileList);
        Assert.Contains("subdir/test3.txt", fileList);
    }

    [Fact(DisplayName = "测试空目录创建Tar")]
    public void TestCreateTarFromEmptyDirectory()
    {
        // 测试工程的多个目标框架会并行运行,固定名称会在系统临时目录互相踩踏,用进程号隔离
        var root = Path.Combine(Path.GetTempPath(), "EmptyTarTest_" + Environment.ProcessId);
        if (Directory.Exists(root)) Directory.Delete(root, true);

        var emptyDir = Path.Combine(root, "EmptyTarTest");
        Directory.CreateDirectory(emptyDir);

        var emptyTarFile = Path.Combine(root, "empty.tar");

        using var archiver = new TarFile(emptyTarFile, true);
        archiver.CreateFromDirectory(emptyDir);

        // 验证tar文件已创建且文件清单为空
        Assert.True(File.Exists(emptyTarFile));
        Assert.Empty(archiver.Entries);
    }

    [Fact(DisplayName = "测试文件头读写")]
    public void TestTarHeaderReadWrite()
    {
        // 创建一个TarHeader
        var originalHeader = new TarEntry
        {
            FileName = "test.txt",
            Mode = "0000644",
            OwnerId = "0000000",
            GroupId = "0000000",
            FileSize = 1024,
            LastModified = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc),
            Checksum = 0,
            TypeFlag = TarEntryType.RegularFile,
            LinkName = String.Empty,
            Magic = "ustar",
            Version = 0,
            OwnerName = String.Empty,
            GroupName = String.Empty,
            DeviceMajor = "0000000\0",
            DeviceMinor = "0000000\0",
            Prefix = String.Empty
        };

        // 写入内存流
        var stream = new MemoryStream();
        originalHeader.Write(stream);

        // 重置流位置以便读取
        stream.Position = 0;

        // 读取头部
        var readHeader = TarEntry.Read(stream);

        // 验证读取的头部与原始头部一致
        Assert.NotNull(readHeader);
        Assert.Equal(originalHeader.FileName, readHeader.FileName);
        Assert.Equal(originalHeader.FileSize, readHeader.FileSize);
        Assert.Equal(originalHeader.TypeFlag, readHeader.TypeFlag);
        Assert.Equal(originalHeader.Magic, readHeader.Magic);
    }

    [Fact(DisplayName = "测试超长文件名")]
    public void TestLongFileName()
    {
        var longFileName = new String('a', 90) + ".txt";
        var longFilePath = Path.Combine(_testDir, longFileName);

        // 创建带有长文件名的测试文件
        CreateTestFile(longFilePath, "这是一个有很长文件名的测试文件");

        if (File.Exists(_tarFile)) File.Delete(_tarFile);
        using var archiver = new TarFile(_tarFile, true);
        archiver.CreateFromDirectory(_testDir);
        archiver.Dispose();

        // 解压并验证长文件名被正确处理
        using var extractArchiver = new TarFile(_tarFile);
        extractArchiver.ExtractToDirectory(_outputDir);

        Assert.True(File.Exists(Path.Combine(_outputDir, longFileName)));
        Assert.Contains(longFileName, extractArchiver.Entries.Select(e => e.FileName).ToList());
    }

    [Fact(DisplayName = "测试超长文件名2")]
    public void TestLongFileName2()
    {
        // 手工构造 GNU 格式长文件名归档(GNU tar 产出形态),验证外部归档的读取兼容性
        var gnuFileName = new String('g', 190) + ".txt";
        var gnuContent = "这是一个有很长文件名的测试文件";
        var gnuFile = _testDir.CombinePath("../test_gnu.tar");
        WriteGnuLongNameArchive(gnuFile, gnuFileName, gnuContent);

        using (var tar = new TarFile(gnuFile))
        {
            Assert.Single(tar.Entries);

            var entry = tar.Entries.First();
            Assert.Equal(gnuFileName, entry.FileName);
            Assert.Equal(gnuContent, entry.Open().ReadBytes(entry.FileSize).ToStr());
        }

        // 我写我读:归档 190 字符长文件名后解包
        var longFileName = new String('a', 190) + ".txt";
        var longFilePath = Path.Combine(_testDir, longFileName);

        // 创建带有长文件名的测试文件
        CreateTestFile(longFilePath, "这是一个有很长文件名的测试文件");

        if (File.Exists(_tarFile)) File.Delete(_tarFile);
        using var archiver = new TarFile(_tarFile, true);
        archiver.CreateFromDirectory(_testDir);
        archiver.Dispose();

        // 解压并验证长文件名被正确处理
        using var extractArchiver = new TarFile(_tarFile);
        extractArchiver.ExtractToDirectory(_outputDir);

        Assert.True(File.Exists(Path.Combine(_outputDir, longFileName)));
        Assert.Contains(longFileName, extractArchiver.Entries.Select(e => e.FileName).ToList());
    }

    /// <summary>手工构造 GNU 格式长文件名归档:长名元数据条目(类型 'L')后跟真实文件头与内容。</summary>
    /// <param name="path">归档文件路径</param>
    /// <param name="fileName">超过 100 字节的长文件名</param>
    /// <param name="content">文件内容</param>
    private static void WriteGnuLongNameArchive(String path, String fileName, String content)
    {
        var name = Encoding.ASCII.GetBytes(fileName);
        var data = Encoding.UTF8.GetBytes(content);

        using var fs = File.Create(path);

        // 长名元数据条目,名称固定为 ././@LongLink,数据区存放真实文件名
        WriteGnuHeader(fs, "././@LongLink", (Byte)'L', name.Length);
        fs.Write(name, 0, name.Length);
        WriteGnuPadding(fs, name.Length);

        // 真实文件头的名称被截断,读取方以元数据条目的长名为准
        WriteGnuHeader(fs, fileName[..100], (Byte)'0', data.Length);
        fs.Write(data, 0, data.Length);
        WriteGnuPadding(fs, data.Length);

        // 两个空块作为归档结束标志
        fs.Write(new Byte[1024], 0, 1024);
    }

    private static void WriteGnuPadding(Stream stream, Int32 length)
    {
        var padding = (512 - length % 512) % 512;
        if (padding > 0) stream.Write(new Byte[padding], 0, padding);
    }

    private static void WriteGnuHeader(Stream stream, String name, Byte type, Int32 size)
    {
        var header = new Byte[512];
        Encoding.ASCII.GetBytes(name, 0, name.Length, header, 0);
        Encoding.ASCII.GetBytes("0000644\0", 0, 8, header, 100);
        Encoding.ASCII.GetBytes("0000000\0", 0, 8, header, 108);
        Encoding.ASCII.GetBytes("0000000\0", 0, 8, header, 116);
        WriteGnuOctal(header, 124, 12, size);
        WriteGnuOctal(header, 136, 12, 0);
        Array.Fill(header, (Byte)' ', 148, 8); // 校验和区域先填空格再累加
        header[156] = type;
        Encoding.ASCII.GetBytes("ustar\0", 0, 6, header, 257);
        Encoding.ASCII.GetBytes("00", 0, 2, header, 263);

        // 校验和:按空格参与累加,写入 6 位八进制 + NUL + 空格
        Int64 sum = 0;
        foreach (var item in header) sum += item;
        WriteGnuOctal(header, 148, 7, sum);
        header[155] = (Byte)' ';

        stream.Write(header, 0, 512);
    }

    private static void WriteGnuOctal(Byte[] header, Int32 offset, Int32 length, Int64 value)
    {
        // 八进制数字右对齐,前置零填充,末字节留 NUL
        for (var i = offset + length - 2; i >= offset; i--)
        {
            header[i] = (Byte)('0' + (value & 0x07));
            value >>= 3;
        }
        header[offset + length - 1] = 0;
    }

    [Fact]
    public void TestFiles()
    {
        var di = "../data".AsDirectory();
        if (!di.Exists) return;

        // 遍历当前目录下的所有tar.gz文件,逐个解析文件头
        foreach (var fi in di.GetFiles("*.tar.gz"))
        {
            XTrace.WriteLine("解析Tar文件:{0}", fi.Name);

            using var tar = new TarFile(fi.FullName);

            var entries = tar.Entries;

            Assert.NotEmpty(entries);

            foreach (var entry in entries)
            {
                Assert.NotEmpty(entry.FileName);
                Assert.True(entry.FileSize > 0);

                XTrace.WriteLine("\t{0,32}\t{1,10:n0}byte", entry.FileName, entry.FileSize);
            }
        }
    }
}