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
| Page | What it covers |
|---|---|
| Cloud storage | Storing and retrieving the uploaded files in cloud storage |
| REST channels | The form data channels parse for uploads |
| Calling REST APIs | The response.content bytes that downloads read |