Enabling Direct File Uploads via API in CKAN with Cloudflare R2

Files and Modifications Required:

  • logic/action.py – Contains API endpoints for generating pre-signed URLs.
  • plugin.py – Registers the new API actions.
  • Dockerfile – Installs the ckanext-s3filestore dependency.
  • .env – Enables the s3filestore plugin in CKAN.

This setup allows users to upload resources directly via the API while seamlessly integrating with Cloudflare R2 storage. 🚀

1. API Endpoints Setup in logic/action.py

Create or modify logic/action.py to include the following API functions:

from ckanext.s3filestore.uploader import BaseS3Uploader
from ckan.plugins.toolkit import get_action
import ckan.plugins.toolkit as tk
import ckan.logic as logic
import logging


ValidationError = logic.ValidationError
log = logging.getLogger(__name__)


def _generate_presigned_url(resource_id):
    try:
        uploader = BaseS3Uploader()
        s3_client = uploader.get_s3_client()
        bucket_name = uploader.bucket_name


        object_key = f"resources/api_uploads/{resource_id}"


        params = {
            'Bucket': bucket_name,
            'Key': object_key,
        }
        presigned_url = s3_client.generate_presigned_url(
            ClientMethod='put_object',
            Params=params,
            ExpiresIn=3600  # URL expires in 1 hour
        )


        return presigned_url
    except ClientError as e:
        log.error({"error": f"Failed to generate presigned URL: {str(e)}"})
        raise ValidationError({"error": f"Failed to generate presigned URL: {str(e)}"})


def resource_upload(context, data_dict):
    resource_id = data_dict.get('id')
    if not resource_id:
        raise ValidationError({"id": "Resource ID is required to generate upload URL."})


    resource = get_action('resource_show')(context, {'id': resource_id})


    try:
        presigned_url = _generate_presigned_url(resource_id)
    except Exception as e:
        log.error(f"Error generating presigned URL: {e}")
        raise ValidationError(f"Error generating presigned URL: {e}")
        
    return {"presigned_url": presigned_url}


@tk.chained_action
def resource_create(up_func, context, data_dict):
    result = up_func(context, data_dict)
    resource_id = result.get("id")
    if not resource_id:
        raise ValidationError({"id": "Resource creation failed, no ID returned."})
    try:
        presigned_url = _generate_presigned_url(resource_id)
    except Exception as e:
        log.error(f"Error generating presigned URL: {e}")
        raise ValidationError(f"Error generating presigned URL: {e}")


    result["presigned_url"] = presigned_url
    return result

2. Register API Endpoints in plugin.py

Modify plugin.py to register the new actions:

from ckan.plugins import implements, SingletonPlugin
from ckan.plugins.interfaces import IActions


class CustomPlugin(SingletonPlugin):
    implements(IActions)


    def get_actions(self):
        return {
            'resource_upload': resource_upload,
            'resource_create': resource_create,
        }

3. Install ckanext-s3filestore Dependency in Dockerfile

Modify your Dockerfile to include the following line:

RUN pip install -r https://raw.githubusercontent.com/luccasmmg/ckanext-s3filestore/75b97dedc4fdbe4dd514c1bece623fd83e6cd988/requirements.txt

This ensures that ckanext-s3filestore is installed during the container build.

4. Enable s3filestore in .env

Modify your .env file to include s3filestore in the CKAN plugins:

CKAN__PLUGINS = s3filestore`

This setup ensures that:

  • API endpoints for resource_upload and resource_create are added to logic/action.py.
  • Endpoints are registered in plugin.py.
  • Required dependencies are installed in the Docker container.
  • s3filestore is enabled in CKAN plugins via the .env file.