summaryrefslogtreecommitdiff
path: root/waitress/buffers.py
blob: 42d575132b6cda3084f078cdcf022be51e3eb376 (plain)
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
204
205
206
207
208
209
210
211
##############################################################################
#
# Copyright (c) 2001-2004 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Buffers
"""
from io import BytesIO
from tempfile import TemporaryFile

# copy_bytes controls the size of temp. strings for shuffling data around.
COPY_BYTES = 1 << 18 # 256K

# The maximum number of bytes to buffer in a simple string.
STRBUF_LIMIT = 8192

class FileBasedBuffer(object):
    seekable = True
    remaining = 0  # -1 would indicate an infinite stream

    def __bool__(self):
        return self.remaining != 0

    __nonzero = __bool__ # py2

    def append(self, s):
        assert self.seekable
        file = self.file
        read_pos = file.tell()
        file.seek(0, 2)
        file.write(s)
        file.seek(read_pos)
        self.remaining += len(s)

    def read(self, numbytes=-1):
        file = self.file
        remaining = self.remaining
        if remaining != -1 and numbytes > remaining:
            numbytes = remaining
        if numbytes < 0:
            # Read all
            res = file.read()
        else:
            res = file.read(numbytes)
        numres = len(res)
        if remaining == -1:
            # keep remaining at -1 until EOF
            if not numres and numbytes != 0:
                self.remaining = 0
        else:
            self.remaining -= numres
        return res

    def rollback(self, numbytes):
        assert self.seekable
        self.file.seek(-numbytes, 1)
        self.remaining += numbytes

    def close(self):
        self.remaining = 0
        if hasattr(self.file, 'close'):
            self.file.close()

class TempfileBasedBuffer(FileBasedBuffer):

    def __init__(self, from_buffer=None):
        file = TemporaryFile('w+b')
        if from_buffer is not None:
            while True:
                data = from_buffer.read(COPY_BYTES)
                if not data:
                    break
                file.write(data)
                self.remaining += len(data)
            file.seek(0)
        self.file = file

class BytesIOBasedBuffer(FileBasedBuffer):

    def __init__(self, value=None):
        self.file = BytesIO(value)
        if value is not None:
            self.remaining = len(value)

def _is_seekable(fp):
    if hasattr(fp, 'seekable'):
        return fp.seekable()
    return hasattr(fp, 'seek') and hasattr(fp, 'tell')

class ReadOnlyFileBasedBuffer(FileBasedBuffer):
    # used as wsgi.file_wrapper
    remaining = -1

    def __init__(self, file, block_size=32768):
        self.file = file
        self.block_size = block_size # for __iter__
        self.seekable = _is_seekable(file)

    def prepare(self, size=None):
        if self.seekable:
            start_pos = self.file.tell()
            self.file.seek(0, 2)
            end_pos = self.file.tell()
            self.file.seek(start_pos)
            fsize = end_pos - start_pos
            if size is None:
                self.remaining = fsize
            else:
                self.remaining = min(fsize, size)
        return self.remaining

    def __iter__(self): # called by task if self.filelike has no seek/tell
        return self

    def __next__(self):
        val = self.file.read(self.block_size)
        if not val:
            raise StopIteration
        return val

    next = __next__ # py2

    def append(self, s):
        raise NotImplementedError

class OverflowableBuffer(object):
    """
    This buffer implementation has four stages:
    - No data
    - Bytes-based buffer
    - BytesIO-based buffer
    - Temporary file storage
    The first two stages are fastest for simple transfers.
    """

    seekable = True
    remaining = 0

    overflowed = False
    buf = None
    strbuf = b'' # Bytes-based buffer.

    def __init__(self, overflow):
        # overflow is the maximum to be stored in a BytesIO buffer.
        self.overflow = overflow

    def __bool__(self):
        return self.remaining != 0

    __nonzero = __bool__ # py2

    def append(self, s):
        buf = self.buf
        if buf is None:
            strbuf = self.strbuf if self.remaining else b''
            if len(strbuf) + len(s) < STRBUF_LIMIT:
                self.strbuf = strbuf + s
                self.remaining += len(s)
                return
            else:
                buf = BytesIOBasedBuffer(self.strbuf + s)
                self.buf = buf
                self.strbuf = b''
        else:
            buf.append(s)
        remaining = buf.remaining
        self.remaining = remaining
        if not self.overflowed and remaining > self.overflow:
            self.buf = TempfileBasedBuffer(buf)
            self.overflowed = True

    def read(self, numbytes=-1):
        buf = self.buf
        if buf is None:
            if self.remaining <= numbytes or numbytes == -1:
                self.remaining = 0
                return self.strbuf
            buf = self.buf = BytesIOBasedBuffer(self.strbuf)
        data = buf.read(numbytes)
        self.remaining = buf.remaining
        return data

    def rollback(self, numbytes):
        buf = self.buf
        if buf is None:
            self.strbuf = self.strbuf[-numbytes:]
            self.remaining = len(self.strbuf)
            return
        buf.rollback(numbytes)
        self.remaining = buf.remaining

    def close(self):
        self.remaining = 0
        self.strbuf = b''
        buf = self.buf
        if buf is not None:
            buf.close()

    def getfile(self):
        buf = self.buf
        if buf is None:
            buf = self.buf = BytesIOBasedBuffer(self.strbuf)
        return buf.file