"""
Contains the CLI class that handles the original command line.
"""
"""
Copyright 2011-2013 Gregory Holt

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import six
from six import moves
from six.moves.configparser import Error as ConfigParserError, SafeConfigParser
import functools
import os
import sys
import tempfile
import textwrap
import time
import traceback

from swiftly import VERSION
from swiftly.cli.context import CLIContext
from swiftly.cli.iomanager import IOManager
from swiftly.cli.optionparser import OptionParser
from swiftly.client import ClientManager, DirectClient, LocalClient, \
    StandardClient

#: The list of CLICommand classes avaiable to CLI. You'll want to add any new
#: CLICommand you create to this list.
COMMANDS = [
    'swiftly.cli.auth.CLIAuth',
    'swiftly.cli.decrypt.CLIDecrypt',
    'swiftly.cli.delete.CLIDelete',
    'swiftly.cli.encrypt.CLIEncrypt',
    'swiftly.cli.fordo.CLIForDo',
    'swiftly.cli.get.CLIGet',
    'swiftly.cli.head.CLIHead',
    'swiftly.cli.help.CLIHelp',
    'swiftly.cli.ping.CLIPing',
    'swiftly.cli.post.CLIPost',
    'swiftly.cli.put.CLIPut',
    'swiftly.cli.tempurl.CLITempURL',
    'swiftly.cli.trans.CLITrans']

TRUE_VALUES = ['1', 'on', 't', 'true', 'y', 'yes']
"""A list of lowercase string values that equate to True."""

class CLI(object):

    """
    Handles the original command line.

    An example script is `swiftly` itself::

        #!/usr/bin/env python
        import sys
        import swiftly.cli
        sys.exit(swiftly.cli.CLI()())

    See the output of ``swiftly help`` for more information.

    :param commands: The list of commands available; if None
        :py:attr:`COMMANDS` will be used.
    """

    def __init__(self, commands=None):
        #: The overall CLIContext containing attributes generated by the
        #: initial main_options parsing.
        #:
        #: The available attributes are:
        #:
        #: ==============  ====================================================
        #: cdn             True if the CDN URL should be used instead of the
        #:                 default Storage URL.
        #: client_manager  The :py:class:`swiftly.client.manager.ClientManager`
        #:                 to use for obtaining clients.
        #: concurrency     Number of concurrent actions to allow.
        #: io_manager      The :py:class:`swiftly.cli.iomanager.IOManager` to
        #:                 use for input and output.
        #: eventlet        True if Eventlet is in use.
        #: original_args   The original args used by the CLI.
        #: original_begin  The original time.time() when the CLI was called.
        #: verbose         Function to call when you want to (optionally) emit
        #:                 verbose output. ``verbose(msg, *args)`` where the
        #:                 output will be constructed with ``msg % args``.
        #: verbosity       Level of verbosity. Just None or 1 right now.
        #: ==============  ====================================================
        self.context = CLIContext()
        self.context.verbose = None
        self.context.io_manager = IOManager()

        #: A dictionary of the available commands and their CLICommand
        #: instances.
        self.commands = {}
        if commands is None:
            commands = COMMANDS
        for command in commands:
            mod, cls = command.rsplit('.', 1)
            cls = getattr(__import__(mod, fromlist=[cls]), cls)
            inst = cls(self)
            self.commands[inst.name] = inst

        #: The main :py:class:`OptionParser`.
        self.option_parser = OptionParser(
            version=VERSION,
            usage="""
Usage: %prog [options] <command> [command_options] [args]

With all these main Swiftly options, you can also specify them in a
configuration file or in an environment variable.

The configuration file option name is the long name of the command line option
with any dashes replaced with underscores, for example:
--auth-url becomes auth_url

The environment variable name is the long name of the command line option in
uppercase prefixed with SWIFTLY_, for example:
--auth-url becomes SWIFTLY_AUTH_URL

Command line options override environment variables which override
configuration file variables.
            """.strip(),
            io_manager=self.context.io_manager)
        self.option_parser.add_option(
            '-h', dest='help', action='store_true',
            help='Shows this help text.')
        self.option_parser.add_option(
            '--conf', dest='conf', metavar='PATH',
            help='Path to the configuration file to use. Default: '
                 '~/.swiftly.conf')
        self.option_parser.add_option(
            '-A', '--auth-url', dest='auth_url', metavar='URL',
            help='URL to auth system, example: '
                 'https://identity.api.rackspacecloud.com/v2.0')
        self.option_parser.add_option(
            '-U', '--auth-user', dest='auth_user', metavar='USER',
            help='User name for auth system, example: test:tester')
        self.option_parser.add_option(
            '-K', '--auth-key', dest='auth_key', metavar='KEY',
            help='Key for auth system, example: testing')
        self.option_parser.add_option(
            '-T', '--auth-tenant', dest='auth_tenant', metavar='TENANT',
            help='Tenant name for auth system, example: test If not '
                 'specified and needed, the auth user will be used.')
        self.option_parser.add_option(
            '--auth-methods', dest='auth_methods', metavar='name[,name[...]]',
            help='Auth methods to use with the auth system, example: '
                 'auth2key,auth2password,auth2password_force_tenant,auth1 The '
                 'best order will try to be determined for you; but if you '
                 'notice it keeps making useless auth attempts and that '
                 'drives you crazy, you can override that here. All the '
                 'available auth methods are listed in the example.')
        self.option_parser.add_option(
            '--region', dest='region', metavar='VALUE',
            help='Region to use, if supported by auth, example: DFW Default: '
                 'default region specified by the auth response.')
        self.option_parser.add_option(
            '-D', '--direct', dest='direct', metavar='PATH',
            help='Uses direct connect method to access Swift. Requires access '
                 'to rings and backend servers. The PATH is the account '
                 'path, example: /v1/AUTH_test')
        self.option_parser.add_option(
            '-L', '--local', dest='local', metavar='PATH',
            help='Uses the local file system method to access a fake Swift. '
                 'The PATH is the path on the local file system where the '
                 'fake Swift stores its data.')
        self.option_parser.add_option(
            '-P', '--proxy', dest='proxy', metavar='URL',
            help='Uses the given HTTP proxy URL.')
        self.option_parser.add_option(
            '-B', '--bypass-url', dest='bypass_url', metavar='URL',
            help='Override Swift endpoint URL provided during authentication.')
        self.option_parser.add_option(
            '-S', '--snet', dest='snet', action='store_true',
            help='Prepends the storage URL host name with "snet-". Mostly '
                 'only useful with Rackspace Cloud Files and Rackspace '
                 'ServiceNet.')
        self.option_parser.add_option(
            '--no-snet', dest='no_snet', action='store_true',
            help='Disables the above snet value if it had been set true in '
                 'the environment or configuration file.')
        self.option_parser.add_option(
            '-R', '--retries', dest='retries', metavar='INTEGER',
            help='Indicates how many times to retry the request on a server '
                 'error. Default: 4')
        self.option_parser.add_option(
            '-C', '--cache-auth', dest='cache_auth', action='store_true',
            help='If set true, the storage URL and auth token are cached in '
                 'your OS temporary directory as <user>.swiftly for reuse. If '
                 'there are already cached values, they are used without '
                 'authenticating first.')
        self.option_parser.add_option(
            '--no-cache-auth', dest='no_cache_auth', action='store_true',
            help='Disables the above cache-auth value if it had been set '
                 'true in the environment or configuration file.')
        self.option_parser.add_option(
            '--cdn', dest='cdn', action='store_true',
            help='Directs requests to the CDN management interface.')
        self.option_parser.add_option(
            '--no-cdn', dest='no_cdn', action='store_true',
            help='Disables the above cdn value if it had been set true in the '
                 'environment or configuration file.')
        self.option_parser.add_option(
            '--concurrency', dest='concurrency', metavar='INTEGER',
            help='Sets the the number of actions that can be done '
                 'simultaneously when possible (currently requires using '
                 'Eventlet too). Default: 1 Note that some nested actions may '
                 'amplify the number of concurrent actions. For instance, a '
                 'put of an entire directory will use up to this number of '
                 'concurrent actions. A put of a segmented object will use up '
                 'to this number of concurrent actions. But, if a directory '
                 'structure put is uploading segmented objects, this nesting '
                 'could cause up to INTEGER * INTEGER concurrent actions.')
        self.option_parser.add_option(
            '--eventlet', dest='eventlet', action='store_true',
            help='Enables Eventlet, if installed. This is disabled by default '
                 'if Eventlet is not installed or is less than version 0.11.0 '
                 '(because older Swiftly+Eventlet tends to use excessive CPU.')
        self.option_parser.add_option(
            '--no-eventlet', dest='no_eventlet', action='store_true',
            help='Disables Eventlet, even if installed and version 0.11.0 or '
                 'greater.')
        self.option_parser.add_option(
            '-v', '--verbose', dest='verbose', action='store_true',
            help='Causes output to standard error indicating actions being '
                 'taken. These output lines will be prefixed with VERBOSE and '
                 'will also include the number of seconds elapsed since '
                 'the command started.')
        self.option_parser.add_option(
            '--no-verbose', dest='no_verbose', action='store_true',
            help='Disables the above verbose value if it had been set true in '
                 'the environment or configuration file.')
        self.option_parser.add_option(
            '-O', '--direct-object-ring',
            dest='direct_object_ring', metavar='PATH',
            help='The current object ring of the cluster being pinged. This '
                 'will enable direct client to use this ring for all the '
                 'queries. Use of this also requires the main Swift code  '
                 'is installed and importable.')
        self.option_parser.add_option(
            '-k', '--insecure', dest='insecure', action='store_true',
            help='Allows "insecure" SSL connections for python >= 2.7.9')

        self.option_parser.raw_epilog = 'Commands:\n'
        for name in sorted(self.commands):
            command = self.commands[name]
            lines = command.option_parser.get_usage().split('\n')
            main_line = '  ' + lines[0].split(']', 1)[1].strip()
            for x in moves.range(4):
                lines.pop(0)
            for x, line in enumerate(lines):
                if not line:
                    lines = lines[:x]
                    break
            if len(main_line) < 24:
                initial_indent = main_line + ' ' * (24 - len(main_line))
            else:
                self.option_parser.raw_epilog += main_line + '\n'
                initial_indent = ' ' * 24
            self.option_parser.raw_epilog += textwrap.fill(
                ' '.join(lines), width=79, initial_indent=initial_indent,
                subsequent_indent=' ' * 24) + '\n'

    def __call__(self, args=None):
        options, args = self._parse_args(args)
        if not options:
            return 1
        return self._perform_command(args)

    def _parse_args(self, args=None):
        self.context.original_begin = time.time()
        self.context.original_args = args if args is not None else sys.argv[1:]
        self.option_parser.disable_interspersed_args()
        try:
            options, args = self.option_parser.parse_args(
                self.context.original_args)
        except UnboundLocalError:
            # Happens sometimes with an error handler that doesn't raise its
            # own exception. We'll catch the error below with
            # error_encountered.
            pass
        self.option_parser.enable_interspersed_args()
        if self.option_parser.error_encountered:
            return None, None
        if options.version:
            self.option_parser.print_version()
            return None, None
        if not args or options.help:
            self.option_parser.print_help()
            return None, None
        self.context.original_main_args = self.context.original_args[
            :-len(args)]

        self.context.conf = {}
        if options.conf is None:
            options.conf = os.environ.get('SWIFTLY_CONF', '~/.swiftly.conf')
        try:
            conf_parser = SafeConfigParser()
            conf_parser.read(os.path.expanduser(options.conf))
            for section in conf_parser.sections():
                self.context.conf[section] = dict(conf_parser.items(section))
        except ConfigParserError:
            pass

        for option_name in (
                'auth_url', 'auth_user', 'auth_key', 'auth_tenant',
                'auth_methods', 'region', 'direct', 'local', 'proxy', 'snet',
                'no_snet', 'retries', 'cache_auth', 'no_cache_auth', 'cdn',
                'no_cdn', 'concurrency', 'eventlet', 'no_eventlet', 'verbose',
                'no_verbose', 'direct_object_ring', 'insecure', 'bypass_url'):
            self._resolve_option(options, option_name, 'swiftly')
        for option_name in (
                'snet', 'no_snet', 'cache_auth', 'no_cache_auth', 'cdn',
                'no_cdn', 'eventlet', 'no_eventlet', 'verbose', 'no_verbose',
                'insecure'):
            if isinstance(getattr(options, option_name), six.string_types):
                setattr(
                    options, option_name,
                    getattr(options, option_name).lower() in TRUE_VALUES)
        for option_name in ('retries', 'concurrency'):
            if isinstance(getattr(options, option_name), six.string_types):
                setattr(
                    options, option_name, int(getattr(options, option_name)))
        if options.snet is None:
            options.snet = False
        if options.no_snet is None:
            options.no_snet = False
        if options.retries is None:
            options.retries = 4
        if options.cache_auth is None:
            options.cache_auth = False
        if options.no_cache_auth is None:
            options.no_cache_auth = False
        if options.cdn is None:
            options.cdn = False
        if options.no_cdn is None:
            options.no_cdn = False
        if options.concurrency is None:
            options.concurrency = 1
        if options.eventlet is None:
            options.eventlet = False
        if options.no_eventlet is None:
            options.no_eventlet = False
        if options.verbose is None:
            options.verbose = False
        if options.no_verbose is None:
            options.no_verbose = False
        if options.insecure is None:
            options.insecure = False

        self.context.eventlet = None
        if options.eventlet:
            self.context.eventlet = True
        if options.no_eventlet:
            self.context.eventlet = False
        if self.context.eventlet is None:
            self.context.eventlet = False
            try:
                import eventlet
                # Eventlet 0.11.0 fixed the CPU bug
                if eventlet.__version__ >= '0.11.0':
                    self.context.eventlet = True
            except ImportError:
                pass

        subprocess_module = None
        if self.context.eventlet:
            try:
                import eventlet.green.subprocess
                subprocess_module = eventlet.green.subprocess
            except ImportError:
                pass
        if subprocess_module is None:
            import subprocess
            subprocess_module = subprocess
        self.context.io_manager.subprocess_module = subprocess_module

        if options.verbose:
            self.context.verbosity = 1
            self.context.verbose = self._verbose
            self.context.io_manager.verbose = functools.partial(
                self._verbose, skip_sub_command=True)

        options.retries = int(options.retries)
        if args and args[0] == 'help':
            return options, args
        elif options.local:
            self.context.client_manager = ClientManager(
                LocalClient, local_path=options.local, verbose=self._verbose)
        elif options.direct:
            self.context.client_manager = ClientManager(
                DirectClient, swift_proxy_storage_path=options.direct,
                attempts=options.retries + 1, eventlet=self.context.eventlet,
                verbose=self._verbose,
                direct_object_ring=options.direct_object_ring)
        else:
            auth_cache_path = None
            if options.cache_auth:
                auth_cache_path = os.path.join(
                    tempfile.gettempdir(),
                    '%s.swiftly' % os.environ.get('USER', 'user'))
            if not options.auth_url:
                with self.context.io_manager.with_stderr() as fp:
                    fp.write('No Auth URL has been given.\n')
                    fp.flush()
                return None, None
            self.context.client_manager = ClientManager(
                StandardClient, auth_methods=options.auth_methods,
                auth_url=options.auth_url, auth_tenant=options.auth_tenant,
                auth_user=options.auth_user, auth_key=options.auth_key,
                auth_cache_path=auth_cache_path, region=options.region,
                snet=options.snet, attempts=options.retries + 1,
                eventlet=self.context.eventlet, verbose=self._verbose,
                http_proxy=options.proxy, insecure=options.insecure,
                bypass_url=options.bypass_url)

        self.context.cdn = options.cdn
        self.context.concurrency = int(options.concurrency)

        return options, args

    def _resolve_option(self, options, option_name, section_name):
        """Resolves an option value into options.

        Sets options.<option_name> to a resolved value. Any value
        already in options overrides a value in os.environ which
        overrides self.context.conf.

        :param options: The options instance as returned by optparse.
        :param option_name: The name of the option, such as
            ``auth_url``.
        :param section_name: The name of the section, such as
            ``swiftly``.
        """
        if getattr(options, option_name, None) is not None:
            return
        if option_name.startswith(section_name + '_'):
            environ_name = option_name.upper()
            conf_name = option_name[len(section_name) + 1:]
        else:
            environ_name = (section_name + '_' + option_name).upper()
            conf_name = option_name
        setattr(
            options, option_name,
            os.environ.get(
                environ_name,
                (self.context.conf.get(section_name, {})).get(conf_name)))

    def _perform_command(self, args):
        command = args[0]
        if command == 'for':
            command = 'fordo'
        if command not in self.commands:
            with self.context.io_manager.with_stderr() as fp:
                fp.write('ERROR unknown command %r\n' % args[0])
                fp.flush()
            return 1
        try:
            self.commands[command](args[1:])
        except Exception as err:
            if hasattr(err, 'text'):
                if err.text:
                    with self.context.io_manager.with_stderr() as fp:
                        fp.write('ERROR ')
                        fp.write(err.text)
                        fp.write('\n')
                        fp.flush()
            else:
                with self.context.io_manager.with_stderr() as fp:
                    fp.write(traceback.format_exc())
                    fp.write('\n')
                    fp.flush()
            return getattr(err, 'code', 1)
        return 0

    def _verbose(self, msg, *args, **kwargs):
        if self.context.verbosity:
            skip_sub_command = kwargs.get('skip_sub_command', False)
            with self.context.io_manager.with_debug(
                    skip_sub_command=skip_sub_command) as fp:
                try:
                    fp.write(
                        'VERBOSE %.02f ' %
                        (time.time() - self.context.original_begin))
                    fp.write(msg % args)
                except TypeError as err:
                    raise TypeError('%s: %r %r' % (err, msg, args))
                fp.write('\n')
                fp.flush()


"""
Contains a CLICommand that can issue POST requests.

Uses the following from :py:class:`swiftly.cli.context.CLIContext`:

==============  ============================================
cdn             True if the CDN Management URL should be used instead
                of the Storage URL.
client_manager  For connecting to Swift.
headers         A dict of headers to send.
io_manager      For directing output.
query           A dict of query parameters to send.
==============  ============================================
"""
"""
Copyright 2011-2013 Gregory Holt

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from swiftly.cli.command import CLICommand, ReturnCode

def cli_post(context, path, body=None):
    """
    Performs a POST on the item (account, container, or object).

    See :py:mod:`swiftly.cli.post` for context usage information.

    See :py:class:`CLIPost` for more information.

    :param context: The :py:class:`swiftly.cli.context.CLIContext` to
        use.
    :param path: The path to the item to issue a POST for.
    :param body: The body send along with the POST.
    """
    path = path.lstrip('/') if path else ''
    status, reason, headers, contents = 0, 'Unknown', {}, ''
    with context.client_manager.with_client() as client:
        if not path:
            status, reason, headers, contents = client.post_account(
                headers=context.headers, query=context.query, cdn=context.cdn,
                body=body)
            if status // 100 != 2:
                raise ReturnCode('posting account: %s %s' % (status, reason))
        elif '/' not in path.rstrip('/'):
            path = path.rstrip('/')
            status, reason, headers, contents = client.post_container(
                path, headers=context.headers, query=context.query,
                cdn=context.cdn, body=body)
            if status // 100 != 2:
                raise ReturnCode(
                    'posting container %r: %s %s' % (path, status, reason))
        else:
            status, reason, headers, contents = client.post_object(
                *path.split('/', 1), headers=context.headers,
                query=context.query, cdn=context.cdn, body=body)
            if status // 100 != 2:
                raise ReturnCode(
                    'posting object %r: %s %s' % (path, status, reason))

class CLIPost(CLICommand):
    """
    A CLICommand that can issue POST requests.

    See the output of ``swiftly help post`` for more information.
    """

    def __init__(self, cli):
        super(CLIPost, self).__init__(
            cli, 'post', max_args=1, usage="""
Usage: %prog [main_options] post [options] [path]

For help on [main_options] run %prog with no args.

Issues a POST request of the [path] given. If no [path] is given, a POST
request on the account is performed.""".strip())
        self.option_parser.add_option(
            '-h', '-H', '--header', dest='header', action='append',
            metavar='HEADER:VALUE',
            help='Add a header to the request. This can be used multiple '
                 'times for multiple headers. Examples: '
                 '-hx-object-meta-color:blue -h "Content-Type: text/html"')
        self.option_parser.add_option(
            '-q', '--query', dest='query', action='append',
            metavar='NAME[=VALUE]',
            help='Add a query parameter to the request. This can be used '
                 'multiple times for multiple query parameters. Example: '
                 '-qmultipart-manifest=get')
        self.option_parser.add_option(
            '-i', '--input', dest='input_', metavar='PATH',
            help='Indicates where to read the POST request body from; '
                 'use a dash (as in "-i -") to specify standard input since '
                 'POSTs to Swift do not normally take input.')

    def __call__(self, args):
        options, args, context = self.parse_args_and_create_context(args)
        context.headers = self.options_list_to_lowered_dict(options.header)
        context.query = self.options_list_to_lowered_dict(options.query)
        path = args.pop(0).lstrip('/') if args else None
        body = None
        if options.input_:
            if options.input_ == '-':
                body = self.cli.context.io_manager.get_stdin()
            else:
                body = open(options.input_, 'rb')
        return cli_post(context, path, body=body)


"""
Contains the CLIContext class used to pass contextual information to
CLI functions.
"""
"""
Copyright 2011-2013 Gregory Holt

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

class CLIContext(object):
    """
    Used to pass contextual information to CLI functions.

    An instance of this class will allow almost any attribute access,
    even if that attribute did not previously exist. It offers an
    easy way to set new attributes on the fly for different use cases
    without having to create a whole new subclasses.
    """

    def __getattr__(self, name):
        try:
            return getattr(super(CLIContext, self), name)
        except AttributeError:
            if name == 'verbose':
                return lambda *a, **k: None
            return None

    def __repr__(self):
        return str(vars(self))

    def copy(self):
        """
        Returns a new CLIContext instance that is a shallow copy of
        the original, much like dict's copy method.
        """
        context = CLIContext()
        for item in dir(self):
            if item[0] != '_' and item not in ('copy', 'write_headers'):
                setattr(context, item, getattr(self, item))
        return context

    def write_headers(self, fp, headers, mute=None):
        """
        Convenience function to output headers in a formatted fashion
        to a file-like fp, optionally muting any headers in the mute
        list.
        """
        if headers:
            if not mute:
                mute = []
            fmt = '%%-%ds %%s\n' % (max(len(k) for k in headers) + 1)
            for key in sorted(headers):
                if key in mute:
                    continue
                fp.write(fmt % (key.title() + ':', headers[key]))
            fp.flush()


"""
Provides a direct client by using loaded Swift Proxy Server code to
work with Swift.
"""
"""
Copyright 2011-2013 Gregory Holt

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import six
import json
from six.moves import StringIO

from swiftly.client.client import Client
from swiftly.client.utils import quote, headers_to_dict

class DirectClient(Client):
    """
    Direct client by using loaded Swift Proxy Server code to work
    with Swift.

    :param swift_proxy: Default: None. If set, the
        swift.proxy.server.Application given will be used instead of
        creating a default Swift proxy application.
    :param swift_proxy_storage_path: The path to the Swift account to
        use (example: /v1/AUTH_test).
    :param swift_proxy_cdn_path: The path to the Swift account to use
        for CDN management (example: /v1/AUTH_test).
    :param attempts: The number of times to try requests if a server
        error occurs (5xx response). Default: 5
    :param eventlet: Default: None. If True, Eventlet will be used if
        installed. If False, Eventlet will not be used even if
        installed. If None, the default, Eventlet will be used if
        installed and its version is at least 0.11.0 when a CPU usage
        bug was fixed.
    :param chunk_size: Maximum size to read or write at one time.
    :param verbose: Set to a ``func(msg, *args)`` that will be called
        with debug messages. Constructing a string for output can be
        done with msg % args.
    :param verbose_id: Set to a string you wish verbose messages to
        be prepended with; can help in identifying output when
        multiple Clients are in use.
    :param direct_object_ring: The path to custom object ring to used
        by the DirectClient
    """

    def __init__(self, swift_proxy=None, swift_proxy_storage_path=None,
                 swift_proxy_cdn_path=None, attempts=5, eventlet=None,
                 chunk_size=65536, verbose=None, verbose_id='',
                 direct_object_ring=None):
        super(DirectClient, self).__init__()
        self.storage_path = swift_proxy_storage_path
        self.cdn_path = swift_proxy_cdn_path
        self.attempts = attempts
        self.chunk_size = chunk_size
        if verbose:
            self.verbose = lambda m, *a, **k: verbose(
                self._verbose_id + m, *a, **k)
        else:
            self.verbose = lambda *a, **k: None
        self.verbose_id = verbose_id
        self._verbose_id = self.verbose_id
        if self._verbose_id:
            self._verbose_id += ' '
        self.swift_proxy = swift_proxy
        if not swift_proxy:
            self.verbose('Creating default proxy instance.')
            import swift.proxy.server
            from swiftly.client.localmemcache import LocalMemcache
            from swiftly.client.nulllogger import NullLogger
            try:
                import swift.common.swob
                self.Request = swift.common.swob.Request
            except ImportError:
                import webob
                self.Request = webob.Request
            self.swift_proxy = swift.proxy.server.Application(
                {}, memcache=LocalMemcache(), logger=NullLogger())
            self.oring = None
            def get_oring(*args):
                return self.oring

            if direct_object_ring:
                self.oring = swift.common.ring.ring.Ring(direct_object_ring)
                self.swift_proxy.get_object_ring = get_oring

        if eventlet is None:
            try:
                import eventlet
                # Eventlet 0.11.0 fixed the CPU bug
                if eventlet.__version__ >= '0.11.0':
                    eventlet = True
            except ImportError:
                pass
        if eventlet:
            try:
                import eventlet
                self.sleep = eventlet.sleep
            except ImportError:
                import time
                self.sleep = time.sleep
        else:
            import time
            self.sleep = time.sleep

    def _default_reset_func(self):
        raise Exception(
            'Failure and no ability to reset contents for reupload.')

    def request(self, method, path, contents, headers, decode_json=False,
                stream=False, query=None, cdn=False):
        """
        See :py:func:`swiftly.client.client.Client.request`
        """
        if query:
            path += '?' + '&'.join(
                ('%s=%s' % (quote(k), quote(v)) if v else quote(k))
                for k, v in sorted(six.iteritems(query)))
        reset_func = self._default_reset_func
        if isinstance(contents, six.string_types):
            contents = StringIO(contents)
        tell = getattr(contents, 'tell', None)
        seek = getattr(contents, 'seek', None)
        if tell and seek:
            try:
                orig_pos = tell()
                reset_func = lambda: seek(orig_pos)
            except Exception:
                tell = seek = None
        elif not contents:
            reset_func = lambda: None
        status = 0
        reason = 'Unknown'
        attempt = 0
        while attempt < self.attempts:
            attempt += 1
            if cdn:
                conn_path = self.cdn_path
            else:
                conn_path = self.storage_path
            titled_headers = dict((k.title(), v) for k, v in six.iteritems({
                'User-Agent': self.user_agent}))
            if headers:
                titled_headers.update(
                    (k.title(), v) for k, v in six.iteritems(headers))
            resp = None
            if not hasattr(contents, 'read'):
                if method not in self.no_content_methods and contents and \
                        'Content-Length' not in titled_headers and \
                        'Transfer-Encoding' not in titled_headers:
                    titled_headers['Content-Length'] = str(
                        len(contents or ''))
                req = self.Request.blank(
                    conn_path + path,
                    environ={'REQUEST_METHOD': method, 'swift_owner': True},
                    headers=titled_headers, body=contents)
                verbose_headers = '  '.join(
                    '%s: %s' % (k, v) for k, v in six.iteritems(titled_headers))
                self.verbose(
                    '> %s %s %s', method, conn_path + path, verbose_headers)
                resp = req.get_response(self.swift_proxy)
            else:
                req = self.Request.blank(
                    conn_path + path,
                    environ={'REQUEST_METHOD': method, 'swift_owner': True},
                    headers=titled_headers)
                content_length = None
                for h, v in six.iteritems(titled_headers):
                    if h.lower() == 'content-length':
                        content_length = int(v)
                    req.headers[h] = v
                if method not in self.no_content_methods and \
                        content_length is None:
                    titled_headers['Transfer-Encoding'] = 'chunked'
                    req.headers['Transfer-Encoding'] = 'chunked'
                else:
                    req.content_length = content_length
                req.body_file = contents
                verbose_headers = '  '.join(
                    '%s: %s' % (k, v) for k, v in six.iteritems(titled_headers))
                self.verbose(
                    '> %s %s %s', method, conn_path + path, verbose_headers)
                resp = req.get_response(self.swift_proxy)
            status = resp.status_int
            reason = resp.status.split(' ', 1)[1]
            hdrs = headers_to_dict(resp.headers.items())
            if stream:
                def iter_reader(size=-1):
                    if size == -1:
                        return ''.join(resp.app_iter)
                    else:
                        try:
                            return next(resp.app_iter)
                        except StopIteration:
                            return ''
                iter_reader.read = iter_reader
                value = iter_reader
            else:
                value = resp.body
            self.verbose('< %s %s', status, reason)
            if status and status // 100 != 5:
                if not stream and decode_json and status // 100 == 2:
                    if value:
                        value = json.loads(value)
                    else:
                        value = None
                return (status, reason, hdrs, value)
            if reset_func:
                reset_func()
            self.sleep(2 ** attempt)
        raise Exception('%s %s failed: %s %s' % (method, path, status, reason))

    def get_account_hash(self):
        """
        See :py:func:`swiftly.client.client.Client.get_account_hash`
        """
        return self.storage_path.rsplit('/', 1)[1]


"""
Contains a CLICommand that authenticates and then outputs the
resulting information.

Uses the following from :py:class:`swiftly.cli.context.CLIContext`:

==============  ========================
client_manager  For connecting to Swift.
io_manager      For directing output.
==============  ========================
"""
"""
Copyright 2011-2013 Gregory Holt

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import contextlib

from swiftly.cli.command import CLICommand

def cli_auth(context):
    """
    Authenticates and then outputs the resulting information.

    See :py:mod:`swiftly.cli.auth` for context usage information.

    See :py:class:`CLIAuth` for more information.
    """
    with context.io_manager.with_stdout() as fp:
        with context.client_manager.with_client() as client:
            info = []
            client.auth()
            if getattr(client, 'auth_cache_path', None):
                info.append(('Auth Cache', client.auth_cache_path))
            if getattr(client, 'auth_url', None):
                info.append(('Auth URL', client.auth_url))
            if getattr(client, 'auth_user', None):
                info.append(('Auth User', client.auth_user))
            if getattr(client, 'auth_key', None):
                info.append(('Auth Key', client.auth_key))
            if getattr(client, 'auth_tenant', None):
                info.append(('Auth Tenant', client.auth_tenant))
            if getattr(client, 'auth_methods', None):
                info.append(('Auth Methods', client.auth_methods))
            if getattr(client, 'storage_path', None):
                info.append(('Direct Storage Path', client.storage_path))
            if getattr(client, 'cdn_path', None):
                info.append(('Direct CDN Path', client.cdn_path))
            if getattr(client, 'local_path', None):
                info.append(('Local Path', client.local_path))
            if getattr(client, 'regions', None):
                info.append(('Regions', ' '.join(client.regions)))
            if getattr(client, 'default_region', None):
                info.append(('Default Region', client.default_region))
            if getattr(client, 'region', None):
                info.append(('Selected Region', client.region))
            if getattr(client, 'snet', None):
                info.append(('SNet', client.snet))
            if getattr(client, 'storage_url', None):
                info.append(('Storage URL', client.storage_url))
            if getattr(client, 'cdn_url', None):
                info.append(('CDN URL', client.cdn_url))
            if getattr(client, 'auth_token', None):
                info.append(('Auth Token', client.auth_token))
            if not info:
                info.append((
                    'No auth information available',
                    'Maybe no credentials were provided?'))
            fmt = '%%-%ds %%s\n' % (max(len(t) for t, v in info) + 1)
            for t, v in info:
                fp.write(fmt % (t + ':', v))
            fp.flush()

class CLIAuth(CLICommand):
    """
    A CLICommand that authenticates and then outputs the resulting
    information.

    See the output of ``swiftly help auth`` for more information.
    """

    def __init__(self, cli):
        super(CLIAuth, self).__init__(
            cli, 'auth', max_args=0, usage="""
Usage: %prog [main_options] auth

For help on [main_options] run %prog with no args.

Authenticates and then outputs the resulting information.

Possible Output Values:

    Auth Cache           The location where auth info may be cached.
    Auth URL             The URL of the auth service if in use.
    Auth User            The user to auth as if in use.
    Auth Key             The key to auth with if in use.
    Auth Tenant          The tenant to auth as if in use.
    Auth Methods         The auth methods in use if any specified.
    Direct Storage Path  The direct-mode path if in use.
    Direct CDN Path      The direct-mode CDN path if in use.
    Regions              The available regions as reported by the auth service.
    Default Region       The default region as reported by the auth service.
    Selected Region      The region selected for use by Swiftly.
    SNet                 True if ServiceNet/InternalURL would be used.
    Storage URL          The URL to use for storage as reported by the auth
                         service.
    CDN URL              The URL to use for CDN management as reported by the
                         auth service.
    Auth Token           The auth token to use as reported by the auth service.

Example Output:

Auth Cache:      /tmp/user.swiftly
Auth URL:        https://identity.api.rackspacecloud.com/v2.0
Auth User:       myusername
Auth Key:        mykey
Regions:         ORD DFW SYD IAD HKG
Default Region:  ORD
Selected Region: IAD
SNet:            True
Storage URL:     https://snet-storage101.iad3.clouddrive.com/v1/account
CDN URL:         https://cdn5.clouddrive.com/v1/account
Auth Token:      abcdef0123456789abcdef0123456789
            """.strip())

    def __call__(self, args):
        options, args, context = self.parse_args_and_create_context(args)
        return cli_auth(context)


"""
Contains general tools useful when accessing Swift services.
"""
"""
Copyright 2011-2013 Gregory Holt

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import six
import hashlib
import hmac
import time
from six.moves import urllib_parse as parse

def generate_temp_url(method, url, seconds, key):
    """
    Returns a TempURL good for the given request method, url, and
    number of seconds from now, signed by the given key.
    """
    method = method.upper()
    base_url, object_path = url.split('/v1/')
    object_path = '/v1/' + object_path
    expires = int(time.time() + seconds)
    hmac_body = '%s\n%s\n%s' % (method, expires, object_path)
    sig = hmac.new(key, hmac_body, hashlib.sha1).hexdigest()
    return '%s%s?temp_url_sig=%s&temp_url_expires=%s' % (
        base_url, object_path, sig, expires)

def get_trans_id_time(trans_id):
    """
    Returns the time.time() embedded in the trans_id or None if no
    time information is embedded.

    Copied from the Swift codebase.
    Copyright (c) 2010-2012 OpenStack Foundation
    """
    if len(trans_id) >= 34 and trans_id[:2] == 'tx' and trans_id[23] == '-':
        try:
            return int(trans_id[24:34], 16)
        except ValueError:
            pass
    return None

def quote(value, safe='/:'):
    """
    Much like parse.quote in that it returns a URL encoded string
    for the given value, protecting the safe characters; but this
    version also ensures the value is UTF-8 encoded.
    """
    if isinstance(value, six.text_type):
        value = value.encode('utf8')
    elif not isinstance(value, six.string_types):
        value = str(value)
    return parse.quote(value, safe)

def headers_to_dict(headers):
    """
    Converts a sequence of (name, value) tuples into a dict where if
    a given name occurs more than once its value in the dict will be
    a list of values.
    """
    hdrs = {}
    for h, v in headers:
        h = h.lower()
        if h in hdrs:
            if isinstance(hdrs[h], list):
                hdrs[h].append(v)
            else:
                hdrs[h] = [hdrs[h], v]
        else:
            hdrs[h] = v
    return hdrs


"""
Contains a CLICommand that can issue other commands for each item in
an account or container listing.

Uses the following from :py:class:`swiftly.cli.context.CLIContext`:

=======================  ============================================
cdn                      True if the CDN Management URL should be
                         used instead of the Storage URL.
client_manager           For connecting to Swift.
concurrency              The number of concurrent actions that can be
                         performed.
headers                  A dict of headers to send.
ignore_404               True if 404s should be silently ignored.
io_manager               For directing output.
query                    A dict of query parameters to send. Of
                         important use are limit, delimiter, prefix,
                         marker, and end_marker as they are common
                         listing query parameters.
remaining_args           The list of command line args to issue to
                         the sub-CLI instance; the first arg that
                         equals '<item>' will be replaced with each
                         item the for encounters. Any additional
                         instances of '<item>' will be left alone, as
                         you might be calling a nested "for ... do".
original_main_args       Used when constructing sub-CLI instances.
output_names             If True, outputs the name of each item just
                         before calling [command] with it. To ensure
                         easier parsing, the name will be url encoded
                         and prefixed with "Item Name: ". For
                         commands that have output of their own, this
                         is usually only useful with single
                         concurrency; otherwise the item names and
                         the command output will get interspersed and
                         impossible to associate.
=======================  ============================================
"""
"""
Copyright 2013 Gregory Holt

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import six
from six.moves import urllib_parse as parse

from swiftly.cli.cli import CLI
from swiftly.cli.command import CLICommand, ReturnCode
from swiftly.concurrency import Concurrency

def _cli_call(context, name, args):
    if context.output_names:
        with context.io_manager.with_stdout() as fp:
            fp.write('Item Name: ')
            fp.write(parse.quote(name.encode('utf8')))
            fp.write('\n')
            fp.flush()
    return CLI()(context.original_main_args + args)

def cli_fordo(context, path=None):
    """
    Issues commands for each item in an account or container listing.

    See :py:mod:`swiftly.cli.fordo` for context usage information.

    See :py:class:`CLIForDo` for more information.
    """
    path = path.lstrip('/') if path else None
    if path and '/' in path:
        raise ReturnCode(
            'path must be an empty string or a container name; was %r' % path)
    limit = context.query.get('limit')
    delimiter = context.query.get('delimiter')
    prefix = context.query.get('prefix')
    marker = context.query.get('marker')
    end_marker = context.query.get('end_marker')
    conc = Concurrency(context.concurrency)
    while True:
        with context.client_manager.with_client() as client:
            if not path:
                status, reason, headers, contents = client.get_account(
                    headers=context.headers, prefix=prefix,
                    delimiter=delimiter, marker=marker, end_marker=end_marker,
                    limit=limit, query=context.query, cdn=context.cdn)
            else:
                status, reason, headers, contents = client.get_container(
                    path, headers=context.headers, prefix=prefix,
                    delimiter=delimiter, marker=marker, end_marker=end_marker,
                    limit=limit, query=context.query, cdn=context.cdn)
            if status // 100 != 2:
                if status == 404 and context.ignore_404:
                    return
                if hasattr(contents, 'read'):
                    contents.read()
                if not path:
                    raise ReturnCode(
                        'listing account: %s %s' % (status, reason))
                else:
                    raise ReturnCode(
                        'listing container %r: %s %s' % (path, status, reason))
        if not contents:
            break
        for item in contents:
            name = (path + '/' if path else '') + item.get(
                'name', item.get('subdir'))
            args = list(context.remaining_args)
            try:
                index = args.index('<item>')
            except ValueError:
                raise ReturnCode(
                    'No "<item>" designation found in the "do" clause.')
            args[index] = name
            for (exc_type, exc_value, exc_tb, result) in \
                    six.itervalues(conc.get_results()):
                if exc_value:
                    conc.join()
                    raise exc_value
            conc.spawn(name, _cli_call, context, name, args)
        marker = contents[-1]['name']
        if limit:
            break
    conc.join()
    for (exc_type, exc_value, exc_tb, result) in \
            six.itervalues(conc.get_results()):
        if exc_value:
            conc.join()
            raise exc_value

class CLIForDo(CLICommand):
    """
    A CLICommand that can issue other commands for each item in an
    account or container listing.

    See the output of ``swiftly help for`` for more information.
    """

    def __init__(self, cli):
        super(CLIForDo, self).__init__(
            cli, 'fordo', min_args=1, max_args=1, usage="""
Usage: %prog [main_options] for [options] <path> do [command]

For help on [main_options] run %prog with no args.

This will issue the [command] for each item encountered performing a listing on
<path>.

If the <path> is an empty string "" then an account listing is performed and
the [command] will be run for each container listed. Otherwise, the <path> must
be a container and the [command] will be run for each object listed.

You may include the options listed below before the "do" to change how the
listing is performed (prefix queries, limits, etc.)

The "do" keyword separates the [command] from the rest of the "for" expression.
After the "do" comes the [command] which will have the first instance of
"<item>" replaced with each item in turn that is in the resulting "for"
listing. Any additional instances of "<item>" will be left alone, as you might
be calling a nested "for ... do".

For example, to head every container for an account:

    %prog for "" do head "<item>"

To head every object in every container for an account:

    %prog for "" do for "<item>" do head "<item>"

To post to every object in a container, forcing an auto-detected update to
each's content-type:

    %prog for my_container do post -hx-detect-content-type:true "<item>"

To head every object in a container, but only those with the name prefix of
"under_here/":

    %prog for -p under_here/ my_container do head "<item>" """.strip())
        self.option_parser.add_option(
            '-h', '-H', '--header', dest='header', action='append',
            metavar='HEADER:VALUE',
            help='Add a header to the request. This can be used multiple '
                 'times for multiple headers. Examples: '
                 '-hif-match:6f432df40167a4af05ca593acc6b3e4c -h '
                 '"If-Modified-Since: Wed, 23 Nov 2011 20:03:38 GMT"')
        self.option_parser.add_option(
            '-q', '--query', dest='query', action='append',
            metavar='NAME[=VALUE]',
            help='Add a query parameter to the request. This can be used '
                 'multiple times for multiple query parameters. Example: '
                 '-qmultipart-manifest=get')
        self.option_parser.add_option(
            '-l', '--limit', dest='limit',
            help='For account and container GETs, this limits the number of '
                 'items returned. Without this option, all items are '
                 'returned, even if it requires several backend requests to '
                 'the gather the information.')
        self.option_parser.add_option(
            '-d', '--delimiter', dest='delimiter',
            help='For account and container GETs, this sets the delimiter for '
                 'the listing retrieved. For example, a container with the '
                 'objects "abc/one", "abc/two", "xyz" and a delimiter of "/" '
                 'would return "abc/" and "xyz". Using the same delimiter, '
                 'but with a prefix of "abc/", would return "abc/one" and '
                 '"abc/two".')
        self.option_parser.add_option(
            '-p', '--prefix', dest='prefix',
            help='For account and container GETs, this sets the prefix for '
                 'the listing retrieved; the items returned will all match '
                 'the PREFIX given.')
        self.option_parser.add_option(
            '-m', '--marker', dest='marker',
            help='For account and container GETs, this sets the marker for '
                 'the listing retrieved; the items returned will begin with '
                 'the item just after the MARKER given (note: the marker does '
                 'not have to actually exist).')
        self.option_parser.add_option(
            '-e', '--end-marker', dest='end_marker', metavar='MARKER',
            help='For account and container GETs, this sets the end-marker '
                 'for the listing retrieved; the items returned will stop '
                 'with the item just before the MARKER given (note: the '
                 'marker does not have to actually exist).')
        self.option_parser.add_option(
            '--ignore-404', dest='ignore_404', action='store_true',
            help='Ignores 404 Not Found responses. Nothing will be output, '
                 'and the exit code will be 0 instead of 1.')
        self.option_parser.add_option(
            '--output-names', dest='output_names', action='store_true',
            help='Outputs the name of each item just before calling [command] '
                 'with it. To ensure easier parsing, the name will be url '
                 'encoded and prefixed with "Item Name: ". For commands that '
                 'have output of their own, this is usually only useful with '
                 'single concurrency; otherwise the item names and the '
                 'command output will get interspersed and impossible to '
                 'associate.')

    def __call__(self, args):
        try:
            index = args.index('do')
        except ValueError:
            raise ReturnCode('No "do" keyword found.')
        args, remaining_args = args[:index], args[index + 1:]
        options, args, context = self.parse_args_and_create_context(args)
        context.remaining_args = remaining_args
        context.headers = self.options_list_to_lowered_dict(options.header)
        context.query = self.options_list_to_lowered_dict(options.query)
        context.ignore_404 = options.ignore_404
        context.output_names = options.output_names
        if options.limit:
            context.query['limit'] = int(options.limit)
        if options.delimiter:
            context.query['delimiter'] = options.delimiter
        if options.prefix:
            context.query['prefix'] = options.prefix
        if options.marker:
            context.query['marker'] = options.marker
        if options.end_marker:
            context.query['end_marker'] = options.end_marker
        path = args.pop(0).lstrip('/') if args else None
        return cli_fordo(context, path)


"""
Contains the ClientManager class that can be used to manage a set of
clients.
"""
"""
Copyright 2011-2013 Gregory Holt

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import contextlib
from six.moves import queue

class ClientManager(object):
    """
    Can be used to manage a set of clients.

    :param client_class: The class to create when a new client is
        needed.
    :param args: The args for the client constructor.
    :param kwargs: The keyword args for the client constructor.
    """

    def __init__(self, client_class, *args, **kwargs):
        self.client_class = client_class
        self.args = args
        self.kwargs = kwargs
        self.clients = queue.Queue()
        self.client_id = 0

    def get_client(self):
        """
        Obtains a client for use, whether an existing unused client
        or a brand new one if none are available.
        """
        client = None
        try:
            client = self.clients.get(block=False)
        except queue.Empty:
            pass
        if not client:
            self.client_id += 1
            kwargs = dict(self.kwargs)
            kwargs['verbose_id'] = kwargs.get(
                'verbose_id', '') + str(self.client_id)
            client = self.client_class(*self.args, **kwargs)
        return client

    def put_client(self, client):
        """
        Returns a client back into the pool for availability to
        future calls to get_client. This should only be called if
        get_client was used to obtain the client; with_client is a
        context manager that does this for you.
        """
        self.clients.put(client)

    @contextlib.contextmanager
    def with_client(self):
        """
        A context manager that obtains a client for use, whether an
        existing unused client or a brand new one if none are
        available.
        """
        client = self.get_client()
        yield client
        self.put_client(client)


"""
Contains a CLICommand that can issue HEAD requests.

Uses the following from :py:class:`swiftly.cli.context.CLIContext`:

=======================  ============================================
cdn                      True if the CDN Management URL should be
                         used instead of the Storage URL.
client_manager           For connecting to Swift.
headers                  A dict of headers to send.
ignore_404               True if 404s should be silently ignored.
io_manager               For directing output.
muted_account_headers    The headers to omit when outputting account
                         response headers.
muted_container_headers  The headers to omit when outputting
                         container response headers.
muted_object_headers     The headers to omit when outputting object
                         response headers.
query                    A dict of query parameters to send.
write_headers            A function used to output the response
                         headers.
=======================  ============================================
"""
"""
Copyright 2011-2013 Gregory Holt

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from swiftly.cli.command import CLICommand, ReturnCode

def cli_head(context, path=None):
    """
    Performs a HEAD on the item (account, container, or object).

    See :py:mod:`swiftly.cli.head` for context usage information.

    See :py:class:`CLIHead` for more information.
    """
    path = path.lstrip('/') if path else None
    with context.client_manager.with_client() as client:
        if not path:
            status, reason, headers, contents = client.head_account(
                headers=context.headers, query=context.query, cdn=context.cdn)
            mute = context.muted_account_headers
        elif '/' not in path.rstrip('/'):
            path = path.rstrip('/')
            status, reason, headers, contents = client.head_container(
                path, headers=context.headers, query=context.query,
                cdn=context.cdn)
            mute = context.muted_container_headers
        else:
            status, reason, headers, contents = client.head_object(
                *path.split('/', 1), headers=context.headers,
                query=context.query, cdn=context.cdn)
            mute = context.muted_object_headers
        if hasattr(contents, 'read'):
            contents = contents.read()
    if status // 100 != 2:
        if status == 404 and context.ignore_404:
            return
        if not path:
            raise ReturnCode('heading account: %s %s' % (status, reason))
        elif '/' not in path:
            raise ReturnCode(
                'heading container %r: %s %s' % (path, status, reason))
        else:
            raise ReturnCode(
                'heading object %r: %s %s' % (path, status, reason))
    else:
        with context.io_manager.with_stdout() as fp:
            context.write_headers(fp, headers, mute)

class CLIHead(CLICommand):
    """
    A CLICommand that can issue HEAD requests.

    See the output of ``swiftly help head`` for more information.
    """

    def __init__(self, cli):
        super(CLIHead, self).__init__(
            cli, 'head', max_args=1, usage="""
Usage: %prog [main_options] head [options] [path]

For help on [main_options] run %prog with no args.

Outputs the resulting headers from a HEAD request of the [path] given. If no
[path] is given, a HEAD request on the account is performed.""".strip())
        self.option_parser.add_option(
            '-h', '-H', '--header', dest='header', action='append',
            metavar='HEADER:VALUE',
            help='Add a header to the request. This can be used multiple '
                 'times for multiple headers. Examples: '
                 '-hif-match:6f432df40167a4af05ca593acc6b3e4c -h '
                 '"If-Modified-Since: Wed, 23 Nov 2011 20:03:38 GMT"')
        self.option_parser.add_option(
            '-q', '--query', dest='query', action='append',
            metavar='NAME[=VALUE]',
            help='Add a query parameter to the request. This can be used '
                 'multiple times for multiple query parameters. Example: '
                 '-qmultipart-manifest=get')
        self.option_parser.add_option(
            '--ignore-404', dest='ignore_404', action='store_true',
            help='Ignores 404 Not Found responses. Nothing will be output, '
                 'but the exit code will be 0 instead of 1.')

    def __call__(self, args):
        options, args, context = self.parse_args_and_create_context(args)
        context.headers = self.options_list_to_lowered_dict(options.header)
        context.query = self.options_list_to_lowered_dict(options.query)
        context.ignore_404 = options.ignore_404
        path = args.pop(0).lstrip('/') if args else None
        return cli_head(context, path)


"""
Encryption routines for Swiftly.

Requires PyCrypto 2.6.1 or greater.
<https://www.dlitz.net/software/pycrypto/>

Copyright 2013 Gregory Holt

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import hashlib

try:
    import Crypto.Cipher.AES
    import Crypto.Random
    AES256CBC_Support = True
except ImportError:
    AES256CBC_Support = False

#: Constant that can be used a preamble for algorithm detection.
AES256CBC = '\x00'

def aes_encrypt(key, stdin, preamble=None, chunk_size=65536,
                content_length=None):
    """
    Generator that encrypts a content stream using AES 256 in CBC
    mode.

    :param key: Any string to use as the encryption key.
    :param stdin: Where to read the contents from.
    :param preamble: str to yield initially useful for providing a
        hint for future readers as to the algorithm in use.
    :param chunk_size: Largest amount to read at once.
    :param content_length: The number of bytes to read from stdin.
        None or < 0 indicates reading until EOF.
    """
    if not AES256CBC_Support:
        raise Exception(
            'AES256CBC not supported; likely pycrypto is not installed')
    if preamble:
        yield preamble
    # Always use 256-bit key
    key = hashlib.sha256(key).digest()
    # At least 16 and a multiple of 16
    chunk_size = max(16, chunk_size >> 4 << 4)
    iv = Crypto.Random.new().read(16)
    yield iv
    encryptor = Crypto.Cipher.AES.new(key, Crypto.Cipher.AES.MODE_CBC, iv)
    reading = True
    left = None
    if content_length is not None and content_length >= 0:
        left = content_length
    while reading:
        size = chunk_size
        if left is not None and size > left:
            size = left
        chunk = stdin.read(size)
        if not chunk:
            if left is not None and left > 0:
                raise IOError('Early EOF from input')
            # Indicates how many usable bytes in last block
            yield encryptor.encrypt('\x00' * 16)
            break
        if left is not None:
            left -= len(chunk)
            if left <= 0:
                reading = False
        block = chunk
        trailing = len(block) % 16
        while trailing:
            size = 16 - trailing
            if left is not None and size > left:
                size = left
            chunk = stdin.read(size)
            if not chunk:
                if left is not None and left > 0:
                    raise IOError('Early EOF from input')
                reading = False
                # Indicates how many usable bytes in last block
                chunk = chr(trailing) * (16 - trailing)
            elif left is not None:
                left -= len(chunk)
                if left <= 0:
                    reading = False
            block += chunk
            trailing = len(block) % 16
        yield encryptor.encrypt(block)

def aes_decrypt(key, stdin, chunk_size=65536):
    """
    Generator that decrypts a content stream using AES 256 in CBC
    mode.

    :param key: Any string to use as the decryption key.
    :param stdin: Where to read the encrypted data from.
    :param chunk_size: Largest amount to read at once.
    """
    if not AES256CBC_Support:
        raise Exception(
            'AES256CBC not supported; likely pycrypto is not installed')
    # Always use 256-bit key
    key = hashlib.sha256(key).digest()
    # At least 16 and a multiple of 16
    chunk_size = max(16, chunk_size >> 4 << 4)
    iv = stdin.read(16)
    while len(iv) < 16:
        chunk = stdin.read(16 - len(iv))
        if not chunk:
            raise IOError('EOF reading IV')
    decryptor = Crypto.Cipher.AES.new(key, Crypto.Cipher.AES.MODE_CBC, iv)
    data = ''
    while True:
        chunk = stdin.read(chunk_size)
        if not chunk:
            if len(data) != 16:
                raise IOError('EOF reading encrypted stream')
            data = decryptor.decrypt(data)
            trailing = ord(data[-1])
            if trailing > 15:
                raise IOError(
                    'EOF reading encrypted stream or trailing value corrupted '
                    '%s' % trailing)
            yield data[:trailing]
            break
        data += chunk
        if len(data) > 16:
            # Always leave at least one byte pending
            trailing = (len(data) % 16) or 16
            yield decryptor.decrypt(data[:-trailing])
            data = data[-trailing:]


"""
Contains a CLICommand that can issue PUT requests.

Uses the following from :py:class:`swiftly.cli.context.CLIContext`:

===============  ====================================================
cdn              True if the CDN Management URL should be used
                 instead of the Storage URL.
client_manager   For connecting to Swift.
concurrency      The number of concurrent actions that can be
                 performed.
different        Set to True to check if the local file is different
                 than an existing object before uploading.
empty            Set to True if you wish to send an empty body with
                 the PUT rather than reading from the io_manager's
                 stdin.
headers          A dict of headers to send.
input\_          A string representing where input should be obtained
                 from. If None, the io_manager's stdin will be used.
                 If a directory path is specified, a set of PUTs will
                 be generated for each item in the directory
                 structure. If a file path is specified, that single
                 file will be used as input.
io_manager       For directing output and obtaining input if needed.
newer            Set to True to check if the local file is newer than
                 an existing object before uploading.
query            A dict of query parameters to send.
seek             Where to seek to in the input\_ before uploading;
                 usually just used by recursive calls with segmented
                 objects.
segment_size     The max size of a file before switching to a
                 segmented object and the max size of each object
                 segment.
static_segments  Set to True to use static large object support
                 instead of dynamic large object support.
===============  ====================================================
"""
"""
Copyright 2011-2013 Gregory Holt

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import six
import json
import os
import time

from swiftly.cli.command import CLICommand, ReturnCode
from swiftly.concurrency import Concurrency
from swiftly.dencrypt import AES256CBC, aes_encrypt
from swiftly.filelikeiter import FileLikeIter

def cli_put_directory_structure(context, path):
    """
    Performs PUTs rooted at the path using a directory structure
    pointed to by context.input\_.

    See :py:mod:`swiftly.cli.put` for context usage information.

    See :py:class:`CLIPut` for more information.
    """
    if not context.input_:
        raise ReturnCode(
            'called cli_put_directory_structure without context.input_ set')
    if not os.path.isdir(context.input_):
        raise ReturnCode(
            '%r is not a directory' % context.input_)
    if not path:
        raise ReturnCode(
            'uploading a directory structure requires at least a container '
            'name')
    new_context = context.copy()
    new_context.input_ = None
    container = path.split('/', 1)[0]
    cli_put_container(new_context, container)
    ilen = len(context.input_)
    if not context.input_.endswith(os.sep):
        ilen += 1
    conc = Concurrency(context.concurrency)
    for (dirpath, dirnames, filenames) in os.walk(context.input_):
        if not dirnames and not filenames:
            new_context = context.copy()
            new_context.headers = dict(context.headers)
            new_context.headers['content-type'] = 'text/directory'
            new_context.headers['x-object-meta-mtime'] = \
                '%f' % os.path.getmtime(context.input_)
            new_context.input_ = None
            new_context.empty = True
            new_path = path
            if path[-1] != '/':
                new_path += '/'
            new_path += dirpath[ilen:]
            for (exc_type, exc_value, exc_tb, result) in \
                    six.itervalues(conc.get_results()):
                if exc_value:
                    conc.join()
                    raise exc_value
            conc.spawn(new_path, cli_put_object, new_context, new_path)
        else:
            for fname in filenames:
                new_context = context.copy()
                new_context.input_ = os.path.join(dirpath, fname)
                new_path = path
                if path[-1] != '/':
                    new_path += '/'
                if dirpath[ilen:]:
                    new_path += dirpath[ilen:] + '/'
                new_path += fname
                for (exc_type, exc_value, exc_tb, result) in \
                        six.itervalues(conc.get_results()):
                    if exc_value:
                        conc.join()
                        raise exc_value
                conc.spawn(new_path, cli_put_object, new_context, new_path)
    conc.join()
    for (exc_type, exc_value, exc_tb, result) in \
            six.itervalues(conc.get_results()):
        if exc_value:
            raise exc_value

def cli_put_account(context):
    """
    Performs a PUT on the account.

    See :py:mod:`swiftly.cli.put` for context usage information.

    See :py:class:`CLIPut` for more information.
    """
    body = None
    if context.input_:
        if context.input_ == '-':
            body = context.io_manager.get_stdin()
        else:
            body = open(context.input_, 'rb')
    with context.client_manager.with_client() as client:
        status, reason, headers, contents = client.put_account(
            headers=context.headers, query=context.query, cdn=context.cdn,
            body=body)
        if hasattr(contents, 'read'):
            contents.read()
    if status // 100 != 2:
        raise ReturnCode('putting account: %s %s' % (status, reason))

def cli_put_container(context, path):
    """
    Performs a PUT on the container.

    See :py:mod:`swiftly.cli.put` for context usage information.

    See :py:class:`CLIPut` for more information.
    """
    path = path.rstrip('/')
    if '/' in path:
        raise ReturnCode('called cli_put_container with object %r' % path)
    body = None
    if context.input_:
        if context.input_ == '-':
            body = context.io_manager.get_stdin()
        else:
            body = open(context.input_, 'rb')
    with context.client_manager.with_client() as client:
        status, reason, headers, contents = client.put_container(
            path, headers=context.headers, query=context.query,
            cdn=context.cdn, body=body)
        if hasattr(contents, 'read'):
            contents.read()
    if status // 100 != 2:
        raise ReturnCode(
            'putting container %r: %s %s' % (path, status, reason))

def cli_put_object(context, path):
    """
    Performs a PUT on the object.

    See :py:mod:`swiftly.cli.put` for context usage information.

    See :py:class:`CLIPut` for more information.
    """
    if context.different and context.encrypt:
        raise ReturnCode(
            'context.different will not work properly with context.encrypt '
            'since encryption may change the object size')
    put_headers = dict(context.headers)
    if context.empty:
        body = ''
        put_headers['content-length'] = '0'
    elif not context.input_ or context.input_ == '-':
        stdin = context.io_manager.get_stdin()

        if context.stdin_segmentation:
            def reader():
                while True:
                    chunk = stdin.read(65536)
                    if chunk:
                        yield chunk
                    else:
                        return

            segment_body = FileLikeIter(reader(), context.segment_size)

            prefix = _create_container(context, path, time.time(), 0)
            new_context = context.copy()
            new_context.stdin_segmentation = False
            new_context.stdin = segment_body
            new_context.headers = dict(context.headers)
            segment_n = 0
            path2info = {}
            while not segment_body.is_empty():
                segment_path = _get_segment_path(prefix, segment_n)
                etag = cli_put_object(new_context, segment_path)
                size = segment_body.limit - segment_body.left
                path2info[segment_path] = (size, etag)

                segment_body.reset_limit()
                segment_n += 1
            body = _get_manifest_body(context, prefix, path2info, put_headers)
        else:
            if hasattr(context, 'stdin'):
                body = context.stdin
            else:
                body = stdin
    elif context.seek is not None:
        if context.encrypt:
            raise ReturnCode(
                'putting object %r: Cannot use encryption and context.seek' %
                path)
        body = open(context.input_, 'rb')
        body.seek(context.seek)
    else:
        l_mtime = os.path.getmtime(context.input_)
        l_size = os.path.getsize(context.input_)
        put_headers['content-length'] = str(l_size)
        if context.newer or context.different:
            r_mtime = None
            r_size = None
            with context.client_manager.with_client() as client:
                status, reason, headers, contents = client.head_object(
                    *path.split('/', 1), headers=context.headers,
                    query=context.query, cdn=context.cdn)
                if hasattr(contents, 'read'):
                    contents.read()
            if status // 100 == 2:
                r_mtime = headers.get('x-object-meta-mtime')
                if r_mtime:
                    try:
                        r_mtime = float(r_mtime)
                    except ValueError:
                        r_mtime = None
                r_size = headers.get('content-length')
                if r_size:
                    try:
                        r_size = int(r_size)
                    except ValueError:
                        r_size = None
            elif status != 404:
                raise ReturnCode(
                    'could not head %r for conditional check; skipping put: '
                    '%s %s' % (path, status, reason))
            if context.newer and r_mtime is not None or l_mtime <= r_mtime:
                return
            if context.different and r_mtime is not None and \
                    l_mtime == r_mtime and r_size is not None and \
                    l_size == r_size:
                return
        put_headers['x-object-meta-mtime'] = '%f' % l_mtime
        size = os.path.getsize(context.input_)
        if size > context.segment_size:
            if context.encrypt:
                raise ReturnCode(
                    'putting object %r: Cannot use encryption for objects '
                    'greater than the segment size' % path)
            prefix = _create_container(context, path, l_mtime, size)
            conc = Concurrency(context.concurrency)
            start = 0
            segment = 0
            path2info = {}
            while start < size:
                new_context = context.copy()
                new_context.headers = dict(context.headers)
                new_context.headers['content-length'] = str(min(
                    size - start, context.segment_size))
                new_context.seek = start
                new_path = _get_segment_path(prefix, segment)
                for (ident, (exc_type, exc_value, exc_tb, result)) in \
                        six.iteritems(conc.get_results()):
                    if exc_value:
                        conc.join()
                        raise exc_value
                    path2info[ident] = result
                conc.spawn(
                    new_path, cli_put_object, new_context, new_path)
                segment += 1
                start += context.segment_size
            conc.join()
            for (ident, (exc_type, exc_value, exc_tb, result)) in \
                    six.iteritems(conc.get_results()):
                if exc_value:
                    raise exc_value
                path2info[ident] = result
            body = _get_manifest_body(context, prefix, path2info, put_headers)
        else:
            body = open(context.input_, 'rb')
    with context.client_manager.with_client() as client:
        if context.encrypt:
            content_length = put_headers.get('content-length')
            if content_length:
                content_length = int(content_length)
            if hasattr(body, 'read'):
                body = FileLikeIter(aes_encrypt(
                    context.encrypt, body, preamble=AES256CBC,
                    chunk_size=getattr(client, 'chunk_size', 65536),
                    content_length=content_length))
            else:
                body = FileLikeIter(aes_encrypt(
                    context.encrypt, FileLikeIter([body]), preamble=AES256CBC,
                    chunk_size=getattr(client, 'chunk_size', 65536),
                    content_length=content_length))
            if 'content-length' in put_headers:
                del put_headers['content-length']
        container, obj = path.split('/', 1)
        status, reason, headers, contents = client.put_object(
            container, obj, body, headers=put_headers, query=context.query,
            cdn=context.cdn)
        if hasattr(contents, 'read'):
            contents = contents.read()
    if status // 100 != 2:
        raise ReturnCode(
            'putting object %r: %s %s %r' % (path, status, reason, contents))
    if context.seek is not None:
        content_length = put_headers.get('content-length')
        etag = headers.get('etag')
        if content_length and etag:
            content_length = int(content_length)
        else:
            with context.client_manager.with_client() as client:
                container, obj = path.split('/', 1)
                status, reason, headers, contents = client.head_object(
                    container, obj, cdn=context.cdn)
                if hasattr(contents, 'read'):
                    contents = contents.read()
            if status // 100 != 2:
                raise ReturnCode(
                    'heading object %r: %s %s %r' %
                    (path, status, reason, contents))
            content_length = headers.get('content-length')
            etag = headers.get('etag')
            if content_length:
                content_length = int(content_length)
        return content_length, etag
    if context.stdin is not None:
        return headers.get('etag')

def cli_put(context, path):
    """
    Performs a PUT on the item (account, container, or object).

    See :py:mod:`swiftly.cli.put` for context usage information.

    See :py:class:`CLIPut` for more information.
    """
    path = path.lstrip('/') if path else ''
    if context.input_ and os.path.isdir(context.input_):
        return cli_put_directory_structure(context, path)
    if not path:
        return cli_put_account(context)
    elif '/' not in path.rstrip('/'):
        return cli_put_container(context, path)
    else:
        return cli_put_object(context, path)

def _get_segment_path(prefix, n):
    """
    Returns segment path for nth segment
    """
    return '%s%08d' % (prefix, n)

def _get_manifest_body(context, prefix, path2info, put_headers):
    """
    Returns body for manifest file and modifies put_headers.

    path2info is a dict like {"path": (size, etag)}
    """
    if context.static_segments:
        body = json.dumps([
            {'path': '/' + p, 'size_bytes': s, 'etag': e}
            for p, (s, e) in sorted(six.iteritems(path2info))
        ])
        put_headers['content-length'] = str(len(body))
        context.query['multipart-manifest'] = 'put'
    else:
        body = ''
        put_headers['content-length'] = '0'
        put_headers['x-object-manifest'] = prefix

    return body

def _create_container(context, path, l_mtime, size):
    """
    Creates container for segments of file with `path`
    """
    new_context = context.copy()
    new_context.input_ = None
    new_context.headers = None
    new_context.query = None
    container = path.split('/', 1)[0] + '_segments'
    cli_put_container(new_context, container)
    prefix = container + '/' + path.split('/', 1)[1]
    prefix = '%s/%s/%s/' % (prefix, l_mtime, size)

    return prefix

class CLIPut(CLICommand):
    """
    A CLICommand that can issue PUT requests.

    See the output of ``swiftly help put`` for more information.
    """

    def __init__(self, cli):
        super(CLIPut, self).__init__(
            cli, 'put', max_args=1, usage="""
Usage: %prog [main_options] put [options] [path]

For help on [main_options] run %prog with no args.

Performs a PUT request on the <path> given. If the <path> is an object, the
contents for the object are read from standard input.

Special Note About Segmented Objects:

For object uploads exceeding the -s [size] (default: 5G) the object will be
uploaded in segments. At this time, auto-segmenting only works for objects
uploaded from source files -- objects sourced from standard input cannot exceed
the maximum object size for the cluster.

A segmented object is one that has its contents in several other objects. On
download, these other objects are concatenated into a single object stream.

Segmented objects can be useful to greatly exceed the maximum single object
size, speed up uploading large objects with concurrent segment uploading, and
provide the option to replace, insert, and delete segments within a whole
object without having to alter or reupload any of the other segments.

The main object of a segmented object is called the "manifest object". This
object just has an X-Object-Manifest header that points to another path where
the segments for the object contents are stored. For Swiftly, this header value
is auto-generated as the same name as the manifest object, but with "_segments"
added to the container name. This keeps the segments out of the main container
listing, which is often useful.

By default, Swift's dynamic large object support is used since it was
implemented first. However, if you prefix the [size] with an 's', as in '-s
s1048576' Swiftly will use static large object support. These static large
objects are very similar as described above, except the manifest contains a
static list of the object segments. For more information on the tradeoffs, see
http://greg.brim.net/post/2013/05/16/1834.html""".strip())
        self.option_parser.add_option(
            '-h', '-H', '--header', dest='header', action='append',
            metavar='HEADER:VALUE',
            help='Add a header to the request. This can be used multiple '
                 'times for multiple headers. Examples: '
                 '-hx-object-meta-color:blue -h "Content-Type: text/html"')
        self.option_parser.add_option(
            '-q', '--query', dest='query', action='append',
            metavar='NAME[=VALUE]',
            help='Add a query parameter to the request. This can be used '
                 'multiple times for multiple query parameters. Example: '
                 '-qmultipart-manifest=get')
        self.option_parser.add_option(
            '-i', '--input', dest='input_', metavar='PATH',
            help='Indicates where to read the contents from; default is '
                 'standard input. If the PATH is a directory, all files in '
                 'the directory will be uploaded as similarly named objects '
                 'and empty directories will create text/directory marker '
                 'objects. Use a dash (as in "-i -") to specify standard '
                 'input for account and container PUTs, as those do not '
                 'normally take input. This is useful with '
                 '-qextract-archive=<format> bulk upload requests. For '
                 'example: tar zc . | swiftly put -qextract-archive=tar.gz -i '
                 '- container')
        self.option_parser.add_option(
            '-n', '--newer', dest='newer', action='store_true',
            help='For PUTs with an --input option, first performs a HEAD on '
                 'the object and compares the X-Object-Meta-Mtime header with '
                 'the modified time of the PATH obtained from the --input '
                 'option and then PUTs the object only if the local time is '
                 'newer. When the --input PATH is a directory, this offers an '
                 'easy way to upload only the newer files since the last '
                 'upload (at the expense of HEAD requests). NOTE THAT THIS '
                 'WILL NOT UPLOAD CHANGED FILES THAT DO NOT HAVE A NEWER '
                 'LOCAL MODIFIED TIME! NEWER does not mean DIFFERENT.')
        self.option_parser.add_option(
            '-d', '--different', dest='different', action='store_true',
            help='For PUTs with an --input option, first performs a HEAD on '
                 'the object and compares the X-Object-Meta-Mtime header with '
                 'the modified time of the PATH obtained from the --input '
                 'option and then PUTs the object only if the local time is '
                 'different. It will also check the local and remote sizes '
                 'and PUT if they differ. ETag/MD5sum checking are not done '
                 '(an option may be provided in the future) since this is '
                 'usually much more disk intensive. When the --input PATH is '
                 'a directory, this offers an easy way to upload only the '
                 'differing files since the last upload (at the expense of '
                 'HEAD requests). NOTE THAT THIS CAN UPLOAD OLDER FILES OVER '
                 'NEWER ONES! DIFFERENT does not mean NEWER.')
        self.option_parser.add_option(
            '-e', '--empty', dest='empty', action='store_true',
            help='Indicates a zero-byte object should be PUT.')
        self.option_parser.add_option(
            '-s', '--segment-size', dest='segment_size', metavar='BYTES',
            help='Indicates the maximum size of an object before uploading it '
                 'as a segmented object. See full help text for more '
                 'information.')
        self.option_parser.add_option(
            '--stdin-segmentation', dest='stdin_segmentation', action='store_true',
            help='Separate STDIN data into segments. This will separate data'
            'even if segment size is not exceeded.')
        self.option_parser.add_option(
            '--encrypt', dest='encrypt', metavar='KEY',
            help='Will encrypt the uploaded object data with KEY. This '
                 'currently uses AES 256 in CBC mode but other algorithms may '
                 'be offered in the future. You may specify a single dash "-" '
                 'as the KEY and instead the KEY will be loaded from the '
                 'SWIFTLY_CRYPT_KEY environment variable.')

    def __call__(self, args):
        options, args, context = self.parse_args_and_create_context(args)
        context.headers = self.options_list_to_lowered_dict(options.header)
        context.query = self.options_list_to_lowered_dict(options.query)
        context.input_ = options.input_
        context.segment_size = options.segment_size
        context.static_segments = False
        if context.segment_size and context.segment_size[0].lower() == 's':
            context.static_segments = True
            context.segment_size = context.segment_size[1:]
        context.segment_size = int(
            context.segment_size or 5 * 1024 * 1024 * 1024)
        if context.segment_size < 1:
            raise ReturnCode('invalid segment size %s' % options.segment_size)
        context.stdin_segmentation = options.stdin_segmentation
        context.empty = options.empty
        context.newer = options.newer
        context.different = options.different
        context.encrypt = options.encrypt
        if context.encrypt == '-':
            context.encrypt = os.environ.get('SWIFTLY_CRYPT_KEY')
            if not context.encrypt:
                raise ReturnCode(
                    'A single dash "-" was given as the encryption key, but '
                    'no key was found in the SWIFTLY_CRYPT_KEY environment '
                    'variable.')
        if context.encrypt and context.different:
            raise ReturnCode(
                '--different will not work properly with --encrypt since '
                'encryption may change the object size')
        path = args.pop(0).lstrip('/') if args else None
        return cli_put(context, path)