Introduction

When using Azure Storage with the IIIF server Cantaloupe, the IIIF URL identifier may differ from the actual file path on Azure Storage. This article provides a detailed explanation of how to solve this problem using delegate scripts.

The Problem

Suppose you are managing images with the following file structure:

Azure Storage Container: mycontainer
├── images/
│   ├── collection1/
│   │   ├── item001/
│   │   │   └── item001_001.jpg
│   │   └── item002/
│   │       └── item002_001.jpg
│   └── collection2/
│       └── ...

However, you want to access them via IIIF URLs like:

https://example.com/iiif/3/collection1/item001/item001_001.jpg/info.json

In this case, the IIIF URL identifier (collection1/item001/item001_001.jpg) differs from the actual Azure Storage path (images/collection1/item001/item001_001.jpg).

Unlike S3Source, AzureStorageSource does not have a PATH_PREFIX setting, so a delegate script is needed to solve this problem.

Solution

1. Docker Compose Configuration

services:
  cantaloupe:
    image: islandora/cantaloupe:main
    environment:
      CANTALOUPE_SOURCE_STATIC: AzureStorageSource
      CANTALOUPE_AZURESTORAGESOURCE_ACCOUNT_NAME: ${AZURE_STORAGE_ACCOUNT_NAME}
      CANTALOUPE_AZURESTORAGESOURCE_ACCOUNT_KEY: ${AZURE_STORAGE_ACCOUNT_KEY}
      CANTALOUPE_AZURESTORAGESOURCE_CONTAINER_NAME: ${AZURE_STORAGE_CONTAINER_NAME}
      CANTALOUPE_AZURESTORAGESOURCE_LOOKUP_STRATEGY: ScriptLookupStrategy  # Important
      CANTALOUPE_DELEGATE_SCRIPT_ENABLED: "true"
      CANTALOUPE_DELEGATE_SCRIPT_PATHNAME: "/opt/cantaloupe/delegates.rb"
    volumes:
      - "./delegates.rb:/opt/cantaloupe/delegates.rb:ro"
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.cantaloupe.rule=Host(`example.com`)"
      - "traefik.http.routers.cantaloupe.entrypoints=websecure"
      - "traefik.http.routers.cantaloupe.tls=true"
      - "traefik.http.services.cantaloupe.loadbalancer.server.port=8182"
    restart: always

2. Delegate Script (delegates.rb)

Development/Debug Version

Start by verifying behavior with a version that includes debug output:

##
# Delegate script for AzureStorageSource with ScriptLookupStrategy
#
class CustomDelegate

  # Accessor for storing context
  attr_accessor :context

  ##
  # Convert IIIF URL identifier to Azure Storage blob key
  #
  def azurestoragesource_blob_key
    identifier = context['identifier'] if context

    if identifier
      blob_key = "images/#{identifier}"
      puts "DEBUG: Input identifier: #{identifier}"
      puts "DEBUG: Output blob key: #{blob_key}"
      return blob_key
    end

    return nil
  end

  ##
  # Authentication methods
  #
  def pre_authorize(options = {})
    puts "DEBUG: pre_authorize called"
    true
  end

  def authorize(options = {})
    puts "DEBUG: authorize called"
    true
  end

  ##
  # Other required methods
  #
  def extra_iiif2_information_response_keys(options = {})
    {}
  end

  def extra_iiif3_information_response_keys(options = {})
    {}
  end

  def redactions(options = {})
    []
  end

  def metadata(options = {})
    nil
  end

end

Production Clean Version

After verifying behavior, remove debug output for the production environment:

##
# Complete delegate script for AzureStorageSource with ScriptLookupStrategy
#
class CustomDelegate

  # Accessor for storing context
  attr_accessor :context

  ##
  # Main function called by Cantaloupe
  # Converts IIIF URL identifier to Azure Storage blob key
  #
  def azurestoragesource_blob_key
    identifier = context['identifier'] if context
    return identifier ? "images/#{identifier}" : nil
  end

  ##
  # Authentication methods - allow all requests
  #
  def pre_authorize(options = {})
    true
  end

  def authorize(options = {})
    true
  end

  ##
  # Additional IIIF information response keys (empty when not used)
  #
  def extra_iiif2_information_response_keys(options = {})
    {}
  end

  def extra_iiif3_information_response_keys(options = {})
    {}
  end

  ##
  # Redaction feature (empty list when not used)
  #
  def redactions(options = {})
    []
  end

  ##
  # Metadata addition (return nil when not used)
  #
  def metadata(options = {})
    nil
  end

end

Troubleshooting

Common Errors and Solutions

1. Cannot invoke "edu.illinois.library.cantaloupe.delegate.DelegateProxy.getAzureStorageSourceBlobKey()" because the return value is null

Cause: The delegate script is not loaded correctly, or it contains a syntax error

Solution:

  • Check the delegate script syntax: ruby -c delegates.rb
  • Verify that attr_accessor :context is defined
  • Check the container logs for delegate script loading status

2. undefined method 'context=' for CustomDelegate

Cause: attr_accessor :context is not defined

Solution: Add attr_accessor :context at the top of the class

3. undefined method 'pre_authorize' etc.

Cause: Required delegate methods are not defined

Solution: Refer to the complete delegate script above and add the necessary methods

4. class org.jruby.RubyHash cannot be cast to class java.lang.String

Cause: The metadata() method is returning an invalid type

Solution:

  • Return nil when metadata is not used
  • It is important to return nil rather than an empty hash {}
def metadata(options = {})
  nil  # Return nil, not an empty hash {}
end

Debugging Methods

  1. Log monitoring:
docker logs container-name -f
  1. Syntax check:
ruby -c delegates.rb
  1. Incremental testing: Start with a simple delegate script and gradually add functionality

Verification

1. Start the Service

docker compose up -d

2. Test Access

# Retrieve info.json
curl https://example.com/iiif/3/collection1/item001/item001_001.jpg/info.json

# Retrieve image
curl https://example.com/iiif/3/collection1/item001/item001_001.jpg/full/300,/0/default.jpg

3. Check Logs (When Using Debug Version)

Expected log output:

DEBUG: pre_authorize called
DEBUG: authorize called
DEBUG: Input identifier: collection1/item001/item001_001.jpg
DEBUG: Output blob key: images/collection1/item001/item001_001.jpg

Summary

By using Cantaloupe’s delegate script, you can bridge the gap between IIIF URLs and actual storage paths. Key points:

  1. ScriptLookupStrategy setting: Required for using delegate scripts with AzureStorageSource
  2. attr_accessor :context: Required for Cantaloupe to set context information
  3. azurestoragesource_blob_key method: Main logic for path conversion
  4. Other delegate methods: Minimum required methods to avoid errors

The approach described in this article enables flexible file management and IIIF URL design. It is particularly useful when you want to add IIIF support without changing the existing file structure.

References