implement main functionality
This commit is contained in:
@ -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<ServerResponse> router() {
|
|
||||||
return RouterFunctions.route()
|
|
||||||
.GET()
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,102 @@
|
|||||||
package ua.com.dxrkness.controller;
|
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 {
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<ServerResponse> 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();
|
||||||
|
}
|
||||||
|
}
|
20
src/main/java/ua/com/dxrkness/function/Action.java
Normal file
20
src/main/java/ua/com/dxrkness/function/Action.java
Normal file
@ -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();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
11
src/main/java/ua/com/dxrkness/model/DirectoryPath.java
Normal file
11
src/main/java/ua/com/dxrkness/model/DirectoryPath.java
Normal file
@ -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
|
||||||
|
) { }
|
@ -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<URI> {
|
||||||
|
@Override
|
||||||
|
public URI deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException {
|
||||||
|
return URI.create(jsonParser.getText());
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,24 @@
|
|||||||
package ua.com.dxrkness.service;
|
package ua.com.dxrkness.service;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.Part;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
import software.amazon.awssdk.core.sync.RequestBody;
|
import software.amazon.awssdk.core.sync.RequestBody;
|
||||||
import software.amazon.awssdk.services.s3.S3Client;
|
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 software.amazon.awssdk.services.s3.model.S3Object;
|
||||||
|
import ua.com.dxrkness.function.Action;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.Reader;
|
import java.net.URI;
|
||||||
import java.nio.channels.*;
|
import java.nio.channels.Channels;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class S3Service {
|
public class S3Service {
|
||||||
@ -20,40 +26,101 @@ public class S3Service {
|
|||||||
private final String bucketName;
|
private final String bucketName;
|
||||||
|
|
||||||
public S3Service(S3Client client,
|
public S3Service(S3Client client,
|
||||||
@Value("s3.bucket-name") String bucketName) {
|
@Value("${s3.bucket-name}") String bucketName) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.bucketName = bucketName;
|
this.bucketName = bucketName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void createDirectory(Path path) {
|
public void createDirectory(URI path) {
|
||||||
throw new UnsupportedOperationException();
|
final var pathString = Optional.of(path)
|
||||||
}
|
.map(URI::toString)
|
||||||
|
.filter(p -> p.charAt(p.length() - 1) == '/')
|
||||||
|
.orElse(path.toString() + '/');
|
||||||
|
|
||||||
public Boolean delete(Path path) {
|
Action createDir = () -> client.putObject(b -> b
|
||||||
return client
|
|
||||||
.deleteObject(b -> b.bucket(bucketName).key(path.toString()).build())
|
|
||||||
.deleteMarker();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<S3Object> listing(Path path) {
|
|
||||||
return client
|
|
||||||
.listObjectsV2(b -> b.bucket(bucketName).build())
|
|
||||||
.contents();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean uploadFile(MultipartFile file) throws IOException {
|
|
||||||
return client
|
|
||||||
.putObject(b -> b
|
|
||||||
.bucket(bucketName)
|
.bucket(bucketName)
|
||||||
.key(file.getName()),
|
.key(pathString)
|
||||||
RequestBody.fromInputStream(file.getInputStream(), file.getSize()))
|
.build(),
|
||||||
.bucketKeyEnabled();
|
RequestBody.empty());
|
||||||
|
Action waitForCreation = () -> client.waiter()
|
||||||
|
.waitUntilObjectExists(b -> b
|
||||||
|
.bucket(bucketName)
|
||||||
|
.key(pathString)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
createDir.andThen(waitForCreation).execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Reader downloadFile(String key, Path destinationPath) {
|
public void delete(URI path) {
|
||||||
return Channels.newReader(
|
Function<Void, List<ObjectIdentifier>> listAllObjectInDirectory = (ignored) ->
|
||||||
Channels.newChannel(client
|
client.listObjectsV2(b -> b
|
||||||
.getObject(b -> b.bucket(bucketName).key(key))),
|
.prefix(path.toString())
|
||||||
StandardCharsets.UTF_8);
|
.bucket(bucketName)
|
||||||
|
.build())
|
||||||
|
.contents().stream()
|
||||||
|
.map(S3Object::key)
|
||||||
|
.map(key -> ObjectIdentifier.builder()
|
||||||
|
.key(key)
|
||||||
|
.build())
|
||||||
|
.toList();
|
||||||
|
Consumer<List<ObjectIdentifier>> 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 List<String> 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<String> listBuckets() {
|
||||||
|
return client.listBuckets()
|
||||||
|
.buckets()
|
||||||
|
.stream()
|
||||||
|
.map(Bucket::name)
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user