Compare commits
No commits in common. "dev_1.0.0" and "main" have entirely different histories.
2
Archiver/.gitignore
vendored
2
Archiver/.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
bin
|
|
||||||
obj
|
|
@ -1,18 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\ProtocolLib\ProtocolLib.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@ -1,25 +0,0 @@
|
|||||||
|
|
||||||
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
|
|
@ -1,85 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,186 +0,0 @@
|
|||||||
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<string> 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<PacketDirectoryModel> CreatePacketDirectory(DirectoryInfo dirInfo)
|
|
||||||
{
|
|
||||||
PacketDirectory packetDirectory = new PacketDirectory(dirInfo.Name);
|
|
||||||
PacketDirectoryModel dirModel = new PacketDirectoryModel()
|
|
||||||
{
|
|
||||||
PacketDirectory = packetDirectory,
|
|
||||||
Files = new List<PacketFileModel>(),
|
|
||||||
};
|
|
||||||
|
|
||||||
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<PacketFileModel> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
|||||||
using PacketIO;
|
|
||||||
|
|
||||||
namespace Archiver.Lib;
|
|
||||||
|
|
||||||
public class PacketDirectoryModel
|
|
||||||
{
|
|
||||||
public PacketDirectory PacketDirectory { get; set; }
|
|
||||||
public List<PacketFileModel> Files { get; set; } = new List<PacketFileModel>();
|
|
||||||
public List<PacketDirectoryModel> Directories { get; set; } = new List<PacketDirectoryModel>();
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
using PacketIO;
|
|
||||||
|
|
||||||
namespace Archiver.Lib;
|
|
||||||
|
|
||||||
public class PacketFileModel
|
|
||||||
{
|
|
||||||
public TempFile TempFile { get; set;}
|
|
||||||
public PacketFile PacketFile { get; set; }
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
using CommandLine;
|
|
||||||
|
|
||||||
namespace Archiver;
|
|
||||||
|
|
||||||
public class Options
|
|
||||||
{
|
|
||||||
[Option('i', "input", Required = true)]
|
|
||||||
public IEnumerable<string> InputFiles { get; set; }
|
|
||||||
|
|
||||||
[Option('o', "output", Required = true)]
|
|
||||||
public string OutputPath { get; set; }
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
using Archiver.Lib;
|
|
||||||
|
|
||||||
namespace Archiver;
|
|
||||||
|
|
||||||
public class Program
|
|
||||||
{
|
|
||||||
public static void Main(string[] args)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Options options = CommandLine.Parser.Default.ParseArguments<Options>(args).Value;
|
|
||||||
if (options is null || options.InputFiles is null || string.IsNullOrEmpty(options.OutputPath))
|
|
||||||
{
|
|
||||||
Console.WriteLine("[Archiver] Usage: archiver -i <input files> -o <output file>");
|
|
||||||
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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
2
ProtocolLib/.gitignore
vendored
2
ProtocolLib/.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
bin
|
|
||||||
obj
|
|
@ -1,12 +0,0 @@
|
|||||||
namespace PacketIO;
|
|
||||||
|
|
||||||
public interface IPacketFile : IDisposable
|
|
||||||
{
|
|
||||||
public PacketFileType GetFileType();
|
|
||||||
|
|
||||||
public string GetFileName();
|
|
||||||
|
|
||||||
public long GetFileSize();
|
|
||||||
|
|
||||||
public IEnumerable<IPacketFile> EnumerableFiles();
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
namespace PacketIO;
|
|
||||||
|
|
||||||
public class PacketDirectory : IPacketFile
|
|
||||||
{
|
|
||||||
private string fileName;
|
|
||||||
private List<IPacketFile> files;
|
|
||||||
|
|
||||||
public PacketDirectory(string fileName, params IPacketFile[] files)
|
|
||||||
{
|
|
||||||
this.fileName = fileName;
|
|
||||||
this.files = new List<IPacketFile>();
|
|
||||||
this.files.AddRange(files);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<IPacketFile> EnumerableFiles()
|
|
||||||
{
|
|
||||||
return this.files.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetFileName()
|
|
||||||
{
|
|
||||||
return this.fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long GetFileSize()
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PacketFileType GetFileType()
|
|
||||||
{
|
|
||||||
return PacketFileType.Directory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long AddFile(IPacketFile file)
|
|
||||||
{
|
|
||||||
this.files.Add(file);
|
|
||||||
return this.files.LongCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool RemoveFile(IPacketFile file)
|
|
||||||
{
|
|
||||||
return this.files.Remove(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
foreach (IPacketFile file in this.EnumerableFiles())
|
|
||||||
{
|
|
||||||
file.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
namespace PacketIO;
|
|
||||||
|
|
||||||
public class PacketFile : IPacketFile
|
|
||||||
{
|
|
||||||
private string fileName;
|
|
||||||
private TempFile? tempFile;
|
|
||||||
private Stream stream;
|
|
||||||
|
|
||||||
public PacketFile(string fileName)
|
|
||||||
{
|
|
||||||
this.fileName = fileName;
|
|
||||||
this.tempFile = new TempFile();
|
|
||||||
this.stream = tempFile.Open(FileMode.Open);
|
|
||||||
}
|
|
||||||
|
|
||||||
public PacketFile(string fileName, Stream stream)
|
|
||||||
{
|
|
||||||
this.fileName = fileName;
|
|
||||||
this.stream = stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<IPacketFile> EnumerableFiles()
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetFileName()
|
|
||||||
{
|
|
||||||
return this.fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long GetFileSize()
|
|
||||||
{
|
|
||||||
return this.stream.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PacketFileType GetFileType()
|
|
||||||
{
|
|
||||||
return PacketFileType.File;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask<int> ReadAsync(byte[] buffer, int offset, int count)
|
|
||||||
{
|
|
||||||
int read = await this.stream.ReadAsync(buffer, offset, count).ConfigureAwait(false);
|
|
||||||
return read;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask WriteAsync(byte[] buffer, int offset, int count)
|
|
||||||
{
|
|
||||||
await this.stream.WriteAsync(buffer, offset, count).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Seek(long offset, SeekOrigin origin)
|
|
||||||
{
|
|
||||||
this.stream.Seek(offset, origin);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
this.stream.Dispose();
|
|
||||||
if (this.tempFile is object)
|
|
||||||
{
|
|
||||||
this.tempFile.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
namespace PacketIO;
|
|
||||||
|
|
||||||
[Flags]
|
|
||||||
public enum PacketFileType : byte
|
|
||||||
{
|
|
||||||
None = 0,
|
|
||||||
|
|
||||||
File = 0x02 << 0,
|
|
||||||
|
|
||||||
Directory = 0x02 << 1,
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
namespace PacketIO;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,150 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace PacketIO;
|
|
||||||
|
|
||||||
public class TreePacketFile
|
|
||||||
{
|
|
||||||
public static async ValueTask WriteTreeFiles(Stream stream, params IPacketFile[] files)
|
|
||||||
{
|
|
||||||
foreach (IPacketFile file in files)
|
|
||||||
{
|
|
||||||
await WriteTreeFile(stream, file).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async ValueTask WriteTreeFile(Stream stream, IPacketFile file)
|
|
||||||
{
|
|
||||||
byte fileType = (byte) file.GetFileType();
|
|
||||||
string fileName = file.GetFileName();
|
|
||||||
long fileSize = file.GetFileSize();
|
|
||||||
|
|
||||||
byte[] fileNameBuffer = Encoding.UTF8.GetBytes(fileName);
|
|
||||||
|
|
||||||
byte[] buffer = new byte[sizeof(byte) + sizeof(int) + sizeof(long) + fileNameBuffer.Length];
|
|
||||||
buffer[0] = fileType;
|
|
||||||
Array.Copy(BitConverter.GetBytes(fileNameBuffer.Length), 0, buffer, 1, sizeof(int));
|
|
||||||
Array.Copy(BitConverter.GetBytes(fileSize), 0, buffer, 5, sizeof(long));
|
|
||||||
Array.Copy(fileNameBuffer, 0, buffer, 13, fileNameBuffer.Length);
|
|
||||||
|
|
||||||
await stream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
|
||||||
|
|
||||||
switch (file.GetFileType())
|
|
||||||
{
|
|
||||||
case PacketFileType.Directory:
|
|
||||||
{
|
|
||||||
long fileCount = file.EnumerableFiles().LongCount();
|
|
||||||
buffer = BitConverter.GetBytes(fileCount);
|
|
||||||
await stream.WriteAsync(buffer, 0, sizeof(long));
|
|
||||||
|
|
||||||
await WriteTreeFiles(stream, file.EnumerableFiles().ToArray()).ConfigureAwait(false);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case PacketFileType.File:
|
|
||||||
{
|
|
||||||
int c;
|
|
||||||
buffer = new byte[8192];
|
|
||||||
PacketFile packetFile = (PacketFile) file;
|
|
||||||
packetFile.Seek(0, SeekOrigin.Begin);
|
|
||||||
do
|
|
||||||
{
|
|
||||||
c = await packetFile.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
|
||||||
|
|
||||||
await stream.WriteAsync(buffer, 0, c).ConfigureAwait(false);
|
|
||||||
} while (c > 0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async ValueTask<IEnumerable<IPacketFile>> ReadTreeFiles(Stream stream)
|
|
||||||
{
|
|
||||||
List<IPacketFile> files = new List<IPacketFile>();
|
|
||||||
|
|
||||||
IPacketFile? file = null;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
file = await ReadTreeFile(stream).ConfigureAwait(false);
|
|
||||||
if (file is object)
|
|
||||||
{
|
|
||||||
files.Add(file);
|
|
||||||
}
|
|
||||||
} while (file is object);
|
|
||||||
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async ValueTask<IPacketFile?> ReadTreeFile(Stream stream)
|
|
||||||
{
|
|
||||||
byte[] buffer = new byte[sizeof(byte) + sizeof(int) + sizeof(long)];
|
|
||||||
int c = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
|
||||||
if (c <= 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
PacketFileType fileType = (PacketFileType) buffer[0];
|
|
||||||
int fileNameBufferSize = BitConverter.ToInt32(buffer, 1);
|
|
||||||
long fileSize = BitConverter.ToInt64(buffer, 5);
|
|
||||||
|
|
||||||
buffer = new byte[fileNameBufferSize];
|
|
||||||
c = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
|
||||||
if (c <= 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
string fileName = Encoding.UTF8.GetString(buffer);
|
|
||||||
|
|
||||||
switch (fileType)
|
|
||||||
{
|
|
||||||
case PacketFileType.Directory:
|
|
||||||
{
|
|
||||||
buffer = new byte[sizeof(long)];
|
|
||||||
c = await stream.ReadAsync(buffer, 0, buffer.Length);
|
|
||||||
if (c <= 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
long fileCount = BitConverter.ToInt64(buffer, 0);
|
|
||||||
|
|
||||||
PacketDirectory directory = new PacketDirectory(fileName);
|
|
||||||
for (long i = 0 ; i < fileCount ; i++)
|
|
||||||
{
|
|
||||||
IPacketFile? file = await ReadTreeFile(stream).ConfigureAwait(false);
|
|
||||||
if (file is null)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
directory.AddFile(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
return directory;
|
|
||||||
}
|
|
||||||
case PacketFileType.File:
|
|
||||||
{
|
|
||||||
PacketFile file = new PacketFile(fileName);
|
|
||||||
|
|
||||||
buffer = new byte[8192];
|
|
||||||
long remainedSize = fileSize;
|
|
||||||
long readed = 0;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
int leftSize = (int) Math.Min(buffer.Length, fileSize - readed);
|
|
||||||
c = await stream.ReadAsync(buffer, 0, leftSize).ConfigureAwait(false);
|
|
||||||
await file.WriteAsync(buffer, 0, c).ConfigureAwait(false);
|
|
||||||
remainedSize -= c;
|
|
||||||
readed += c;
|
|
||||||
} while (remainedSize > 0);
|
|
||||||
|
|
||||||
file.Seek(0, SeekOrigin.Begin);
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
using PacketIO;
|
|
||||||
|
|
||||||
namespace ProtocolLib;
|
|
||||||
|
|
||||||
public class Program
|
|
||||||
{
|
|
||||||
public static void Main(string[] args)
|
|
||||||
{
|
|
||||||
// Make tree file.
|
|
||||||
using (FileStream fs = new FileStream("tree.bin", FileMode.OpenOrCreate))
|
|
||||||
{
|
|
||||||
DirectoryInfo directoryInfo = new DirectoryInfo("i");
|
|
||||||
if (directoryInfo.Exists)
|
|
||||||
{
|
|
||||||
foreach (FileInfo fileInfo in directoryInfo.GetFiles())
|
|
||||||
{
|
|
||||||
using (PacketFile file = new PacketFile(fileInfo.Name, fileInfo.Open(FileMode.Open, FileAccess.ReadWrite)))
|
|
||||||
{
|
|
||||||
TreePacketFile.WriteTreeFile(fs, file).ConfigureAwait(false).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get files from tree file.
|
|
||||||
using (FileStream fs = new FileStream("tree.bin", FileMode.OpenOrCreate))
|
|
||||||
{
|
|
||||||
IEnumerable<IPacketFile> files = TreePacketFile.ReadTreeFiles(fs).ConfigureAwait(false).GetAwaiter().GetResult();
|
|
||||||
foreach (IPacketFile ifile in files)
|
|
||||||
{
|
|
||||||
PacketFile file = (PacketFile) ifile;
|
|
||||||
using (FileStream outStream = new FileStream(file.GetFileName(), FileMode.OpenOrCreate))
|
|
||||||
{
|
|
||||||
byte[] buffer = new byte[8192];
|
|
||||||
int c;
|
|
||||||
while ((c = file.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false).GetAwaiter().GetResult()) > 0)
|
|
||||||
{
|
|
||||||
outStream.Write(buffer, 0, c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Paket test.
|
|
||||||
using (MemoryStream ms = new MemoryStream())
|
|
||||||
{
|
|
||||||
PacketStream packetStream = new PacketStream(ms);
|
|
||||||
|
|
||||||
byte[] buffer = System.Text.Encoding.UTF8.GetBytes("Hello, World!!!");
|
|
||||||
foreach (Packet packet in PacketFactory.CreatePacket(DataOperator.Connect, DataType.Binary, buffer))
|
|
||||||
{
|
|
||||||
packetStream.WritePacket(packet).ConfigureAwait(false).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
ms.Seek(0, SeekOrigin.Begin);
|
|
||||||
|
|
||||||
Packet? packet1 = null;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
packet1 = packetStream.ReadPacket().ConfigureAwait(false).GetAwaiter().GetResult();
|
|
||||||
if (packet1 is object)
|
|
||||||
{
|
|
||||||
Console.Write($"{System.Text.Encoding.UTF8.GetString(packet1.Payload)}");
|
|
||||||
}
|
|
||||||
} while (packet1 is object);
|
|
||||||
|
|
||||||
Console.WriteLine();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>Library</OutputType>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@ -1,25 +0,0 @@
|
|||||||
|
|
||||||
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}") = "ProtocolLib", "ProtocolLib.csproj", "{BB1F2133-E83C-4975-A685-D2DCE2FC24C9}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{BB1F2133-E83C-4975-A685-D2DCE2FC24C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{BB1F2133-E83C-4975-A685-D2DCE2FC24C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{BB1F2133-E83C-4975-A685-D2DCE2FC24C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{BB1F2133-E83C-4975-A685-D2DCE2FC24C9}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
|
||||||
HideSolutionNode = FALSE
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
|
||||||
SolutionGuid = {AD8CE391-A853-498C-A2E6-7A83701164D4}
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
@ -1,15 +0,0 @@
|
|||||||
namespace ProtocolLib;
|
|
||||||
|
|
||||||
internal static class ByteConverter
|
|
||||||
{
|
|
||||||
public static byte[] Convert(bool value)
|
|
||||||
{
|
|
||||||
byte[] buffer = BitConverter.GetBytes(value);
|
|
||||||
if (BitConverter.IsLittleEndian)
|
|
||||||
{
|
|
||||||
Array.Reverse(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
namespace ProtocolLib;
|
|
||||||
|
|
||||||
public enum DataOperator : byte
|
|
||||||
{
|
|
||||||
None = 0x00,
|
|
||||||
|
|
||||||
Connect = 0x02 << 0,
|
|
||||||
|
|
||||||
Request = 0x02 << 1,
|
|
||||||
|
|
||||||
Response = 0x02 << 2,
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
namespace ProtocolLib;
|
|
||||||
|
|
||||||
public enum DataType : byte
|
|
||||||
{
|
|
||||||
None = 0x00,
|
|
||||||
|
|
||||||
Binary = 0x02 << 0,
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
namespace ProtocolLib;
|
|
||||||
|
|
||||||
public static class PacketFactory
|
|
||||||
{
|
|
||||||
public static readonly int MAX_PAYLOAD_SIZE = 810;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 指定されたバイト配列からパケットを作成する。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dataOperator">オペレータの種類</param>
|
|
||||||
/// <param name="dataType">データの種類</param>
|
|
||||||
/// <param name="data">バイトデータ</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static IEnumerable<Packet> CreatePacket(DataOperator dataOperator, DataType dataType, byte[] data)
|
|
||||||
{
|
|
||||||
List<Packet> packets = new List<Packet>();
|
|
||||||
|
|
||||||
long packetNumber = 0;
|
|
||||||
int remainedSize = data.Length;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
int packSize = Math.Min(MAX_PAYLOAD_SIZE, remainedSize);
|
|
||||||
byte[] payload = data.AsSpan(data.Length - remainedSize, packSize).ToArray();
|
|
||||||
|
|
||||||
Packet packet = new Packet()
|
|
||||||
{
|
|
||||||
Operator = dataOperator,
|
|
||||||
Type = dataType,
|
|
||||||
PacketNumber = packetNumber,
|
|
||||||
PayloadSize = packSize,
|
|
||||||
Payload = payload,
|
|
||||||
};
|
|
||||||
|
|
||||||
packets.Add(packet);
|
|
||||||
remainedSize -= packSize;
|
|
||||||
} while (remainedSize > 0);
|
|
||||||
|
|
||||||
return packets;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 指定されたバイト配列からパケットを作成する。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dataOperator">オペレータの種類</param>
|
|
||||||
/// <param name="dataType">データの種類</param>
|
|
||||||
/// <param name="data">バイトデータ</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static IEnumerable<Packet> CreatePacket(DataOperator dataOperator, DataType dataType, Stream stream)
|
|
||||||
{
|
|
||||||
List<Packet> packets = new List<Packet>();
|
|
||||||
|
|
||||||
long packetNumber = 0;
|
|
||||||
long remainedSize = stream.Length;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
int packSize = (int) Math.Min(MAX_PAYLOAD_SIZE, remainedSize);
|
|
||||||
byte[] payload = new byte[packSize];
|
|
||||||
int c = stream.Read(payload, 0, payload.Length);
|
|
||||||
if (c <= 0)
|
|
||||||
{
|
|
||||||
throw new IOException();
|
|
||||||
}
|
|
||||||
|
|
||||||
Packet packet = new Packet()
|
|
||||||
{
|
|
||||||
Operator = dataOperator,
|
|
||||||
Type = dataType,
|
|
||||||
PacketNumber = packetNumber,
|
|
||||||
PayloadSize = packSize,
|
|
||||||
Payload = payload,
|
|
||||||
};
|
|
||||||
|
|
||||||
packets.Add(packet);
|
|
||||||
remainedSize -= packSize;
|
|
||||||
} while (remainedSize > 0);
|
|
||||||
|
|
||||||
return packets;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
namespace ProtocolLib;
|
|
||||||
|
|
||||||
public class Packet
|
|
||||||
{
|
|
||||||
public DataOperator Operator { get; set; }
|
|
||||||
|
|
||||||
public DataType Type { get; set; }
|
|
||||||
|
|
||||||
public long PacketNumber { get; set;}
|
|
||||||
|
|
||||||
public int PayloadSize { get; set; }
|
|
||||||
|
|
||||||
public byte[] Payload { get; set; }
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
namespace ProtocolLib;
|
|
||||||
|
|
||||||
public class PacketStream
|
|
||||||
{
|
|
||||||
private Stream stream;
|
|
||||||
|
|
||||||
public PacketStream(Stream stream)
|
|
||||||
{
|
|
||||||
this.stream = stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask WritePacket(Packet packet)
|
|
||||||
{
|
|
||||||
byte[] buffer = this.PacketToBytes(packet);
|
|
||||||
await this.stream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask<Packet?> ReadPacket()
|
|
||||||
{
|
|
||||||
byte[] buffer = new byte[2 + sizeof(long) + sizeof(int)];
|
|
||||||
int r = await this.stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
|
||||||
if (r <= 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
DataOperator dataOperator = (DataOperator) buffer[0];
|
|
||||||
DataType dataType = (DataType) buffer[1];
|
|
||||||
long packetNumber = BitConverter.ToInt64(buffer, 2);
|
|
||||||
int payloadSize = BitConverter.ToInt32(buffer, 10);
|
|
||||||
if (payloadSize < 0)
|
|
||||||
{
|
|
||||||
throw new IOException();
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer = new byte[payloadSize];
|
|
||||||
r = await this.stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
|
||||||
if (r <= 0)
|
|
||||||
{
|
|
||||||
throw new IOException();
|
|
||||||
}
|
|
||||||
|
|
||||||
Packet packet = new Packet()
|
|
||||||
{
|
|
||||||
Operator = dataOperator,
|
|
||||||
Type = dataType,
|
|
||||||
PacketNumber = packetNumber,
|
|
||||||
PayloadSize = payloadSize,
|
|
||||||
Payload = buffer,
|
|
||||||
};
|
|
||||||
|
|
||||||
return packet;
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] PacketToBytes(Packet packet)
|
|
||||||
{
|
|
||||||
byte[] buffer = new byte[2 + sizeof(long) + sizeof(int) + packet.PayloadSize];
|
|
||||||
|
|
||||||
buffer[0] = (byte) packet.Operator;
|
|
||||||
buffer[1] = (byte) packet.Type;
|
|
||||||
|
|
||||||
byte[] temp = BitConverter.GetBytes(packet.PacketNumber);
|
|
||||||
Array.Copy(temp, 0, buffer, 2, temp.Length);
|
|
||||||
temp = BitConverter.GetBytes(packet.PayloadSize);
|
|
||||||
Array.Copy(temp, 0, buffer, 10, temp.Length);
|
|
||||||
Array.Copy(packet.Payload, 0, buffer, 14, packet.PayloadSize);
|
|
||||||
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user