Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/active_toolkit/sockets.py
blob: a131c3ad5ebbaab5bb10458b381c90616d23e4cb (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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# Copyright (C) 2012 Aleksey Lim
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""Utilities to work with sockets.

$Repo: git://git.sugarlabs.org/alsroot/codelets.git$
$File: src/socket.py$
$Date: 2012-05-15$

"""

import os
import sys
import json
import errno
import struct
import random
import tempfile
from os.path import join, isfile, relpath

from . import util
enforce = util.enforce


BUFFER_SIZE = 1024 * 10


class SocketFile(object):

    def __init__(self, socket):
        self._socket = socket
        self._message_buffer = bytearray('\0' * BUFFER_SIZE)
        self._read_size = None

    @property
    def socket(self):
        return self._socket

    def write_message(self, message):
        try:
            message_str = json.dumps(message)
        except Exception, error:
            raise RuntimeError('Cannot encode %r message: %s' %
                    (message, error))
        self.write(message_str)

    def read_message(self):
        message_str = self.read()
        if not message_str:
            return None
        try:
            message = json.loads(message_str)
        except Exception, error:
            raise RuntimeError('Cannot decode "%s" message: %s' %
                    (message_str, error))
        return message

    def write(self, data, size=None):
        if data is None:
            data = ''

        if hasattr(data, 'read'):
            enforce(size)
        else:
            size = len(data)

        size_str = struct.pack('i', size)
        self._socket.send(size_str)

        if hasattr(data, 'read'):
            while size:
                chunk_size = min(size, BUFFER_SIZE)
                # pylint: disable-msg=E1103
                self._socket.send(data.read(chunk_size))
                size -= chunk_size
        else:
            self._socket.send(data)

    def read(self, size=None):

        def read_size():
            size_str = self._recv(struct.calcsize('i'))
            if not size_str:
                return 0
            return struct.unpack('i', size_str)[0]

        if size is None:
            chunks = []
            size = read_size()
            while size:
                chunk = self._recv(min(size, BUFFER_SIZE))
                if not chunk:
                    break
                chunks.append(chunk)
                size -= len(chunk)
            return ''.join(chunks)
        else:
            if self._read_size is None:
                self._read_size = read_size()
            if self._read_size:
                chunk = self._recv(min(self._read_size, BUFFER_SIZE, size))
            else:
                chunk = ''
            if not chunk:
                self._read_size = None
            else:
                self._read_size -= len(chunk)
            return chunk

    def close(self):
        if self._socket is not None:
            self._socket.close()
            self._socket = None

    def _recv(self, size):
        while True:
            try:
                chunk = self._socket.recv(size)
            except OSError, error:
                if error.errno == errno.EINTR:
                    continue
                raise
            return chunk

    def __repr__(self):
        return repr(self._socket)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.close()

    def __iter__(self):
        while True:
            chunk = self.read(BUFFER_SIZE)
            if not chunk:
                break
            yield chunk

    def __getattr__(self, name):
        return getattr(self._socket, name)


def encode_directory(path, boundary=None):
    """Encode directory content as a multipart stream.

    Function calculate number of files to encode, returns stat object, then,
    encode data.

    :param path:
        path to directory to encode
    :param boundary:
        boundary string to separate multipart portions; if not specified,
        will be set default one
    :returns:
        a tuple of `MultipartEncoder` object and an iterator that returns
        encoded data by chunks

    """
    multipart = MultipartEncoder(boundary)
    to_encode = []

    for root, __, files in os.walk(path):
        for filename in files:
            filename_path = join(root, filename)
            if not isfile(filename_path):
                continue
            name = relpath(filename_path, path)
            multipart.count_file(name, os.stat(filename_path).st_size)
            to_encode.append((filename_path, name))

    def feeder():
        for path, filename in to_encode:
            with file(path, 'rb') as f:
                yield filename, f

    return multipart, multipart.encode(feeder())


def decode_multipart(stream, size, boundary):
    """Decode multipart data.

    :param stream:
        object that has `read()` to restore data from
    :param size:
        number of bytes to read from `stream`
    :param boundary:
        boundary string that separates multipart portions
    :returns:
        iterator object that returns `(filename, stream)` for each
        multipart portion

    """
    from werkzeug.formparser import FormDataParser

    if hasattr(stream, 'recv'):
        stream = SocketFile(stream)

    parser = FormDataParser(silent=False,
            stream_factory=lambda * args: tempfile.NamedTemporaryFile())
    __, __, files = parser.parse(stream, 'multipart/form-data', size,
            {'boundary': boundary})

    for __, chunk in files.iteritems(multi=True):
        yield chunk.filename, chunk.stream
        chunk.close()


class MultipartEncoder(object):

    def __init__(self, boundary=None, files=None):
        if boundary is None:
            rnd = '%020d' % random.randrange(sys.maxint)
            boundary = ('=' * 15) + rnd + '=='

        self.boundary = boundary
        self.content_length = 0
        self.content_type = 'multipart/mixed; boundary="%s"' % boundary
        self._files = files

        for filename, content in (files or []):
            if hasattr(content, 'fileno'):
                file_size = os.fstat(content.fileno()).st_size
            elif hasattr(content, 'seek'):
                content.seek(0, 2)
                file_size = content.tell()
                content.seek(0)
            else:
                file_size = len(content)
            self.count_file(filename, file_size)

    def count_file(self, name, size):
        if self.content_length == 0:
            self.content_length += len(_multipart_tail(self.boundary))
        self.content_length += len(_multipart_header(self.boundary, name)) + \
                size + 2

    def encode(self, files=None):
        """Encode data into a multipart encoded stream.

        :param files:
            a `dict` with filename and data object (stream or buffer), or,
            an iterator that returns the same tuples
        :returns:
            an iterator that returns encoded data by chunks

        """
        if files is None:
            files = self._files
        if type(files) is dict:
            files = files.items()

        for filename, content in files:
            yield _multipart_header(self.boundary, filename)

            if hasattr(content, 'read'):
                try:
                    while True:
                        chunk = content.read(BUFFER_SIZE)
                        if not chunk:
                            break
                        yield chunk
                finally:
                    content.close()
            else:
                if isinstance(content, unicode):
                    content = content.encode('utf-8')
                yield content

            yield '\r\n'

        yield _multipart_tail(self.boundary)


def _multipart_header(boundary, filename):
    return """\
--%s\r
Content-Disposition: attachment; filename="%s"\r
Content-Type: application/octet-stream\r
\r
""" % (boundary, filename)


def _multipart_tail(boundary):
    return '--%s--\r\n' % boundary