io.camunda.document.store.gcp.GcpDocumentStore Maven / Gradle / Ivy
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
* one or more contributor license agreements. See the NOTICE file distributed
* with this work for additional information regarding copyright ownership.
* Licensed under the Camunda License 1.0. You may not use this file
* except in compliance with the Camunda License 1.0.
*/
package io.camunda.document.store.gcp;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.cloud.storage.Blob;
import com.google.cloud.storage.BlobId;
import com.google.cloud.storage.BlobInfo;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
import io.camunda.document.api.DocumentContent;
import io.camunda.document.api.DocumentCreationRequest;
import io.camunda.document.api.DocumentError;
import io.camunda.document.api.DocumentError.UnknownDocumentError;
import io.camunda.document.api.DocumentLink;
import io.camunda.document.api.DocumentMetadataModel;
import io.camunda.document.api.DocumentReference;
import io.camunda.document.api.DocumentStore;
import io.camunda.zeebe.util.Either;
import java.nio.channels.Channels;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
public class GcpDocumentStore implements DocumentStore {
private static final String METADATA_PROCESS_DEFINITION_ID = "camunda.processDefinitionId";
private static final String METADATA_PROCESS_INSTANCE_KEY = "camunda.processInstanceKey";
private final String bucketName;
private final String prefix;
private final Storage storage;
private final ObjectMapper objectMapper;
private final ExecutorService executor;
public GcpDocumentStore(
final String bucketName, final String prefix, final ExecutorService executor) {
this(bucketName, prefix, new ObjectMapper(), executor);
}
public GcpDocumentStore(
final String bucketName,
final String prefix,
final ObjectMapper objectMapper,
final ExecutorService executor) {
this.bucketName = bucketName;
this.prefix = prefix;
storage = StorageOptions.getDefaultInstance().getService();
this.objectMapper = objectMapper;
this.executor = executor;
}
public GcpDocumentStore(
final String bucketName,
final String prefix,
final Storage storage,
final ObjectMapper objectMapper,
final ExecutorService executor) {
this.bucketName = bucketName;
this.prefix = prefix;
this.storage = storage;
this.objectMapper = objectMapper;
this.executor = executor;
}
@Override
public CompletableFuture> createDocument(
final DocumentCreationRequest request) {
return CompletableFuture.supplyAsync(() -> createDocumentInternal(request), executor);
}
@Override
public CompletableFuture> getDocument(
final String documentId) {
return CompletableFuture.supplyAsync(() -> getDocumentContentInternal(documentId), executor);
}
@Override
public CompletableFuture> deleteDocument(final String documentId) {
return CompletableFuture.supplyAsync(() -> deleteDocumentInternal(documentId), executor);
}
@Override
public CompletableFuture> createLink(
final String documentId, final long durationInMillis) {
return CompletableFuture.supplyAsync(
() -> createLinkInternal(documentId, durationInMillis), executor);
}
private Either createDocumentInternal(
final DocumentCreationRequest request) {
final String documentId =
Optional.ofNullable(request.documentId()).orElse(UUID.randomUUID().toString());
final String fullBlobName = getFullBlobName(documentId);
final String fileName = Optional.ofNullable(request.metadata().fileName()).orElse(documentId);
final Blob existingBlob;
try {
existingBlob = storage.get(bucketName, fullBlobName);
} catch (final Exception e) {
return Either.left(new UnknownDocumentError(e));
}
if (existingBlob != null) {
return Either.left(new DocumentError.DocumentAlreadyExists(documentId));
}
final BlobId blobId = BlobId.of(bucketName, fullBlobName);
final var blobInfoBuilder = BlobInfo.newBuilder(blobId);
try {
applyMetadata(blobInfoBuilder, request.metadata(), fileName);
} catch (final JsonProcessingException e) {
return Either.left(
new DocumentError.InvalidInput("Failed to serialize metadata: " + e.getMessage()));
}
try {
storage.createFrom(blobInfoBuilder.build(), request.contentInputStream());
} catch (final Exception e) {
return Either.left(new UnknownDocumentError(e));
}
final var updatedMetadata =
new DocumentMetadataModel(
request.metadata().contentType(),
fileName,
request.metadata().expiresAt(),
request.metadata().size(),
request.metadata().processDefinitionId(),
request.metadata().processInstanceKey(),
request.metadata().customProperties());
final var documentReference = new DocumentReference(documentId, updatedMetadata);
return Either.right(documentReference);
}
private Either getDocumentContentInternal(
final String documentId) {
try {
final String fullBlobName = getFullBlobName(documentId);
final Blob blob = storage.get(bucketName, fullBlobName);
if (blob == null) {
return Either.left(new DocumentError.DocumentNotFound(documentId));
}
final var inputStream = Channels.newInputStream(blob.reader());
final var contentType = blob.getContentType();
return Either.right(new DocumentContent(inputStream, contentType));
} catch (final Exception e) {
return Either.left(new UnknownDocumentError(e));
}
}
private Either deleteDocumentInternal(final String documentId) {
try {
final String fullBlobName = getFullBlobName(documentId);
final boolean result = storage.delete(bucketName, fullBlobName);
if (!result) {
return Either.left(new DocumentError.DocumentNotFound(documentId));
}
return Either.right(null);
} catch (final Exception e) {
return Either.left(new UnknownDocumentError(e));
}
}
private Either createLinkInternal(
final String documentId, final long durationInMillis) {
try {
final String fullBlobName = getFullBlobName(documentId);
final Blob blob = storage.get(bucketName, fullBlobName);
if (blob == null) {
return Either.left(new DocumentError.DocumentNotFound(documentId));
}
final var link = blob.signUrl(durationInMillis, TimeUnit.MILLISECONDS);
return Either.right(
new DocumentLink(
link.toString(), OffsetDateTime.now().plus(Duration.ofMillis(durationInMillis))));
} catch (final Exception e) {
return Either.left(new UnknownDocumentError(e));
}
}
private String getFullBlobName(final String documentId) {
return Optional.ofNullable(prefix).orElse("") + documentId;
}
private void applyMetadata(
final BlobInfo.Builder blobInfoBuilder,
final DocumentMetadataModel metadata,
final String fileName)
throws JsonProcessingException {
if (metadata == null) {
return;
}
if (metadata.contentType() != null && !metadata.contentType().isEmpty()) {
blobInfoBuilder.setContentType(metadata.contentType());
}
if (metadata.expiresAt() != null) {
blobInfoBuilder.setCustomTimeOffsetDateTime(OffsetDateTime.from(metadata.expiresAt()));
}
blobInfoBuilder.setContentDisposition("attachment; filename=" + fileName);
final Map blobMetadata = new HashMap<>();
final var valueAsString = objectMapper.writeValueAsString(metadata.customProperties());
if (metadata.customProperties() != null && !metadata.customProperties().isEmpty()) {
metadata.customProperties().forEach((key, value) -> blobMetadata.put(key, valueAsString));
}
if (metadata.processDefinitionId() != null) {
blobMetadata.put(METADATA_PROCESS_DEFINITION_ID, metadata.processDefinitionId());
}
if (metadata.processInstanceKey() != null) {
blobMetadata.put(METADATA_PROCESS_INSTANCE_KEY, metadata.processInstanceKey().toString());
}
blobInfoBuilder.setMetadata(blobMetadata);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy