330 lines
11 KiB
C#
330 lines
11 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using AMWD.Common.Packing.Tar.Interfaces;
|
|
|
|
namespace AMWD.Common.Packing.Tar.Utils
|
|
{
|
|
/// <summary>
|
|
/// Implements a legacy TAR writer.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Writes tar (see GNU tar) archive to a stream
|
|
/// <br/>
|
|
/// Copied from: <see href="https://github.com/ygoe/DotnetMakeDeb/blob/v1.1.0/DotnetMakeDeb/Tar/LegacyTarWriter.cs">DotnetMakeDeb</see>
|
|
/// </remarks>
|
|
/// <param name="outStream">stream to write archive to</param>
|
|
public class LegacyTarWriter(Stream outStream) : IDisposable
|
|
{
|
|
private readonly Stream _outStream = outStream;
|
|
private bool _isClosed;
|
|
|
|
/// <summary>
|
|
/// The buffer for writing.
|
|
/// </summary>
|
|
protected byte[] buffer = new byte[1024];
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether to read on zero.
|
|
/// </summary>
|
|
public bool ReadOnZero { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Gets the output stream.
|
|
/// </summary>
|
|
protected virtual Stream OutStream
|
|
{
|
|
get { return _outStream; }
|
|
}
|
|
|
|
#region IDisposable Members
|
|
|
|
/// <inheritdoc/>
|
|
public void Dispose()
|
|
=> Close();
|
|
|
|
#endregion IDisposable Members
|
|
|
|
/// <summary>
|
|
/// Writes a directory entry.
|
|
/// </summary>
|
|
/// <param name="path">The path to the directory.</param>
|
|
/// <param name="userId">The user id.</param>
|
|
/// <param name="groupId">The group id.</param>
|
|
/// <param name="mode">The access mode.</param>
|
|
/// <exception cref="ArgumentNullException"><paramref name="path"/> is not set.</exception>
|
|
public void WriteDirectoryEntry(string path, int userId, int groupId, int mode)
|
|
{
|
|
if (string.IsNullOrEmpty(path))
|
|
throw new ArgumentNullException(nameof(path), "The path is not set.");
|
|
if (path[path.Length - 1] != '/')
|
|
path += '/';
|
|
|
|
DateTime lastWriteTime;
|
|
if (Directory.Exists(path))
|
|
{
|
|
lastWriteTime = Directory.GetLastWriteTime(path);
|
|
}
|
|
else
|
|
{
|
|
lastWriteTime = DateTime.Now;
|
|
}
|
|
|
|
// handle long path names (> 99 characters)
|
|
if (path.Length > 99)
|
|
{
|
|
WriteLongName(
|
|
name: path,
|
|
userId: userId,
|
|
groupId: groupId,
|
|
mode: mode,
|
|
lastModificationTime: lastWriteTime);
|
|
|
|
// shorten the path name so it can be written properly
|
|
path = path.Substring(0, 99);
|
|
}
|
|
|
|
WriteHeader(path, lastWriteTime, 0, userId, groupId, mode, EntryType.Directory);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a directory and its contents.
|
|
/// </summary>
|
|
/// <param name="directory">The directory.</param>
|
|
/// <param name="doRecursive">Write also sub-directories.</param>
|
|
/// <exception cref="ArgumentNullException"><paramref name="directory"/> is not set.</exception>
|
|
public void WriteDirectory(string directory, bool doRecursive)
|
|
{
|
|
if (string.IsNullOrEmpty(directory))
|
|
throw new ArgumentNullException(nameof(directory), "The directory is not set.");
|
|
|
|
WriteDirectoryEntry(directory, 0, 0, 0755);
|
|
|
|
string[] files = Directory.GetFiles(directory);
|
|
foreach (string fileName in files)
|
|
Write(fileName);
|
|
|
|
string[] directories = Directory.GetDirectories(directory);
|
|
foreach (string dirName in directories)
|
|
{
|
|
WriteDirectoryEntry(dirName, 0, 0, 0755);
|
|
if (doRecursive)
|
|
WriteDirectory(dirName, true);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a file.
|
|
/// </summary>
|
|
/// <param name="fileName">The file.</param>
|
|
/// <exception cref="ArgumentNullException"><paramref name="fileName"/> is not set.</exception>
|
|
public void Write(string fileName)
|
|
{
|
|
if (string.IsNullOrEmpty(fileName))
|
|
throw new ArgumentNullException(nameof(fileName), "The file name is not set.");
|
|
|
|
using var fileStream = File.OpenRead(fileName);
|
|
Write(fileStream, fileStream.Length, fileName, 61, 61, 511, File.GetLastWriteTime(fileStream.Name));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a file stream.
|
|
/// </summary>
|
|
/// <param name="file">The file stream.</param>
|
|
public void Write(FileStream file)
|
|
{
|
|
string path = Path.GetFullPath(file.Name).Replace(Path.GetPathRoot(file.Name), string.Empty);
|
|
path = path.Replace(Path.DirectorySeparatorChar, '/');
|
|
Write(file, file.Length, path, 61, 61, 511, File.GetLastWriteTime(file.Name));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a stream.
|
|
/// </summary>
|
|
/// <param name="data">The contents.</param>
|
|
/// <param name="dataSizeInBytes">The file size in bytes.</param>
|
|
/// <param name="name">The file name.</param>
|
|
public void Write(Stream data, long dataSizeInBytes, string name)
|
|
=> Write(data, dataSizeInBytes, name, 61, 61, 511, DateTime.Now);
|
|
|
|
/// <summary>
|
|
/// Writes a file to the archive.
|
|
/// </summary>
|
|
/// <param name="name">The file name.</param>
|
|
/// <param name="dataSizeInBytes">The file size in bytes.</param>
|
|
/// <param name="userId">The user id.</param>
|
|
/// <param name="groupId">The group id.</param>
|
|
/// <param name="mode">The access mode.</param>
|
|
/// <param name="lastModificationTime">The last modification timestamp.</param>
|
|
/// <param name="writeDelegate">The <see cref="WriteDataDelegate"/>.</param>
|
|
public virtual void Write(string name, long dataSizeInBytes, int userId, int groupId, int mode, DateTime lastModificationTime, WriteDataDelegate writeDelegate)
|
|
{
|
|
var writer = new DataWriter(OutStream, dataSizeInBytes);
|
|
|
|
WriteHeader(name, lastModificationTime, dataSizeInBytes, userId, groupId, mode, EntryType.File);
|
|
|
|
while (writer.CanWrite)
|
|
writeDelegate(writer);
|
|
|
|
AlignTo512(dataSizeInBytes, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a stream as file to the archive.
|
|
/// </summary>
|
|
/// <param name="data">The content as <see cref="Stream"/>.</param>
|
|
/// <param name="dataSizeInBytes">The file size in bytes.</param>
|
|
/// <param name="name">The file name.</param>
|
|
/// <param name="userId">The user id.</param>
|
|
/// <param name="groupId">The group id.</param>
|
|
/// <param name="mode">The access mode.</param>
|
|
/// <param name="lastModificationTime">The last modification timestamp.</param>
|
|
/// <exception cref="TarException">This writer is already closed.</exception>
|
|
public virtual void Write(Stream data, long dataSizeInBytes, string name, int userId, int groupId, int mode, DateTime lastModificationTime)
|
|
{
|
|
if (_isClosed)
|
|
throw new TarException("Can not write to the closed writer");
|
|
|
|
// handle long file names (> 99 characters)
|
|
if (name.Length > 99)
|
|
{
|
|
WriteLongName(
|
|
name: name,
|
|
userId: userId,
|
|
groupId: groupId,
|
|
mode: mode,
|
|
lastModificationTime: lastModificationTime);
|
|
|
|
// shorten the file name so it can be written properly
|
|
name = name.Substring(0, 99);
|
|
}
|
|
|
|
WriteHeader(name, lastModificationTime, dataSizeInBytes, userId, groupId, mode, EntryType.File);
|
|
WriteContent(dataSizeInBytes, data);
|
|
AlignTo512(dataSizeInBytes, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle long file or path names (> 99 characters).
|
|
/// Write header and content, which its content contain the long (complete) file/path name.
|
|
/// <para>This handling method is adapted from https://github.com/qmfrederik/dotnet-packaging/pull/50/files#diff-f64c58cc18e8e445cee6ffed7a0d765cdb442c0ef21a3ed80bd20514057967b1 </para>
|
|
/// </summary>
|
|
/// <param name="name">File name or path name.</param>
|
|
/// <param name="userId">User ID.</param>
|
|
/// <param name="groupId">Group ID.</param>
|
|
/// <param name="mode">Mode.</param>
|
|
/// <param name="lastModificationTime">Last modification time.</param>
|
|
private void WriteLongName(string name, int userId, int groupId, int mode, DateTime lastModificationTime)
|
|
{
|
|
// must include a trailing \0
|
|
int nameLength = Encoding.UTF8.GetByteCount(name);
|
|
byte[] entryName = new byte[nameLength + 1];
|
|
|
|
Encoding.UTF8.GetBytes(name, 0, name.Length, entryName, 0);
|
|
|
|
// add a "././@LongLink" pseudo-entry which contains the full name
|
|
using var nameStream = new MemoryStream(entryName);
|
|
WriteHeader("././@LongLink", lastModificationTime, entryName.Length, userId, groupId, mode, EntryType.LongName);
|
|
WriteContent(entryName.Length, nameStream);
|
|
AlignTo512(entryName.Length, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a stream as file to the archive.
|
|
/// </summary>
|
|
/// <param name="count">The size of the file in bytes.</param>
|
|
/// <param name="data">The file content as stream.</param>
|
|
/// <exception cref="IOException"><paramref name="data"/> has not enough to read from.</exception>
|
|
protected void WriteContent(long count, Stream data)
|
|
{
|
|
while (count > 0 && count > buffer.Length)
|
|
{
|
|
int bytesRead = data.Read(buffer, 0, buffer.Length);
|
|
if (bytesRead < 0)
|
|
throw new IOException($"{nameof(LegacyTarWriter)} unable to read from provided stream");
|
|
|
|
if (bytesRead == 0)
|
|
{
|
|
if (ReadOnZero)
|
|
Thread.Sleep(100);
|
|
else
|
|
break;
|
|
}
|
|
OutStream.Write(buffer, 0, bytesRead);
|
|
count -= bytesRead;
|
|
}
|
|
if (count > 0)
|
|
{
|
|
int bytesRead = data.Read(buffer, 0, (int)count);
|
|
if (bytesRead < 0)
|
|
throw new IOException($"{nameof(LegacyTarWriter)} unable to read from provided stream");
|
|
|
|
if (bytesRead == 0)
|
|
{
|
|
while (count > 0)
|
|
{
|
|
OutStream.WriteByte(0);
|
|
--count;
|
|
}
|
|
}
|
|
else
|
|
OutStream.Write(buffer, 0, bytesRead);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a entry header to the archive.
|
|
/// </summary>
|
|
/// <param name="name">The file name.</param>
|
|
/// <param name="lastModificationTime">The last modification time.</param>
|
|
/// <param name="count">The number of bytes.</param>
|
|
/// <param name="userId">The user id.</param>
|
|
/// <param name="groupId">The group id.</param>
|
|
/// <param name="mode">The file mode.</param>
|
|
/// <param name="entryType">The entry type</param>
|
|
protected virtual void WriteHeader(string name, DateTime lastModificationTime, long count, int userId, int groupId, int mode, EntryType entryType)
|
|
{
|
|
var header = new TarHeader
|
|
{
|
|
FileName = name,
|
|
LastModification = lastModificationTime,
|
|
SizeInBytes = count,
|
|
UserId = userId,
|
|
GroupId = groupId,
|
|
Mode = mode,
|
|
EntryType = entryType
|
|
};
|
|
OutStream.Write(header.GetHeaderValue(), 0, header.HeaderSize);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Aligns the entry to 512 bytes.
|
|
/// </summary>
|
|
public void AlignTo512(long size, bool acceptZero)
|
|
{
|
|
size %= 512;
|
|
if (size == 0 && !acceptZero) return;
|
|
while (size < 512)
|
|
{
|
|
OutStream.WriteByte(0);
|
|
size++;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Closes the writer and aligns to 512 bytes.
|
|
/// </summary>
|
|
public virtual void Close()
|
|
{
|
|
if (_isClosed)
|
|
return;
|
|
|
|
AlignTo512(0, true);
|
|
AlignTo512(0, true);
|
|
|
|
_isClosed = true;
|
|
}
|
|
}
|
|
}
|