9

I'm trying to write an endpoint that just accepts an image and attempts to convert it into another format, by running a command on the system. Then I return the converted file. It's slow and oh-so-simple, and I don't have to store files anywhere, except temporarily.

I'd like all the file-writing to happen in a temporary directory, so it gets cleaned up.

The route works fine if the output file is not in the temporary directory. But if I try to put the output file in the temporary directory, the FileResponse can't find it, and requests fail.

RuntimeError: File at path /tmp/tmpp5x_p4n9/out.jpg does not exist.

Is there something going on related to the asynchronous nature of FastApi that FileResponse can't wait for the subprocess to create the file its making? Can I make it wait? (removing async from the route does not help).

@app.post("/heic") async def heic(img: UploadFile): with TemporaryDirectory() as dir: inname = os.path.join(dir, "img.heic") f = open(inname,"wb") f.write(img.file.read()) f.flush() # setting outname in the temp dir fails! # outname = os.path.join(dir, 'out.jpg') outname = os.path.join('out.jpg') cmd = f"oiiotool {f.name} -o {outname}" process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) process.wait() return FileResponse(outname, headers={'Content-Disposition':'attachment; filename=response.csv'}) 

Thank you for any insights!

2 Answers 2

11

According to the documentation of TemporaryDirectory()

This class securely creates a temporary directory using the same rules as mkdtemp(). The resulting object can be used as a context manager (see Examples). On completion of the context or destruction of the temporary directory object, the newly created temporary directory and all its contents are removed from the filesystem.

It seems that the directory and contents are already being released before the FastAPI request is returned. However you can use Dependency injection in FastAPI, so why no inject a temporary directory?

First define the dependency:

async def get_temp_dir(): dir = TemporaryDirectory() try: yield dir.name finally: del dir 

And add the dependency to your endpoint:

@app.post("/heic") async def heic(imgfile: UploadFile = File(...), dir=Depends(get_temp_dir)): inname = os.path.join(dir, "img.heic") f = open(inname,"wb") f.write(imgfile.file.read()) f.flush() outname = os.path.join(dir, 'out.jpg') cmd = f"oiiotool {f.name} -o {outname}" process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) process.wait() return FileResponse(inname, headers={'Content-Disposition':'attachment; filename=response.csv'}) 

I've tested it with returning the incoming file and that works.

Sign up to request clarification or add additional context in comments.

Comments

1

Here's an alternative approach by creating a new FileResponseWithCleanup class that extends the FileResponse class.

Simply create a permanent file, and add the file you want to delete after returning to the cleanup_path prop.

class FileResponseWithCleanup(FileResponse): def __init__(self, *args, **kwargs): self.cleanup_path = kwargs.pop("cleanup_path", None) super().__init__(*args, **kwargs) async def __call__(self, scope, receive, send): try: await super().__call__(scope, receive, send) finally: if self.cleanup_path and os.path.exists(self.cleanup_path): os.remove(self.cleanup_path) @app.post("/upload") async def upload_file(file: UploadFile = File(...)): # Create a temporary file to save the uploaded file with tempfile.NamedTemporaryFile(delete=False, suffix=".tmp") as temp_file: temp_file.write(await file.read()) temp_file_path = temp_file.name # Copy the temporary file to a more permanent location permanent_file_path = f"static/{Path(file.filename).stem}_uploaded.tmp" shutil.copyfile(temp_file_path, permanent_file_path) # Return the file with cleanup after response return FileResponseWithCleanup( path=permanent_file_path, media_type="application/octet-stream", filename=file.filename, cleanup_path=permanent_file_path, ) 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.