Browse code

Added support for sending and receiving files.

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

Michal Ludvig authored on 2007/01/11 11:55:01
Showing 2 changed files
... ...
@@ -2,7 +2,7 @@
2 2
 
3 3
 import httplib2
4 4
 import sys
5
-import os
5
+import os, os.path
6 6
 import logging
7 7
 import time
8 8
 import base64
... ...
@@ -12,6 +12,7 @@ import httplib
12 12
 
13 13
 from optparse import OptionParser
14 14
 from logging import debug, info, warn, error
15
+from stat import ST_SIZE
15 16
 import elementtree.ElementTree as ET
16 17
 
17 18
 ## Our modules
... ...
@@ -25,6 +26,10 @@ class AwsConfig:
25 25
 	secret_key = ""
26 26
 	host = "s3.amazonaws.com"
27 27
 	verbosity = logging.WARNING
28
+	send_chunk = 4096
29
+	recv_chunk = 4096
30
+	human_readable_sizes = False
31
+	force = False
28 32
 
29 33
 	def __init__(self, configfile = None):
30 34
 		if configfile:
... ...
@@ -38,13 +43,11 @@ class AwsConfig:
38 38
 		verbosity = cp.get("verbosity", "WARNING")
39 39
 		try:
40 40
 			AwsConfig.verbosity = logging._levelNames[verbosity]
41
-			print "verbosity set to "+verbosity
42 41
 		except KeyError:
43 42
 			error("AwsConfig: verbosity level '%s' is not valid" % verbosity)
44 43
 
45 44
 class S3Error (Exception):
46 45
 	def __init__(self, response):
47
-		#Exception.__init__(self)
48 46
 		self.status = response["status"]
49 47
 		self.reason = response["reason"]
50 48
 		tree = ET.fromstring(response["data"])
... ...
@@ -56,6 +59,9 @@ class S3Error (Exception):
56 56
 	def __str__(self):
57 57
 		return "%d (%s): %s" % (self.status, self.reason, self.Code)
58 58
 
59
+class ParameterError(Exception):
60
+	pass
61
+
59 62
 class S3:
60 63
 	http_methods = BidirMap(
61 64
 		GET = 0x01,
... ...
@@ -84,6 +90,12 @@ class S3:
84 84
 		OBJECT_DELETE = targets["OBJECT"] | http_methods["DELETE"],
85 85
 	)
86 86
 
87
+	codes = {
88
+		"NoSuchBucket" : "Bucket '%s' does not exist",
89
+		"AccessDenied" : "Access to bucket '%s' was denied",
90
+		"BucketAlreadyExists" : "Bucket '%s' already exists",
91
+		}
92
+
87 93
 	def __init__(self, config):
88 94
 		self.config = config
89 95
 
... ...
@@ -99,6 +111,41 @@ class S3:
99 99
 		response["list"] = getListFromXml(response["data"], "Contents")
100 100
 		return response
101 101
 
102
+	def bucket_create(self, bucket):
103
+		self.check_bucket_name(bucket)
104
+		request = self.create_request("BUCKET_CREATE", bucket = bucket)
105
+		response = self.send_request(request)
106
+		return response
107
+
108
+	def bucket_delete(self, bucket):
109
+		request = self.create_request("BUCKET_DELETE", bucket = bucket)
110
+		response = self.send_request(request)
111
+		return response
112
+
113
+	def object_put(self, filename, bucket, object):
114
+		if not os.path.isfile(filename):
115
+			raise ParameterProblem("%s is not a regular file" % filename)
116
+		try:
117
+			file = open(filename, "r")
118
+			size = os.stat(filename)[ST_SIZE]
119
+		except IOError, e:
120
+			raise ParameterProblem("%s: %s" % (filename, e.strerror))
121
+		headers = SortedDict()
122
+		headers["content-length"] = size
123
+		request = self.create_request("OBJECT_PUT", bucket = bucket, object = object, headers = headers)
124
+		response = self.send_file(request, file)
125
+		response["size"] = size
126
+		return response
127
+
128
+	def object_get(self, filename, bucket, object):
129
+		try:
130
+			file = open(filename, "w")
131
+		except IOError, e:
132
+			raise ParameterProblem("%s: %s" % (filename, e.strerror))
133
+		request = self.create_request("OBJECT_GET", bucket = bucket, object = object)
134
+		response = self.recv_file(request, file)
135
+		return response
136
+
102 137
 	def create_request(self, operation, bucket = None, object = None, headers = None):
103 138
 		resource = "/"
104 139
 		if bucket:
... ...
@@ -133,21 +180,94 @@ class S3:
133 133
 		response["reason"] = http_response.reason
134 134
 		response["data"] =  http_response.read()
135 135
 		conn.close()
136
-		if response["status"] != 200:
136
+		if response["status"] < 200 or response["status"] > 299:
137
+			raise S3Error(response)
138
+		return response
139
+
140
+	def send_file(self, request, file):
141
+		method_string, resource, headers = request
142
+		info("Sending file '%s', please wait..." % file.name)
143
+		conn = httplib.HTTPConnection(self.config.host)
144
+		conn.connect()
145
+		conn.putrequest(method_string, resource)
146
+		for header in headers.keys():
147
+			conn.putheader(header, str(headers[header]))
148
+		conn.endheaders()
149
+		size_left = size_total = headers.get("content-length")
150
+		while (size_left > 0):
151
+			debug("SendFile: Reading up to %d bytes from '%s'" % (AwsConfig.send_chunk, file.name))
152
+			data = file.read(AwsConfig.send_chunk)
153
+			debug("SendFile: Sending %d bytes to the server" % len(data))
154
+			conn.send(data)
155
+			size_left -= len(data)
156
+			info("Sent %d bytes (%d %%)" % (
157
+				(size_total - size_left),
158
+				(size_total - size_left) * 100 / size_total))
159
+		response = {}
160
+		http_response = conn.getresponse()
161
+		response["status"] = http_response.status
162
+		response["reason"] = http_response.reason
163
+		response["data"] =  http_response.read()
164
+		conn.close()
165
+		if response["status"] < 200 or response["status"] > 299:
166
+			raise S3Error(response)
167
+		return response
168
+
169
+	def recv_file(self, request, file):
170
+		method_string, resource, headers = request
171
+		info("Receiving file '%s', please wait..." % file.name)
172
+		conn = httplib.HTTPConnection(self.config.host)
173
+		conn.connect()
174
+		conn.putrequest(method_string, resource)
175
+		for header in headers.keys():
176
+			conn.putheader(header, str(headers[header]))
177
+		conn.endheaders()
178
+		response = {}
179
+		http_response = conn.getresponse()
180
+		response["status"] = http_response.status
181
+		response["reason"] = http_response.reason
182
+		response["headers"] = convertTupleListToDict(http_response.getheaders())
183
+		size_left = size_total = int(response["headers"]["content-length"])
184
+		info("Size appears to be %d bytes" % size_total)
185
+		while (size_left > 0):
186
+			this_chunk = size_left > AwsConfig.recv_chunk and AwsConfig.recv_chunk or size_left
187
+			debug("ReceiveFile: Receiving up to %d bytes from the server" % this_chunk)
188
+			data = http_response.read(this_chunk)
189
+			debug("ReceiveFile: Writing %d bytes to file '%s'" % (len(data), file.name))
190
+			file.write(data)
191
+			size_left -= len(data)
192
+			info("Received %d bytes (%d %%)" % (
193
+				(size_total - size_left),
194
+				(size_total - size_left) * 100 / size_total))
195
+		conn.close()
196
+		if response["status"] < 200 or response["status"] > 299:
137 197
 			raise S3Error(response)
138 198
 		return response
139 199
 
140 200
 	def sign_headers(self, method, resource, headers):
141 201
 		h  = method+"\n"
142
-		h += headers.pop("content-md5", "")+"\n"
143
-		h += headers.pop("content-type", "")+"\n"
144
-		h += headers.pop("date", "")+"\n"
202
+		h += headers.get("content-md5", "")+"\n"
203
+		h += headers.get("content-type", "")+"\n"
204
+		h += headers.get("date", "")+"\n"
145 205
 		for header in headers.keys():
146 206
 			if header.startswith("x-amz-"):
147 207
 				h += header+":"+str(headers[header])+"\n"
148 208
 		h += resource
149 209
 		return base64.encodestring(hmac.new(self.config.secret_key, h, hashlib.sha1).digest()).strip()
150 210
 
211
+	def check_bucket_name(self, bucket):
212
+		if re.compile("[^A-Za-z0-9\._-]").search(bucket):
213
+			raise ParameterError("Bucket name '%s' contains unallowed characters" % bucket)
214
+		if len(bucket) < 3:
215
+			raise ParameterError("Bucket name '%s' is too short (min 3 characters)" % bucket)
216
+		if len(bucket) > 255:
217
+			raise ParameterError("Bucket name '%s' is too long (max 255 characters)" % bucket)
218
+		return True
219
+
220
+
221
+def output(message):
222
+	print message
223
+
151 224
 def cmd_buckets_list_all(args):
152 225
 	s3 = S3(AwsConfig())
153 226
 	response = s3.list_all_buckets()
... ...
@@ -157,10 +277,10 @@ def cmd_buckets_list_all(args):
157 157
 		if len(bucket["Name"]) > maxlen:
158 158
 			maxlen = len(bucket["Name"])
159 159
 	for bucket in response["list"]:
160
-		print "%s  %s" % (
160
+		output("%s  %s" % (
161 161
 			formatDateTime(bucket["CreationDate"]),
162 162
 			bucket["Name"].ljust(maxlen),
163
-			)
163
+			))
164 164
 
165 165
 def cmd_buckets_list_all_all(args):
166 166
 	s3 = S3(AwsConfig())
... ...
@@ -168,22 +288,18 @@ def cmd_buckets_list_all_all(args):
168 168
 
169 169
 	for bucket in response["list"]:
170 170
 		cmd_bucket_list([bucket["Name"]])
171
-		print
171
+		output("")
172 172
 
173 173
 
174 174
 def cmd_bucket_list(args):
175 175
 	bucket = args[0]
176
-	print "Bucket '%s':" % bucket
176
+	output("Bucket '%s':" % bucket)
177 177
 	s3 = S3(AwsConfig())
178 178
 	try:
179 179
 		response = s3.bucket_list(bucket)
180 180
 	except S3Error, e:
181
-		codes = {
182
-			"NoSuchBucket" : "Bucket '%s' does not exist",
183
-			"AccessDenied" : "Access to bucket '%s' was denied",
184
-			}
185
-		if codes.has_key(e.Code):
186
-			error(codes[e.Code] % bucket)
181
+		if S3.codes.has_key(e.Code):
182
+			error(S3.codes[e.Code] % bucket)
187 183
 			return
188 184
 		else:
189 185
 			raise
... ...
@@ -192,20 +308,71 @@ def cmd_bucket_list(args):
192 192
 		if len(object["Key"]) > maxlen:
193 193
 			maxlen = len(object["Key"])
194 194
 	for object in response["list"]:
195
-		size, size_coeff = formatSize(object["Size"], True)
196
-		print "%s  %s%s  %s" % (
195
+		size, size_coeff = formatSize(object["Size"], AwsConfig.human_readable_sizes)
196
+		output("%s  %s%s  %s" % (
197 197
 			formatDateTime(object["LastModified"]),
198 198
 			str(size).rjust(4), size_coeff.ljust(1),
199 199
 			object["Key"].ljust(maxlen),
200
-			)
200
+			))
201
+
202
+def cmd_bucket_create(args):
203
+	bucket = args[0]
204
+	s3 = S3(AwsConfig())
205
+	try:
206
+		response = s3.bucket_create(bucket)
207
+	except S3Error, e:
208
+		if S3.codes.has_key(e.Code):
209
+			error(S3.codes[e.Code] % bucket)
210
+			return
211
+		else:
212
+			raise
213
+	output("Bucket '%s' created" % bucket)
201 214
 
215
+def cmd_bucket_delete(args):
216
+	bucket = args[0]
217
+	s3 = S3(AwsConfig())
218
+	try:
219
+		response = s3.bucket_delete(bucket)
220
+	except S3Error, e:
221
+		if S3.codes.has_key(e.Code):
222
+			error(S3.codes[e.Code] % bucket)
223
+			return
224
+		else:
225
+			raise
226
+	output("Bucket '%s' removed" % bucket)
227
+
228
+def cmd_object_put(args):
229
+	bucket = args.pop()
230
+	files = args[:]
231
+	s3 = S3(AwsConfig())
232
+	for file in files:
233
+		object = file
234
+		response = s3.object_put(file, bucket, file)
235
+		output("File '%s' stored as s3://%s/%s (%s bytes)" %
236
+			(file, bucket, object, response["size"]))
237
+
238
+def cmd_object_get(args):
239
+	bucket = args.pop(0)
240
+	object = args.pop(0)
241
+	destination = args.pop(0)
242
+	if os.path.isdir(destination):
243
+		destination.append("/" + object)
244
+	if not AwsConfig.force and os.path.exists(destination):
245
+		raise ParameterError("File %s already exists. Use --force to overwrite it" % destination)
246
+	s3 = S3(AwsConfig())
247
+	s3.object_get(destination, bucket, object)
202 248
 
203 249
 commands = {
204
-	"la" : ("List all buckets", cmd_buckets_list_all, 0),
205
-	"laa" : ("List all object in all buckets", cmd_buckets_list_all_all, 0),
206
-	"lb" : ("List objects in bucket", cmd_bucket_list, 1),
207
-#	"cb" : ("Create bucket", cmd_bucket_create, 1),
208
-#	"rb" : ("Remove bucket", cmd_bucket_remove, 1)
250
+	"lb" : ("List all buckets", cmd_buckets_list_all, 0),
251
+	"cb" : ("Create bucket", cmd_bucket_create, 1),
252
+	"mb" : ("Create bucket", cmd_bucket_create, 1),
253
+	"rb" : ("Remove bucket", cmd_bucket_delete, 1),
254
+	"db" : ("Remove bucket", cmd_bucket_delete, 1),
255
+	"ls" : ("List objects in bucket", cmd_bucket_list, 1),
256
+	"la" : ("List all object in all buckets", cmd_buckets_list_all_all, 0),
257
+	"put": ("Put file(s) into a bucket", cmd_object_put, 2),
258
+	"get": ("Get file(s) from a bucket", cmd_object_get, 1),
259
+#	"del": ("Delete file(s) from a bucket", cmd_object_del, 1),
209 260
 	}
210 261
 
211 262
 if __name__ == '__main__':
... ...
@@ -220,6 +387,10 @@ if __name__ == '__main__':
220 220
 	optparser.set_defaults(verbosity = default_verbosity)
221 221
 	optparser.add_option("-d", "--debug", dest="verbosity", action="store_const", const=logging.DEBUG, help="Enable debug output")
222 222
 	optparser.add_option("-v", "--verbose", dest="verbosity", action="store_const", const=logging.INFO, help="Enable verbose output")
223
+	optparser.set_defaults(human_readable = False)
224
+	optparser.add_option("-H", "--human-readable", dest="human_readable", action="store_true", help="Print sizes in human readable form")
225
+	optparser.set_defaults(force = False)
226
+	optparser.add_option("-f", "--force", dest="force", action="store_true", help="Force overwrite and other dangerous operations")
223 227
 	(options, args) = optparser.parse_args()
224 228
 
225 229
 	## Some mucking with logging levels to enable 
... ...
@@ -235,6 +406,10 @@ if __name__ == '__main__':
235 235
 		AwsConfig.verbosity = options.verbosity
236 236
 	logging.root.setLevel(AwsConfig.verbosity)
237 237
 
238
+	## Update AwsConfig with other parameters
239
+	AwsConfig.human_readable_sizes = options.human_readable
240
+	AwsConfig.force = options.force
241
+
238 242
 	if len(args) < 1:
239 243
 		error("Missing command. Please run with --help for more information.")
240 244
 		exit(1)
... ...
@@ -258,4 +433,7 @@ if __name__ == '__main__':
258 258
 		cmd_func(args)
259 259
 	except S3Error, e:
260 260
 		error("S3 error: " + str(e))
261
+	except ParameterError, e:
262
+		error("Parameter problem: " + str(e))
263
+
261 264
 
... ...
@@ -48,7 +48,7 @@ def formatSize(size, human_readable = False):
48 48
 		coeffs = ['k', 'M', 'G', 'T']
49 49
 		coeff = ""
50 50
 		while size > 2048:
51
-			size /= 2048
51
+			size /= 1024
52 52
 			coeff = coeffs.pop(0)
53 53
 		return (size, coeff)
54 54
 	else:
... ...
@@ -56,3 +56,10 @@ def formatSize(size, human_readable = False):
56 56
 
57 57
 def formatDateTime(s3timestamp):
58 58
 	return time.strftime("%Y-%m-%d %H:%M", dateS3toPython(s3timestamp))
59
+
60
+def convertTupleListToDict(list):
61
+	retval = {}
62
+	for tuple in list:
63
+		retval[tuple[0]] = tuple[1]
64
+	return retval
65
+