Browse code

* S3/S3.py: Support for buckets stored in Europe, access now goes via <bucket>.s3.amazonaws.com where possible.

git-svn-id: https://s3tools.svn.sourceforge.net/svnroot/s3tools/s3cmd/trunk@155 830e0280-6d2a-0410-9c65-932aecc39d9d

Michal Ludvig authored on 2007/11/13 13:58:53
Showing 6 changed files
... ...
@@ -1,3 +1,8 @@
1
+2007-11-13  Michal Ludvig  <michal@logix.cz>
2
+
3
+	* S3/S3.py: Support for buckets stored in Europe, access now 
4
+	  goes via <bucket>.s3.amazonaws.com where possible.
5
+
1 6
 2007-11-12  Michal Ludvig  <michal@logix.cz>
2 7
 
3 8
 	* s3cmd: Support for storing file attributes (like ownership, 
... ...
@@ -1,5 +1,7 @@
1 1
 s3cmd 0.9.5   -   ???
2 2
 ===========
3
+* Support for buckets created in Europe
4
+* Initial 'sync' support, for now local to s3 direction only
3 5
 * Much better handling of multiple args to put, get and del
4 6
 * Tries to use ElementTree from any available module
5 7
 * Support for buckets with over 1000 objects.
... ...
@@ -13,7 +13,8 @@ class Config(object):
13 13
 	_doc = {}
14 14
 	access_key = ""
15 15
 	secret_key = ""
16
-	host = "s3.amazonaws.com"
16
+	host_base = "s3.amazonaws.com"
17
+	host_bucket = "%(bucket)s.s3.amazonaws.com"
17 18
 	verbosity = logging.WARNING
18 19
 	send_chunk = 4096
19 20
 	recv_chunk = 4096
... ...
@@ -42,6 +43,7 @@ class Config(object):
42 42
 	gpg_encrypt = "%(gpg_command)s -c --verbose --no-use-agent --batch --yes --passphrase-fd %(passphrase_fd)s -o %(output_file)s %(input_file)s"
43 43
 	gpg_decrypt = "%(gpg_command)s -d --verbose --no-use-agent --batch --yes --passphrase-fd %(passphrase_fd)s -o %(output_file)s %(input_file)s"
44 44
 	use_https = False
45
+	bucket_location = "US"
45 46
 
46 47
 	## Creating a singleton
47 48
 	def __new__(self, configfile = None):
... ...
@@ -39,7 +39,7 @@ class S3Error (Exception):
39 39
 		retval = "%d (%s)" % (self.status, self.reason)
40 40
 		try:
41 41
 			retval += (": %s" % self.info["Code"])
42
-		except AttributeError:
42
+		except (AttributeError, KeyError):
43 43
 			pass
44 44
 		return retval
45 45
 
... ...
@@ -83,19 +83,30 @@ class S3(object):
83 83
 	def __init__(self, config):
84 84
 		self.config = config
85 85
 
86
-	def get_connection(self):
87
-		if self.config.use_https:
88
-			return httplib.HTTPSConnection(self.config.host)
86
+	def get_connection(self, bucket):
89 87
 		if self.config.proxy_host != "":
90 88
 			return httplib.HTTPConnection(self.config.proxy_host, self.config.proxy_port)
91 89
 		else:
92
-			return httplib.HTTPConnection(self.config.host)
90
+			if self.config.use_https:
91
+				return httplib.HTTPSConnection(self.get_hostname(bucket))
92
+			else:
93
+				return httplib.HTTPConnection(self.get_hostname(bucket))
93 94
 
94
-	def format_resource(self, resource):
95
-		if self.config.proxy_host != "":
96
-			resource = "http://%s%s" % (self.config.host, resource)
95
+	def get_hostname(self, bucket):
96
+		if bucket:
97
+			host = self.config.host_bucket % { 'bucket' : bucket }
98
+		else:
99
+			host = self.config.host_base
100
+		debug('get_hostname(): ' + host)
101
+		return host
97 102
 
98
-		return resource
103
+	def format_uri(self, resource):
104
+		if self.config.proxy_host != "":
105
+			uri = "http://%s%s" % (self.get_hostname(resource['bucket']), resource['uri'])
106
+		else:
107
+			uri = resource['uri']
108
+		debug('format_uri(): ' + uri)
109
+		return uri
99 110
 
100 111
 	## Commands / Actions
101 112
 	def list_all_buckets(self):
... ...
@@ -126,12 +137,18 @@ class S3(object):
126 126
 		response['list'] = list
127 127
 		return response
128 128
 
129
-	def bucket_create(self, bucket):
129
+	def bucket_create(self, bucket, bucket_location = None):
130 130
 		self.check_bucket_name(bucket)
131 131
 		headers = SortedDict()
132
-		headers["content-length"] = 0
132
+		body = ""
133
+		if bucket_location and bucket_location.strip().upper() != "US":
134
+			body  = "<CreateBucketConfiguration><LocationConstraint>"
135
+			body += bucket_location.strip().upper()
136
+			body += "</LocationConstraint></CreateBucketConfiguration>"
137
+			debug("bucket_location: " + body)
138
+		headers["content-length"] = len(body)
133 139
 		request = self.create_request("BUCKET_CREATE", bucket = bucket, headers = headers)
134
-		response = self.send_request(request)
140
+		response = self.send_request(request, body)
135 141
 		return response
136 142
 
137 143
 	def bucket_delete(self, bucket):
... ...
@@ -140,9 +157,9 @@ class S3(object):
140 140
 		return response
141 141
 
142 142
 	def bucket_info(self, bucket):
143
-		request = self.create_request("BUCKET_LIST", bucket = bucket + "?location")
143
+		request = self.create_request("BUCKET_LIST", bucket = bucket, extra = "?location")
144 144
 		response = self.send_request(request)
145
-		response['bucket-location'] = getTextFromXml(response['data'], ".//LocationConstraint") or "any"
145
+		response['bucket-location'] = getTextFromXml(response['data'], "LocationConstraint") or "any"
146 146
 		return response
147 147
 
148 148
 	def object_put(self, filename, bucket, object, extra_headers = None):
... ...
@@ -237,12 +254,14 @@ class S3(object):
237 237
 		debug("String '%s' encoded to '%s'" % (string, encoded))
238 238
 		return encoded
239 239
 
240
-	def create_request(self, operation, bucket = None, object = None, headers = None, **params):
241
-		resource = "/"
240
+	def create_request(self, operation, bucket = None, object = None, headers = None, extra = None, **params):
241
+		resource = { 'bucket' : None, 'uri' : "/" }
242 242
 		if bucket:
243
-			resource += str(bucket)
243
+			resource['bucket'] = str(bucket)
244 244
 			if object:
245
-				resource += "/" + self.urlencode_string(object)
245
+				resource['uri'] = "/" + self.urlencode_string(object)
246
+		if extra:
247
+			resource['uri'] += extra
246 248
 
247 249
 		if not headers:
248 250
 			headers = SortedDict()
... ...
@@ -265,21 +284,22 @@ class S3(object):
265 265
 			else:
266 266
 				param_str += "&%s" % param
267 267
 		if param_str != "":
268
-			resource += "?" + param_str[1:]
269
-		debug("CreateRequest: resource=" + resource)
268
+			resource['uri'] += "?" + param_str[1:]
269
+		debug("CreateRequest: resource[uri]=" + resource['uri'])
270 270
 		return (method_string, resource, headers)
271 271
 	
272
-	def send_request(self, request):
272
+	def send_request(self, request, body = None):
273 273
 		method_string, resource, headers = request
274 274
 		info("Processing request, please wait...")
275
- 		conn = self.get_connection()
276
- 		conn.request(method_string, self.format_resource(resource), {}, headers)
275
+ 		conn = self.get_connection(resource['bucket'])
276
+ 		conn.request(method_string, self.format_uri(resource), body, headers)
277 277
 		response = {}
278 278
 		http_response = conn.getresponse()
279 279
 		response["status"] = http_response.status
280 280
 		response["reason"] = http_response.reason
281 281
 		response["headers"] = convertTupleListToDict(http_response.getheaders())
282 282
 		response["data"] =  http_response.read()
283
+		debug("Response: " + str(response))
283 284
 		conn.close()
284 285
 		if response["status"] < 200 or response["status"] > 299:
285 286
 			raise S3Error(response)
... ...
@@ -288,9 +308,9 @@ class S3(object):
288 288
 	def send_file(self, request, file):
289 289
 		method_string, resource, headers = request
290 290
 		info("Sending file '%s', please wait..." % file.name)
291
-		conn = self.get_connection()
291
+		conn = self.get_connection(resource['bucket'])
292 292
 		conn.connect()
293
-		conn.putrequest(method_string, self.format_resource(resource))
293
+		conn.putrequest(method_string, self.format_uri(resource))
294 294
 		for header in headers.keys():
295 295
 			conn.putheader(header, str(headers[header]))
296 296
 		conn.endheaders()
... ...
@@ -319,9 +339,9 @@ class S3(object):
319 319
 	def recv_file(self, request, stream):
320 320
 		method_string, resource, headers = request
321 321
 		info("Receiving file '%s', please wait..." % stream.name)
322
-		conn = self.get_connection()
322
+		conn = self.get_connection(resource['bucket'])
323 323
 		conn.connect()
324
-		conn.putrequest(method_string, self.format_resource(resource))
324
+		conn.putrequest(method_string, self.format_uri(resource))
325 325
 		for header in headers.keys():
326 326
 			conn.putheader(header, str(headers[header]))
327 327
 		conn.endheaders()
... ...
@@ -369,7 +389,9 @@ class S3(object):
369 369
 		for header in headers.keys():
370 370
 			if header.startswith("x-amz-"):
371 371
 				h += header+":"+str(headers[header])+"\n"
372
-		h += resource
372
+		if resource['bucket']:
373
+			h += "/" + resource['bucket']
374
+		h += resource['uri']
373 375
 		debug("SignHeaders: " + repr(h))
374 376
 		return base64.encodestring(hmac.new(self.config.secret_key, h, sha).digest()).strip()
375 377
 
... ...
@@ -59,7 +59,10 @@ def getListFromXml(xml, node):
59 59
 def getTextFromXml(xml, xpath):
60 60
 	tree = ET.fromstring(xml)
61 61
 	xmlns = getNameSpace(tree)
62
-	return tree.findtext(fixupXPath(xmlns, xpath))
62
+	if tree.tag.endswith(xpath):
63
+		return tree.text
64
+	else:
65
+		return tree.findtext(fixupXPath(xmlns, xpath))
63 66
 
64 67
 def dateS3toPython(date):
65 68
 	date = re.compile("\.\d\d\dZ").sub(".000Z", date)
... ...
@@ -133,10 +133,9 @@ def cmd_bucket_create(args):
133 133
 	uri = S3Uri(args[0])
134 134
 	if not uri.type == "s3" or not uri.has_bucket() or uri.has_object():
135 135
 		raise ParameterError("Expecting S3 URI with just the bucket name set instead of '%s'" % args[0])
136
-
137 136
 	try:
138 137
 		s3 = S3(Config())
139
-		response = s3.bucket_create(uri.bucket())
138
+		response = s3.bucket_create(uri.bucket(), cfg.bucket_location)
140 139
 	except S3Error, e:
141 140
 		if S3.codes.has_key(e.info["Code"]):
142 141
 			error(S3.codes[e.info["Code"]] % uri.bucket())
... ...
@@ -648,6 +647,7 @@ if __name__ == '__main__':
648 648
 	optparser.add_option(      "--no-delete-removed", dest="delete_removed", action="store_false", help="Don't delete remote objects.")
649 649
 	optparser.add_option("-p", "--preserve", dest="preserve_attrs", action="store_true", help="Preserve filesystem attributes (mode, ownership, timestamps). Default for [sync] command.")
650 650
 	optparser.add_option(      "--no-preserve", dest="preserve_attrs", action="store_false", help="Don't store FS attributes")
651
+	optparser.add_option(      "--bucket-location", dest="bucket_location", help="Datacentre to create bucket in. Either EU or US (default)")
651 652
 
652 653
 	optparser.add_option("-m", "--mime-type", dest="default_mime_type", type="mimetype", metavar="MIME/TYPE", help="Default MIME-type to be set for objects stored.")
653 654
 	optparser.add_option("-M", "--guess-mime-type", dest="guess_mime_type", action="store_true", help="Guess MIME-type of files by their extension. Falls back to default MIME-Type as specified by --mime-type option")