1# Copyright (c) 2006-2012 Mitch Garnaat http://garnaat.org/
2# Copyright (c) 2012 Amazon.com, Inc. or its affiliates.
3# Copyright (c) 2010, Eucalyptus Systems, Inc.
4# All Rights Reserved
5#
6# Permission is hereby granted, free of charge, to any person obtaining a
7# copy of this software and associated documentation files (the
8# "Software"), to deal in the Software without restriction, including
9# without limitation the rights to use, copy, modify, merge, publish, dis-
10# tribute, sublicense, and/or sell copies of the Software, and to permit
11# persons to whom the Software is furnished to do so, subject to the fol-
12# lowing conditions:
13#
14# The above copyright notice and this permission notice shall be included
15# in all copies or substantial portions of the Software.
16#
17# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
18# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
19# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
20# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
23# IN THE SOFTWARE.
24
25from boto.s3 import user
26from boto.s3 import key
27from boto import handler
28import xml.sax
29
30
31class CompleteMultiPartUpload(object):
32    """
33    Represents a completed MultiPart Upload.  Contains the
34    following useful attributes:
35
36     * location - The URI of the completed upload
37     * bucket_name - The name of the bucket in which the upload
38                     is contained
39     * key_name - The name of the new, completed key
40     * etag - The MD5 hash of the completed, combined upload
41     * version_id - The version_id of the completed upload
42     * encrypted - The value of the encryption header
43    """
44
45    def __init__(self, bucket=None):
46        self.bucket = bucket
47        self.location = None
48        self.bucket_name = None
49        self.key_name = None
50        self.etag = None
51        self.version_id = None
52        self.encrypted = None
53
54    def __repr__(self):
55        return '<CompleteMultiPartUpload: %s.%s>' % (self.bucket_name,
56                                                     self.key_name)
57
58    def startElement(self, name, attrs, connection):
59        return None
60
61    def endElement(self, name, value, connection):
62        if name == 'Location':
63            self.location = value
64        elif name == 'Bucket':
65            self.bucket_name = value
66        elif name == 'Key':
67            self.key_name = value
68        elif name == 'ETag':
69            self.etag = value
70        else:
71            setattr(self, name, value)
72
73
74class Part(object):
75    """
76    Represents a single part in a MultiPart upload.
77    Attributes include:
78
79     * part_number - The integer part number
80     * last_modified - The last modified date of this part
81     * etag - The MD5 hash of this part
82     * size - The size, in bytes, of this part
83    """
84
85    def __init__(self, bucket=None):
86        self.bucket = bucket
87        self.part_number = None
88        self.last_modified = None
89        self.etag = None
90        self.size = None
91
92    def __repr__(self):
93        if isinstance(self.part_number, int):
94            return '<Part %d>' % self.part_number
95        else:
96            return '<Part %s>' % None
97
98    def startElement(self, name, attrs, connection):
99        return None
100
101    def endElement(self, name, value, connection):
102        if name == 'PartNumber':
103            self.part_number = int(value)
104        elif name == 'LastModified':
105            self.last_modified = value
106        elif name == 'ETag':
107            self.etag = value
108        elif name == 'Size':
109            self.size = int(value)
110        else:
111            setattr(self, name, value)
112
113
114def part_lister(mpupload, part_number_marker=None):
115    """
116    A generator function for listing parts of a multipart upload.
117    """
118    more_results = True
119    part = None
120    while more_results:
121        parts = mpupload.get_all_parts(None, part_number_marker)
122        for part in parts:
123            yield part
124        part_number_marker = mpupload.next_part_number_marker
125        more_results = mpupload.is_truncated
126
127
128class MultiPartUpload(object):
129    """
130    Represents a MultiPart Upload operation.
131    """
132
133    def __init__(self, bucket=None):
134        self.bucket = bucket
135        self.bucket_name = None
136        self.key_name = None
137        self.id = id
138        self.initiator = None
139        self.owner = None
140        self.storage_class = None
141        self.initiated = None
142        self.part_number_marker = None
143        self.next_part_number_marker = None
144        self.max_parts = None
145        self.is_truncated = False
146        self._parts = None
147
148    def __repr__(self):
149        return '<MultiPartUpload %s>' % self.key_name
150
151    def __iter__(self):
152        return part_lister(self)
153
154    def to_xml(self):
155        s = '<CompleteMultipartUpload>\n'
156        for part in self:
157            s += '  <Part>\n'
158            s += '    <PartNumber>%d</PartNumber>\n' % part.part_number
159            s += '    <ETag>%s</ETag>\n' % part.etag
160            s += '  </Part>\n'
161        s += '</CompleteMultipartUpload>'
162        return s
163
164    def startElement(self, name, attrs, connection):
165        if name == 'Initiator':
166            self.initiator = user.User(self)
167            return self.initiator
168        elif name == 'Owner':
169            self.owner = user.User(self)
170            return self.owner
171        elif name == 'Part':
172            part = Part(self.bucket)
173            self._parts.append(part)
174            return part
175        return None
176
177    def endElement(self, name, value, connection):
178        if name == 'Bucket':
179            self.bucket_name = value
180        elif name == 'Key':
181            self.key_name = value
182        elif name == 'UploadId':
183            self.id = value
184        elif name == 'StorageClass':
185            self.storage_class = value
186        elif name == 'PartNumberMarker':
187            self.part_number_marker = value
188        elif name == 'NextPartNumberMarker':
189            self.next_part_number_marker = value
190        elif name == 'MaxParts':
191            self.max_parts = int(value)
192        elif name == 'IsTruncated':
193            if value == 'true':
194                self.is_truncated = True
195            else:
196                self.is_truncated = False
197        elif name == 'Initiated':
198            self.initiated = value
199        else:
200            setattr(self, name, value)
201
202    def get_all_parts(self, max_parts=None, part_number_marker=None,
203                      encoding_type=None):
204        """
205        Return the uploaded parts of this MultiPart Upload.  This is
206        a lower-level method that requires you to manually page through
207        results.  To simplify this process, you can just use the
208        object itself as an iterator and it will automatically handle
209        all of the paging with S3.
210        """
211        self._parts = []
212        query_args = 'uploadId=%s' % self.id
213        if max_parts:
214            query_args += '&max-parts=%d' % max_parts
215        if part_number_marker:
216            query_args += '&part-number-marker=%s' % part_number_marker
217        if encoding_type:
218            query_args += '&encoding-type=%s' % encoding_type
219        response = self.bucket.connection.make_request('GET', self.bucket.name,
220                                                       self.key_name,
221                                                       query_args=query_args)
222        body = response.read()
223        if response.status == 200:
224            h = handler.XmlHandler(self, self)
225            xml.sax.parseString(body, h)
226            return self._parts
227
228    def upload_part_from_file(self, fp, part_num, headers=None, replace=True,
229                              cb=None, num_cb=10, md5=None, size=None):
230        """
231        Upload another part of this MultiPart Upload.
232
233        .. note::
234
235            After you initiate multipart upload and upload one or more parts,
236            you must either complete or abort multipart upload in order to stop
237            getting charged for storage of the uploaded parts. Only after you
238            either complete or abort multipart upload, Amazon S3 frees up the
239            parts storage and stops charging you for the parts storage.
240
241        :type fp: file
242        :param fp: The file object you want to upload.
243
244        :type part_num: int
245        :param part_num: The number of this part.
246
247        The other parameters are exactly as defined for the
248        :class:`boto.s3.key.Key` set_contents_from_file method.
249
250        :rtype: :class:`boto.s3.key.Key` or subclass
251        :returns: The uploaded part containing the etag.
252        """
253        if part_num < 1:
254            raise ValueError('Part numbers must be greater than zero')
255        query_args = 'uploadId=%s&partNumber=%d' % (self.id, part_num)
256        key = self.bucket.new_key(self.key_name)
257        key.set_contents_from_file(fp, headers=headers, replace=replace,
258                                   cb=cb, num_cb=num_cb, md5=md5,
259                                   reduced_redundancy=False,
260                                   query_args=query_args, size=size)
261        return key
262
263    def copy_part_from_key(self, src_bucket_name, src_key_name, part_num,
264                           start=None, end=None, src_version_id=None,
265                           headers=None):
266        """
267        Copy another part of this MultiPart Upload.
268
269        :type src_bucket_name: string
270        :param src_bucket_name: Name of the bucket containing the source key
271
272        :type src_key_name: string
273        :param src_key_name: Name of the source key
274
275        :type part_num: int
276        :param part_num: The number of this part.
277
278        :type start: int
279        :param start: Zero-based byte offset to start copying from
280
281        :type end: int
282        :param end: Zero-based byte offset to copy to
283
284        :type src_version_id: string
285        :param src_version_id: version_id of source object to copy from
286
287        :type headers: dict
288        :param headers: Any headers to pass along in the request
289        """
290        if part_num < 1:
291            raise ValueError('Part numbers must be greater than zero')
292        query_args = 'uploadId=%s&partNumber=%d' % (self.id, part_num)
293        if start is not None and end is not None:
294            rng = 'bytes=%s-%s' % (start, end)
295            provider = self.bucket.connection.provider
296            if headers is None:
297                headers = {}
298            else:
299                headers = headers.copy()
300            headers[provider.copy_source_range_header] = rng
301        return self.bucket.copy_key(self.key_name, src_bucket_name,
302                                    src_key_name,
303                                    src_version_id=src_version_id,
304                                    storage_class=None,
305                                    headers=headers,
306                                    query_args=query_args)
307
308    def complete_upload(self):
309        """
310        Complete the MultiPart Upload operation.  This method should
311        be called when all parts of the file have been successfully
312        uploaded to S3.
313
314        :rtype: :class:`boto.s3.multipart.CompletedMultiPartUpload`
315        :returns: An object representing the completed upload.
316        """
317        xml = self.to_xml()
318        return self.bucket.complete_multipart_upload(self.key_name,
319                                                     self.id, xml)
320
321    def cancel_upload(self):
322        """
323        Cancels a MultiPart Upload operation.  The storage consumed by
324        any previously uploaded parts will be freed. However, if any
325        part uploads are currently in progress, those part uploads
326        might or might not succeed. As a result, it might be necessary
327        to abort a given multipart upload multiple times in order to
328        completely free all storage consumed by all parts.
329        """
330        self.bucket.cancel_multipart_upload(self.key_name, self.id)
331