Skip to content

Commit

Permalink
4.x: Update streaming example so it starts and downloads last uploade…
Browse files Browse the repository at this point in the history
…d file (#8515)

* Update streaming example so it starts correctly and downloads last uploaded file
* Add tests
  • Loading branch information
barchetta authored Mar 22, 2024
1 parent 4f6bf0d commit f268194
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 21 deletions.
17 changes: 11 additions & 6 deletions examples/webserver/streaming/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# Streaming Example

This application uses NIO and data buffers to show the implementation of a simple streaming service.
Files can be uploaded and downloaded in a streaming fashion using `Subscriber<DataChunk>` and
`Producer<DataChunk>`. As a result, service runs in constant space instead of proportional
to the size of the file being uploaded or downloaded.
This application is an example of a very simple streaming service. It leverages the
fact that Helidon uses virtual threads to perform simple input/output stream blocking
operations in the endpoint handlers. As a result, this service runs in constant space instead
of proportional to the size of the file being uploaded or downloaded.

There are two endpoints:

- `upload` : uploads a file to the service
- `download` : downloads the previously uploaded file

## Build and run

Expand All @@ -14,6 +19,6 @@ java -jar target/helidon-examples-webserver-streaming.jar

Upload a file and download it back with `curl`:
```shell
curl --data-binary "@target/classes/large-file.bin" http://localhost:8080/upload
curl http://localhost:8080/download
curl --data-binary "@large-file.bin" http://localhost:8080/upload
curl http://localhost:8080/download --output myfile.bin
```
File renamed without changes.
10 changes: 10 additions & 0 deletions examples/webserver/streaming/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.helidon.webserver.testing.junit5</groupId>
<artifactId>helidon-webserver-testing-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.helidon.webclient</groupId>
<artifactId>helidon-webclient</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2023 Oracle and/or its affiliates.
* Copyright (c) 2018, 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -25,8 +25,6 @@
*/
public class Main {

static final String LARGE_FILE_RESOURCE = "/large-file.bin";

private Main() {
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2023 Oracle and/or its affiliates.
* Copyright (c) 2018, 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,14 +18,12 @@

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Objects;
import java.util.logging.Logger;

import io.helidon.http.Status;
import io.helidon.webserver.http.HttpRules;
import io.helidon.webserver.http.HttpService;
import io.helidon.webserver.http.ServerRequest;
Expand All @@ -38,14 +36,11 @@
public class StreamingService implements HttpService {
private static final Logger LOGGER = Logger.getLogger(StreamingService.class.getName());

private final Path filePath;
// Last file uploaded (or default). Since we don't do any locking
// when operating on the file this example is not safe for concurrent requests.
private volatile Path filePath;

StreamingService() {
try {
filePath = Paths.get(Objects.requireNonNull(getClass().getResource(Main.LARGE_FILE_RESOURCE)).toURI());
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}

@Override
Expand All @@ -59,22 +54,28 @@ private void upload(ServerRequest request, ServerResponse response) {
try {
Path tempFilePath = Files.createTempFile("large-file", ".tmp");
Files.copy(request.content().inputStream(), tempFilePath, StandardCopyOption.REPLACE_EXISTING);
filePath = tempFilePath;
response.send("File was stored as " + tempFilePath);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
LOGGER.info("Exiting upload ...");
LOGGER.info("Exiting upload after uploading " + filePath.toFile().length() + " bytes...");
}

private void download(ServerRequest request, ServerResponse response) {
LOGGER.info("Entering download ..." + Thread.currentThread());
if (filePath == null) {
response.status(Status.BAD_REQUEST_400).send("No file to download. Please upload file first.");
return;
}
long length = filePath.toFile().length();
response.headers().contentLength(length);
try {
Files.copy(filePath, response.outputStream());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
LOGGER.info("Exiting download ...");
LOGGER.info("Exiting download after serving " + length + " bytes...");
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright (c) 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.helidon.examples.webserver.streaming;

import io.helidon.http.Status;
import io.helidon.webclient.http1.Http1Client;
import io.helidon.webclient.http1.Http1ClientResponse;
import io.helidon.webserver.http.HttpRouting;
import io.helidon.webserver.testing.junit5.ServerTest;
import io.helidon.webserver.testing.junit5.SetUpRoute;

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

@ServerTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class MainTest {

private final Http1Client client;

private final String TEST_DATA_1 = "Test Data 1";
private final String TEST_DATA_2 = "Test Data 2";

protected MainTest(Http1Client client) {
this.client = client;
}

@SetUpRoute
static void routing(HttpRouting.Builder builder) {
Main.routing(builder);
}

@Test
@Order(0)
void testBadRequest() {
try (Http1ClientResponse response = client.get("/download").request()) {
assertThat(response.status(), is(Status.BAD_REQUEST_400));
}
}

@Test
@Order(1)
void testUpload1() {
try (Http1ClientResponse response = client.post("/upload").submit(TEST_DATA_1)) {
assertThat(response.status(), is(Status.OK_200));
}
}

@Test
@Order(2)
void testDownload1() {
try (Http1ClientResponse response = client.get("/download").request()) {
assertThat(response.status(), is(Status.OK_200));
assertThat(response.as(String.class), is(TEST_DATA_1));
}
}

@Test
@Order(3)
void testUpload2() {
try (Http1ClientResponse response = client.post("/upload").submit(TEST_DATA_2)) {
assertThat(response.status(), is(Status.OK_200));
}
}

@Test
@Order(4)
void testDownload2() {
try (Http1ClientResponse response = client.get("/download").request()) {
assertThat(response.status(), is(Status.OK_200));
assertThat(response.as(String.class), is(TEST_DATA_2));
}
}
}

0 comments on commit f268194

Please sign in to comment.