Cover Page
Backend Page
Python with Starlette
To support uploading of large media files, first change to your chatterd folder and
install the following packages in your python’s virtual environment:
server$ cd ~/reactive/chatterd
server$ uv add python-multipart werkzeug
handlers.py
Edit the file handlers.py:
server$ vi handlers.py
Add the following libraries to the import block at the top of the file:
import os
from werkzeug.utils import secure_filename
Next add the postimages() function along with the saveFormFile() helper function.
We also specify our designated media directory to store image/video files, and maximum allowed media file size.
MEDIA_ROOT = '/home/ubuntu/reactive/chatterd/media/'
MEDIA_MXSZ = 10485760 # 10 MB
async def saveFormFile(fields, media, url, username, ext):
try:
file = fields[media]
if file.size > MEDIA_MXSZ:
# but the whole file will still be received, just not saved
raise BufferError
except KeyError:
return None # not an error, media not sent
except Exception:
raise
try:
if not (filename := secure_filename(username)):
raise NameError
filename = f'{filename}-{str(time.time())}{ext}'
filepath = os.path.join(MEDIA_ROOT, filename)
with open(filepath, 'wb') as f:
f.write(await file.read(MEDIA_MXSZ))
f.close()
return f'{url}{filename}'
except BaseException:
raise
async def postimages(request):
try:
url = str(request.url_for('media', path='/'))
# loading form-encoded data
async with request.form() as fields:
username = fields['username']
message = fields['message']
imageurl = await saveFormFile(fields, 'image', url, username, '.jpeg')
videourl = await saveFormFile(fields, 'video', url, username, '.mp4')
except BaseException as err:
print(f'{err=}')
return JSONResponse('Unprocessable entity', status_code=422)
try:
async with main.server.pool.connection() as connection:
async with connection.cursor() as cursor:
await cursor.execute('INSERT INTO chatts (username, message, id, imageurl, videourl) VALUES '
'(%s, %s, gen_random_uuid(), %s, %s);', (username, message, imageurl, videourl))
return JSONResponse({})
except StringDataRightTruncation as err:
print(f'Message too long: {str(err)}')
return JSONResponse(f'Message too long', status_code = 400)
except Exception as err:
print(f'{err=}')
return JSONResponse(f'{type(err).__name__}: {str(err)}', status_code = 500)
File size limit
Neither Starlette, uvicorn, nor Granian support limiting maximum data upload size. In handlers.py, we refuse to save an uploaded media file larger than MEDIA_MXSZ, but this check happens after Starlette has stored the upload as Python’s SpooledTemporaryFile, potentially overflowing the server’s storage space.
Reverse proxies, such as Nginx, can put a limit on the maximum upload size, but enforcement seems to be limited to checking the content of HTTP requests’ Content-Length header, which may not actually correspond to the real body size.
Next, make a copy of your getchatts() function inside your handlers.py file and name the copy
getimages(). In getimages(), replace the SELECT statement with the following: 'SELECT username, message, id, time, imageurl, videourl FROM chatts;'. This statement will retrieve all data, including our new image and video URLs from the PostgreSQL database.
We’re done with handlers.py. Save and exit the file.
main.py
Edit the file main.py:
server$ vi main.py
To serve image/video static files, add the StaticFiles import and add the Mount module to the existing import from starlette.routing such that the lines near the top of the file reads:
from starlette.routing import Mount
from starlette.staticfiles import StaticFiles
Find the routes array and add the following new routes for the new API endpoints /getimages
and /postimages. We also tell Starlette to redirect URL path /media to serve media files
from our designated media directory, whose value is stored in MEDIA_ROOT above.
Route('/getimages', handlers.getimages, methods=['GET']),
Route('/postimages', handlers.postimages, methods=['POST']),
Mount('/media', app=StaticFiles(directory=handlers.MEDIA_ROOT), name='media'),
We’re done with main.py. Save and exit the file.
Test run
To test run your server, launch it from the command line:
server$ sudo su
# You are now root, note the command-line prompt changed from '$' or '%' to '#'.
# You can do a lot of harm with all of root's privileges, so be very careful what you do.
server# source .venv/bin/activate
(chattterd) ubuntu@server:/home/ubuntu/reactive/chatterd# granian --host 0.0.0.0 --port 443 --interface asgi --ssl-certificate /home/ubuntu/reactive/chatterd.crt --ssl-keyfile /home/ubuntu/reactive/chatterd.key --access-log --workers-kill-timeout 1 main:server
# Hit ^C to end the test
(chattterd) ubuntu@server:/home/ubuntu/reactive/chatterd# exit
# So that you're no longer root.
server$
The cover backend spec provides instructions for Testing image and video upload.
References
| Prepared by Wendan Jiang, Tianyi Zhao, Ollie Elmgren, Benjamin Brengman, Mark Wassink, Alexander Wu, Yibo Pi, and Sugih Jamin | Last updated August 31st, 2025 |