Repository: conda/conda

Issue:
put ftp:// back into 4.2



Discussion:

Resolving patch:
diff --git a/conda/connection.py b/conda/connection.py
--- a/conda/connection.py
+++ b/conda/connection.py
@@ -6,6 +6,10 @@
 
 from __future__ import print_function, division, absolute_import
 
+from base64 import b64decode
+
+import ftplib
+
 import cgi
 import email
 import mimetypes
@@ -16,10 +20,12 @@
 from io import BytesIO
 from logging import getLogger
 
+from conda.exceptions import AuthenticationError
 from . import __version__ as VERSION
 from .base.context import context, platform as context_platform
-from .common.url import url_to_path, url_to_s3_info
+from .common.url import url_to_path, url_to_s3_info, urlparse
 from .utils import gnu_get_libc_version
+from .compat import StringIO
 
 RETRIES = 3
 
@@ -195,6 +201,196 @@ def close(self):
         pass
 
 
+# Taken from requests-ftp
+# (https://github.com/Lukasa/requests-ftp/blob/master/requests_ftp/ftp.py)
+
+# Copyright 2012 Cory Benfield
+
+# 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 FTPAdapter(requests.adapters.BaseAdapter):
+    '''A Requests Transport Adapter that handles FTP urls.'''
+    def __init__(self):
+        super(FTPAdapter, self).__init__()
+
+        # Build a dictionary keyed off the methods we support in upper case.
+        # The values of this dictionary should be the functions we use to
+        # send the specific queries.
+        self.func_table = {'LIST': self.list,
+                           'RETR': self.retr,
+                           'STOR': self.stor,
+                           'NLST': self.nlst,
+                           'GET': self.retr}
+
+    def send(self, request, **kwargs):
+        '''Sends a PreparedRequest object over FTP. Returns a response object.
+        '''
+        # Get the authentication from the prepared request, if any.
+        auth = self.get_username_password_from_header(request)
+
+        # Next, get the host and the path.
+        host, port, path = self.get_host_and_path_from_url(request)
+
+        # Sort out the timeout.
+        timeout = kwargs.get('timeout', None)
+
+        # Establish the connection and login if needed.
+        self.conn = ftplib.FTP()
+        self.conn.connect(host, port, timeout)
+
+        if auth is not None:
+            self.conn.login(auth[0], auth[1])
+        else:
+            self.conn.login()
+
+        # Get the method and attempt to find the function to call.
+        resp = self.func_table[request.method](path, request)
+
+        # Return the response.
+        return resp
+
+    def close(self):
+        '''Dispose of any internal state.'''
+        # Currently this is a no-op.
+        pass
+
+    def list(self, path, request):
+        '''Executes the FTP LIST command on the given path.'''
+        data = StringIO()
+
+        # To ensure the StringIO gets cleaned up, we need to alias its close
+        # method to the release_conn() method. This is a dirty hack, but there
+        # you go.
+        data.release_conn = data.close
+
+        self.conn.cwd(path)
+        code = self.conn.retrbinary('LIST', data_callback_factory(data))
+
+        # When that call has finished executing, we'll have all our data.
+        response = build_text_response(request, data, code)
+
+        # Close the connection.
+        self.conn.close()
+
+        return response
+
+    def retr(self, path, request):
+        '''Executes the FTP RETR command on the given path.'''
+        data = BytesIO()
+
+        # To ensure the BytesIO gets cleaned up, we need to alias its close
+        # method. See self.list().
+        data.release_conn = data.close
+
+        code = self.conn.retrbinary('RETR ' + path, data_callback_factory(data))
+
+        response = build_binary_response(request, data, code)
+
+        # Close the connection.
+        self.conn.close()
+
+        return response
+
+    def stor(self, path, request):
+        '''Executes the FTP STOR command on the given path.'''
+
+        # First, get the file handle. We assume (bravely)
+        # that there is only one file to be sent to a given URL. We also
+        # assume that the filename is sent as part of the URL, not as part of
+        # the files argument. Both of these assumptions are rarely correct,
+        # but they are easy.
+        data = parse_multipart_files(request)
+
+        # Split into the path and the filename.
+        path, filename = os.path.split(path)
+
+        # Switch directories and upload the data.
+        self.conn.cwd(path)
+        code = self.conn.storbinary('STOR ' + filename, data)
+
+        # Close the connection and build the response.
+        self.conn.close()
+
+        response = build_binary_response(request, BytesIO(), code)
+
+        return response
+
+    def nlst(self, path, request):
+        '''Executes the FTP NLST command on the given path.'''
+        data = StringIO()
+
+        # Alias the close method.
+        data.release_conn = data.close
+
+        self.conn.cwd(path)
+        code = self.conn.retrbinary('NLST', data_callback_factory(data))
+
+        # When that call has finished executing, we'll have all our data.
+        response = build_text_response(request, data, code)
+
+        # Close the connection.
+        self.conn.close()
+
+        return response
+
+    def get_username_password_from_header(self, request):
+        '''Given a PreparedRequest object, reverse the process of adding HTTP
+        Basic auth to obtain the username and password. Allows the FTP adapter
+        to piggyback on the basic auth notation without changing the control
+        flow.'''
+        auth_header = request.headers.get('Authorization')
+
+        if auth_header:
+            # The basic auth header is of the form 'Basic xyz'. We want the
+            # second part. Check that we have the right kind of auth though.
+            encoded_components = auth_header.split()[:2]
+            if encoded_components[0] != 'Basic':
+                raise AuthenticationError('Invalid form of Authentication used.')
+            else:
+                encoded = encoded_components[1]
+
+            # Decode the base64 encoded string.
+            decoded = b64decode(encoded)
+
+            # The string is of the form 'username:password'. Split on the
+            # colon.
+            components = decoded.split(':')
+            username = components[0]
+            password = components[1]
+            return (username, password)
+        else:
+            # No auth header. Return None.
+            return None
+
+    def get_host_and_path_from_url(self, request):
+        '''Given a PreparedRequest object, split the URL in such a manner as to
+        determine the host and the path. This is a separate method to wrap some
+        of urlparse's craziness.'''
+        url = request.url
+        # scheme, netloc, path, params, query, fragment = urlparse(url)
+        parsed = urlparse(url)
+        path = parsed.path
+
+        # If there is a slash on the front of the path, chuck it.
+        if path[0] == '/':
+            path = path[1:]
+
+        host = parsed.hostname
+        port = parsed.port or 0
+
+        return (host, port, path)
+
+
 def data_callback_factory(variable):
     '''Returns a callback suitable for use by the FTP library. This callback
     will repeatedly save data into the variable provided to this function. This
diff --git a/conda/exceptions.py b/conda/exceptions.py
--- a/conda/exceptions.py
+++ b/conda/exceptions.py
@@ -203,6 +203,10 @@ def __init__(self, message, *args, **kwargs):
         super(CondaHTTPError, self).__init__(msg, *args, **kwargs)
 
 
+class AuthenticationError(CondaError):
+    pass
+
+
 class NoPackagesFoundError(CondaError, RuntimeError):
     '''An exception to report that requested packages are missing.
 


Test patch: