diff --git a/Archiver/.gitignore b/Archiver/.gitignore new file mode 100644 index 0000000..1746e32 --- /dev/null +++ b/Archiver/.gitignore @@ -0,0 +1,2 @@ +bin +obj diff --git a/Archiver/Archiver.csproj b/Archiver/Archiver.csproj new file mode 100644 index 0000000..bb3fa24 --- /dev/null +++ b/Archiver/Archiver.csproj @@ -0,0 +1,18 @@ + + + + + + + + + + + + Exe + net8.0 + enable + enable + + + diff --git a/Archiver/Archiver.sln b/Archiver/Archiver.sln new file mode 100644 index 0000000..7433994 --- /dev/null +++ b/Archiver/Archiver.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Archiver", "Archiver.csproj", "{EF1E4119-D2B7-420D-9D4B-3EBC40FCE68C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EF1E4119-D2B7-420D-9D4B-3EBC40FCE68C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF1E4119-D2B7-420D-9D4B-3EBC40FCE68C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF1E4119-D2B7-420D-9D4B-3EBC40FCE68C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF1E4119-D2B7-420D-9D4B-3EBC40FCE68C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {92B64C8C-A729-4720-8CCF-F1A1BE5FB4D1} + EndGlobalSection +EndGlobal diff --git a/Archiver/Lib/ArchiveDecoder.cs b/Archiver/Lib/ArchiveDecoder.cs new file mode 100644 index 0000000..9d08379 --- /dev/null +++ b/Archiver/Lib/ArchiveDecoder.cs @@ -0,0 +1,85 @@ +using PacketIO; + +namespace Archiver.Lib; + +public static class ArchiveDecoder +{ + public static async ValueTask Decode(string inputPath, string outputPath) + { + if (!File.Exists(inputPath)) + { + throw new FileNotFoundException($"{inputPath} does not exist", inputPath); + } + + FileAttributes attributes = File.GetAttributes(inputPath); + if (attributes.HasFlag(FileAttributes.Directory)) + { + throw new IOException("Target does not file."); + } + + using (FileStream fs = new FileStream(inputPath, FileMode.Open)) + { + byte[] buffer = new byte[ArchiveEncoder.CreateFileHeader().Length]; + await fs.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + if (!buffer.SequenceEqual(ArchiveEncoder.CreateFileHeader())) + { + throw new IOException("Not supported file type."); + } + + await ReadFile(fs, outputPath).ConfigureAwait(false); + } + } + + private static async ValueTask ReadFile(Stream stream, string outputPath) + { + IPacketFile? file = await TreePacketFile.ReadTreeFile(stream).ConfigureAwait(false); + if (file is null) + { + return; + } + + await OutputFile(file, outputPath).ConfigureAwait(false); + await ReadFile(stream, outputPath).ConfigureAwait(false); + } + + private static async ValueTask OutputFile(IPacketFile file, string outputPath) + { + switch (file.GetFileType()) + { + case PacketFileType.Directory: + { + string dirPath = Path.Combine(outputPath, file.GetFileName()); + if (!Directory.Exists(dirPath)) + { + Directory.CreateDirectory(dirPath); + } + + foreach (IPacketFile inFile in file.EnumerableFiles()) + { + await OutputFile(inFile, dirPath).ConfigureAwait(false); + } + + break; + } + case PacketFileType.File: + { + string filePath = Path.Combine(outputPath, file.GetFileName()); + using (FileStream outputStream = new FileStream(filePath, FileMode.OpenOrCreate)) + { + PacketFile pfile = (PacketFile) file; + byte[] buffer = new byte[8192]; + int c; + do + { + c = await pfile.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + await outputStream.WriteAsync(buffer, 0, c).ConfigureAwait(false); + } while (c > 0); + } + + break; + } + } + + file.Dispose(); + } +} diff --git a/Archiver/Lib/ArchiveEncoder.cs b/Archiver/Lib/ArchiveEncoder.cs new file mode 100644 index 0000000..02c238e --- /dev/null +++ b/Archiver/Lib/ArchiveEncoder.cs @@ -0,0 +1,186 @@ +using PacketIO; + +namespace Archiver.Lib; + +public static class ArchiveEncoder +{ + public static async ValueTask Encode(string inputPath, string outputPath) + { + if (!Directory.Exists(inputPath)) + { + throw new FileNotFoundException($"{inputPath} does not exist", inputPath); + } + + using (FileStream fs = new FileStream(outputPath, FileMode.OpenOrCreate)) + { + byte[] buffer = CreateFileHeader(); + await fs.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + + FileAttributes attributes = File.GetAttributes(inputPath); + if (attributes.HasFlag(FileAttributes.Directory)) + { + DirectoryInfo dirInfo = new DirectoryInfo(inputPath); + PacketDirectoryModel model = await CreatePacketDirectory(dirInfo).ConfigureAwait(false); + try + { + await TreePacketFile.WriteTreeFiles(fs, model.PacketDirectory).ConfigureAwait(false); + } + finally + { + DisposeFiles(model); + } + } + else + { + FileInfo fileInfo = new FileInfo(inputPath); + PacketFileModel model = await CreatePacketFile(fileInfo).ConfigureAwait(false); + try + { + await TreePacketFile.WriteTreeFiles(fs, model.PacketFile).ConfigureAwait(false); + } + finally + { + DisposeFiles(model); + } + } + } + } + + public static async ValueTask Encode(IEnumerable inputFiles, string outputPath) + { + using (FileStream fs = new FileStream(outputPath, FileMode.OpenOrCreate)) + { + byte[] buffer = CreateFileHeader(); + await fs.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + + foreach (string inputPath in inputFiles) + { + FileAttributes attributes = File.GetAttributes(inputPath); + if (attributes.HasFlag(FileAttributes.Directory)) + { + DirectoryInfo dirInfo = new DirectoryInfo(inputPath); + PacketDirectoryModel model = await CreatePacketDirectory(dirInfo).ConfigureAwait(false); + try + { + await TreePacketFile.WriteTreeFiles(fs, model.PacketDirectory).ConfigureAwait(false); + } + finally + { + DisposeFiles(model); + } + } + else + { + FileInfo fileInfo = new FileInfo(inputPath); + PacketFileModel model = await CreatePacketFile(fileInfo).ConfigureAwait(false); + try + { + await TreePacketFile.WriteTreeFiles(fs, model.PacketFile).ConfigureAwait(false); + } + finally + { + DisposeFiles(model); + } + } + } + } + } + + public static bool IsArchiveFile(string inputPath) + { + FileAttributes attributes = File.GetAttributes(inputPath); + if (attributes.HasFlag(FileAttributes.Directory)) + { + return false; + } + + using (FileStream fs = File.OpenRead(inputPath)) + { + byte[] buffer = new byte[CreateFileHeader().Length]; + int c = fs.Read(buffer, 0, buffer.Length); + if (c <= 0) + { + return false; + } + + return buffer.SequenceEqual(CreateFileHeader()); + } + } + + internal static byte[] CreateFileHeader() + { + byte[] header = new byte[] + { + 0xCC, 0xDE, 0xFF, 0x00, + }; + return header; + } + + private static void DisposeFiles(PacketDirectoryModel dirModel) + { + dirModel.PacketDirectory.Dispose(); + foreach (PacketDirectoryModel subDirModel in dirModel.Directories) + { + DisposeFiles(subDirModel); + } + + foreach (PacketFileModel fileModel in dirModel.Files) + { + DisposeFiles(fileModel); + } + } + + private static void DisposeFiles(PacketFileModel fileModel) + { + fileModel.PacketFile.Dispose(); + fileModel.TempFile.Dispose(); + } + + private static async ValueTask CreatePacketDirectory(DirectoryInfo dirInfo) + { + PacketDirectory packetDirectory = new PacketDirectory(dirInfo.Name); + PacketDirectoryModel dirModel = new PacketDirectoryModel() + { + PacketDirectory = packetDirectory, + Files = new List(), + }; + + foreach (FileInfo fileInfo in dirInfo.EnumerateFiles()) + { + PacketFileModel model = await CreatePacketFile(fileInfo).ConfigureAwait(false); + dirModel.Files.Add(model); + packetDirectory.AddFile(model.PacketFile); + } + + foreach (DirectoryInfo subInfo in dirInfo.EnumerateDirectories()) + { + PacketDirectoryModel subDirModel = await CreatePacketDirectory(subInfo).ConfigureAwait(false); + dirModel.Directories.Add(subDirModel); + packetDirectory.AddFile(subDirModel.PacketDirectory); + } + + return dirModel; + } + + private static async ValueTask CreatePacketFile(FileInfo fileInfo) + { + TempFile tempFile = new TempFile(); + Stream outputStream = tempFile.Open(FileMode.Open); + + using (FileStream sourceStream = fileInfo.OpenRead()) + { + await sourceStream.CopyToAsync(outputStream).ConfigureAwait(false); + } + + outputStream.Seek(0, SeekOrigin.Begin); + PacketFile file = new PacketFile(fileInfo.Name, outputStream); + PacketFileModel model = new PacketFileModel() + { + TempFile = tempFile, + PacketFile = file, + }; + + return model; + } +} + diff --git a/Archiver/Lib/PacketDirectoryModel.cs b/Archiver/Lib/PacketDirectoryModel.cs new file mode 100644 index 0000000..fb32c64 --- /dev/null +++ b/Archiver/Lib/PacketDirectoryModel.cs @@ -0,0 +1,10 @@ +using PacketIO; + +namespace Archiver.Lib; + +public class PacketDirectoryModel +{ + public PacketDirectory PacketDirectory { get; set; } + public List Files { get; set; } = new List(); + public List Directories { get; set; } = new List(); +} diff --git a/Archiver/Lib/PacketFileModel.cs b/Archiver/Lib/PacketFileModel.cs new file mode 100644 index 0000000..8351dd7 --- /dev/null +++ b/Archiver/Lib/PacketFileModel.cs @@ -0,0 +1,9 @@ +using PacketIO; + +namespace Archiver.Lib; + +public class PacketFileModel +{ + public TempFile TempFile { get; set;} + public PacketFile PacketFile { get; set; } +} diff --git a/Archiver/Lib/TempFile.cs b/Archiver/Lib/TempFile.cs new file mode 100644 index 0000000..67da443 --- /dev/null +++ b/Archiver/Lib/TempFile.cs @@ -0,0 +1,21 @@ +namespace Archiver.Lib; + +public class TempFile : IDisposable +{ + public string FileName { get; } + + public TempFile() + { + FileName = Path.GetTempFileName(); + } + + public Stream Open(FileMode mode) + { + return new FileStream(FileName, mode); + } + + public void Dispose() + { + File.Delete(FileName); + } +} diff --git a/Archiver/Options.cs b/Archiver/Options.cs new file mode 100644 index 0000000..85def64 --- /dev/null +++ b/Archiver/Options.cs @@ -0,0 +1,12 @@ +using CommandLine; + +namespace Archiver; + +public class Options +{ + [Option('i', "input", Required = true)] + public IEnumerable InputFiles { get; set; } + + [Option('o', "output", Required = true)] + public string OutputPath { get; set; } +} diff --git a/Archiver/Program.cs b/Archiver/Program.cs new file mode 100644 index 0000000..1974abd --- /dev/null +++ b/Archiver/Program.cs @@ -0,0 +1,56 @@ +using Archiver.Lib; + +namespace Archiver; + +public class Program +{ + public static void Main(string[] args) + { + try + { + Options options = CommandLine.Parser.Default.ParseArguments(args).Value; + if (options is null || options.InputFiles is null || string.IsNullOrEmpty(options.OutputPath)) + { + Console.WriteLine("[Archiver] Usage: archiver -i -o "); + return; + } + + int c = options.InputFiles.Count(); + if (c == 0) + { + Console.WriteLine("[Archiver] Empty input files."); + return; + } + + if (c > 1) + { + Console.WriteLine($"[Archiver] Encode"); + Console.WriteLine($"[Archiver] Input : {string.Join(", ", options.InputFiles)}"); + Console.WriteLine($"[Archiver] Output: {options.OutputPath}"); + ArchiveEncoder.Encode(options.InputFiles, options.OutputPath).ConfigureAwait(false).GetAwaiter().GetResult(); + } + else + { + string inputFile = options.InputFiles.ToArray()[0]; + if (ArchiveEncoder.IsArchiveFile(inputFile)) + { + Console.WriteLine($"[Archiver] Decode"); + Console.WriteLine($"[Archiver] Input : {inputFile}"); + Console.WriteLine($"[Archiver] Output: {options.OutputPath}"); + ArchiveDecoder.Decode(inputFile, options.OutputPath).ConfigureAwait(false).GetAwaiter().GetResult(); + } + else + { + Console.WriteLine($"[Archiver] Encode"); + Console.WriteLine($"[Archiver] Input : {string.Join(',', options.InputFiles)}"); + Console.WriteLine($"[Archiver] Output: {options.OutputPath}"); + ArchiveEncoder.Encode(inputFile, options.OutputPath).ConfigureAwait(false).GetAwaiter().GetResult(); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[Archiver] Error: {ex.Message}"); + } + } +}