blob: 7ed17a61d7d5d0a2c58c18c328cc3562fe5c9479 [file] [log] [blame]
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +01001# Copyright 2012 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Encoding of MIME multipart data."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = [
16 'encode_multipart_data',
17 ]
18
19from collections import (
20 Iterable,
21 Mapping,
22 )
23from email.generator import Generator
24from email.mime.application import MIMEApplication
25from email.mime.multipart import MIMEMultipart
26from io import (
27 BytesIO,
28 IOBase,
29 )
30from itertools import chain
31import mimetypes
32
33
34def get_content_type(*names):
35 """Return the MIME content type for the file with the given name."""
36 for name in names:
37 if name is not None:
38 mimetype, encoding = mimetypes.guess_type(name)
39 if mimetype is not None:
40 return mimetype
41 else:
42 return "application/octet-stream"
43
44
45def make_bytes_payload(name, content):
46 payload = MIMEApplication(content)
47 payload.add_header("Content-Disposition", "form-data", name=name)
48 return payload
49
50
51def make_string_payload(name, content):
52 payload = MIMEApplication(content.encode("utf-8"), charset="utf-8")
53 payload.add_header("Content-Disposition", "form-data", name=name)
54 payload.set_type("text/plain")
55 return payload
56
57
58def make_file_payload(name, content):
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010059 print('********************************')
60 print('content %s' % content)
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +010061 payload = MIMEApplication(content.read())
62 payload.add_header(
63 "Content-Disposition", "form-data", name=name, filename=name)
64 names = name, getattr(content, "name", None)
65 payload.set_type(get_content_type(*names))
66 return payload
67
68
69def make_payloads(name, content):
70 if isinstance(content, bytes):
71 yield make_bytes_payload(name, content)
72 elif isinstance(content, unicode):
73 yield make_string_payload(name, content)
74 elif isinstance(content, IOBase):
75 yield make_file_payload(name, content)
76 elif callable(content):
77 with content() as content:
78 for payload in make_payloads(name, content):
79 yield payload
80 elif isinstance(content, Iterable):
81 for part in content:
82 for payload in make_payloads(name, part):
83 yield payload
84 else:
85 raise AssertionError(
86 "%r is unrecognised: %r" % (name, content))
87
88
89def build_multipart_message(data):
90 message = MIMEMultipart("form-data")
91 for name, content in data:
92 for payload in make_payloads(name, content):
93 message.attach(payload)
94 return message
95
96
97def encode_multipart_message(message):
98 # The message must be multipart.
99 assert message.is_multipart()
100 # The body length cannot yet be known.
101 assert "Content-Length" not in message
102 # So line-endings can be fixed-up later on, component payloads must have
103 # no Content-Length and their Content-Transfer-Encoding must be base64
104 # (and not quoted-printable, which Django doesn't appear to understand).
105 for part in message.get_payload():
106 assert "Content-Length" not in part
107 assert part["Content-Transfer-Encoding"] == "base64"
108 # Flatten the message without headers.
109 buf = BytesIO()
110 generator = Generator(buf, False) # Don't mangle "^From".
111 generator._write_headers = lambda self: None # Ignore.
112 generator.flatten(message)
113 # Ensure the body has CRLF-delimited lines. See
114 # http://bugs.python.org/issue1349106.
115 body = b"\r\n".join(buf.getvalue().splitlines())
116 # Only now is it safe to set the content length.
117 message.add_header("Content-Length", "%d" % len(body))
118 return message.items(), body
119
120
121def encode_multipart_data(data=(), files=()):
122 """Create a MIME multipart payload from L{data} and L{files}.
123
124 **Note** that this function is deprecated. Use `build_multipart_message`
125 and `encode_multipart_message` instead.
126
127 @param data: A mapping of names (ASCII strings) to data (byte string).
128 @param files: A mapping of names (ASCII strings) to file objects ready to
129 be read.
130 @return: A 2-tuple of C{(body, headers)}, where C{body} is a a byte string
131 and C{headers} is a dict of headers to add to the enclosing request in
132 which this payload will travel.
133 """
134 if isinstance(data, Mapping):
135 data = data.items()
136 if isinstance(files, Mapping):
137 files = files.items()
138 message = build_multipart_message(chain(data, files))
139 headers, body = encode_multipart_message(message)
140 return body, dict(headers)