Browse code

Merge branch 'merge-website-feature'

Michal Ludvig authored on 2011/06/07 15:15:33
Showing 5 changed files
... ...
@@ -2,6 +2,8 @@ s3cmd 1.1.0   -   ???
2 2
 ===========
3 3
 * CloudFront invalidation via [sync --cf-invalidate] and [cfinvalinfo].
4 4
 * Increased socket_timeout from 10 secs to 5 mins.
5
+* Added "Static WebSite" support [ws-create / ws-delete / ws-info]
6
+  (contributed by Jens Braeuer)
5 7
 
6 8
 s3cmd 1.0.0   -   2011-01-18
7 9
 ===========
... ...
@@ -76,6 +76,9 @@ class Config(object):
76 76
 	follow_symlinks = False
77 77
 	socket_timeout = 300
78 78
 	invalidate_on_cf = False
79
+	website_index = "index.html"
80
+	website_error = ""
81
+	website_endpoint = "http://%(bucket)s.s3-website-%(location)s.amazonaws.com/"
79 82
 
80 83
 	## Creating a singleton
81 84
 	def __new__(self, configfile = None):
... ...
@@ -76,6 +76,9 @@ class S3DownloadError(S3Exception):
76 76
 class S3RequestError(S3Exception):
77 77
 	pass
78 78
 
79
+class S3ResponseError(S3Exception):
80
+	pass
81
+
79 82
 class InvalidFileError(S3Exception):
80 83
 	pass
81 84
 
... ...
@@ -238,10 +238,75 @@ class S3(object):
238 238
 		response = self.send_request(request)
239 239
 		return response
240 240
 
241
-	def bucket_info(self, uri):
241
+	def get_bucket_location(self, uri):
242 242
 		request = self.create_request("BUCKET_LIST", bucket = uri.bucket(), extra = "?location")
243 243
 		response = self.send_request(request)
244
-		response['bucket-location'] = getTextFromXml(response['data'], "LocationConstraint") or "any"
244
+		location = getTextFromXml(response['data'], "LocationConstraint")
245
+		if not location or location in [ "", "US" ]:
246
+			location = "us-east-1"
247
+		elif location == "EU":
248
+			location = "eu-west-1"
249
+		return location
250
+
251
+	def bucket_info(self, uri):
252
+		# For now reports only "Location". One day perhaps more.
253
+		response = {}
254
+		response['bucket-location'] = self.get_bucket_location(uri)
255
+		return response
256
+
257
+	def website_info(self, uri, bucket_location = None):
258
+		headers = SortedDict(ignore_case = True)
259
+		bucket = uri.bucket()
260
+		body = ""
261
+
262
+		request = self.create_request("BUCKET_LIST", bucket = bucket, extra="?website")
263
+		try:
264
+			response = self.send_request(request, body)
265
+			response['index_document'] = getTextFromXml(response['data'], ".//IndexDocument//Suffix")
266
+			response['error_document'] = getTextFromXml(response['data'], ".//ErrorDocument//Key")
267
+			response['website_endpoint'] = self.config.website_endpoint % {
268
+				"bucket" : uri.bucket(),
269
+				"location" : self.get_bucket_location(uri)}
270
+			return response
271
+		except S3Error, e:
272
+			if e.status == 404:
273
+				debug("Could not get /?website - website probably not configured for this bucket")
274
+				return None
275
+			raise
276
+
277
+	def website_create(self, uri, bucket_location = None):
278
+		headers = SortedDict(ignore_case = True)
279
+		bucket = uri.bucket()
280
+		body = '<WebsiteConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">'
281
+		body += '  <IndexDocument>'
282
+		body += ('    <Suffix>%s</Suffix>' % self.config.website_index)
283
+		body += '  </IndexDocument>'
284
+		if self.config.website_error:
285
+			body += '  <ErrorDocument>'
286
+			body += ('    <Key>%s</Key>' % self.config.website_error)
287
+			body += '  </ErrorDocument>'
288
+		body += '</WebsiteConfiguration>'
289
+
290
+		request = self.create_request("BUCKET_CREATE", bucket = bucket, extra="?website")
291
+		debug("About to send request '%s' with body '%s'" % (request, body))
292
+		response = self.send_request(request, body)
293
+		debug("Received response '%s'" % (response))
294
+
295
+		return response
296
+
297
+	def website_delete(self, uri, bucket_location = None):
298
+		headers = SortedDict(ignore_case = True)
299
+		bucket = uri.bucket()
300
+		body = ""
301
+
302
+		request = self.create_request("BUCKET_DELETE", bucket = bucket, extra="?website")
303
+		debug("About to send request '%s' with body '%s'" % (request, body))
304
+		response = self.send_request(request, body)
305
+		debug("Received response '%s'" % (response))
306
+
307
+		if response['status'] != 204:
308
+			raise S3ResponseError("Expected status 204: %s" % response)
309
+
245 310
 		return response
246 311
 
247 312
 	def object_put(self, filename, uri, extra_headers = None, extra_label = ""):
... ...
@@ -164,6 +164,60 @@ def cmd_bucket_create(args):
164 164
 			else:
165 165
 				raise
166 166
 
167
+def cmd_website_info(args):
168
+	s3 = S3(Config())
169
+	for arg in args:
170
+		uri = S3Uri(arg)
171
+		if not uri.type == "s3" or not uri.has_bucket() or uri.has_object():
172
+			raise ParameterError("Expecting S3 URI with just the bucket name set instead of '%s'" % arg)
173
+		try:
174
+			response = s3.website_info(uri, cfg.bucket_location)
175
+			if response:
176
+				output(u"Bucket %s: Website configuration" % uri.uri())
177
+				output(u"Website endpoint: %s" % response['website_endpoint'])
178
+				output(u"Index document:   %s" % response['index_document'])
179
+				output(u"Error document:   %s" % response['error_document'])
180
+			else:
181
+				output(u"Bucket %s: Unable to receive website configuration." % (uri.uri()))
182
+		except S3Error, e:
183
+			if S3.codes.has_key(e.info["Code"]):
184
+				error(S3.codes[e.info["Code"]] % uri.bucket())
185
+				return
186
+			else:
187
+				raise
188
+
189
+def cmd_website_create(args):
190
+	s3 = S3(Config())
191
+	for arg in args:
192
+		uri = S3Uri(arg)
193
+		if not uri.type == "s3" or not uri.has_bucket() or uri.has_object():
194
+			raise ParameterError("Expecting S3 URI with just the bucket name set instead of '%s'" % arg)
195
+		try:
196
+			response = s3.website_create(uri, cfg.bucket_location)
197
+			output(u"Bucket '%s': website configuration created." % (uri.uri()))
198
+		except S3Error, e:
199
+			if S3.codes.has_key(e.info["Code"]):
200
+				error(S3.codes[e.info["Code"]] % uri.bucket())
201
+				return
202
+			else:
203
+				raise
204
+
205
+def cmd_website_delete(args):
206
+	s3 = S3(Config())
207
+	for arg in args:
208
+		uri = S3Uri(arg)
209
+		if not uri.type == "s3" or not uri.has_bucket() or uri.has_object():
210
+			raise ParameterError("Expecting S3 URI with just the bucket name set instead of '%s'" % arg)
211
+		try:
212
+			response = s3.website_delete(uri, cfg.bucket_location)
213
+			output(u"Bucket '%s': website configuration deleted." % (uri.uri()))
214
+		except S3Error, e:
215
+			if S3.codes.has_key(e.info["Code"]):
216
+				error(S3.codes[e.info["Code"]] % uri.bucket())
217
+				return
218
+			else:
219
+				raise
220
+
167 221
 def cmd_bucket_delete(args):
168 222
 	def _bucket_delete_one(uri):
169 223
 		try:
... ...
@@ -1323,6 +1377,11 @@ def get_commands_list():
1323 1323
 	{"cmd":"sign", "label":"Sign arbitrary string using the secret key", "param":"STRING-TO-SIGN", "func":cmd_sign, "argc":1},
1324 1324
 	{"cmd":"fixbucket", "label":"Fix invalid file names in a bucket", "param":"s3://BUCKET[/PREFIX]", "func":cmd_fixbucket, "argc":1},
1325 1325
 
1326
+	## Website commands
1327
+	{"cmd":"ws-create", "label":"Create Website from bucket", "param":"s3://BUCKET", "func":cmd_website_create, "argc":1},
1328
+	{"cmd":"ws-delete", "label":"Delete Website", "param":"s3://BUCKET", "func":cmd_website_delete, "argc":1},
1329
+	{"cmd":"ws-info", "label":"Info about Website", "param":"s3://BUCKET", "func":cmd_website_info, "argc":1},
1330
+
1326 1331
 	## CloudFront commands
1327 1332
 	{"cmd":"cflist", "label":"List CloudFront distribution points", "param":"", "func":CfCmd.info, "argc":0},
1328 1333
 	{"cmd":"cfinfo", "label":"Display CloudFront distribution point parameters", "param":"[cf://DIST_ID]", "func":CfCmd.info, "argc":0},
... ...
@@ -1449,6 +1508,9 @@ def main():
1449 1449
 	optparser.add_option(      "--list-md5", dest="list_md5", action="store_true", help="Include MD5 sums in bucket listings (only for 'ls' command).")
1450 1450
 	optparser.add_option("-H", "--human-readable-sizes", dest="human_readable_sizes", action="store_true", help="Print sizes in human readable form (eg 1kB instead of 1234).")
1451 1451
 
1452
+	optparser.add_option(      "--ws-index", dest="website_index", action="store", help="Name of error-document (only for [ws-create] command)")
1453
+	optparser.add_option(      "--ws-error", dest="website_error", action="store", help="Name of index-document (only for [ws-create] command)")
1454
+
1452 1455
 	optparser.add_option(      "--progress", dest="progress_meter", action="store_true", help="Display progress meter (default on TTY).")
1453 1456
 	optparser.add_option(      "--no-progress", dest="progress_meter", action="store_false", help="Don't display progress meter (default on non-TTY).")
1454 1457
 	optparser.add_option(      "--enable", dest="enable", action="store_true", help="Enable given CloudFront distribution (only for [cfmodify] command)")