File: file_sender.py

package info (click to toggle)
python-aiohttp 1.2.0-1
  • links: PTS, VCS
  • area: main
  • in suites: stretch
  • size: 2,288 kB
  • ctags: 4,380
  • sloc: python: 27,221; makefile: 236
file content (203 lines) | stat: -rw-r--r-- 6,593 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
import asyncio
import mimetypes
import os

from . import hdrs
from .helpers import create_future
from .web_exceptions import (HTTPNotModified, HTTPOk, HTTPPartialContent,
                             HTTPRequestRangeNotSatisfiable)
from .web_reqrep import StreamResponse


class FileSender:
    """"A helper that can be used to send files.
    """

    def __init__(self, *, resp_factory=StreamResponse, chunk_size=256*1024):
        self._response_factory = resp_factory
        self._chunk_size = chunk_size
        if bool(os.environ.get("AIOHTTP_NOSENDFILE")):
            self._sendfile = self._sendfile_fallback

    def _sendfile_cb(self, fut, out_fd, in_fd, offset,
                     count, loop, registered):
        if registered:
            loop.remove_writer(out_fd)
        if fut.cancelled():
            return
        try:
            n = os.sendfile(out_fd, in_fd, offset, count)
            if n == 0:  # EOF reached
                n = count
        except (BlockingIOError, InterruptedError):
            n = 0
        except Exception as exc:
            fut.set_exception(exc)
            return

        if n < count:
            loop.add_writer(out_fd, self._sendfile_cb, fut, out_fd, in_fd,
                            offset + n, count - n, loop, True)
        else:
            fut.set_result(None)

    @asyncio.coroutine
    def _sendfile_system(self, request, resp, fobj, count):
        # Write count bytes of fobj to resp using
        # the os.sendfile system call.
        #
        # request should be a aiohttp.web.Request instance.
        #
        # resp should be a aiohttp.web.StreamResponse instance.
        #
        # fobj should be an open file object.
        #
        # count should be an integer > 0.

        transport = request.transport

        if transport.get_extra_info("sslcontext"):
            yield from self._sendfile_fallback(request, resp, fobj, count)
            return

        def _send_headers(resp_impl):
            # Durty hack required for
            # https://github.com/KeepSafe/aiohttp/issues/1093
            # don't send headers in sendfile mode
            pass

        resp._send_headers = _send_headers

        @asyncio.coroutine
        def write_eof():
            # Durty hack required for
            # https://github.com/KeepSafe/aiohttp/issues/1177
            # do nothing in write_eof
            pass

        resp.write_eof = write_eof

        resp_impl = yield from resp.prepare(request)

        loop = request.app.loop
        # See https://github.com/KeepSafe/aiohttp/issues/958 for details

        # send headers
        headers = ['HTTP/{0.major}.{0.minor} {1} OK\r\n'.format(
            request.version, resp.status)]
        for hdr, val in resp.headers.items():
            headers.append('{}: {}\r\n'.format(hdr, val))
        headers.append('\r\n')

        out_socket = transport.get_extra_info("socket").dup()
        out_socket.setblocking(False)
        out_fd = out_socket.fileno()
        in_fd = fobj.fileno()
        offset = fobj.tell()

        bheaders = ''.join(headers).encode('utf-8')
        headers_length = len(bheaders)
        resp_impl.headers_length = headers_length
        resp_impl.output_length = headers_length + count

        try:
            yield from loop.sock_sendall(out_socket, bheaders)
            fut = create_future(loop)
            self._sendfile_cb(fut, out_fd, in_fd, offset, count, loop, False)

            yield from fut
        finally:
            out_socket.close()

    @asyncio.coroutine
    def _sendfile_fallback(self, request, resp, fobj, count):
        # Mimic the _sendfile_system() method, but without using the
        # os.sendfile() system call. This should be used on systems
        # that don't support the os.sendfile().

        # To avoid blocking the event loop & to keep memory usage low,
        # fobj is transferred in chunks controlled by the
        # constructor's chunk_size argument.

        yield from resp.prepare(request)

        resp.set_tcp_cork(True)
        try:
            chunk_size = self._chunk_size

            chunk = fobj.read(chunk_size)
            while True:
                resp.write(chunk)
                yield from resp.drain()
                count = count - chunk_size
                if count <= 0:
                    break
                chunk = fobj.read(min(chunk_size, count))
        finally:
            resp.set_tcp_nodelay(True)

    if hasattr(os, "sendfile"):  # pragma: no cover
        _sendfile = _sendfile_system
    else:  # pragma: no cover
        _sendfile = _sendfile_fallback

    @asyncio.coroutine
    def send(self, request, filepath):
        """Send filepath to client using request."""
        gzip = False
        if 'gzip' in request.headers.get(hdrs.ACCEPT_ENCODING, ''):
            gzip_path = filepath.with_name(filepath.name + '.gz')

            if gzip_path.is_file():
                filepath = gzip_path
                gzip = True

        st = filepath.stat()

        modsince = request.if_modified_since
        if modsince is not None and st.st_mtime <= modsince.timestamp():
            raise HTTPNotModified()

        ct, encoding = mimetypes.guess_type(str(filepath))
        if not ct:
            ct = 'application/octet-stream'

        status = HTTPOk.status_code
        file_size = st.st_size
        count = file_size

        try:
            rng = request.http_range
            start = rng.start
            end = rng.stop
        except ValueError:
            raise HTTPRequestRangeNotSatisfiable

        # If a range request has been made, convert start, end slice notation
        # into file pointer offset and count
        if start is not None or end is not None:
            status = HTTPPartialContent.status_code
            if start is None and end < 0:  # return tail of file
                start = file_size + end
                count = -end
            else:
                count = (end or file_size) - start

            if start + count > file_size:
                raise HTTPRequestRangeNotSatisfiable

        resp = self._response_factory(status=status)
        resp.content_type = ct
        if encoding:
            resp.headers[hdrs.CONTENT_ENCODING] = encoding
        if gzip:
            resp.headers[hdrs.VARY] = hdrs.ACCEPT_ENCODING
        resp.last_modified = st.st_mtime

        resp.content_length = count
        with filepath.open('rb') as f:
            if start:
                f.seek(start)
            yield from self._sendfile(request, resp, f, count)

        return resp