Repository: conda/conda

Issue:
conda clean -pt with empty package cache and non-root user
I have a root miniconda install at /opt/anaconda.  I ran 

```
conda clean -pt 
```

successfully as root then immediately tried running the same command as a lesser user.  I got this error even though the package cache was empty:

```
Cache location: 
There are no tarballs to remove
An unexpected error has occurred, please consider sending the
following traceback to the conda GitHub issue tracker at:

    https://github.com/conda/conda/issues

Include the output of the command 'conda info' in your report.


Traceback (most recent call last):
  File "/opt/anaconda/envs/anaconda.org/bin/conda", line 5, in <module>
    sys.exit(main())
  File "/opt/anaconda/lib/python2.7/site-packages/conda/cli/main.py", line 195, in main
    args_func(args, p)
  File "/opt/anaconda/lib/python2.7/site-packages/conda/cli/main.py", line 202, in args_func
    args.func(args, p)
  File "/opt/anaconda/lib/python2.7/site-packages/conda/cli/main_clean.py", line 340, in execute
    pkgs_dirs, warnings, totalsize, pkgsizes = find_pkgs()
  File "/opt/anaconda/lib/python2.7/site-packages/conda/cli/main_clean.py", line 166, in find_pkgs
    pkgs = [i for i in listdir(pkgs_dir) if isdir(join(pkgs_dir, i)) and
OSError: [Errno 2] No such file or directory: '/home/user5/envs/.pkgs'
```



Discussion:
Appears somewhat related to #1751.  Fixing #1751 and #1752 is necessary for the future needs of anaconda-build.


Resolving patch:
diff --git a/conda/cli/main_clean.py b/conda/cli/main_clean.py
--- a/conda/cli/main_clean.py
+++ b/conda/cli/main_clean.py
@@ -151,9 +151,13 @@ def rm_tarballs(args, pkgs_dirs, totalsize, verbose=True):
 
     for pkgs_dir in pkgs_dirs:
         for fn in pkgs_dirs[pkgs_dir]:
-            if verbose:
-                print("removing %s" % fn)
-            os.unlink(os.path.join(pkgs_dir, fn))
+            if os.access(os.path.join(pkgs_dir, fn), os.W_OK):
+                if verbose:
+                    print("Removing %s" % fn)
+                os.unlink(os.path.join(pkgs_dir, fn))
+            else:
+                if verbose:
+                    print("WARNING: cannot remove, file permissions: %s" % fn)
 
 
 def find_pkgs():
@@ -163,6 +167,9 @@ def find_pkgs():
 
     pkgs_dirs = defaultdict(list)
     for pkgs_dir in config.pkgs_dirs:
+        if not os.path.exists(pkgs_dir):
+            print("WARNING: {0} does not exist".format(pkgs_dir))
+            continue
         pkgs = [i for i in listdir(pkgs_dir) if isdir(join(pkgs_dir, i)) and
             # Only include actual packages
             isdir(join(pkgs_dir, i, 'info'))]
diff --git a/conda/install.py b/conda/install.py
--- a/conda/install.py
+++ b/conda/install.py
@@ -138,6 +138,13 @@ def _remove_readonly(func, path, excinfo):
     os.chmod(path, stat.S_IWRITE)
     func(path)
 
+def warn_failed_remove(function, path, exc_info):
+    if exc_info[1].errno == errno.EACCES:
+        log.warn( "WARNING: cannot remove, permission denied: %s" % path )
+    elif exc_info[1].errno == errno.ENOTEMPTY:
+        log.warn( "WARNING: cannot remove, not empty: %s" % path )
+    else:
+        log.warn( "WARNING: cannot remove, unknown reason: %s" % path )
 
 def rm_rf(path, max_retries=5, trash=True):
     """
@@ -152,12 +159,15 @@ def rm_rf(path, max_retries=5, trash=True):
         # Note that we have to check if the destination is a link because
         # exists('/path/to/dead-link') will return False, although
         # islink('/path/to/dead-link') is True.
-        os.unlink(path)
+        if os.access(path, os.W_OK):
+            os.unlink(path)
+        else:
+            log.warn("WARNING: cannot remove, permission denied: %s" % path)
 
     elif isdir(path):
         for i in range(max_retries):
             try:
-                shutil.rmtree(path)
+                shutil.rmtree(path, ignore_errors=False, onerror=warn_failed_remove)
                 return
             except OSError as e:
                 msg = "Unable to delete %s\n%s\n" % (path, e)
@@ -189,7 +199,7 @@ def rm_rf(path, max_retries=5, trash=True):
                 log.debug(msg + "Retrying after %s seconds..." % i)
                 time.sleep(i)
         # Final time. pass exceptions to caller.
-        shutil.rmtree(path)
+        shutil.rmtree(path, ignore_errors=False, onerror=warn_failed_remove)
 
 def rm_empty_dir(path):
     """


Test patch:
diff --git a/tests/test_install.py b/tests/test_install.py
--- a/tests/test_install.py
+++ b/tests/test_install.py
@@ -8,7 +8,7 @@
 
 
 from conda import install
-from conda.install import PaddingError, binary_replace, update_prefix
+from conda.install import PaddingError, binary_replace, update_prefix, warn_failed_remove
 
 from .decorators import skip_if_no_mock
 from .helpers import mock
@@ -140,6 +140,11 @@ def generate_mock_isfile(self, value):
         with patch.object(install, 'isfile', return_value=value) as isfile:
             yield isfile
 
+    @contextmanager
+    def generate_mock_os_access(self, value):
+        with patch.object(install.os, 'access', return_value=value) as os_access:
+            yield os_access
+
     @contextmanager
     def generate_mock_unlink(self):
         with patch.object(install.os, 'unlink') as unlink:
@@ -173,25 +178,27 @@ def generate_mock_check_call(self):
             yield check_call
 
     @contextmanager
-    def generate_mocks(self, islink=True, isfile=True, isdir=True, on_win=False):
+    def generate_mocks(self, islink=True, isfile=True, isdir=True, on_win=False, os_access=True):
         with self.generate_mock_islink(islink) as mock_islink:
             with self.generate_mock_isfile(isfile) as mock_isfile:
-                with self.generate_mock_isdir(isdir) as mock_isdir:
-                    with self.generate_mock_unlink() as mock_unlink:
-                        with self.generate_mock_rmtree() as mock_rmtree:
-                            with self.generate_mock_sleep() as mock_sleep:
-                                with self.generate_mock_log() as mock_log:
-                                    with self.generate_mock_on_win(on_win):
-                                        with self.generate_mock_check_call() as check_call:
-                                            yield {
-                                                'islink': mock_islink,
-                                                'isfile': mock_isfile,
-                                                'isdir': mock_isdir,
-                                                'unlink': mock_unlink,
-                                                'rmtree': mock_rmtree,
-                                                'sleep': mock_sleep,
-                                                'log': mock_log,
-                                                'check_call': check_call,
+                with self.generate_mock_os_access(os_access) as mock_os_access:
+                    with self.generate_mock_isdir(isdir) as mock_isdir:
+                        with self.generate_mock_unlink() as mock_unlink:
+                            with self.generate_mock_rmtree() as mock_rmtree:
+                                with self.generate_mock_sleep() as mock_sleep:
+                                    with self.generate_mock_log() as mock_log:
+                                        with self.generate_mock_on_win(on_win):
+                                            with self.generate_mock_check_call() as check_call:
+                                                yield {
+                                                    'islink': mock_islink,
+                                                    'isfile': mock_isfile,
+                                                    'isdir': mock_isdir,
+                                                    'os_access': mock_os_access,
+                                                    'unlink': mock_unlink,
+                                                    'rmtree': mock_rmtree,
+                                                    'sleep': mock_sleep,
+                                                    'log': mock_log,
+                                                    'check_call': check_call,
                                             }
 
     def generate_directory_mocks(self, on_win=False):
@@ -219,6 +226,13 @@ def test_calls_unlink_on_true_islink(self):
             install.rm_rf(some_path)
         mocks['unlink'].assert_called_with(some_path)
 
+    @skip_if_no_mock
+    def test_does_not_call_unlink_on_os_access_false(self):
+        with self.generate_mocks(os_access=False) as mocks:
+            some_path = self.generate_random_path
+            install.rm_rf(some_path)
+        self.assertFalse(mocks['unlink'].called)
+
     @skip_if_no_mock
     def test_does_not_call_isfile_if_islink_is_true(self):
         with self.generate_mocks() as mocks:
@@ -259,7 +273,8 @@ def test_calls_rmtree_at_least_once_on_isdir_true(self):
         with self.generate_directory_mocks() as mocks:
             some_path = self.generate_random_path
             install.rm_rf(some_path)
-        mocks['rmtree'].assert_called_with(some_path)
+        mocks['rmtree'].assert_called_with(
+            some_path, onerror=warn_failed_remove, ignore_errors=False)
 
     @skip_if_no_mock
     def test_calls_rmtree_only_once_on_success(self):
@@ -342,7 +357,7 @@ def test_tries_extra_kwarg_on_windows(self):
             install.rm_rf(random_path)
 
         expected_call_list = [
-            mock.call(random_path),
+            mock.call(random_path, ignore_errors=False, onerror=warn_failed_remove),
             mock.call(random_path, onerror=install._remove_readonly)
         ]
         mocks['rmtree'].assert_has_calls(expected_call_list)