From b3710025946dc44613f6effbef09f7327d42ef83 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Fri, 27 Sep 2024 17:16:20 +0200 Subject: [PATCH] Issue #929 - Implement a utility class to save large downloads to a file. * PathResponseListener now yields a PathResponse record with Path and Response. * Code cleanups. * Simplified test cases. * Added documentation. Signed-off-by: Simone Bordet --- .../client/http/HTTPClientDocs.java | 25 ++ .../programming-guide/pages/client/http.adoc | 7 + .../jetty/client/PathResponseListener.java | 119 +++--- .../client/PathResponseListenerTest.java | 372 +++--------------- 4 files changed, 146 insertions(+), 377 deletions(-) diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java index 1782d1de9f12..dd478f1ab267 100644 --- a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java @@ -47,6 +47,7 @@ import org.eclipse.jetty.client.InputStreamResponseListener; import org.eclipse.jetty.client.OutputStreamRequestContent; import org.eclipse.jetty.client.PathRequestContent; +import org.eclipse.jetty.client.PathResponseListener; import org.eclipse.jetty.client.ProxyConfiguration; import org.eclipse.jetty.client.Request; import org.eclipse.jetty.client.Response; @@ -494,6 +495,30 @@ public void inputStreamResponseListener() throws Exception // end::inputStreamResponseListener[] } + public void pathResponseListener() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::pathResponseListener[] + Path savePath = Path.of("/path/to/save/file.bin"); + + // Typical usage as a response listener. + PathResponseListener listener = new PathResponseListener(savePath, true); + httpClient.newRequest("http://domain.com/path") + .send(listener); + // Wait for the response content to be saved. + var result = listener.get(5, TimeUnit.SECONDS); + + // Alternative usage with CompletableFuture. + var completable = PathResponseListener.write(httpClient.newRequest("http://domain.com/path"), savePath, true); + completable.whenComplete((pathResponse, failure) -> + { + // Your logic here. + }); + // end::pathResponseListener[] + } + public void forwardContent() throws Exception { HttpClient httpClient = new HttpClient(); diff --git a/documentation/jetty/modules/programming-guide/pages/client/http.adoc b/documentation/jetty/modules/programming-guide/pages/client/http.adoc index e09b2ef1ea60..5ee9cba567ee 100644 --- a/documentation/jetty/modules/programming-guide/pages/client/http.adoc +++ b/documentation/jetty/modules/programming-guide/pages/client/http.adoc @@ -468,6 +468,13 @@ If you want to avoid buffering, you can wait for the response and then stream th include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=inputStreamResponseListener] ---- +If you want to save the response content to a file, you can use the `PathResponseListener` utility class: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=pathResponseListener] +---- + Finally, let's look at the advanced usage of the response content handling. The response content is provided by the `HttpClient` implementation to application listeners following the read/demand model of `org.eclipse.jetty.io.Content.Source`. diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/PathResponseListener.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/PathResponseListener.java index d6c623ddbdfc..a908861b462e 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/PathResponseListener.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/PathResponseListener.java @@ -20,27 +20,31 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import org.eclipse.jetty.client.Response.Listener; import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.util.IO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Implementation of {@link Response.ContentListener} that produces an {@link Path} - * that allows applications to save a file from a response {@link Response} - * like {@code curl -o file.bin} does. - *

- * Typical usage is: - *

{@code httpClient.newRequest(host, port)
- * .send(new PathResponseListener(Path.of("/tmp/file.bin"));
- * 
- *  var request = httpClient.newRequest(host, port);
- *  CompletableFuture completable = PathResponseListener.write(request, Path.of("/tmp/file.bin"), rewriteExistingFile);
- *  }
+ *

Implementation of {@link Response.ContentListener} that + * saves the response content to a file {@link Path}, like + * {@code curl -o file.bin} does.

+ *

Typical usage is:

+ *
{@code
+ * // Typical usage.
+ * httpClient.newRequest(host, port)
+ *     .send(new PathResponseListener(Path.of("/tmp/file.bin")), overwriteExistingFile);
+ *
+ * // Alternative usage.
+ * var request = httpClient.newRequest(host, port);
+ * CompletableFuture completable = PathResponseListener.write(request, Path.of("/tmp/file.bin"), overwriteExistingFile);
+ * }
*/ -public class PathResponseListener extends CompletableFuture implements Listener +public class PathResponseListener extends CompletableFuture implements Listener { private static final Logger LOG = LoggerFactory.getLogger(InputStreamResponseListener.class); @@ -51,23 +55,19 @@ public PathResponseListener(Path path, boolean overwrite) throws IOException { this.path = path; - // Throws the exception if file can't be overwritten - // otherwise truncate it. if (Files.exists(path) && !overwrite) - { - throw new FileAlreadyExistsException("File can't be overwritten"); - } + throw new FileAlreadyExistsException(path.toString(), null, "File cannot be overwritten"); - fileChannel = FileChannel.open(this.path, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); + fileChannel = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); } @Override public void onHeaders(Response response) { if (response.getStatus() != HttpStatus.OK_200) - { - response.abort(new HttpResponseException(String.format("HTTP status code of response %d", response.getStatus()), response)); - } + response.abort(new HttpResponseException(String.format("Cannot save response content for HTTP status code %d", response.getStatus()), response)); + else if (LOG.isDebugEnabled()) + LOG.debug("saving response content to {}", path); } @Override @@ -77,63 +77,64 @@ public void onContent(Response response, ByteBuffer content) { var bytesWritten = fileChannel.write(content); if (LOG.isDebugEnabled()) - LOG.debug("%d bytes written", bytesWritten); + LOG.debug("{} bytes written to {}", bytesWritten, path); } - catch (IOException e) + catch (Throwable x) { - response.abort(e); + response.abort(x); } } @Override - public void onComplete(Result result) + public void onSuccess(Response response) { - try - { - if (result.isFailed()) - { - if (LOG.isDebugEnabled()) - LOG.debug("Result failure", result.getFailure()); - completeExceptionally(result.getFailure()); - return; - } + if (LOG.isDebugEnabled()) + LOG.debug("saved response content to {}", path); + } - this.complete(this.path); - } - finally - { - try - { - fileChannel.close(); - } - catch (IOException e) - { - e.printStackTrace(); - } - } + @Override + public void onFailure(Response response, Throwable failure) + { + if (LOG.isDebugEnabled()) + LOG.debug("failed to save response content to {}", path); + } + @Override + public void onComplete(Result result) + { + IO.close(fileChannel); + if (result.isSucceeded()) + complete(new PathResponse(result.getResponse(), path)); + else + completeExceptionally(result.getFailure()); } /** - * Writes a file into {@link Path}. + *

Writes the response content to the given file {@link Path}.

* - * @param request to a server - * @param path to write a file - * @param overwrite true overwrites a file, otherwise fails - * @return {@code CompletableFuture} + * @param request the HTTP request + * @param path the path to write the response content to + * @param overwrite whether to overwrite an existing file + * @return a {@link CompletableFuture} that is completed when the exchange completes */ - public static CompletableFuture write(Request request, Path path, boolean overwrite) + public static CompletableFuture write(Request request, Path path, boolean overwrite) { - PathResponseListener l = null; + PathResponseListener listener = null; try { - l = new PathResponseListener(path, overwrite); - request.send(l); + listener = new PathResponseListener(path, overwrite); + request.send(listener); + return listener; } - catch (Throwable e) + catch (Throwable x) { - l.completeExceptionally(e); + CompletableFuture completable = Objects.requireNonNullElse(listener, new CompletableFuture<>()); + completable.completeExceptionally(x); + return completable; } - return l; + } + + public record PathResponse(Response response, Path path) + { } } diff --git a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/PathResponseListenerTest.java b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/PathResponseListenerTest.java index 8ef3da78b973..d5a4a7b333c9 100644 --- a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/PathResponseListenerTest.java +++ b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/PathResponseListenerTest.java @@ -13,385 +13,121 @@ package org.eclipse.jetty.client; -import java.io.BufferedInputStream; -import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URL; +import java.net.URI; import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; -import java.security.MessageDigest; -import java.util.Random; -import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; -import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.ResourceHandler; +import org.eclipse.jetty.toolchain.test.FS; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.resource.ResourceFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class PathResponseListenerTest { private Server server; private ServerConnector connector; - private MessageDigest origDigest; - private MessageDigest respDigest; + private Path resourceDir; - private static final Path ORIGIN_ZERO_FILE = Path.of(System.getProperty("user.dir"), "origin_zero"); - private static final Path ORIGIN_SMALL_FILE = Path.of(System.getProperty("user.dir"), "origin_small"); - private static final Path ORIGIN_LARGE_FILE = Path.of(System.getProperty("user.dir"), "origin_large"); - - private static final Path RESPONSE_ZERO_FILE = Path.of(System.getProperty("user.dir"), "response_zero"); - private static final Path RESPONSE_SMALL_FILE = Path.of(System.getProperty("user.dir"), "response_small"); - private static final Path RESPONSE_LARGE_FILE = Path.of(System.getProperty("user.dir"), "response_large"); - - private void configureTestEnvironment() throws Exception + @BeforeEach + public void startServer() throws Exception { - origDigest = MessageDigest.getInstance("MD5"); - respDigest = MessageDigest.getInstance("MD5"); QueuedThreadPool serverThreads = new QueuedThreadPool(); serverThreads.setName("server"); server = new Server(serverThreads); ResourceHandler resourceHandler = new ResourceHandler(); - resourceHandler.setBaseResource(ResourceFactory.of(resourceHandler).newResource(System.getProperty("user.dir"))); + resourceDir = MavenTestingUtils.getTargetTestingPath(PathResponseListenerTest.class.getSimpleName()); + FS.ensureEmpty(resourceDir); + resourceHandler.setBaseResource(ResourceFactory.of(server).newResource(resourceDir)); resourceHandler.setDirAllowed(false); connector = new ServerConnector(server); server.addConnector(connector); server.setHandler(resourceHandler); - } - - private void deleteFiles(Path... paths) - { - for (Path p : paths) - { - try - { - Files.deleteIfExists(p); - } - catch (IOException e) - { - e.printStackTrace(); - } - } - } - - private void createZeroFile() throws IOException - { - try (OutputStream zeroFileWriter = Files.newOutputStream(ORIGIN_ZERO_FILE, StandardOpenOption.CREATE_NEW)) - { - zeroFileWriter.write(ByteBuffer.allocate(0).array()); - } - catch (IOException e) - { - throw e; - } - } - - private void createSmallFile() throws IOException - { - try (OutputStream smallFileWriter = Files.newOutputStream(ORIGIN_SMALL_FILE, StandardOpenOption.CREATE_NEW)) - { - ByteBuffer buff = ByteBuffer.allocate(1024); - Random random = new Random(); - int writeBytes = 0; - while (writeBytes < 1024) - { - random.nextBytes(buff.array()); - buff.flip(); - smallFileWriter.write(buff.array()); - buff.clear(); - writeBytes++; - } - } - catch (IOException e) - { - throw e; - } - } - - private void createLargeFile() throws IOException - { - try (OutputStream largeFileWriter = Files.newOutputStream(ORIGIN_LARGE_FILE, StandardOpenOption.CREATE_NEW)) - { - ByteBuffer buff = ByteBuffer.allocate(1048576); - Random random = new Random(); - int writeBytes = 0; - while (writeBytes < 1024) - { - random.nextBytes(buff.array()); - buff.flip(); - largeFileWriter.write(buff.array()); - buff.clear(); - writeBytes++; - } - } - catch (IOException e) - { - throw e; - } - } - - @BeforeEach - public void startServer() throws Exception - { - configureTestEnvironment(); server.start(); } @AfterEach public void stopServer() throws Exception { - server.stop(); - - // Reuse message digest - origDigest.reset(); - respDigest.reset(); - } - - @Test - public void testZeroFileDownload() throws Exception - { - try (HttpClient client = new HttpClient(new HttpClientTransportOverHTTP(1))) - { - deleteFiles(ORIGIN_ZERO_FILE, RESPONSE_ZERO_FILE); - createZeroFile(); - - client.start(); - - URL url = new URL("http", "localhost", connector.getLocalPort(), "/" + ORIGIN_ZERO_FILE.getFileName().toString()); - - PathResponseListener listener = new PathResponseListener(RESPONSE_ZERO_FILE, true); - Request request = client.newRequest(url.toURI().toString()); - request.send(listener); - Path path = listener.get(600, TimeUnit.SECONDS); - - assertTrue(Files.exists(path)); - - File originFile = new File(ORIGIN_ZERO_FILE.toUri()); - File responseFile = new File(RESPONSE_ZERO_FILE.toUri()); - - assertTrue(originFile.exists() && responseFile.exists()); - assertTrue(originFile.length() == 0 && responseFile.length() == 0); - } - catch (Exception e) - { - throw e; - } - finally - { - deleteFiles(ORIGIN_ZERO_FILE, RESPONSE_ZERO_FILE); - } - } - - @Test - public void testSmallFileDownload() throws Exception - { - try (HttpClient client = new HttpClient(new HttpClientTransportOverHTTP(1));) - { - deleteFiles(ORIGIN_SMALL_FILE, RESPONSE_SMALL_FILE); - createSmallFile(); - - client.start(); - - URL url = new URL("http", "localhost", connector.getLocalPort(), "/" + ORIGIN_SMALL_FILE.getFileName().toString()); - - PathResponseListener listener = new PathResponseListener(RESPONSE_SMALL_FILE, true); - Request request = client.newRequest(url.toURI().toString()); - request.send(listener); - Path path = listener.get(); - - assertTrue(Files.exists(path)); - - try (InputStream responseFile = Files.newInputStream(RESPONSE_SMALL_FILE, StandardOpenOption.READ); - InputStream originFile = Files.newInputStream(ORIGIN_SMALL_FILE, StandardOpenOption.READ)) - { - origDigest.update(originFile.readAllBytes()); - respDigest.update(responseFile.readAllBytes()); - - assertTrue(MessageDigest.isEqual(origDigest.digest(), respDigest.digest())); - } - } - catch (Exception e) - { - throw e; - } - finally - { - deleteFiles(ORIGIN_SMALL_FILE, RESPONSE_SMALL_FILE); - } + FS.ensureEmpty(resourceDir); + LifeCycle.stop(server); } - @Test - public void testLargeFileDownload() throws Exception + private Path createServerFile(int length) throws IOException { - try (HttpClient client = new HttpClient(new HttpClientTransportOverHTTP(1));) - { - deleteFiles(ORIGIN_LARGE_FILE, RESPONSE_LARGE_FILE); - createLargeFile(); - - client.start(); - - URL url = new URL("http", "localhost", connector.getLocalPort(), "/" + ORIGIN_LARGE_FILE.getFileName().toString()); - - PathResponseListener listener = new PathResponseListener(RESPONSE_LARGE_FILE, true); - Request request = client.newRequest(url.toURI().toString()); - request.send(listener); - Path path = listener.get(); - - assertTrue(Files.exists(path)); - - try (InputStream responseFile = Files.newInputStream(RESPONSE_LARGE_FILE, StandardOpenOption.READ); - InputStream originFile = Files.newInputStream(ORIGIN_LARGE_FILE, StandardOpenOption.READ)) - { - origDigest.update(originFile.readAllBytes()); - respDigest.update(responseFile.readAllBytes()); - - assertTrue(MessageDigest.isEqual(origDigest.digest(), respDigest.digest())); - } - } - catch (Exception e) - { - throw e; - } - finally + Path path = Files.createTempFile(resourceDir, "file-", ".bin"); + try (SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.WRITE)) { - deleteFiles(ORIGIN_LARGE_FILE, RESPONSE_LARGE_FILE); + ByteBuffer buffer = ByteBuffer.allocate(length); + ThreadLocalRandom.current().nextBytes(buffer.array()); + channel.write(buffer); } + return path; } - @Test - public void testZeroFileDownloadCompletable() throws Exception + @ParameterizedTest + @ValueSource(ints = {0, 1024, 1048576}) + public void testFileDownload(int length) throws Exception { - try (HttpClient client = new HttpClient(new HttpClientTransportOverHTTP(1));) + try (HttpClient client = new HttpClient()) { - deleteFiles(ORIGIN_ZERO_FILE, RESPONSE_ZERO_FILE); - createZeroFile(); - client.start(); - URL url = new URL("http", "localhost", connector.getLocalPort(), "/" + ORIGIN_ZERO_FILE.getFileName().toString()); - - Request request = client.newRequest(url.toURI().toString()); - - CompletableFuture completable = PathResponseListener.write(request, RESPONSE_ZERO_FILE, true); - completable.thenAccept(path -> - { - try (InputStream responseFile = new BufferedInputStream(Files.newInputStream(path, StandardOpenOption.READ)); - InputStream originFile = new BufferedInputStream(Files.newInputStream(ORIGIN_ZERO_FILE, StandardOpenOption.READ))) - { - origDigest.update(originFile.readAllBytes()); - respDigest.update(responseFile.readAllBytes()); - - assertTrue(MessageDigest.isEqual(origDigest.digest(), respDigest.digest())); - } - catch (IOException e) - { - e.printStackTrace(); - } - }); - completable.get(); - } - catch (Exception e) - { - throw e; - } - finally - { - deleteFiles(ORIGIN_ZERO_FILE, RESPONSE_ZERO_FILE); - } - } - - @Test - public void testSmallFileDownloadCompletable() throws Exception - { - try (HttpClient client = new HttpClient(new HttpClientTransportOverHTTP(1));) - { - deleteFiles(ORIGIN_SMALL_FILE, RESPONSE_SMALL_FILE); - createSmallFile(); - - client.start(); + Path serverPath = createServerFile(length); + URI uri = URI.create("http://localhost:" + connector.getLocalPort() + "/" + serverPath.getFileName()); - URL url = new URL("http", "localhost", connector.getLocalPort(), "/" + ORIGIN_SMALL_FILE.getFileName().toString()); + Path clientPath = Files.createTempFile(resourceDir, "saved-", ".bin"); + PathResponseListener listener = new PathResponseListener(clientPath, true); + client.newRequest(uri).send(listener); + var pathResponse = listener.get(5, TimeUnit.SECONDS); - Request request = client.newRequest(url.toURI().toString()); - CompletableFuture completable = PathResponseListener.write(request, RESPONSE_SMALL_FILE, true); + assertTrue(Files.exists(pathResponse.path())); + assertArrayEquals(Files.readAllBytes(serverPath), Files.readAllBytes(clientPath)); - completable.thenAccept(path -> - { - try (InputStream responseFile = new BufferedInputStream(Files.newInputStream(path, StandardOpenOption.READ)); - InputStream originFile = new BufferedInputStream(Files.newInputStream(ORIGIN_SMALL_FILE, StandardOpenOption.READ))) - { - origDigest.update(originFile.readAllBytes()); - respDigest.update(responseFile.readAllBytes()); + // Do it again with overwrite=false. + Files.delete(clientPath); + listener = new PathResponseListener(clientPath, false); + client.newRequest(uri).send(listener); + pathResponse = listener.get(5, TimeUnit.SECONDS); - assertTrue(MessageDigest.isEqual(origDigest.digest(), respDigest.digest())); - } - catch (IOException e) - { - e.printStackTrace(); - } - }); - completable.get(); - } - catch (Exception e) - { - throw e; - } - finally - { - deleteFiles(ORIGIN_SMALL_FILE, RESPONSE_SMALL_FILE); + assertTrue(Files.exists(pathResponse.path())); + assertArrayEquals(Files.readAllBytes(serverPath), Files.readAllBytes(clientPath)); } } - @Test - public void testLargeFileDownloadCompletable() throws Exception + @ParameterizedTest + @ValueSource(ints = {0, 1024, 1048576}) + public void testFileDownloadWithCompletable(int length) throws Exception { - try (HttpClient client = new HttpClient(new HttpClientTransportOverHTTP(1));) + try (HttpClient client = new HttpClient()) { - deleteFiles(ORIGIN_LARGE_FILE, RESPONSE_LARGE_FILE); - createLargeFile(); - client.start(); - URL url = new URL("http", "localhost", connector.getLocalPort(), "/" + ORIGIN_LARGE_FILE.getFileName().toString()); - - Request request = client.newRequest(url.toURI().toString()); - CompletableFuture completable = PathResponseListener.write(request, RESPONSE_LARGE_FILE, true); + Path serverPath = createServerFile(length); + URI uri = URI.create("http://localhost:" + connector.getLocalPort() + "/" + serverPath.getFileName()); - completable.thenAccept(path -> - { - try (BufferedInputStream responseFile = new BufferedInputStream(Files.newInputStream(path, StandardOpenOption.READ)); - BufferedInputStream originFile = new BufferedInputStream(Files.newInputStream(ORIGIN_LARGE_FILE, StandardOpenOption.READ));) - { - origDigest.update(originFile.readAllBytes()); - respDigest.update(responseFile.readAllBytes()); + Path clientPath = Files.createTempFile(resourceDir, "saved-", ".bin"); + var pathResponse = PathResponseListener.write(client.newRequest(uri), clientPath, true) + .get(5, TimeUnit.SECONDS); - assertTrue(MessageDigest.isEqual(origDigest.digest(), respDigest.digest())); - } - catch (IOException e) - { - e.printStackTrace(); - } - }); - completable.get(); - } - catch (Exception e) - { - throw e; - } - finally - { - deleteFiles(ORIGIN_LARGE_FILE, RESPONSE_LARGE_FILE); + assertTrue(Files.exists(pathResponse.path())); + assertArrayEquals(Files.readAllBytes(serverPath), Files.readAllBytes(clientPath)); } } }