Handling Files
Now you have decided on the storage its time to start saving files.
The first thing to do it to setup an attachment class. This is the SQLAlchemy field that stores the info for the file including its location in the storage. It is also what interacts between the table and the storage to save and retrieve the files.
from starlette_files.constants import MB
from starlette_files.fields import FileAttachment
class FileType(FileAttachment):
# your storage
storage = my_storage
# directory in the stroage to save files to
directory = "files"
# allowed content types
allowed_content_types = ["application/pdf"]
# maximum allowed size in bytes
max_length = MB * 5
About content types
To determine if the content type is allowed we use a package called python-magic. This identifies file types by checking their headers according to a predefined list of file types. This guards against the likes of someone renaming a .exe to a .pdf. This should be able to detect the file is infact an exe and will raise an exception if not valid.
Please read their docs on installation as you will need additional os dependencies.
Next setup your table to include the field:
import sqlalchemy as sa
class File(Base):
file = sa.Column(FileType.as_mutable(sa.JSON))
Saving Files
async def post(request):
form = await request.form()
session = Session()
file_obj = FileType.create_from(
file=form["file"].file,
original_filename=form["file"].filename
)
instance = File(file=file_obj)
session.add(instance)
session.commit()
# return your response
An example form:
<form method="post" enctype="multipart/form-data">
<input type="file" name="file" required>
<input type="submit" value="Submit">
</form>
Working with Files
A FileAttachment
is a sqlalchemy.ext.mutable.MutableDict
and therefore stores
JSON as its value in the database. An example of this is:
>>> instance = session.query(File).first()
>>> instance.file
{
'original_filename': 'example.pdf',
'uploaded_on': 1576792678,
'content_type': 'application/pdf',
'extension': '.pdf',
'file_size': 2897,
'saved_filename': '9fabf09a-d915-48e5-8775-4f553c571a0b.pdf'
}
Each of the above has a property too so you can just do:
>>> instance.file.original_filename
'example.pdf'
Additional properties include:
@property
def path(self) -> str:
"""
the path located within the storage
if the directory on the FileAttachment is 'files' and the
saved_filename is 'foo.txt' this will be 'files/foo.txt'
"""
@property
def locate(self) -> str:
"""
this differs between storages and uses .path to get the file.
using .path allows you to move the files to a different storage
and as long as they live in the same path the rest will work as usual.
for the filesystem storage this is the full file path on the system
ie: /storage-path/files/foo.txt
for the s3 storage its the url to access the file
ie: https://bucket.s3.....storage-path/files/foo.txt
"""
@property
def open(self) -> typing.IO:
"""
opens the file
with file.open as f:
# do something with f
"""