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
 from S3.S3 import *
b008e471
 from S3.Config import Config
3cc025ae
 
 def output(message):
 	print message
 
9081133d
 def cmd_ls(args):
9b7618ae
 	s3 = S3(Config())
9081133d
 	bucket = None
 	if len(args) > 0:
 		isuri, bucket, object = s3.parse_s3_uri(args[0])
 		if not isuri:
 			bucket = args[0]
 	if bucket:
 		cmd_bucket_list(args)
 	else:
 		cmd_buckets_list_all(args)
 
3cc025ae
 def cmd_buckets_list_all(args):
9b7618ae
 	s3 = S3(Config())
3cc025ae
 	response = s3.list_all_buckets()
 	for bucket in response["list"]:
 		output("%s  %s" % (
 			formatDateTime(bucket["CreationDate"]),
 			s3.compose_uri(bucket["Name"]),
 			))
 
 def cmd_buckets_list_all_all(args):
9b7618ae
 	s3 = S3(Config())
3cc025ae
 	response = s3.list_all_buckets()
 
 	for bucket in response["list"]:
 		cmd_bucket_list([bucket["Name"]])
 		output("")
 
 
 def cmd_bucket_list(args):
9b7618ae
 	s3 = S3(Config())
3cc025ae
 	isuri, bucket, object = s3.parse_s3_uri(args[0])
 	if not isuri:
 		bucket = args[0]
 	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:
 		if S3.codes.has_key(e.Code):
 			error(S3.codes[e.Code] % bucket)
 			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),
 			s3.compose_uri(bucket, object["Key"]),
 			))
 
 def cmd_bucket_create(args):
9b7618ae
 	s3 = S3(Config())
3cc025ae
 	isuri, bucket, object = s3.parse_s3_uri(args[0])
 	if not isuri:
 		bucket = args[0]
 	try:
 		response = s3.bucket_create(bucket)
 	except S3Error, e:
 		if S3.codes.has_key(e.Code):
 			error(S3.codes[e.Code] % bucket)
 			return
 		else:
 			raise
 	output("Bucket '%s' created" % bucket)
 
 def cmd_bucket_delete(args):
9b7618ae
 	s3 = S3(Config())
3cc025ae
 	isuri, bucket, object = s3.parse_s3_uri(args[0])
 	if not isuri:
 		bucket = args[0]
 	try:
 		response = s3.bucket_delete(bucket)
 	except S3Error, e:
 		if S3.codes.has_key(e.Code):
 			error(S3.codes[e.Code] % bucket)
 			return
 		else:
 			raise
 	output("Bucket '%s' removed" % bucket)
 
f4555c39
 def cmd_cp(args):
 	raise ParameterError("Not yet implemented")
 
3cc025ae
 def cmd_object_put(args):
9b7618ae
 	s3 = S3(Config())
3cc025ae
 
 	s3uri = args.pop()
 	files = args[:]
 
 	isuri, bucket, object = s3.parse_s3_uri(s3uri)
 	if not isuri:
 		raise ParameterError("Expecting S3 URI instead of '%s'" % s3uri)
 
9b7618ae
 	if len(files) > 1 and 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")
 		error("object name will be prefixed to all stored filanames.")
 		exit(1)
 
 	for file in files:
 		if len(files) > 1:
 			object_final = object + os.path.basename(file)
 		elif object == "":
 			object_final = os.path.basename(file)
 		else:
 			object_final = object
 		response = s3.object_put(file, bucket, object_final)
 		output("File '%s' stored as %s (%d bytes)" %
9081133d
 			(file, s3.compose_uri(bucket, object_final, force_uri = True), response["size"]))
3cc025ae
 
 def cmd_object_get(args):
9b7618ae
 	s3 = S3(Config())
3cc025ae
 	s3uri = args.pop(0)
 	isuri, bucket, object = s3.parse_s3_uri(s3uri)
 	if not isuri or not bucket or not object:
 		raise ParameterError("Expecting S3 object URI instead of '%s'" % s3uri)
 	destination = len(args) > 0 and args.pop(0) or object
 	if os.path.isdir(destination):
 		destination += ("/" + object)
9b7618ae
 	if not Config().force and os.path.exists(destination):
3cc025ae
 		raise ParameterError("File %s already exists. Use --force to overwrite it" % destination)
 	response = s3.object_get(destination, bucket, object)
 	output("Object %s saved as '%s' (%d bytes)" %
 		(s3uri, destination, response["size"]))
 
 def cmd_object_del(args):
9b7618ae
 	s3 = S3(Config())
3cc025ae
 	s3uri = args.pop(0)
 	isuri, bucket, object = s3.parse_s3_uri(s3uri)
 	if not isuri or not bucket or not object:
 		raise ParameterError("Expecting S3 object URI instead of '%s'" % s3uri)
 	response = s3.object_delete(bucket, object)
 	output("Object %s deleted" % s3uri)
 
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])))
 			val = raw_input("\nChange any setting? [y/N] ")
 			if not val.lower().startswith("y"):
 				break
 		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))
 		exit(1)
 
 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":"cp", "label":"Copy files to / from S3 bucket", "param":"SRC DST", "func":cmd_cp, "argc":2},
 	{"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__':
 	if float("%d.%d" %(sys.version_info[0], sys.version_info[1])) < 2.5:
 		sys.stderr.write("ERROR: Python 2.5 or higher required, sorry.\n")
 		exit(1)
 
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()
3cc025ae
 	optparser.set_defaults(config=os.getenv("HOME")+"/.s3cfg")
9b7618ae
 	optparser.add_option("-c", "--config", dest="config", metavar="FILE", help="Config file name. Defaults to %default")
3cc025ae
 	optparser.set_defaults(verbosity = default_verbosity)
9b7618ae
 	optparser.add_option("-d", "--debug", dest="verbosity", action="store_const", const=logging.DEBUG, help="Enable debug output.")
 	optparser.add_option("-v", "--verbose", dest="verbosity", action="store_const", const=logging.INFO, help="Enable verbose output.")
 	optparser.add_option("-H", "--human-readable-sizes", dest="human_readable_sizes", action="store_true", help="Print sizes in human readable form.")
 	optparser.add_option("-f", "--force", dest="force", action="store_true", help="Force overwrite and other dangerous operations.")
 	optparser.add_option("-u", "--show-uri", dest="show_uri", action="store_true", help="Show complete S3 URI in listings.")
 	optparser.add_option("-P", "--acl-public", dest="acl_public", action="store_true", help="Store objects with ACL allowing read by anyone.")
 	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")
5a736f08
 	optparser.add_option(      "--dump-config", dest="dump_config", action="store_true", help="Dump current configuration after parsin config files and command line options and exit.")
 	optparser.add_option(      "--configure", dest="run_configure", action="store_true", help="Invoke interactive (re)configuration tool.")
3cc025ae
 
f4555c39
 	optparser.set_usage(optparser.usage + " COMMAND [parameters]")
f45580a2
 	optparser.set_description('S3cmd is a tool to manage objects in '+
 		'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())
f45580a2
 	optparser.epilog += '\nSee program homepage for more information at\nhttp://www.logix.cz/michal/devel/s3tools\n'
 
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')
 	
 	## 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.")
 			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)
 		exit(0)
 
 	if options.run_configure:
 		run_configure(options.config)
 		exit(0)
3cc025ae
 
 	if len(args) < 1:
 		error("Missing command. Please run with --help for more information.")
 		exit(1)
 
 	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)
 		exit(1)
 
5a736f08
 	if len(args) < commands[command]["argc"]:
3cc025ae
 		error("Not enough paramters for command '%s'" % command)
 		exit(1)
 
 	try:
 		cmd_func(args)
 	except S3Error, e:
 		error("S3 error: " + str(e))
 	except ParameterError, e:
 		error("Parameter problem: " + str(e))