s3cmd
3cc025ae
 #!/usr/bin/env python
 
 ## Amazon S3 manager
 ## Author: Michal Ludvig <michal@logix.cz>
 ##         http://www.logix.cz/michal
 ## License: GPL Version 2
 
 import sys
 import logging
 import time
8a4a98b1
 import os
 import re
ac9940ec
 import errno
2d7d5543
 import glob
4a52baa8
 import traceback
4da602a5
 import codecs
315e527b
 import locale
3cc025ae
 
9b7618ae
 from copy import copy
f4555c39
 from optparse import OptionParser, Option, OptionValueError, IndentedHelpFormatter
3cc025ae
 from logging import debug, info, warning, error
49731b40
 from distutils.spawn import find_executable
3cc025ae
 
 def output(message):
82d9eafa
 	sys.stdout.write(message + "\n")
3cc025ae
 
7c07fd66
 def check_args_type(args, type, verbose_type):
 	for arg in args:
 		if S3Uri(arg).type != type:
 			raise ParameterError("Expecting %s instead of '%s'" % (verbose_type, arg))
 
b96ddebe
 def cmd_du(args):
 	s3 = S3(Config())
 	if len(args) > 0:
 		uri = S3Uri(args[0])
 		if uri.type == "s3" and uri.has_bucket():
 			subcmd_bucket_usage(s3, uri)
 			return
 	subcmd_bucket_usage_all(s3)
 
 def subcmd_bucket_usage_all(s3):
 	response = s3.list_all_buckets()
 
 	buckets_size = 0
 	for bucket in response["list"]:
 		size = subcmd_bucket_usage(s3, S3Uri("s3://" + bucket["Name"]))
 		if size != None:
 			buckets_size += size
 	total_size, size_coeff = formatSize(buckets_size, Config().human_readable_sizes)
475b5bc2
 	total_size_str = str(total_size) + size_coeff 
315e527b
 	output(u"".rjust(8, "-"))
 	output(u"%s Total" % (total_size_str.ljust(8)))
b96ddebe
 
 def subcmd_bucket_usage(s3, uri):
 	bucket = uri.bucket()
 	object = uri.object()
 
 	if object.endswith('*'):
 		object = object[:-1]
 	try:
416741b2
 		response = s3.bucket_list(bucket, prefix = object, recursive = True)
b96ddebe
 	except S3Error, e:
 		if S3.codes.has_key(e.Code):
 			error(S3.codes[e.Code] % bucket)
 			return
 		else:
 			raise
 	bucket_size = 0
 	for object in response["list"]:
 		size, size_coeff = formatSize(object["Size"], False)
 		bucket_size += size
 	total_size, size_coeff = formatSize(bucket_size, Config().human_readable_sizes)
475b5bc2
 	total_size_str = str(total_size) + size_coeff 
315e527b
 	output(u"%s %s" % (total_size_str.ljust(8), uri))
475b5bc2
 	return bucket_size
b96ddebe
 
9081133d
 def cmd_ls(args):
9b7618ae
 	s3 = S3(Config())
9081133d
 	if len(args) > 0:
b819c70c
 		uri = S3Uri(args[0])
 		if uri.type == "s3" and uri.has_bucket():
 			subcmd_bucket_list(s3, uri)
 			return
 	subcmd_buckets_list_all(s3)
3cc025ae
 
 def cmd_buckets_list_all_all(args):
9b7618ae
 	s3 = S3(Config())
b819c70c
 
3cc025ae
 	response = s3.list_all_buckets()
 
 	for bucket in response["list"]:
b819c70c
 		subcmd_bucket_list(s3, S3Uri("s3://" + bucket["Name"]))
315e527b
 		output(u"")
3cc025ae
 
 
b819c70c
 def subcmd_buckets_list_all(s3):
 	response = s3.list_all_buckets()
 	for bucket in response["list"]:
315e527b
 		output(u"%s  s3://%s" % (
b819c70c
 			formatDateTime(bucket["CreationDate"]),
 			bucket["Name"],
 			))
 
 def subcmd_bucket_list(s3, uri):
 	bucket = uri.bucket()
36cfce67
 	prefix = uri.object()
b819c70c
 
315e527b
 	debug(u"Bucket 's3://%s':" % bucket)
36cfce67
 	if prefix.endswith('*'):
 		prefix = prefix[:-1]
3cc025ae
 	try:
36cfce67
 		response = s3.bucket_list(bucket, prefix = prefix)
3cc025ae
 	except S3Error, e:
75405909
 		if S3.codes.has_key(e.info["Code"]):
 			error(S3.codes[e.info["Code"]] % bucket)
3cc025ae
 			return
 		else:
 			raise
36cfce67
 
 	for prefix in response['common_prefixes']:
315e527b
 		output(u"%s %s" % (
36cfce67
 			"D".rjust(28),
 			uri.compose_uri(bucket, prefix["Prefix"])))
 
3cc025ae
 	for object in response["list"]:
9b7618ae
 		size, size_coeff = formatSize(object["Size"], Config().human_readable_sizes)
315e527b
 		output(u"%s  %s%s  %s" % (
3cc025ae
 			formatDateTime(object["LastModified"]),
 			str(size).rjust(8), size_coeff.ljust(1),
ec50b5a7
 			uri.compose_uri(bucket, object["Key"]),
3cc025ae
 			))
 
 def cmd_bucket_create(args):
7406fc6c
 	s3 = S3(Config())
 	for arg in args:
 		uri = S3Uri(arg)
 		if not uri.type == "s3" or not uri.has_bucket() or uri.has_object():
 			raise ParameterError("Expecting S3 URI with just the bucket name set instead of '%s'" % arg)
 		try:
 			response = s3.bucket_create(uri.bucket(), cfg.bucket_location)
315e527b
 			output(u"Bucket '%s' created" % uri.uri())
7406fc6c
 		except S3Error, e:
 			if S3.codes.has_key(e.info["Code"]):
 				error(S3.codes[e.info["Code"]] % uri.bucket())
 				return
 			else:
 				raise
3cc025ae
 
 def cmd_bucket_delete(args):
7406fc6c
 	def _bucket_delete_one(uri):
 		try:
 			response = s3.bucket_delete(uri.bucket())
 		except S3Error, e:
 			if e.info['Code'] == 'BucketNotEmpty' and (cfg.force or cfg.recursive):
315e527b
 				warning(u"Bucket is not empty. Removing all the objects from it first. This may take some time...")
7406fc6c
 				subcmd_object_del_uri(uri, recursive = True)
 				return _bucket_delete_one(uri)
 			elif S3.codes.has_key(e.info["Code"]):
 				error(S3.codes[e.info["Code"]] % uri.bucket())
 				return
 			else:
 				raise
 		
 	s3 = S3(Config())
 	for arg in args:
 		uri = S3Uri(arg)
 		if not uri.type == "s3" or not uri.has_bucket() or uri.has_object():
 			raise ParameterError("Expecting S3 URI with just the bucket name set instead of '%s'" % arg)
 		_bucket_delete_one(uri)
315e527b
 		output(u"Bucket '%s' removed" % uri.uri())
f4555c39
 
90137a39
 def fetch_remote_keys(args):
 	remote_uris = []
 	remote_keys = []
 
 	for arg in args:
 		uri = S3Uri(arg)
 		if not uri.type == 's3':
 			raise ParameterError("Expecting S3 URI instead of '%s'" % arg)
 		remote_uris.append(uri)
 
 	if cfg.recursive:
 		for uri in remote_uris:
 			objectlist = _get_filelist_remote(uri)
 			for key in objectlist.iterkeys():
 				object = S3Uri(objectlist[key]['object_uri_str'])
 				## Remove leading '/' from remote filenames
 				if key.find("/") == 0:
 					key = key[1:]
 				download_item = {
 					'remote_uri' : object,
 					'key' : key
 				}
 				remote_keys.append(download_item)
 	else:
 		for uri in remote_uris:
 			uri_str = str(uri)
 			## Wildcards used in remote URI?
 			## If yes we'll need a bucket listing...
 			if uri_str.find('*') > -1 or uri_str.find('?') > -1:
 				first_wildcard = uri_str.find('*')
 				first_questionmark = uri_str.find('?')
 				if first_questionmark > -1 and first_questionmark < first_wildcard:
 					first_wildcard = first_questionmark
 				prefix = uri_str[:first_wildcard]
 				rest = uri_str[first_wildcard+1:]
 				## Only request recursive listing if the 'rest' of the URI,
 				## i.e. the part after first wildcard, contains '/'
 				need_recursion = rest.find('/') > -1
 				objectlist = _get_filelist_remote(S3Uri(prefix), recursive = need_recursion)
 				for key in objectlist:
 					## Check whether the 'key' matches the requested wildcards
 					if glob.fnmatch.fnmatch(objectlist[key]['object_uri_str'], uri_str):
 						download_item = {
 							'remote_uri' : S3Uri(objectlist[key]['object_uri_str']),
 							'key' : key,
 						}
 						remote_keys.append(download_item)
 			else:
 				## No wildcards - simply append the given URI to the list
 				key = os.path.basename(uri.object())
 				if not key:
 					raise ParameterError(u"Expecting S3 URI with a filename or --recursive: %s" % uri.uri())
 				download_item = {
 					'remote_uri' : uri,
 					'key' : key
 				}
 				remote_keys.append(download_item)
 	return remote_keys
 
3cc025ae
 def cmd_object_put(args):
9b7618ae
 	s3 = S3(Config())
3cc025ae
 
b819c70c
 	uri_arg = args.pop()
7c07fd66
 	check_args_type(args, 'file', 'filename')
3cc025ae
 
b819c70c
 	uri = S3Uri(uri_arg)
af3425b6
 	if uri.type != "s3":
b819c70c
 		raise ParameterError("Expecting S3 URI instead of '%s'" % uri_arg)
3cc025ae
 
7c07fd66
 	if len(args) > 1 and uri.object() != "" and not Config().force:
315e527b
 		error(u"When uploading multiple files the last argument must")
 		error(u"be a S3 URI specifying just the bucket name")
 		error(u"WITHOUT object name!")
 		error(u"Alternatively use --force argument and the specified")
 		error(u"object name will be prefixed to all stored filenames.")
1f7d2de3
 		sys.exit(1)
747ddb2a
 	
d9777ac6
 	seq = 0
 	total = len(args)
7c07fd66
 	for file in args:
d9777ac6
 		seq += 1
b819c70c
 		uri_arg_final = str(uri)
7c07fd66
 		if len(args) > 1 or uri.object() == "":
b819c70c
 			uri_arg_final += os.path.basename(file)
8ec1807f
 		
b819c70c
 		uri_final = S3Uri(uri_arg_final)
8ec1807f
 		extra_headers = {}
 		real_filename = file
688964d7
 		seq_label = "[%d of %d]" % (seq, total)
8ec1807f
 		if Config().encrypt:
 			exitcode, real_filename, extra_headers["x-amz-meta-s3tools-gpgenc"] = gpg_encrypt(file)
63ba9974
 		try:
688964d7
 			response = s3.object_put(real_filename, uri_final, extra_headers, extra_label = seq_label)
63ba9974
 		except S3UploadError, e:
315e527b
 			error(u"Upload of '%s' failed too many times. Skipping that file." % real_filename)
63ba9974
 			continue
451a19a2
 		except InvalidFileError, e:
315e527b
 			warning(u"File can not be uploaded: %s" % e)
451a19a2
 			continue
63ba9974
 		speed_fmt = formatSize(response["speed"], human_readable = True, floating_point = True)
4396d217
 		if not Config().progress_meter:
315e527b
 			output(u"File '%s' stored as %s (%d bytes in %0.1f seconds, %0.2f %sB/s) %s" %
4396d217
 				(file, uri_final, response["size"], response["elapsed"], speed_fmt[0], speed_fmt[1],
688964d7
 				seq_label))
72d9ddf5
 		if Config().acl_public:
315e527b
 			output(u"Public URL of the object is: %s" %
3fc8c43b
 				(uri_final.public_url()))
8ec1807f
 		if Config().encrypt and real_filename != file:
315e527b
 			debug(u"Removing temporary encrypted file: %s" % real_filename)
8ec1807f
 			os.remove(real_filename)
3cc025ae
 
 def cmd_object_get(args):
7c0863d5
 	cfg = Config()
 	s3 = S3(cfg)
 
 	## Check arguments:
 	## if not --recursive:
 	##   - first N arguments must be S3Uri
 	##   - if the last one is S3 make current dir the destination_base
 	##   - if the last one is a directory:
 	##       - take all 'basenames' of the remote objects and
 	##         make the destination name be 'destination_base'+'basename'
 	##   - if the last one is a file or not existing:
 	##       - if the number of sources (N, above) == 1 treat it
 	##         as a filename and save the object there.
 	##       - if there's more sources -> Error
 	## if --recursive:
 	##   - first N arguments must be S3Uri
 	##       - for each Uri get a list of remote objects with that Uri as a prefix
 	##       - apply exclude/include rules
 	##       - each list item will have MD5sum, Timestamp and pointer to S3Uri
 	##         used as a prefix.
 	##   - the last arg may be a local directory - destination_base
 	##   - if the last one is S3 make current dir the destination_base
 	##   - if the last one doesn't exist check remote list:
 	##       - if there is only one item and its_prefix==its_name 
 	##         download that item to the name given in last arg.
 	##       - if there are more remote items use the last arg as a destination_base
 	##         and try to create the directory (incl. all parents).
 	##
 	## In both cases we end up with a list mapping remote object names (keys) to local file names.
 
6f933653
 	## Each item will be a dict with the following attributes
 	# {'remote_uri', 'local_filename'}
559c963f
 	download_list = []
7c0863d5
 
 	if len(args) == 0:
 		raise ParameterError("Nothing to download. Expecting S3 URI.")
 
559c963f
 	if S3Uri(args[-1]).type == 'file':
7c0863d5
 		destination_base = args.pop()
 	else:
 		destination_base = "."
b819c70c
 
7c0863d5
 	if len(args) == 0:
 		raise ParameterError("Nothing to download. Expecting S3 URI.")
b819c70c
 
90137a39
 	remote_keys = fetch_remote_keys(args)
 	total_count = len(remote_keys)
7c07fd66
 
90137a39
 	if not os.path.isdir(destination_base) or destination_base == '-':
 		## We were either given a file name (existing or not) or want STDOUT
 		if total_count > 1:
 			raise ParameterError("Destination must be a directory when downloading multiple sources.")
 		remote_keys[0]['local_filename'] = destination_base
 	elif os.path.isdir(destination_base):
559c963f
 		if destination_base[-1] != os.path.sep:
 			destination_base += os.path.sep
90137a39
 		for key in remote_keys:
 			key['local_filename'] = destination_base + key['key']
7c0863d5
 	else:
90137a39
 		raise InternalError("WTF? Is it a dir or not? -- %s" % destination_base)
6f933653
 
 	seq = 0
7c0863d5
 	for item in remote_keys:
6f933653
 		seq += 1
7c0863d5
 		uri = item['remote_uri']
5f7a2d5f
 		## Encode / Decode destination with "replace" to make sure it's compatible with current encoding
 		destination = unicodise_safe(item['local_filename'])
6f933653
 		seq_label = "[%d of %d]" % (seq, total_count)
7c07fd66
 
9197e62e
 		start_position = 0
7c0863d5
 
ed27a45e
 		if destination == "-":
 			## stdout
82d9eafa
 			dst_stream = sys.__stdout__
ed27a45e
 		else:
 			## File
 			try:
7e3782c5
 				file_exists = os.path.exists(destination)
559c963f
 				try:
 					dst_stream = open(destination, "ab")
 				except IOError, e:
 					if e.errno == errno.ENOENT:
 						basename = destination[:destination.rindex(os.path.sep)]
a1e3fd9c
 						info(u"Creating directory: %s" % basename)
559c963f
 						os.makedirs(basename)
 						dst_stream = open(destination, "ab")
 					else:
 						raise
7e3782c5
 				if file_exists:
9197e62e
 					if Config().get_continue:
 						start_position = dst_stream.tell()
 					elif Config().force:
 						start_position = 0L
 						dst_stream.seek(0L)
 						dst_stream.truncate()
559c963f
 					elif Config().skip_existing:
a1e3fd9c
 						info(u"Skipping over existing file: %s" % (destination))
559c963f
 						continue
9197e62e
 					else:
7e3782c5
 						dst_stream.close()
a1e3fd9c
 						raise ParameterError(u"File %s already exists. Use either of --force / --continue / --skip-existing or give it a new name." % destination)
ed27a45e
 			except IOError, e:
315e527b
 				error(u"Skipping %s: %s" % (destination, e.strerror))
ed27a45e
 				continue
6f933653
 		response = s3.object_get(uri, dst_stream, start_position = start_position, extra_label = seq_label)
7c07fd66
 		if response["headers"].has_key("x-amz-meta-s3tools-gpgenc"):
 			gpg_decrypt(destination, response["headers"]["x-amz-meta-s3tools-gpgenc"])
 			response["size"] = os.stat(destination)[6]
4396d217
 		if not Config().progress_meter and destination != "-":
63ba9974
 			speed_fmt = formatSize(response["speed"], human_readable = True, floating_point = True)
315e527b
 			output(u"File %s saved as '%s' (%d bytes in %0.1f seconds, %0.2f %sB/s)" %
63ba9974
 				(uri, destination, response["size"], response["elapsed"], speed_fmt[0], speed_fmt[1]))
3cc025ae
 
 def cmd_object_del(args):
7c07fd66
 	while (len(args)):
 		uri_arg = args.pop(0)
 		uri = S3Uri(uri_arg)
a120a4eb
 		if uri.type != "s3":
7c07fd66
 			raise ParameterError("Expecting S3 URI instead of '%s'" % uri_arg)
a120a4eb
 		if not uri.has_object():
 			if Config().recursive and not Config().force:
 				raise ParameterError("Please use --force to delete ALL contents of %s" % uri)
 			elif not Config().recursive:
 				raise ParameterError("Object name required, not only the bucket name")
7406fc6c
 		subcmd_object_del_uri(uri)
 
 def subcmd_object_del_uri(uri, recursive = None):
 	s3 = S3(Config())
 	if recursive is None:
 		recursive = cfg.recursive
 	uri_list = []
 	if recursive:
 		filelist = _get_filelist_remote(uri)
 		uri_base = 's3://' + uri.bucket() + "/"
 		for idx in filelist:
 			object = filelist[idx]
315e527b
 			debug(u"Adding URI " + uri_base + object['object_key'])
7406fc6c
 			uri_list.append(S3Uri(uri_base + object['object_key']))
 	else:
 		uri_list.append(uri)
 	for _uri in uri_list:
 		response = s3.object_delete(_uri)
315e527b
 		output(u"Object %s deleted" % _uri)
3cc025ae
 
7d61be89
 def subcmd_cp_mv(args, process_fce, message):
7d0ac8ee
 	src_uri = S3Uri(args.pop(0))
 	dst_uri = S3Uri(args.pop(0))
 
 	if len(args): 
 		raise ParameterError("Too many parameters! Expected: %s" % commands['cp']['param']) 
 
 	if src_uri.type != "s3" or dst_uri.type != "s3": 
 		raise ParameterError("Parameters are not URIs! Expected: %s" % commands['cp']['param']) 
 
 	if dst_uri.object() == "":
 		dst_uri = S3Uri(dst_uri.uri() + src_uri.object())
  
7d61be89
 	response = process_fce(src_uri, dst_uri) 
 	output(message % { "src" : src_uri, "dst" : dst_uri})
7d0ac8ee
 	if Config().acl_public:
315e527b
 		output(u"Public URL is: %s" % dst_uri.public_url())
7d0ac8ee
 
7d61be89
 def cmd_cp(args):
 	s3 = S3(Config())
 	subcmd_cp_mv(args, s3.object_copy, "Object %(src)s copied to %(dst)s")
 
 def cmd_mv(args):
 	s3 = S3(Config())
 	subcmd_cp_mv(args, s3.object_move, "Object %(src)s moved to %(dst)s")
 
e5c6f6c5
 def cmd_info(args):
 	s3 = S3(Config())
 
 	while (len(args)):
 		uri_arg = args.pop(0)
 		uri = S3Uri(uri_arg)
 		if uri.type != "s3" or not uri.has_bucket():
 			raise ParameterError("Expecting S3 URI instead of '%s'" % uri_arg)
 
 		try:
 			if uri.has_object():
 				info = s3.object_info(uri)
315e527b
 				output(u"%s (object):" % uri.uri())
 				output(u"   File size: %s" % info['headers']['content-length'])
 				output(u"   Last mod:  %s" % info['headers']['last-modified'])
 				output(u"   MIME type: %s" % info['headers']['content-type'])
 				output(u"   MD5 sum:   %s" % info['headers']['etag'].strip('"'))
e5c6f6c5
 			else:
 				info = s3.bucket_info(uri)
315e527b
 				output(u"%s (bucket):" % uri.uri())
 				output(u"   Location:  %s" % info['bucket-location'])
e5c6f6c5
 			acl = s3.get_acl(uri)
90137a39
 			acl_list = acl.getGrantList()
 			for user in acl_list:
 				output(u"   ACL:       %s: %s" % (user, acl_list[user]))
 			if acl.isAnonRead():
 				output(u"   URL:       %s" % uri.public_url())
e5c6f6c5
 		except S3Error, e:
 			if S3.codes.has_key(e.info["Code"]):
 				error(S3.codes[e.info["Code"]] % uri.bucket())
 				return
 			else:
 				raise
 
01fe3a25
 def _get_filelist_local(local_uri):
a1e3fd9c
 	info(u"Compiling list of local files...")
5f7a2d5f
 	local_path = deunicodise(local_uri.path())
01fe3a25
 	if os.path.isdir(local_path):
 		loc_base = os.path.join(local_path, "")
 		filelist = os.walk(local_path)
a368faf1
 	else:
9856527a
 		loc_base = "." + os.path.sep
01fe3a25
 		filelist = [( '.', [], [local_path] )]
0d91ff3f
 	loc_base_len = len(loc_base)
 	loc_list = {}
a368faf1
 	for root, dirs, files in filelist:
0d91ff3f
 		## TODO: implement explicit exclude
 		for f in files:
 			full_name = os.path.join(root, f)
 			if not os.path.isfile(full_name):
 				continue
d9777ac6
 			if os.path.islink(full_name):
 				## Synchronize symlinks... one day
 				## for now skip over
 				continue
5f7a2d5f
 			file = unicodise(full_name[loc_base_len:])
0d91ff3f
 			sr = os.stat_result(os.lstat(full_name))
 			loc_list[file] = {
5f7a2d5f
 				'full_name_unicoded' : unicodise(full_name),
0d91ff3f
 				'full_name' : full_name,
 				'size' : sr.st_size, 
 				'mtime' : sr.st_mtime,
 				## TODO: Possibly more to save here...
 			}
01fe3a25
 	return loc_list
 
6f933653
 def _get_filelist_remote(remote_uri, recursive = True):
 	## If remote_uri ends with '/' then all remote files will have 
 	## the remote_uri prefix removed in the relative path.
 	## If, on the other hand, the remote_uri ends with something else
 	## (probably alphanumeric symbol) we'll use the last path part 
 	## in the relative path.
 	##
 	## Complicated, eh? See an example:
 	## _get_filelist_remote("s3://bckt/abc/def") may yield:
 	## { 'def/file1.jpg' : {}, 'def/xyz/blah.txt' : {} }
 	## _get_filelist_remote("s3://bckt/abc/def/") will yield:
 	## { 'file1.jpg' : {}, 'xyz/blah.txt' : {} }
 	## Furthermore a prefix-magic can restrict the return list:
 	## _get_filelist_remote("s3://bckt/abc/def/x") yields:
 	## { 'xyz/blah.txt' : {} }
 
a1e3fd9c
 	info(u"Retrieving list of remote files for %s ..." % remote_uri)
0d91ff3f
 
01fe3a25
 	s3 = S3(Config())
6f933653
 	response = s3.bucket_list(remote_uri.bucket(), prefix = remote_uri.object(), recursive = recursive)
01fe3a25
 
6f933653
 	rem_base_original = rem_base = remote_uri.object()
 	remote_uri_original = remote_uri
 	if rem_base != '' and rem_base[-1] != '/':
 		rem_base = rem_base[:rem_base.rfind('/')+1]
 		remote_uri = S3Uri("s3://%s/%s" % (remote_uri.bucket(), rem_base))
0d91ff3f
 	rem_base_len = len(rem_base)
 	rem_list = {}
559c963f
 	break_now = False
0d91ff3f
 	for object in response['list']:
6f933653
 		if object['Key'] == rem_base_original and object['Key'][-1] != os.path.sep:
559c963f
 			## We asked for one file and we got that file :-)
 			key = os.path.basename(object['Key'])
6f933653
 			object_uri_str = remote_uri_original.uri()
559c963f
 			break_now = True
 			rem_list = {}	## Remove whatever has already been put to rem_list
 		else:
 			key = object['Key'][rem_base_len:]		## Beware - this may be '' if object['Key']==rem_base !!
 			object_uri_str = remote_uri.uri() + key
0d91ff3f
 		rem_list[key] = { 
 			'size' : int(object['Size']),
7c0863d5
 			'timestamp' : dateS3toUnix(object['LastModified']), ## Sadly it's upload time, not our lastmod time :-(
0d91ff3f
 			'md5' : object['ETag'][1:-1],
7c0863d5
 			'object_key' : object['Key'],
559c963f
 			'object_uri_str' : object_uri_str,
 			'base_uri' : remote_uri,
0d91ff3f
 		}
559c963f
 		if break_now:
 			break
01fe3a25
 	return rem_list
7c0863d5
 
01fe3a25
 def _compare_filelists(src_list, dst_list, src_is_local_and_dst_is_remote):
a1e3fd9c
 	info(u"Verifying checksums...")
d5e87cdf
 	cfg = Config()
01fe3a25
 	exists_list = {}
8829e891
 	exclude_list = {}
d5e87cdf
 	if cfg.debug_syncmatch:
 		logging.root.setLevel(logging.DEBUG)
01fe3a25
 	for file in src_list.keys():
d5e87cdf
 		if not cfg.debug_syncmatch:
315e527b
 			debug(u"CHECK: %s" % (os.sep + file))
8829e891
 		excluded = False
d5e87cdf
 		for r in cfg.exclude:
8829e891
 			## all paths start with '/' from the base dir
 			if r.search(os.sep + file):
 				## Can't directly 'continue' to the outer loop
 				## therefore this awkward excluded switch :-(
 				excluded = True
d5e87cdf
 				if cfg.debug_syncmatch:
315e527b
 					debug(u"EXCL: %s" % (os.sep + file))
 					debug(u"RULE: '%s'" % (cfg.debug_exclude[r]))
d5e87cdf
 				else:
a1e3fd9c
 					info(u"%s: excluded" % file)
8829e891
 				break
 		if excluded:
 			exclude_list = src_list[file]
 			del(src_list[file])
 			continue
d5e87cdf
 		else:
315e527b
 			debug(u"PASS: %s" % (os.sep + file))
01fe3a25
 		if dst_list.has_key(file):
559c963f
 			## Was --skip-existing requested?
 			if cfg.skip_existing:
315e527b
 				debug(u"IGNR: %s (used --skip-existing)" % (file))
559c963f
 				exists_list[file] = src_list[file]
 				del(src_list[file])
 				## Remove from destination-list, all that is left there will be deleted
 				del(dst_list[file])
 				continue
 
0d91ff3f
 			## Check size first
01fe3a25
 			if dst_list[file]['size'] == src_list[file]['size']:
315e527b
 				#debug(u"%s same size: %s" % (file, dst_list[file]['size']))
0d91ff3f
 				## ... same size, check MD5
01fe3a25
 				if src_is_local_and_dst_is_remote:
 					src_md5 = Utils.hash_file_md5(src_list[file]['full_name'])
 					dst_md5 = dst_list[file]['md5']
 				else:
 					src_md5 = src_list[file]['md5']
 					dst_md5 = Utils.hash_file_md5(dst_list[file]['full_name'])
 				if src_md5 == dst_md5:
315e527b
 					#debug(u"%s md5 matches: %s" % (file, dst_md5))
0d91ff3f
 					## Checksums are the same.
01fe3a25
 					## Remove from source-list, all that is left there will be transferred
315e527b
 					debug(u"IGNR: %s (transfer not needed: MD5 OK, Size OK)" % file)
01fe3a25
 					exists_list[file] = src_list[file]
 					del(src_list[file])
0d91ff3f
 				else:
315e527b
 					debug(u"XFER: %s (md5 mismatch: src=%s dst=%s)" % (file, src_md5, dst_md5))
0d91ff3f
 			else:
315e527b
 				debug(u"XFER: %s (size mismatch: src=%s dst=%s)" % (file, src_list[file]['size'], dst_list[file]['size']))
0d91ff3f
                         
01fe3a25
 			## Remove from destination-list, all that is left there will be deleted
315e527b
 			#debug(u"%s removed from destination list" % file)
01fe3a25
 			del(dst_list[file])
d5e87cdf
 	if cfg.debug_syncmatch:
315e527b
 		warning(u"Exiting because of --debug-syncmatch")
d5e87cdf
 		sys.exit(0)
 
8829e891
 	return src_list, dst_list, exists_list, exclude_list
01fe3a25
 
 def cmd_sync_remote2local(src, dst):
ed27a45e
 	def _parse_attrs_header(attrs_header):
 		attrs = {}
 		for attr in attrs_header.split("/"):
 			key, val = attr.split(":")
 			attrs[key] = val
 		return attrs
 		
 	s3 = S3(Config())
 
 	src_uri = S3Uri(src)
 	dst_uri = S3Uri(dst)
 
35920829
 	src_base = src_uri.uri()
 	dst_base = dst_uri.path()
 	if not src_base[-1] == "/": src_base += "/"
 
ed27a45e
 	rem_list = _get_filelist_remote(src_uri)
 	rem_count = len(rem_list)
 
 	loc_list = _get_filelist_local(dst_uri)
 	loc_count = len(loc_list)
 	
a1e3fd9c
 	info(u"Found %d remote files, %d local files" % (rem_count, loc_count))
ed27a45e
 
 	_compare_filelists(rem_list, loc_list, False)
 
a1e3fd9c
 	info(u"Summary: %d remote files to download, %d local files to delete" % (len(rem_list), len(loc_list)))
ed27a45e
 
 	for file in loc_list:
 		if cfg.delete_removed:
35920829
 			os.unlink(dst_base + file)
315e527b
 			output(u"deleted '%s'" % (dst_base + file))
ed27a45e
 		else:
315e527b
 			output(u"not-deleted '%s'" % file)
ed27a45e
 
 	total_size = 0
 	total_count = len(rem_list)
 	total_elapsed = 0.0
 	timestamp_start = time.time()
 	seq = 0
 	dir_cache = {}
 	file_list = rem_list.keys()
 	file_list.sort()
 	for file in file_list:
 		seq += 1
 		uri = S3Uri(src_base + file)
 		dst_file = dst_base + file
688964d7
 		seq_label = "[%d of %d]" % (seq, total_count)
ed27a45e
 		try:
 			dst_dir = os.path.dirname(dst_file)
 			if not dir_cache.has_key(dst_dir):
bc4c306d
 				dir_cache[dst_dir] = Utils.mkdir_with_parents(dst_dir)
ed27a45e
 			if dir_cache[dst_dir] == False:
315e527b
 				warning(u"%s: destination directory not writable: %s" % (file, dst_dir))
ed27a45e
 				continue
 			try:
 				open_flags = os.O_CREAT
 				if cfg.force:
 					open_flags |= os.O_TRUNC
 				else:
 					open_flags |= os.O_EXCL
 
315e527b
 				debug(u"dst_file=%s" % dst_file)
ed27a45e
 				# This will have failed should the file exist
0a38dc64
 				os.close(os.open(dst_file, open_flags))
ed27a45e
 				# Yeah I know there is a race condition here. Sadly I don't know how to open() in exclusive mode.
 				dst_stream = open(dst_file, "wb")
688964d7
 				response = s3.object_get(uri, dst_stream, extra_label = seq_label)
ed27a45e
 				dst_stream.close()
 				if response['headers'].has_key('x-amz-meta-s3cmd-attrs') and cfg.preserve_attrs:
 					attrs = _parse_attrs_header(response['headers']['x-amz-meta-s3cmd-attrs'])
 					if attrs.has_key('mode'):
 						os.chmod(dst_file, int(attrs['mode']))
bc4c306d
 					if attrs.has_key('mtime') or attrs.has_key('atime'):
 						mtime = attrs.has_key('mtime') and int(attrs['mtime']) or int(time.time())
 						atime = attrs.has_key('atime') and int(attrs['atime']) or int(time.time())
 						os.utime(dst_file, (atime, mtime))
 					## FIXME: uid/gid / uname/gname handling comes here! TODO
ed27a45e
 			except OSError, e:
d5e87cdf
 				try: dst_stream.close()
 				except: pass
ed27a45e
 				if e.errno == errno.EEXIST:
315e527b
 					warning(u"%s exists - not overwriting" % (dst_file))
ed27a45e
 					continue
bc4c306d
 				if e.errno in (errno.EPERM, errno.EACCES):
315e527b
 					warning(u"%s not writable: %s" % (dst_file, e.strerror))
bc4c306d
 					continue
d412a82b
 				raise e
d5e87cdf
 			except KeyboardInterrupt:
 				try: dst_stream.close()
 				except: pass
315e527b
 				warning(u"Exiting after keyboard interrupt")
d5e87cdf
 				return
bc4c306d
 			except Exception, e:
d5e87cdf
 				try: dst_stream.close()
 				except: pass
315e527b
 				error(u"%s: %s" % (file, e))
ed27a45e
 				continue
4f209fa4
 			# We have to keep repeating this call because 
 			# Python 2.4 doesn't support try/except/finally
 			# construction :-(
d5e87cdf
 			try: dst_stream.close()
 			except: pass
ed27a45e
 		except S3DownloadError, e:
315e527b
 			error(u"%s: download failed too many times. Skipping that file." % file)
ed27a45e
 			continue
 		speed_fmt = formatSize(response["speed"], human_readable = True, floating_point = True)
4396d217
 		if not Config().progress_meter:
315e527b
 			output(u"File '%s' stored as %s (%d bytes in %0.1f seconds, %0.2f %sB/s) %s" %
4396d217
 				(uri, dst_file, response["size"], response["elapsed"], speed_fmt[0], speed_fmt[1],
688964d7
 				seq_label))
ed27a45e
 		total_size += response["size"]
 
 	total_elapsed = time.time() - timestamp_start
 	speed_fmt = formatSize(total_size/total_elapsed, human_readable = True, floating_point = True)
89a93383
 
 	# Only print out the result if any work has been done or 
 	# if the user asked for verbose output
 	outstr = "Done. Downloaded %d bytes in %0.1f seconds, %0.2f %sB/s" % (total_size, total_elapsed, speed_fmt[0], speed_fmt[1])
 	if total_size > 0:
 		output(outstr)
 	else:
 		info(outstr)
01fe3a25
 
 def cmd_sync_local2remote(src, dst):
db340c09
 	def _build_attr_header(src):
9856527a
 		import pwd, grp
01fe3a25
 		attrs = {}
5f7a2d5f
 		src = deunicodise(src)
01fe3a25
 		st = os.stat_result(os.stat(src))
 		for attr in cfg.preserve_attrs_list:
 			if attr == 'uname':
 				try:
 					val = pwd.getpwuid(st.st_uid).pw_name
 				except KeyError:
 					attr = "uid"
 					val = st.st_uid
6fa688fa
 					warning(u"%s: Owner username not known. Storing UID=%d instead." % (unicodise(src), val))
01fe3a25
 			elif attr == 'gname':
 				try:
 					val = grp.getgrgid(st.st_gid).gr_name
 				except KeyError:
 					attr = "gid"
 					val = st.st_gid
6fa688fa
 					warning(u"%s: Owner groupname not known. Storing GID=%d instead." % (unicodise(src), val))
01fe3a25
 			else:
 				val = getattr(st, 'st_' + attr)
 			attrs[attr] = val
 		result = ""
 		for k in attrs: result += "%s:%s/" % (k, attrs[k])
 		return { 'x-amz-meta-s3cmd-attrs' : result[:-1] }
 
ec00bb88
 	s3 = S3(cfg)
 
 	if cfg.encrypt:
315e527b
 		error(u"S3cmd 'sync' doesn't support GPG encryption, sorry.")
 		error(u"Either use unconditional 's3cmd put --recursive'")
 		error(u"or disable encryption with --no-encrypt parameter.")
ec00bb88
 		sys.exit(1)
 
01fe3a25
 
 	src_uri = S3Uri(src)
 	dst_uri = S3Uri(dst)
 
 	loc_list = _get_filelist_local(src_uri)
 	loc_count = len(loc_list)
 	
 	rem_list = _get_filelist_remote(dst_uri)
 	rem_count = len(rem_list)
 
a1e3fd9c
 	info(u"Found %d local files, %d remote files" % (loc_count, rem_count))
01fe3a25
 
 	_compare_filelists(loc_list, rem_list, True)
0d91ff3f
 
a1e3fd9c
 	info(u"Summary: %d local files to upload, %d remote files to delete" % (len(loc_list), len(rem_list)))
ed27a45e
 
d9777ac6
 	for file in rem_list:
 		uri = S3Uri("s3://" + dst_uri.bucket()+"/"+rem_list[file]['object_key'])
 		if cfg.delete_removed:
fc8a5df8
 			response = s3.object_delete(uri)
315e527b
 			output(u"deleted '%s'" % uri)
d9777ac6
 		else:
315e527b
 			output(u"not-deleted '%s'" % uri)
0d91ff3f
 
 	total_size = 0
d9777ac6
 	total_count = len(loc_list)
63ba9974
 	total_elapsed = 0.0
 	timestamp_start = time.time()
d9777ac6
 	seq = 0
0d91ff3f
 	dst_base = dst_uri.uri()
 	if not dst_base[-1] == "/": dst_base += "/"
 	file_list = loc_list.keys()
 	file_list.sort()
 	for file in file_list:
d9777ac6
 		seq += 1
5f7a2d5f
 		src = loc_list[file]
0d91ff3f
 		uri = S3Uri(dst_base + file)
688964d7
 		seq_label = "[%d of %d]" % (seq, total_count)
9856527a
 		attr_header = None
a368faf1
 		if cfg.preserve_attrs:
5f7a2d5f
 			attr_header = _build_attr_header(src['full_name'])
a368faf1
 			debug(attr_header)
63ba9974
 		try:
5f7a2d5f
 			response = s3.object_put(src['full_name'], uri, attr_header, extra_label = seq_label)
63ba9974
 		except S3UploadError, e:
5f7a2d5f
 			error(u"%s: upload failed too many times. Skipping that file." % src['full_name_unicode'])
63ba9974
 			continue
451a19a2
 		except InvalidFileError, e:
315e527b
 			warning(u"File can not be uploaded: %s" % e)
451a19a2
 			continue
63ba9974
 		speed_fmt = formatSize(response["speed"], human_readable = True, floating_point = True)
4396d217
 		if not cfg.progress_meter:
315e527b
 			output(u"File '%s' stored as %s (%d bytes in %0.1f seconds, %0.2f %sB/s) %s" %
4396d217
 				(src, uri, response["size"], response["elapsed"], speed_fmt[0], speed_fmt[1],
688964d7
 				seq_label))
0d91ff3f
 		total_size += response["size"]
63ba9974
 
 	total_elapsed = time.time() - timestamp_start
b5f1867d
 	total_speed = total_elapsed and total_size/total_elapsed or 0.0
 	speed_fmt = formatSize(total_speed, human_readable = True, floating_point = True)
89a93383
 
 	# Only print out the result if any work has been done or 
 	# if the user asked for verbose output
 	outstr = "Done. Uploaded %d bytes in %0.1f seconds, %0.2f %sB/s" % (total_size, total_elapsed, speed_fmt[0], speed_fmt[1])
 	if total_size > 0:
 		output(outstr)
 	else:
 		info(outstr)
0d91ff3f
 
01fe3a25
 def cmd_sync(args):
 	src = args.pop(0)
 	dst = args.pop(0)
 	if (len(args)):
 		raise ParameterError("Too many parameters! Expected: %s" % commands['sync']['param'])
 
ed27a45e
 	if S3Uri(src).type == "s3" and not src.endswith('/'):
 		src += "/"
 
01fe3a25
 	if not dst.endswith('/'):
 		dst += "/"
 
 	if S3Uri(src).type == "file" and S3Uri(dst).type == "s3":
 		return cmd_sync_local2remote(src, dst)
 	if S3Uri(src).type == "s3" and S3Uri(dst).type == "file":
 		return cmd_sync_remote2local(src, dst)
585c735a
 
 def cmd_setacl(args):
 	s3 = S3(cfg)
 
 	set_to_acl = cfg.acl_public and "Public" or "Private"
 
 	remote_keys = fetch_remote_keys(args)
 	total_keys = len(remote_keys)
 	seq = 0
 	for key in remote_keys:
 		seq += 1
 		seq_label = "[%d of %d]" % (seq, total_keys)
 		uri = key['remote_uri']
 		acl = s3.get_acl(uri)
e3244a8c
 		debug(u"acl: %s - %r" % (uri, acl.grantees))
585c735a
 		if cfg.acl_public:
 			if acl.isAnonRead():
e3244a8c
 				info(u"%s: already Public, skipping %s" % (uri, seq_label))
585c735a
 				continue
 			acl.grantAnonRead()
 		else:
 			if not acl.isAnonRead():
 				info(u"%s: already Private, skipping %s" % (uri, seq_label))
 				continue
 			acl.revokeAnonRead()
 		retsponse = s3.set_acl(uri, acl)
 		if retsponse['status'] == 200:
 			output(u"%s: ACL set to %s  %s" % (uri, set_to_acl, seq_label))
 
8ec1807f
 def resolve_list(lst, args):
 	retval = []
 	for item in lst:
 		retval.append(item % args)
 	return retval
 
 def gpg_command(command, passphrase = ""):
 	p_in, p_out = os.popen4(command)
 	if command.count("--passphrase-fd"):
 		p_in.write(passphrase+"\n")
 		p_in.flush()
 	for line in p_out:
 		info(line.strip())
 	p_pid, p_exitcode = os.wait()
 	return p_exitcode
 
 def gpg_encrypt(filename):
 	tmp_filename = Utils.mktmpfile()
 	args = {
 		"gpg_command" : cfg.gpg_command,
 		"passphrase_fd" : "0",
 		"input_file" : filename, 
 		"output_file" : tmp_filename,
 	}
a1e3fd9c
 	info(u"Encrypting file %(input_file)s to %(output_file)s..." % args)
8ec1807f
 	command = resolve_list(cfg.gpg_encrypt.split(" "), args)
 	code = gpg_command(command, cfg.gpg_passphrase)
 	return (code, tmp_filename, "gpg")
 
49731b40
 def gpg_decrypt(filename, gpgenc_header = "", in_place = True):
8ec1807f
 	tmp_filename = Utils.mktmpfile(filename)
 	args = {
 		"gpg_command" : cfg.gpg_command,
 		"passphrase_fd" : "0",
 		"input_file" : filename, 
 		"output_file" : tmp_filename,
 	}
a1e3fd9c
 	info(u"Decrypting file %(input_file)s to %(output_file)s..." % args)
8ec1807f
 	command = resolve_list(cfg.gpg_decrypt.split(" "), args)
 	code = gpg_command(command, cfg.gpg_passphrase)
49731b40
 	if code == 0 and in_place:
315e527b
 		debug(u"Renaming %s to %s" % (tmp_filename, filename))
8ec1807f
 		os.unlink(filename)
 		os.rename(tmp_filename, filename)
49731b40
 		tmp_filename = filename
 	return (code, tmp_filename)
8ec1807f
 
5a736f08
 def run_configure(config_file):
 	cfg = Config()
 	options = [
796e95db
 		("access_key", "Access Key", "Access key and Secret key are your identifiers for Amazon S3"),
5a736f08
 		("secret_key", "Secret Key"),
49731b40
 		("gpg_passphrase", "Encryption password", "Encryption password is used to protect your files from reading\nby unauthorized persons while in transfer to S3"),
 		("gpg_command", "Path to GPG program"),
d35b41f4
 		("use_https", "Use HTTPS protocol", "When using secure HTTPS protocol all communication with Amazon S3\nservers is protected from 3rd party eavesdropping. This method is\nslower than plain HTTP and can't be used if you're behind a proxy"),
8a4a98b1
 		("proxy_host", "HTTP Proxy server name", "On some networks all internet access must go through a HTTP proxy.\nTry setting it here if you can't conect to S3 directly"),
 		("proxy_port", "HTTP Proxy server port"),
5a736f08
 		]
8a4a98b1
 	## Option-specfic defaults
49731b40
 	if getattr(cfg, "gpg_command") == "":
 		setattr(cfg, "gpg_command", find_executable("gpg"))
 
8a4a98b1
 	if getattr(cfg, "proxy_host") == "" and os.getenv("http_proxy"):
 		re_match=re.match("(http://)?([^:]+):(\d+)", os.getenv("http_proxy"))
 		if re_match:
 			setattr(cfg, "proxy_host", re_match.groups()[1])
 			setattr(cfg, "proxy_port", re_match.groups()[2])
 
5a736f08
 	try:
 		while 1:
315e527b
 			output(u"\nEnter new values or accept defaults in brackets with Enter.")
 			output(u"Refer to user manual for detailed description of all options.")
5a736f08
 			for option in options:
 				prompt = option[1]
d35b41f4
 				## Option-specific handling
 				if option[0] == 'proxy_host' and getattr(cfg, 'use_https') == True:
 					setattr(cfg, option[0], "")
 					continue
 				if option[0] == 'proxy_port' and getattr(cfg, 'proxy_host') == "":
 					setattr(cfg, option[0], 0)
 					continue
 
5a736f08
 				try:
 					val = getattr(cfg, option[0])
d35b41f4
 					if type(val) is bool:
 						val = val and "Yes" or "No"
5a736f08
 					if val not in (None, ""):
 						prompt += " [%s]" % val
 				except AttributeError:
 					pass
 
 				if len(option) >= 3:
315e527b
 					output(u"\n%s" % option[2])
5a736f08
 
 				val = raw_input(prompt + ": ")
 				if val != "":
d35b41f4
 					if type(getattr(cfg, option[0])) is bool:
 						# Turn 'Yes' into True, everything else into False
 						val = val.lower().startswith('y')
5a736f08
 					setattr(cfg, option[0], val)
315e527b
 			output(u"\nNew settings:")
5a736f08
 			for option in options:
315e527b
 				output(u"  %s: %s" % (option[1], getattr(cfg, option[0])))
18485e25
 			val = raw_input("\nTest access with supplied credentials? [Y/n] ")
 			if val.lower().startswith("y") or val == "":
 				try:
315e527b
 					output(u"Please wait...")
18485e25
 					S3(Config()).bucket_list("", "")
315e527b
 					output(u"Success. Your access key and secret key worked fine :-)")
49731b40
 
315e527b
 					output(u"\nNow verifying that encryption works...")
aa1c976f
 					if not getattr(cfg, "gpg_command") or not getattr(cfg, "gpg_passphrase"):
315e527b
 						output(u"Not configured. Never mind.")
49731b40
 					else:
aa1c976f
 						if not getattr(cfg, "gpg_command"):
 							raise Exception("Path to GPG program not set")
 						if not os.path.isfile(getattr(cfg, "gpg_command")):
 							raise Exception("GPG program not found")
 						filename = Utils.mktmpfile()
 						f = open(filename, "w")
 						f.write(os.sys.copyright)
 						f.close()
 						ret_enc = gpg_encrypt(filename)
 						ret_dec = gpg_decrypt(ret_enc[1], ret_enc[2], False)
 						hash = [
 							Utils.hash_file_md5(filename),
 							Utils.hash_file_md5(ret_enc[1]),
 							Utils.hash_file_md5(ret_dec[1]),
 						]
 						os.unlink(filename)
 						os.unlink(ret_enc[1])
 						os.unlink(ret_dec[1])
 						if hash[0] == hash[2] and hash[0] != hash[1]:
 							output ("Success. Encryption and decryption worked fine :-)") 
 						else:
 							raise Exception("Encryption verification error.")
49731b40
 
 				except Exception, e:
315e527b
 					error(u"Test failed: %s" % (e))
18485e25
 					val = raw_input("\nRetry configuration? [Y/n] ")
 					if val.lower().startswith("y") or val == "":
 						continue
49731b40
 					
18485e25
 
 			val = raw_input("\nSave settings? [y/N] ")
 			if val.lower().startswith("y"):
5a736f08
 				break
18485e25
 			val = raw_input("Retry configuration? [Y/n] ")
 			if val.lower().startswith("n"):
 				raise EOFError()
ac9940ec
 
 		## Overwrite existing config file, make it user-readable only
 		old_mask = os.umask(0077)
 		try:
 			os.remove(config_file)
 		except OSError, e:
 			if e.errno != errno.ENOENT:
 				raise
5a736f08
 		f = open(config_file, "w")
ac9940ec
 		os.umask(old_mask)
5a736f08
 		cfg.dump_config(f)
 		f.close()
315e527b
 		output(u"Configuration saved to '%s'" % config_file)
5a736f08
 
 	except (EOFError, KeyboardInterrupt):
315e527b
 		output(u"\nConfiguration aborted. Changes were NOT saved.")
5a736f08
 		return
 	
 	except IOError, e:
315e527b
 		error(u"Writing config file failed: %s: %s" % (config_file, e.strerror))
1f7d2de3
 		sys.exit(1)
5a736f08
 
2d7d5543
 def process_exclude_from_file(exf, exclude_array):
cb303737
 	try:
 		exfi = open(exf, "rt")
 	except IOError, e:
 		error(e)
 		sys.exit(1)
2d7d5543
 	for ex in exfi:
 		ex = ex.strip()
 		if re.match("^#", ex) or re.match("^\s*$", ex):
 			continue
315e527b
 		debug(u"adding rule: %s" % ex)
2d7d5543
 		exclude_array.append(ex)
 
5a736f08
 commands = {}
 commands_list = [
 	{"cmd":"mb", "label":"Make bucket", "param":"s3://BUCKET", "func":cmd_bucket_create, "argc":1},
 	{"cmd":"rb", "label":"Remove bucket", "param":"s3://BUCKET", "func":cmd_bucket_delete, "argc":1},
 	{"cmd":"ls", "label":"List objects or buckets", "param":"[s3://BUCKET[/PREFIX]]", "func":cmd_ls, "argc":0},
 	{"cmd":"la", "label":"List all object in all buckets", "param":"", "func":cmd_buckets_list_all_all, "argc":0},
 	{"cmd":"put", "label":"Put file into bucket", "param":"FILE [FILE...] s3://BUCKET[/PREFIX]", "func":cmd_object_put, "argc":2},
 	{"cmd":"get", "label":"Get file from bucket", "param":"s3://BUCKET/OBJECT LOCAL_FILE", "func":cmd_object_get, "argc":1},
 	{"cmd":"del", "label":"Delete file from bucket", "param":"s3://BUCKET/OBJECT", "func":cmd_object_del, "argc":1},
0d91ff3f
 	#{"cmd":"mkdir", "label":"Make a virtual S3 directory", "param":"s3://BUCKET/path/to/dir", "func":cmd_mkdir, "argc":1},
29893afc
 	{"cmd":"sync", "label":"Synchronize a directory tree to S3", "param":"LOCAL_DIR s3://BUCKET[/PREFIX] or s3://BUCKET[/PREFIX] LOCAL_DIR", "func":cmd_sync, "argc":2},
f298b348
 	{"cmd":"du", "label":"Disk usage by buckets", "param":"[s3://BUCKET[/PREFIX]]", "func":cmd_du, "argc":0},
7393bdba
 	{"cmd":"info", "label":"Get various information about Buckets or Objects", "param":"s3://BUCKET[/OBJECT]", "func":cmd_info, "argc":1},
7d0ac8ee
 	{"cmd":"cp", "label":"Copy object", "param":"s3://BUCKET1/OBJECT1 s3://BUCKET2[/OBJECT2]", "func":cmd_cp, "argc":2},
7d61be89
 	{"cmd":"mv", "label":"Move object", "param":"s3://BUCKET1/OBJECT1 s3://BUCKET2[/OBJECT2]", "func":cmd_mv, "argc":2},
585c735a
 	{"cmd":"setacl", "label":"Modify Access control list for Bucket or Object", "param":"s3://BUCKET[/OBJECT]", "func":cmd_setacl, "argc":1},
5a736f08
 	]
3cc025ae
 
f4555c39
 def format_commands(progname):
 	help = "Commands:\n"
5a736f08
 	for cmd in commands_list:
 		help += "  %s\n      %s %s %s\n" % (cmd["label"], progname, cmd["cmd"], cmd["param"])
f4555c39
 	return help
 
9b7618ae
 class OptionMimeType(Option):
 	def check_mimetype(option, opt, value):
 		if re.compile("^[a-z0-9]+/[a-z0-9+\.-]+$", re.IGNORECASE).match(value):
 			return value
 		raise OptionValueError("option %s: invalid MIME-Type format: %r" % (opt, value))
 
 	TYPES = Option.TYPES + ("mimetype",)
 	TYPE_CHECKER = copy(Option.TYPE_CHECKER)
 	TYPE_CHECKER["mimetype"] = check_mimetype
 
f4555c39
 class MyHelpFormatter(IndentedHelpFormatter):
 	def format_epilog(self, epilog):
 		if epilog:
 			return "\n" + epilog + "\n"
 		else:
 			return ""
 
4a52baa8
 def main():
87c0b03a
 	global cfg
1f7d2de3
 	if float("%d.%d" %(sys.version_info[0], sys.version_info[1])) < 2.4:
 		sys.stderr.write("ERROR: Python 2.4 or higher required, sorry.\n")
 		sys.exit(1)
3cc025ae
 
5a736f08
 	## Populate "commands" from "commands_list"
 	for cmd in commands_list:
 		if cmd.has_key("cmd"):
 			commands[cmd["cmd"]] = cmd
 
9b7618ae
 	default_verbosity = Config().verbosity
f4555c39
 	optparser = OptionParser(option_class=OptionMimeType, formatter=MyHelpFormatter())
9b7618ae
 	#optparser.disable_interspersed_args()
db340c09
 
7c1c5a19
 	config_file = None
db340c09
 	if os.getenv("HOME"):
 		config_file = os.path.join(os.getenv("HOME"), ".s3cfg")
 	elif os.name == "nt" and os.getenv("USERPROFILE"):
 		config_file = os.path.join(os.getenv("USERPROFILE"), "Application Data", "s3cmd.ini")
 
82d9eafa
 	preferred_encoding = locale.getpreferredencoding() or "UTF-8"
 
 	optparser.set_defaults(encoding = preferred_encoding)
 	optparser.set_defaults(config = config_file)
3cc025ae
 	optparser.set_defaults(verbosity = default_verbosity)
747ddb2a
 
09b29caf
 	optparser.add_option(      "--configure", dest="run_configure", action="store_true", help="Invoke interactive (re)configuration tool.")
747ddb2a
 	optparser.add_option("-c", "--config", dest="config", metavar="FILE", help="Config file name. Defaults to %default")
09b29caf
 	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.")
 
29893afc
 	#optparser.add_option("-n", "--dry-run", dest="dry_run", action="store_true", help="Only show what should be uploaded or downloaded but don't actually do it. May still perform S3 requests to get bucket listings and other information though.")
03575797
 
8ec1807f
 	optparser.add_option("-e", "--encrypt", dest="encrypt", action="store_true", help="Encrypt files before uploading to S3.")
03575797
 	optparser.add_option(      "--no-encrypt", dest="encrypt", action="store_false", help="Don't encrypt files.")
9b7618ae
 	optparser.add_option("-f", "--force", dest="force", action="store_true", help="Force overwrite and other dangerous operations.")
9197e62e
 	optparser.add_option(      "--continue", dest="get_continue", action="store_true", help="Continue getting a partially downloaded file (only for [get] command).")
559c963f
 	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).")
7406fc6c
 	optparser.add_option("-r", "--recursive", dest="recursive", action="store_true", help="Recursive upload, download or removal.")
7393bdba
 	optparser.add_option("-P", "--acl-public", dest="acl_public", action="store_true", help="Store objects with ACL allowing read for anyone.")
 	optparser.add_option(      "--acl-private", dest="acl_public", action="store_false", help="Store objects with default ACL allowing access for you only.")
0d91ff3f
 	optparser.add_option(      "--delete-removed", dest="delete_removed", action="store_true", help="Delete remote objects with no corresponding local file [sync]")
03575797
 	optparser.add_option(      "--no-delete-removed", dest="delete_removed", action="store_false", help="Don't delete remote objects.")
 	optparser.add_option("-p", "--preserve", dest="preserve_attrs", action="store_true", help="Preserve filesystem attributes (mode, ownership, timestamps). Default for [sync] command.")
 	optparser.add_option(      "--no-preserve", dest="preserve_attrs", action="store_false", help="Don't store FS attributes")
2d7d5543
 	optparser.add_option(      "--exclude", dest="exclude", action="append", metavar="GLOB", help="Filenames and paths matching GLOB will be excluded from sync")
 	optparser.add_option(      "--exclude-from", dest="exclude_from", action="append", metavar="FILE", help="Read --exclude GLOBs from FILE")
 	optparser.add_option(      "--rexclude", dest="rexclude", action="append", metavar="REGEXP", help="Filenames and paths matching REGEXP (regular expression) will be excluded from sync")
 	optparser.add_option(      "--rexclude-from", dest="rexclude_from", action="append", metavar="FILE", help="Read --rexclude REGEXPs from FILE")
3490bb40
 	optparser.add_option(      "--debug-syncmatch", "--debug-exclude", dest="debug_syncmatch", action="store_true", help="Output detailed information about remote vs. local filelist matching and --exclude processing and then exit")
8829e891
 
dc758146
 	optparser.add_option(      "--bucket-location", dest="bucket_location", help="Datacentre to create bucket in. Either EU or US (default)")
09b29caf
 
9b7618ae
 	optparser.add_option("-m", "--mime-type", dest="default_mime_type", type="mimetype", metavar="MIME/TYPE", help="Default MIME-type to be set for objects stored.")
 	optparser.add_option("-M", "--guess-mime-type", dest="guess_mime_type", action="store_true", help="Guess MIME-type of files by their extension. Falls back to default MIME-Type as specified by --mime-type option")
09b29caf
 
82d9eafa
 	optparser.add_option(      "--encoding", dest="encoding", metavar="ENCODING", help="Override autodetected terminal and filesystem encoding (character set). Autodetected: %s" % preferred_encoding)
 
09b29caf
 	optparser.add_option("-H", "--human-readable-sizes", dest="human_readable_sizes", action="store_true", help="Print sizes in human readable form.")
03575797
 
25f6f8c9
 	optparser.add_option(      "--progress", dest="progress_meter", action="store_true", help="Display progress meter (default on TTY).")
 	optparser.add_option(      "--no-progress", dest="progress_meter", action="store_false", help="Don't display progress meter (default on non-TTY).")
747ddb2a
 	optparser.add_option("-v", "--verbose", dest="verbosity", action="store_const", const=logging.INFO, help="Enable verbose output.")
09b29caf
 	optparser.add_option("-d", "--debug", dest="verbosity", action="store_const", const=logging.DEBUG, help="Enable debug output.")
ed61a5fa
 	optparser.add_option(      "--version", dest="show_version", action="store_true", help="Show s3cmd version (%s) and exit." % (PkgInfo.version))
3cc025ae
 
f4555c39
 	optparser.set_usage(optparser.usage + " COMMAND [parameters]")
09b29caf
 	optparser.set_description('S3cmd is a tool for managing objects in '+
f45580a2
 		'Amazon S3 storage. It allows for making and removing '+
 		'"buckets" and uploading, downloading and removing '+
 		'"objects" from these buckets.')
f4555c39
 	optparser.epilog = format_commands(optparser.get_prog_name())
ed61a5fa
 	optparser.epilog += ("\nSee program homepage for more information at\n%s\n" % PkgInfo.url)
f45580a2
 
3cc025ae
 	(options, args) = optparser.parse_args()
 
 	## Some mucking with logging levels to enable 
 	## debugging/verbose output for config file parser on request
53abee97
 	logging.basicConfig(level=options.verbosity,
 	                    format='%(levelname)s: %(message)s',
82d9eafa
 	                    stream = sys.stderr)
3cc025ae
 	
747ddb2a
 	if options.show_version:
315e527b
 		output(u"s3cmd version %s" % PkgInfo.version)
747ddb2a
 		sys.exit(0)
 
3cc025ae
 	## Now finally parse the config file
ca168590
 	if not options.config:
315e527b
 		error(u"Can't find a config file. Please use --config option.")
ca168590
 		sys.exit(1)
 
5a736f08
 	try:
 		cfg = Config(options.config)
 	except IOError, e:
 		if options.run_configure:
 			cfg = Config()
 		else:
315e527b
 			error(u"%s: %s"  % (options.config, e.strerror))
 			error(u"Configuration file not available.")
 			error(u"Consider using --configure parameter to create one.")
1f7d2de3
 			sys.exit(1)
3cc025ae
 
 	## And again some logging level adjustments
 	## according to configfile and command line parameters
 	if options.verbosity != default_verbosity:
5a736f08
 		cfg.verbosity = options.verbosity
 	logging.root.setLevel(cfg.verbosity)
9b7618ae
 
25f6f8c9
 	## Default to --progress on TTY devices, --no-progress elsewhere
 	## Can be overriden by actual --(no-)progress parameter
 	cfg.update_option('progress_meter', sys.stdout.isatty())
 
db340c09
 	## Unsupported features on Win32 platform
 	if os.name == "nt":
 		if cfg.preserve_attrs:
 			error(u"Option --preserve is not yet supported on MS Windows platform. Assuming --no-preserve.")
 			cfg.preserve_attrs = False
 		if cfg.progress_meter:
 			error(u"Option --progress is not yet supported on MS Windows platform. Assuming --no-progress.")
 			cfg.progress_meter = False
a120a4eb
 
9b7618ae
 	## Update Config with other parameters
5a736f08
 	for option in cfg.option_list():
 		try:
 			if getattr(options, option) != None:
315e527b
 				debug(u"Updating %s -> %s" % (option, getattr(options, option)))
5a736f08
 				cfg.update_option(option, getattr(options, option))
 		except AttributeError:
 			## Some Config() options are not settable from command line
 			pass
 
82d9eafa
 	## Set output and filesystem encoding for printing out filenames.
 	sys.stdout = codecs.getwriter(cfg.encoding)(sys.stdout, "replace")
 	sys.stderr = codecs.getwriter(cfg.encoding)(sys.stderr, "replace")
 
2d7d5543
 	## Process GLOB (shell wildcard style) excludes
d5e87cdf
 	if options.exclude is None:
 		options.exclude = []
 
 	if options.exclude_from:
 		for exf in options.exclude_from:
315e527b
 			debug(u"processing --exclude-from %s" % exf)
2d7d5543
 			process_exclude_from_file(exf, options.exclude)
d5e87cdf
 
 	if options.exclude:
 		for ex in options.exclude:
315e527b
 			debug(u"processing rule: %s" % ex)
2d7d5543
 			exc = re.compile(glob.fnmatch.translate(ex))
 			cfg.exclude.append(exc)
 			if options.debug_syncmatch:
 				cfg.debug_exclude[exc] = ex
 
 	## Process REGEXP style excludes
 	if options.rexclude is None:
 		options.rexclude = []
 
 	if options.rexclude_from:
 		for exf in options.rexclude_from:
315e527b
 			debug(u"processing --rexclude-from %s" % exf)
2d7d5543
 			process_exclude_from_file(exf, options.rexclude)
 
 	if options.rexclude:
 		for ex in options.rexclude:
315e527b
 			debug(u"processing rule: %s" % ex)
d5e87cdf
 			exc = re.compile(ex)
 			cfg.exclude.append(exc)
 			if options.debug_syncmatch:
 				cfg.debug_exclude[exc] = ex
8829e891
 
8ec1807f
 	if cfg.encrypt and cfg.gpg_passphrase == "":
315e527b
 		error(u"Encryption requested but no passphrase set in config file.")
 		error(u"Please re-run 's3cmd --configure' and supply it.")
8ec1807f
 		sys.exit(1)
 
5a736f08
 	if options.dump_config:
 		cfg.dump_config(sys.stdout)
1f7d2de3
 		sys.exit(0)
5a736f08
 
 	if options.run_configure:
 		run_configure(options.config)
1f7d2de3
 		sys.exit(0)
3cc025ae
 
 	if len(args) < 1:
315e527b
 		error(u"Missing command. Please run with --help for more information.")
1f7d2de3
 		sys.exit(1)
3cc025ae
 
d90a7929
 	## Unicodise all remaining arguments:
 	args = [unicodise(arg) for arg in args]
 
3cc025ae
 	command = args.pop(0)
 	try:
315e527b
 		debug(u"Command: %s" % commands[command]["cmd"])
3cc025ae
 		## We must do this lookup in extra step to 
 		## avoid catching all KeyError exceptions
 		## from inner functions.
5a736f08
 		cmd_func = commands[command]["func"]
3cc025ae
 	except KeyError, e:
315e527b
 		error(u"Invalid command: %s" % e)
1f7d2de3
 		sys.exit(1)
3cc025ae
 
5a736f08
 	if len(args) < commands[command]["argc"]:
315e527b
 		error(u"Not enough paramters for command '%s'" % command)
1f7d2de3
 		sys.exit(1)
3cc025ae
 
 	try:
 		cmd_func(args)
 	except S3Error, e:
315e527b
 		error(u"S3 error: %s" % e)
67a8d099
 		if e.info.has_key("Message"):
 			error(e.info['Message'])
85baf810
 		sys.exit(1)
3cc025ae
 	except ParameterError, e:
315e527b
 		error(u"Parameter problem: %s" % e)
85baf810
 		sys.exit(1)
3cc025ae
 
4a52baa8
 if __name__ == '__main__':
 	try:
 		## Our modules
 		## Keep them in try/except block to 
 		## detect any syntax errors in there
 		from S3 import PkgInfo
 		from S3.S3 import *
 		from S3.Config import Config
 		from S3.S3Uri import *
 		from S3 import Utils
 		from S3.Exceptions import *
d90a7929
 		from S3.Utils import unicodise
a120a4eb
 		from S3.Progress import Progress
4a52baa8
 
 		main()
 		sys.exit(0)
2031f301
 
2dad9f86
 	except SystemExit, e:
 		sys.exit(e.code)
 
2031f301
 	except KeyboardInterrupt:
 		sys.stderr.write("See ya!\n")
 		sys.exit(1)
 
4a52baa8
 	except Exception, e:
 		sys.stderr.write("""
 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
     An unexpected error has occurred.
   Please report the following lines to:
1e2d476a
    s3tools-bugs@lists.sourceforge.net
4a52baa8
 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 
 """)
2031f301
 		tb = traceback.format_exc(sys.exc_info())
 		e_class = str(e.__class__)
 		e_class = e_class[e_class.rfind(".")+1 : -2]
a1e3fd9c
 		sys.stderr.write(u"Problem: %s: %s\n" % (e_class, e))
2031f301
 		try:
 			sys.stderr.write("S3cmd:   %s\n" % PkgInfo.version)
 		except NameError:
 			sys.stderr.write("S3cmd:   unknown version. Module import problem?\n")
 		sys.stderr.write("Python:  %s\n" % sys.version.replace('\n', ' '))
26b4a8e5
 		sys.stderr.write("\n")
a1e3fd9c
 		sys.stderr.write(unicode(tb, errors="replace"))
2031f301
 		if type(e) == ImportError:
 			sys.stderr.write("\n")
 			sys.stderr.write("Your sys.path contains these entries:\n")
 			for path in sys.path:
a1e3fd9c
 				sys.stderr.write(u"\t%s\n" % path)
2031f301
 			sys.stderr.write("Now the question is where has S3/S3.py been installed?\n")
4a52baa8
 		sys.stderr.write("""
 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
     An unexpected error has occurred.
     Please report the above lines to:
1e2d476a
    s3tools-bugs@lists.sourceforge.net
4a52baa8
 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 """)
 		sys.exit(1)