File uploads and downloads

Serve downloads, accept uploads and move binary data through REST services.

REST services handle files in both directions - they return reports for download, accept document uploads and fetch attachments from external systems. The response headers tell browsers and clients that they receive a file rather than JSON.

To expose the services below to clients, create a REST channel for each endpoint.

Return a file attachment

To return a downloadable file, set Content-Disposition to attachment with a filename - the browser then saves the file instead of displaying it:

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

# Zato
from zato.server.service import Service

class DownloadReport(Service):
    """ Serves a PDF report as a downloadable attachment.
    """
    name = 'demo.rest.download-report'

    input = 'report_id'

    def handle(self) -> 'None':

        report_id = self.request.input.report_id

        # The report's bytes come from another service ..
        content = self.invoke('reports.generate', report_id=report_id)

        # .. and the headers make the response a named download.
        self.response.payload = content
        self.response.content_type = 'application/pdf'

        filename = f'report-{report_id}.pdf'
        self.response.headers['Content-Disposition'] = f'attachment; filename={filename}'

With curl, -O -J saves the file under the name the response suggests:

# Download and save as the suggested filename
curl -O -J http://localhost:11223/api/reports/download?report_id=RPT-001

# Or name the output file yourself
curl http://localhost:11223/api/reports/download?report_id=RPT-001 --output report.pdf

Return other file types

The pattern is the same for every format - only the content type and the filename change:

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

# Zato
from zato.server.service import Service

class DownloadCSV(Service):
    name = 'demo.rest.download-csv'

    def handle(self) -> 'None':

        lines = [
            'id,name,email',
            '1,John,john@example.com',
            '2,Jane,jane@example.com',
        ]
        csv_data = '\n'.join(lines)

        self.response.payload = csv_data
        self.response.headers['Content-Disposition'] = 'attachment; filename=export.csv'
        self.response.content_type = 'text/csv'


class DownloadExcel(Service):
    name = 'demo.rest.download-excel'

    def handle(self) -> 'None':

        # The workbook bytes come from a service using a library such as openpyxl
        excel_bytes = self.invoke('reports.build-excel')

        self.response.payload = excel_bytes
        self.response.headers['Content-Disposition'] = 'attachment; filename=export.xlsx'

        # The standard MIME type for .xlsx files
        xlsx_mime = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
        self.response.content_type = xlsx_mime

Accept file uploads

Uploaded files arrive as multipart form data, available through self.request.http.get_form_data:

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

# Zato
from zato.common.exception import BadRequest
from zato.server.service import Service

class UploadDocument(Service):
    """ Accepts a document upload and stores it.
    """
    name = 'demo.rest.upload-document'

    def handle(self) -> 'None':

        # The multipart form, files included ..
        form_data = self.request.http.get_form_data()

        # .. a request without a file is rejected with 400 ..
        uploaded_file = form_data.get('file')
        if not uploaded_file:
            raise BadRequest(self.cid, 'No file provided')

        # .. and the file's name, bytes and type go to storage.
        filename = uploaded_file.filename
        content = uploaded_file.read()
        content_type = uploaded_file.content_type

        self.invoke('storage.save',
            filename=filename,
            content=content,
            content_type=content_type)

        self.response.payload = {
            'status': 'uploaded',
            'filename': filename,
            'size': len(content)
        }

Download files from external APIs

Files from external systems arrive through outgoing connections, with the raw bytes in response.content. The service below fetches a Jira attachment - Jira answers with a redirect to the file's storage location and the connection follows it automatically:

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

# Zato
from zato.server.service import Service

class FetchJiraAttachment(Service):
    """ Serves a Jira attachment to the caller.
    """
    name = 'demo.rest.fetch-jira-attachment'

    input = 'attachment_id'

    def handle(self) -> 'None':

        attachment_id = self.request.input.attachment_id

        # The connection's URL path is /rest/api/3/attachment/content/{attachment_id},
        # with Jira credentials attached through its security definition
        conn = self.rest['Jira']
        response = conn.get(self.cid, params={'attachment_id': attachment_id})

        # The attachment's bytes pass through to our own caller
        self.response.payload = response.content
        self.response.content_type = 'application/octet-stream'

Base64 in JSON responses

Raw bytes do not fit in JSON - when a JSON response has to carry binary data, encode it as base64:

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

# stdlib
import base64

# Zato
from zato.server.service import Service

class GetFileAsBase64(Service):
    name = 'demo.rest.get-file-base64'

    input = 'file_id'

    def handle(self) -> 'None':

        file_id = self.request.input.file_id

        # The binary content from storage ..
        content = self.invoke('storage.get', file_id=file_id)

        # .. becomes text that JSON can carry.
        encoded = base64.b64encode(content).decode('utf-8')

        self.response.payload = {
            'file_id': file_id,
            'content_base64': encoded,
            'size': len(content)
        }

See also

PageWhat it covers
Cloud storageStoring and retrieving the uploaded files in cloud storage
REST channelsThe form data channels parse for uploads
Calling REST APIsThe response.content bytes that downloads read

Learn more