0

I'm working on a kind of personal cloud project which allows to upload and download files (with a google like search feature). The backend is written in python (using aiohttp) while I'm using a react.js website as a client. When a file is uploaded, the backend stores it on the filesystem and renames it with its sha256 hash. The original name and a few other metadata are stored next to it (description, etc). When the user downloads the file, I'm serving it using multipart and I want the user to get it with the original name and not the hash, indeed my-cool-image.png is more user friendly than a14e0414-b84c-4d7b-b0d4-49619b9edd8a. But I'm not able to do it (whatever I try, the download file is called with the hash).

Here is my code:

    async def download(self, request):

        if not request.username:
            raise exceptions.Unauthorized("A valid token is needed")

        data = await request.post()
        hash = data["hash"]

        file_path = storage.get_file(hash)
        dotfile_path = storage.get_file("." + hash)
        if not os.path.exists(file_path) or not os.path.exists(dotfile_path):
            raise exceptions.NotFound("file <{}> does not exist".format(hash))
        with open(dotfile_path) as dotfile:
            dotfile_content = json.load(dotfile)
            name = dotfile_content["name"]

        headers = {
            "Content-Type": "application/octet-stream; charset=binary",
            "Content-Disposition": "attachment; filename*=UTF-8''{}".format(
                urllib.parse.quote(name, safe="")
            ),
        }

        return web.Response(body=self._file_sender(file_path), headers=headers)

Here is what it looks like (according to the browser): request seen from browser Seems right, yet it's not working.

One thing I want to make clear though: sometimes I get a warning (on the client side) saying Resource interpreted as Document but transferred with MIME type application/octet-stream. I don't know the MIME type of the files since they are provided by the users, but I tried to use image/png (I did my test with a png image stored on the server). The file didn't get downloaded (it was displayed in the browser, which is not something that I want) and the file name was still its hash so it didn't help with my issue.

Here is the entire source codes of the backend: https://git.io/nexmind-node And of the frontend: https://git.io/nexmind-client

EDIT: I received a first answer by Julien Castiaux so I tried to implement it, even though it looks better, it doesn't solve my issue (I still have the exact same behaviour):


    async def download(self, request):

        if not request.username:
            raise exceptions.Unauthorized("A valid token is needed")

        data = await request.post()
        hash = data["hash"]

        file_path = storage.get_file(hash)
        dotfile_path = storage.get_file("." + hash)
        if not os.path.exists(file_path) or not os.path.exists(dotfile_path):
            raise exceptions.NotFound("file <{}> does not exist".format(hash))
        with open(dotfile_path) as dotfile:
            dotfile_content = json.load(dotfile)
            name = dotfile_content["name"]

        response = web.StreamResponse()
        response.headers['Content-Type'] = 'application/octet-stream'
        response.headers['Content-Disposition'] = "attachment; filename*=UTF-8''{}".format(
            urllib.parse.quote(name, safe="")  # replace with the filename
        )
        response.enable_chunked_encoding()
        await response.prepare(request)

        with open(file_path, 'rb') as fd:  # replace with the path
            for chunk in iter(lambda: fd.read(1024), b""):
                await response.write(chunk)
        await response.write_eof()

        return response
Th0rgal
  • 703
  • 8
  • 27

1 Answers1

1

From aiohttp3 documentation

StreamResponse is intended for streaming data, while Response contains HTTP BODY as an attribute and sends own content as single piece with the correct Content-Length HTTP header.

You rather use aiohttp.web.StreamResponse as you are sending (potentially very large) files. Using StreamResponse, you have full control of the outgoing http response stream : headers manipulation (including the filename) and chunked encoding.

from aiohttp import web
import urllib.parse

async def download(req):
    resp = web.StreamResponse()
    resp.headers['Content-Type'] = 'application/octet-stream'
    resp.headers['Content-Disposition'] = "attachment; filename*=UTF-8''{}".format(
        urllib.parse.quote(filename, safe="")  # replace with the filename
    )
    resp.enable_chunked_encoding()
    await resp.prepare(req)

    with open(path_to_the_file, 'rb') as fd:  # replace with the path
        for chunk in iter(lambda: fd.read(1024), b""):
            await resp.write(chunk)
    await resp.write_eof()

    return resp

app = web.Application()
app.add_routes([web.get('/', download)])

web.run_app(app)

Hope it helps !

Julien Castiaux
  • 96
  • 1
  • 11