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
 
9b7618ae
 from copy import copy
f4555c39
 from optparse import OptionParser, Option, OptionValueError, IndentedHelpFormatter
3cc025ae
 from logging import debug, info, warning, error
 import elementtree.ElementTree as ET
 
 ## Our modules
ed61a5fa
 from S3 import PkgInfo
3cc025ae
 from S3.S3 import *
b008e471
 from S3.Config import Config
ec50b5a7
 from S3.S3Uri import *
ed61a5fa
 
3cc025ae
 
 def output(message):
 	print message
 
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"]))
3cc025ae
 		output("")
 
 
b819c70c
 def subcmd_buckets_list_all(s3):
 	response = s3.list_all_buckets()
 	for bucket in response["list"]:
 		output("%s  s3://%s" % (
 			formatDateTime(bucket["CreationDate"]),
 			bucket["Name"],
 			))
 
 def subcmd_bucket_list(s3, uri):
 	bucket = uri.bucket()
 	object = uri.object()
 
3cc025ae
 	output("Bucket '%s':" % bucket)
f4555c39
 	if object.endswith('*'):
 		object = object[:-1]
3cc025ae
 	try:
f4555c39
 		response = s3.bucket_list(bucket, prefix = object)
3cc025ae
 	except S3Error, e:
75405909
 		if S3.codes.has_key(e.info["Code"]):
 			error(S3.codes[e.info["Code"]] % bucket)
3cc025ae
 			return
 		else:
 			raise
 	for object in response["list"]:
9b7618ae
 		size, size_coeff = formatSize(object["Size"], Config().human_readable_sizes)
3cc025ae
 		output("%s  %s%s  %s" % (
 			formatDateTime(object["LastModified"]),
 			str(size).rjust(8), size_coeff.ljust(1),
ec50b5a7
 			uri.compose_uri(bucket, object["Key"]),
3cc025ae
 			))
 
 def cmd_bucket_create(args):
b819c70c
 	uri = S3Uri(args[0])
 	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'" % args[0])
 
3cc025ae
 	try:
b819c70c
 		s3 = S3(Config())
 		response = s3.bucket_create(uri.bucket())
3cc025ae
 	except S3Error, e:
75405909
 		if S3.codes.has_key(e.info["Code"]):
 			error(S3.codes[e.info["Code"]] % uri.bucket())
3cc025ae
 			return
 		else:
 			raise
b819c70c
 	output("Bucket '%s' created" % uri.bucket())
3cc025ae
 
 def cmd_bucket_delete(args):
b819c70c
 	uri = S3Uri(args[0])
 	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'" % args[0])
3cc025ae
 	try:
b819c70c
 		s3 = S3(Config())
 		response = s3.bucket_delete(uri.bucket())
3cc025ae
 	except S3Error, e:
75405909
 		if S3.codes.has_key(e.info["Code"]):
 			error(S3.codes[e.info["Code"]] % uri.bucket())
3cc025ae
 			return
 		else:
 			raise
b819c70c
 	output("Bucket '%s' removed" % uri.bucket())
f4555c39
 
3cc025ae
 def cmd_object_put(args):
9b7618ae
 	s3 = S3(Config())
3cc025ae
 
b819c70c
 	uri_arg = args.pop()
3cc025ae
 	files = args[:]
 
b819c70c
 	uri = S3Uri(uri_arg)
af3425b6
 	if uri.type != "s3":
b819c70c
 		raise ParameterError("Expecting S3 URI instead of '%s'" % uri_arg)
3cc025ae
 
af3425b6
 	if len(files) > 1 and uri.object != "" and not Config().force:
3cc025ae
 		error("When uploading multiple files the last argument must")
 		error("be a S3 URI specifying just the bucket name")
 		error("WITHOUT object name!")
 		error("Alternatively use --force argument and the specified")
1f7d2de3
 		error("object name will be prefixed to all stored filenames.")
 		sys.exit(1)
747ddb2a
 	
3cc025ae
 	for file in files:
b819c70c
 		uri_arg_final = str(uri)
af3425b6
 		if len(files) > 1 or uri.object() == "":
b819c70c
 			uri_arg_final += os.path.basename(file)
af3425b6
 
b819c70c
 		uri_final = S3Uri(uri_arg_final)
af3425b6
 		response = s3.object_put_uri(file, uri_final)
3cc025ae
 		output("File '%s' stored as %s (%d bytes)" %
af3425b6
 			(file, uri_final, response["size"]))
72d9ddf5
 		if Config().acl_public:
 			output("Public URL of the object is: %s" %
 				(uri.public_url()))
3cc025ae
 
 def cmd_object_get(args):
9b7618ae
 	s3 = S3(Config())
b819c70c
 
 	uri_arg = args.pop(0)
 	uri = S3Uri(uri_arg)
 	if uri.type != "s3" or not uri.has_object():
 		raise ParameterError("Expecting S3 URI instead of '%s'" % uri_arg)
 
 	destination = len(args) > 0 and args.pop(0) or uri.object()
3cc025ae
 	if os.path.isdir(destination):
b819c70c
 		destination += ("/" + uri.object())
9b7618ae
 	if not Config().force and os.path.exists(destination):
3cc025ae
 		raise ParameterError("File %s already exists. Use --force to overwrite it" % destination)
f98a27f2
 	response = s3.object_get_uri(uri, destination)
 	if destination != "-":
 		output("Object %s saved as '%s' (%d bytes)" %
 			(uri, destination, response["size"]))
3cc025ae
 
 def cmd_object_del(args):
9b7618ae
 	s3 = S3(Config())
b819c70c
 
 	uri_arg = args.pop(0)
 	uri = S3Uri(uri_arg)
 	if uri.type != "s3" or not uri.has_object():
 		raise ParameterError("Expecting S3 URI instead of '%s'" % uri_arg)
 
 	response = s3.object_delete_uri(uri)
 	output("Object %s deleted" % uri)
3cc025ae
 
5a736f08
 def run_configure(config_file):
 	cfg = Config()
 	options = [
 		("access_key", "Access Key"),
 		("secret_key", "Secret Key"),
 		]
 	try:
 		while 1:
 			output("\nEnter new values or accept defaults in brackets with Enter.")
 			output("Refer to user manual for detailed description of all options.\n")
 			for option in options:
 				prompt = option[1]
 				try:
 					val = getattr(cfg, option[0])
 					if val not in (None, ""):
 						prompt += " [%s]" % val
 				except AttributeError:
 					pass
 
 				if len(option) >= 3:
 					output("%s" % option[2])
 
 				val = raw_input(prompt + ": ")
 				if val != "":
 					setattr(cfg, option[0], val)
 			output("\nNew settings:")
 			for option in options:
 				output("  %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:
 					output("Please wait...")
 					S3(Config()).bucket_list("", "")
 					output("\nSuccess. Your access key and secret key worked fine :-)")
 				except S3Error, e:
 					error("Test failed: %s" % (e))
 					val = raw_input("\nRetry configuration? [Y/n] ")
 					if val.lower().startswith("y") or val == "":
 						continue
 
 			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()
5a736f08
 		f = open(config_file, "w")
 		cfg.dump_config(f)
 		f.close()
 		output("Configuration saved to '%s'" % config_file)
 
 	except (EOFError, KeyboardInterrupt):
 		output("\nConfiguration aborted. Changes were NOT saved.")
 		return
 	
 	except IOError, e:
 		error("Writing config file failed: %s: %s" % (config_file, e.strerror))
1f7d2de3
 		sys.exit(1)
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},
 	]
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 ""
 
3cc025ae
 if __name__ == '__main__':
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()
747ddb2a
 
3cc025ae
 	optparser.set_defaults(config=os.getenv("HOME")+"/.s3cfg")
 	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.")
 
9b7618ae
 	optparser.add_option("-f", "--force", dest="force", action="store_true", help="Force overwrite and other dangerous operations.")
 	optparser.add_option("-P", "--acl-public", dest="acl_public", action="store_true", help="Store objects with ACL allowing read by anyone.")
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
 
 	optparser.add_option("-H", "--human-readable-sizes", dest="human_readable_sizes", action="store_true", help="Print sizes in human readable form.")
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
 	logging.basicConfig(level=options.verbosity, format='%(levelname)s: %(message)s')
 	
747ddb2a
 	if options.show_version:
ed61a5fa
 		output("s3cmd version %s" % PkgInfo.version)
747ddb2a
 		sys.exit(0)
 
3cc025ae
 	## Now finally parse the config file
5a736f08
 	try:
 		cfg = Config(options.config)
 	except IOError, e:
 		if options.run_configure:
 			cfg = Config()
 		else:
 			error("%s: %s"  % (options.config, e.strerror))
 			error("Configuration file not available.")
 			error("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
 
 	## Update Config with other parameters
5a736f08
 	for option in cfg.option_list():
 		try:
 			if getattr(options, option) != None:
 				debug("Updating %s -> %s" % (option, getattr(options, option)))
 				cfg.update_option(option, getattr(options, option))
 		except AttributeError:
 			## Some Config() options are not settable from command line
 			pass
 
 	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:
 		error("Missing command. Please run with --help for more information.")
1f7d2de3
 		sys.exit(1)
3cc025ae
 
 	command = args.pop(0)
 	try:
5a736f08
 		debug("Command: " + 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:
 		error("Invalid command: %s" % e)
1f7d2de3
 		sys.exit(1)
3cc025ae
 
5a736f08
 	if len(args) < commands[command]["argc"]:
3cc025ae
 		error("Not enough paramters for command '%s'" % command)
1f7d2de3
 		sys.exit(1)
3cc025ae
 
 	try:
 		cmd_func(args)
 	except S3Error, e:
 		error("S3 error: " + str(e))
 	except ParameterError, e:
 		error("Parameter problem: " + str(e))