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... | ... |
@@ -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 |
|
|
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 |
+ |