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