All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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