File Storage¶
SQLAlchemy type for storing file objects with automatic cloud storage integration.
FileObject Type¶
The FileObject type stores file metadata in the database while delegating file content to configurable storage backends.
Characteristics:
Python type:
FileObjectorFileObjectListDatabase storage: JSON/JSONB (metadata only)
File content: External storage backend (S3, GCS, Azure, local)
Automatic cleanup: Files deleted when records removed
StoredObject Column Type¶
Use StoredObject to define file storage columns:
from typing import Optional
from sqlalchemy.orm import Mapped, mapped_column
from advanced_alchemy.base import UUIDAuditBase
from advanced_alchemy.types import FileObject, FileObjectList, StoredObject
class Document(UUIDAuditBase):
__tablename__ = "documents"
title: "Mapped[str]"
# Single file
attachment: "Mapped[Optional[FileObject]]" = mapped_column(
StoredObject(backend="documents"),
nullable=True,
)
# Multiple files
images: "Mapped[Optional[FileObjectList]]" = mapped_column(
StoredObject(backend="documents", multiple=True),
nullable=True,
)
from sqlalchemy.orm import Mapped, mapped_column
from advanced_alchemy.base import UUIDAuditBase
from advanced_alchemy.types import FileObject, FileObjectList, StoredObject
class Document(UUIDAuditBase):
__tablename__ = "documents"
title: "Mapped[str]"
# Single file
attachment: "Mapped[FileObject | None]" = mapped_column(
StoredObject(backend="documents"),
nullable=True,
)
# Multiple files
images: "Mapped[FileObjectList | None]" = mapped_column(
StoredObject(backend="documents", multiple=True),
nullable=True,
)
FileObject Attributes¶
FileObject instances contain file metadata and provide file operations:
file_obj = FileObject(
backend="s3",
filename="report.pdf",
content_type="application/pdf",
metadata={"author": "John", "version": "1.0"},
content=pdf_bytes,
)
# Attributes
print(file_obj.backend) # "s3"
print(file_obj.filename) # "report.pdf"
print(file_obj.content_type) # "application/pdf"
print(file_obj.size) # File size in bytes
print(file_obj.metadata) # {"author": "John", "version": "1.0"}
File Operations¶
Saving Files¶
from advanced_alchemy.types import FileObject
# Create file object
file_obj = FileObject(
backend="s3",
filename="document.pdf",
content_type="application/pdf",
content=pdf_bytes,
)
# Async save
await file_obj.save_async()
# Sync save
file_obj.save()
Retrieving Content¶
# Async get content
content = await file_obj.get_content_async()
# Sync get content
content = file_obj.get_content()
Deleting Files¶
# Async delete
await file_obj.delete_async()
# Sync delete
file_obj.delete()
Signed URLs¶
Generate temporary URLs for direct upload/download:
# Download URL (expires in 1 hour)
download_url = await file_obj.sign_async(expires_in=3600)
# Upload URL
upload_url = await file_obj.sign_async(expires_in=300, for_upload=True)
# Sync version
download_url = file_obj.sign(expires_in=3600)
Metadata Management¶
# Set metadata during creation
file_obj = FileObject(
backend="s3",
filename="invoice.pdf",
metadata={"invoice_number": "INV-001", "amount": "1500.00"},
content=pdf_bytes,
)
# Update metadata
file_obj.update_metadata({"status": "paid"})
await file_obj.save_async()
# Access metadata
invoice_number = file_obj.metadata["invoice_number"]
Storage Backend Configuration¶
Storage backends must be registered before use. See Storage Backends for detailed backend configuration.
Quick Example¶
from advanced_alchemy.types.file_object import storages
from advanced_alchemy.types.file_object.backends.fsspec import FSSpecBackend
# Register local filesystem backend
storages.register_backend(FSSpecBackend(
key="documents",
fs="file",
prefix="/var/app/uploads",
))
# Now use in models
class Document(UUIDAuditBase):
attachment: "Mapped[Optional[FileObject]]" = mapped_column(
StoredObject(backend="documents")
)
Automatic File Cleanup¶
When using framework integrations, file cleanup is automatic:
# Update file - old file deleted automatically
doc.attachment = FileObject(
backend="documents",
filename="updated.pdf",
content=new_pdf_bytes,
)
await session.commit() # Old file deleted, new file saved
# Clear file - file deleted automatically
doc.attachment = None
await session.commit() # File deleted from storage
# Delete model - all files deleted automatically
await session.delete(doc)
await session.commit() # All associated files deleted
Manual Cleanup Configuration¶
For non-framework applications, configure cleanup manually:
from advanced_alchemy.config import SQLAlchemyAsyncConfig
config = SQLAlchemyAsyncConfig(
connection_string="postgresql+asyncpg://user:pass@localhost/db",
)
# Configure file cleanup listeners
config.configure_listeners()
Framework Integration¶
File upload patterns for web frameworks.
Litestar¶
from litestar import post
from litestar.datastructures import UploadFile
from advanced_alchemy.types import FileObject
@post("/documents")
async def upload_document(
data: UploadFile,
documents_service: "DocumentService",
) -> "Document":
"""Upload document file."""
doc = await documents_service.create(
DocumentModel(
title=data.filename or "untitled",
attachment=FileObject(
backend="documents",
filename=data.filename or "file",
content_type=data.content_type,
content=await data.read(),
),
)
)
return documents_service.to_schema(doc, schema_type=DocumentSchema)
FastAPI¶
from fastapi import UploadFile
from advanced_alchemy.types import FileObject
@app.post("/documents")
async def upload_document(
file: UploadFile,
service: "DocumentService" = Depends(get_document_service),
) -> "Document":
"""Upload document file."""
content = await file.read()
doc = await service.create(
DocumentModel(
title=file.filename or "untitled",
attachment=FileObject(
backend="documents",
filename=file.filename or "file",
content_type=file.content_type,
content=content,
),
)
)
return doc
Flask¶
from flask import request
from advanced_alchemy.types import FileObject
@app.route("/documents", methods=["POST"])
def upload_document():
"""Upload document file."""
file = request.files["file"]
doc = document_service.create(
DocumentModel(
title=file.filename or "untitled",
attachment=FileObject(
backend="documents",
filename=file.filename or "file",
content_type=file.content_type,
content=file.read(),
),
)
)
return doc.to_dict()
Common Patterns¶
Unique Filenames¶
Prevent filename collisions with UUID-based names:
from uuid import uuid4
from pathlib import Path
def generate_storage_path(original_filename: str) -> str:
"""Generate unique storage path preserving extension."""
ext = Path(original_filename).suffix
return f"{uuid4()}{ext}"
file_obj = FileObject(
backend="documents",
filename=data.filename or "file", # Display name
to_filename=generate_storage_path(data.filename or "file"), # Storage name
content=await data.read(),
)
File Validation¶
Validate file size and type before storage:
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB
ALLOWED_TYPES = {"application/pdf", "image/jpeg", "image/png"}
async def upload_validated(
data: UploadFile,
service: "DocumentService",
) -> "Document":
"""Upload file with validation."""
# Validate content type
if data.content_type not in ALLOWED_TYPES:
raise ValueError(f"file type {data.content_type} not allowed")
# Read and validate size
content = await data.read()
if len(content) > MAX_FILE_SIZE:
raise ValueError("file size exceeds 10 MB limit")
# Create document
doc = await service.create(
DocumentModel(
title=data.filename or "untitled",
attachment=FileObject(
backend="documents",
filename=data.filename or "file",
content_type=data.content_type,
content=content,
),
)
)
return doc
Multiple File Upload¶
@post("/galleries")
async def upload_gallery(
files: "list[UploadFile]",
service: "GalleryService",
) -> "Gallery":
"""Upload multiple images."""
images = []
for file in files:
images.append(FileObject(
backend="images",
filename=file.filename or "image",
content_type=file.content_type,
content=await file.read(),
))
gallery = await service.create(
GalleryModel(
title="New Gallery",
images=images,
)
)
return service.to_schema(gallery, schema_type=GallerySchema)
Direct Upload URLs¶
Generate signed URLs for client-side direct upload:
@post("/documents/upload-url")
async def create_upload_url(filename: str) -> "dict[str, str]":
"""Generate signed upload URL."""
file_obj = FileObject(
backend="documents",
filename=filename,
)
upload_url = await file_obj.sign_async(expires_in=300, for_upload=True)
return {
"upload_url": upload_url,
"filename": filename,
}
Testing¶
In-Memory Storage¶
Use memory backend for tests:
import pytest
from advanced_alchemy.types.file_object import storages
from advanced_alchemy.types.file_object.backends.fsspec import FSSpecBackend
@pytest.fixture
def memory_storage():
"""Configure in-memory storage for tests."""
backend = FSSpecBackend(key="test-storage", fs="memory")
storages.register_backend(backend)
yield backend
storages._backends.pop("test-storage", None)
async def test_file_upload(memory_storage, document_service):
"""Test file upload with in-memory storage."""
doc = await document_service.create(
DocumentModel(
title="Test",
attachment=FileObject(
backend="test-storage",
filename="test.txt",
content=b"Hello, World!",
),
)
)
assert doc.attachment is not None
content = await doc.attachment.get_content_async()
assert content == b"Hello, World!"
Performance Considerations¶
Database Field Size¶
FileObject metadata is stored as JSON in the database. Limit metadata size:
# Minimal metadata
file_obj = FileObject(
backend="documents",
filename="report.pdf",
content=pdf_bytes,
)
# Avoid large metadata
file_obj = FileObject(
backend="documents",
filename="report.pdf",
metadata={
"preview": base64_encoded_thumbnail, # Don't store large data
},
content=pdf_bytes,
)
Batch Operations¶
Use batch operations for multiple files:
# Save multiple files in parallel
import asyncio
files = [
FileObject(backend="documents", filename=f"file{i}.txt", content=b"data")
for i in range(100)
]
await asyncio.gather(*[f.save_async() for f in files])
See Also¶
Storage Backends - Storage backend configuration
FSSpec Backend - FSSpec backend details
Obstore Backend - Obstore backend details
Basic Types - Other custom types
types - Complete API reference