Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multipart examples update #2830

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions examples/media/multipart/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Helidon SE MultiPart Example

This example demonstrates how to use `MultiPartSupport` with both the `WebServer`
and `WebClient` APIs.

This project implements a simple file service web application that supports uploading
and downloading files. The unit test uses the `WebClient` API to test the endpoints.

## Build

```
mvn package
```

## Run

First, start the server:

```
java -jar target/helidon-examples-microprofile-multipart.jar
```

Then open <http://localhost:8080/ui> in your browser.
15 changes: 15 additions & 0 deletions examples/media/multipart/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@
<groupId>org.glassfish</groupId>
<artifactId>jakarta.json</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.helidon.webclient</groupId>
<artifactId>helidon-webclient</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2020 Oracle and/or its affiliates.
* Copyright (c) 2020, 2021 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 @@ -16,13 +16,13 @@
package io.helidon.examples.media.multipart;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Map;
import java.util.stream.Stream;

import javax.json.Json;
import javax.json.JsonArrayBuilder;
Expand All @@ -31,7 +31,6 @@
import io.helidon.common.http.DataChunk;
import io.helidon.common.http.Http;
import io.helidon.common.http.MediaType;
import io.helidon.common.reactive.Multi;
import io.helidon.media.multipart.ContentDisposition;
import io.helidon.media.multipart.ReadableBodyPart;
import io.helidon.media.multipart.ReadableMultiPart;
Expand All @@ -41,24 +40,19 @@
import io.helidon.webserver.ServerResponse;
import io.helidon.webserver.Service;

import static io.helidon.common.http.Http.Status.BAD_REQUEST_400;
import static io.helidon.common.http.Http.Status.NOT_FOUND_404;

/**
* File service.
*/
public final class FileService implements Service {

private final JsonBuilderFactory jsonFactory;
private final Path storage;
private static final JsonBuilderFactory JSON_FACTORY = Json.createBuilderFactory(Map.of());
private final FileStorage storage;

/**
* Create a new file upload service instance.
*/
FileService() {
jsonFactory = Json.createBuilderFactory(Map.of());
storage = createStorage();
System.out.println("Storage: " + storage);
storage = new FileStorage();
}

@Override
Expand All @@ -69,25 +63,13 @@ public void update(Routing.Rules rules) {
}

private void list(ServerRequest req, ServerResponse res) {
JsonArrayBuilder arrayBuilder = jsonFactory.createArrayBuilder();
listFiles(storage).forEach(arrayBuilder::add);
res.send(jsonFactory.createObjectBuilder().add("files", arrayBuilder).build());
JsonArrayBuilder arrayBuilder = JSON_FACTORY.createArrayBuilder();
storage.listFiles().forEach(arrayBuilder::add);
res.send(JSON_FACTORY.createObjectBuilder().add("files", arrayBuilder).build());
}

private void download(ServerRequest req, ServerResponse res) {
Path filePath = storage.resolve(req.path().param("fname"));
if (!filePath.getParent().equals(storage)) {
res.status(BAD_REQUEST_400).send("Invalid file name");
return;
}
if (!Files.exists(filePath)) {
res.status(NOT_FOUND_404).send();
return;
}
if (!Files.isRegularFile(filePath)) {
res.status(BAD_REQUEST_400).send("Not a file");
return;
}
Path filePath = storage.lookup(req.path().param("fname"));
ResponseHeaders headers = res.headers();
headers.contentType(MediaType.APPLICATION_OCTET_STREAM);
headers.put(Http.Header.CONTENT_DISPOSITION, ContentDisposition.builder()
Expand All @@ -108,7 +90,7 @@ private void upload(ServerRequest req, ServerResponse res) {
private void bufferedUpload(ServerRequest req, ServerResponse res) {
req.content().as(ReadableMultiPart.class).thenAccept(multiPart -> {
for (ReadableBodyPart part : multiPart.fields("file[]")) {
writeBytes(storage, part.filename(), part.as(byte[].class));
writeBytes(storage.create(part.filename()), part.as(byte[].class));
}
res.status(Http.Status.MOVED_PERMANENTLY_301);
res.headers().put(Http.Header.LOCATION, "/ui");
Expand All @@ -125,41 +107,22 @@ private void streamUpload(ServerRequest req, ServerResponse res) {
res.send();
}).forEach((part) -> {
if ("file[]".equals(part.name())) {
final ByteChannel channel = newByteChannel(storage, part.filename());
Multi.create(part.content())
final ByteChannel channel = newByteChannel(storage.create(part.filename()));
part.content()
.forEach(chunk -> writeChunk(channel, chunk))
.thenAccept(it -> closeChannel(channel));
}
});
}

private static Path createStorage() {
try {
return Files.createTempDirectory("fileupload");
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}

private static Stream<String> listFiles(Path storage) {
try {
return Files.walk(storage)
.filter(Files::isRegularFile)
.map(storage::relativize)
.map(Path::toString);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}

private static void writeBytes(Path storage, String fname, byte[] bytes) {
private static void writeBytes(Path file, byte[] bytes) {
try {
Files.write(storage.resolve(fname), bytes,
Files.write(file, bytes,
romain-grecourt marked this conversation as resolved.
Show resolved Hide resolved
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING);
} catch (IOException ex) {
throw new RuntimeException(ex);
throw new UncheckedIOException(ex);
}
}

Expand All @@ -169,7 +132,7 @@ private static void writeChunk(ByteChannel channel, DataChunk chunk) {
channel.write(byteBuffer);
}
} catch (IOException ex) {
throw new RuntimeException(ex);
throw new UncheckedIOException(ex);
} finally {
chunk.release();
}
Expand All @@ -179,18 +142,18 @@ private void closeChannel(ByteChannel channel) {
try {
channel.close();
} catch (IOException ex) {
throw new RuntimeException(ex);
throw new UncheckedIOException(ex);
}
}

private static ByteChannel newByteChannel(Path storage, String fname) {
private static ByteChannel newByteChannel(Path file) {
try {
return Files.newByteChannel(storage.resolve(fname),
return Files.newByteChannel(file,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING);
} catch (IOException ex) {
throw new RuntimeException(ex);
throw new UncheckedIOException(ex);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
*
* 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.media.multipart;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;

import io.helidon.webserver.BadRequestException;
import io.helidon.webserver.NotFoundException;

/**
* Simple bean to managed a directory based storage.
*/
public class FileStorage {

private final Path storageDir;

/**
* Create a new instance.
*/
public FileStorage() {
try {
storageDir = Files.createTempDirectory("fileupload");
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}

/**
* Get the storage directory.
*
* @return directory
*/
public Path storageDir() {
return storageDir;
}

/**
* Get the names of the files in the storage directory.
*
* @return Stream of file names
*/
public Stream<String> listFiles() {
try {
return Files.walk(storageDir)
.filter(Files::isRegularFile)
.map(storageDir::relativize)
.map(java.nio.file.Path::toString);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}

/**
* Create a new file in the storage.
*
* @param fname file name
* @return file
* @throws BadRequestException if the resolved file is not contained in the storage directory
*/
public Path create(String fname) {
Path file = storageDir.resolve(fname);
if (!file.getParent().equals(storageDir)) {
throw new BadRequestException("Invalid file name");
}
return file;
}

/**
* Lookup an existing file in the storage.
*
* @param fname file name
* @return file
* @throws NotFoundException If the resolved file does not exist
* @throws BadRequestException if the resolved file is not contained in the storage directory
*/
public Path lookup(String fname) {
Path file = storageDir.resolve(fname);
if (!file.getParent().equals(storageDir)) {
throw new BadRequestException("Invalid file name");
}
if (!Files.exists(file)) {
throw new NotFoundException("file not found");
}
if (!Files.isRegularFile(file)) {
throw new BadRequestException("Not a file");
}
return file;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,18 @@ static Routing createRouting() {
}

/**
* Executes the example.
*
* Application main entry point.
* @param args command line arguments.
*/
public static void main(String[] args) {
public static void main(final String[] args) {
startServer();
}

/**
* Start the server.
* @return the created {@link WebServer} instance
*/
static WebServer startServer() {
WebServer server = WebServer.builder(createRouting())
.port(8080)
.addMediaSupport(MultiPartSupport.create())
Expand All @@ -71,5 +77,8 @@ public static void main(String[] args) {
server.whenShutdown()
.thenRun(() -> System.out.println("WEB server is DOWN. Good bye!"));

return server;
}


}
Loading