How to upload and download a file with Azure Blob Storage and Spring Boot

Azure Blob storage is Microsoft's object storage solution for the cloud. Blob storage is optimized for storing massive amounts of unstructured data. Unstructured data is data that doesn't adhere to a particular data model or definition, such as text or binary data.

How to upload and download a file with Azure Blob Storage and Spring Boot

Hello, I'm Srivastava, in this post we will see how to upload a file to Azure Blob Storage and download the file from the same.

What is Azure Blob Storage?

Azure Blob storage is Microsoft's object storage solution for the cloud. Blob storage is optimized for storing massive amounts of unstructured data. Unstructured data is data that doesn't adhere to a particular data model or definition, such as text or binary data.

Why use Blob Storage?

Blob storage is designed for:

  • Serving images or documents directly to a browser.
  • Storing files for distributed access.
  • Streaming video and audio.
  • Writing to log files.
  • Storing data for backup and restore, disaster recovery, and archiving.
  • Storing data for analysis by an on-premises or Azure-hosted service.

Blob Storage Resources

Blob storage offers three types of resources:

  • The storage account
  • A container in the storage account
  • A blob in a container

The following diagram shows the relationship between these resources.

Storage Accounts

A storage account provides a unique namespace in Azure for your data. Every object that you store in Azure Storage has an address that includes your unique account name. The combination of the account name and the Azure Storage blob endpoint forms the base address for the objects in your storage account.

For example, if your storage account is named mystorageaccount, then the default endpoint for Blob storage is:

http://mystorageaccount.blob.core.windows.net

Follow the instructions provided here to create storage account. 

The details we need from the storage account are: Account Name, Account Key, Blob Endpoint

Containers

A container organizes a set of blobs, similar to a directory in a file system. A storage account can include an unlimited number of containers, and a container can store an unlimited number of blobs.

Note: The container name must be lowercase.

Blobs

Azure Storage supports three types of blobs:

  • Block blobs store text and binary data. Block blobs are made up of blocks of data that can be managed individually. Block blobs can store up to about 190.7 TiB.
  • Append blobs are made up of blocks like block blobs, but are optimized for append operations. Append blobs are ideal for scenarios such as logging data from virtual machines.
  • Page blobs store random access files up to 8 TiB in size. Page blobs store virtual hard drive (VHD) files and serve as disks for Azure virtual machines.

Implementation

Creating the Project

Spring Boot provides Spring Initializr, a quick start generator for Spring Projects. Head over to https://start.spring.io to access this web tool. Include web,azure storage, lombok, devtools and configuration processor dependencies and click generate. A zip file will be downloaded which contains your project related settings and a pom.xml to get dependencies. You can unzip it and import it into your favourite IDE.

(OR)

Some IDEs such as IntelliJ, STS will allow you to create project right from it that has Spring Initializr.

If you want to manually include the storage dependency, the following dependency should be added to your pom.xml.

<dependency>
    <groupId>com.azure.spring</groupId>
    <artifactId>azure-spring-boot-starter-storage</artifactId>
    <version>3.13.0</version>
</dependency>

Making Changes

Once you boostrapped the project, open application.properties and add the following properties to it

azure.storage.account-name=youraccountname
azure.storage.account-key=youraccountkey
azure.storage.blob-endpoint=https://youraccountname.blob.core.windows.net

file.upload-dir=./uploads

I gave uploads directory as my file storage location which would be under our project root path. This would directory will be created when we start our app (of course we will configure it). You can give your own path to be uploaded.

Create few packages inside your main package (e.g. com.fileapi) such as controller, config, property, and service packages.

Add the following code in property package.

FileStorageProperties.java

package com.fileapi.property;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Getter
@Setter
@ConfigurationProperties(prefix = "file")
public class FileStorageProperties {

    private String uploadDir;

}

We have used @ConfigurationProperties(prefix = "file") annotation so that it binds all the properties with prefix file to the fields in our class on application startup. If you want to create an additional file property in properties file, all you need to do is to just add a field in our class.

Enabling Configuration Properties

To make @ConfigurationProperties feature work, you need to add @EnableConfigurationProperties to any configuration class. We will add it in our main class. So open src/main/java/com/fileapi/SpringBootAzureFileApiApplication.java and add the @EnableConfigurationProperties annoation to it like this:

package com.fileapi;

import com.fileapi.property.FileStorageProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

@SpringBootApplication
@EnableConfigurationProperties({FileStorageProperties.class})
public class SpringBootAzureFileApiApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootAzureFileApiApplication.class, args);
    }

}

Now create a file named AzureBlobStorageConfig.java and add the following code to it

package com.fileapi.config;

import com.azure.storage.blob.BlobServiceClient;
import com.azure.storage.blob.BlobServiceClientBuilder;
import com.azure.storage.common.StorageSharedKeyCredential;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Locale;

@Configuration
public class AzureBlobStorageConfig {

    @Value("${azure.storage.account-name}")
    private String accountName;

    @Value("${azure.storage.account-key}")
    private String accountKey;

    @Bean
    public BlobServiceClient getBlobServiceClient() {
        return new BlobServiceClientBuilder()
                .endpoint(String.format(Locale.ROOT, "https://%s.blob.core.windows.net", accountName))
                .credential(new StorageSharedKeyCredential(accountName, accountKey))
                .buildClient();
    }
}

We created the configuration to get the BlobServiceClient bean. It uses the storage account name and account key.

Service for upload and download

Create a file FileService.java in service package and add the following code to it.

package com.fileapi.service;

import com.azure.storage.blob.BlobContainerClient;
import com.azure.storage.blob.BlobServiceClient;
import com.azure.storage.blob.specialized.BlockBlobClient;
import com.fileapi.property.FileStorageProperties;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

@Log4j2
@Service
public class FileService {

    private final Path fileStorageLocation;
    private final BlobServiceClient blobServiceClient;

    @Autowired
    public FileService(@NonNull FileStorageProperties fileStorageProperties, BlobServiceClient blobServiceClient) {
        // get the path of the upload directory
        fileStorageLocation = Path.of(fileStorageProperties.getUploadDir());
        this.blobServiceClient = blobServiceClient;
        try {
            // creates directory/directories, if directory already exists, it will not throw exception
            Files.createDirectories(fileStorageLocation);
        } catch (IOException e) {
            log.error("Could not create the directory where the uploaded files will be stored.", e);
        }
    }

    public Path getFileStorageLocation() {
        try {
            Files.createDirectories(this.fileStorageLocation);
        } catch (IOException e) {
            log.error("Could not create the directory where the uploaded file will be stored.", e);
        }
        return fileStorageLocation;
    }

    public Boolean uploadAndDownloadFile(@NonNull MultipartFile file, String containerName) {
        boolean isSuccess = true;
        BlobContainerClient blobContainerClient = getBlobContainerClient(containerName);
        String filename = file.getOriginalFilename();
        BlockBlobClient blockBlobClient = blobContainerClient.getBlobClient(filename).getBlockBlobClient();
        try {
            // delete file if already exists in that container
            if (blockBlobClient.exists()) {
                blockBlobClient.delete();
            }
            // upload file to azure blob storage
            blockBlobClient.upload(new BufferedInputStream(file.getInputStream()), file.getSize(), true);
            String tempFilePath = fileStorageLocation + "/" + filename;
            Files.deleteIfExists(Paths.get(tempFilePath));
            // download file from azure blob storage to a file
            blockBlobClient.downloadToFile(new File(tempFilePath).getPath());
        } catch (IOException e) {
            isSuccess = false;
            log.error("Error while processing file {}", e.getLocalizedMessage());
        }
        return isSuccess;
    }


    private @NonNull BlobContainerClient getBlobContainerClient(@NonNull String containerName) {
        // create container if not exists
        BlobContainerClient blobContainerClient = blobServiceClient.getBlobContainerClient(containerName);
        if (!blobContainerClient.exists()) {
            blobContainerClient.create();
        }
        return blobContainerClient;
    }
}

In the above service there are a couple of important things going on. In the getBlobContainerClient() method, we are creating the container if not exists. Remember we created bean to get BlobServiceClient and now we have autorwired in the service. And we are building BlockBlobClient passing file to it. If the same file exists in the container, we are deleting it. Finally we upload the file to azure blob storage. As we already have the reference to the to the uploaded file, we are calling the method downloadToFile() and storing it in our file storage location.

Controller for REST API

Create a file FileController.java in controller package and add the following code to it.

package com.fileapi.controller;

import com.fileapi.service.FileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.nio.file.Files;
import java.nio.file.Paths;

@RestController
@RequestMapping("/api/v1/file")
public class FileController {

    private final FileService fileService;

    @Autowired
    public FileController(FileService fileService) {
        this.fileService = fileService;
    }

    @PostMapping(value = "/", produces = {MediaType.IMAGE_PNG_VALUE, MediaType.IMAGE_JPEG_VALUE})
    public ResponseEntity<?> uploadAndDownload(@RequestParam("file") MultipartFile file) {
        try {
            if (fileService.uploadAndDownloadFile(file, "files")) {
                final ByteArrayResource resource = new ByteArrayResource(Files.readAllBytes(Paths.get(fileService
                        .getFileStorageLocation() + "/" + file.getOriginalFilename())));
                return ResponseEntity.status(HttpStatus.OK).contentLength(resource.contentLength()).body(resource);
            }
            return ResponseEntity.ok("Error while processing file");
        } catch (Exception e) {
            return ResponseEntity.ok("Error while processing file");
        }
    }
}

For the purpose of this example, I have used only images (png and jpeg) to upload and download. I have given the container name as "files".

Testing the API

You can also find the source code at github.

Thanks for reading!

Files