... | ... |
@@ -179,6 +179,9 @@ Simple s3cmd HowTo |
179 | 179 |
account page. Be careful when copying them! They are |
180 | 180 |
case sensitive and must be entered accurately or you'll |
181 | 181 |
keep getting errors about invalid signatures or similar. |
182 |
+ |
|
183 |
+ Remember to add ListAllMyBuckets permissions to the keys |
|
184 |
+ or you will get an AccessDenied error while testing access. |
|
182 | 185 |
|
183 | 186 |
3) Run "s3cmd ls" to list all your buckets. |
184 | 187 |
As you just started using S3 there are no buckets owned by |
... | ... |
@@ -146,7 +146,6 @@ class ACL(object): |
146 | 146 |
if self.hasGrant(name, permission): |
147 | 147 |
return |
148 | 148 |
|
149 |
- name = name.lower() |
|
150 | 149 |
permission = permission.upper() |
151 | 150 |
|
152 | 151 |
if "ALL" == permission: |
... | ... |
@@ -159,12 +158,17 @@ class ACL(object): |
159 | 159 |
grantee.name = name |
160 | 160 |
grantee.permission = permission |
161 | 161 |
|
162 |
- if name.find('@') <= -1: # ultra lame attempt to differenciate emails id from canonical ids |
|
163 |
- grantee.xsi_type = "CanonicalUser" |
|
164 |
- grantee.tag = "ID" |
|
165 |
- else: |
|
162 |
+ if name.find('@') > -1: |
|
163 |
+ grantee.name = grantee.name.lower |
|
166 | 164 |
grantee.xsi_type = "AmazonCustomerByEmail" |
167 | 165 |
grantee.tag = "EmailAddress" |
166 |
+ elif name.find('http://acs.amazonaws.com/groups/') > -1: |
|
167 |
+ grantee.xsi_type = "Group" |
|
168 |
+ grantee.tag = "URI" |
|
169 |
+ else: |
|
170 |
+ grantee.name = grantee.name.lower |
|
171 |
+ grantee.xsi_type = "CanonicalUser" |
|
172 |
+ grantee.tag = "ID" |
|
168 | 173 |
|
169 | 174 |
self.appendGrantee(grantee) |
170 | 175 |
|
... | ... |
@@ -36,8 +36,11 @@ class Config(object): |
36 | 36 |
human_readable_sizes = False |
37 | 37 |
extra_headers = SortedDict(ignore_case = True) |
38 | 38 |
force = False |
39 |
+ server_side_encryption = False |
|
39 | 40 |
enable = None |
40 | 41 |
get_continue = False |
42 |
+ put_continue = False |
|
43 |
+ upload_id = None |
|
41 | 44 |
skip_existing = False |
42 | 45 |
recursive = False |
43 | 46 |
acl_public = None |
... | ... |
@@ -75,6 +78,7 @@ class Config(object): |
75 | 75 |
bucket_location = "US" |
76 | 76 |
default_mime_type = "binary/octet-stream" |
77 | 77 |
guess_mime_type = True |
78 |
+ use_mime_magic = True |
|
78 | 79 |
mime_type = "" |
79 | 80 |
enable_multipart = True |
80 | 81 |
multipart_chunk_size_mb = 15 # MB |
... | ... |
@@ -87,6 +91,7 @@ class Config(object): |
87 | 87 |
debug_exclude = {} |
88 | 88 |
debug_include = {} |
89 | 89 |
encoding = "utf-8" |
90 |
+ add_content_encoding = True |
|
90 | 91 |
urlencoding_mode = "normal" |
91 | 92 |
log_target_prefix = "" |
92 | 93 |
reduced_redundancy = False |
... | ... |
@@ -180,7 +185,7 @@ class Config(object): |
180 | 180 |
if "key" in data: |
181 | 181 |
Config().update_option(data["key"], data["value"]) |
182 | 182 |
if data["key"] in ("access_key", "secret_key", "gpg_passphrase"): |
183 |
- print_value = (data["value"][:2]+"...%d_chars..."+data["value"][-1:]) % (len(data["value"]) - 3) |
|
183 |
+ print_value = ("%s...%d_chars...%s") % (data["value"][:2], len(data["value"]) - 3, data["value"][-1:]) |
|
184 | 184 |
else: |
185 | 185 |
print_value = data["value"] |
186 | 186 |
debug("env_Config: %s->%s" % (data["key"], print_value)) |
... | ... |
@@ -274,7 +279,7 @@ class ConfigParser(object): |
274 | 274 |
data["value"] = data["value"][1:-1] |
275 | 275 |
self.__setitem__(data["key"], data["value"]) |
276 | 276 |
if data["key"] in ("access_key", "secret_key", "gpg_passphrase"): |
277 |
- print_value = (data["value"][:2]+"...%d_chars..."+data["value"][-1:]) % (len(data["value"]) - 3) |
|
277 |
+ print_value = ("%s...%d_chars...%s") % (data["value"][:2], len(data["value"]) - 3, data["value"][-1:]) |
|
278 | 278 |
else: |
279 | 279 |
print_value = data["value"] |
280 | 280 |
debug("ConfigParser: %s->%s" % (data["key"], print_value)) |
... | ... |
@@ -36,6 +36,7 @@ class FileDict(SortedDict): |
36 | 36 |
return md5 |
37 | 37 |
|
38 | 38 |
def record_hardlink(self, relative_file, dev, inode, md5): |
39 |
+ if dev == 0 or inode == 0: return # Windows |
|
39 | 40 |
if dev not in self.hardlinks: |
40 | 41 |
self.hardlinks[dev] = dict() |
41 | 42 |
if inode not in self.hardlinks[dev]: |
... | ... |
@@ -17,6 +17,7 @@ import os |
17 | 17 |
import sys |
18 | 18 |
import glob |
19 | 19 |
import copy |
20 |
+import re |
|
20 | 21 |
|
21 | 22 |
__all__ = ["fetch_local_list", "fetch_remote_list", "compare_filelists", "filter_exclude_include"] |
22 | 23 |
|
... | ... |
@@ -250,7 +251,11 @@ def fetch_local_list(args, is_src = False, recursive = None): |
250 | 250 |
return loc_list, single_file |
251 | 251 |
|
252 | 252 |
def _maintain_cache(cache, local_list): |
253 |
- if cfg.cache_file: |
|
253 |
+ # if getting the file list from files_from, it is going to be |
|
254 |
+ # a subset of the actual tree. We should not purge content |
|
255 |
+ # outside of that subset as we don't know if it's valid or |
|
256 |
+ # not. Leave it to a non-files_from run to purge. |
|
257 |
+ if cfg.cache_file and len(cfg.files_from) == 0: |
|
254 | 258 |
cache.mark_all_for_purge() |
255 | 259 |
for i in local_list.keys(): |
256 | 260 |
cache.unmark_for_purge(local_list[i]['dev'], local_list[i]['inode'], local_list[i]['mtime'], local_list[i]['size']) |
... | ... |
@@ -363,7 +368,7 @@ def fetch_remote_list(args, require_attribs = False, recursive = None): |
363 | 363 |
'dev' : None, |
364 | 364 |
'inode' : None, |
365 | 365 |
} |
366 |
- if rem_list[key]['md5'].find("-"): # always get it for multipart uploads |
|
366 |
+ if rem_list[key]['md5'].find("-") > 0: # always get it for multipart uploads |
|
367 | 367 |
_get_remote_attribs(S3Uri(object_uri_str), rem_list[key]) |
368 | 368 |
md5 = rem_list[key]['md5'] |
369 | 369 |
rem_list.record_md5(key, md5) |
... | ... |
@@ -398,16 +403,12 @@ def fetch_remote_list(args, require_attribs = False, recursive = None): |
398 | 398 |
uri_str = str(uri) |
399 | 399 |
## Wildcards used in remote URI? |
400 | 400 |
## If yes we'll need a bucket listing... |
401 |
- if uri_str.find('*') > -1 or uri_str.find('?') > -1: |
|
402 |
- first_wildcard = uri_str.find('*') |
|
403 |
- first_questionmark = uri_str.find('?') |
|
404 |
- if first_questionmark > -1 and first_questionmark < first_wildcard: |
|
405 |
- first_wildcard = first_questionmark |
|
406 |
- prefix = uri_str[:first_wildcard] |
|
407 |
- rest = uri_str[first_wildcard+1:] |
|
401 |
+ wildcard_split_result = re.split("\*|\?", uri_str, maxsplit=1) |
|
402 |
+ if len(wildcard_split_result) == 2: # wildcards found |
|
403 |
+ prefix, rest = wildcard_split_result |
|
408 | 404 |
## Only request recursive listing if the 'rest' of the URI, |
409 | 405 |
## i.e. the part after first wildcard, contains '/' |
410 |
- need_recursion = rest.find('/') > -1 |
|
406 |
+ need_recursion = '/' in rest |
|
411 | 407 |
objectlist = _get_filelist_remote(S3Uri(prefix), recursive = need_recursion) |
412 | 408 |
for key in objectlist: |
413 | 409 |
## Check whether the 'key' matches the requested wildcards |
... | ... |
@@ -5,6 +5,7 @@ class HashCache(object): |
5 | 5 |
self.inodes = dict() |
6 | 6 |
|
7 | 7 |
def add(self, dev, inode, mtime, size, md5): |
8 |
+ if dev == 0 or inode == 0: return # Windows |
|
8 | 9 |
if dev not in self.inodes: |
9 | 10 |
self.inodes[dev] = dict() |
10 | 11 |
if inode not in self.inodes[dev]: |
... | ... |
@@ -27,7 +28,10 @@ class HashCache(object): |
27 | 27 |
self.inodes[d][i][c]['purge'] = True |
28 | 28 |
|
29 | 29 |
def unmark_for_purge(self, dev, inode, mtime, size): |
30 |
- d = self.inodes[dev][inode][mtime] |
|
30 |
+ try: |
|
31 |
+ d = self.inodes[dev][inode][mtime] |
|
32 |
+ except KeyError: |
|
33 |
+ return |
|
31 | 34 |
if d['size'] == size and 'purge' in d: |
32 | 35 |
del self.inodes[dev][inode][mtime]['purge'] |
33 | 36 |
|
... | ... |
@@ -3,10 +3,12 @@ |
3 | 3 |
## License: GPL Version 2 |
4 | 4 |
|
5 | 5 |
import os |
6 |
+import sys |
|
6 | 7 |
from stat import ST_SIZE |
7 | 8 |
from logging import debug, info, warning, error |
8 |
-from Utils import getTextFromXml, formatSize, unicodise |
|
9 |
+from Utils import getTextFromXml, getTreeFromXml, formatSize, unicodise, calculateChecksum, parseNodes |
|
9 | 10 |
from Exceptions import S3UploadError |
11 |
+from collections import defaultdict |
|
10 | 12 |
|
11 | 13 |
class MultiPartUpload(object): |
12 | 14 |
|
... | ... |
@@ -22,15 +24,55 @@ class MultiPartUpload(object): |
22 | 22 |
self.headers_baseline = headers_baseline |
23 | 23 |
self.upload_id = self.initiate_multipart_upload() |
24 | 24 |
|
25 |
+ def get_parts_information(self, uri, upload_id): |
|
26 |
+ multipart_response = self.s3.list_multipart(uri, upload_id) |
|
27 |
+ tree = getTreeFromXml(multipart_response['data']) |
|
28 |
+ |
|
29 |
+ parts = defaultdict(lambda: None) |
|
30 |
+ for elem in parseNodes(tree): |
|
31 |
+ try: |
|
32 |
+ parts[int(elem['PartNumber'])] = {'checksum': elem['ETag'], 'size': elem['Size']} |
|
33 |
+ except KeyError: |
|
34 |
+ pass |
|
35 |
+ |
|
36 |
+ return parts |
|
37 |
+ |
|
38 |
+ def get_unique_upload_id(self, uri): |
|
39 |
+ upload_id = None |
|
40 |
+ multipart_response = self.s3.get_multipart(uri) |
|
41 |
+ tree = getTreeFromXml(multipart_response['data']) |
|
42 |
+ for mpupload in parseNodes(tree): |
|
43 |
+ try: |
|
44 |
+ mp_upload_id = mpupload['UploadId'] |
|
45 |
+ mp_path = mpupload['Key'] |
|
46 |
+ info("mp_path: %s, object: %s" % (mp_path, uri.object())) |
|
47 |
+ if mp_path == uri.object(): |
|
48 |
+ if upload_id is not None: |
|
49 |
+ raise ValueError("More than one UploadId for URI %s. Disable multipart upload, or use\n %s multipart %s\nto list the Ids, then pass a unique --upload-id into the put command." % (uri, sys.argv[0], uri)) |
|
50 |
+ upload_id = mp_upload_id |
|
51 |
+ except KeyError: |
|
52 |
+ pass |
|
53 |
+ |
|
54 |
+ return upload_id |
|
55 |
+ |
|
25 | 56 |
def initiate_multipart_upload(self): |
26 | 57 |
""" |
27 | 58 |
Begin a multipart upload |
28 | 59 |
http://docs.amazonwebservices.com/AmazonS3/latest/API/index.html?mpUploadInitiate.html |
29 | 60 |
""" |
30 |
- request = self.s3.create_request("OBJECT_POST", uri = self.uri, headers = self.headers_baseline, extra = "?uploads") |
|
31 |
- response = self.s3.send_request(request) |
|
32 |
- data = response["data"] |
|
33 |
- self.upload_id = getTextFromXml(data, "UploadId") |
|
61 |
+ if self.s3.config.upload_id is not None: |
|
62 |
+ self.upload_id = self.s3.config.upload_id |
|
63 |
+ elif self.s3.config.put_continue: |
|
64 |
+ self.upload_id = self.get_unique_upload_id(self.uri) |
|
65 |
+ else: |
|
66 |
+ self.upload_id = None |
|
67 |
+ |
|
68 |
+ if self.upload_id is None: |
|
69 |
+ request = self.s3.create_request("OBJECT_POST", uri = self.uri, headers = self.headers_baseline, extra = "?uploads") |
|
70 |
+ response = self.s3.send_request(request) |
|
71 |
+ data = response["data"] |
|
72 |
+ self.upload_id = getTextFromXml(data, "UploadId") |
|
73 |
+ |
|
34 | 74 |
return self.upload_id |
35 | 75 |
|
36 | 76 |
def upload_all_parts(self): |
... | ... |
@@ -51,6 +93,10 @@ class MultiPartUpload(object): |
51 | 51 |
else: |
52 | 52 |
debug("MultiPart: Uploading from %s" % (self.file.name)) |
53 | 53 |
|
54 |
+ remote_statuses = defaultdict(lambda: None) |
|
55 |
+ if self.s3.config.put_continue: |
|
56 |
+ remote_statuses = self.get_parts_information(self.uri, self.upload_id) |
|
57 |
+ |
|
54 | 58 |
seq = 1 |
55 | 59 |
if self.file.name != "<stdin>": |
56 | 60 |
while size_left > 0: |
... | ... |
@@ -63,10 +109,10 @@ class MultiPartUpload(object): |
63 | 63 |
'extra' : "[part %d of %d, %s]" % (seq, nr_parts, "%d%sB" % formatSize(current_chunk_size, human_readable = True)) |
64 | 64 |
} |
65 | 65 |
try: |
66 |
- self.upload_part(seq, offset, current_chunk_size, labels) |
|
66 |
+ self.upload_part(seq, offset, current_chunk_size, labels, remote_status = remote_statuses[seq]) |
|
67 | 67 |
except: |
68 |
- error(u"Upload of '%s' part %d failed. Aborting multipart upload." % (self.file.name, seq)) |
|
69 |
- self.abort_upload() |
|
68 |
+ error(u"\nUpload of '%s' part %d failed. Use\n %s abortmp %s %s\nto abort the upload, or\n %s --upload-id %s put ...\nto continue the upload." |
|
69 |
+ % (self.file.name, seq, sys.argv[0], self.uri, self.upload_id, sys.argv[0], self.upload_id)) |
|
70 | 70 |
raise |
71 | 71 |
seq += 1 |
72 | 72 |
else: |
... | ... |
@@ -82,22 +128,38 @@ class MultiPartUpload(object): |
82 | 82 |
if len(buffer) == 0: # EOF |
83 | 83 |
break |
84 | 84 |
try: |
85 |
- self.upload_part(seq, offset, current_chunk_size, labels, buffer) |
|
85 |
+ self.upload_part(seq, offset, current_chunk_size, labels, buffer, remote_status = remote_statuses[seq]) |
|
86 | 86 |
except: |
87 |
- error(u"Upload of '%s' part %d failed. Aborting multipart upload." % (self.file.name, seq)) |
|
88 |
- self.abort_upload() |
|
87 |
+ error(u"\nUpload of '%s' part %d failed. Use\n %s abortmp %s %s\nto abort, or\n %s --upload-id %s put ...\nto continue the upload." |
|
88 |
+ % (self.file.name, seq, self.uri, sys.argv[0], self.upload_id, sys.argv[0], self.upload_id)) |
|
89 | 89 |
raise |
90 | 90 |
seq += 1 |
91 | 91 |
|
92 | 92 |
debug("MultiPart: Upload finished: %d parts", seq - 1) |
93 | 93 |
|
94 |
- def upload_part(self, seq, offset, chunk_size, labels, buffer = ''): |
|
94 |
+ def upload_part(self, seq, offset, chunk_size, labels, buffer = '', remote_status = None): |
|
95 | 95 |
""" |
96 | 96 |
Upload a file chunk |
97 | 97 |
http://docs.amazonwebservices.com/AmazonS3/latest/API/index.html?mpUploadUploadPart.html |
98 | 98 |
""" |
99 | 99 |
# TODO implement Content-MD5 |
100 | 100 |
debug("Uploading part %i of %r (%s bytes)" % (seq, self.upload_id, chunk_size)) |
101 |
+ |
|
102 |
+ if remote_status is not None: |
|
103 |
+ if int(remote_status['size']) == chunk_size: |
|
104 |
+ checksum = calculateChecksum(buffer, self.file, offset, chunk_size, self.s3.config.send_chunk) |
|
105 |
+ remote_checksum = remote_status['checksum'].strip('"') |
|
106 |
+ if remote_checksum == checksum: |
|
107 |
+ warning("MultiPart: size and md5sum match for %s part %d, skipping." % (self.uri, seq)) |
|
108 |
+ self.parts[seq] = remote_status['checksum'] |
|
109 |
+ return |
|
110 |
+ else: |
|
111 |
+ warning("MultiPart: checksum (%s vs %s) does not match for %s part %d, reuploading." |
|
112 |
+ % (remote_checksum, checksum, self.uri, seq)) |
|
113 |
+ else: |
|
114 |
+ warning("MultiPart: size (%d vs %d) does not match for %s part %d, reuploading." |
|
115 |
+ % (int(remote_status['size']), chunk_size, self.uri, seq)) |
|
116 |
+ |
|
101 | 117 |
headers = { "content-length": chunk_size } |
102 | 118 |
query_string = "?partNumber=%i&uploadId=%s" % (seq, self.upload_id) |
103 | 119 |
request = self.s3.create_request("OBJECT_PUT", uri = self.uri, headers = headers, extra = query_string) |
... | ... |
@@ -130,8 +192,19 @@ class MultiPartUpload(object): |
130 | 130 |
http://docs.amazonwebservices.com/AmazonS3/latest/API/index.html?mpUploadAbort.html |
131 | 131 |
""" |
132 | 132 |
debug("MultiPart: Aborting upload: %s" % self.upload_id) |
133 |
- request = self.s3.create_request("OBJECT_DELETE", uri = self.uri, extra = "?uploadId=%s" % (self.upload_id)) |
|
134 |
- response = self.s3.send_request(request) |
|
133 |
+ #request = self.s3.create_request("OBJECT_DELETE", uri = self.uri, extra = "?uploadId=%s" % (self.upload_id)) |
|
134 |
+ #response = self.s3.send_request(request) |
|
135 |
+ response = None |
|
135 | 136 |
return response |
136 | 137 |
|
137 | 138 |
# vim:et:ts=4:sts=4:ai |
139 |
+ |
|
140 |
+ |
|
141 |
+ |
|
142 |
+ |
|
143 |
+ |
|
144 |
+ |
|
145 |
+ |
|
146 |
+ |
|
147 |
+ |
|
148 |
+ |
... | ... |
@@ -79,7 +79,7 @@ class Progress(object): |
79 | 79 |
self._stdout.flush() |
80 | 80 |
return |
81 | 81 |
|
82 |
- rel_position = selfself.current_position * 100 / self.total_size |
|
82 |
+ rel_position = self.current_position * 100 / self.total_size |
|
83 | 83 |
if rel_position >= self.last_milestone: |
84 | 84 |
self.last_milestone = (int(rel_position) / 5) * 5 |
85 | 85 |
self._stdout.write("%d%% ", self.last_milestone) |
... | ... |
@@ -403,12 +403,16 @@ class S3(object): |
403 | 403 |
headers = SortedDict(ignore_case = True) |
404 | 404 |
if extra_headers: |
405 | 405 |
headers.update(extra_headers) |
406 |
- |
|
406 |
+ |
|
407 |
+ ## Set server side encryption |
|
408 |
+ if self.config.server_side_encryption: |
|
409 |
+ headers["x-amz-server-side-encryption"] = "AES256" |
|
410 |
+ |
|
407 | 411 |
## MIME-type handling |
408 | 412 |
content_type = self.config.mime_type |
409 | 413 |
content_encoding = None |
410 | 414 |
if filename != "-" and not content_type and self.config.guess_mime_type: |
411 |
- (content_type, content_encoding) = mime_magic(filename) |
|
415 |
+ (content_type, content_encoding) = mime_magic(filename) if self.config.use_mime_magic else mimetypes.guess_type(filename) |
|
412 | 416 |
if not content_type: |
413 | 417 |
content_type = self.config.default_mime_type |
414 | 418 |
|
... | ... |
@@ -417,7 +421,7 @@ class S3(object): |
417 | 417 |
content_type = content_type + "; charset=" + self.config.encoding.upper() |
418 | 418 |
|
419 | 419 |
headers["content-type"] = content_type |
420 |
- if content_encoding is not None: |
|
420 |
+ if content_encoding is not None and self.config.add_content_encoding: |
|
421 | 421 |
headers["content-encoding"] = content_encoding |
422 | 422 |
|
423 | 423 |
## Other Amazon S3 attributes |
... | ... |
@@ -438,6 +442,31 @@ class S3(object): |
438 | 438 |
return self.send_file_multipart(file, headers, uri, size) |
439 | 439 |
|
440 | 440 |
## Not multipart... |
441 |
+ if self.config.put_continue: |
|
442 |
+ # Note, if input was stdin, we would be performing multipart upload. |
|
443 |
+ # So this will always work as long as the file already uploaded was |
|
444 |
+ # not uploaded via MultiUpload, in which case its ETag will not be |
|
445 |
+ # an md5. |
|
446 |
+ try: |
|
447 |
+ info = self.object_info(uri) |
|
448 |
+ except: |
|
449 |
+ info = None |
|
450 |
+ |
|
451 |
+ if info is not None: |
|
452 |
+ remote_size = int(info['headers']['content-length']) |
|
453 |
+ remote_checksum = info['headers']['etag'].strip('"') |
|
454 |
+ if size == remote_size: |
|
455 |
+ checksum = calculateChecksum('', file, 0, size, self.config.send_chunk) |
|
456 |
+ if remote_checksum == checksum: |
|
457 |
+ warning("Put: size and md5sum match for %s, skipping." % uri) |
|
458 |
+ return |
|
459 |
+ else: |
|
460 |
+ warning("MultiPart: checksum (%s vs %s) does not match for %s, reuploading." |
|
461 |
+ % (remote_checksum, checksum, uri)) |
|
462 |
+ else: |
|
463 |
+ warning("MultiPart: size (%d vs %d) does not match for %s, reuploading." |
|
464 |
+ % (remote_size, size, uri)) |
|
465 |
+ |
|
441 | 466 |
headers["content-length"] = size |
442 | 467 |
request = self.create_request("OBJECT_PUT", uri = uri, headers = headers) |
443 | 468 |
labels = { 'source' : unicodise(filename), 'destination' : unicodise(uri.uri()), 'extra' : extra_label } |
... | ... |
@@ -474,6 +503,11 @@ class S3(object): |
474 | 474 |
headers["x-amz-storage-class"] = "REDUCED_REDUNDANCY" |
475 | 475 |
# if extra_headers: |
476 | 476 |
# headers.update(extra_headers) |
477 |
+ |
|
478 |
+ ## Set server side encryption |
|
479 |
+ if self.config.server_side_encryption: |
|
480 |
+ headers["x-amz-server-side-encryption"] = "AES256" |
|
481 |
+ |
|
477 | 482 |
request = self.create_request("OBJECT_PUT", uri = dst_uri, headers = headers) |
478 | 483 |
response = self.send_request(request) |
479 | 484 |
return response |
... | ... |
@@ -535,6 +569,23 @@ class S3(object): |
535 | 535 |
response = self.send_request(request) |
536 | 536 |
return response |
537 | 537 |
|
538 |
+ def get_multipart(self, uri): |
|
539 |
+ request = self.create_request("BUCKET_LIST", bucket = uri.bucket(), extra = "?uploads") |
|
540 |
+ response = self.send_request(request) |
|
541 |
+ return response |
|
542 |
+ |
|
543 |
+ def abort_multipart(self, uri, id): |
|
544 |
+ request = self.create_request("OBJECT_DELETE", uri=uri, |
|
545 |
+ extra = ("?uploadId=%s" % id)) |
|
546 |
+ response = self.send_request(request) |
|
547 |
+ return response |
|
548 |
+ |
|
549 |
+ def list_multipart(self, uri, id): |
|
550 |
+ request = self.create_request("OBJECT_GET", uri=uri, |
|
551 |
+ extra = ("?uploadId=%s" % id)) |
|
552 |
+ response = self.send_request(request) |
|
553 |
+ return response |
|
554 |
+ |
|
538 | 555 |
def get_accesslog(self, uri): |
539 | 556 |
request = self.create_request("BUCKET_LIST", bucket = uri.bucket(), extra = "?logging") |
540 | 557 |
response = self.send_request(request) |
... | ... |
@@ -739,6 +790,7 @@ class S3(object): |
739 | 739 |
if buffer == '': |
740 | 740 |
file.seek(offset) |
741 | 741 |
md5_hash = md5() |
742 |
+ |
|
742 | 743 |
try: |
743 | 744 |
while (size_left > 0): |
744 | 745 |
#debug("SendFile: Reading up to %d bytes from '%s' - remaining bytes: %s" % (self.config.send_chunk, file.name, size_left)) |
... | ... |
@@ -746,6 +798,7 @@ class S3(object): |
746 | 746 |
data = file.read(min(self.config.send_chunk, size_left)) |
747 | 747 |
else: |
748 | 748 |
data = buffer |
749 |
+ |
|
749 | 750 |
md5_hash.update(data) |
750 | 751 |
conn.c.send(data) |
751 | 752 |
if self.config.progress_meter: |
... | ... |
@@ -754,6 +807,7 @@ class S3(object): |
754 | 754 |
if throttle: |
755 | 755 |
time.sleep(throttle) |
756 | 756 |
md5_computed = md5_hash.hexdigest() |
757 |
+ |
|
757 | 758 |
response = {} |
758 | 759 |
http_response = conn.c.getresponse() |
759 | 760 |
response["status"] = http_response.status |
... | ... |
@@ -217,11 +217,11 @@ def mktmpsomething(prefix, randchars, createfunc): |
217 | 217 |
return dirname |
218 | 218 |
__all__.append("mktmpsomething") |
219 | 219 |
|
220 |
-def mktmpdir(prefix = "/tmp/tmpdir-", randchars = 10): |
|
220 |
+def mktmpdir(prefix = os.getenv('TMP','/tmp') + "/tmpdir-", randchars = 10): |
|
221 | 221 |
return mktmpsomething(prefix, randchars, os.mkdir) |
222 | 222 |
__all__.append("mktmpdir") |
223 | 223 |
|
224 |
-def mktmpfile(prefix = "/tmp/tmpfile-", randchars = 20): |
|
224 |
+def mktmpfile(prefix = os.getenv('TMP','/tmp') + "/tmpfile-", randchars = 20): |
|
225 | 225 |
createfunc = lambda filename : os.close(os.open(filename, os.O_CREAT | os.O_EXCL)) |
226 | 226 |
return mktmpsomething(prefix, randchars, createfunc) |
227 | 227 |
__all__.append("mktmpfile") |
... | ... |
@@ -383,7 +383,7 @@ def time_to_epoch(t): |
383 | 383 |
return int(time.mktime(t)) |
384 | 384 |
elif hasattr(t, 'timetuple'): |
385 | 385 |
# Looks like a datetime object or compatible |
386 |
- return int(time.mktime(ex.timetuple())) |
|
386 |
+ return int(time.mktime(t.timetuple())) |
|
387 | 387 |
elif hasattr(t, 'strftime'): |
388 | 388 |
# Looks like the object supports standard srftime() |
389 | 389 |
return int(t.strftime('%s')) |
... | ... |
@@ -459,4 +459,22 @@ def getHostnameFromBucket(bucket): |
459 | 459 |
return Config.Config().host_bucket % { 'bucket' : bucket } |
460 | 460 |
__all__.append("getHostnameFromBucket") |
461 | 461 |
|
462 |
+ |
|
463 |
+def calculateChecksum(buffer, mfile, offset, chunk_size, send_chunk): |
|
464 |
+ md5_hash = md5() |
|
465 |
+ size_left = chunk_size |
|
466 |
+ if buffer == '': |
|
467 |
+ mfile.seek(offset) |
|
468 |
+ while size_left > 0: |
|
469 |
+ data = mfile.read(min(send_chunk, size_left)) |
|
470 |
+ md5_hash.update(data) |
|
471 |
+ size_left -= len(data) |
|
472 |
+ else: |
|
473 |
+ md5_hash.update(buffer) |
|
474 |
+ |
|
475 |
+ return md5_hash.hexdigest() |
|
476 |
+ |
|
477 |
+ |
|
478 |
+__all__.append("calculateChecksum") |
|
479 |
+ |
|
462 | 480 |
# vim:et:ts=4:sts=4:ai |
... | ... |
@@ -339,14 +339,15 @@ def cmd_object_put(args): |
339 | 339 |
except InvalidFileError, e: |
340 | 340 |
warning(u"File can not be uploaded: %s" % e) |
341 | 341 |
continue |
342 |
- speed_fmt = formatSize(response["speed"], human_readable = True, floating_point = True) |
|
343 |
- if not Config().progress_meter: |
|
344 |
- output(u"File '%s' stored as '%s' (%d bytes in %0.1f seconds, %0.2f %sB/s) %s" % |
|
345 |
- (unicodise(full_name_orig), uri_final, response["size"], response["elapsed"], |
|
346 |
- speed_fmt[0], speed_fmt[1], seq_label)) |
|
342 |
+ if response is not None: |
|
343 |
+ speed_fmt = formatSize(response["speed"], human_readable = True, floating_point = True) |
|
344 |
+ if not Config().progress_meter: |
|
345 |
+ output(u"File '%s' stored as '%s' (%d bytes in %0.1f seconds, %0.2f %sB/s) %s" % |
|
346 |
+ (unicodise(full_name_orig), uri_final, response["size"], response["elapsed"], |
|
347 |
+ speed_fmt[0], speed_fmt[1], seq_label)) |
|
347 | 348 |
if Config().acl_public: |
348 | 349 |
output(u"Public URL of the object is: %s" % |
349 |
- (uri_final.public_url())) |
|
350 |
+ (uri_final.public_url())) |
|
350 | 351 |
if Config().encrypt and full_name != full_name_orig: |
351 | 352 |
debug(u"Removing temporary encrypted file: %s" % unicodise(full_name)) |
352 | 353 |
os.remove(full_name) |
... | ... |
@@ -625,6 +626,11 @@ def cmd_info(args): |
625 | 625 |
except KeyError: |
626 | 626 |
pass |
627 | 627 |
output(u" MD5 sum: %s" % md5) |
628 |
+ if 'x-amz-server-side-encryption' in info['headers']: |
|
629 |
+ output(u" SSE: %s" % info['headers']['x-amz-server-side-encryption']) |
|
630 |
+ else: |
|
631 |
+ output(u" SSE: NONE") |
|
632 |
+ |
|
628 | 633 |
else: |
629 | 634 |
info = s3.bucket_info(uri) |
630 | 635 |
output(u"%s (bucket):" % uri.uri()) |
... | ... |
@@ -751,7 +757,14 @@ def cmd_sync_remote2remote(args): |
751 | 751 |
seq = 0 |
752 | 752 |
seq = _upload(src_list, seq, src_count + update_count) |
753 | 753 |
seq = _upload(update_list, seq, src_count + update_count) |
754 |
- n_copied, bytes_saved = remote_copy(s3, copy_pairs, destination_base) |
|
754 |
+ n_copied, bytes_saved, failed_copy_files = remote_copy(s3, copy_pairs, destination_base) |
|
755 |
+ |
|
756 |
+ #process files not copied |
|
757 |
+ output("Process files that was not remote copied") |
|
758 |
+ failed_copy_count = len (failed_copy_files) |
|
759 |
+ for key in failed_copy_files: |
|
760 |
+ failed_copy_files[key]['target_uri'] = destination_base + key |
|
761 |
+ seq = _upload(failed_copy_files, seq, failed_copy_count) |
|
755 | 762 |
|
756 | 763 |
total_elapsed = time.time() - timestamp_start |
757 | 764 |
outstr = "Done. Copied %d files in %0.1f seconds, %0.2f files/s" % (seq, total_elapsed, seq/total_elapsed) |
... | ... |
@@ -797,6 +810,7 @@ def cmd_sync_remote2local(args): |
797 | 797 |
|
798 | 798 |
info(u"Summary: %d remote files to download, %d local files to delete, %d local files to hardlink" % (remote_count + update_count, local_count, copy_pairs_count)) |
799 | 799 |
|
800 |
+ empty_fname_re = re.compile(r'\A\s*\Z') |
|
800 | 801 |
def _set_local_filename(remote_list, destination_base): |
801 | 802 |
if len(remote_list) == 0: |
802 | 803 |
return |
... | ... |
@@ -809,7 +823,12 @@ def cmd_sync_remote2local(args): |
809 | 809 |
if destination_base[-1] != os.path.sep: |
810 | 810 |
destination_base += os.path.sep |
811 | 811 |
for key in remote_list: |
812 |
- local_filename = destination_base + key |
|
812 |
+ local_basename = key |
|
813 |
+ if empty_fname_re.match(key): |
|
814 |
+ # Objects may exist on S3 with empty names (''), which don't map so well to common filesystems. |
|
815 |
+ local_basename = '__AWS-EMPTY-OBJECT-NAME__' |
|
816 |
+ warning(u"Empty object name on S3 found, saving locally as %s" % (local_basename)) |
|
817 |
+ local_filename = destination_base + local_basename |
|
813 | 818 |
if os.path.sep != "/": |
814 | 819 |
local_filename = os.path.sep.join(local_filename.split("/")) |
815 | 820 |
remote_list[key]['local_filename'] = deunicodise(local_filename) |
... | ... |
@@ -887,7 +906,10 @@ def cmd_sync_remote2local(args): |
887 | 887 |
mtime = attrs.has_key('mtime') and int(attrs['mtime']) or int(time.time()) |
888 | 888 |
atime = attrs.has_key('atime') and int(attrs['atime']) or int(time.time()) |
889 | 889 |
os.utime(dst_file, (atime, mtime)) |
890 |
- ## FIXME: uid/gid / uname/gname handling comes here! TODO |
|
890 |
+ if attrs.has_key('uid') and attrs.has_key('gid'): |
|
891 |
+ uid = int(attrs['uid']) |
|
892 |
+ gid = int(attrs['gid']) |
|
893 |
+ os.lchown(dst_file,uid,gid) |
|
891 | 894 |
except OSError, e: |
892 | 895 |
try: |
893 | 896 |
dst_stream.close() |
... | ... |
@@ -986,6 +1008,7 @@ def local_copy(copy_pairs, destination_base): |
986 | 986 |
|
987 | 987 |
def remote_copy(s3, copy_pairs, destination_base): |
988 | 988 |
saved_bytes = 0 |
989 |
+ failed_copy_list = FileDict() |
|
989 | 990 |
for (src_obj, dst1, dst2) in copy_pairs: |
990 | 991 |
debug(u"Remote Copying from %s to %s" % (dst1, dst2)) |
991 | 992 |
dst1_uri = S3Uri(destination_base + dst1) |
... | ... |
@@ -997,8 +1020,9 @@ def remote_copy(s3, copy_pairs, destination_base): |
997 | 997 |
saved_bytes = saved_bytes + int(info['headers']['content-length']) |
998 | 998 |
output(u"remote copy: %s -> %s" % (dst1, dst2)) |
999 | 999 |
except: |
1000 |
- raise |
|
1001 |
- return (len(copy_pairs), saved_bytes) |
|
1000 |
+ warning(u'Unable to remote copy files %s -> %s' % (dst1_uri, dst2_uri)) |
|
1001 |
+ failed_copy_list[dst2] = src_obj |
|
1002 |
+ return (len(copy_pairs), saved_bytes, failed_copy_list) |
|
1002 | 1003 |
|
1003 | 1004 |
def _build_attr_header(local_list, src): |
1004 | 1005 |
import pwd, grp |
... | ... |
@@ -1188,7 +1212,14 @@ def cmd_sync_local2remote(args): |
1188 | 1188 |
timestamp_start = time.time() |
1189 | 1189 |
n, total_size = _upload(local_list, 0, local_count, total_size) |
1190 | 1190 |
n, total_size = _upload(update_list, n, local_count, total_size) |
1191 |
- n_copies, saved_bytes = remote_copy(s3, copy_pairs, destination_base) |
|
1191 |
+ n_copies, saved_bytes, failed_copy_files = remote_copy(s3, copy_pairs, destination_base) |
|
1192 |
+ |
|
1193 |
+ #upload file that could not be copied |
|
1194 |
+ output("Process files that was not remote copied") |
|
1195 |
+ failed_copy_count = len(failed_copy_files) |
|
1196 |
+ _set_remote_uri(failed_copy_files, destination_base, single_file_local) |
|
1197 |
+ n, total_size = _upload(failed_copy_files, n, failed_copy_count, total_size) |
|
1198 |
+ |
|
1192 | 1199 |
if cfg.delete_removed and cfg.delete_after: |
1193 | 1200 |
_do_deletes(s3, remote_list) |
1194 | 1201 |
total_elapsed = time.time() - timestamp_start |
... | ... |
@@ -1331,6 +1362,50 @@ def cmd_delpolicy(args): |
1331 | 1331 |
output(u"%s: Policy deleted" % uri) |
1332 | 1332 |
|
1333 | 1333 |
|
1334 |
+def cmd_multipart(args): |
|
1335 |
+ s3 = S3(cfg) |
|
1336 |
+ uri = S3Uri(args[0]) |
|
1337 |
+ |
|
1338 |
+ #id = '' |
|
1339 |
+ #if(len(args) > 1): id = args[1] |
|
1340 |
+ |
|
1341 |
+ response = s3.get_multipart(uri) |
|
1342 |
+ debug(u"response - %s" % response['status']) |
|
1343 |
+ output(u"%s" % uri) |
|
1344 |
+ tree = getTreeFromXml(response['data']) |
|
1345 |
+ debug(parseNodes(tree)) |
|
1346 |
+ output(u"Initiated\tPath\tId") |
|
1347 |
+ for mpupload in parseNodes(tree): |
|
1348 |
+ try: |
|
1349 |
+ output("%s\t%s\t%s" % (mpupload['Initiated'], "s3://" + uri.bucket() + "/" + mpupload['Key'], mpupload['UploadId'])) |
|
1350 |
+ except KeyError: |
|
1351 |
+ pass |
|
1352 |
+ |
|
1353 |
+def cmd_abort_multipart(args): |
|
1354 |
+ '''{"cmd":"abortmp", "label":"abort a multipart upload", "param":"s3://BUCKET Id", "func":cmd_abort_multipart, "argc":2},''' |
|
1355 |
+ s3 = S3(cfg) |
|
1356 |
+ uri = S3Uri(args[0]) |
|
1357 |
+ id = args[1] |
|
1358 |
+ response = s3.abort_multipart(uri, id) |
|
1359 |
+ debug(u"response - %s" % response['status']) |
|
1360 |
+ output(u"%s" % uri) |
|
1361 |
+ |
|
1362 |
+def cmd_list_multipart(args): |
|
1363 |
+ '''{"cmd":"abortmp", "label":"list a multipart upload", "param":"s3://BUCKET Id", "func":cmd_list_multipart, "argc":2},''' |
|
1364 |
+ s3 = S3(cfg) |
|
1365 |
+ uri = S3Uri(args[0]) |
|
1366 |
+ id = args[1] |
|
1367 |
+ |
|
1368 |
+ response = s3.list_multipart(uri, id) |
|
1369 |
+ debug(u"response - %s" % response['status']) |
|
1370 |
+ tree = getTreeFromXml(response['data']) |
|
1371 |
+ output(u"LastModified\t\t\tPartNumber\tETag\tSize") |
|
1372 |
+ for mpupload in parseNodes(tree): |
|
1373 |
+ try: |
|
1374 |
+ output("%s\t%s\t%s\t%s" % (mpupload['LastModified'], mpupload['PartNumber'], mpupload['ETag'], mpupload['Size'])) |
|
1375 |
+ except: |
|
1376 |
+ pass |
|
1377 |
+ |
|
1334 | 1378 |
def cmd_accesslog(args): |
1335 | 1379 |
s3 = S3(cfg) |
1336 | 1380 |
bucket_uri = S3Uri(args.pop()) |
... | ... |
@@ -1587,6 +1662,8 @@ def run_configure(config_file, args): |
1587 | 1587 |
|
1588 | 1588 |
except Exception, e: |
1589 | 1589 |
error(u"Test failed: %s" % (e)) |
1590 |
+ if e.find('403') != -1: |
|
1591 |
+ error(u"Are you sure your keys have ListAllMyBuckets permissions?") |
|
1590 | 1592 |
val = raw_input("\nRetry configuration? [Y/n] ") |
1591 | 1593 |
if val.lower().startswith("y") or val == "": |
1592 | 1594 |
continue |
... | ... |
@@ -1685,6 +1762,11 @@ def get_commands_list(): |
1685 | 1685 |
{"cmd":"setpolicy", "label":"Modify Bucket Policy", "param":"FILE s3://BUCKET", "func":cmd_setpolicy, "argc":2}, |
1686 | 1686 |
{"cmd":"delpolicy", "label":"Delete Bucket Policy", "param":"s3://BUCKET", "func":cmd_delpolicy, "argc":1}, |
1687 | 1687 |
|
1688 |
+ {"cmd":"multipart", "label":"show multipart uploads", "param":"s3://BUCKET [Id]", "func":cmd_multipart, "argc":1}, |
|
1689 |
+ {"cmd":"abortmp", "label":"abort a multipart upload", "param":"s3://BUCKET/OBJECT Id", "func":cmd_abort_multipart, "argc":2}, |
|
1690 |
+ |
|
1691 |
+ {"cmd":"listmp", "label":"list parts of a multipart upload", "param":"s3://BUCKET/OBJECT Id", "func":cmd_list_multipart, "argc":2}, |
|
1692 |
+ |
|
1688 | 1693 |
{"cmd":"accesslog", "label":"Enable/disable bucket access logging", "param":"s3://BUCKET", "func":cmd_accesslog, "argc":1}, |
1689 | 1694 |
{"cmd":"sign", "label":"Sign arbitrary string using the secret key", "param":"STRING-TO-SIGN", "func":cmd_sign, "argc":1}, |
1690 | 1695 |
{"cmd":"signurl", "label":"Sign an S3 URL to provide limited public access with expiry", "param":"s3://BUCKET/OBJECT expiry_epoch", "func":cmd_signurl, "argc":2}, |
... | ... |
@@ -1813,7 +1895,7 @@ def main(): |
1813 | 1813 |
optparser.set_defaults(config = config_file) |
1814 | 1814 |
optparser.set_defaults(verbosity = default_verbosity) |
1815 | 1815 |
|
1816 |
- optparser.add_option( "--configure", dest="run_configure", action="store_true", help="Invoke interactive (re)configuration tool. Optionally use as '--configure s3://come-bucket' to test access to a specific bucket instead of attempting to list them all.") |
|
1816 |
+ optparser.add_option( "--configure", dest="run_configure", action="store_true", help="Invoke interactive (re)configuration tool. Optionally use as '--configure s3://some-bucket' to test access to a specific bucket instead of attempting to list them all.") |
|
1817 | 1817 |
optparser.add_option("-c", "--config", dest="config", metavar="FILE", help="Config file name. Defaults to %default") |
1818 | 1818 |
optparser.add_option( "--dump-config", dest="dump_config", action="store_true", help="Dump current configuration after parsing config files and command line options and exit.") |
1819 | 1819 |
optparser.add_option( "--access_key", dest="access_key", help="AWS Access Key") |
... | ... |
@@ -1825,6 +1907,8 @@ def main(): |
1825 | 1825 |
optparser.add_option( "--no-encrypt", dest="encrypt", action="store_false", help="Don't encrypt files.") |
1826 | 1826 |
optparser.add_option("-f", "--force", dest="force", action="store_true", help="Force overwrite and other dangerous operations.") |
1827 | 1827 |
optparser.add_option( "--continue", dest="get_continue", action="store_true", help="Continue getting a partially downloaded file (only for [get] command).") |
1828 |
+ optparser.add_option( "--continue-put", dest="put_continue", action="store_true", help="Continue uploading partially uploaded files or multipart upload parts. Restarts/parts files that don't have matching size and md5. Skips files/parts that do. Note: md5sum checks are not always sufficient to check (part) file equality. Enable this at your own risk.") |
|
1829 |
+ optparser.add_option( "--upload-id", dest="upload_id", help="UploadId for Multipart Upload, in case you want continue an existing upload (equivalent to --continue-put) and there are multiple partial uploads. Use s3cmd multipart [URI] to see what UploadIds are associated with the given URI.") |
|
1828 | 1830 |
optparser.add_option( "--skip-existing", dest="skip_existing", action="store_true", help="Skip over files that exist at the destination (only for [get] and [sync] commands).") |
1829 | 1831 |
optparser.add_option("-r", "--recursive", dest="recursive", action="store_true", help="Recursive upload, download or removal.") |
1830 | 1832 |
optparser.add_option( "--check-md5", dest="check_md5", action="store_true", help="Check MD5 sums when comparing files for [sync]. (default)") |
... | ... |
@@ -1860,14 +1944,18 @@ def main(): |
1860 | 1860 |
optparser.add_option( "--access-logging-target-prefix", dest="log_target_prefix", help="Target prefix for access logs (S3 URI) (for [cfmodify] and [accesslog] commands)") |
1861 | 1861 |
optparser.add_option( "--no-access-logging", dest="log_target_prefix", action="store_false", help="Disable access logging (for [cfmodify] and [accesslog] commands)") |
1862 | 1862 |
|
1863 |
- optparser.add_option( "--default-mime-type", dest="default_mime_type", action="store_true", help="Default MIME-type for stored objects. Application default is binary/octet-stream.") |
|
1863 |
+ optparser.add_option( "--default-mime-type", dest="default_mime_type", type="mimetype", action="store", help="Default MIME-type for stored objects. Application default is binary/octet-stream.") |
|
1864 | 1864 |
optparser.add_option("-M", "--guess-mime-type", dest="guess_mime_type", action="store_true", help="Guess MIME-type of files by their extension or mime magic. Fall back to default MIME-Type as specified by --default-mime-type option") |
1865 | 1865 |
optparser.add_option( "--no-guess-mime-type", dest="guess_mime_type", action="store_false", help="Don't guess MIME-type and use the default type instead.") |
1866 |
+ optparser.add_option( "--no-mime-magic", dest="use_mime_magic", action="store_false", help="Don't use mime magic when guessing MIME-type.") |
|
1866 | 1867 |
optparser.add_option("-m", "--mime-type", dest="mime_type", type="mimetype", metavar="MIME/TYPE", help="Force MIME-type. Override both --default-mime-type and --guess-mime-type.") |
1867 | 1868 |
|
1868 | 1869 |
optparser.add_option( "--add-header", dest="add_header", action="append", metavar="NAME:VALUE", help="Add a given HTTP header to the upload request. Can be used multiple times. For instance set 'Expires' or 'Cache-Control' headers (or both) using this options if you like.") |
1869 | 1870 |
|
1871 |
+ optparser.add_option( "--server-side-encryption", dest="server_side_encryption", action="store_true", help="Specifies that server-side encryption will be used when putting objects.") |
|
1872 |
+ |
|
1870 | 1873 |
optparser.add_option( "--encoding", dest="encoding", metavar="ENCODING", help="Override autodetected terminal and filesystem encoding (character set). Autodetected: %s" % preferred_encoding) |
1874 |
+ optparser.add_option( "--disable-content-encoding", dest="add_content_encoding", action="store_false", help="Don't include a Content-encoding header to the the uploaded objects") |
|
1871 | 1875 |
optparser.add_option( "--add-encoding-exts", dest="add_encoding_exts", metavar="EXTENSIONs", help="Add encoding to these comma delimited extensions i.e. (css,js,html) when uploading to S3 )") |
1872 | 1876 |
optparser.add_option( "--verbatim", dest="urlencoding_mode", action="store_const", const="verbatim", help="Use the S3 name as given on the command line. No pre-processing, encoding, etc. Use with caution!") |
1873 | 1877 |
|
... | ... |
@@ -1906,7 +1994,7 @@ def main(): |
1906 | 1906 |
'"buckets" and uploading, downloading and removing '+ |
1907 | 1907 |
'"objects" from these buckets.') |
1908 | 1908 |
optparser.epilog = format_commands(optparser.get_prog_name(), commands_list) |
1909 |
- optparser.epilog += ("\nFor more informations see the progect homepage:\n%s\n" % PkgInfo.url) |
|
1909 |
+ optparser.epilog += ("\nFor more information see the project homepage:\n%s\n" % PkgInfo.url) |
|
1910 | 1910 |
optparser.epilog += ("\nConsider a donation if you have found s3cmd useful:\n%s/donate\n" % PkgInfo.url) |
1911 | 1911 |
|
1912 | 1912 |
(options, args) = optparser.parse_args() |
... | ... |
@@ -2017,6 +2105,14 @@ def main(): |
2017 | 2017 |
if cfg.multipart_chunk_size_mb > MultiPartUpload.MAX_CHUNK_SIZE_MB: |
2018 | 2018 |
raise ParameterError("Chunk size %d MB is too large, must be <= %d MB. Please adjust --multipart-chunk-size-mb" % (cfg.multipart_chunk_size_mb, MultiPartUpload.MAX_CHUNK_SIZE_MB)) |
2019 | 2019 |
|
2020 |
+ ## If an UploadId was provided, set put_continue True |
|
2021 |
+ if options.upload_id is not None: |
|
2022 |
+ cfg.upload_id = options.upload_id |
|
2023 |
+ cfg.put_continue = True |
|
2024 |
+ |
|
2025 |
+ if cfg.upload_id and not cfg.multipart_chunk_size_mb: |
|
2026 |
+ raise ParameterError("Must have --multipart-chunk-size-mb if using --put-continue or --upload-id") |
|
2027 |
+ |
|
2020 | 2028 |
## CloudFront's cf_enable and Config's enable share the same --enable switch |
2021 | 2029 |
options.cf_enable = options.enable |
2022 | 2030 |
|
... | ... |
@@ -1,4 +1,3 @@ |
1 |
- |
|
2 | 1 |
.TH s3cmd 1 |
3 | 2 |
.SH NAME |
4 | 3 |
s3cmd \- tool for managing Amazon S3 storage space and Amazon CloudFront content delivery network |
... | ... |
@@ -66,7 +65,7 @@ Delete Bucket Policy |
66 | 66 |
s3cmd \fBaccesslog\fR \fIs3://BUCKET\fR |
67 | 67 |
Enable/disable bucket access logging |
68 | 68 |
.TP |
69 |
-s3cmd \fBsign\fR \fISTRING-TO-SIGN\fR |
|
69 |
+s3cmd \fBsign\fR \fISTRING\-TO\-SIGN\fR |
|
70 | 70 |
Sign arbitrary string using the secret key |
71 | 71 |
.TP |
72 | 72 |
s3cmd \fBsignurl\fR \fIs3://BUCKET/OBJECT expiry_epoch\fR |
... | ... |
@@ -79,13 +78,13 @@ Fix invalid file names in a bucket |
79 | 79 |
.PP |
80 | 80 |
Commands for static WebSites configuration |
81 | 81 |
.TP |
82 |
-s3cmd \fBws-create\fR \fIs3://BUCKET\fR |
|
82 |
+s3cmd \fBws\-create\fR \fIs3://BUCKET\fR |
|
83 | 83 |
Create Website from bucket |
84 | 84 |
.TP |
85 |
-s3cmd \fBws-delete\fR \fIs3://BUCKET\fR |
|
85 |
+s3cmd \fBws\-delete\fR \fIs3://BUCKET\fR |
|
86 | 86 |
Delete Website |
87 | 87 |
.TP |
88 |
-s3cmd \fBws-info\fR \fIs3://BUCKET\fR |
|
88 |
+s3cmd \fBws\-info\fR \fIs3://BUCKET\fR |
|
89 | 89 |
Info about Website |
90 | 90 |
|
91 | 91 |
|
... | ... |
@@ -124,7 +123,7 @@ changes you like. |
124 | 124 |
show this help message and exit |
125 | 125 |
.TP |
126 | 126 |
\fB\-\-configure\fR |
127 |
-Invoke interactive (re)configuration tool. Optionally use as '--configure s3://come-bucket' to test access to a specific bucket instead of attempting to list them all. |
|
127 |
+Invoke interactive (re)configuration tool. Optionally use as '\-\-configure s3://come\-bucket' to test access to a specific bucket instead of attempting to list them all. |
|
128 | 128 |
.TP |
129 | 129 |
\fB\-c\fR FILE, \fB\-\-config\fR=FILE |
130 | 130 |
Config file name. Defaults to /home/mludvig/.s3cfg |
... | ... |
@@ -208,32 +207,32 @@ Don't store FS attributes |
208 | 208 |
Filenames and paths matching GLOB will be excluded from sync |
209 | 209 |
.TP |
210 | 210 |
\fB\-\-exclude\-from\fR=FILE |
211 |
-Read --exclude GLOBs from FILE |
|
211 |
+Read \-\-exclude GLOBs from FILE |
|
212 | 212 |
.TP |
213 | 213 |
\fB\-\-rexclude\fR=REGEXP |
214 | 214 |
Filenames and paths matching REGEXP (regular expression) will be excluded from sync |
215 | 215 |
.TP |
216 | 216 |
\fB\-\-rexclude\-from\fR=FILE |
217 |
-Read --rexclude REGEXPs from FILE |
|
217 |
+Read \-\-rexclude REGEXPs from FILE |
|
218 | 218 |
.TP |
219 | 219 |
\fB\-\-include\fR=GLOB |
220 |
-Filenames and paths matching GLOB will be included even if previously excluded by one of --(r)exclude(-from) patterns |
|
220 |
+Filenames and paths matching GLOB will be included even if previously excluded by one of \-\-(r)exclude(\-from) patterns |
|
221 | 221 |
.TP |
222 | 222 |
\fB\-\-include\-from\fR=FILE |
223 |
-Read --include GLOBs from FILE |
|
223 |
+Read \-\-include GLOBs from FILE |
|
224 | 224 |
.TP |
225 | 225 |
\fB\-\-rinclude\fR=REGEXP |
226 |
-Same as --include but uses REGEXP (regular expression) instead of GLOB |
|
226 |
+Same as \-\-include but uses REGEXP (regular expression) instead of GLOB |
|
227 | 227 |
.TP |
228 | 228 |
\fB\-\-rinclude\-from\fR=FILE |
229 |
-Read --rinclude REGEXPs from FILE |
|
229 |
+Read \-\-rinclude REGEXPs from FILE |
|
230 | 230 |
.TP |
231 | 231 |
\fB\-\-files\-from\fR=FILE |
232 |
-Read list of source-file names from FILE. Use - to read from stdin. |
|
232 |
+Read list of source-file names from FILE. Use \- to read from stdin. |
|
233 | 233 |
May be repeated. |
234 | 234 |
.TP |
235 | 235 |
\fB\-\-bucket\-location\fR=BUCKET_LOCATION |
236 |
-Datacentre to create bucket in. As of now the datacenters are: US (default), EU, ap-northeast-1, ap-southeast-1, sa-east-1, us-west-1 and us-west-2 |
|
236 |
+Datacentre to create bucket in. As of now the datacenters are: US (default), EU, ap\-northeast\-1, ap\-southeast\-1, sa\-east\-1, us\-west\-1 and us\-west\-2 |
|
237 | 237 |
.TP |
238 | 238 |
\fB\-\-reduced\-redundancy\fR, \fB\-\-rr\fR |
239 | 239 |
Store object with 'Reduced redundancy'. Lower per-GB price. [put, cp, mv] |
... | ... |
@@ -245,22 +244,28 @@ Target prefix for access logs (S3 URI) (for [cfmodify] and [accesslog] commands) |
245 | 245 |
Disable access logging (for [cfmodify] and [accesslog] commands) |
246 | 246 |
.TP |
247 | 247 |
\fB\-\-default\-mime\-type\fR |
248 |
-Default MIME-type for stored objects. Application default is binary/octet-stream. |
|
248 |
+Default MIME-type for stored objects. Application default is binary/octet\-stream. |
|
249 | 249 |
.TP |
250 | 250 |
\fB\-M\fR, \fB\-\-guess\-mime\-type\fR |
251 |
-Guess MIME-type of files by their extension or mime magic. Fall back to default MIME-Type as specified by \fB--default-mime-type\fR option |
|
251 |
+Guess MIME-type of files by their extension or mime magic. Fall back to default MIME-type as specified by \fB\-\-default\-mime\-type\fR option |
|
252 | 252 |
.TP |
253 | 253 |
\fB\-\-no\-guess\-mime\-type\fR |
254 | 254 |
Don't guess MIME-type and use the default type instead. |
255 | 255 |
.TP |
256 |
+\fB\-\-no\-mime\-magic\fR |
|
257 |
+Don't use mime magic when guessing MIME-type. |
|
258 |
+.TP |
|
256 | 259 |
\fB\-m\fR MIME/TYPE, \fB\-\-mime\-type\fR=MIME/TYPE |
257 |
-Force MIME-type. Override both \fB--default-mime-type\fR and \fB--guess-mime-type\fR. |
|
260 |
+Force MIME-type. Override both \fB\-\-default\-mime\-type\fR and \fB\-\-guess\-mime\-type\fR. |
|
258 | 261 |
.TP |
259 | 262 |
\fB\-\-add\-header\fR=NAME:VALUE |
260 |
-Add a given HTTP header to the upload request. Can be used multiple times. For instance set 'Expires' or 'Cache-Control' headers (or both) using this options if you like. |
|
263 |
+Add a given HTTP header to the upload request. Can be used multiple times. For instance set 'Expires' or 'Cache\-Control' headers (or both) using this options if you like. |
|
261 | 264 |
.TP |
262 | 265 |
\fB\-\-encoding\fR=ENCODING |
263 |
-Override autodetected terminal and filesystem encoding (character set). Autodetected: UTF-8 |
|
266 |
+Override autodetected terminal and filesystem encoding (character set). Autodetected: UTF\-8 |
|
267 |
+.TP |
|
268 |
+\fB\-\-disable\-content\-encoding\fR |
|
269 |
+Don't include a Content-encoding header to the the uploaded objects. Default: Off |
|
264 | 270 |
.TP |
265 | 271 |
\fB\-\-add\-encoding\-exts\fR=EXTENSIONs |
266 | 272 |
Add encoding to these comma delimited extensions i.e. (css,js,html) when uploading to S3 ) |
... | ... |
@@ -269,11 +274,11 @@ Add encoding to these comma delimited extensions i.e. (css,js,html) when uploadi |
269 | 269 |
Use the S3 name as given on the command line. No pre-processing, encoding, etc. Use with caution! |
270 | 270 |
.TP |
271 | 271 |
\fB\-\-disable\-multipart\fR |
272 |
-Disable multipart upload on files bigger than --multipart-chunk-size-mb |
|
272 |
+Disable multipart upload on files bigger than \-\-multipart\-chunk\-size\-mb |
|
273 | 273 |
.TP |
274 | 274 |
\fB\-\-multipart\-chunk\-size\-mb\fR=SIZE |
275 | 275 |
Size of each chunk of a multipart upload. Files bigger than SIZE are automatically uploaded as multithreaded-multipart, smaller files are uploaded using the traditional method. SIZE is in Mega-Bytes, |
276 |
-default chunk size is noneMB, minimum allowed chunk size is 5MB, maximum is 5GB. |
|
276 |
+default chunk size is 15MB, minimum allowed chunk size is 5MB, maximum is 5GB. |
|
277 | 277 |
.TP |
278 | 278 |
\fB\-\-list\-md5\fR |
279 | 279 |
Include MD5 sums in bucket listings (only for 'ls' command). |
... | ... |
@@ -282,10 +287,10 @@ Include MD5 sums in bucket listings (only for 'ls' command). |
282 | 282 |
Print sizes in human readable form (eg 1kB instead of 1234). |
283 | 283 |
.TP |
284 | 284 |
\fB\-\-ws\-index\fR=WEBSITE_INDEX |
285 |
-Name of index-document (only for [ws-create] command) |
|
285 |
+Name of index-document (only for [ws\-create] command) |
|
286 | 286 |
.TP |
287 | 287 |
\fB\-\-ws\-error\fR=WEBSITE_ERROR |
288 |
-Name of error-document (only for [ws-create] command) |
|
288 |
+Name of error-document (only for [ws\-create] command) |
|
289 | 289 |
.TP |
290 | 290 |
\fB\-\-progress\fR |
291 | 291 |
Display progress meter (default on TTY). |
... | ... |
@@ -347,11 +352,11 @@ synchronising complete directory trees to or from remote S3 storage. To some ext |
347 | 347 |
.PP |
348 | 348 |
Basic usage common in backup scenarios is as simple as: |
349 | 349 |
.nf |
350 |
- s3cmd sync /local/path/ s3://test-bucket/backup/ |
|
350 |
+ s3cmd sync /local/path/ s3://test\-bucket/backup/ |
|
351 | 351 |
.fi |
352 | 352 |
.PP |
353 | 353 |
This command will find all files under /local/path directory and copy them |
354 |
-to corresponding paths under s3://test-bucket/backup on the remote side. |
|
354 |
+to corresponding paths under s3://test\-bucket/backup on the remote side. |
|
355 | 355 |
For example: |
356 | 356 |
.nf |
357 | 357 |
/local/path/\fBfile1.ext\fR \-> s3://bucket/backup/\fBfile1.ext\fR |
... | ... |
@@ -361,7 +366,7 @@ For example: |
361 | 361 |
However if the local path doesn't end with a slash the last directory's name |
362 | 362 |
is used on the remote side as well. Compare these with the previous example: |
363 | 363 |
.nf |
364 |
- s3cmd sync /local/path s3://test-bucket/backup/ |
|
364 |
+ s3cmd sync /local/path s3://test\-bucket/backup/ |
|
365 | 365 |
.fi |
366 | 366 |
will sync: |
367 | 367 |
.nf |
... | ... |
@@ -371,7 +376,7 @@ will sync: |
371 | 371 |
.PP |
372 | 372 |
To retrieve the files back from S3 use inverted syntax: |
373 | 373 |
.nf |
374 |
- s3cmd sync s3://test-bucket/backup/ /tmp/restore/ |
|
374 |
+ s3cmd sync s3://test\-bucket/backup/ /tmp/restore/ |
|
375 | 375 |
.fi |
376 | 376 |
that will download files: |
377 | 377 |
.nf |
... | ... |
@@ -382,7 +387,7 @@ that will download files: |
382 | 382 |
Without the trailing slash on source the behaviour is similar to |
383 | 383 |
what has been demonstrated with upload: |
384 | 384 |
.nf |
385 |
- s3cmd sync s3://test-bucket/backup /tmp/restore/ |
|
385 |
+ s3cmd sync s3://test\-bucket/backup /tmp/restore/ |
|
386 | 386 |
.fi |
387 | 387 |
will download the files as: |
388 | 388 |
.nf |
... | ... |
@@ -414,9 +419,14 @@ about matching file names against exclude and include rules. |
414 | 414 |
.PP |
415 | 415 |
For example to exclude all files with ".jpg" extension except those beginning with a number use: |
416 | 416 |
.PP |
417 |
- \-\-exclude '*.jpg' \-\-rinclude '[0-9].*\.jpg' |
|
417 |
+ \-\-exclude '*.jpg' \-\-rinclude '[0\-9].*\.jpg' |
|
418 |
+ |
|
419 |
+.SH ENVIRONMENT |
|
420 |
+.TP |
|
421 |
+.B TMP |
|
422 |
+Directory used to write temp files (/tmp by default) |
|
418 | 423 |
.SH SEE ALSO |
419 |
-For the most up to date list of options run |
|
424 |
+For the most up to date list of options run |
|
420 | 425 |
.B s3cmd \-\-help |
421 | 426 |
.br |
422 | 427 |
For more info about usage, examples and other related info visit project homepage at |
... | ... |
@@ -429,7 +439,7 @@ Please consider a donation if you have found s3cmd useful: |
429 | 429 |
.SH AUTHOR |
430 | 430 |
Written by Michal Ludvig <mludvig@logix.net.nz> and 15+ contributors |
431 | 431 |
.SH CONTACT, SUPPORT |
432 |
-Prefered way to get support is our mailing list: |
|
432 |
+Preferred way to get support is our mailing list: |
|
433 | 433 |
.I s3tools\-general@lists.sourceforge.net |
434 | 434 |
.SH REPORTING BUGS |
435 | 435 |
Report bugs to |