diff --git a/.env.example b/.env.example index 20dac54..2d5ea2d 100644 --- a/.env.example +++ b/.env.example @@ -5,3 +5,4 @@ DATABASE_USERNAME=user S3_ENDPOINT=http://s3:9000 S3_ACCESS_KEY=minioadmin S3_SECRET_KEY=minioadmin +S3_BUCKET=composer-dev diff --git a/pom.xml b/pom.xml index 4655418..b5836eb 100644 --- a/pom.xml +++ b/pom.xml @@ -32,6 +32,7 @@ 3.2.4 4.0.0 + 3.2.3 @@ -87,6 +88,16 @@ spring-dotenv ${spring-dotenv.version} + + org.apache.tika + tika-core + ${apache-tika.version} + + + org.apache.tika + tika-parsers-standard-package + ${apache-tika.version} + org.postgresql diff --git a/src/main/java/com/bivashy/backend/composer/config/MimeDetectionConfig.java b/src/main/java/com/bivashy/backend/composer/config/MimeDetectionConfig.java new file mode 100644 index 0000000..84053c0 --- /dev/null +++ b/src/main/java/com/bivashy/backend/composer/config/MimeDetectionConfig.java @@ -0,0 +1,13 @@ +package com.bivashy.backend.composer.config; + +import org.apache.tika.Tika; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MimeDetectionConfig { + @Bean + public Tika tika() { + return new Tika(); + } +} diff --git a/src/main/java/com/bivashy/backend/composer/config/StorageS3Config.java b/src/main/java/com/bivashy/backend/composer/config/StorageS3Config.java index 8307caa..194f435 100644 --- a/src/main/java/com/bivashy/backend/composer/config/StorageS3Config.java +++ b/src/main/java/com/bivashy/backend/composer/config/StorageS3Config.java @@ -22,9 +22,10 @@ public class StorageS3Config { @Bean public S3Client s3Client() { return S3Client.builder() - .endpointOverride(URI.create(endpoint)) .region(Region.US_WEST_1) + .forcePathStyle(true) .credentialsProvider(() -> AwsBasicCredentials.create(accessKey, secretKey)) + .endpointOverride(URI.create(endpoint)) .build(); } diff --git a/src/main/java/com/bivashy/backend/composer/controller/S3Controller.java b/src/main/java/com/bivashy/backend/composer/controller/S3Controller.java new file mode 100644 index 0000000..2a88836 --- /dev/null +++ b/src/main/java/com/bivashy/backend/composer/controller/S3Controller.java @@ -0,0 +1,37 @@ +package com.bivashy.backend.composer.controller; + +import java.io.IOException; + +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import com.bivashy.backend.composer.service.AudioBlobStorageService.Blob; +import com.bivashy.backend.composer.service.AudioS3StorageService; + +@RestController +public class S3Controller { + private final AudioS3StorageService s3Service; + + public S3Controller(AudioS3StorageService s3Service) { + this.s3Service = s3Service; + } + + @PostMapping(path = "/upload", consumes = { MediaType.MULTIPART_FORM_DATA_VALUE }) + public String upload(@RequestPart MultipartFile document) throws IOException { + return s3Service.store(document.getBytes()); + } + + @GetMapping(path = "/read") + public ResponseEntity read(@RequestParam("document") String document) throws IOException { + Blob blob = s3Service.read(document); + return ResponseEntity.ok() + .contentType(blob.contentType()) + .body(blob.stream().readAllBytes()); + } +} diff --git a/src/main/java/com/bivashy/backend/composer/model/SourceProviders.java b/src/main/java/com/bivashy/backend/composer/model/SourceProviders.java new file mode 100644 index 0000000..b1e48af --- /dev/null +++ b/src/main/java/com/bivashy/backend/composer/model/SourceProviders.java @@ -0,0 +1,8 @@ +package com.bivashy.backend.composer.model; + +public class SourceProviders { + public static final String YOUTUBE = "YOUTUBE"; + public static final String LOCAL = "SOUNDCLOUD"; + public static final String EXTERNAL = "EXTERNAL"; + +} diff --git a/src/main/java/com/bivashy/backend/composer/model/SourceTypes.java b/src/main/java/com/bivashy/backend/composer/model/SourceTypes.java new file mode 100644 index 0000000..f416568 --- /dev/null +++ b/src/main/java/com/bivashy/backend/composer/model/SourceTypes.java @@ -0,0 +1,8 @@ +package com.bivashy.backend.composer.model; + +public class SourceTypes { + public static final String AUDIO = "VIDEO"; + public static final String PLAYLIST = "PLAYLIST"; + public static final String FILE = "FILE"; + public static final String URL = "URL"; +} diff --git a/src/main/java/com/bivashy/backend/composer/service/AudioBlobStorageService.java b/src/main/java/com/bivashy/backend/composer/service/AudioBlobStorageService.java new file mode 100644 index 0000000..c4e1b02 --- /dev/null +++ b/src/main/java/com/bivashy/backend/composer/service/AudioBlobStorageService.java @@ -0,0 +1,19 @@ +package com.bivashy.backend.composer.service; + +import java.io.IOException; +import java.io.InputStream; + +import org.springframework.http.MediaType; + +public interface AudioBlobStorageService { + String store(InputStream inputStream); + + String store(byte[] data); + + byte[] readRaw(String path) throws IOException; + + Blob read(String path); + + public record Blob(InputStream stream, MediaType contentType) { + } +} diff --git a/src/main/java/com/bivashy/backend/composer/service/AudioS3StorageService.java b/src/main/java/com/bivashy/backend/composer/service/AudioS3StorageService.java new file mode 100644 index 0000000..1161f29 --- /dev/null +++ b/src/main/java/com/bivashy/backend/composer/service/AudioS3StorageService.java @@ -0,0 +1,87 @@ +package com.bivashy.backend.composer.service; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.UUID; + +import org.apache.tika.Tika; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.InvalidMediaTypeException; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; + +import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +@Service +public class AudioS3StorageService implements AudioBlobStorageService { + private static final Logger logger = LoggerFactory.getLogger(AudioS3StorageService.class); + + private static final byte[] DEFAULT_BUFFER = new byte[8192]; + private final S3Client s3Client; + private final Tika tika; + @Value("${spring.s3.bucket}") + private String bucket; + + public AudioS3StorageService(S3Client s3Client, Tika tika) { + this.s3Client = s3Client; + this.tika = tika; + } + + @Override + public String store(InputStream inputStream) { + return store(new ByteArrayInputStream(DEFAULT_BUFFER).readAllBytes()); + } + + @Override + public String store(byte[] data) { + String objectKey = newObjectName(); + String contentType = detectContentType(data); + s3Client.putObject(PutObjectRequest.builder() + .bucket(bucket) + .key(objectKey) + .contentType(contentType) + .build(), RequestBody.fromBytes(data)); + return String.join("/", bucket, objectKey); + } + + @Override + public byte[] readRaw(String path) throws IOException { + return read(path).stream().readAllBytes(); + } + + @Override + public Blob read(String path) { + if (path.startsWith(bucket + "/")) { + path = path.substring(bucket.length()); + } + ResponseInputStream response = s3Client.getObject(GetObjectRequest.builder() + .bucket(bucket) + .key(path) + .build()); + + MediaType mediaType = MediaType.APPLICATION_OCTET_STREAM; + try { + mediaType = MediaType.parseMediaType(response.response().contentType()); + } catch (InvalidMediaTypeException e) { + logger.error("invalid media type", e); + } + return new Blob(response, mediaType); + } + + private String detectContentType(byte[] data) { + return tika.detect(data); + } + + private String newObjectName() { + return String.join("/", "audio", UUID.randomUUID().toString()); + } + +}