diff --git a/Hw4.Tests/Hw4.Tests.csproj b/Hw4.Tests/Hw4.Tests.csproj new file mode 100644 index 0000000..af4c0e9 --- /dev/null +++ b/Hw4.Tests/Hw4.Tests.csproj @@ -0,0 +1,23 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + + + + + + + + diff --git a/Hw4.Tests/ServerAndClientTests.cs b/Hw4.Tests/ServerAndClientTests.cs new file mode 100644 index 0000000..b0706fe --- /dev/null +++ b/Hw4.Tests/ServerAndClientTests.cs @@ -0,0 +1,80 @@ +namespace Hw4.Tests; + +using NUnit.Framework.Internal; + + +public class Tests +{ + private Client client; + private Server server; + + [OneTimeSetUp] + public void SetUp() + { + Directory.CreateDirectory("testDir"); + File.WriteAllText("testDir/test.txt", "test message"); + + Directory.CreateDirectory("testDir/directory"); + File.WriteAllText("testDir/directory/test.txt", "test message"); + File.WriteAllText("testDir/test.txt", "test message"); + + Directory.CreateDirectory("downloadedFiles"); + + server = new Server(2303); + client = new Client(2303); + Task.Run(() => server.Start()); + } + + [OneTimeTearDown] + public void Delete() + { + server.Stop(); + Directory.Delete("testDir", true); + Directory.Delete("downloadedFiles", true); + } + + [Test] + public async Task ListTest() + { + var result = await client.List("testDir"); + Assert.Multiple(() => + { + Assert.That(result.size, Is.EqualTo(2)); + Assert.That(result.Item2, Is.EqualTo(new List<(string, bool)> { ("testDir/directory", true), ("testDir/test.txt", false) })); + }); + } + + [Test] + public async Task GetTest() + { + using (FileStream SourceStream = File.Open("downloadedFiles/file1", FileMode.OpenOrCreate)) + { + await client.Get("testDir/test.txt", SourceStream); + } + + var expected = File.ReadAllBytes("testDir/test.txt"); + var result = File.ReadAllBytes("downloadedFiles/file1"); + + Assert.That(result, Is.EqualTo(expected)); + } + + [Test] + public async Task GetForNotExistingFileTest() + { + var result = 0; + using (FileStream SourceStream = File.Open("downloadedFiles/file1", FileMode.OpenOrCreate)) + { + result = await client.Get("notExistingFile.txt", SourceStream); + } + + Assert.That(result, Is.EqualTo(-1)); + } + + [Test] + public async Task ListForNotExistingFileTest() + { + var result = await client.List("notExistingDirectory"); + + Assert.That(result.size, Is.EqualTo(-1)); + } +} \ No newline at end of file diff --git a/Hw4.Tests/Usings.cs b/Hw4.Tests/Usings.cs new file mode 100644 index 0000000..9a28bd8 --- /dev/null +++ b/Hw4.Tests/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; diff --git a/Hw4/Client.cs b/Hw4/Client.cs new file mode 100644 index 0000000..39d6bc5 --- /dev/null +++ b/Hw4/Client.cs @@ -0,0 +1,143 @@ +using System.Net.Sockets; +using System.Text; + +namespace Hw4; + +public class Client +{ + private string host; + private int port; + + /// + /// Instantiates a new instance of Client class. + /// + public Client(int port, string host = "localhost") + { + this.port = port; + this.host = host; + } + + /// + /// Requests a listing of files in a directory on the server and reads server's response + /// + /// The path on the server to list files from. + /// the parsed response from the server - how many entries in directory (size), list of entries with bool value - "is this entry a directory" + public async Task<(int size, List<(string, bool)>?)> List(string filePath) + { + using (var client = new TcpClient(host, port)) + { + var stream = client.GetStream(); + var writer = new StreamWriter(stream) { AutoFlush = true }; + var reader = new StreamReader(stream); + await writer.WriteAsync("1 " + filePath + "\n"); + try + { + return await ListResponse(reader); + } + catch + { + throw; + } + } + } + + /// + /// Requests the file size and contents in bytes and writes the contents to the passed stream. + /// + /// The path to get files from. + /// The stream to which Get sends content from downloaded file. + /// amount of bytes in downloaded file + public async Task Get(string filePath, Stream outputStream) + { + using (var client = new TcpClient(host, port)) + { + var stream = client.GetStream(); + var writer = new StreamWriter(stream) { AutoFlush = true }; + var reader = new StreamReader(stream); + await writer.WriteAsync("2 " + filePath + "\n"); + try + { + return await GetResponse(reader, outputStream); + } + catch (InvalidDataException e) + { + throw new InvalidDataException(e.Message + ":Wrong server response"); + } + catch (InvalidOperationException) + { + throw new InvalidOperationException("Asynchronous read operation returned null"); + } + } + } + + private static async Task<(int size, List<(string, bool)>?)> ListResponse(StreamReader reader) + { + var listResponse = await reader.ReadLineAsync() ?? throw new InvalidOperationException(); + var parsedListResponse = listResponse.Split(); + var isFirstItemNumber = int.TryParse(parsedListResponse[0], out int size); + if (!isFirstItemNumber) + { + throw new InvalidDataException("Amount of content in directory expected"); + } + + if (size == -1) + { + return (-1, null); + } + + List<(string, bool)> directoryContent = new(); + + if (size * 2 != parsedListResponse.Length - 1) + { + throw new InvalidDataException("Size isn't equal to amount of content in directory"); + } + for (int i = 1; i <= size; i++) + { + string currentPath = parsedListResponse[2 * i - 1]; + bool currentPathIsDirectory; + if (parsedListResponse[i * 2] == "True") + { + currentPathIsDirectory = true; + } + else if (parsedListResponse[i * 2] == "False") + { + currentPathIsDirectory = false; + } + else + { + throw new InvalidDataException("True or false expected"); + } + directoryContent.Add((currentPath, currentPathIsDirectory)); + } + + return (size, directoryContent); + } + + private static async Task GetResponse(StreamReader reader, Stream output) + { + var response = await reader.ReadToEndAsync(); + + var responseArray = response.Split(" ", 2); + var stringSize = responseArray[0]; + + var isFirstItemNumber = int.TryParse(stringSize, out int size); + + if (!isFirstItemNumber || size < -1) + { + throw new InvalidDataException("Amount of bytes in file expected"); + } + + if (size == -1) + { + return -1; + } + + var content = responseArray[1]; + + await output.WriteAsync(Encoding.UTF8.GetBytes(content).AsMemory(0, size)); + return size; + } +} + + + diff --git a/Hw4/Hw4.csproj b/Hw4/Hw4.csproj new file mode 100644 index 0000000..4658cbf --- /dev/null +++ b/Hw4/Hw4.csproj @@ -0,0 +1,9 @@ + + + + net7.0 + enable + enable + + + diff --git a/Hw4/Hw4.sln b/Hw4/Hw4.sln new file mode 100644 index 0000000..2bb4550 --- /dev/null +++ b/Hw4/Hw4.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 25.0.1706.3 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hw4", "Hw4.csproj", "{9757B2DC-6B82-4675-B32F-615890E1AC73}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hw4.Tests", "..\Hw4.Tests\Hw4.Tests.csproj", "{03EAEE58-748F-446E-B3C3-0395DA404E39}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9757B2DC-6B82-4675-B32F-615890E1AC73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9757B2DC-6B82-4675-B32F-615890E1AC73}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9757B2DC-6B82-4675-B32F-615890E1AC73}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9757B2DC-6B82-4675-B32F-615890E1AC73}.Release|Any CPU.Build.0 = Release|Any CPU + {03EAEE58-748F-446E-B3C3-0395DA404E39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03EAEE58-748F-446E-B3C3-0395DA404E39}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03EAEE58-748F-446E-B3C3-0395DA404E39}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03EAEE58-748F-446E-B3C3-0395DA404E39}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3D80B059-63A4-4CF8-B8EA-D0774CEDF1B8} + EndGlobalSection +EndGlobal diff --git a/Hw4/Server.cs b/Hw4/Server.cs new file mode 100644 index 0000000..d86a772 --- /dev/null +++ b/Hw4/Server.cs @@ -0,0 +1,129 @@ +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace Hw4; +public class Server +{ + private TcpListener listener; + private CancellationTokenSource cancellationTokenSource; + + /// + /// Instantiates a new instance of Server class. + /// + public Server(int port) + { + listener = new TcpListener(IPAddress.Any, port); + cancellationTokenSource = new CancellationTokenSource(); + } + + /// + /// Starts the server and begins listening for client connections.. + /// + public async Task Start() + { + listener.Start(); + var serverTasks = new List(); + while (!cancellationTokenSource.IsCancellationRequested) + { + var socket = await listener.AcceptSocketAsync(); + serverTasks.Add(Task.Run(async () => + { + var stream = new NetworkStream(socket); + var reader = new StreamReader(stream); + var writer = new StreamWriter(stream) { AutoFlush = true }; + + var data = await reader.ReadLineAsync(); + + if (data != null) + { + (int request, string filePath) = ClientRequestParse(data); + try + { + if (request == 1) + { + await List(filePath, writer); + } + else if (request == 2) + { + await Get(filePath, writer); + } + else + { + throw new ArgumentException("Expected number 1 or 2 in request"); + } + } + catch (Exception e) + { + socket.Close(); + throw new ArgumentException(e.Message + ": Wrong Client request"); + } + + } + else + { + socket.Close(); + throw new InvalidOperationException("Asynchronous read operation returned null"); + } + socket.Close(); + } + )); + } + await Task.WhenAll(serverTasks); + } + + private static (int, string) ClientRequestParse(string clientRequest) + { + var clientRequestParsed = clientRequest.Split(); + var stringRequestType = clientRequestParsed[0]; + bool isRequestTypeNumber = int.TryParse(stringRequestType, out int requestType); + + if (!isRequestTypeNumber) + { + throw new ArgumentException("Number expected"); + } + + return (requestType, clientRequestParsed[1]); + } + + private static async Task List(string path, StreamWriter writer) + { + if (!Directory.Exists(path)) + { + await writer.WriteAsync("-1"); + return; + } + + var fileSystemEntries = Directory.GetFileSystemEntries(path); + var size = fileSystemEntries.Length; + + await writer.WriteAsync(size.ToString()); + foreach (var entry in fileSystemEntries) + { + await writer.WriteAsync($" {entry} {Directory.Exists(entry)}"); + } + } + + private static async Task Get(string path, StreamWriter writer) + { + if (!File.Exists(path)) + { + await writer.WriteAsync("-1"); + return; + } + var content = await File.ReadAllBytesAsync(path); + await writer.WriteAsync($"{content.Length} {Encoding.UTF8.GetString(content)}"); + } + + /// + /// Stops the server. + /// + public void Stop() + { + cancellationTokenSource.Cancel(); + listener.Stop(); + } +} + + +