Browse code

Added support for "Static Websites"

Original patch taken from:
https://github.com/NumberFour/s3cmd/commit/579ab33f222da6c112eab76bc32757de2bf09d98
(repo not clone of this tree)

Contributor: Jens Braeuer (aka NumberFour)

Jens Braeuer authored on 2011/04/15 00:01:40
Showing 4 changed files
... ...
@@ -76,6 +76,8 @@ 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 = None
79 81
 
80 82
 	## Creating a singleton
81 83
 	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
 
... ...
@@ -244,6 +244,57 @@ class S3(object):
244 244
 		response['bucket-location'] = getTextFromXml(response['data'], "LocationConstraint") or "any"
245 245
 		return response
246 246
 
247
+	def website_list(self, uri, bucket_location = None):
248
+		headers = SortedDict(ignore_case = True)
249
+		bucket = uri.bucket()
250
+		body = ""
251
+
252
+		request = self.create_request("BUCKET_LIST", bucket = bucket, extra="?website")
253
+		response = None
254
+		try:
255
+			response = self.send_request(request, body)
256
+		except S3Error, e:
257
+			if e.status == 404:
258
+				debug("Could not get ?website. Assuming none set.")
259
+			else:
260
+				raise
261
+		return response
262
+
263
+	def website_create(self, uri, bucket_location = None):
264
+		headers = SortedDict(ignore_case = True)
265
+		bucket = uri.bucket()
266
+		body = '<WebsiteConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">'
267
+		body += '  <IndexDocument>'
268
+		body += ('    <Suffix>%s</Suffix>' % self.config.website_index)
269
+		body += '  </IndexDocument>'
270
+		if self.config.website_error:
271
+			body += '  <ErrorDocument>'
272
+			body += ('    <Key>%s</Key>' % self.config.website_error)
273
+			body += '  </ErrorDocument>'
274
+		body += '</WebsiteConfiguration>'
275
+
276
+		request = self.create_request("BUCKET_CREATE", bucket = bucket, extra="?website")
277
+		debug("About to send request '%s' with body '%s'" % (request, body))
278
+		response = self.send_request(request, body)
279
+		debug("Received response '%s'" % (response))
280
+
281
+		return response
282
+
283
+	def website_delete(self, uri, bucket_location = None):
284
+		headers = SortedDict(ignore_case = True)
285
+		bucket = uri.bucket()
286
+		body = ""
287
+
288
+		request = self.create_request("BUCKET_DELETE", bucket = bucket, extra="?website")
289
+		debug("About to send request '%s' with body '%s'" % (request, body))
290
+		response = self.send_request(request, body)
291
+		debug("Received response '%s'" % (response))
292
+
293
+		if response['status'] != 204:
294
+			raise S3ResponseError("Expected status 204: %s" % response)
295
+
296
+		return response
297
+
247 298
 	def object_put(self, filename, uri, extra_headers = None, extra_label = ""):
248 299
 		# TODO TODO
249 300
 		# Make it consistent with stream-oriented object_get()
... ...
@@ -164,6 +164,59 @@ def cmd_bucket_create(args):
164 164
 			else:
165 165
 				raise
166 166
 
167
+def cmd_website_list(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_list(uri, cfg.bucket_location)
175
+			if response:
176
+				import xml.dom.minidom
177
+				xml = xml.dom.minidom.parseString(response['data'])
178
+				output(u"Bucket '%s': website configuration:\n%s" % (uri.uri(), xml.toprettyxml()))
179
+			else:
180
+				output(u"Bucket '%s': unable to receive website configuration. None set?" % (uri.uri()))
181
+		except S3Error, e:
182
+			if S3.codes.has_key(e.info["Code"]):
183
+				error(S3.codes[e.info["Code"]] % uri.bucket())
184
+				return
185
+			else:
186
+				raise
187
+
188
+def cmd_website_create(args):
189
+	s3 = S3(Config())
190
+	for arg in args:
191
+		uri = S3Uri(arg)
192
+		if not uri.type == "s3" or not uri.has_bucket() or uri.has_object():
193
+			raise ParameterError("Expecting S3 URI with just the bucket name set instead of '%s'" % arg)
194
+		try:
195
+			response = s3.website_create(uri, cfg.bucket_location)
196
+			output(u"Bucket '%s': website configuration created." % (uri.uri()))
197
+		except S3Error, e:
198
+			if S3.codes.has_key(e.info["Code"]):
199
+				error(S3.codes[e.info["Code"]] % uri.bucket())
200
+				return
201
+			else:
202
+				raise
203
+
204
+def cmd_website_delete(args):
205
+	s3 = S3(Config())
206
+	for arg in args:
207
+		uri = S3Uri(arg)
208
+		if not uri.type == "s3" or not uri.has_bucket() or uri.has_object():
209
+			raise ParameterError("Expecting S3 URI with just the bucket name set instead of '%s'" % arg)
210
+		try:
211
+			response = s3.website_delete(uri, cfg.bucket_location)
212
+			output(u"Bucket '%s': website configuration deleted." % (uri.uri()))
213
+		except S3Error, e:
214
+			if S3.codes.has_key(e.info["Code"]):
215
+				error(S3.codes[e.info["Code"]] % uri.bucket())
216
+				return
217
+			else:
218
+				raise
219
+
167 220
 def cmd_bucket_delete(args):
168 221
 	def _bucket_delete_one(uri):
169 222
 		try:
... ...
@@ -1323,6 +1376,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-list", "label":"List Websites", "param":"s3://BUCKET", "func":cmd_website_list, "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 +1507,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)")