S3.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. #!/usr/bin/env python
  2. # This software code is made available "AS IS" without warranties of any
  3. # kind. You may copy, display, modify and redistribute the software
  4. # code either by itself or as incorporated into your code; provided that
  5. # you do not remove any proprietary notices. Your use of this software
  6. # code is at your own risk and you waive any claim against Amazon
  7. # Digital Services, Inc. or its affiliates with respect to your use of
  8. # this software code. (c) 2006-2007 Amazon Digital Services, Inc. or its
  9. # affiliates.
  10. import base64
  11. import hmac
  12. import httplib
  13. import re
  14. import sha
  15. import sys
  16. import time
  17. import urllib
  18. import urlparse
  19. import xml.sax
  20. DEFAULT_HOST = 's3.amazonaws.com'
  21. PORTS_BY_SECURITY = { True: 443, False: 80 }
  22. METADATA_PREFIX = 'x-amz-meta-'
  23. AMAZON_HEADER_PREFIX = 'x-amz-'
  24. # generates the aws canonical string for the given parameters
  25. def canonical_string(method, bucket="", key="", query_args={}, headers={}, expires=None):
  26. interesting_headers = {}
  27. for header_key in headers:
  28. lk = header_key.lower()
  29. if lk in ['content-md5', 'content-type', 'date'] or lk.startswith(AMAZON_HEADER_PREFIX):
  30. interesting_headers[lk] = headers[header_key].strip()
  31. # these keys get empty strings if they don't exist
  32. if not interesting_headers.has_key('content-type'):
  33. interesting_headers['content-type'] = ''
  34. if not interesting_headers.has_key('content-md5'):
  35. interesting_headers['content-md5'] = ''
  36. # just in case someone used this. it's not necessary in this lib.
  37. if interesting_headers.has_key('x-amz-date'):
  38. interesting_headers['date'] = ''
  39. # if you're using expires for query string auth, then it trumps date
  40. # (and x-amz-date)
  41. if expires:
  42. interesting_headers['date'] = str(expires)
  43. sorted_header_keys = interesting_headers.keys()
  44. sorted_header_keys.sort()
  45. buf = "%s\n" % method
  46. for header_key in sorted_header_keys:
  47. if header_key.startswith(AMAZON_HEADER_PREFIX):
  48. buf += "%s:%s\n" % (header_key, interesting_headers[header_key])
  49. else:
  50. buf += "%s\n" % interesting_headers[header_key]
  51. # append the bucket if it exists
  52. if bucket != "":
  53. buf += "/%s" % bucket
  54. # add the key. even if it doesn't exist, add the slash
  55. buf += "/%s" % urllib.quote_plus(key)
  56. # handle special query string arguments
  57. if query_args.has_key("acl"):
  58. buf += "?acl"
  59. elif query_args.has_key("torrent"):
  60. buf += "?torrent"
  61. elif query_args.has_key("logging"):
  62. buf += "?logging"
  63. elif query_args.has_key("location"):
  64. buf += "?location"
  65. return buf
  66. # computes the base64'ed hmac-sha hash of the canonical string and the secret
  67. # access key, optionally urlencoding the result
  68. def encode(aws_secret_access_key, str, urlencode=False):
  69. b64_hmac = base64.encodestring(hmac.new(aws_secret_access_key, str, sha).digest()).strip()
  70. if urlencode:
  71. return urllib.quote_plus(b64_hmac)
  72. else:
  73. return b64_hmac
  74. def merge_meta(headers, metadata):
  75. final_headers = headers.copy()
  76. for k in metadata.keys():
  77. final_headers[METADATA_PREFIX + k] = metadata[k]
  78. return final_headers
  79. # builds the query arg string
  80. def query_args_hash_to_string(query_args):
  81. query_string = ""
  82. pairs = []
  83. for k, v in query_args.items():
  84. piece = k
  85. if v != None:
  86. piece += "=%s" % urllib.quote_plus(str(v))
  87. pairs.append(piece)
  88. return '&'.join(pairs)
  89. class CallingFormat:
  90. PATH = 1
  91. SUBDOMAIN = 2
  92. VANITY = 3
  93. def build_url_base(protocol, server, port, bucket, calling_format):
  94. url_base = '%s://' % protocol
  95. if bucket == '':
  96. url_base += server
  97. elif calling_format == CallingFormat.SUBDOMAIN:
  98. url_base += "%s.%s" % (bucket, server)
  99. elif calling_format == CallingFormat.VANITY:
  100. url_base += bucket
  101. else:
  102. url_base += server
  103. url_base += ":%s" % port
  104. if (bucket != '') and (calling_format == CallingFormat.PATH):
  105. url_base += "/%s" % bucket
  106. return url_base
  107. build_url_base = staticmethod(build_url_base)
  108. class Location:
  109. DEFAULT = None
  110. EU = 'EU'
  111. class AWSAuthConnection:
  112. def __init__(self, aws_access_key_id, aws_secret_access_key, is_secure=True,
  113. server=DEFAULT_HOST, port=None, calling_format=CallingFormat.SUBDOMAIN):
  114. if not port:
  115. port = PORTS_BY_SECURITY[is_secure]
  116. self.aws_access_key_id = aws_access_key_id
  117. self.aws_secret_access_key = aws_secret_access_key
  118. self.is_secure = is_secure
  119. self.server = server
  120. self.port = port
  121. self.calling_format = calling_format
  122. def create_bucket(self, bucket, headers={}):
  123. return Response(self._make_request('PUT', bucket, '', {}, headers))
  124. def create_located_bucket(self, bucket, location=Location.DEFAULT, headers={}):
  125. if location == Location.DEFAULT:
  126. body = ""
  127. else:
  128. body = "<CreateBucketConstraint><LocationConstraint>" + \
  129. location + \
  130. "</LocationConstraint></CreateBucketConstraint>"
  131. return Response(self._make_request('PUT', bucket, '', {}, headers, body))
  132. def check_bucket_exists(self, bucket):
  133. return self._make_request('HEAD', bucket, '', {}, {})
  134. def list_bucket(self, bucket, options={}, headers={}):
  135. return ListBucketResponse(self._make_request('GET', bucket, '', options, headers))
  136. def delete_bucket(self, bucket, headers={}):
  137. return Response(self._make_request('DELETE', bucket, '', {}, headers))
  138. def put(self, bucket, key, object, headers={}):
  139. if not isinstance(object, S3Object):
  140. object = S3Object(object)
  141. return Response(
  142. self._make_request(
  143. 'PUT',
  144. bucket,
  145. key,
  146. {},
  147. headers,
  148. object.data,
  149. object.metadata))
  150. def get(self, bucket, key, headers={}):
  151. return GetResponse(
  152. self._make_request('GET', bucket, key, {}, headers))
  153. def delete(self, bucket, key, headers={}):
  154. return Response(
  155. self._make_request('DELETE', bucket, key, {}, headers))
  156. def get_bucket_logging(self, bucket, headers={}):
  157. return GetResponse(self._make_request('GET', bucket, '', { 'logging': None }, headers))
  158. def put_bucket_logging(self, bucket, logging_xml_doc, headers={}):
  159. return Response(self._make_request('PUT', bucket, '', { 'logging': None }, headers, logging_xml_doc))
  160. def get_bucket_acl(self, bucket, headers={}):
  161. return self.get_acl(bucket, '', headers)
  162. def get_acl(self, bucket, key, headers={}):
  163. return GetResponse(
  164. self._make_request('GET', bucket, key, { 'acl': None }, headers))
  165. def put_bucket_acl(self, bucket, acl_xml_document, headers={}):
  166. return self.put_acl(bucket, '', acl_xml_document, headers)
  167. def put_acl(self, bucket, key, acl_xml_document, headers={}):
  168. return Response(
  169. self._make_request(
  170. 'PUT',
  171. bucket,
  172. key,
  173. { 'acl': None },
  174. headers,
  175. acl_xml_document))
  176. def list_all_my_buckets(self, headers={}):
  177. return ListAllMyBucketsResponse(self._make_request('GET', '', '', {}, headers))
  178. def get_bucket_location(self, bucket):
  179. return LocationResponse(self._make_request('GET', bucket, '', {'location' : None}))
  180. # end public methods
  181. def _make_request(self, method, bucket='', key='', query_args={}, headers={}, data='', metadata={}):
  182. server = ''
  183. if bucket == '':
  184. server = self.server
  185. elif self.calling_format == CallingFormat.SUBDOMAIN:
  186. server = "%s.%s" % (bucket, self.server)
  187. elif self.calling_format == CallingFormat.VANITY:
  188. server = bucket
  189. else:
  190. server = self.server
  191. path = ''
  192. if (bucket != '') and (self.calling_format == CallingFormat.PATH):
  193. path += "/%s" % bucket
  194. # add the slash after the bucket regardless
  195. # the key will be appended if it is non-empty
  196. path += "/%s" % urllib.quote_plus(key)
  197. # build the path_argument string
  198. # add the ? in all cases since
  199. # signature and credentials follow path args
  200. if len(query_args):
  201. path += "?" + query_args_hash_to_string(query_args)
  202. is_secure = self.is_secure
  203. host = "%s:%d" % (server, self.port)
  204. while True:
  205. if (is_secure):
  206. connection = httplib.HTTPSConnection(host)
  207. else:
  208. connection = httplib.HTTPConnection(host)
  209. final_headers = merge_meta(headers, metadata);
  210. # add auth header
  211. self._add_aws_auth_header(final_headers, method, bucket, key, query_args)
  212. connection.request(method, path, data, final_headers)
  213. resp = connection.getresponse()
  214. if resp.status < 300 or resp.status >= 400:
  215. return resp
  216. # handle redirect
  217. location = resp.getheader('location')
  218. if not location:
  219. return resp
  220. # (close connection)
  221. resp.read()
  222. scheme, host, path, params, query, fragment \
  223. = urlparse.urlparse(location)
  224. if scheme == "http": is_secure = True
  225. elif scheme == "https": is_secure = False
  226. else: raise invalidURL("Not http/https: " + location)
  227. if query: path += "?" + query
  228. # retry with redirect
  229. def _add_aws_auth_header(self, headers, method, bucket, key, query_args):
  230. if not headers.has_key('Date'):
  231. headers['Date'] = time.strftime("%a, %d %b %Y %X GMT", time.gmtime())
  232. c_string = canonical_string(method, bucket, key, query_args, headers)
  233. headers['Authorization'] = \
  234. "AWS %s:%s" % (self.aws_access_key_id, encode(self.aws_secret_access_key, c_string))
  235. class QueryStringAuthGenerator:
  236. # by default, expire in 1 minute
  237. DEFAULT_EXPIRES_IN = 60
  238. def __init__(self, aws_access_key_id, aws_secret_access_key, is_secure=True,
  239. server=DEFAULT_HOST, port=None, calling_format=CallingFormat.SUBDOMAIN):
  240. if not port:
  241. port = PORTS_BY_SECURITY[is_secure]
  242. self.aws_access_key_id = aws_access_key_id
  243. self.aws_secret_access_key = aws_secret_access_key
  244. if (is_secure):
  245. self.protocol = 'https'
  246. else:
  247. self.protocol = 'http'
  248. self.is_secure = is_secure
  249. self.server = server
  250. self.port = port
  251. self.calling_format = calling_format
  252. self.__expires_in = QueryStringAuthGenerator.DEFAULT_EXPIRES_IN
  253. self.__expires = None
  254. # for backwards compatibility with older versions
  255. self.server_name = "%s:%s" % (self.server, self.port)
  256. def set_expires_in(self, expires_in):
  257. self.__expires_in = expires_in
  258. self.__expires = None
  259. def set_expires(self, expires):
  260. self.__expires = expires
  261. self.__expires_in = None
  262. def create_bucket(self, bucket, headers={}):
  263. return self.generate_url('PUT', bucket, '', {}, headers)
  264. def list_bucket(self, bucket, options={}, headers={}):
  265. return self.generate_url('GET', bucket, '', options, headers)
  266. def delete_bucket(self, bucket, headers={}):
  267. return self.generate_url('DELETE', bucket, '', {}, headers)
  268. def put(self, bucket, key, object, headers={}):
  269. if not isinstance(object, S3Object):
  270. object = S3Object(object)
  271. return self.generate_url(
  272. 'PUT',
  273. bucket,
  274. key,
  275. {},
  276. merge_meta(headers, object.metadata))
  277. def get(self, bucket, key, headers={}):
  278. return self.generate_url('GET', bucket, key, {}, headers)
  279. def delete(self, bucket, key, headers={}):
  280. return self.generate_url('DELETE', bucket, key, {}, headers)
  281. def get_bucket_logging(self, bucket, headers={}):
  282. return self.generate_url('GET', bucket, '', { 'logging': None }, headers)
  283. def put_bucket_logging(self, bucket, logging_xml_doc, headers={}):
  284. return self.generate_url('PUT', bucket, '', { 'logging': None }, headers)
  285. def get_bucket_acl(self, bucket, headers={}):
  286. return self.get_acl(bucket, '', headers)
  287. def get_acl(self, bucket, key='', headers={}):
  288. return self.generate_url('GET', bucket, key, { 'acl': None }, headers)
  289. def put_bucket_acl(self, bucket, acl_xml_document, headers={}):
  290. return self.put_acl(bucket, '', acl_xml_document, headers)
  291. # don't really care what the doc is here.
  292. def put_acl(self, bucket, key, acl_xml_document, headers={}):
  293. return self.generate_url('PUT', bucket, key, { 'acl': None }, headers)
  294. def list_all_my_buckets(self, headers={}):
  295. return self.generate_url('GET', '', '', {}, headers)
  296. def make_bare_url(self, bucket, key=''):
  297. full_url = self.generate_url(self, bucket, key)
  298. return full_url[:full_url.index('?')]
  299. def generate_url(self, method, bucket='', key='', query_args={}, headers={}):
  300. expires = 0
  301. if self.__expires_in != None:
  302. expires = int(time.time() + self.__expires_in)
  303. elif self.__expires != None:
  304. expires = int(self.__expires)
  305. else:
  306. raise "Invalid expires state"
  307. canonical_str = canonical_string(method, bucket, key, query_args, headers, expires)
  308. encoded_canonical = encode(self.aws_secret_access_key, canonical_str)
  309. url = CallingFormat.build_url_base(self.protocol, self.server, self.port, bucket, self.calling_format)
  310. url += "/%s" % urllib.quote_plus(key)
  311. query_args['Signature'] = encoded_canonical
  312. query_args['Expires'] = expires
  313. query_args['AWSAccessKeyId'] = self.aws_access_key_id
  314. url += "?%s" % query_args_hash_to_string(query_args)
  315. return url
  316. class S3Object:
  317. def __init__(self, data, metadata={}):
  318. self.data = data
  319. self.metadata = metadata
  320. class Owner:
  321. def __init__(self, id='', display_name=''):
  322. self.id = id
  323. self.display_name = display_name
  324. class ListEntry:
  325. def __init__(self, key='', last_modified=None, etag='', size=0, storage_class='', owner=None):
  326. self.key = key
  327. self.last_modified = last_modified
  328. self.etag = etag
  329. self.size = size
  330. self.storage_class = storage_class
  331. self.owner = owner
  332. class CommonPrefixEntry:
  333. def __init(self, prefix=''):
  334. self.prefix = prefix
  335. class Bucket:
  336. def __init__(self, name='', creation_date=''):
  337. self.name = name
  338. self.creation_date = creation_date
  339. class Response:
  340. def __init__(self, http_response):
  341. self.http_response = http_response
  342. # you have to do this read, even if you don't expect a body.
  343. # otherwise, the next request fails.
  344. self.body = http_response.read()
  345. if http_response.status >= 300 and self.body:
  346. self.message = self.body
  347. else:
  348. self.message = "%03d %s" % (http_response.status, http_response.reason)
  349. class ListBucketResponse(Response):
  350. def __init__(self, http_response):
  351. Response.__init__(self, http_response)
  352. if http_response.status < 300:
  353. handler = ListBucketHandler()
  354. xml.sax.parseString(self.body, handler)
  355. self.entries = handler.entries
  356. self.common_prefixes = handler.common_prefixes
  357. self.name = handler.name
  358. self.marker = handler.marker
  359. self.prefix = handler.prefix
  360. self.is_truncated = handler.is_truncated
  361. self.delimiter = handler.delimiter
  362. self.max_keys = handler.max_keys
  363. self.next_marker = handler.next_marker
  364. else:
  365. self.entries = []
  366. class ListAllMyBucketsResponse(Response):
  367. def __init__(self, http_response):
  368. Response.__init__(self, http_response)
  369. if http_response.status < 300:
  370. handler = ListAllMyBucketsHandler()
  371. xml.sax.parseString(self.body, handler)
  372. self.entries = handler.entries
  373. else:
  374. self.entries = []
  375. class GetResponse(Response):
  376. def __init__(self, http_response):
  377. Response.__init__(self, http_response)
  378. response_headers = http_response.msg # older pythons don't have getheaders
  379. metadata = self.get_aws_metadata(response_headers)
  380. self.object = S3Object(self.body, metadata)
  381. def get_aws_metadata(self, headers):
  382. metadata = {}
  383. for hkey in headers.keys():
  384. if hkey.lower().startswith(METADATA_PREFIX):
  385. metadata[hkey[len(METADATA_PREFIX):]] = headers[hkey]
  386. del headers[hkey]
  387. return metadata
  388. class LocationResponse(Response):
  389. def __init__(self, http_response):
  390. Response.__init__(self, http_response)
  391. if http_response.status < 300:
  392. handler = LocationHandler()
  393. xml.sax.parseString(self.body, handler)
  394. self.location = handler.location
  395. class ListBucketHandler(xml.sax.ContentHandler):
  396. def __init__(self):
  397. self.entries = []
  398. self.curr_entry = None
  399. self.curr_text = ''
  400. self.common_prefixes = []
  401. self.curr_common_prefix = None
  402. self.name = ''
  403. self.marker = ''
  404. self.prefix = ''
  405. self.is_truncated = False
  406. self.delimiter = ''
  407. self.max_keys = 0
  408. self.next_marker = ''
  409. self.is_echoed_prefix_set = False
  410. def startElement(self, name, attrs):
  411. if name == 'Contents':
  412. self.curr_entry = ListEntry()
  413. elif name == 'Owner':
  414. self.curr_entry.owner = Owner()
  415. elif name == 'CommonPrefixes':
  416. self.curr_common_prefix = CommonPrefixEntry()
  417. def endElement(self, name):
  418. if name == 'Contents':
  419. self.entries.append(self.curr_entry)
  420. elif name == 'CommonPrefixes':
  421. self.common_prefixes.append(self.curr_common_prefix)
  422. elif name == 'Key':
  423. self.curr_entry.key = self.curr_text
  424. elif name == 'LastModified':
  425. self.curr_entry.last_modified = self.curr_text
  426. elif name == 'ETag':
  427. self.curr_entry.etag = self.curr_text
  428. elif name == 'Size':
  429. self.curr_entry.size = int(self.curr_text)
  430. elif name == 'ID':
  431. self.curr_entry.owner.id = self.curr_text
  432. elif name == 'DisplayName':
  433. self.curr_entry.owner.display_name = self.curr_text
  434. elif name == 'StorageClass':
  435. self.curr_entry.storage_class = self.curr_text
  436. elif name == 'Name':
  437. self.name = self.curr_text
  438. elif name == 'Prefix' and self.is_echoed_prefix_set:
  439. self.curr_common_prefix.prefix = self.curr_text
  440. elif name == 'Prefix':
  441. self.prefix = self.curr_text
  442. self.is_echoed_prefix_set = True
  443. elif name == 'Marker':
  444. self.marker = self.curr_text
  445. elif name == 'IsTruncated':
  446. self.is_truncated = self.curr_text == 'true'
  447. elif name == 'Delimiter':
  448. self.delimiter = self.curr_text
  449. elif name == 'MaxKeys':
  450. self.max_keys = int(self.curr_text)
  451. elif name == 'NextMarker':
  452. self.next_marker = self.curr_text
  453. self.curr_text = ''
  454. def characters(self, content):
  455. self.curr_text += content
  456. class ListAllMyBucketsHandler(xml.sax.ContentHandler):
  457. def __init__(self):
  458. self.entries = []
  459. self.curr_entry = None
  460. self.curr_text = ''
  461. def startElement(self, name, attrs):
  462. if name == 'Bucket':
  463. self.curr_entry = Bucket()
  464. def endElement(self, name):
  465. if name == 'Name':
  466. self.curr_entry.name = self.curr_text
  467. elif name == 'CreationDate':
  468. self.curr_entry.creation_date = self.curr_text
  469. elif name == 'Bucket':
  470. self.entries.append(self.curr_entry)
  471. def characters(self, content):
  472. self.curr_text = content
  473. class LocationHandler(xml.sax.ContentHandler):
  474. def __init__(self):
  475. self.location = None
  476. self.state = 'init'
  477. def startElement(self, name, attrs):
  478. if self.state == 'init':
  479. if name == 'LocationConstraint':
  480. self.state = 'tag_location'
  481. self.location = ''
  482. else: self.state = 'bad'
  483. else: self.state = 'bad'
  484. def endElement(self, name):
  485. if self.state == 'tag_location' and name == 'LocationConstraint':
  486. self.state = 'done'
  487. else: self.state = 'bad'
  488. def characters(self, content):
  489. if self.state == 'tag_location':
  490. self.location += content