diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/client/GridFSClient.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/client/GridFSClient.java new file mode 100644 index 00000000000..e28b5f4ceba --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/client/GridFSClient.java @@ -0,0 +1,93 @@ +/* + * Copyright 2017-2023 original authors + * + * 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 + * + * https://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.micronaut.data.mongodb.client; + +import com.mongodb.client.gridfs.GridFSBucket; +import com.mongodb.client.gridfs.model.GridFSFile; +import com.mongodb.client.gridfs.model.GridFSUploadOptions; +import com.mongodb.client.model.Filters; +import io.micronaut.context.annotation.Bean; +import io.micronaut.data.exceptions.DataAccessException; +import org.bson.Document; +import org.bson.types.ObjectId; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Optional; + +/** + * Client for a given (or default) GridFS bucket. + */ +@Bean +public class GridFSClient { + + /** Bucket backing the client instance. */ + private final GridFSBucket bucket; + + public GridFSClient(GridFSBucket bucket) { + this.bucket = bucket; + } + + /** + * Upload a file to GridFS bucket. + * + * @param path Path to the temporary file + * @param fileName Filename for the uploaded file + * @param metadata Metadata for the uploaded file + * @return ObjectId for the uploaded file + */ + public ObjectId uploadFile(Path path, String fileName, Map metadata) { + + Document document = new Document(); + if (metadata != null) { + document.putAll(metadata); + } + + try (InputStream streamToUploadFrom = new FileInputStream(path.toFile())) { + GridFSUploadOptions options = new GridFSUploadOptions() + .metadata(document); + return bucket.uploadFromStream(fileName, streamToUploadFrom, options); + } catch (IOException e) { + throw new DataAccessException(e.getMessage(), e); + } + } + + /** + * Download a file for given id. + * + * @param id ObjectId of the given file + * @return Optional Path for the downloaded file + */ + public Optional downloadFile(ObjectId id) { + try { + GridFSFile file = bucket.find(Filters.eq(id)).first(); + if (file != null) { + Path tempFile = Files.createTempFile("gridfs-" + id + "-", ".tmp"); + FileOutputStream destination = new FileOutputStream(tempFile.toFile()); + bucket.downloadToStream(id, destination); + destination.flush(); + return Optional.of(tempFile); + } else { + return Optional.empty(); + } + } catch (IOException e) { + throw new DataAccessException(e.getMessage(), e); + } + } + +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/GridFSConfiguration.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/GridFSConfiguration.java new file mode 100644 index 00000000000..78ee658c51c --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/GridFSConfiguration.java @@ -0,0 +1,98 @@ +/* + * Copyright 2017-2023 original authors + * + * 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 + * + * https://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.micronaut.data.mongodb.conf; + +import com.mongodb.ReadConcern; +import com.mongodb.ReadConcernLevel; +import com.mongodb.ReadPreference; +import com.mongodb.WriteConcern; +import io.micronaut.context.annotation.ConfigurationProperties; + +/** + * Top level configuration for GridFS. + */ +@ConfigurationProperties(MongoDataConfiguration.PREFIX + GridFSConfiguration.GRID_FS) +public final class GridFSConfiguration { + + public static final String GRID_FS = ".gridfs"; + public static final String DEFAULT_BUCKET_NAME = "files"; + public static final int DEFAULT_CHUNK_SIZE = 1048576; + + /** + * Chunk size for the bucket. Default is 1 MB. + */ + private int chunkSize = DEFAULT_CHUNK_SIZE; + + /** + * Database for the bucket. If not specified, the value of micronaut.data.mongodb.gridfs.database is used. + */ + private String database; + + /** Bucket Name. */ + private String bucketName; + + private ReadConcernLevel readConcern; + private WriteConcernOptions writeConcern; + private ReadPreferenceOptions readPreference; + + public void setReadConcern(ReadConcernLevel readConcern) { + this.readConcern = readConcern; + } + + public void setWriteConcern(WriteConcernOptions writeConcern) { + this.writeConcern = writeConcern; + } + + public void setReadPreference(ReadPreferenceOptions readPreference) { + this.readPreference = readPreference; + } + + public ReadConcern getReadConcern() { + return (this.readConcern != null) ? new ReadConcern(this.readConcern) : null; + } + + public WriteConcern getWriteConcern() { + return (this.writeConcern != null) ? this.writeConcern.getValue() : null; + } + + public ReadPreference getReadPreference() { + return (this.readPreference != null) ? this.readPreference.getValue() : null; + } + + public int getChunkSize() { + return chunkSize; + } + + public void setChunkSize(int chunkSize) { + this.chunkSize = chunkSize; + } + + public String getDatabase() { + return database; + } + + public void setDatabase(String database) { + this.database = database; + } + + public String getBucketName() { + return bucketName; + } + + public void setBucketName(String bucketName) { + this.bucketName = bucketName; + } +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/NamedBucketConfiguration.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/NamedBucketConfiguration.java new file mode 100644 index 00000000000..0154d9e3a20 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/NamedBucketConfiguration.java @@ -0,0 +1,99 @@ +/* + * Copyright 2017-2023 original authors + * + * 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 + * + * https://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.micronaut.data.mongodb.conf; + +import com.mongodb.ReadConcern; +import com.mongodb.ReadConcernLevel; +import com.mongodb.ReadPreference; +import com.mongodb.WriteConcern; +import io.micronaut.context.annotation.EachProperty; +import io.micronaut.context.annotation.Parameter; + +import javax.validation.constraints.NotBlank; + +/** + * Configuration for class GriDFS bucket. + */ +@EachProperty(value = MongoDataConfiguration.PREFIX + GridFSConfiguration.GRID_FS + NamedBucketConfiguration.BUCKETS) +public final class NamedBucketConfiguration { + + public static final String BUCKETS = ".buckets"; + + /** + * Required. Name of the bucket + */ + @NotBlank + private String name; + + /** + * Database for the bucket. If not specified, the value of micronaut.data.mongodb.gridfs.database is used. + */ + private String database; + + /** + * Chunk size for the bucket. Default is 1 MB + */ + private int chunkSize; + + private ReadConcernLevel readConcern; + private WriteConcernOptions writeConcern; + private ReadPreferenceOptions readPreference; + + public NamedBucketConfiguration(@Parameter String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public int getChunkSize() { + return chunkSize; + } + + public void setChunkSize(int chunkSize) { + this.chunkSize = chunkSize; + } + + public String getDatabase() { + return database; + } + + public void setReadConcern(ReadConcernLevel readConcern) { + this.readConcern = readConcern; + } + + public void setWriteConcern(WriteConcernOptions writeConcern) { + this.writeConcern = writeConcern; + } + + public void setReadPreference(ReadPreferenceOptions readPreference) { + this.readPreference = readPreference; + } + + public ReadConcern getReadConcern() { + return (this.readConcern != null) ? new ReadConcern(this.readConcern) : null; + } + + public WriteConcern getWriteConcern() { + return (this.writeConcern != null) ? this.writeConcern.getValue() : null; + } + + public ReadPreference getReadPreference() { + return (this.readPreference != null) ? this.readPreference.getValue() : null; + } + +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/ReadPreferenceOptions.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/ReadPreferenceOptions.java new file mode 100644 index 00000000000..bc801a32a64 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/ReadPreferenceOptions.java @@ -0,0 +1,37 @@ +/* + * Copyright 2017-2023 original authors + * + * 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 + * + * https://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.micronaut.data.mongodb.conf; + +import com.mongodb.ReadPreference; + +/** Enum for ReadPreference value. */ +public enum ReadPreferenceOptions { + primary(ReadPreference.primary()), + secondary(ReadPreference.secondary()), + secondary_preferred(ReadPreference.secondaryPreferred()), + primary_preferred(ReadPreference.primaryPreferred()), + nearest(ReadPreference.nearest()); + + private final ReadPreference value; + + ReadPreferenceOptions(ReadPreference value) { + this.value = value; + } + + public ReadPreference getValue() { + return this.value; + } +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/WriteConcernOptions.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/WriteConcernOptions.java new file mode 100644 index 00000000000..eae90548dad --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/conf/WriteConcernOptions.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017-2023 original authors + * + * 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 + * + * https://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.micronaut.data.mongodb.conf; + +import com.mongodb.WriteConcern; + +/** + * Enum for WriteConcern value. + */ +public enum WriteConcernOptions { + w1(WriteConcern.W1), + w2(WriteConcern.W2), + w3(WriteConcern.W3), + acknowledged(WriteConcern.ACKNOWLEDGED), + unacknowledged(WriteConcern.UNACKNOWLEDGED), + journaled(WriteConcern.JOURNALED), + majority(WriteConcern.MAJORITY); + + private final WriteConcern value; + + WriteConcernOptions(WriteConcern value) { + this.value = value; + } + + public WriteConcern getValue() { + return this.value; + } +} diff --git a/data-mongodb/src/main/java/io/micronaut/data/mongodb/database/GridFSClientFactory.java b/data-mongodb/src/main/java/io/micronaut/data/mongodb/database/GridFSClientFactory.java new file mode 100644 index 00000000000..233548e8f23 --- /dev/null +++ b/data-mongodb/src/main/java/io/micronaut/data/mongodb/database/GridFSClientFactory.java @@ -0,0 +1,101 @@ +/* + * Copyright 2017-2023 original authors + * + * 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 + * + * https://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.micronaut.data.mongodb.database; + +import com.mongodb.ReadConcern; +import com.mongodb.ReadPreference; +import com.mongodb.WriteConcern; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.gridfs.GridFSBucket; +import com.mongodb.client.gridfs.GridFSBuckets; +import io.micronaut.context.annotation.EachBean; +import io.micronaut.context.annotation.Factory; +import io.micronaut.context.annotation.Primary; +import io.micronaut.context.exceptions.BeanInstantiationException; +import io.micronaut.data.mongodb.conf.GridFSConfiguration; +import io.micronaut.data.mongodb.conf.NamedBucketConfiguration; +import io.micronaut.data.mongodb.client.GridFSClient; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import static io.micronaut.data.mongodb.conf.GridFSConfiguration.DEFAULT_BUCKET_NAME; + +/** Factory class for GridFSClient instances. */ +@Factory +public final class GridFSClientFactory { + + private final MongoClient mongoClient; + private final GridFSConfiguration gridFSConfiguration; + + @Inject + public GridFSClientFactory(MongoClient mongoClient, GridFSConfiguration gridFSConfiguration) { + this.mongoClient = mongoClient; + this.gridFSConfiguration = gridFSConfiguration; + } + + @Primary + @Singleton + public GridFSClient defaultGridFSBucket() { + + String bucketName = gridFSConfiguration.getBucketName() != null ? gridFSConfiguration.getBucketName() : DEFAULT_BUCKET_NAME; + String databaseName = gridFSConfiguration.getDatabase(); + int chunkSize = gridFSConfiguration.getChunkSize(); + + ReadPreference readPreference = gridFSConfiguration.getReadPreference(); + ReadConcern readConcern = gridFSConfiguration.getReadConcern(); + WriteConcern writeConcern = gridFSConfiguration.getWriteConcern(); + return create(bucketName, databaseName, chunkSize, readPreference, readConcern, writeConcern); + } + + @EachBean(NamedBucketConfiguration.class) + @Singleton + public GridFSClient namedGridFSBucket(NamedBucketConfiguration bucketConfiguration) { + + String bucketName = bucketConfiguration.getName(); + String databaseName = bucketConfiguration.getDatabase() != null ? bucketConfiguration.getDatabase() : gridFSConfiguration.getDatabase(); + int chunkSize = bucketConfiguration.getChunkSize() == 0 ? gridFSConfiguration.getChunkSize() : bucketConfiguration.getChunkSize(); + + ReadPreference readPreference = bucketConfiguration.getReadPreference() != null ? bucketConfiguration.getReadPreference() : gridFSConfiguration.getReadPreference(); + ReadConcern readConcern = bucketConfiguration.getReadConcern() != null ? bucketConfiguration.getReadConcern() : gridFSConfiguration.getReadConcern(); + WriteConcern writeConcern = bucketConfiguration.getWriteConcern() != null ? bucketConfiguration.getWriteConcern() : gridFSConfiguration.getWriteConcern(); + + return create(bucketName, databaseName, chunkSize, readPreference, readConcern, writeConcern); + } + + private GridFSClient create(String bucketName, String databaseName, int chunkSize, ReadPreference readPreference, ReadConcern readConcern, WriteConcern writeConcern) { + if (databaseName == null) { + throw new BeanInstantiationException("Could not determine database name for GridFS bucket: " + bucketName); + } + + MongoDatabase database = mongoClient.getDatabase(databaseName); + GridFSBucket gridFSBucket = GridFSBuckets.create(database, bucketName).withChunkSizeBytes(chunkSize); + + if (readConcern != null) { + gridFSBucket = gridFSBucket.withReadConcern(readConcern); + } + + if (readPreference != null) { + gridFSBucket = gridFSBucket.withReadPreference(readPreference); + } + + if (writeConcern != null) { + gridFSBucket = gridFSBucket.withWriteConcern(writeConcern); + } + + return new GridFSClient(gridFSBucket); + } +} diff --git a/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/GridFSClientSpec.groovy b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/GridFSClientSpec.groovy new file mode 100644 index 00000000000..2ead92d866f --- /dev/null +++ b/data-mongodb/src/test/groovy/io/micronaut/data/document/mongodb/GridFSClientSpec.groovy @@ -0,0 +1,52 @@ +package io.micronaut.data.document.mongodb + +import io.micronaut.context.ApplicationContext +import io.micronaut.data.mongodb.client.GridFSClient +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import org.bson.UuidRepresentation +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +import java.nio.charset.StandardCharsets +import java.nio.file.Files + +@MicronautTest +class GridFSClientSpec extends Specification implements MongoTestPropertyProvider { + + @AutoCleanup + @Shared + ApplicationContext applicationContext = ApplicationContext.run(getTestProperties()) + + + void 'upload file to default bucket'() { + given: + GridFSClient gridFSClient = applicationContext.getBean(GridFSClient) + def tempFile = File.createTempFile("temp", ".tmp") + tempFile.write("Hello world!") + tempFile.deleteOnExit() + + when: + def objectId = gridFSClient.uploadFile(tempFile.toPath(), "test.txt", Map.of("hello", "world")) + def downloadedFile = gridFSClient.downloadFile(objectId) + def fileContent = "" + if(downloadedFile.isPresent()){ + fileContent = Files.readString(downloadedFile.get(), StandardCharsets.UTF_8) + } + then: + objectId != null + downloadedFile.isPresent() + fileContent == "Hello world!" + + } + + Map getTestProperties() { + return [ + "micronaut.data.mongodb.driver-type" : "sync", + 'micronaut.data.mongodb.create-collections': 'true', + 'micronaut.data.mongodb.gridfs.database' : 'test', + 'mongodb.uuid-representation' : UuidRepresentation.STANDARD.name(), + 'mongodb.package-names' : getPackageNames() + ] + } +} diff --git a/src/main/docs/guide/mongo/mongoGridFS.adoc b/src/main/docs/guide/mongo/mongoGridFS.adoc new file mode 100644 index 00000000000..566402cc70c --- /dev/null +++ b/src/main/docs/guide/mongo/mongoGridFS.adoc @@ -0,0 +1,121 @@ +== GridFS support for MongoDB + +This package adds support for +https://www.mongodb.com/docs/manual/core/gridfs/[MongoDB GridFS] + +=== Configuration with a single bucket + +Base configuration key is `micronaut.data.mongodb.gridfs` + +[width="100%",cols="33%,5%,9%,53%",options="header",] +|======================================================================= +|Configuration |Type |Default Value |Notes +|`micronaut.data.mongodb.gridfs.database` |String | |Database Name + +|`micronaut.data.mongodb.gridfs.bucket-name` |String |`files` +|https://www.mongodb.com/docs/manual/core/gridfs/#gridfs-collections[Bucket +Name] + +|`micronaut.data.mongodb.gridfs.chunk-size` |Integer |`1048576` |Chunk +size to split the uploaded file + +|`micronaut.data.mongodb.gridfs.read-concern` |Enum | |Values: local, +majority, linearizable, snapshot, available + +|`micronaut.data.mongodb.gridfs.read-preference` |Enum | |Values: +primary, secondary, secondary_preferred, primary_preferred, nearest + +|`micronaut.data.mongodb.gridfs.write-concern` |Enum | |Values: w1, w2, +w3, acknowledged, unacknowledged, journaled, majority +|======================================================================= + +==== Example configuration + +[source,yaml] +---- +micronaut: + data: + mongodb: + gridfs: + database: + bucket-name: + chunk-size: 10000 + read-concern: majority + read-preference: primary + write-concern: w2 +---- + +=== Configuration with multiple buckets + +Multiple buckets can be defined under the `buckets` map. Individual +bucket can be configured the same as default bucket. + +[width="100%",cols="38%,5%,9%,48%",options="header",] +|======================================================================= +|Configuration |Type |Default Value |Notes +|`micronaut.data.mongodb.gridfs.buckets.*.database` |String | |Database +Name + +|`micronaut.data.mongodb.gridfs.buckets.*.chunk-size` |Integer +|`1048576` |Chunk size to split the uploaded file + +|`micronaut.data.mongodb.gridfs.buckets.*.read-concern` |Enum | |Values: +local, majority, linearizable, snapshot, available + +|`micronaut.data.mongodb.gridfs.buckets.*.read-preference` |Enum | +|Values: primary, secondary, secondary_preferred, primary_preferred, +nearest + +|`micronaut.data.mongodb.gridfs.buckets.*.write-concern` |Enum | +|Values: w1, w2, w3, acknowledged, unacknowledged, journaled, majority +|======================================================================= + +==== Example configuration + +[source,yaml] +---- +micronaut: + data: + mongodb: + gridfs: + buckets: + bucket-name: # No overrides, uses default values + profile-pictures: # Bucket with overridden values + database: app_db + chunk-size: 2097152 + +---- + +=== Usage + +Bean of type `GridFSClient` is available for injection. Use the `@Named` +annotation for getting a instance for a specific bucket. + +==== Example code + +[source,java] +---- +import jakarta.inject.Inject; +import jakarta.inject.Named; +import io.micronaut.data.mongodb.client.GridFSClient; + +public class MongoFileStorage { + + private final GridFSClient defaultBucketClient; + private final GridFSClient namedBucketClient; + + @Inject + public MongoFileStorage(GridFSClient defaultBucketClient, + @Named("profile-pictures") GridFSClient namedBucketClient) { + this.defaultBucketClient = defaultBucketClient; + this.namedBucketClient = namedBucketClient; + } + + public ObjectId yourUploadMethod(Path fileToUpload, String fileName, Map metadata) { + return defaultBucketClient.uploadFile(fileToUpload, fileName, metadata); + } + public Optional downloadFile(ObjectId id) { + return defaultBucketClient.downloadFile(id); + } +} +----