aiohttp file uploading

Blog post can have images, let’s support them!

Extend Post structure

Add image BLOB field.

Field

Description

Type

id

Post id

int

title

Title

str

text

Content

str

owner

Post creator

str

editor

Last editor

str

image

Image content

bytes

Note

Usually you serve images by Content Delivery Network or save them on BLOB storage like Amazon S3. Database keeps an image URL only.

But for sake of simplicity we use database for storing the image content.

Image web handler

@router.get("/{post}/image")
async def render_post_image(request: web.Request) -> web.Response:
    post_id = request.match_info["post"]
    db = request.config_dict["DB"]
    async with db.execute("SELECT image FROM posts WHERE id = ?", [post_id]) as cursor:
        row = await cursor.fetchone()
        if row is None or row["image"] is None:
            img = PIL.Image.new("RGB", (64, 64), color=0)
            fp = io.BytesIO()
            img.save(fp, format="JPEG")
            content = fp.getvalue()
        else:
            content = row["image"]
    return web.Response(body=content, content_type="image/jpeg")

Return a black image if nothing is present in DB, the image content with image/jpeg content type otherwise.

To render image we need to change view.html template:

{% block content %}
<h1>{{ post.title }}</h1>
...
<p><img src="/{{ post.id }}/image"></p>
...
{% endblock %}

Upload form

Add <input type="file" name="image" accept="image/png, image/jpeg"> HTML form field:

{% block content %}
<h1>Edit: {{ post.title }}</h1>
<form action="/{{ post.id }}/edit" method="POST"
      enctype="multipart/form-data">
  ...
  <input type="file" name="image" accept="image/png, image/jpeg">
  ...
</form>
{% endblock %}

Handle uploaded image

@router.post("/{post}/edit")
async def edit_post_apply(request: web.Request) -> web.Response:
    post_id = request.match_info["post"]
    db = request.config_dict["DB"]
    post = await request.post()
    image = post.get("image")
    await db.execute(
        f"UPDATE posts SET title = ?, text = ? WHERE id = ?",
        [post["title"], post["text"], post_id],
    )
    if image:
        img_content = image.file.read()  # type: ignore
        await apply_image(db, post_id, img_content)
    await db.commit()
    raise web.HTTPSeeOther(location=f"/{post_id}/edit")

If a new image was provided by HTML form – update DB:

async def apply_image(
    db: aiosqlite.Connection, post_id: int, img_content: bytes
) -> None:
    buf = io.BytesIO(img_content)
    loop = asyncio.get_event_loop()
    img = PIL.Image.open(buf)
    new_img = await loop.run_in_executor(None, img.resize, (64, 64), PIL.Image.LANCZOS)
    out_buf = io.BytesIO()
    new_img.save(out_buf, format="JPEG")
    await db.execute(
        "UPDATE posts SET image = ? WHERE id = ?", [out_buf.getvalue(), post_id]
    )

Full example for server with images support

Example for HTML version of blogs server with images: Full server example with templates and image uploading