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