我正在寻找一种将嵌入式资源添加到我的解决方案中的方法。这些资源将是包含大量文件的文件夹。根据用户需求,需要对其进行解压缩。
我正在寻找一种将此类文件夹存储在可执行文件中而不涉及第三方库的方法(看起来相当愚蠢,但这就是任务)。
我发现我可以使用标准库对它们进行 GZip 和 UnGZip。但 GZip 只处理单个文件。在这种情况下,TAR 应该赶到现场。但我还没有在标准类中找到 TAR 实现。
也许可以用纯 C# 解压 TAR?
在寻找同一问题的快速答案时,我遇到了这个线程,并且对当前的答案并不完全满意,因为它们都指向使用第三方依赖关系到更大的库,所有这些都只是为了实现简单的提取将
tar.gz
文件保存到磁盘。
虽然
gz
格式可能被认为相当复杂,但 tar
另一方面却相当简单。从本质上讲,它只需要一堆文件,在每个描述文件的前面添加一个 500 字节的标头(但需要 512 字节),然后将它们全部写入 512 字节对齐的单个存档中。没有压缩,通常通过将创建的文件压缩到 gz
存档来处理,.NET 方便地内置了该存档,它可以处理所有困难的部分。
查看了 tar
格式的
spec后,我们需要从标头中挑选出真正的 2 个值(尤其是在 Windows 上),以便从流中提取文件。第一个是
name
,第二个是size
。使用这两个值,我们只需要查找流中的适当位置并将字节复制到文件中。
我做了一个非常基本、简陋的方法来将
tar
存档提取到目录中,并添加了一些辅助函数,用于从流或文件名中打开,并首先使用内置函数解压缩 gz
文件。
主要方法是这样的:
public static void ExtractTar(Stream stream, string outputDir)
{
var buffer = new byte[100];
while (true)
{
stream.Read(buffer, 0, 100);
var name = Encoding.ASCII.GetString(buffer).Trim('\0');
if (String.IsNullOrWhiteSpace(name))
break;
stream.Seek(24, SeekOrigin.Current);
stream.Read(buffer, 0, 12);
var size = Convert.ToInt64(Encoding.ASCII.GetString(buffer, 0, 12).Trim(), 8);
stream.Seek(376L, SeekOrigin.Current);
var output = Path.Combine(outputDir, name);
if (!Directory.Exists(Path.GetDirectoryName(output)))
Directory.CreateDirectory(Path.GetDirectoryName(output));
using (var str = File.Open(output, FileMode.OpenOrCreate, FileAccess.Write))
{
var buf = new byte[size];
stream.Read(buf, 0, buf.Length);
str.Write(buf, 0, buf.Length);
}
var pos = stream.Position;
var offset = 512 - (pos % 512);
if (offset == 512)
offset = 0;
stream.Seek(offset, SeekOrigin.Current);
}
}
这里有一些辅助函数,用于从文件中打开,并在提取之前自动首先解压缩
tar.gz
文件/流。
public static void ExtractTarGz(string filename, string outputDir)
{
using (var stream = File.OpenRead(filename))
ExtractTarGz(stream, outputDir);
}
public static void ExtractTarGz(Stream stream, string outputDir)
{
// A GZipStream is not seekable, so copy it first to a MemoryStream
using (var gzip = new GZipStream(stream, CompressionMode.Decompress))
{
const int chunk = 4096;
using (var memStr = new MemoryStream())
{
int read;
var buffer = new byte[chunk];
do
{
read = gzip.Read(buffer, 0, chunk);
memStr.Write(buffer, 0, read);
} while (read == chunk);
memStr.Seek(0, SeekOrigin.Begin);
ExtractTar(memStr, outputDir);
}
}
}
public static void ExtractTar(string filename, string outputDir)
{
using (var stream = File.OpenRead(filename))
ExtractTar(stream, outputDir);
}
这是完整文件的要点以及一些评论。
Tar-cs 可以完成这项工作,但速度相当慢。我建议使用SharpCompress,它的速度要快得多。它还支持其他压缩类型,并且最近已更新。
using System;
using System.IO;
using SharpCompress.Common;
using SharpCompress.Reader;
private static String directoryPath = @"C:\Temp";
public static void unTAR(String tarFilePath)
{
using (Stream stream = File.OpenRead(tarFilePath))
{
var reader = ReaderFactory.Open(stream);
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
ExtractionOptions opt = new ExtractionOptions {
ExtractFullPath = true,
Overwrite = true
};
reader.WriteEntryToDirectory(directoryPath, opt);
}
}
}
}
参见 tar-cs
using (FileStream unarchFile = File.OpenRead(tarfile))
{
TarReader reader = new TarReader(unarchFile);
reader.ReadToEnd("out_dir");
}
.NET 7 添加了多个类来处理 TAR 文件:
解压到目录:
await TarFile.ExtractToDirectoryAsync(tarFilePath, outputDir);
枚举 TAR 文件并手动提取其条目:
await using var tarStream = new FileStream(tarFilePath, new FileStreamOptions { Mode = FileMode.Open, Access = FileAccess.Read, Options = FileOptions.Asynchronous });
await using var tarReader = new TarReader(tarStream);
TarEntry entry;
while ((entry = await tarReader.GetNextEntryAsync()) != null)
{
if (entry.EntryType is TarEntryType.SymbolicLink or TarEntryType.HardLink or TarEntryType.GlobalExtendedAttributes)
{
continue;
}
Console.WriteLine($"Extracting {entry.Name}");
await entry.ExtractToFileAsync(Path.Join(outputDirectory, entry.Name));
}
由于您不允许使用外部库,因此您也不受
tar
文件的特定格式的限制。事实上,他们甚至不需要将它们全部放在同一个文件中。
您可以用 C# 编写自己的类似 tar 的实用程序,它遍历目录树,并生成两个文件:一个“头”文件,其中包含将
System.IO.Path
实例映射到偏移/长度对的序列化字典,以及一个大文件包含连接成一个巨大 blob 的各个文件的内容。这不是一项微不足道的任务,但也不是太复杂。
在.NET中有两种压缩/解压缩的方法,首先你可以使用Gzipstream类和DeflatStream实际上都可以以.gz格式压缩你的文件,所以如果你在Gzipstream中压缩任何文件,它可以用任何流行的压缩应用程序打开,例如winzip/ winrar、7zip 但无法使用 DeflatStream 打开压缩文件。这两个类来自 .NET 2。
还有另一种方法,即 Package 类,它实际上与 Gzipstream 和 DeflatStream 相同,唯一的区别是您可以压缩多个文件,然后可以使用 winzip/ winrar、7zip.so 打开这些文件,这就是 .NET 的全部。但它甚至不是通用的 .zip 文件, Microsoft 使用它来压缩其 *x 扩展名的 Office 文件。如果您解压任何带有包类的 docx 文件,您可以看到其中存储的所有内容。因此,不要使用 .NET 库进行压缩甚至解压缩,因为您甚至无法制作通用压缩文件,甚至无法解压缩通用 zip 文件。你必须考虑第三方库,例如 http://www.icsharpcode.net/OpenSource/SharpZipLib/
或者从底层开始实施一切。
基于 ForeverZer0 的答案。修复了一些问题。它通过避免流复制而使用显着更少的内存,并处理更大的存档和更长的文件名(前缀标记)。这仍然不能处理 100% 的 USTAR tar 规范。
public static void ExtractTarGz(string filename, string outputDir)
{
void ReadExactly(Stream stream, byte[] buffer, int count)
{
var total = 0;
while (true)
{
int n = stream.Read(buffer, total, count - total);
total += n;
if (total == count)
return;
}
}
var SeekExactly = ReadExactly;
using (var fs = File.OpenRead(filename))
{
using (var stream = new GZipStream(fs, CompressionMode.Decompress))
{
var buffer = new byte[1024];
while (true)
{
ReadExactly(stream, buffer, 100);
var name = Encoding.ASCII.GetString(buffer, 0, 100).Split('\0')[0];
if (String.IsNullOrWhiteSpace(name))
break;
SeekExactly(stream, buffer, 24);
ReadExactly(stream, buffer, 12);
var sizeString = Encoding.ASCII.GetString(buffer, 0, 12).Split('\0')[0];
var size = Convert.ToInt64(sizeString, 8);
SeekExactly(stream, buffer, 209);
ReadExactly(stream, buffer, 155);
var prefix = Encoding.ASCII.GetString(buffer, 0, 155).Split('\0')[0];
if (!String.IsNullOrWhiteSpace(prefix))
{
name = prefix + name;
}
SeekExactly(stream, buffer, 12);
var output = Path.GetFullPath(Path.Combine(outputDir, name));
if (!Directory.Exists(Path.GetDirectoryName(output)))
{
Directory.CreateDirectory(Path.GetDirectoryName(output)!);
}
using (var outfs = File.Open(output, FileMode.OpenOrCreate, FileAccess.Write))
{
var total = 0;
var next = 0;
while (true)
{
next = Math.Min(buffer.Length, (int)size - total);
ReadExactly(stream, buffer, next);
outfs.Write(buffer, 0, next);
total += next;
if (total == size)
break;
}
}
var offset = 512 - ((int)size % 512);
if (offset == 512)
offset = 0;
SeekExactly(stream, buffer, offset);
}
}
}
}