Cloud storage

Store and retrieve files in Azure Blob Storage and AWS S3 from your services.

Integration workflows store files - audit logs, generated reports, workflow state and documents received from external systems. Cloud storage services such as Azure Blob Storage and AWS S3 hold them, and your services access them through the providers' SDKs.

Credentials go in Zato's configuration, not in your code - the services carry only the logic of what to store and when.

Upload blobs to Azure

# -*- coding: utf-8 -*-

# stdlib
import html

# xmltodict
import xmltodict

# Azure
from azure.storage.blob import BlobServiceClient

# Zato
from zato.server.service import Service

class UploadBlob(Service):
    """ Uploads a file to Azure Blob Storage.
    """
    name = 'azure.blob.upload'

    input = 'folder', 'filename', 'content'

    def handle(self) -> 'None':

        folder = self.request.input.folder
        filename = self.request.input.filename
        content = self.request.input.content

        # Credentials come from configuration ..
        account_url = self.config.azure.storage.account_url
        account_key = self.config.azure.storage.account_key
        container_name = self.config.azure.storage.container_name

        # .. the client connects to the storage account ..
        blob_service = BlobServiceClient(
            account_url=account_url,
            credential=account_key
        )

        # .. and the blob is addressed inside its container.
        container = blob_service.get_container_client(container_name)
        blob = container.get_blob_client(f'{folder}/{filename}')

        try:
            blob.upload_blob(content)

            self.response.payload = {
                'status': 'uploaded',
                'path': f'{folder}/{filename}'
            }
            self.logger.info('%s uploaded to Azure Blob Storage', filename)

        except Exception as e:

            # Azure reports errors as XML embedded in the exception's text
            error_str = html.unescape(str(e))
            xml_start = error_str.find('<?xml')
            xml_end = error_str.rfind('</Error>') + len('</Error>')

            if xml_start >= 0 and xml_end > xml_start:
                xml_content = error_str[xml_start:xml_end]
                error_response = xmltodict.parse(xml_content)
                self.logger.error('Azure error: %s', error_response['Error']['Code'])
                self.response.payload = error_response
            else:
                self.logger.error('Upload failed, e:`%s`', e)
                self.response.payload = {'error': str(e)}

Download blobs from Azure

# -*- coding: utf-8 -*-

# Azure
from azure.storage.blob import BlobServiceClient

# Zato
from zato.server.service import Service

class DownloadBlob(Service):
    name = 'azure.blob.download'

    input = 'folder', 'filename'

    def handle(self) -> 'None':

        folder = self.request.input.folder
        filename = self.request.input.filename

        account_url = self.config.azure.storage.account_url
        account_key = self.config.azure.storage.account_key
        container_name = self.config.azure.storage.container_name

        blob_service = BlobServiceClient(
            account_url=account_url,
            credential=account_key
        )

        container = blob_service.get_container_client(container_name)
        blob = container.get_blob_client(f'{folder}/{filename}')

        # The blob's bytes become a downloadable response
        download_stream = blob.download_blob()
        content = download_stream.readall()

        self.response.payload = content
        self.response.headers['Content-Disposition'] = f'attachment; filename={filename}'

List Azure blobs

# -*- coding: utf-8 -*-

# Azure
from azure.storage.blob import BlobServiceClient

# Zato
from zato.server.service import Service

class ListBlobs(Service):
    name = 'azure.blob.list'

    input = '-prefix'

    def handle(self) -> 'None':

        # The optional prefix narrows the listing, an empty one lists everything
        prefix = self.request.input.prefix

        account_url = self.config.azure.storage.account_url
        account_key = self.config.azure.storage.account_key
        container_name = self.config.azure.storage.container_name

        blob_service = BlobServiceClient(
            account_url=account_url,
            credential=account_key
        )

        container = blob_service.get_container_client(container_name)

        blobs = []
        for blob in container.list_blobs(name_starts_with=prefix):
            blobs.append({
                'name': blob.name,
                'size': blob.size,
                'last_modified': blob.last_modified.isoformat()
            })

        self.response.payload = {'blobs': blobs, 'count': len(blobs)}

Upload to S3

# -*- coding: utf-8 -*-

# Boto
import boto3

# Zato
from zato.server.service import Service

class UploadToS3(Service):
    """ Uploads a file to an S3 bucket.
    """
    name = 'aws.s3.upload'

    input = 'bucket', 'key', 'content'

    def handle(self) -> 'None':

        bucket = self.request.input.bucket
        key = self.request.input.key
        content = self.request.input.content

        # Credentials come from configuration
        s3 = boto3.client(
            's3',
            aws_access_key_id=self.config.aws.access_key_id,
            aws_secret_access_key=self.config.aws.secret_access_key,
            region_name=self.config.aws.region
        )

        s3.put_object(Bucket=bucket, Key=key, Body=content)

        self.response.payload = {
            'status': 'uploaded',
            'bucket': bucket,
            'key': key
        }

Download from S3

# -*- coding: utf-8 -*-

# Boto
import boto3

# Zato
from zato.server.service import Service

class DownloadFromS3(Service):
    name = 'aws.s3.download'

    input = 'bucket', 'key'

    def handle(self) -> 'None':

        bucket = self.request.input.bucket
        key = self.request.input.key

        s3 = boto3.client(
            's3',
            aws_access_key_id=self.config.aws.access_key_id,
            aws_secret_access_key=self.config.aws.secret_access_key,
            region_name=self.config.aws.region
        )

        response = s3.get_object(Bucket=bucket, Key=key)
        content = response['Body'].read()

        # The object's own name becomes the download's filename
        filename = key.split('/')[-1]

        self.response.payload = content
        self.response.headers['Content-Disposition'] = f'attachment; filename={filename}'

Store workflow data

Cloud storage also persists data between workflow steps - each step writes its state as a JSON blob that later steps or audits read back:

# -*- coding: utf-8 -*-

# stdlib
import json
from datetime import datetime

# Zato
from zato.server.service import Service

class StoreWorkflowData(Service):
    """ Persists one workflow step's data as a JSON blob.
    """
    name = 'workflow.data.store'

    input = 'workflow_id', 'step', 'data'

    def handle(self) -> 'None':

        workflow_id = self.request.input.workflow_id
        step = self.request.input.step
        data = self.request.input.data

        # The blob carries the step's data with its metadata ..
        blob_content = json.dumps({
            'workflow_id': workflow_id,
            'step': step,
            'timestamp': datetime.now().isoformat(),
            'data': data
        }, ensure_ascii=False).encode('utf-8')

        # .. filed under the current date, one file per workflow step.
        folder = f'workflows/{datetime.now().date()}'
        filename = f'{workflow_id}-{step}.json'

        self.invoke('azure.blob.upload',
            folder=folder,
            filename=filename,
            content=blob_content)

        self.response.payload = {'stored': f'{folder}/{filename}'}

See also

PageWhat it covers
File uploads and downloadsThe uploads and downloads that feed cloud storage
OrchestrationThe workflows whose state the blobs persist
Error handlingWhat callers receive when a storage operation fails

Learn more