Archive name when streaming from a Python plugin

Hello,
We are using a Python script to stream on the fly created archive. We need this script to allow proper auth from our main server.

This script calls /tools/create-archive then stream the archive.
It works well, except one thing:
The archive name is always Stream-Orthanc-{id}.zip.
I have tried with no success:

  • adding ?filename=archive_name to the url
  • adding "Filename": archive_name, to our POST json body
  • setting our script output Http headers:
        # Setting output headers for the response does not work in this context
        output.SetHttpHeader('Content-Type', 'application/zip')
        output.SetHttpHeader('Content-Disposition', f'attachment; filename="{archive_name}.zip"')

Relevant part of our script is:


def create_archive(
    decoded_jwt: Dict[str, Any],
    orthanc_url: str,
    archive_endpoint: str,
    archive_name: str,
    token: str,
    http_client=requests
) -> requests.Response:
    """Create an archive by calling the Orthanc API."""
    try:
        # archive_url = f"{orthanc_url}{archive_endpoint}?filename={archive_name}"
        archive_url = f"{orthanc_url}{archive_endpoint}"
        logger.info(f"Creating archive at {archive_url} with filename={archive_name} & payload: {decoded_jwt}")
        response = http_client.post(
            archive_url,
            json=decoded_jwt,
            headers={'Authorization': token},
            stream=True,
            timeout=120
        )

        if response.status_code != 200:
            error_msg = f"Failed to create archive, status: {response.status_code}, response: {response.text}"
            logger.error(error_msg)
            raise ValueError(error_msg)

        return response

    except requests.exceptions.RequestException as e:
        error_msg = f"Archive creation request failed: {str(e)}"
        logger.error(error_msg)
        raise ValueError(error_msg)

def stream_archive(
    archive_response: requests.Response,
    output: orthanc.RestOutput,
    archive_name: str,
    chunk_size: int = 16 * 1024
) -> None:
    """Stream the archive to the output."""
    try:
        logger.info(f"archive_name: {archive_name}.zip")
        output.StartStreamAnswer('application/zip')
        # Setting output headers for the response does not work in this context
        # output.SetHttpHeader('Content-Type', 'application/zip')
        # output.SetHttpHeader('Content-Disposition', f'attachment; filename="{archive_name}.zip"')

        for buffer in archive_response.iter_content(chunk_size, False):
            output.SendStreamChunk(buffer)

    except Exception as e:
        error_msg = f"Streaming failed: {str(e)}"
        logger.error(error_msg)
        raise

def handle_rest_archive(output: orthanc.RestOutput, uri: str, **kwargs):
    """Main function that handles the REST request."""
    try:
        # Extract headers from kwargs (Orthanc passes them separately)
        headers = kwargs.get('headers', {})
        request_method = kwargs.get('method', 'GET')
        groups = kwargs.get('groups', [])
        config = kwargs.get('config', DEFAULT_CONFIG)

        logger.info(f"Handling {request_method} request for {uri} from groups: {groups}")

        # Get the JWT from headers
        jwt_token = get_jwt_from_headers(headers)
        if not jwt_token:
            raise ValueError("Missing Authorization header")

        # Decode JWT
        decoded_jwt = decode_jwt(jwt_token, config['JwtDecodeUrl'])
        logger.info(f"Decoded JWT: {decoded_jwt}")

        archive_name = decoded_jwt.get('name', 'archive')
        resources_ids = decoded_jwt.get('resourceList', [])

        payload = {
            "Resources": resources_ids,
            "Asynchronous": True,
            "Priority": 0,
            "Synchronous": True,
            # "Filename": archive_name,
        }

        # Create archive
        archive_response = create_archive(
            payload,
            config['OrthancSelfUrl'],
            config['ArchiveEndpoint'],
            archive_name,
            TOKEN
        )

        # Stream response
        stream_archive(archive_response, output, archive_name)

    except ValueError as e:
        error_msg = f"Validation error: {str(e)}"
        logger.error(error_msg)
        output.AnswerBuffer(json.dumps({"error": error_msg}), "application/json")

    except Exception as e:
        error_msg = f"Unexpected error: {str(e)}"
        logger.error(f"{error_msg}\n{traceback.format_exc()}")
        output.AnswerBuffer(json.dumps({"error": error_msg}), "application/json")

# Registration using the correct signature
def rest_callback(output: orthanc.RestOutput, uri: str, **kwargs):
    """Wrapper that matches Orthanc's expected callback signature."""
    return handle_rest_archive(output, uri, **kwargs)

orthanc.RegisterRestCallback('/stream_archive', rest_callback)

Thanks for any help :slight_smile:

Hi @OPatrick

The only way is to set the Content-Disposition yourself but, this must be done before the call to StartStreamAnswer

# Note, it is important to set the headers before calling StartStreamAnswer, once the answer has started to stream, it is to late to modify headers
output.SetHttpHeader('Content-Disposition', 'filename=my-custom-name.zip')

output.StartStreamAnswer('application/zip')

I have just updated the sample in the orthanc book accordingly.

Hope this helps,

Alain.

2 Likes

Hi,
I should have given an update earlier, sorry.
It works fine!
Thanks a lot

1 Like