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 :contextis 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
nilwhen metadata is not used - It is important to return
nilrather than an empty hash{}
def metadata(options = {})
nil # Return nil, not an empty hash {}
end
Debugging Methods
- Log monitoring:
docker logs container-name -f
- Syntax check:
ruby -c delegates.rb
- 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:
ScriptLookupStrategysetting: Required for using delegate scripts with AzureStorageSourceattr_accessor :context: Required for Cantaloupe to set context informationazurestoragesource_blob_keymethod: Main logic for path conversion- 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.