aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorjason2009-12-08 12:30:10 -0700
committerjason2009-12-08 12:30:10 -0700
commitf8c6036648d91abe1c3d07256c2690e56fdff8cd (patch)
tree8bcf8b325d4fcf548db4ae270a5b48dd322401d7
downloadamazons3-py-f8c6036648d91abe1c3d07256c2690e56fdff8cd.tar.gz
amazons3-py-f8c6036648d91abe1c3d07256c2690e56fdff8cd.zip
Initial commit
-rw-r--r--.gitignore2
-rw-r--r--S3.py617
-rw-r--r--__init__.py0
-rw-r--r--django/__init__.py121
-rw-r--r--s3-driver.py118
-rw-r--r--s3-test.py267
6 files changed, 1125 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c9b568f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
1*.pyc
2*.swp
diff --git a/S3.py b/S3.py
new file mode 100644
index 0000000..37682f3
--- /dev/null
+++ b/S3.py
@@ -0,0 +1,617 @@
1#!/usr/bin/env python
2
3# This software code is made available "AS IS" without warranties of any
4# kind. You may copy, display, modify and redistribute the software
5# code either by itself or as incorporated into your code; provided that
6# you do not remove any proprietary notices. Your use of this software
7# code is at your own risk and you waive any claim against Amazon
8# Digital Services, Inc. or its affiliates with respect to your use of
9# this software code. (c) 2006-2007 Amazon Digital Services, Inc. or its
10# affiliates.
11
12import base64
13import hmac
14import httplib
15import re
16import sha
17import sys
18import time
19import urllib
20import urlparse
21import xml.sax
22
23DEFAULT_HOST = 's3.amazonaws.com'
24PORTS_BY_SECURITY = { True: 443, False: 80 }
25METADATA_PREFIX = 'x-amz-meta-'
26AMAZON_HEADER_PREFIX = 'x-amz-'
27
28# generates the aws canonical string for the given parameters
29def canonical_string(method, bucket="", key="", query_args={}, headers={}, expires=None):
30 interesting_headers = {}
31 for header_key in headers:
32 lk = header_key.lower()
33 if lk in ['content-md5', 'content-type', 'date'] or lk.startswith(AMAZON_HEADER_PREFIX):
34 interesting_headers[lk] = headers[header_key].strip()
35
36 # these keys get empty strings if they don't exist
37 if not interesting_headers.has_key('content-type'):
38 interesting_headers['content-type'] = ''
39 if not interesting_headers.has_key('content-md5'):
40 interesting_headers['content-md5'] = ''
41
42 # just in case someone used this. it's not necessary in this lib.
43 if interesting_headers.has_key('x-amz-date'):
44 interesting_headers['date'] = ''
45
46 # if you're using expires for query string auth, then it trumps date
47 # (and x-amz-date)
48 if expires:
49 interesting_headers['date'] = str(expires)
50
51 sorted_header_keys = interesting_headers.keys()
52 sorted_header_keys.sort()
53
54 buf = "%s\n" % method
55 for header_key in sorted_header_keys:
56 if header_key.startswith(AMAZON_HEADER_PREFIX):
57 buf += "%s:%s\n" % (header_key, interesting_headers[header_key])
58 else:
59 buf += "%s\n" % interesting_headers[header_key]
60
61 # append the bucket if it exists
62 if bucket != "":
63 buf += "/%s" % bucket
64
65 # add the key. even if it doesn't exist, add the slash
66 buf += "/%s" % urllib.quote_plus(key.encode('utf-8'))
67
68 # handle special query string arguments
69
70 if query_args.has_key("acl"):
71 buf += "?acl"
72 elif query_args.has_key("torrent"):
73 buf += "?torrent"
74 elif query_args.has_key("logging"):
75 buf += "?logging"
76 elif query_args.has_key("location"):
77 buf += "?location"
78
79 return buf
80
81# computes the base64'ed hmac-sha hash of the canonical string and the secret
82# access key, optionally urlencoding the result
83def encode(aws_secret_access_key, str, urlencode=False):
84 b64_hmac = base64.encodestring(hmac.new(aws_secret_access_key, str, sha).digest()).strip()
85 if urlencode:
86 return urllib.quote_plus(b64_hmac)
87 else:
88 return b64_hmac
89
90def merge_meta(headers, metadata):
91 final_headers = headers.copy()
92 for k in metadata.keys():
93 final_headers[METADATA_PREFIX + k] = metadata[k]
94
95 return final_headers
96
97# builds the query arg string
98def query_args_hash_to_string(query_args):
99 query_string = ""
100 pairs = []
101 for k, v in query_args.items():
102 piece = k
103 if v != None:
104 piece += "=%s" % urllib.quote_plus(str(v).encode('utf-8'))
105 pairs.append(piece)
106
107 return '&'.join(pairs)
108
109
110class CallingFormat:
111 PATH = 1
112 SUBDOMAIN = 2
113 VANITY = 3
114
115 def build_url_base(protocol, server, port, bucket, calling_format):
116 url_base = '%s://' % protocol
117
118 if bucket == '':
119 url_base += server
120 elif calling_format == CallingFormat.SUBDOMAIN:
121 url_base += "%s.%s" % (bucket, server)
122 elif calling_format == CallingFormat.VANITY:
123 url_base += bucket
124 else:
125 url_base += server
126
127 url_base += ":%s" % port
128
129 if (bucket != '') and (calling_format == CallingFormat.PATH):
130 url_base += "/%s" % bucket
131
132 return url_base
133
134 build_url_base = staticmethod(build_url_base)
135
136
137
138class Location:
139 DEFAULT = None
140 EU = 'EU'
141
142
143
144class AWSAuthConnection:
145 def __init__(self, aws_access_key_id, aws_secret_access_key, is_secure=True,
146 server=DEFAULT_HOST, port=None, calling_format=CallingFormat.SUBDOMAIN):
147
148 if not port:
149 port = PORTS_BY_SECURITY[is_secure]
150
151 self.aws_access_key_id = aws_access_key_id
152 self.aws_secret_access_key = aws_secret_access_key
153 self.is_secure = is_secure
154 self.server = server
155 self.port = port
156 self.calling_format = calling_format
157
158 def create_bucket(self, bucket, headers={}):
159 return Response(self._make_request('PUT', bucket, '', {}, headers))
160
161 def create_located_bucket(self, bucket, location=Location.DEFAULT, headers={}):
162 if location == Location.DEFAULT:
163 body = ""
164 else:
165 body = "<CreateBucketConstraint><LocationConstraint>" + \
166 location + \
167 "</LocationConstraint></CreateBucketConstraint>"
168 return Response(self._make_request('PUT', bucket, '', {}, headers, body))
169
170 def check_bucket_exists(self, bucket):
171 return self._make_request('HEAD', bucket, '', {}, {})
172
173 def list_bucket(self, bucket, options={}, headers={}):
174 return ListBucketResponse(self._make_request('GET', bucket, '', options, headers))
175
176 def delete_bucket(self, bucket, headers={}):
177 return Response(self._make_request('DELETE', bucket, '', {}, headers))
178
179 def put(self, bucket, key, object, headers={}):
180 if not isinstance(object, S3Object):
181 object = S3Object(object)
182
183 return Response(
184 self._make_request(
185 'PUT',
186 bucket,
187 key,
188 {},
189 headers,
190 object.data,
191 object.metadata))
192
193 def get(self, bucket, key, headers={}):
194 return GetResponse(
195 self._make_request('GET', bucket, key, {}, headers))
196
197 def delete(self, bucket, key, headers={}):
198 return Response(
199 self._make_request('DELETE', bucket, key, {}, headers))
200
201 def get_bucket_logging(self, bucket, headers={}):
202 return GetResponse(self._make_request('GET', bucket, '', { 'logging': None }, headers))
203
204 def put_bucket_logging(self, bucket, logging_xml_doc, headers={}):
205 return Response(self._make_request('PUT', bucket, '', { 'logging': None }, headers, logging_xml_doc))
206
207 def get_bucket_acl(self, bucket, headers={}):
208 return self.get_acl(bucket, '', headers)
209
210 def get_acl(self, bucket, key, headers={}):
211 return GetResponse(
212 self._make_request('GET', bucket, key, { 'acl': None }, headers))
213
214 def put_bucket_acl(self, bucket, acl_xml_document, headers={}):
215 return self.put_acl(bucket, '', acl_xml_document, headers)
216
217 def put_acl(self, bucket, key, acl_xml_document, headers={}):
218 return Response(
219 self._make_request(
220 'PUT',
221 bucket,
222 key,
223 { 'acl': None },
224 headers,
225 acl_xml_document))
226
227 def list_all_my_buckets(self, headers={}):
228 return ListAllMyBucketsResponse(self._make_request('GET', '', '', {}, headers))
229
230 def get_bucket_location(self, bucket):
231 return LocationResponse(self._make_request('GET', bucket, '', {'location' : None}))
232
233 # end public methods
234
235 def _make_request(self, method, bucket='', key='', query_args={}, headers={}, data='', metadata={}):
236
237 server = ''
238 if bucket == '':
239 server = self.server
240 elif self.calling_format == CallingFormat.SUBDOMAIN:
241 server = "%s.%s" % (bucket, self.server)
242 elif self.calling_format == CallingFormat.VANITY:
243 server = bucket
244 else:
245 server = self.server
246
247 path = ''
248
249 if (bucket != '') and (self.calling_format == CallingFormat.PATH):
250 path += "/%s" % bucket
251
252 # add the slash after the bucket regardless
253 # the key will be appended if it is non-empty
254 path += "/%s" % urllib.quote_plus(key.encode('utf-8'))
255
256
257 # build the path_argument string
258 # add the ? in all cases since
259 # signature and credentials follow path args
260 if len(query_args):
261 path += "?" + query_args_hash_to_string(query_args)
262
263 is_secure = self.is_secure
264 host = "%s:%d" % (server, self.port)
265 while True:
266 if (is_secure):
267 connection = httplib.HTTPSConnection(host)
268 else:
269 connection = httplib.HTTPConnection(host)
270
271 final_headers = merge_meta(headers, metadata);
272 # add auth header
273 self._add_aws_auth_header(final_headers, method, bucket, key, query_args)
274
275 connection.request(method, path, data, final_headers)
276 resp = connection.getresponse()
277 if resp.status < 300 or resp.status >= 400:
278 return resp
279 # handle redirect
280 location = resp.getheader('location')
281 if not location:
282 return resp
283 # (close connection)
284 resp.read()
285 scheme, host, path, params, query, fragment \
286 = urlparse.urlparse(location)
287 if scheme == "http": is_secure = True
288 elif scheme == "https": is_secure = False
289 else: raise invalidURL("Not http/https: " + location)
290 if query: path += "?" + query
291 # retry with redirect
292
293 def _add_aws_auth_header(self, headers, method, bucket, key, query_args):
294 if not headers.has_key('Date'):
295 headers['Date'] = time.strftime("%a, %d %b %Y %X GMT", time.gmtime())
296
297 c_string = canonical_string(method, bucket, key, query_args, headers)
298 headers['Authorization'] = \
299 "AWS %s:%s" % (self.aws_access_key_id, encode(self.aws_secret_access_key, c_string))
300
301
302class QueryStringAuthGenerator:
303 # by default, expire in 1 minute
304 DEFAULT_EXPIRES_IN = 60
305
306 def __init__(self, aws_access_key_id, aws_secret_access_key, is_secure=True,
307 server=DEFAULT_HOST, port=None, calling_format=CallingFormat.SUBDOMAIN):
308
309 if not port:
310 port = PORTS_BY_SECURITY[is_secure]
311
312 self.aws_access_key_id = aws_access_key_id
313 self.aws_secret_access_key = aws_secret_access_key
314 if (is_secure):
315 self.protocol = 'https'
316 else:
317 self.protocol = 'http'
318
319 self.is_secure = is_secure
320 self.server = server
321 self.port = port
322 self.calling_format = calling_format
323 self.__expires_in = QueryStringAuthGenerator.DEFAULT_EXPIRES_IN
324 self.__expires = None
325
326 # for backwards compatibility with older versions
327 self.server_name = "%s:%s" % (self.server, self.port)
328
329 def set_expires_in(self, expires_in):
330 self.__expires_in = expires_in
331 self.__expires = None
332
333 def set_expires(self, expires):
334 self.__expires = expires
335 self.__expires_in = None
336
337 def create_bucket(self, bucket, headers={}):
338 return self.generate_url('PUT', bucket, '', {}, headers)
339
340 def list_bucket(self, bucket, options={}, headers={}):
341 return self.generate_url('GET', bucket, '', options, headers)
342
343 def delete_bucket(self, bucket, headers={}):
344 return self.generate_url('DELETE', bucket, '', {}, headers)
345
346 def put(self, bucket, key, object, headers={}):
347 if not isinstance(object, S3Object):
348 object = S3Object(object)
349
350 return self.generate_url(
351 'PUT',
352 bucket,
353 key,
354 {},
355 merge_meta(headers, object.metadata))
356
357 def get(self, bucket, key, headers={}):
358 return self.generate_url('GET', bucket, key, {}, headers)
359
360 def delete(self, bucket, key, headers={}):
361 return self.generate_url('DELETE', bucket, key, {}, headers)
362
363 def get_bucket_logging(self, bucket, headers={}):
364 return self.generate_url('GET', bucket, '', { 'logging': None }, headers)
365
366 def put_bucket_logging(self, bucket, logging_xml_doc, headers={}):
367 return self.generate_url('PUT', bucket, '', { 'logging': None }, headers)
368
369 def get_bucket_acl(self, bucket, headers={}):
370 return self.get_acl(bucket, '', headers)
371
372 def get_acl(self, bucket, key='', headers={}):
373 return self.generate_url('GET', bucket, key, { 'acl': None }, headers)
374
375 def put_bucket_acl(self, bucket, acl_xml_document, headers={}):
376 return self.put_acl(bucket, '', acl_xml_document, headers)
377
378 # don't really care what the doc is here.
379 def put_acl(self, bucket, key, acl_xml_document, headers={}):
380 return self.generate_url('PUT', bucket, key, { 'acl': None }, headers)
381
382 def list_all_my_buckets(self, headers={}):
383 return self.generate_url('GET', '', '', {}, headers)
384
385 def make_bare_url(self, bucket, key=''):
386 full_url = self.generate_url(self, bucket, key)
387 return full_url[:full_url.index('?')]
388
389 def generate_url(self, method, bucket='', key='', query_args={}, headers={}):
390 expires = 0
391 if self.__expires_in != None:
392 expires = int(time.time() + self.__expires_in)
393 elif self.__expires != None:
394 expires = int(self.__expires)
395 else:
396 raise "Invalid expires state"
397
398 canonical_str = canonical_string(method, bucket, key, query_args, headers, expires)
399 encoded_canonical = encode(self.aws_secret_access_key, canonical_str)
400
401 url = CallingFormat.build_url_base(self.protocol, self.server, self.port, bucket, self.calling_format)
402
403 url += "/%s" % urllib.quote_plus(key.encode('utf-8'))
404
405 query_args['Signature'] = encoded_canonical
406 query_args['Expires'] = expires
407 query_args['AWSAccessKeyId'] = self.aws_access_key_id
408
409 url += "?%s" % query_args_hash_to_string(query_args)
410
411 return url
412
413
414class S3Object:
415 def __init__(self, data, metadata={}):
416 self.data = data
417 self.metadata = metadata
418
419class Owner:
420 def __init__(self, id='', display_name=''):
421 self.id = id
422 self.display_name = display_name
423
424class ListEntry:
425 def __init__(self, key='', last_modified=None, etag='', size=0, storage_class='', owner=None):
426 self.key = key
427 self.last_modified = last_modified
428 self.etag = etag
429 self.size = size
430 self.storage_class = storage_class
431 self.owner = owner
432
433class CommonPrefixEntry:
434 def __init(self, prefix=''):
435 self.prefix = prefix
436
437class Bucket:
438 def __init__(self, name='', creation_date=''):
439 self.name = name
440 self.creation_date = creation_date
441
442class Response:
443 def __init__(self, http_response):
444 self.http_response = http_response
445 # you have to do this read, even if you don't expect a body.
446 # otherwise, the next request fails.
447 self.body = http_response.read()
448 if http_response.status >= 300 and self.body:
449 self.message = self.body
450 else:
451 self.message = "%03d %s" % (http_response.status, http_response.reason)
452
453
454
455class ListBucketResponse(Response):
456 def __init__(self, http_response):
457 Response.__init__(self, http_response)
458 if http_response.status < 300:
459 handler = ListBucketHandler()
460 xml.sax.parseString(self.body, handler)
461 self.entries = handler.entries
462 self.common_prefixes = handler.common_prefixes
463 self.name = handler.name
464 self.marker = handler.marker
465 self.prefix = handler.prefix
466 self.is_truncated = handler.is_truncated
467 self.delimiter = handler.delimiter
468 self.max_keys = handler.max_keys
469 self.next_marker = handler.next_marker
470 else:
471 self.entries = []
472
473class ListAllMyBucketsResponse(Response):
474 def __init__(self, http_response):
475 Response.__init__(self, http_response)
476 if http_response.status < 300:
477 handler = ListAllMyBucketsHandler()
478 xml.sax.parseString(self.body, handler)
479 self.entries = handler.entries
480 else:
481 self.entries = []
482
483class GetResponse(Response):
484 def __init__(self, http_response):
485 Response.__init__(self, http_response)
486 response_headers = http_response.msg # older pythons don't have getheaders
487 metadata = self.get_aws_metadata(response_headers)
488 self.object = S3Object(self.body, metadata)
489
490 def get_aws_metadata(self, headers):
491 metadata = {}
492 for hkey in headers.keys():
493 if hkey.lower().startswith(METADATA_PREFIX):
494 metadata[hkey[len(METADATA_PREFIX):]] = headers[hkey]
495 del headers[hkey]
496
497 return metadata
498
499class LocationResponse(Response):
500 def __init__(self, http_response):
501 Response.__init__(self, http_response)
502 if http_response.status < 300:
503 handler = LocationHandler()
504 xml.sax.parseString(self.body, handler)
505 self.location = handler.location
506
507class ListBucketHandler(xml.sax.ContentHandler):
508 def __init__(self):
509 self.entries = []
510 self.curr_entry = None
511 self.curr_text = ''
512 self.common_prefixes = []
513 self.curr_common_prefix = None
514 self.name = ''
515 self.marker = ''
516 self.prefix = ''
517 self.is_truncated = False
518 self.delimiter = ''
519 self.max_keys = 0
520 self.next_marker = ''
521 self.is_echoed_prefix_set = False
522
523 def startElement(self, name, attrs):
524 if name == 'Contents':
525 self.curr_entry = ListEntry()
526 elif name == 'Owner':
527 self.curr_entry.owner = Owner()
528 elif name == 'CommonPrefixes':
529 self.curr_common_prefix = CommonPrefixEntry()
530
531
532 def endElement(self, name):
533 if name == 'Contents':
534 self.entries.append(self.curr_entry)
535 elif name == 'CommonPrefixes':
536 self.common_prefixes.append(self.curr_common_prefix)
537 elif name == 'Key':
538 self.curr_entry.key = self.curr_text
539 elif name == 'LastModified':
540 self.curr_entry.last_modified = self.curr_text
541 elif name == 'ETag':
542 self.curr_entry.etag = self.curr_text
543 elif name == 'Size':
544 self.curr_entry.size = int(self.curr_text)
545 elif name == 'ID':
546 self.curr_entry.owner.id = self.curr_text
547 elif name == 'DisplayName':
548 self.curr_entry.owner.display_name = self.curr_text
549 elif name == 'StorageClass':
550 self.curr_entry.storage_class = self.curr_text
551 elif name == 'Name':
552 self.name = self.curr_text
553 elif name == 'Prefix' and self.is_echoed_prefix_set:
554 self.curr_common_prefix.prefix = self.curr_text
555 elif name == 'Prefix':
556 self.prefix = self.curr_text
557 self.is_echoed_prefix_set = True
558 elif name == 'Marker':
559 self.marker = self.curr_text
560 elif name == 'IsTruncated':
561 self.is_truncated = self.curr_text == 'true'
562 elif name == 'Delimiter':
563 self.delimiter = self.curr_text
564 elif name == 'MaxKeys':
565 self.max_keys = int(self.curr_text)
566 elif name == 'NextMarker':
567 self.next_marker = self.curr_text
568
569 self.curr_text = ''
570
571 def characters(self, content):
572 self.curr_text += content
573
574
575class ListAllMyBucketsHandler(xml.sax.ContentHandler):
576 def __init__(self):
577 self.entries = []
578 self.curr_entry = None
579 self.curr_text = ''
580
581 def startElement(self, name, attrs):
582 if name == 'Bucket':
583 self.curr_entry = Bucket()
584
585 def endElement(self, name):
586 if name == 'Name':
587 self.curr_entry.name = self.curr_text
588 elif name == 'CreationDate':
589 self.curr_entry.creation_date = self.curr_text
590 elif name == 'Bucket':
591 self.entries.append(self.curr_entry)
592
593 def characters(self, content):
594 self.curr_text = content
595
596
597class LocationHandler(xml.sax.ContentHandler):
598 def __init__(self):
599 self.location = None
600 self.state = 'init'
601
602 def startElement(self, name, attrs):
603 if self.state == 'init':
604 if name == 'LocationConstraint':
605 self.state = 'tag_location'
606 self.location = ''
607 else: self.state = 'bad'
608 else: self.state = 'bad'
609
610 def endElement(self, name):
611 if self.state == 'tag_location' and name == 'LocationConstraint':
612 self.state = 'done'
613 else: self.state = 'bad'
614
615 def characters(self, content):
616 if self.state == 'tag_location':
617 self.location += content
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/__init__.py
diff --git a/django/__init__.py b/django/__init__.py
new file mode 100644
index 0000000..7027c19
--- /dev/null
+++ b/django/__init__.py
@@ -0,0 +1,121 @@
1from django.conf import settings
2from amazons3 import S3
3
4from django.core.files.storage import Storage
5
6class S3Error(Exception):
7 "Misc. S3 Service Error"
8 pass
9
10class S3Storage(Storage):
11 options = None
12
13 def __init__(self, options=None):
14 if not options:
15 options = settings.S3_SETTINGS
16 self.options = options
17 self.perm_tuple = (
18 'private',
19 'public-read',
20 'public-read-write',
21 'authenticated-read'
22 )
23 if self.options['default_perm'] not in self.perm_tuple:
24 self.options['default_perm'] = 'private'
25
26 self.connect()
27
28 def connect(self):
29 self.conn = S3.AWSAuthConnection(self.options['aws_key'], self.options['aws_secret_key'])
30
31 res = self.conn.check_bucket_exists(self.options['bucket'])
32
33 if res.status != 200:
34 res = self.conn.create_bucket(self.options['bucket'])
35 if res.http_response.status != 200:
36 raise S3Error, 'Unable to create bucket %s' % (self.options['bucket'])
37
38 return True
39
40 def exists(self, filename):
41 contents = self.conn.list_bucket(self.options['bucket'])
42 if filename in [f.key for f in contents.entries]:
43 return True
44 else:
45 return False
46
47 def size(self, filename):
48 contents = self.conn.list_bucket(self.options['bucket'])
49 for f in contents.entries:
50 if f.name == filename:
51 return f.size
52
53 return False
54
55 def url(self, filename):
56 server = self.options['bucket']
57 if not self.options['vanity_url']:
58 server += '.s3.amazonaws.com'
59 return 'http://' + server + '/' + filename
60
61
62 def _save(self, filename, content):
63 # a stupid hack
64 content = content.file
65 try:
66 data = content.read()
67 except IOError, err:
68 raise S3Error, 'Unable to read %s: %s' % (filename, err.strerror)
69
70 if not content.content_type:
71 import mimetypes
72 content_type = mimetypes.guess_type(filename)[0]
73 if content_type is None:
74 content_type = 'text/plain'
75 else:
76 content_type = content.content_type
77
78 perm = self.options['default_perm']
79
80 res = self.conn.put(
81 self.options['bucket'],
82 filename,
83 S3.S3Object(data),
84 {
85 'x-amz-acl': perm,
86 'Content-Type': content_type
87 }
88 )
89
90 if res.http_response.status != 200:
91 raise S3Error, 'Unable to upload file %s: Error code %s: %s' % (filename, self.options['bucket'], res.body)
92
93
94 content.filename = filename
95 content.url = self.url(filename)
96
97 return filename
98
99 def delete(self, filename):
100 res = self.conn.delete(self.options['bucket'], filename)
101 if res.http_response.status != 204:
102 pass
103 #raise S3Error, 'Unable to delete file %s' % (filename)
104
105 return (res.http_response.status == 204)
106
107 def path(self, filename):
108 raise NotImplementedError
109
110 def open(self, filename, mode):
111 from urllib import urlopen
112 return urlopen(self.url(filename))
113
114 def get_available_name(self, filename):
115 import os
116 basefilename = os.path.splitext(filename)
117 i = 1
118 while self.exists(filename):
119 filename = '%s-%d%s' % (basefilename[0], i, basefilename[1])
120
121 return filename
diff --git a/s3-driver.py b/s3-driver.py
new file mode 100644
index 0000000..29f700b
--- /dev/null
+++ b/s3-driver.py
@@ -0,0 +1,118 @@
1#!/usr/bin/env python
2
3# This software code is made available "AS IS" without warranties of any
4# kind. You may copy, display, modify and redistribute the software
5# code either by itself or as incorporated into your code; provided that
6# you do not remove any proprietary notices. Your use of this software
7# code is at your own risk and you waive any claim against Amazon
8# Digital Services, Inc. or its affiliates with respect to your use of
9# this software code. (c) 2006-2007 Amazon Digital Services, Inc. or its
10# affiliates.
11
12import S3
13import time
14import sys
15
16AWS_ACCESS_KEY_ID = '<INSERT YOUR AWS ACCESS KEY ID HERE>'
17AWS_SECRET_ACCESS_KEY = '<INSERT YOUR AWS SECRET ACCESS KEY HERE>'
18# remove these next two lines when you've updated your credentials.
19print "update s3-driver.py with your AWS credentials"
20sys.exit();
21
22# convert the bucket to lowercase for vanity domains
23# the bucket name must be lowercase since DNS is case-insensitive
24BUCKET_NAME = AWS_ACCESS_KEY_ID.lower() + '-test-bucket'
25KEY_NAME = 'test-key'
26
27conn = S3.AWSAuthConnection(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
28generator = S3.QueryStringAuthGenerator(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
29
30
31# Check if the bucket exists. The high availability engineering of
32# Amazon S3 is focused on get, put, list, and delete operations.
33# Because bucket operations work against a centralized, global
34# resource space, it is not appropriate to make bucket create or
35# delete calls on the high availability code path of your application.
36# It is better to create or delete buckets in a separate initialization
37# or setup routine that you run less often.
38if (conn.check_bucket_exists(BUCKET_NAME).status == 200):
39 print '----- bucket already exists! -----'
40else:
41 print '----- creating bucket -----'
42 print conn.create_located_bucket(BUCKET_NAME, S3.Location.DEFAULT).message
43 # to create an EU bucket
44 #print conn.create_located_bucket(BUCKET_NAME, S3.Location.EU).message
45
46print '----- bucket location -----'
47print conn.get_bucket_location(BUCKET_NAME).location
48
49print '----- listing bucket -----'
50print map(lambda x: x.key, conn.list_bucket(BUCKET_NAME).entries)
51
52print '----- putting object (with content type) -----'
53print conn.put(
54 BUCKET_NAME,
55 KEY_NAME,
56 S3.S3Object('this is a test'),
57 { 'Content-Type': 'text/plain' }).message
58
59print '----- listing bucket -----'
60print map(lambda x: x.key, conn.list_bucket(BUCKET_NAME).entries)
61
62print '----- getting object -----'
63print conn.get(BUCKET_NAME, KEY_NAME).object.data
64
65print '----- query string auth example -----'
66print "\nTry this url out in your browser (it will only be valid for 60 seconds).\n"
67generator.set_expires_in(60);
68url = generator.get(BUCKET_NAME, KEY_NAME)
69print url
70print '\npress enter> ',
71sys.stdin.readline()
72
73print "\nNow try just the url without the query string arguments. it should fail.\n"
74print generator.make_bare_url(BUCKET_NAME, KEY_NAME)
75print '\npress enter> ',
76sys.stdin.readline()
77
78
79print '----- putting object with metadata and public read acl -----'
80print conn.put(
81 BUCKET_NAME,
82 KEY_NAME + '-public',
83 S3.S3Object('this is a publicly readable test'),
84 { 'x-amz-acl': 'public-read' , 'Content-Type': 'text/plain' }
85).message
86
87print '----- anonymous read test ----'
88print "\nYou should be able to try this in your browser\n"
89public_key = KEY_NAME + '-public'
90print generator.make_bare_url(BUCKET_NAME, public_key)
91print "\npress enter> ",
92sys.stdin.readline()
93
94print "----- getting object's acl -----"
95print conn.get_acl(BUCKET_NAME, KEY_NAME).object.data
96
97print "\n----- path style url example -----";
98print "Non-location-constrained buckets can also be specified as part of the url path. (This was the original url style supported by S3.)\n";
99print "Try this url out in your browser (it will only be valid for 60 seconds).\n"
100generator.calling_format = S3.CallingFormat.PATH
101url = generator.get(BUCKET_NAME, KEY_NAME)
102print url
103print "\npress enter> ",
104sys.stdin.readline()
105
106print '----- deleting objects -----'
107print conn.delete(BUCKET_NAME, KEY_NAME).message
108print conn.delete(BUCKET_NAME, KEY_NAME + '-public').message
109
110print '----- listing bucket -----'
111print map(lambda x: x.key, conn.list_bucket(BUCKET_NAME).entries)
112
113print '----- listing all my buckets -----'
114print map(lambda x: x.name, conn.list_all_my_buckets().entries)
115
116print '----- deleting bucket ------'
117print conn.delete_bucket(BUCKET_NAME).message
118
diff --git a/s3-test.py b/s3-test.py
new file mode 100644
index 0000000..fbd8d9c
--- /dev/null
+++ b/s3-test.py
@@ -0,0 +1,267 @@
1#!/usr/bin/env python
2
3# This software code is made available "AS IS" without warranties of any
4# kind. You may copy, display, modify and redistribute the software
5# code either by itself or as incorporated into your code; provided that
6# you do not remove any proprietary notices. Your use of this software
7# code is at your own risk and you waive any claim against Amazon
8# Digital Services, Inc. or its affiliates with respect to your use of
9# this software code. (c) 2006-2007 Amazon Digital Services, Inc. or its
10# affiliates.
11
12import unittest
13import S3
14import httplib
15import sys
16
17AWS_ACCESS_KEY_ID = '<INSERT YOUR AWS ACCESS KEY ID HERE>'
18AWS_SECRET_ACCESS_KEY = '<INSERT YOUR AWS SECRET ACCESS KEY HERE>'
19# remove these next two lines when you've updated your credentials.
20print "update s3-test.py with your AWS credentials"
21sys.exit();
22
23# for subdomains (bucket.s3.amazonaws.com),
24# the bucket name must be lowercase since DNS is case-insensitive
25BUCKET_NAME = "%s-test-bucket" % AWS_ACCESS_KEY_ID.lower();
26
27
28class TestAWSAuthConnection(unittest.TestCase):
29 def setUp(self):
30 self.conn = S3.AWSAuthConnection(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
31
32 # test all operations for both regular and vanity domains
33 # regular: http://s3.amazonaws.com/bucket/key
34 # subdomain: http://bucket.s3.amazonaws.com/key
35 # testing pure vanity domains (http://<vanity domain>/key) is not covered here
36 # but is possible with some additional setup (set the server in @conn to your vanity domain)
37
38 def test_subdomain_default(self):
39 self.run_tests(S3.CallingFormat.SUBDOMAIN, S3.Location.DEFAULT)
40
41 def test_subdomain_eu(self):
42 self.run_tests(S3.CallingFormat.SUBDOMAIN, S3.Location.EU)
43
44 def test_path_default(self):
45 self.run_tests(S3.CallingFormat.PATH, S3.Location.DEFAULT)
46
47
48 def run_tests(self, calling_format, location):
49 self.conn.calling_format = calling_format
50
51 response = self.conn.create_located_bucket(BUCKET_NAME, location)
52 self.assertEquals(response.http_response.status, 200, 'create bucket')
53
54 response = self.conn.list_bucket(BUCKET_NAME)
55 self.assertEquals(response.http_response.status, 200, 'list bucket')
56 self.assertEquals(len(response.entries), 0, 'bucket is empty')
57
58 text = 'this is a test'
59 key = 'example.txt'
60
61 response = self.conn.put(BUCKET_NAME, key, text)
62 self.assertEquals(response.http_response.status, 200, 'put with a string argument')
63
64 response = \
65 self.conn.put(
66 BUCKET_NAME,
67 key,
68 S3.S3Object(text, {'title': 'title'}),
69 {'Content-Type': 'text/plain'})
70
71 self.assertEquals(response.http_response.status, 200, 'put with complex argument and headers')
72
73 response = self.conn.get(BUCKET_NAME, key)
74 self.assertEquals(response.http_response.status, 200, 'get object')
75 self.assertEquals(response.object.data, text, 'got right data')
76 self.assertEquals(response.object.metadata, { 'title': 'title' }, 'metadata is correct')
77 self.assertEquals(int(response.http_response.getheader('Content-Length')), len(text), 'got content-length header')
78
79 title_with_spaces = " \t title with leading and trailing spaces "
80 response = \
81 self.conn.put(
82 BUCKET_NAME,
83 key,
84 S3.S3Object(text, {'title': title_with_spaces}),
85 {'Content-Type': 'text/plain'})
86
87 self.assertEquals(response.http_response.status, 200, 'put with headers with spaces')
88
89 response = self.conn.get(BUCKET_NAME, key)
90 self.assertEquals(response.http_response.status, 200, 'get object')
91 self.assertEquals(
92 response.object.metadata,
93 { 'title': title_with_spaces.strip() },
94 'metadata with spaces is correct')
95
96 # delimited list tests
97 inner_key = 'test/inner.txt'
98 last_key = 'z-last-key.txt'
99 response = self.conn.put(BUCKET_NAME, inner_key, text)
100 self.assertEquals(response.http_response.status, 200, 'put inner key')
101
102 response = self.conn.put(BUCKET_NAME, last_key, text)
103 self.assertEquals(response.http_response.status, 200, 'put last key')
104
105 response = self.do_delimited_list(BUCKET_NAME, False, {'delimiter': '/'}, 2, 1, 'root list')
106
107 response = self.do_delimited_list(BUCKET_NAME, True, {'max-keys': 1, 'delimiter': '/'}, 1, 0, 'root list with max keys of 1', 'example.txt')
108
109 response = self.do_delimited_list(BUCKET_NAME, True, {'max-keys': 2, 'delimiter': '/'}, 1, 1, 'root list with max keys of 2, page 1', 'test/')
110
111 marker = response.next_marker
112
113 response = self.do_delimited_list(BUCKET_NAME, False, {'marker': marker, 'max-keys': 2, 'delimiter': '/'}, 1, 0, 'root list with max keys of 2, page 2')
114
115 response = self.do_delimited_list(BUCKET_NAME, False, {'prefix': 'test/', 'delimiter': '/'}, 1, 0, 'test/ list')
116
117 response = self.conn.delete(BUCKET_NAME, inner_key)
118 self.assertEquals(response.http_response.status, 204, 'delete %s' % inner_key)
119
120 response = self.conn.delete(BUCKET_NAME, last_key)
121 self.assertEquals(response.http_response.status, 204, 'delete %s' % last_key)
122
123
124 weird_key = '&=//%# ++++'
125
126 response = self.conn.put(BUCKET_NAME, weird_key, text)
127 self.assertEquals(response.http_response.status, 200, 'put weird key')
128
129 response = self.conn.get(BUCKET_NAME, weird_key)
130 self.assertEquals(response.http_response.status, 200, 'get weird key')
131
132 response = self.conn.get_acl(BUCKET_NAME, key)
133 self.assertEquals(response.http_response.status, 200, 'get acl')
134
135 acl = response.object.data
136
137 response = self.conn.put_acl(BUCKET_NAME, key, acl)
138 self.assertEquals(response.http_response.status, 200, 'put acl')
139
140 response = self.conn.get_bucket_acl(BUCKET_NAME)
141 self.assertEquals(response.http_response.status, 200, 'get bucket acl')
142
143 bucket_acl = response.object.data
144
145 response = self.conn.put_bucket_acl(BUCKET_NAME, bucket_acl)
146 self.assertEquals(response.http_response.status, 200, 'put bucket acl')
147
148 response = self.conn.get_bucket_acl(BUCKET_NAME)
149 self.assertEquals(response.http_response.status, 200, 'get bucket logging')
150
151 bucket_logging = response.object.data
152
153 response = self.conn.put_bucket_acl(BUCKET_NAME, bucket_logging)
154 self.assertEquals(response.http_response.status, 200, 'put bucket logging')
155
156 response = self.conn.list_bucket(BUCKET_NAME)
157 self.assertEquals(response.http_response.status, 200, 'list bucket')
158 entries = response.entries
159 self.assertEquals(len(entries), 2, 'got back right number of keys')
160 # depends on weird_key < key
161 self.assertEquals(entries[0].key, weird_key, 'first key is right')
162 self.assertEquals(entries[1].key, key, 'second key is right')
163
164 response = self.conn.list_bucket(BUCKET_NAME, {'max-keys': 1})
165 self.assertEquals(response.http_response.status, 200, 'list bucket with args')
166 self.assertEquals(len(response.entries), 1, 'got back right number of keys')
167
168 for entry in entries:
169 response = self.conn.delete(BUCKET_NAME, entry.key)
170 self.assertEquals(response.http_response.status, 204, 'delete %s' % entry.key)
171
172 response = self.conn.list_all_my_buckets()
173 self.assertEquals(response.http_response.status, 200, 'list all my buckets')
174 buckets = response.entries
175
176 response = self.conn.delete_bucket(BUCKET_NAME)
177 self.assertEquals(response.http_response.status, 204, 'delete bucket')
178
179 response = self.conn.list_all_my_buckets()
180 self.assertEquals(response.http_response.status, 200, 'list all my buckets again')
181
182 self.assertEquals(len(response.entries), len(buckets) - 1, 'bucket count is correct')
183
184 def verify_list_bucket_response(self, response, bucket, is_truncated, parameters, next_marker=''):
185 prefix = ''
186 marker = ''
187
188 if parameters.has_key('prefix'):
189 prefix = parameters['prefix']
190 if parameters.has_key('marker'):
191 marker = parameters['marker']
192
193 self.assertEquals(bucket, response.name, 'bucket name should match')
194 self.assertEquals(prefix, response.prefix, 'prefix should match')
195 self.assertEquals(marker, response.marker, 'marker should match')
196 if parameters.has_key('max-keys'):
197 self.assertEquals(parameters['max-keys'], response.max_keys, 'max-keys should match')
198 self.assertEquals(parameters['delimiter'], response.delimiter, 'delimiter should match')
199 self.assertEquals(is_truncated, response.is_truncated, 'is_truncated should match')
200 self.assertEquals(next_marker, response.next_marker, 'next_marker should match')
201
202 def do_delimited_list(self, bucket_name, is_truncated, parameters, regular_expected, common_expected, test_name, next_marker=''):
203 response = self.conn.list_bucket(bucket_name, parameters)
204 self.assertEquals(response.http_response.status, 200, test_name)
205 self.assertEquals(regular_expected, len(response.entries), 'right number of regular entries')
206 self.assertEquals(common_expected, len(response.common_prefixes), 'right number of common prefixes')
207
208 self.verify_list_bucket_response(response, bucket_name, is_truncated, parameters, next_marker)
209
210 return response
211
212class TestQueryStringAuthGenerator(unittest.TestCase):
213 def setUp(self):
214 self.generator = S3.QueryStringAuthGenerator(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
215 if (self.generator.is_secure == True):
216 self.connection = httplib.HTTPSConnection(self.generator.server_name)
217 else:
218 self.connection = httplib.HTTPConnection(self.generator.server_name)
219
220 def check_url(self, url, method, status, message, data=''):
221 if (method == 'PUT'):
222 headers = { 'Content-Length': len(data) }
223 self.connection.request(method, url, data, headers)
224 else:
225 self.connection.request(method, url)
226
227 response = self.connection.getresponse()
228 self.assertEquals(response.status, status, message)
229
230 return response.read()
231
232 # test all operations for both regular and vanity domains
233 # regular: http://s3.amazonaws.com/bucket/key
234 # subdomain: http://bucket.s3.amazonaws.com/key
235 # testing pure vanity domains (http://<vanity domain>/key) is not covered here
236 # but is possible with some additional setup (set the server in @conn to your vanity domain)
237
238 def test_subdomain(self):
239 self.run_tests(S3.CallingFormat.SUBDOMAIN)
240
241 def test_path(self):
242 self.run_tests(S3.CallingFormat.PATH)
243
244 def run_tests(self, calling_format):
245 self.generator.calling_format = calling_format
246
247 key = 'test'
248
249 self.check_url(self.generator.create_bucket(BUCKET_NAME), 'PUT', 200, 'create_bucket')
250 self.check_url(self.generator.put(BUCKET_NAME, key, ''), 'PUT', 200, 'put object', 'test data')
251 self.check_url(self.generator.get(BUCKET_NAME, key), 'GET', 200, 'get object')
252 self.check_url(self.generator.list_bucket(BUCKET_NAME), 'GET', 200, 'list bucket')
253 self.check_url(self.generator.list_all_my_buckets(), 'GET', 200, 'list all my buckets')
254 acl = self.check_url(self.generator.get_acl(BUCKET_NAME, key), 'GET', 200, 'get acl')
255 self.check_url(self.generator.put_acl(BUCKET_NAME, key, acl), 'PUT', 200, 'put acl', acl)
256 bucket_acl = self.check_url(self.generator.get_bucket_acl(BUCKET_NAME), 'GET', 200, 'get bucket acl')
257 self.check_url(self.generator.put_bucket_acl(BUCKET_NAME, bucket_acl), 'PUT', 200, 'put bucket acl', bucket_acl)
258 bucket_logging = self.check_url(self.generator.get_bucket_logging(BUCKET_NAME), 'GET', 200, 'get bucket logging')
259 self.check_url(self.generator.put_bucket_logging(BUCKET_NAME, bucket_logging), 'PUT', 200, 'put bucket logging', bucket_logging)
260 self.check_url(self.generator.delete(BUCKET_NAME, key), 'DELETE', 204, 'delete object')
261 self.check_url(self.generator.delete_bucket(BUCKET_NAME), 'DELETE', 204, 'delete bucket')
262
263
264if __name__ == '__main__':
265 unittest.main()
266
267