Milesoft Storage Extension (storage-ext)

1. Overview & Purpose

The Storage Extension (storage-ext) is a unified, highly optimized, and robust developer utility designed to provide a secure and standardized facade around Google Cloud Storage (GCS). It simplifies object storage interactions within the Milesoft ecosystem by offering a simple API to read, write, and delete files, as well as generate time-limited signed URLs for secure direct-to-cloud client interactions.

Key Benefits & Core Capabilities:

  • Standardized Facade: Provides a clean, type-safe API for reading and writing string or byte payloads, shielding downstream developers from raw Google Cloud API boilerplate.
  • Time-Limited Signed URLs: Natively supports secure URL generation for both client download (GET) and direct upload (PUT) workflows without exposing private cloud credentials.
  • Seamless Testing & Mocking: Exposes package-private constructors that facilitate 100% test coverage using standard mock frameworks (e.g. Mockito) in downstream microservices.
  • Consistent Validation: Integrates built-in argument and null-pointer checks to catch configuration and runtime errors early, before making expensive cloud network calls.

2. Installation & Dependency Configuration

The Storage Extension is hosted securely inside our Google Cloud Artifact Registry repository.

To configure your application to resolve and pull this dependency:

2.1 Gradle Setup (build.gradle)

Apply the Google Cloud Artifact Registry plugin and configure the Maven repository block:

plugins {
    id "com.google.cloud.artifactregistry.gradle-plugin" version "2.2.5"
    id "java"
}

repositories {
    mavenLocal()
    mavenCentral()
    maven {
        url "artifactregistry://us-west2-maven.pkg.dev/milesoft-repo/repo-maven"
    }
}

dependencies {
    // Milesoft Commons foundation (required by storage-ext)
    implementation "io.milesoft:commons:2.0.1"
    
    // Storage Integration Extension
    implementation "io.milesoft:storage-ext:2.0.1"
}

3. Configuring Storage in Spring Boot

Downstream Spring Boot microservices and applications utilize a standard configuration pattern to initialize and inject the StorageHelper as a Spring-managed bean.

3.1 Define Application Properties

Configure your target GCS bucket name in your properties configuration (e.g., application.yml or application.properties):

storage:
  bucket: "milesoft-production-assets"

3.2 Defining Storage Configuration Bean

To enable the StorageHelper bean inside your application context, declare it in a standard configuration class:

import io.milesoft.storage.utils.StorageHelper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class StorageConfig {

    @Bean
    public StorageHelper storageHelper() {
        return new StorageHelper();
    }
}

4. Key Components

4.1 StorageHelper

The single, thread-safe orchestrator utility provided by this extension. It manages connections to Google Cloud Storage and acts as the entry point for all storage transactions, encapsulating standard operations on GCS Storage and GCS Blob objects.


5. API Reference Guide

5.1 StorageHelper Class

The class exposes the following public methods for handling storage operations.

Constructors:

  • public StorageHelper() Creates a new helper instance, resolving GCS credentials and configurations automatically via the default environment/IAM settings (StorageOptions.getDefaultInstance().getService()).

Core Methods:

Method Signature Return Type Description
getDownloadSignedUrl(String bucketName, String fileName) String Generates a time-limited GET signed URL valid for 1 hour. Useful for secure, direct file downloads or media rendering on frontends.
getUploadSignedUrl(String bucketName, String fileName, String contentType) String Generates a time-limited PUT signed URL valid for 1 hour with a specified contentType. Allows clients to upload large payloads directly to GCS without hitting the application backend.
write(String bucketName, String fileName, String content) void Writes string content (converted as UTF-8) to the target GCS bucket and file path. Throws an IllegalArgumentException if content or paths are blank.
write(String bucketName, String fileName, byte[] content) void Writes raw binary byte content to the target GCS bucket and file path. Throws appropriate exceptions if the byte array is null or empty.
readStr(String bucketName, String fileName) Optional<String> Reads the file contents from GCS and decodes them as a UTF-8 string. Returns Optional.empty() if the file does not exist, rather than throwing.
readBytes(String bucketName, String fileName) Optional<byte[]> Reads raw binary bytes from the target GCS bucket and file path. Returns Optional.empty() if the file does not exist.
delete(String bucketName, String fileName) void Deletes the specified object/file from GCS. No-op if the file is not found.

6. Practical Real-World Code Examples

These integration examples represent standard, battle-tested patterns for reading/writing files and handling signed URL workflows across the Milesoft codebase.

Example A: Injecting and Using StorageHelper in a Document Service

This example demonstrates a service that uploads metadata to a database (e.g. Firestore) while storing the actual file content on Google Cloud Storage using StorageHelper.

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkArgument;

import io.milesoft.storage.utils.StorageHelper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.Optional;

@Service
public class DocumentService {

    private final StorageHelper storage;
    private final String bucket;

    @Autowired
    public DocumentService(StorageHelper storage, @Value("${storage.bucket}") String bucket) {
        checkArgument(StringUtils.isNotBlank(bucket), "bucket must not be blank");
        this.storage = checkNotNull(storage, "storage helper must not be null");
        this.bucket = bucket;
    }

    /**
     * Uploads file content to the configured GCS bucket using the document ID as key.
     */
    public void uploadDocument(String documentId, byte[] fileContent) {
        checkArgument(StringUtils.isNotBlank(documentId), "documentId must not be blank");
        checkNotNull(fileContent, "fileContent must not be null");

        // Write raw bytes directly to storage
        storage.write(bucket, documentId, fileContent);
    }

    /**
     * Downloads file content from the configured GCS bucket.
     */
    public byte[] downloadDocument(String documentId) {
        checkArgument(StringUtils.isNotBlank(documentId), "documentId must not be blank");

        return storage.readBytes(bucket, documentId)
            .orElseThrow(() -> new RuntimeException("Document not found in storage: " + documentId));
    }
}

Example B: Secure Direct-to-Cloud Upload & Download Workflow

Directly proxying binary transfers through an application server can cause memory pressure and latency. A better approach is using Signed URLs to allow the frontend client to securely interact directly with Google Cloud Storage.

import io.milesoft.storage.utils.StorageHelper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class SignedUrlService {

    private final StorageHelper storage;
    private final String bucket;

    public SignedUrlService(StorageHelper storage, @Value("${storage.bucket}") String bucket) {
        this.storage = storage;
        this.bucket = bucket;
    }

    /**
     * Generates a secure, temporary PUT URL. The frontend can perform an HTTP PUT request 
     * directly to this URL with the file payload.
     */
    public String generateUploadUrl(String fileName, String contentType) {
        return storage.getUploadSignedUrl(bucket, fileName, contentType);
    }

    /**
     * Generates a temporary GET URL. The frontend can render or download this file directly 
     * in the browser for up to 1 hour.
     */
    public String generateDownloadUrl(String fileName) {
        return storage.getDownloadSignedUrl(bucket, fileName);
    }
}

Example C: Unit Testing Services with Mocked StorageHelper

This standard pattern shows how to mock StorageHelper when testing your services, avoiding actual external network calls and verifying that the correct integration methods are invoked.

import static org.mockito.Mockito.*;
import static org.junit.Assert.*;

import io.milesoft.storage.utils.StorageHelper;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public class DocumentServiceTest {

    @Mock
    private StorageHelper storageHelper;

    private DocumentService documentService;
    private final String testBucket = "test-bucket";

    @Before
    public void setUp() {
        MockitoAnnotations.openMocks(this);
        documentService = new DocumentService(storageHelper, testBucket);
    }

    @Test
    public void testUploadDocument() {
        String docId = "doc-123";
        byte[] content = "Hello World".getBytes(StandardCharsets.UTF_8);

        documentService.uploadDocument(docId, content);

        // Verify write was called on GCS
        verify(storageHelper, times(1)).write(testBucket, docId, content);
    }

    @Test
    public void testDownloadDocument_Success() {
        String docId = "doc-123";
        byte[] expectedContent = "Hello World".getBytes(StandardCharsets.UTF_8);

        when(storageHelper.readBytes(testBucket, docId)).thenReturn(Optional.of(expectedContent));

        byte[] actualContent = documentService.downloadDocument(docId);

        assertArrayEquals(expectedContent, actualContent);
        verify(storageHelper, times(1)).readBytes(testBucket, docId);
    }

    @Test(expected = RuntimeException.class)
    public void testDownloadDocument_NotFound() {
        String docId = "doc-999";

        when(storageHelper.readBytes(testBucket, docId)).thenReturn(Optional.empty());

        documentService.downloadDocument(docId);
    }
}