|
1 | 1 | # **File & Image Column Types**
|
2 | 2 |
|
| 3 | +EllarSQL provides **File** and **Image** column type descriptors to attach files to your models. It integrates seamlessly with the [Sqlalchemy-file](https://jowilf.github.io/sqlalchemy-file/tutorial/using-files-in-models/){target="_blank"} and [EllarStorage](https://github.com/python-ellar/ellar-storage){target="_blank"} packages. |
| 4 | + |
3 | 5 | ## **FileField Column**
|
| 6 | +`FileField` can handle any type of file, making it a versatile option for file storage in your database models. |
| 7 | + |
| 8 | +```python |
| 9 | +from ellar_sql import model |
| 10 | + |
| 11 | +class Attachment(model.Model): |
| 12 | + __tablename__ = "attachments" |
| 13 | + |
| 14 | + id: model.Mapped[int] = model.mapped_column(autoincrement=True, primary_key=True) |
| 15 | + name: model.Mapped[str] = model.mapped_column(model.String(50), unique=True) |
| 16 | + content: model.Mapped[model.typeDecorator.File] = model.mapped_column(model.typeDecorator.FileField) |
| 17 | +``` |
| 18 | + |
4 | 19 | ## **ImageField Column**
|
5 |
| -### **Uploading File** |
6 |
| -#### Save file object |
7 |
| -#### Retrieve file object |
8 |
| -#### Extra and Headers |
9 |
| -#### Metadata |
10 |
| -## **Validators** |
11 |
| -## **Processors** |
| 20 | +`ImageField` builds on **FileField**, adding validation to ensure the uploaded file is a valid image. This guarantees that only image files are stored. |
| 21 | + |
| 22 | +```python |
| 23 | +from ellar_sql import model |
| 24 | + |
| 25 | +class Book(model.Model): |
| 26 | + __tablename__ = "books" |
| 27 | + |
| 28 | + id: model.Mapped[int] = model.mapped_column(autoincrement=True, primary_key=True) |
| 29 | + title: model.Mapped[str] = model.mapped_column(model.String(100), unique=True) |
| 30 | + cover: model.Mapped[model.typeDecorator.File] = model.mapped_column( |
| 31 | + model.typeDecorator.ImageField( |
| 32 | + thumbnail_size=(128, 128), |
| 33 | + ) |
| 34 | + ) |
| 35 | +``` |
| 36 | + |
| 37 | +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`. |
| 38 | + |
| 39 | +**Note**: `ImageField` requires the [`Pillow`](https://pypi.org/project/pillow/) package: |
| 40 | +```shell |
| 41 | +pip install pillow |
| 42 | +``` |
| 43 | + |
| 44 | +### **Uploading Files** |
| 45 | +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](https://github.com/python-ellar/ellar-storage?tab=readme-ov-file#storagemodulesetup){target="_blank"}. |
| 46 | + |
| 47 | +### **Saving Files** |
| 48 | +EllarSQL supports `Starlette.datastructures.UploadFile` for Image and File Fields, simplifying file saving directly from requests. |
| 49 | + |
| 50 | +For example: |
| 51 | + |
| 52 | +```python |
| 53 | +import ellar.common as ecm |
| 54 | +from ellar_sql import model |
| 55 | +from ..models import Book |
| 56 | +from .schema import BookSchema |
| 57 | + |
| 58 | +@ecm.Controller |
| 59 | +class BooksController(ecm.ControllerBase): |
| 60 | + @ecm.post("/", response={201: BookSchema}) |
| 61 | + def create_book( |
| 62 | + self, |
| 63 | + title: ecm.Body[str], |
| 64 | + cover: ecm.File[ecm.UploadFile], |
| 65 | + session: ecm.Inject[model.Session], |
| 66 | + ): |
| 67 | + book = Book(title=title, cover=cover) |
| 68 | + session.add(book) |
| 69 | + session.commit() |
| 70 | + session.refresh(book) |
| 71 | + return book |
| 72 | +``` |
| 73 | + |
| 74 | +#### Retrieving File Object |
| 75 | +The object retrieved from an Image or File Field is an instance of [`ellar_sql.model.typeDecorator.File`](https://github.com/python-ellar/ellar-sql/blob/master/ellar_sql/model/typeDecorator/file/file.py). |
| 76 | + |
| 77 | +```python |
| 78 | +@ecm.get("/{book_id:int}", response={200: BookSchema}) |
| 79 | +def get_book_by_id( |
| 80 | + self, |
| 81 | + book_id: int, |
| 82 | + session: ecm.Inject[model.Session], |
| 83 | +): |
| 84 | + book = session.execute( |
| 85 | + model.select(Book).where(Book.id == book_id) |
| 86 | + ).scalar_one() |
| 87 | + |
| 88 | + assert book.cover.saved # saved is True for a saved file |
| 89 | + assert book.cover.file.read() is not None # access file content |
| 90 | + |
| 91 | + assert book.cover.filename is not None # `unnamed` when no filename is provided |
| 92 | + assert book.cover.file_id is not None # UUID v4 |
| 93 | + |
| 94 | + assert book.cover.upload_storage == "default" |
| 95 | + assert book.cover.content_type is not None |
| 96 | + |
| 97 | + assert book.cover.uploaded_at is not None |
| 98 | + assert len(book.cover.files) == 2 # original image and generated thumbnail image |
| 99 | + |
| 100 | + return book |
| 101 | +``` |
| 102 | + |
| 103 | +#### Adding More Information to a Saved File Object |
| 104 | +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. |
| 105 | + |
| 106 | +```python |
| 107 | +from ellar_sql.model.typeDecorator import File |
| 108 | +from ..models import Book |
| 109 | + |
| 110 | +content = File(open("./example.png", "rb"), custom_key1="custom_value1", custom_key2="custom_value2") |
| 111 | +content["custom_key3"] = "custom_value3" |
| 112 | +book = Book(title="Dummy", cover=content) |
| 113 | + |
| 114 | +session.add(book) |
| 115 | +session.commit() |
| 116 | +session.refresh(book) |
| 117 | + |
| 118 | +assert book.cover.custom_key1 == "custom_value1" |
| 119 | +assert book.cover.custom_key2 == "custom_value2" |
| 120 | +assert book.cover["custom_key3"] == "custom_value3" |
| 121 | +``` |
| 122 | + |
| 123 | +## **Extra and Headers** |
| 124 | +`Apache-libcloud` allows you to store each object with additional attributes or headers. |
| 125 | + |
| 126 | +You can add extras and headers in two ways: |
| 127 | + |
| 128 | +### Inline Field Declaration |
| 129 | +You can specify these extras and headers directly in the field declaration: |
| 130 | + |
| 131 | +```python |
| 132 | +from ellar_sql import model |
| 133 | + |
| 134 | +class Attachment(model.Model): |
| 135 | + __tablename__ = "attachments" |
| 136 | + |
| 137 | + id: model.Mapped[int] = model.mapped_column(autoincrement=True, primary_key=True) |
| 138 | + name: model.Mapped[str] = model.mapped_column(model.String(50), unique=True) |
| 139 | + content: model.Mapped[model.typeDecorator.File] = model.mapped_column(model.typeDecorator.FileField( |
| 140 | + extra={ |
| 141 | + "acl": "private", |
| 142 | + "dummy_key": "dummy_value", |
| 143 | + "meta_data": {"key1": "value1", "key2": "value2"}, |
| 144 | + }, |
| 145 | + headers={ |
| 146 | + "Access-Control-Allow-Origin": "http://test.com", |
| 147 | + "Custom-Key": "xxxxxxx", |
| 148 | + }, |
| 149 | + )) |
| 150 | +``` |
| 151 | + |
| 152 | +### In File Object |
| 153 | +Alternatively, you can set extras and headers in the File object itself: |
| 154 | + |
| 155 | +```python |
| 156 | +from ellar_sql.model.typeDecorator import File |
| 157 | + |
| 158 | +attachment = Attachment( |
| 159 | + name="Public document", |
| 160 | + content=File(DummyFile(), extra={"acl": "public-read"}), |
| 161 | +) |
| 162 | +session.add(attachment) |
| 163 | +session.commit() |
| 164 | +session.refresh(attachment) |
| 165 | + |
| 166 | +assert attachment.content.file.object.extra["acl"] == "public-read" |
| 167 | +``` |
| 168 | + |
| 169 | +## **Uploading to a Specific Storage** |
| 170 | +By default, files are uploaded to the `default` storage specified in `StorageModule`. |
| 171 | +You can change this by specifying a different `upload_storage` in the field declaration: |
| 172 | + |
| 173 | +```python |
| 174 | +from ellar_sql import model |
| 175 | + |
| 176 | +class Book(model.Model): |
| 177 | + __tablename__ = "books" |
| 178 | + |
| 179 | + id: model.Mapped[int] = model.mapped_column(autoincrement=True, primary_key=True) |
| 180 | + title: model.Mapped[str] = model.mapped_column(model.String(100), unique=True) |
| 181 | + cover: model.Mapped[model.typeDecorator.File] = model.mapped_column( |
| 182 | + model.typeDecorator.ImageField( |
| 183 | + thumbnail_size=(128, 128), upload_storage="bookstore" |
| 184 | + ) |
| 185 | + ) |
| 186 | +``` |
| 187 | +Setting `upload_storage="bookstore"` ensures |
| 188 | +that the book cover is uploaded to the `bookstore` container defined in `StorageModule`. |
| 189 | + |
12 | 190 | ## **Multiple Files**
|
| 191 | +A File or Image Field column can be configured to hold multiple files by setting `multiple=True`. |
| 192 | + |
| 193 | +For example: |
| 194 | + |
| 195 | +```python |
| 196 | +import typing as t |
| 197 | +from ellar_sql import model |
| 198 | + |
| 199 | +class Article(model.Model): |
| 200 | + __tablename__ = "articles" |
| 201 | + |
| 202 | + id: model.Mapped[int] = model.mapped_column(autoincrement=True, primary_key=True) |
| 203 | + title: model.Mapped[str] = model.mapped_column(model.String(100), unique=True) |
| 204 | + documents: model.Mapped[t.List[model.typeDecorator.File]] = model.mapped_column( |
| 205 | + model.typeDecorator.FileField(multiple=True, upload_storage="documents") |
| 206 | + ) |
| 207 | +``` |
| 208 | +The `Article` model's `documents` column will store a list of files, |
| 209 | +applying validators and processors to each file individually. |
| 210 | +The returned model is a list of File objects. |
| 211 | + |
| 212 | +#### Saving Multiple File Fields |
| 213 | +Saving multiple files is as simple as passing a list of file contents to the file field column. For example: |
| 214 | + |
| 215 | +```python |
| 216 | +import typing as t |
| 217 | +import ellar.common as ecm |
| 218 | +from ellar_sql import model |
| 219 | +from ..models import Article |
| 220 | +from .schema import ArticleSchema |
| 221 | + |
| 222 | +@ecm.Controller |
| 223 | +class ArticlesController(ecm.ControllerBase): |
| 224 | + @ecm.post("/", response={201: ArticleSchema}) |
| 225 | + def create_article( |
| 226 | + self, |
| 227 | + title: ecm.Body[str], |
| 228 | + documents: ecm.File[t.List[ecm.UploadFile]], |
| 229 | + session: ecm.Inject[model.Session], |
| 230 | + ): |
| 231 | + article = Article( |
| 232 | + title=title, documents=[ |
| 233 | + model.typeDecorator.File( |
| 234 | + content="Hello World", |
| 235 | + filename="hello.txt", |
| 236 | + content_type="text/plain", |
| 237 | + ) |
| 238 | + ] + documents |
| 239 | + ) |
| 240 | + session.add(article) |
| 241 | + session.commit() |
| 242 | + session.refresh(article) |
| 243 | + return article |
| 244 | +``` |
| 245 | + |
| 246 | +## **See Also** |
| 247 | +- [Validators](https://jowilf.github.io/sqlalchemy-file/tutorial/using-files-in-models/#validators) |
| 248 | +- [Processors](https://jowilf.github.io/sqlalchemy-file/tutorial/using-files-in-models/#processors) |
| 249 | + |
| 250 | +For a more comprehensive hands-on experience, check out the [file-field-example](https://github.com/python-ellar/ellar-sql/tree/main/samples/file-field-example) project. |
0 commit comments