Backend Capabilities
Capabilities describe what a backend can do. Instead of trying an operation and hoping it works, you can ask the backend first if it supports it.
Why They Matter
Different storage types have different capabilities:
S3 supports versioning, metadata, temporary URLs
Local filesystem supports symbolic links (Unix/Linux) but not versioning
HTTP is read-only, no writes
Memory supports everything but data is temporary
Capabilities let you:
Write adaptive code that works with different storage types
Avoid errors by checking before attempting an operation
Give clear messages to users about what they can/cannot do
Enable conditional features in your UI
How They Work
Each backend declares its capabilities. You check them before using a feature:
node = storage.node('s3:documents/file.pdf')
# Check a capability
if node.capabilities.versioning:
print("I can access previous versions")
versions = node.versions
else:
print("Versioning not available")
# Check before generating URLs
if node.capabilities.presigned_urls:
url = node.get_presigned_url(expires_in=3600)
print(f"Temporary link: {url}")
else:
print("Cannot generate URLs for this storage")
Available Capabilities
Basic Operations
readboolCan read files
writeboolCan write files
deleteboolCan delete files
mkdirboolCan create directories
list_dirboolCan list directory contents
Versioning
versioningboolSupports file versioning (maintains history of changes)
version_listingboolCan list all available versions
version_accessboolCan access specific versions
Metadata and URLs
metadataboolSupports custom metadata (key-value) on files
presigned_urlsboolCan generate temporary signed URLs for direct access
public_urlsboolHas public URLs accessible via HTTP
Advanced Features
atomic_operationsboolGuarantees atomic operations (all or nothing)
symbolic_linksboolSupports symbolic links (Unix/Linux filesystems)
copy_optimizationboolHas server-side optimized copy (without download/upload)
hash_on_metadataboolMD5/ETag available in metadata without reading the file
Performance
append_modeboolSupports append mode to add content
seek_supportboolSupports seek operations in file handles
Characteristics
readonlyboolBackend is read-only (HTTP, read-only mounts)
temporaryboolTemporary/ephemeral storage (memory backend)
Capabilities by Backend
S3
# S3 with versioning enabled
{
'read': True,
'write': True,
'delete': True,
'versioning': True, # ✓ If enabled on bucket
'metadata': True, # ✓ Custom metadata
'presigned_urls': True, # ✓ Temporary URLs
'hash_on_metadata': True, # ✓ ETag = MD5
'append_mode': False, # ✗ S3 doesn't support append
}
Local (Filesystem)
# Local filesystem
{
'read': True,
'write': True,
'delete': True,
'symbolic_links': True, # ✓ On Unix/Linux
'versioning': False, # ✗ No versioning
'presigned_urls': False, # ✗ No URLs
'metadata': False, # ✗ No custom metadata
}
HTTP/HTTPS
# HTTP storage (read-only)
{
'read': True,
'write': False, # ✗ Read-only
'delete': False, # ✗ Read-only
'readonly': True, # ✓ Explicitly read-only
'public_urls': True, # ✓ Public URLs
'versioning': False, # ✗ No versioning
}
Memory
# In-memory storage (testing)
{
'read': True,
'write': True,
'delete': True,
'temporary': True, # ✓ Data lost at end of process
'versioning': False, # ✗ No versioning
}
Practical Use
Adaptive Code
Write code that adapts to the available backend:
def save_with_metadata(node, data, author, tags):
"""Save file with metadata if supported"""
node.write(data, mode='wb')
# Add metadata only if supported
if node.capabilities.metadata:
node.set_metadata({
'author': author,
'tags': ','.join(tags)
})
print("Metadata saved")
else:
print("Metadata not supported, only file saved")
Conditional Features in UI
Show options only if available:
def get_file_actions(node):
"""Return available actions for a file"""
actions = ['download'] # Always available
if not node.capabilities.readonly:
actions.append('edit')
actions.append('delete')
if node.capabilities.versioning:
actions.append('view_history')
actions.append('rollback')
if node.capabilities.presigned_urls:
actions.append('share_link')
return actions
Preventive Validation
Check before attempting operations:
def backup_with_versioning(source, destination):
"""Backup with versioning if available"""
if not destination.capabilities.write:
raise ValueError(f"{destination.fullpath} is read-only!")
# Copy the file
source.copy_to(destination)
# Check if versioning is active
if destination.capabilities.versioning:
print(f"Backup saved, versions available: {destination.version_count}")
else:
print("Backup saved (versioning not available)")
Clear User Messages
Explain what’s possible and what’s not:
def explain_limitations(node):
"""Explain what you can do with this storage"""
caps = node.capabilities
print(f"Storage: {node._mount_name}")
if caps.readonly:
print("⚠️ Read-only - you cannot modify files")
if caps.temporary:
print("⚠️ Temporary storage - data will be lost")
features = []
if caps.versioning:
features.append("versioning")
if caps.metadata:
features.append("metadata")
if caps.presigned_urls:
features.append("temporary URLs")
if features:
print(f"✓ Supports: {', '.join(features)}")
else:
print("• Basic file operations (read/write)")
Capability String
Each backend has a __str__() method that lists features:
print(node.capabilities)
# Output: "versioning, metadata, presigned URLs, fast hashing"
# Use in error messages
try:
versions = node.versions
except PermissionError as e:
print(e)
# "s3 backend does not support versioning.
# Supported features: basic file operations"
When to Use Capabilities
✅ Use capabilities when:
You write libraries/tools that work with different storage types
You build UI where some features might not be available
You want to give clear and useful error messages
You do operations that not all backends support
❌ No need to check them if:
You always use the same storage type
You only do basic operations (read/write) supported everywhere
Your code is specific to one backend
Complete Example
from genro_storage import StorageManager
storage = StorageManager()
storage.configure([
{'name': 's3', 'type': 's3', 'bucket': 'my-bucket'},
{'name': 'local', 'type': 'local', 'path': '/data'},
{'name': 'web', 'type': 'http', 'base_url': 'https://example.com'}
])
def analyze_storage(mount_name):
"""Analyze capabilities of a storage"""
node = storage.node(f'{mount_name}:test.txt')
caps = node.capabilities
print(f"\n=== Storage: {mount_name} ===")
print(f"Type: {node._backend.__class__.__name__}")
print(f"Available features: {caps}")
# Check basic operations
can_write = caps.write and not caps.readonly
print(f"Can write: {'✓' if can_write else '✗'}")
# Advanced features
if caps.versioning:
print("✓ Versioning available")
if caps.metadata:
print("✓ Custom metadata")
if caps.presigned_urls:
print("✓ Can generate temporary URLs")
# Limitations
if caps.readonly:
print("⚠️ Read-only")
if caps.temporary:
print("⚠️ Temporary storage")
# Analyze all storages
analyze_storage('s3')
analyze_storage('local')
analyze_storage('web')
Adding New Capabilities
If you extend genro-storage with a new backend, declare its capabilities:
from genro_storage.backends.base import StorageBackend
from genro_storage.capabilities import BackendCapabilities
class MyCustomBackend(StorageBackend):
@property
def capabilities(self) -> BackendCapabilities:
return BackendCapabilities(
read=True,
write=True,
versioning=False, # My backend doesn't have versioning
metadata=True, # But supports metadata
# ... other capabilities
)
Capabilities are explicit and verifiable. No surprises!