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
| Page | What it covers |
|---|---|
| File uploads and downloads | The uploads and downloads that feed cloud storage |
| Orchestration | The workflows whose state the blobs persist |
| Error handling | What callers receive when a storage operation fails |