diff --git a/src/main/java/ua/com/dxrkness/controller/S3ControllerConfig.java b/src/main/java/ua/com/dxrkness/controller/S3ControllerConfig.java deleted file mode 100644 index 6a6802f..0000000 --- a/src/main/java/ua/com/dxrkness/controller/S3ControllerConfig.java +++ /dev/null @@ -1,17 +0,0 @@ -package ua.com.dxrkness.controller; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.function.RouterFunction; -import org.springframework.web.servlet.function.RouterFunctions; -import org.springframework.web.servlet.function.ServerResponse; - -@Configuration -public class S3ControllerConfig { - @Bean - public RouterFunction router() { - return RouterFunctions.route() - .GET() - .build(); - } -} \ No newline at end of file diff --git a/src/main/java/ua/com/dxrkness/controller/S3FileHandler.java b/src/main/java/ua/com/dxrkness/controller/S3FileHandler.java index 4fd837f..fc1abad 100644 --- a/src/main/java/ua/com/dxrkness/controller/S3FileHandler.java +++ b/src/main/java/ua/com/dxrkness/controller/S3FileHandler.java @@ -1,4 +1,102 @@ package ua.com.dxrkness.controller; +import jakarta.servlet.ServletException; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.function.ServerRequest; +import org.springframework.web.servlet.function.ServerResponse; +import software.amazon.awssdk.services.s3.model.S3Exception; +import ua.com.dxrkness.model.DirectoryPath; +import ua.com.dxrkness.service.S3Service; + +import java.io.IOException; +import java.net.URI; +import java.util.Optional; + +@Component public class S3FileHandler { + private final S3Service service; + + public S3FileHandler(S3Service service) { + this.service = service; + } + + public ServerResponse createDirectory(ServerRequest request) throws ServletException, IOException { + return Optional.of(request.body(DirectoryPath.class)) + .map(DirectoryPath::path) + .map(body -> { + try { + service.createDirectory(body); + return ServerResponse.created(body).build(); + } catch (S3Exception exception) { + return ServerResponse.badRequest().build(); + } + }).get(); + } + + public ServerResponse downloadFile(ServerRequest request) throws ServletException, IOException { + return Optional.of(request.body(DirectoryPath.class)) + .map(DirectoryPath::path) + .map(URI::toString) + .map(service::downloadFile) + .map(reader -> { + try (reader) { + return ServerResponse + .ok() + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(reader.readAllBytes()); + } catch (IOException e) { + return ServerResponse.badRequest().build(); + } + }).get(); + } + + public ServerResponse uploadFile(ServerRequest request) throws ServletException, IOException { + return Optional.of(request.multipartData()) + .map(data -> data.getFirst("file")) + .map(file -> { + try { + final var path = request.param("path") + .map(URI::create) + .orElse(URI.create("")); + + service.uploadFile(file, path); + return ServerResponse + .created(URI.create("/files")) + .body(new DirectoryPath( + path.resolve( + file.getSubmittedFileName() + ) + )); + } catch (IOException e) { + return ServerResponse.badRequest().build(); + } + }) + .get(); + } + + public ServerResponse listing(ServerRequest request) throws ServletException, IOException { + return Optional.of(request.body(DirectoryPath.class)) + .map(key -> service.listing(key.path())) + .map(objects -> ServerResponse.ok().body(objects)) + .get(); + } + + public ServerResponse delete(ServerRequest request) throws ServletException, IOException { + return Optional.of(request.body(DirectoryPath.class)) + .map(DirectoryPath::path) + .map(path -> { + try { + service.delete(path); + return ServerResponse.noContent().build(); + } catch (S3Exception e) { + return ServerResponse.badRequest().build(); + } + }) + .get(); + } + + public ServerResponse listBuckets(ServerRequest request) { + return ServerResponse.ok().body(service.listBuckets()); + } } diff --git a/src/main/java/ua/com/dxrkness/controller/S3FileHandlerConfig.java b/src/main/java/ua/com/dxrkness/controller/S3FileHandlerConfig.java new file mode 100644 index 0000000..d1844cd --- /dev/null +++ b/src/main/java/ua/com/dxrkness/controller/S3FileHandlerConfig.java @@ -0,0 +1,28 @@ +package ua.com.dxrkness.controller; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; + +@Configuration +public class S3FileHandlerConfig { + @Bean + public RouterFunction router(S3FileHandler handler) { + return RouterFunctions.route() + .path("/files", b -> b + .GET(handler::downloadFile) + .POST(handler::uploadFile) + ) + .path("/dirs", b -> b + .GET(handler::listing) + .POST(handler::createDirectory) + ) + .path("/common", b -> b + .GET(handler::listBuckets) + .DELETE(handler::delete) + ) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/ua/com/dxrkness/function/Action.java b/src/main/java/ua/com/dxrkness/function/Action.java new file mode 100644 index 0000000..6231dcf --- /dev/null +++ b/src/main/java/ua/com/dxrkness/function/Action.java @@ -0,0 +1,20 @@ +package ua.com.dxrkness.function; + +@FunctionalInterface +public interface Action { + void execute(); + + default Action andThen(Action after) { + return () -> { + this.execute(); + after.execute(); + }; + } + + default Action compose(Action before) { + return () -> { + before.execute(); + this.execute(); + }; + } +} diff --git a/src/main/java/ua/com/dxrkness/model/DirectoryPath.java b/src/main/java/ua/com/dxrkness/model/DirectoryPath.java new file mode 100644 index 0000000..e9b436c --- /dev/null +++ b/src/main/java/ua/com/dxrkness/model/DirectoryPath.java @@ -0,0 +1,11 @@ +package ua.com.dxrkness.model; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import ua.com.dxrkness.model.serialization.URIDeserializer; + +import java.net.URI; + +public record DirectoryPath( + @JsonDeserialize(using = URIDeserializer.class) + URI path +) { } \ No newline at end of file diff --git a/src/main/java/ua/com/dxrkness/model/serialization/URIDeserializer.java b/src/main/java/ua/com/dxrkness/model/serialization/URIDeserializer.java new file mode 100644 index 0000000..7bb71e4 --- /dev/null +++ b/src/main/java/ua/com/dxrkness/model/serialization/URIDeserializer.java @@ -0,0 +1,16 @@ +package ua.com.dxrkness.model.serialization; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.net.URI; + +public class URIDeserializer extends JsonDeserializer { + @Override + public URI deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException { + return URI.create(jsonParser.getText()); + } +} \ No newline at end of file diff --git a/src/main/java/ua/com/dxrkness/service/S3Service.java b/src/main/java/ua/com/dxrkness/service/S3Service.java index ae755be..b2b072b 100644 --- a/src/main/java/ua/com/dxrkness/service/S3Service.java +++ b/src/main/java/ua/com/dxrkness/service/S3Service.java @@ -1,18 +1,24 @@ package ua.com.dxrkness.service; +import jakarta.servlet.http.Part; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.Bucket; +import software.amazon.awssdk.services.s3.model.ObjectIdentifier; import software.amazon.awssdk.services.s3.model.S3Object; +import ua.com.dxrkness.function.Action; +import java.io.BufferedInputStream; import java.io.IOException; -import java.io.Reader; -import java.nio.channels.*; +import java.net.URI; +import java.nio.channels.Channels; import java.nio.charset.StandardCharsets; -import java.nio.file.Path; import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; @Service public class S3Service { @@ -20,40 +26,101 @@ public class S3Service { private final String bucketName; public S3Service(S3Client client, - @Value("s3.bucket-name") String bucketName) { + @Value("${s3.bucket-name}") String bucketName) { this.client = client; this.bucketName = bucketName; } - public void createDirectory(Path path) { - throw new UnsupportedOperationException(); + public void createDirectory(URI path) { + final var pathString = Optional.of(path) + .map(URI::toString) + .filter(p -> p.charAt(p.length() - 1) == '/') + .orElse(path.toString() + '/'); + + Action createDir = () -> client.putObject(b -> b + .bucket(bucketName) + .key(pathString) + .build(), + RequestBody.empty()); + Action waitForCreation = () -> client.waiter() + .waitUntilObjectExists(b -> b + .bucket(bucketName) + .key(pathString) + .build()); + + createDir.andThen(waitForCreation).execute(); } - public Boolean delete(Path path) { - return client - .deleteObject(b -> b.bucket(bucketName).key(path.toString()).build()) - .deleteMarker(); - } - - public List listing(Path path) { - return client - .listObjectsV2(b -> b.bucket(bucketName).build()) - .contents(); - } - - public Boolean uploadFile(MultipartFile file) throws IOException { - return client - .putObject(b -> b + public void delete(URI path) { + Function> listAllObjectInDirectory = (ignored) -> + client.listObjectsV2(b -> b + .prefix(path.toString()) .bucket(bucketName) - .key(file.getName()), - RequestBody.fromInputStream(file.getInputStream(), file.getSize())) - .bucketKeyEnabled(); + .build()) + .contents().stream() + .map(S3Object::key) + .map(key -> ObjectIdentifier.builder() + .key(key) + .build()) + .toList(); + Consumer> deleteObjects = + keys -> client.deleteObjects(b -> b + .bucket(bucketName) + .delete(b1 -> b1 + .objects(keys))); + Action waitForDeletion = () -> client.waiter() + .waitUntilObjectNotExists(b -> b + .bucket(bucketName) + .key(path.toString()) + .build()); + + listAllObjectInDirectory + .andThen(keys -> { + deleteObjects.accept(keys); + return null; + }) + .andThen(n -> { + waitForDeletion.execute(); + return null; + }) + .apply(null); } - public Reader downloadFile(String key, Path destinationPath) { - return Channels.newReader( - Channels.newChannel(client - .getObject(b -> b.bucket(bucketName).key(key))), - StandardCharsets.UTF_8); + public List listing(URI path) { + return client + .listObjectsV2(b -> b + .bucket(bucketName) + .prefix(path.toString()) + .build()) + .contents() + .stream() + .map(S3Object::key) + .filter(filePath -> !filePath.equals(path.toString())) + .toList(); + } + + public void uploadFile(Part file, URI destinationPath) throws IOException { + client.putObject(b -> b + .bucket(bucketName) + .key(destinationPath + .resolve(file.getSubmittedFileName()) + .toString()), + RequestBody.fromInputStream(file.getInputStream(), file.getSize())); + } + + public BufferedInputStream downloadFile(String key) { + return new BufferedInputStream( + Channels.newInputStream( + Channels.newChannel(client.getObject(b -> b + .bucket(bucketName) + .key(key))))); + } + + public List listBuckets() { + return client.listBuckets() + .buckets() + .stream() + .map(Bucket::name) + .toList(); } } \ No newline at end of file