Skip to content

File & Image Column Types

EllarSQL provides File and Image column type descriptors to attach files to your models. It integrates seamlessly with the Sqlalchemy-file and EllarStorage packages.

FileField Column

FileField can handle any type of file, making it a versatile option for file storage in your database models.

from ellar_sql import model

class Attachment(model.Model):
    __tablename__ = "attachments"

    id: model.Mapped[int] = model.mapped_column(autoincrement=True, primary_key=True)
    name: model.Mapped[str] = model.mapped_column(model.String(50), unique=True)
    content: model.Mapped[model.typeDecorator.File] = model.mapped_column(model.typeDecorator.FileField)

ImageField Column

ImageField builds on FileField, adding validation to ensure the uploaded file is a valid image. This guarantees that only image files are stored.

from ellar_sql import model

class Book(model.Model):
    __tablename__ = "books"

    id: model.Mapped[int] = model.mapped_column(autoincrement=True, primary_key=True)
    title: model.Mapped[str] = model.mapped_column(model.String(100), unique=True)
    cover: model.Mapped[model.typeDecorator.File] = model.mapped_column(
        model.typeDecorator.ImageField(
            thumbnail_size=(128, 128),
        )
    )

By setting thumbnail_size, an additional thumbnail image is created and saved alongside the original cover image. You can access the thumbnail via book.cover.thumbnail.

Note: ImageField requires the Pillow package:

pip install pillow

Uploading Files

To handle where files are saved, EllarSQL's File and Image Fields require EllarStorage's StorageModule setup. For more details, refer to the StorageModule setup.

Saving Files

EllarSQL supports Starlette.datastructures.UploadFile for Image and File Fields, simplifying file saving directly from requests.

For example:

import ellar.common as ecm
from ellar_sql import model
from ..models import Book
from .schema import BookSchema

@ecm.Controller
class BooksController(ecm.ControllerBase):
    @ecm.post("/", response={201: BookSchema})
    def create_book(
        self,
        title: ecm.Body[str],
        cover: ecm.File[ecm.UploadFile],
        session: ecm.Inject[model.Session],
    ):
        book = Book(title=title, cover=cover)
        session.add(book)
        session.commit()
        session.refresh(book)
        return book

Retrieving File Object

The object retrieved from an Image or File Field is an instance of ellar_sql.model.typeDecorator.File.

@ecm.get("/{book_id:int}", response={200: BookSchema})
def get_book_by_id(
    self,
    book_id: int, 
    session: ecm.Inject[model.Session],
):
    book = session.execute(
        model.select(Book).where(Book.id == book_id)
    ).scalar_one()

    assert book.cover.saved  # saved is True for a saved file
    assert book.cover.file.read() is not None  # access file content

    assert book.cover.filename is not None  # `unnamed` when no filename is provided
    assert book.cover.file_id is not None  # UUID v4

    assert book.cover.upload_storage == "default"
    assert book.cover.content_type is not None

    assert book.cover.uploaded_at is not None
    assert len(book.cover.files) == 2  # original image and generated thumbnail image

    return book

Adding More Information to a Saved File Object

The File object behaves like a Python dictionary, allowing you to add custom metadata. Be careful not to overwrite default attributes used by the File object internally.

from ellar_sql.model.typeDecorator import File
from ..models import Book

content = File(open("./example.png", "rb"), custom_key1="custom_value1", custom_key2="custom_value2")
content["custom_key3"] = "custom_value3"
book = Book(title="Dummy", cover=content)

session.add(book)
session.commit()
session.refresh(book)

assert book.cover.custom_key1 == "custom_value1"
assert book.cover.custom_key2 == "custom_value2"
assert book.cover["custom_key3"] == "custom_value3"

Extra and Headers

Apache-libcloud allows you to store each object with additional attributes or headers.

You can add extras and headers in two ways:

Inline Field Declaration

You can specify these extras and headers directly in the field declaration:

from ellar_sql import model

class Attachment(model.Model):
    __tablename__ = "attachments"

    id: model.Mapped[int] = model.mapped_column(autoincrement=True, primary_key=True)
    name: model.Mapped[str] = model.mapped_column(model.String(50), unique=True)
    content: model.Mapped[model.typeDecorator.File] = model.mapped_column(model.typeDecorator.FileField(
        extra={
            "acl": "private",
            "dummy_key": "dummy_value",
            "meta_data": {"key1": "value1", "key2": "value2"},
        },
        headers={
            "Access-Control-Allow-Origin": "http://test.com",
            "Custom-Key": "xxxxxxx",
        },
    ))

In File Object

Alternatively, you can set extras and headers in the File object itself:

from ellar_sql.model.typeDecorator import File

attachment = Attachment(
    name="Public document",
    content=File(DummyFile(), extra={"acl": "public-read"}),
)
session.add(attachment)
session.commit()
session.refresh(attachment)

assert attachment.content.file.object.extra["acl"] == "public-read"

Uploading to a Specific Storage

By default, files are uploaded to the default storage specified in StorageModule. You can change this by specifying a different upload_storage in the field declaration:

from ellar_sql import model

class Book(model.Model):
    __tablename__ = "books"

    id: model.Mapped[int] = model.mapped_column(autoincrement=True, primary_key=True)
    title: model.Mapped[str] = model.mapped_column(model.String(100), unique=True)
    cover: model.Mapped[model.typeDecorator.File] = model.mapped_column(
        model.typeDecorator.ImageField(
            thumbnail_size=(128, 128), upload_storage="bookstore"
        )
    )
Setting upload_storage="bookstore" ensures that the book cover is uploaded to the bookstore container defined in StorageModule.

Multiple Files

A File or Image Field column can be configured to hold multiple files by setting multiple=True.

For example:

import typing as t
from ellar_sql import model

class Article(model.Model):
    __tablename__ = "articles"

    id: model.Mapped[int] = model.mapped_column(autoincrement=True, primary_key=True)
    title: model.Mapped[str] = model.mapped_column(model.String(100), unique=True)
    documents: model.Mapped[t.List[model.typeDecorator.File]] = model.mapped_column(
        model.typeDecorator.FileField(multiple=True, upload_storage="documents")
    )
The Article model's documents column will store a list of files, applying validators and processors to each file individually. The returned model is a list of File objects.

Saving Multiple File Fields

Saving multiple files is as simple as passing a list of file contents to the file field column. For example:

import typing as t
import ellar.common as ecm
from ellar_sql import model
from ..models import Article
from .schema import ArticleSchema

@ecm.Controller
class ArticlesController(ecm.ControllerBase):
    @ecm.post("/", response={201: ArticleSchema})
    def create_article(
        self,
        title: ecm.Body[str],
        documents: ecm.File[t.List[ecm.UploadFile]],
        session: ecm.Inject[model.Session],
    ):
        article = Article(
            title=title, documents=[
                model.typeDecorator.File(
                    content="Hello World",
                    filename="hello.txt",
                    content_type="text/plain",
                )
            ] + documents
        )
        session.add(article)
        session.commit()
        session.refresh(article)
        return article

See Also

For a more comprehensive hands-on experience, check out the file-field-example project.