This is something that took me a while to figure out, and I’m surprised why no one put out a simple tutorial for this.

So here’s what we’re going to do today.

  • Write an API to upload “files” to the web server local directory
  • Each request will have a unique request ID folder (this helps prevent same filename collision)
  • Manage file Path in a “clean” manner

Let’s get right into it.

Imports

Here’s what you will need to import:

import os
import shutil
import uuid
from pathlib import Path
from typing import List

from fastapi import FastAPI, File, UploadFile
from fastapi.responses import HTMLResponse

Request Body

Here’s your actual API body:

@app.post("/upload/files/")
async def create_upload_files(files: List[UploadFile] = File(...)):
	# base directory 
    WORK_DIR = Path(config.get('WORK_DIR'))
    # UUID to prevent file overwrite
	REQUEST_ID = Path(str(uuid.uuid4())[:8])
    # 'beautiful' path concat instead of WORK_DIR + '/' + REQUEST_ID
	WORKSPACE = WORK_DIR / REQUEST_ID
	if not os.path.exists(WORKSPACE):
    	# recursively create workdir/unique_id
		os.makedirs(WORKSPACE)
    # iterate through all uploaded files
	for file in files:
		FILE_PATH = Path(file.filename)
		WRITE_PATH = WORKSPACE / FILE_PATH
		with open(str(WRITE_PATH) ,'wb') as myfile:
			contents = await file.read()
			myfile.write(contents)
	# return local file paths
	return {"file_paths": [str(WORKSPACE)+'/'+file.filename for file in files]}


@app.get("/")
async def main():
	content = """
<body>
<form action="/upload/files/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
</body>
	"""
	return HTMLResponse(content=content)

Download Structure

Which will download your files in the following tree:

➜  simple-fastapi-upload git:(master) ✗ tree                 
.
├── data # WORK_DIR
│   ├── 366b2e73 # unique to each request
│   │   ├── image-picker.css # files uploaded in that request
│   │   ├── image-picker.js
│   │   ├── index.html
│   │   └── jquery-3.0.0.min.js
│   ├── 9bce9738
│   │   ├── image-picker.css
│   │   ├── image-picker.js
│   │   ├── index.html
│   │   └── jquery-3.0.0.min.js
│   ├── a0ac30a3
│   │   ├── image-picker.css
│   │   ├── image-picker.js
│   │   ├── index.html
│   │   └── jquery-3.0.0.min.js
│   └── ff400b65
│       ├── image-picker.css
│       ├── image-picker.js
│       ├── index.html
│       └── jquery-3.0.0.min.js

Code

Tying it all together:

import os
import shutil
import uuid
from pathlib import Path
from typing import List

from fastapi import FastAPI, File, UploadFile
from fastapi.responses import HTMLResponse

app = FastAPI()

config = {
	'WORK_DIR':'data/'
}

@app.post("/upload/files/")
async def create_upload_files(files: List[UploadFile] = File(...)):
	# base directory 
    WORK_DIR = Path(config.get('WORK_DIR'))
    # UUID to prevent file overwrite
	REQUEST_ID = Path(str(uuid.uuid4())[:8])
    # 'beautiful' path concat instead of WORK_DIR + '/' + REQUEST_ID
	WORKSPACE = WORK_DIR / REQUEST_ID
	if not os.path.exists(WORKSPACE):
    	# recursively create workdir/unique_id
		os.makedirs(WORKSPACE)
    # iterate through all uploaded files
	for file in files:
		FILE_PATH = Path(file.filename)
		WRITE_PATH = WORKSPACE / FILE_PATH
		with open(str(WRITE_PATH) ,'wb') as myfile:
			contents = await file.read()
			myfile.write(contents)
	# return local file paths
	return {"file_paths": [str(WORKSPACE)+'/'+file.filename for file in files]}


@app.get("/")
async def main():
	content = """
<body>
<form action="/upload/files/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
</body>
	"""
	return HTMLResponse(content=content)


@app.get("/ping")
def ping():
	return {"message": "pong"}

That’s about it!

I’m always learning, so if you spot an error or find an optimization in any of the content feel free mention it in the comments so that I can fix it for everyone :)

If you face any issues with the file upload snippet, drop a comment and I’ll try to help.

Peace ✌🏾