blob: f6c5ce8de8ac1cde1f832e3ccd558b14ea07f953 [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):
59 payload = MIMEApplication(content.read())
60 payload.add_header(
61 "Content-Disposition", "form-data", name=name, filename=name)
62 names = name, getattr(content, "name", None)
63 payload.set_type(get_content_type(*names))
64 return payload
65
66
67def make_payloads(name, content):
68 if isinstance(content, bytes):
69 yield make_bytes_payload(name, content)
70 elif isinstance(content, unicode):
71 yield make_string_payload(name, content)
72 elif isinstance(content, IOBase):
73 yield make_file_payload(name, content)
74 elif callable(content):
75 with content() as content:
76 for payload in make_payloads(name, content):
77 yield payload
78 elif isinstance(content, Iterable):
79 for part in content:
80 for payload in make_payloads(name, part):
81 yield payload
82 else:
83 raise AssertionError(
84 "%r is unrecognised: %r" % (name, content))
85
86
87def build_multipart_message(data):
88 message = MIMEMultipart("form-data")
89 for name, content in data:
90 for payload in make_payloads(name, content):
91 message.attach(payload)
92 return message
93
94
95def encode_multipart_message(message):
96 # The message must be multipart.
97 assert message.is_multipart()
98 # The body length cannot yet be known.
99 assert "Content-Length" not in message
100 # So line-endings can be fixed-up later on, component payloads must have
101 # no Content-Length and their Content-Transfer-Encoding must be base64
102 # (and not quoted-printable, which Django doesn't appear to understand).
103 for part in message.get_payload():
104 assert "Content-Length" not in part
105 assert part["Content-Transfer-Encoding"] == "base64"
106 # Flatten the message without headers.
107 buf = BytesIO()
108 generator = Generator(buf, False) # Don't mangle "^From".
109 generator._write_headers = lambda self: None # Ignore.
110 generator.flatten(message)
111 # Ensure the body has CRLF-delimited lines. See
112 # http://bugs.python.org/issue1349106.
113 body = b"\r\n".join(buf.getvalue().splitlines())
114 # Only now is it safe to set the content length.
115 message.add_header("Content-Length", "%d" % len(body))
116 return message.items(), body
117
118
119def encode_multipart_data(data=(), files=()):
120 """Create a MIME multipart payload from L{data} and L{files}.
121
122 **Note** that this function is deprecated. Use `build_multipart_message`
123 and `encode_multipart_message` instead.
124
125 @param data: A mapping of names (ASCII strings) to data (byte string).
126 @param files: A mapping of names (ASCII strings) to file objects ready to
127 be read.
128 @return: A 2-tuple of C{(body, headers)}, where C{body} is a a byte string
129 and C{headers} is a dict of headers to add to the enclosing request in
130 which this payload will travel.
131 """
132 if isinstance(data, Mapping):
133 data = data.items()
134 if isinstance(files, Mapping):
135 files = files.items()
136 message = build_multipart_message(chain(data, files))
137 headers, body = encode_multipart_message(message)
138 return body, dict(headers)