# 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](https://zato.io/docs/dev/rest/channels.html) for each endpoint.

## Return a file attachment {#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:

```python
# -*- 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:

```bash
# 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 {#return-other-file-types}

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

```python
# -*- 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 {#accept-file-uploads}

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

```python
# -*- 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 {#download-files-from-external-apis}

Files from external systems arrive through [outgoing connections](https://zato.io/docs/dev/rest/outconns.html), 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:

```python
# -*- 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(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 {#base64-in-json-responses}

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

```python
# -*- 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 fits in JSON.
        encoded = base64.b64encode(content).decode('utf-8')

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

## See also {#see-also}

- [Cloud storage](https://zato.io/docs/dev/rest/cloud-storage.html) - Storing and retrieving the uploaded files in cloud storage
- [REST channels](https://zato.io/docs/dev/rest/channels.html) - The form data channels parse for uploads
- [Calling REST APIs](https://zato.io/docs/dev/rest/calling-apis.html) - The response.content bytes that downloads read

## Learn more {#learn-more}

- [REST integration overview](https://zato.io/integrations/rest/) - Start here to see how teams build REST integrations
- [In-depth API tutorial](https://zato.io/tutorials/main/01.html) - Master architectural patterns for production-ready integrations
- [Modern REST API tutorial](https://zato.io/tutorials/rest-api/python.html) - Design, build and secure REST APIs with Python
