1# -*- coding: utf-8 -*-
2# MinIO Python Library for Amazon S3 Compatible Cloud Storage, (C)
3# 2015, 2016, 2017, 2018, 2019 MinIO, Inc.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""
18minio.xml_marshal
19~~~~~~~~~~~~~~~
20
21This module contains the simple wrappers for XML marshaller's.
22
23:copyright: (c) 2015 by MinIO, Inc.
24:license: Apache 2.0, see LICENSE for more details.
25
26"""
27
28from __future__ import absolute_import
29import io
30
31from xml.etree import ElementTree as s3_xml
32
33_S3_NAMESPACE = 'http://s3.amazonaws.com/doc/2006-03-01/'
34
35
36def xml_marshal_bucket_constraint(region):
37    """
38    Marshal's bucket constraint based on *region*.
39
40    :param region: Region name of a given bucket.
41    :return: Marshalled XML data.
42    """
43    root = s3_xml.Element('CreateBucketConfiguration', {'xmlns': _S3_NAMESPACE})
44    location_constraint = s3_xml.SubElement(root, 'LocationConstraint')
45    location_constraint.text = region
46    data = io.BytesIO()
47    s3_xml.ElementTree(root).write(data, encoding=None, xml_declaration=False)
48    return data.getvalue()
49
50
51def xml_marshal_select(opts):
52    root = s3_xml.Element('SelectObjectContentRequest')
53    expression = s3_xml.SubElement(root, 'Expression')
54    expression.text = opts.expression
55    expression_type = s3_xml.SubElement(root, 'ExpressionType')
56    expression_type.text = 'SQL'
57    input_serialization = s3_xml.SubElement(root, 'InputSerialization')
58
59    if opts.in_ser.csv_input is not None:
60        compression_type = s3_xml.SubElement(input_serialization, 'CompressionType')
61        compression_type.text = opts.in_ser.compression_type
62        csv = s3_xml.SubElement(input_serialization, 'CSV')
63        file_header_info = s3_xml.SubElement(csv, 'FileHeaderInfo')
64        file_header_info.text = opts.in_ser.csv_input.FileHeaderInfo
65        record_delimiter = s3_xml.SubElement(csv, 'RecordDelimiter')
66        record_delimiter.text = opts.in_ser.csv_input.RecordDelimiter
67        field_delimiter = s3_xml.SubElement(csv, 'FieldDelimiter')
68        field_delimiter.text = opts.in_ser.csv_input.FieldDelimiter
69        quote_character = s3_xml.SubElement(csv, 'QuoteCharacter')
70        quote_character.text = opts.in_ser.csv_input.QuoteCharacter
71        quote_escape_character = s3_xml.SubElement(csv, 'QuoteEscapeCharacter')
72        quote_escape_character.text = opts.in_ser.csv_input.QuoteEscapeCharacter
73        comments = s3_xml.SubElement(csv, 'Comments')
74        comments.text = opts.in_ser.csv_input.Comments
75        allow_quoted_record_delimiter = s3_xml.SubElement(csv, 'AllowQuotedRecordDelimiter')
76        allow_quoted_record_delimiter.text = opts.in_ser.csv_input.AllowQuotedRecordDelimiter.lower()
77
78    if opts.in_ser.json_input is not None:
79        compression_type = s3_xml.SubElement(input_serialization, 'CompressionType')
80        compression_type.text = opts.in_ser.compression_type
81        json = s3_xml.SubElement(input_serialization, 'JSON')
82        type_input = s3_xml.SubElement(json, 'Type')
83        type_input.text = opts.in_ser.json_input.Type
84
85    if opts.in_ser.parquet_input is not None:
86        compression_type = s3_xml.SubElement(input_serialization, 'CompressionType')
87        compression_type.text = opts.in_ser.compression_type
88        s3_xml.SubElement(input_serialization, 'Parquet')
89
90    output_serialization = s3_xml.SubElement(root, 'OutputSerialization')
91    if opts.out_ser.csv_output is not None:
92        csv = s3_xml.SubElement(output_serialization, 'CSV')
93        quote_field = s3_xml.SubElement(csv, 'QuoteFields')
94        quote_field.text = opts.out_ser.csv_output.QuoteFields
95        record_delimiter = s3_xml.SubElement(csv, 'RecordDelimiter')
96        record_delimiter.text = opts.out_ser.csv_output.RecordDelimiter
97        field_delimiter = s3_xml.SubElement(csv, 'FieldDelimiter')
98        field_delimiter.text = opts.out_ser.csv_output.FieldDelimiter
99        quote_character = s3_xml.SubElement(csv, 'QuoteCharacter')
100        quote_character.text = opts.out_ser.csv_output.QuoteCharacter
101        quote_escape_character = s3_xml.SubElement(csv, 'QuoteEscapeCharacter')
102        quote_escape_character.text = opts.out_ser.csv_output.QuoteEscapeCharacter
103
104    if opts.out_ser.json_output is not None:
105        json = s3_xml.SubElement(output_serialization, 'JSON')
106        record_delimiter = s3_xml.SubElement(json, 'RecordDelimiter')
107        record_delimiter.text = opts.out_ser.json_output.RecordDelimiter
108
109    request_progress = s3_xml.SubElement(root, 'RequestProgress')
110    enabled = s3_xml.SubElement(request_progress, 'Enabled')
111    enabled.text = opts.req_progress.enabled.lower()
112
113    data = io.BytesIO()
114    s3_xml.ElementTree(root).write(data, encoding=None, xml_declaration=False)
115    return data.getvalue()
116
117
118def xml_marshal_complete_multipart_upload(uploaded_parts):
119    """
120    Marshal's complete multipart upload request based on *uploaded_parts*.
121
122    :param uploaded_parts: List of all uploaded parts, ordered by part number.
123    :return: Marshalled XML data.
124    """
125    root = s3_xml.Element('CompleteMultipartUpload', {'xmlns': _S3_NAMESPACE})
126    for uploaded_part in uploaded_parts:
127        part_number = uploaded_part.part_number
128        part = s3_xml.SubElement(root, 'Part')
129        part_num = s3_xml.SubElement(part, 'PartNumber')
130        part_num.text = str(part_number)
131        etag = s3_xml.SubElement(part, 'ETag')
132        etag.text = '"' + uploaded_part.etag + '"'
133        data = io.BytesIO()
134        s3_xml.ElementTree(root).write(data, encoding=None,
135                                       xml_declaration=False)
136    return data.getvalue()
137
138
139def xml_marshal_bucket_notifications(notifications):
140    """
141    Marshals the notifications structure for sending to S3 compatible storage
142
143    :param notifications: Dictionary with following structure:
144
145    {
146        'TopicConfigurations': [
147            {
148                'Id': 'string',
149                'Arn': 'string',
150                'Events': [
151                    's3:ReducedRedundancyLostObject'|'s3:ObjectCreated:*'|'s3:ObjectCreated:Put'|'s3:ObjectCreated:Post'|'s3:ObjectCreated:Copy'|'s3:ObjectCreated:CompleteMultipartUpload'|'s3:ObjectRemoved:*'|'s3:ObjectRemoved:Delete'|'s3:ObjectRemoved:DeleteMarkerCreated',
152                ],
153                'Filter': {
154                    'Key': {
155                        'FilterRules': [
156                            {
157                                'Name': 'prefix'|'suffix',
158                                'Value': 'string'
159                            },
160                        ]
161                    }
162                }
163            },
164        ],
165        'QueueConfigurations': [
166            {
167                'Id': 'string',
168                'Arn': 'string',
169                'Events': [
170                    's3:ReducedRedundancyLostObject'|'s3:ObjectCreated:*'|'s3:ObjectCreated:Put'|'s3:ObjectCreated:Post'|'s3:ObjectCreated:Copy'|'s3:ObjectCreated:CompleteMultipartUpload'|'s3:ObjectRemoved:*'|'s3:ObjectRemoved:Delete'|'s3:ObjectRemoved:DeleteMarkerCreated',
171                ],
172                'Filter': {
173                    'Key': {
174                        'FilterRules': [
175                            {
176                                'Name': 'prefix'|'suffix',
177                                'Value': 'string'
178                            },
179                        ]
180                    }
181                }
182            },
183        ],
184        'CloudFunctionConfigurations': [
185            {
186                'Id': 'string',
187                'Arn': 'string',
188                'Events': [
189                    's3:ReducedRedundancyLostObject'|'s3:ObjectCreated:*'|'s3:ObjectCreated:Put'|'s3:ObjectCreated:Post'|'s3:ObjectCreated:Copy'|'s3:ObjectCreated:CompleteMultipartUpload'|'s3:ObjectRemoved:*'|'s3:ObjectRemoved:Delete'|'s3:ObjectRemoved:DeleteMarkerCreated',
190                ],
191                'Filter': {
192                    'Key': {
193                        'FilterRules': [
194                            {
195                                'Name': 'prefix'|'suffix',
196                                'Value': 'string'
197                            },
198                        ]
199                    }
200                }
201            },
202        ]
203    }
204
205    :return: Marshalled XML data
206    """
207    root = s3_xml.Element('NotificationConfiguration', {'xmlns': _S3_NAMESPACE})
208    _add_notification_config_to_xml(
209        root,
210        'TopicConfiguration',
211        notifications.get('TopicConfigurations', [])
212    )
213    _add_notification_config_to_xml(
214        root,
215        'QueueConfiguration',
216        notifications.get('QueueConfigurations', [])
217    )
218    _add_notification_config_to_xml(
219        root,
220        'CloudFunctionConfiguration',
221        notifications.get('CloudFunctionConfigurations', [])
222    )
223
224    data = io.BytesIO()
225    s3_xml.ElementTree(root).write(data, encoding=None, xml_declaration=False)
226    return data.getvalue()
227
228NOTIFICATIONS_ARN_FIELDNAME_MAP = {
229    'TopicConfiguration': 'Topic',
230    'QueueConfiguration': 'Queue',
231    'CloudFunctionConfiguration': 'CloudFunction',
232}
233
234
235def _add_notification_config_to_xml(node, element_name, configs):
236    """
237    Internal function that builds the XML sub-structure for a given
238    kind of notification configuration.
239
240    """
241    for config in configs:
242        config_node = s3_xml.SubElement(node, element_name)
243
244        if 'Id' in config:
245            id_node = s3_xml.SubElement(config_node, 'Id')
246            id_node.text = config['Id']
247
248        arn_node = s3_xml.SubElement(
249            config_node,
250            NOTIFICATIONS_ARN_FIELDNAME_MAP[element_name]
251        )
252        arn_node.text = config['Arn']
253
254        for event in config['Events']:
255            event_node = s3_xml.SubElement(config_node, 'Event')
256            event_node.text = event
257
258        filter_rules = config.get('Filter', {}).get(
259            'Key', {}).get('FilterRules', [])
260        if filter_rules:
261            filter_node = s3_xml.SubElement(config_node, 'Filter')
262            s3key_node = s3_xml.SubElement(filter_node, 'S3Key')
263            for filter_rule in filter_rules:
264                filter_rule_node = s3_xml.SubElement(s3key_node, 'FilterRule')
265                name_node = s3_xml.SubElement(filter_rule_node, 'Name')
266                name_node.text = filter_rule['Name']
267                value_node = s3_xml.SubElement(filter_rule_node, 'Value')
268                value_node.text = filter_rule['Value']
269    return node
270
271
272def xml_marshal_delete_objects(object_names):
273    """
274    Marshal Multi-Object Delete request body from object names.
275
276    :param object_names: List of object keys to be deleted.
277    :return: Serialized XML string for multi-object delete request body.
278    """
279    root = s3_xml.Element('Delete')
280
281    # use quiet mode in the request - this causes the S3 Server to
282    # limit its response to only object keys that had errors during
283    # the delete operation.
284    quiet = s3_xml.SubElement(root, 'Quiet')
285    quiet.text = "true"
286
287    # add each object to the request.
288    for object_name in object_names:
289        object_elt = s3_xml.SubElement(root, 'Object')
290        key_elt = s3_xml.SubElement(object_elt, 'Key')
291        key_elt.text = object_name
292
293    # return the marshalled xml.
294    data = io.BytesIO()
295    s3_xml.ElementTree(root).write(data, encoding=None, xml_declaration=False)
296    return data.getvalue()
297