* S3/SortedDict.py: Add case-sensitive mode.
* s3cmd, S3/S3.py, S3/Config.py: Use SortedDict() in
case-sensitive mode to avoid dropping filenames
differing only in capitalisation
* run-tests.py: Testsuite for the above.
* NEWS: Updated.
git-svn-id: https://s3tools.svn.sourceforge.net/svnroot/s3tools/s3cmd/trunk@386 830e0280-6d2a-0410-9c65-932aecc39d9d
... | ... |
@@ -1,3 +1,12 @@ |
1 |
+2009-05-27 Michal Ludvig <michal@logix.cz> |
|
2 |
+ |
|
3 |
+ * S3/SortedDict.py: Add case-sensitive mode. |
|
4 |
+ * s3cmd, S3/S3.py, S3/Config.py: Use SortedDict() in |
|
5 |
+ case-sensitive mode to avoid dropping filenames |
|
6 |
+ differing only in capitalisation |
|
7 |
+ * run-tests.py: Testsuite for the above. |
|
8 |
+ * NEWS: Updated. |
|
9 |
+ |
|
1 | 10 |
2009-03-20 Michal Ludvig <michal@logix.cz> |
2 | 11 |
|
3 | 12 |
* S3/S3.py: Re-sign requests before retrial to avoid |
... | ... |
@@ -27,7 +27,7 @@ from ACL import ACL |
27 | 27 |
class S3Request(object): |
28 | 28 |
def __init__(self, s3, method_string, resource, headers, params = {}): |
29 | 29 |
self.s3 = s3 |
30 |
- self.headers = SortedDict(headers or {}) |
|
30 |
+ self.headers = SortedDict(headers or {}, ignore_case = True) |
|
31 | 31 |
self.resource = resource |
32 | 32 |
self.method_string = method_string |
33 | 33 |
self.params = params |
... | ... |
@@ -195,7 +195,7 @@ class S3(object): |
195 | 195 |
return response |
196 | 196 |
|
197 | 197 |
def bucket_create(self, bucket, bucket_location = None): |
198 |
- headers = SortedDict() |
|
198 |
+ headers = SortedDict(ignore_case = True) |
|
199 | 199 |
body = "" |
200 | 200 |
if bucket_location and bucket_location.strip().upper() != "US": |
201 | 201 |
body = "<CreateBucketConfiguration><LocationConstraint>" |
... | ... |
@@ -235,7 +235,7 @@ class S3(object): |
235 | 235 |
size = os.stat(filename)[ST_SIZE] |
236 | 236 |
except IOError, e: |
237 | 237 |
raise InvalidFileError(u"%s: %s" % (unicodise(filename), e.strerror)) |
238 |
- headers = SortedDict() |
|
238 |
+ headers = SortedDict(ignore_case = True) |
|
239 | 239 |
if extra_headers: |
240 | 240 |
headers.update(extra_headers) |
241 | 241 |
headers["content-length"] = size |
... | ... |
@@ -273,7 +273,7 @@ class S3(object): |
273 | 273 |
raise ValueError("Expected URI type 's3', got '%s'" % src_uri.type) |
274 | 274 |
if dst_uri.type != "s3": |
275 | 275 |
raise ValueError("Expected URI type 's3', got '%s'" % dst_uri.type) |
276 |
- headers = SortedDict() |
|
276 |
+ headers = SortedDict(ignore_case = True) |
|
277 | 277 |
headers['x-amz-copy-source'] = "/%s/%s" % (src_uri.bucket(), self.urlencode_string(src_uri.object())) |
278 | 278 |
if self.config.acl_public: |
279 | 279 |
headers["x-amz-acl"] = "public-read" |
... | ... |
@@ -17,11 +17,19 @@ class SortedDictIterator(object): |
17 | 17 |
raise StopIteration |
18 | 18 |
|
19 | 19 |
class SortedDict(dict): |
20 |
- keys_sort_lowercase = True |
|
20 |
+ def __init__(self, mapping = {}, ignore_case = True, **kwargs): |
|
21 |
+ """ |
|
22 |
+ WARNING: SortedDict() with ignore_case==True will |
|
23 |
+ drop entries differing only in capitalisation! |
|
24 |
+ Eg: SortedDict({'auckland':1, 'Auckland':2}).keys() => ['Auckland'] |
|
25 |
+ With ignore_case==False it's all right |
|
26 |
+ """ |
|
27 |
+ dict.__init__(self, mapping, **kwargs) |
|
28 |
+ self.ignore_case = ignore_case |
|
21 | 29 |
|
22 | 30 |
def keys(self): |
23 | 31 |
keys = dict.keys(self) |
24 |
- if self.keys_sort_lowercase: |
|
32 |
+ if self.ignore_case: |
|
25 | 33 |
# Translation map |
26 | 34 |
xlat_map = BidirMap() |
27 | 35 |
for key in keys: |
... | ... |
@@ -38,18 +46,16 @@ class SortedDict(dict): |
38 | 38 |
return SortedDictIterator(self, self.keys()) |
39 | 39 |
|
40 | 40 |
if __name__ == "__main__": |
41 |
- d = SortedDict() |
|
42 |
- d['AWS'] = 1 |
|
43 |
- d['Action'] = 2 |
|
44 |
- d['america'] = 3 |
|
45 |
- d.keys_sort_lowercase = True |
|
46 |
- print "Wanted: Action, america, AWS," |
|
41 |
+ d = { 'AWS' : 1, 'Action' : 2, 'america' : 3, 'Auckland' : 4, 'America' : 5 } |
|
42 |
+ sd = SortedDict(d) |
|
43 |
+ print "Wanted: Action, america, Auckland, AWS, [ignore case]" |
|
47 | 44 |
print "Got: ", |
48 |
- for key in d: |
|
45 |
+ for key in sd: |
|
49 | 46 |
print "%s," % key, |
50 |
- print " __iter__()" |
|
51 |
- d.keys_return_lowercase = True |
|
47 |
+ print " [used: __iter__()]" |
|
48 |
+ d = SortedDict(d, ignore_case = False) |
|
49 |
+ print "Wanted: AWS, Action, Auckland, america, [case sensitive]" |
|
52 | 50 |
print "Got: ", |
53 | 51 |
for key in d.keys(): |
54 | 52 |
print "%s," % key, |
55 |
- print " keys()" |
|
53 |
+ print " [used: keys()]" |
... | ... |
@@ -338,6 +338,9 @@ test_s3cmd("Get multiple files", ['get', 's3://s3cmd-autotest-1/xyz/etc2/Logo.PN |
338 | 338 |
test_s3cmd("Copy between buckets", ['cp', 's3://s3cmd-autotest-1/xyz/etc2/Logo.PNG', 's3://s3cmd-Autotest-3'], |
339 | 339 |
must_find = [ "File s3://s3cmd-autotest-1/xyz/etc2/Logo.PNG copied to s3://s3cmd-Autotest-3/xyz/etc2/Logo.PNG" ]) |
340 | 340 |
|
341 |
+## ====== Upload files differing in capitalisation |
|
342 |
+test_s3cmd("blah.txt / Blah.txt", ['put', '-r', 'testsuite/blahBlah', 's3://s3cmd-autotest-1/'], |
|
343 |
+ must_find = [ 's3://s3cmd-autotest-1/blahBlah/Blah.txt', 's3://s3cmd-autotest-1/blahBlah/blah.txt' ]) |
|
341 | 344 |
|
342 | 345 |
## ====== Simple delete |
343 | 346 |
test_s3cmd("Simple delete", ['del', 's3://s3cmd-autotest-1/xyz/etc2/Logo.PNG'], |
... | ... |
@@ -187,7 +187,7 @@ def cmd_bucket_delete(args): |
187 | 187 |
|
188 | 188 |
def fetch_local_list(args, recursive = None): |
189 | 189 |
local_uris = [] |
190 |
- local_list = SortedDict() |
|
190 |
+ local_list = SortedDict(ignore_case = False) |
|
191 | 191 |
single_file = False |
192 | 192 |
|
193 | 193 |
if type(args) not in (list, tuple): |
... | ... |
@@ -220,7 +220,7 @@ def fetch_local_list(args, recursive = None): |
220 | 220 |
|
221 | 221 |
def fetch_remote_list(args, require_attribs = False, recursive = None): |
222 | 222 |
remote_uris = [] |
223 |
- remote_list = SortedDict() |
|
223 |
+ remote_list = SortedDict(ignore_case = False) |
|
224 | 224 |
|
225 | 225 |
if type(args) not in (list, tuple): |
226 | 226 |
args = [args] |
... | ... |
@@ -586,7 +586,7 @@ def _get_filelist_local(local_uri): |
586 | 586 |
local_path = deunicodise(local_uri.dirname()) |
587 | 587 |
filelist = [( local_path, [], [deunicodise(local_uri.basename())] )] |
588 | 588 |
single_file = True |
589 |
- loc_list = SortedDict() |
|
589 |
+ loc_list = SortedDict(ignore_case = False) |
|
590 | 590 |
for root, dirs, files in filelist: |
591 | 591 |
rel_root = root.replace(local_path, local_base, 1) |
592 | 592 |
for f in files: |
... | ... |
@@ -637,7 +637,7 @@ def _get_filelist_remote(remote_uri, recursive = True): |
637 | 637 |
rem_base = rem_base[:rem_base.rfind('/')+1] |
638 | 638 |
remote_uri = S3Uri("s3://%s/%s" % (remote_uri.bucket(), rem_base)) |
639 | 639 |
rem_base_len = len(rem_base) |
640 |
- rem_list = SortedDict() |
|
640 |
+ rem_list = SortedDict(ignore_case = False) |
|
641 | 641 |
break_now = False |
642 | 642 |
for object in response['list']: |
643 | 643 |
if object['Key'] == rem_base_original and object['Key'][-1] != os.path.sep: |
... | ... |
@@ -664,7 +664,7 @@ def _get_filelist_remote(remote_uri, recursive = True): |
664 | 664 |
def _filelist_filter_exclude_include(src_list): |
665 | 665 |
info(u"Applying --exclude/--include") |
666 | 666 |
cfg = Config() |
667 |
- exclude_list = SortedDict() |
|
667 |
+ exclude_list = SortedDict(ignore_case = False) |
|
668 | 668 |
for file in src_list.keys(): |
669 | 669 |
debug(u"CHECK: %s" % file) |
670 | 670 |
excluded = False |
... | ... |
@@ -693,7 +693,7 @@ def _filelist_filter_exclude_include(src_list): |
693 | 693 |
def _compare_filelists(src_list, dst_list, src_is_local_and_dst_is_remote): |
694 | 694 |
info(u"Verifying attributes...") |
695 | 695 |
cfg = Config() |
696 |
- exists_list = SortedDict() |
|
696 |
+ exists_list = SortedDict(ignore_case = False) |
|
697 | 697 |
|
698 | 698 |
for file in src_list.keys(): |
699 | 699 |
debug(u"CHECK: %s" % file) |