Repository: conan-io/conan

Issue:
[SVN] Export of readonly conanfile.py results in permission denied error
To help us debug your issue please explain:

- [x] I've read the [CONTRIBUTING guide](https://raw.githubusercontent.com/conan-io/conan/develop/.github/CONTRIBUTING.md).
- [x] I've specified the Conan version, operating system version and any tool that can be relevant.
- [x] I've explained the steps to reproduce the error or the motivation/use case of the question/suggestion.

I am using Conan 1.8.0-dev, Win10 & Python3.6.

When having an readonly `conanfile.py` file (SVN locking mechanism make them readonly), we get the follwing error message:
`ERROR: [Errno 13] Permission denied: 'C:\\...\\user\\svn25\\export\\conanfile.py'`

Because `_export_conanfile` function uses `shutil.copy2` all stat info including readonly are taken. May be add an option, to ignore readonly flag.



Discussion:
The error was detected, by playing around with SVN feature (#3192). But it could also happen with every read-only file.
We need to check this, if this happens for any read-only file, it is more like a bug than an issue related to an experimental feature (SCM for SVN). 
It looks like, that it only happens for SCM (Git & SVN) modules. I tried it with a normal local file system and had no success to get an exception. But I am still no expert on Conan...
PR merged! It will be fixed in next 1.9 release. Thanks!
I'm reopening it as there is an ongoing discussion here: https://github.com/conan-io/conan/pull/3748#issuecomment-432730079. I don't like to _close_ v1.9 without noticing what is the decision taken there.
This has been reverted, because it is not possible to change permissions in that way.
It is not clear why is happening with SVN and not with other RO permissions. We need to further investigate it, but we are opening 1.9 branch today, so moving this to investigate for 1.10.
I'm trying to reproduce it in Mac and I'm not able to. It seems to happen only in Windows.

Having a locked file:

```
⇒  svn lock conanfile.py
svn: warning: W160035: Path '/conanfile.py' is already locked by user 'jgsogo' in filesystem '5d1b4008-cece-4d52-b44a-c9b3911d192e'
svn: E200009: One or more locks could not be obtained
```

and exporting the recipe twice, it works:

```
⇒  conan export . issue/test                                  
DEBUG :export.py      [45]: Exporting /Users/jgsogo/dev/conan/issues/3748/repo-wc/repo/conanfile.py [2018-10-29 11:50:52,479]
Exporting package recipe
name/ver@issue/test: WARN: Repo origin looks like a local path: file:///Users/jgsogo/dev/conan/issues/3748/repo@2
name/ver@issue/test: Repo origin deduced by 'auto': file:///Users/jgsogo/dev/conan/issues/3748/repo@2
name/ver@issue/test: Revision deduced by 'auto': 2
name/ver@issue/test: The stored package has not changed


⇒  conan export . issue/test
DEBUG :export.py      [45]: Exporting /Users/jgsogo/dev/conan/issues/3748/repo-wc/repo/conanfile.py [2018-10-29 11:50:56,463]
Exporting package recipe
name/ver@issue/test: WARN: Repo origin looks like a local path: file:///Users/jgsogo/dev/conan/issues/3748/repo@2
name/ver@issue/test: Repo origin deduced by 'auto': file:///Users/jgsogo/dev/conan/issues/3748/repo@2
name/ver@issue/test: Revision deduced by 'auto': 2
name/ver@issue/test: The stored package has not changed
``` 

Resolving patch:
diff --git a/conans/client/cmd/export.py b/conans/client/cmd/export.py
--- a/conans/client/cmd/export.py
+++ b/conans/client/cmd/export.py
@@ -12,7 +12,7 @@
 from conans.model.scm import SCM, get_scm_data
 from conans.paths import CONANFILE, CONAN_MANIFEST
 from conans.search.search import search_recipes
-from conans.util.files import is_dirty, load, mkdir, rmdir, save, set_dirty
+from conans.util.files import is_dirty, load, mkdir, rmdir, save, set_dirty, remove
 from conans.util.log import logger
 
 
@@ -144,6 +144,8 @@ def _replace_scm_data_in_conanfile(conanfile_path, scm_data):
     new_text = "scm = " + ",\n          ".join(str(scm_data).split(",")) + "\n"
     content = content.replace(to_replace[0], new_text)
     content = content if not headers else ''.join(headers) + content
+
+    remove(conanfile_path)
     save(conanfile_path, content)
 
 
diff --git a/conans/util/files.py b/conans/util/files.py
--- a/conans/util/files.py
+++ b/conans/util/files.py
@@ -1,3 +1,4 @@
+import errno
 import hashlib
 import os
 import platform
@@ -7,8 +8,6 @@
 import sys
 import tarfile
 import tempfile
-from contextlib import contextmanager
-from errno import ENOENT
 from os.path import abspath, join as joinpath, realpath
 
 import six
@@ -207,7 +206,19 @@ def rmdir(path):
     try:
         shutil.rmtree(path, onerror=_change_permissions)
     except OSError as err:
-        if err.errno == ENOENT:
+        if err.errno == errno.ENOENT:
+            return
+        raise
+
+
+def remove(path):
+    try:
+        assert os.path.isfile(path)
+        os.remove(path)
+    except (IOError, OSError) as e:  # for py3, handle just PermissionError
+        if e.errno == errno.EPERM or e.errno == errno.EACCES:
+            os.chmod(path, stat.S_IRWXU)
+            os.remove(path)
             return
         raise
 


Test patch:
diff --git a/conans/test/functional/scm/scm_test.py b/conans/test/functional/scm/scm_test.py
--- a/conans/test/functional/scm/scm_test.py
+++ b/conans/test/functional/scm/scm_test.py
@@ -5,6 +5,7 @@
 
 from nose.plugins.attrib import attr
 
+from conans.client.tools.files import chdir
 from conans.client.tools.scm import Git, SVN
 from conans.client.tools.win import get_cased_path
 from conans.model.ref import ConanFileReference, PackageReference
@@ -852,3 +853,25 @@ def test_scm_serialization(self):
         the_json = str(scm_data)
         data2 = json.loads(the_json)
         self.assertEquals(data, data2)
+
+
+@attr('svn')
+class SCMSVNWithLockedFilesTest(SVNLocalRepoTestCase):
+
+    def test_propset_own(self):
+        """ Apply svn:needs-lock property to every file in the own working-copy of the repository """
+
+        conanfile = base_svn.format(directory="None", url="auto", revision="auto")
+        project_url, _ = self.create_project(files={"conanfile.py": conanfile,})
+        project_url = project_url.replace(" ", "%20")
+
+        # Add property needs-lock to my own copy
+        client = TestClient()
+        client.runner('svn co "{url}" "{path}"'.format(url=project_url,
+                                                       path=client.current_folder))
+        with chdir(client.current_folder):
+            for item in ['conanfile.py', ]:
+                client.runner('svn propset svn:needs-lock yes {}'.format(item))
+            client.runner('svn commit -m "lock some files"')
+
+        client.run("export . user/channel")
diff --git a/conans/test/unittests/util/files/__init__.py b/conans/test/unittests/util/files/__init__.py
new file mode 100644
diff --git a/conans/test/unittests/util/files/test_remove.py b/conans/test/unittests/util/files/test_remove.py
new file mode 100644
--- /dev/null
+++ b/conans/test/unittests/util/files/test_remove.py
@@ -0,0 +1,34 @@
+# coding=utf-8
+
+import os
+import stat
+import unittest
+
+from conans.test.utils.test_files import temp_folder
+from conans.util.files import remove, save
+
+
+class RemoveTest(unittest.TestCase):
+
+    def setUp(self):
+        self.file = os.path.join(temp_folder(), 'file.txt')
+        save(self.file, "some content")
+
+    def test_remove(self):
+        remove(self.file)
+        self.assertFalse(os.path.exists(self.file))
+        self.assertTrue(os.path.exists(os.path.dirname(self.file)))
+
+    def test_remove_readonly(self):
+        os.chmod(self.file, stat.S_IREAD|stat.S_IRGRP|stat.S_IROTH)
+        with self.assertRaisesRegexp((IOError, OSError), "Permission denied"):
+            save(self.file, "change the content")
+        remove(self.file)
+        self.assertFalse(os.path.exists(self.file))
+        self.assertTrue(os.path.exists(os.path.dirname(self.file)))
+
+    def test_remove_folder(self):
+        dirname = os.path.dirname(self.file)
+        self.assertRaises(AssertionError, remove, dirname)
+        self.assertTrue(os.path.exists(dirname))
+