본문 바로가기

개발하자

FastAPI에서 파일을 가져올 때 브라우저 캐시를 사용하는 것은 어떻습니까?

반응형

FastAPI에서 파일을 가져올 때 브라우저 캐시를 사용하는 것은 어떻습니까?

Fastapi에서 제공하는 웹 앱에 PDF를 표시하고 싶습니다. 파일은 Javascript로 가져옵니다. 단순화된 코드:

fetch('/files/', {method: "POST", body: JSON.stringify(myFilePathString),cache: "no-cache"})
.then(function(response) { return response.blob(); })
.then(function(blob) { myDomElement.src = URL.createObjectURL(blob); });

문제: 브라우저는 항상 파일을 다시 로드하고 캐시된 파일을 사용하지 않습니다. 캐시: "no-cache"를 사용하면 브라우저가 최근에 로드된 파일이 최신인지 확인하고 해당 파일을 가져올 것으로 예상됩니다. 파일을 다시 로드할 때 헤더의 ETag는 변경되지 않습니다. 분명히, BLOB에서 생성된 URL은 다시 로드할 때마다 변경됩니다. 내가 알기로는, 이것이 이유일 수 있다. 어쨌든 이 메커니즘으로 브라우저 캐시를 사용할 수 있는 방법이 있나요?




This mainly needs understanding some HTTP details.

At the client, you have to set the If-None-Match header in your request to the current ETag value.

At the server, compare the ETag value passed in the If-None-Match with the current ETag value of the file. If they are the same, return a 304 Not Modified status, without a body. This tells the browser to use the cached version. If they are different, this means the file is changed and you need to return the new file.

You can understand more about this mechanism from the MDN Web Docs

Here is a working example in FastAPI that implements the backend part:

import hashlib
import os
import stat
import anyio
from fastapi import Body, FastAPI, Header
from fastapi.responses import Response, FileResponse
from pydantic import FileUrl

app = FastAPI()

async def get_etag(file_path):
    try:
        stat_result = await anyio.to_thread.run_sync(os.stat, file_path)
    except FileNotFoundError:
        raise RuntimeError(f"File at path {file_path} does not exist.")
    else:
        mode = stat_result.st_mode
        if not stat.S_ISREG(mode):
            raise RuntimeError(f"File at path {file_path} is not a file.")
        
        #calculate the etag based on file size and last modification time
        etag_base = str(stat_result.st_mtime) + "-" + str(stat_result.st_size)
        etag = hashlib.md5(etag_base.encode()).hexdigest()
        return etag

@app.post("/file/")
async def get_file(file_url: FileUrl = Body(),
                   if_none_match: str | None = Header(default=None)):
    print(file_url.path)
    file_etag = await get_etag(file_url.path)
    if if_none_match == file_etag:
        return Response(status_code=304)
    else:
        return FileResponse(file_url.path)

The get_etag function implementation is inspired by how FastAPI (actually Starlette) calculates it in their source code to return it in the FileResponse Etag header. You can replace the get_etag function with your method of calculating the ETag.

The example uses a POST request passing the file path in a JSON body because you did that in your Javascript example. However, I think it is better to use a GET request passing the file path as a path parameter. You can find out how to do this in FastAPI Tutorial

Also, I have used the FileUrl Pydantic type to validate the passed file path. But you have to specify the URL schema as the file schema before the path. For example, pass the file path as "file:///path/to/file". The first 2 slashes are part of the schema. The third is the start of the path.


반응형