We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
SSH remote permission error
Hi

I get a permission error when trying to push to an ssh remote.
I assume it is due to the fact, that I don't have full permission on the remote (only from /cluster/data/user onwards)

As far as I can see dvc tries to create directories from the root.
https://github.com/iterative/dvc/blob/be012e70cf9b16ec73b0fbeb0f2308558c2a10dd/dvc/remote/ssh/connection.py#L96-L110

Is this the intention or do i get something wrong?

I tried using password and keyfile.

Config:
['remote "cluster"']
url = ssh://user@host/cluster/data/user
user = user
keyfile = path-to-keyfile

Info:
dvc version 0.60.0
macos
pip installed

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `dvc/remote/ssh/connection.py`
Content:
```
1 import os
2 import posixpath
3 import logging
4 import errno
5 import stat
6 from funcy import cached_property
7 
8 try:
9     import paramiko
10 except ImportError:
11     paramiko = None
12 
13 from dvc.utils import tmp_fname
14 from dvc.utils.compat import ignore_file_not_found
15 from dvc.progress import Tqdm
16 from dvc.exceptions import DvcException
17 from dvc.remote.base import RemoteCmdError
18 
19 
20 logger = logging.getLogger(__name__)
21 
22 
23 def sizeof_fmt(num, suffix="B"):
24     """ Convert number of bytes to human-readable string """
25     for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]:
26         if abs(num) < 1024.0:
27             return "%3.1f%s%s" % (num, unit, suffix)
28         num /= 1024.0
29     return "%.1f%s%s" % (num, "Y", suffix)
30 
31 
32 class SSHConnection:
33     def __init__(self, host, *args, **kwargs):
34         logger.debug(
35             "Establishing ssh connection with '{host}' "
36             "through port '{port}' as user '{username}'".format(
37                 host=host, **kwargs
38             )
39         )
40         self.timeout = kwargs.get("timeout", 1800)
41 
42         self._ssh = paramiko.SSHClient()
43         self._ssh.load_system_host_keys()
44         self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
45 
46         self._ssh.connect(host, *args, **kwargs)
47         self._ssh.get_transport().set_keepalive(10)
48         self._sftp_channels = []
49 
50     @property
51     def sftp(self):
52         if not self._sftp_channels:
53             self._sftp_channels = [self._ssh.open_sftp()]
54         return self._sftp_channels[0]
55 
56     def close(self):
57         for sftp in self._sftp_channels:
58             sftp.close()
59         self._ssh.close()
60 
61     def st_mode(self, path):
62         with ignore_file_not_found():
63             return self.sftp.lstat(path).st_mode
64 
65         return 0
66 
67     def getsize(self, path):
68         with ignore_file_not_found():
69             return self.sftp.lstat(path).st_size
70 
71         return 0
72 
73     def exists(self, path, sftp=None):
74         return bool(self.st_mode(path))
75 
76     def isdir(self, path):
77         return stat.S_ISDIR(self.st_mode(path))
78 
79     def isfile(self, path):
80         return stat.S_ISREG(self.st_mode(path))
81 
82     def islink(self, path):
83         return stat.S_ISLNK(self.st_mode(path))
84 
85     def makedirs(self, path):
86         # Single stat call will say whether this is a dir, a file or a link
87         st_mode = self.st_mode(path)
88 
89         if stat.S_ISDIR(st_mode):
90             return
91 
92         if stat.S_ISREG(st_mode) or stat.S_ISLNK(st_mode):
93             raise DvcException(
94                 "a file with the same name '{}' already exists".format(path)
95             )
96 
97         head, tail = posixpath.split(path)
98 
99         if head:
100             self.makedirs(head)
101 
102         if tail:
103             try:
104                 self.sftp.mkdir(path)
105             except IOError as e:
106                 # Since paramiko errors are very vague we need to recheck
107                 # whether it's because path already exists or something else
108                 if e.errno == errno.EACCES or not self.exists(path):
109                     raise
110 
111     def walk(self, directory, topdown=True):
112         # NOTE: original os.walk() implementation [1] with default options was
113         # used as a template.
114         #
115         # [1] https://github.com/python/cpython/blob/master/Lib/os.py
116         try:
117             dir_entries = self.sftp.listdir_attr(directory)
118         except IOError as exc:
119             raise DvcException(
120                 "couldn't get the '{}' remote directory files list".format(
121                     directory
122                 ),
123                 cause=exc,
124             )
125 
126         dirs = []
127         nondirs = []
128         for entry in dir_entries:
129             name = entry.filename
130             if stat.S_ISDIR(entry.st_mode):
131                 dirs.append(name)
132             else:
133                 nondirs.append(name)
134 
135         if topdown:
136             yield directory, dirs, nondirs
137 
138         for dname in dirs:
139             newpath = posixpath.join(directory, dname)
140             for entry in self.walk(newpath, topdown=topdown):
141                 yield entry
142 
143         if not topdown:
144             yield directory, dirs, nondirs
145 
146     def walk_files(self, directory):
147         for root, dirs, files in self.walk(directory):
148             for fname in files:
149                 yield posixpath.join(root, fname)
150 
151     def _remove_file(self, path):
152         with ignore_file_not_found():
153             self.sftp.remove(path)
154 
155     def _remove_dir(self, path):
156         for root, dirs, files in self.walk(path, topdown=False):
157             for fname in files:
158                 path = posixpath.join(root, fname)
159                 with ignore_file_not_found():
160                     self._remove_file(path)
161 
162             for dname in dirs:
163                 path = posixpath.join(root, dname)
164                 with ignore_file_not_found():
165                     self.sftp.rmdir(dname)
166 
167         with ignore_file_not_found():
168             self.sftp.rmdir(path)
169 
170     def remove(self, path):
171         if self.isdir(path):
172             self._remove_dir(path)
173         else:
174             self._remove_file(path)
175 
176     def download(self, src, dest, no_progress_bar=False, progress_title=None):
177         with Tqdm(
178             desc=progress_title or os.path.basename(src),
179             disable=no_progress_bar,
180             bytes=True,
181         ) as pbar:
182             self.sftp.get(src, dest, callback=pbar.update_to)
183 
184     def move(self, src, dst):
185         self.makedirs(posixpath.dirname(dst))
186         self.sftp.rename(src, dst)
187 
188     def upload(self, src, dest, no_progress_bar=False, progress_title=None):
189         self.makedirs(posixpath.dirname(dest))
190         tmp_file = tmp_fname(dest)
191         if not progress_title:
192             progress_title = posixpath.basename(dest)
193 
194         with Tqdm(
195             desc=progress_title, disable=no_progress_bar, bytes=True
196         ) as pbar:
197             self.sftp.put(src, tmp_file, callback=pbar.update_to)
198 
199         self.sftp.rename(tmp_file, dest)
200 
201     def execute(self, cmd):
202         stdin, stdout, stderr = self._ssh.exec_command(cmd)
203         channel = stdout.channel
204 
205         stdin.close()
206         channel.shutdown_write()
207 
208         stdout_chunks = []
209         stderr_chunks = []
210         while (
211             not channel.closed
212             or channel.recv_ready()
213             or channel.recv_stderr_ready()
214         ):
215             import select
216 
217             got_chunk = False
218             readq, _, _ = select.select([stdout.channel], [], [], self.timeout)
219             for c in readq:
220                 if c.recv_ready():
221                     stdout_chunks.append(stdout.channel.recv(len(c.in_buffer)))
222                     got_chunk = True
223 
224                 if c.recv_stderr_ready():
225                     stderr_len = len(c.in_stderr_buffer)
226                     s = stderr.channel.recv_stderr(stderr_len)
227                     stderr_chunks.append(s)
228                     got_chunk = True
229 
230             if (
231                 not got_chunk
232                 and stdout.channel.exit_status_ready()
233                 and not stderr.channel.recv_stderr_ready()
234                 and not stdout.channel.recv_ready()
235             ):
236                 stdout.channel.shutdown_read()
237                 stdout.channel.close()
238                 break
239 
240         stdout.close()
241         stderr.close()
242 
243         ret = stdout.channel.recv_exit_status()
244         if ret != 0:
245             err = b"".join(stderr_chunks).decode("utf-8")
246             raise RemoteCmdError("ssh", cmd, ret, err)
247 
248         return b"".join(stdout_chunks).decode("utf-8")
249 
250     @cached_property
251     def uname(self):
252         return self.execute("uname").strip()
253 
254     def md5(self, path):
255         """
256         Use different md5 commands depending on the OS:
257 
258          - Darwin's `md5` returns BSD-style checksums by default
259          - Linux's `md5sum` needs the `--tag` flag for a similar output
260 
261          Example:
262               MD5 (foo.txt) = f3d220a856b52aabbf294351e8a24300
263         """
264         if self.uname == "Linux":
265             md5 = self.execute("md5sum " + path).split()[0]
266         elif self.uname == "Darwin":
267             md5 = self.execute("md5 " + path).split()[-1]
268         else:
269             raise DvcException(
270                 "'{}' is not supported as a SSH remote".format(self.uname)
271             )
272 
273         assert len(md5) == 32
274         return md5
275 
276     def copy(self, src, dest):
277         self.execute("cp {} {}".format(src, dest))
278 
279     def open_max_sftp_channels(self):
280         # If there are more than 1 it means we've already opened max amount
281         if len(self._sftp_channels) <= 1:
282             while True:
283                 try:
284                     self._sftp_channels.append(self._ssh.open_sftp())
285                 except paramiko.ssh_exception.ChannelException:
286                     if not self._sftp_channels:
287                         raise
288                     break
289         return self._sftp_channels
290 
291     def open(self, *args, **kwargs):
292         return self.sftp.open(*args, **kwargs)
293 
294     def symlink(self, src, dest):
295         self.sftp.symlink(src, dest)
296 
297     def reflink(self, src, dest):
298         if self.uname == "Linux":
299             return self.execute("cp --reflink {} {}".format(src, dest))
300 
301         if self.uname == "Darwin":
302             return self.execute("cp -c {} {}".format(src, dest))
303 
304         raise DvcException(
305             "'{}' is not supported as a SSH remote".format(self.uname)
306         )
307 
308     def hardlink(self, src, dest):
309         self.execute("ln {} {}".format(src, dest))
310
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/dvc/remote/ssh/connection.py b/dvc/remote/ssh/connection.py
--- a/dvc/remote/ssh/connection.py
+++ b/dvc/remote/ssh/connection.py
@@ -106,7 +106,10 @@
                 # Since paramiko errors are very vague we need to recheck
                 # whether it's because path already exists or something else
                 if e.errno == errno.EACCES or not self.exists(path):
-                    raise
+                    raise DvcException(
+                        "unable to create remote directory '{}'".format(path),
+                        cause=e,
+                    )
 
     def walk(self, directory, topdown=True):
         # NOTE: original os.walk() implementation [1] with default options was

{"golden_diff": "diff --git a/dvc/remote/ssh/connection.py b/dvc/remote/ssh/connection.py\n--- a/dvc/remote/ssh/connection.py\n+++ b/dvc/remote/ssh/connection.py\n@@ -106,7 +106,10 @@\n                 # Since paramiko errors are very vague we need to recheck\n                 # whether it's because path already exists or something else\n                 if e.errno == errno.EACCES or not self.exists(path):\n-                    raise\n+                    raise DvcException(\n+                        \"unable to create remote directory '{}'\".format(path),\n+                        cause=e,\n+                    )\n \n     def walk(self, directory, topdown=True):\n         # NOTE: original os.walk() implementation [1] with default options was\n", "issue": "SSH remote permission error\nHi\r\n\r\nI get a permission error when trying to push to an ssh remote.\r\nI assume it is due to the fact, that I don't have full permission on the remote (only from /cluster/data/user onwards)\r\n\r\nAs far as I can see dvc tries to create directories from the root.\r\nhttps://github.com/iterative/dvc/blob/be012e70cf9b16ec73b0fbeb0f2308558c2a10dd/dvc/remote/ssh/connection.py#L96-L110\r\n\r\nIs this the intention or do i get something wrong?\r\n\r\nI tried using password and keyfile.\r\n\r\nConfig:\r\n['remote \"cluster\"']\r\nurl = ssh://user@host/cluster/data/user\r\nuser = user\r\nkeyfile = path-to-keyfile\r\n\r\nInfo:\r\ndvc version 0.60.0\r\nmacos\r\npip installed\n", "before_files": [{"content": "import os\nimport posixpath\nimport logging\nimport errno\nimport stat\nfrom funcy import cached_property\n\ntry:\n    import paramiko\nexcept ImportError:\n    paramiko = None\n\nfrom dvc.utils import tmp_fname\nfrom dvc.utils.compat import ignore_file_not_found\nfrom dvc.progress import Tqdm\nfrom dvc.exceptions import DvcException\nfrom dvc.remote.base import RemoteCmdError\n\n\nlogger = logging.getLogger(__name__)\n\n\ndef sizeof_fmt(num, suffix=\"B\"):\n    \"\"\" Convert number of bytes to human-readable string \"\"\"\n    for unit in [\"\", \"K\", \"M\", \"G\", \"T\", \"P\", \"E\", \"Z\"]:\n        if abs(num) < 1024.0:\n            return \"%3.1f%s%s\" % (num, unit, suffix)\n        num /= 1024.0\n    return \"%.1f%s%s\" % (num, \"Y\", suffix)\n\n\nclass SSHConnection:\n    def __init__(self, host, *args, **kwargs):\n        logger.debug(\n            \"Establishing ssh connection with '{host}' \"\n            \"through port '{port}' as user '{username}'\".format(\n                host=host, **kwargs\n            )\n        )\n        self.timeout = kwargs.get(\"timeout\", 1800)\n\n        self._ssh = paramiko.SSHClient()\n        self._ssh.load_system_host_keys()\n        self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())\n\n        self._ssh.connect(host, *args, **kwargs)\n        self._ssh.get_transport().set_keepalive(10)\n        self._sftp_channels = []\n\n    @property\n    def sftp(self):\n        if not self._sftp_channels:\n            self._sftp_channels = [self._ssh.open_sftp()]\n        return self._sftp_channels[0]\n\n    def close(self):\n        for sftp in self._sftp_channels:\n            sftp.close()\n        self._ssh.close()\n\n    def st_mode(self, path):\n        with ignore_file_not_found():\n            return self.sftp.lstat(path).st_mode\n\n        return 0\n\n    def getsize(self, path):\n        with ignore_file_not_found():\n            return self.sftp.lstat(path).st_size\n\n        return 0\n\n    def exists(self, path, sftp=None):\n        return bool(self.st_mode(path))\n\n    def isdir(self, path):\n        return stat.S_ISDIR(self.st_mode(path))\n\n    def isfile(self, path):\n        return stat.S_ISREG(self.st_mode(path))\n\n    def islink(self, path):\n        return stat.S_ISLNK(self.st_mode(path))\n\n    def makedirs(self, path):\n        # Single stat call will say whether this is a dir, a file or a link\n        st_mode = self.st_mode(path)\n\n        if stat.S_ISDIR(st_mode):\n            return\n\n        if stat.S_ISREG(st_mode) or stat.S_ISLNK(st_mode):\n            raise DvcException(\n                \"a file with the same name '{}' already exists\".format(path)\n            )\n\n        head, tail = posixpath.split(path)\n\n        if head:\n            self.makedirs(head)\n\n        if tail:\n            try:\n                self.sftp.mkdir(path)\n            except IOError as e:\n                # Since paramiko errors are very vague we need to recheck\n                # whether it's because path already exists or something else\n                if e.errno == errno.EACCES or not self.exists(path):\n                    raise\n\n    def walk(self, directory, topdown=True):\n        # NOTE: original os.walk() implementation [1] with default options was\n        # used as a template.\n        #\n        # [1] https://github.com/python/cpython/blob/master/Lib/os.py\n        try:\n            dir_entries = self.sftp.listdir_attr(directory)\n        except IOError as exc:\n            raise DvcException(\n                \"couldn't get the '{}' remote directory files list\".format(\n                    directory\n                ),\n                cause=exc,\n            )\n\n        dirs = []\n        nondirs = []\n        for entry in dir_entries:\n            name = entry.filename\n            if stat.S_ISDIR(entry.st_mode):\n                dirs.append(name)\n            else:\n                nondirs.append(name)\n\n        if topdown:\n            yield directory, dirs, nondirs\n\n        for dname in dirs:\n            newpath = posixpath.join(directory, dname)\n            for entry in self.walk(newpath, topdown=topdown):\n                yield entry\n\n        if not topdown:\n            yield directory, dirs, nondirs\n\n    def walk_files(self, directory):\n        for root, dirs, files in self.walk(directory):\n            for fname in files:\n                yield posixpath.join(root, fname)\n\n    def _remove_file(self, path):\n        with ignore_file_not_found():\n            self.sftp.remove(path)\n\n    def _remove_dir(self, path):\n        for root, dirs, files in self.walk(path, topdown=False):\n            for fname in files:\n                path = posixpath.join(root, fname)\n                with ignore_file_not_found():\n                    self._remove_file(path)\n\n            for dname in dirs:\n                path = posixpath.join(root, dname)\n                with ignore_file_not_found():\n                    self.sftp.rmdir(dname)\n\n        with ignore_file_not_found():\n            self.sftp.rmdir(path)\n\n    def remove(self, path):\n        if self.isdir(path):\n            self._remove_dir(path)\n        else:\n            self._remove_file(path)\n\n    def download(self, src, dest, no_progress_bar=False, progress_title=None):\n        with Tqdm(\n            desc=progress_title or os.path.basename(src),\n            disable=no_progress_bar,\n            bytes=True,\n        ) as pbar:\n            self.sftp.get(src, dest, callback=pbar.update_to)\n\n    def move(self, src, dst):\n        self.makedirs(posixpath.dirname(dst))\n        self.sftp.rename(src, dst)\n\n    def upload(self, src, dest, no_progress_bar=False, progress_title=None):\n        self.makedirs(posixpath.dirname(dest))\n        tmp_file = tmp_fname(dest)\n        if not progress_title:\n            progress_title = posixpath.basename(dest)\n\n        with Tqdm(\n            desc=progress_title, disable=no_progress_bar, bytes=True\n        ) as pbar:\n            self.sftp.put(src, tmp_file, callback=pbar.update_to)\n\n        self.sftp.rename(tmp_file, dest)\n\n    def execute(self, cmd):\n        stdin, stdout, stderr = self._ssh.exec_command(cmd)\n        channel = stdout.channel\n\n        stdin.close()\n        channel.shutdown_write()\n\n        stdout_chunks = []\n        stderr_chunks = []\n        while (\n            not channel.closed\n            or channel.recv_ready()\n            or channel.recv_stderr_ready()\n        ):\n            import select\n\n            got_chunk = False\n            readq, _, _ = select.select([stdout.channel], [], [], self.timeout)\n            for c in readq:\n                if c.recv_ready():\n                    stdout_chunks.append(stdout.channel.recv(len(c.in_buffer)))\n                    got_chunk = True\n\n                if c.recv_stderr_ready():\n                    stderr_len = len(c.in_stderr_buffer)\n                    s = stderr.channel.recv_stderr(stderr_len)\n                    stderr_chunks.append(s)\n                    got_chunk = True\n\n            if (\n                not got_chunk\n                and stdout.channel.exit_status_ready()\n                and not stderr.channel.recv_stderr_ready()\n                and not stdout.channel.recv_ready()\n            ):\n                stdout.channel.shutdown_read()\n                stdout.channel.close()\n                break\n\n        stdout.close()\n        stderr.close()\n\n        ret = stdout.channel.recv_exit_status()\n        if ret != 0:\n            err = b\"\".join(stderr_chunks).decode(\"utf-8\")\n            raise RemoteCmdError(\"ssh\", cmd, ret, err)\n\n        return b\"\".join(stdout_chunks).decode(\"utf-8\")\n\n    @cached_property\n    def uname(self):\n        return self.execute(\"uname\").strip()\n\n    def md5(self, path):\n        \"\"\"\n        Use different md5 commands depending on the OS:\n\n         - Darwin's `md5` returns BSD-style checksums by default\n         - Linux's `md5sum` needs the `--tag` flag for a similar output\n\n         Example:\n              MD5 (foo.txt) = f3d220a856b52aabbf294351e8a24300\n        \"\"\"\n        if self.uname == \"Linux\":\n            md5 = self.execute(\"md5sum \" + path).split()[0]\n        elif self.uname == \"Darwin\":\n            md5 = self.execute(\"md5 \" + path).split()[-1]\n        else:\n            raise DvcException(\n                \"'{}' is not supported as a SSH remote\".format(self.uname)\n            )\n\n        assert len(md5) == 32\n        return md5\n\n    def copy(self, src, dest):\n        self.execute(\"cp {} {}\".format(src, dest))\n\n    def open_max_sftp_channels(self):\n        # If there are more than 1 it means we've already opened max amount\n        if len(self._sftp_channels) <= 1:\n            while True:\n                try:\n                    self._sftp_channels.append(self._ssh.open_sftp())\n                except paramiko.ssh_exception.ChannelException:\n                    if not self._sftp_channels:\n                        raise\n                    break\n        return self._sftp_channels\n\n    def open(self, *args, **kwargs):\n        return self.sftp.open(*args, **kwargs)\n\n    def symlink(self, src, dest):\n        self.sftp.symlink(src, dest)\n\n    def reflink(self, src, dest):\n        if self.uname == \"Linux\":\n            return self.execute(\"cp --reflink {} {}\".format(src, dest))\n\n        if self.uname == \"Darwin\":\n            return self.execute(\"cp -c {} {}\".format(src, dest))\n\n        raise DvcException(\n            \"'{}' is not supported as a SSH remote\".format(self.uname)\n        )\n\n    def hardlink(self, src, dest):\n        self.execute(\"ln {} {}\".format(src, dest))\n", "path": "dvc/remote/ssh/connection.py"}], "after_files": [{"content": "import os\nimport posixpath\nimport logging\nimport errno\nimport stat\nfrom funcy import cached_property\n\ntry:\n    import paramiko\nexcept ImportError:\n    paramiko = None\n\nfrom dvc.utils import tmp_fname\nfrom dvc.utils.compat import ignore_file_not_found\nfrom dvc.progress import Tqdm\nfrom dvc.exceptions import DvcException\nfrom dvc.remote.base import RemoteCmdError\n\n\nlogger = logging.getLogger(__name__)\n\n\ndef sizeof_fmt(num, suffix=\"B\"):\n    \"\"\" Convert number of bytes to human-readable string \"\"\"\n    for unit in [\"\", \"K\", \"M\", \"G\", \"T\", \"P\", \"E\", \"Z\"]:\n        if abs(num) < 1024.0:\n            return \"%3.1f%s%s\" % (num, unit, suffix)\n        num /= 1024.0\n    return \"%.1f%s%s\" % (num, \"Y\", suffix)\n\n\nclass SSHConnection:\n    def __init__(self, host, *args, **kwargs):\n        logger.debug(\n            \"Establishing ssh connection with '{host}' \"\n            \"through port '{port}' as user '{username}'\".format(\n                host=host, **kwargs\n            )\n        )\n        self.timeout = kwargs.get(\"timeout\", 1800)\n\n        self._ssh = paramiko.SSHClient()\n        self._ssh.load_system_host_keys()\n        self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())\n\n        self._ssh.connect(host, *args, **kwargs)\n        self._ssh.get_transport().set_keepalive(10)\n        self._sftp_channels = []\n\n    @property\n    def sftp(self):\n        if not self._sftp_channels:\n            self._sftp_channels = [self._ssh.open_sftp()]\n        return self._sftp_channels[0]\n\n    def close(self):\n        for sftp in self._sftp_channels:\n            sftp.close()\n        self._ssh.close()\n\n    def st_mode(self, path):\n        with ignore_file_not_found():\n            return self.sftp.lstat(path).st_mode\n\n        return 0\n\n    def getsize(self, path):\n        with ignore_file_not_found():\n            return self.sftp.lstat(path).st_size\n\n        return 0\n\n    def exists(self, path, sftp=None):\n        return bool(self.st_mode(path))\n\n    def isdir(self, path):\n        return stat.S_ISDIR(self.st_mode(path))\n\n    def isfile(self, path):\n        return stat.S_ISREG(self.st_mode(path))\n\n    def islink(self, path):\n        return stat.S_ISLNK(self.st_mode(path))\n\n    def makedirs(self, path):\n        # Single stat call will say whether this is a dir, a file or a link\n        st_mode = self.st_mode(path)\n\n        if stat.S_ISDIR(st_mode):\n            return\n\n        if stat.S_ISREG(st_mode) or stat.S_ISLNK(st_mode):\n            raise DvcException(\n                \"a file with the same name '{}' already exists\".format(path)\n            )\n\n        head, tail = posixpath.split(path)\n\n        if head:\n            self.makedirs(head)\n\n        if tail:\n            try:\n                self.sftp.mkdir(path)\n            except IOError as e:\n                # Since paramiko errors are very vague we need to recheck\n                # whether it's because path already exists or something else\n                if e.errno == errno.EACCES or not self.exists(path):\n                    raise DvcException(\n                        \"unable to create remote directory '{}'\".format(path),\n                        cause=e,\n                    )\n\n    def walk(self, directory, topdown=True):\n        # NOTE: original os.walk() implementation [1] with default options was\n        # used as a template.\n        #\n        # [1] https://github.com/python/cpython/blob/master/Lib/os.py\n        try:\n            dir_entries = self.sftp.listdir_attr(directory)\n        except IOError as exc:\n            raise DvcException(\n                \"couldn't get the '{}' remote directory files list\".format(\n                    directory\n                ),\n                cause=exc,\n            )\n\n        dirs = []\n        nondirs = []\n        for entry in dir_entries:\n            name = entry.filename\n            if stat.S_ISDIR(entry.st_mode):\n                dirs.append(name)\n            else:\n                nondirs.append(name)\n\n        if topdown:\n            yield directory, dirs, nondirs\n\n        for dname in dirs:\n            newpath = posixpath.join(directory, dname)\n            for entry in self.walk(newpath, topdown=topdown):\n                yield entry\n\n        if not topdown:\n            yield directory, dirs, nondirs\n\n    def walk_files(self, directory):\n        for root, dirs, files in self.walk(directory):\n            for fname in files:\n                yield posixpath.join(root, fname)\n\n    def _remove_file(self, path):\n        with ignore_file_not_found():\n            self.sftp.remove(path)\n\n    def _remove_dir(self, path):\n        for root, dirs, files in self.walk(path, topdown=False):\n            for fname in files:\n                path = posixpath.join(root, fname)\n                with ignore_file_not_found():\n                    self._remove_file(path)\n\n            for dname in dirs:\n                path = posixpath.join(root, dname)\n                with ignore_file_not_found():\n                    self.sftp.rmdir(dname)\n\n        with ignore_file_not_found():\n            self.sftp.rmdir(path)\n\n    def remove(self, path):\n        if self.isdir(path):\n            self._remove_dir(path)\n        else:\n            self._remove_file(path)\n\n    def download(self, src, dest, no_progress_bar=False, progress_title=None):\n        with Tqdm(\n            desc=progress_title or os.path.basename(src),\n            disable=no_progress_bar,\n            bytes=True,\n        ) as pbar:\n            self.sftp.get(src, dest, callback=pbar.update_to)\n\n    def move(self, src, dst):\n        self.makedirs(posixpath.dirname(dst))\n        self.sftp.rename(src, dst)\n\n    def upload(self, src, dest, no_progress_bar=False, progress_title=None):\n        self.makedirs(posixpath.dirname(dest))\n        tmp_file = tmp_fname(dest)\n        if not progress_title:\n            progress_title = posixpath.basename(dest)\n\n        with Tqdm(\n            desc=progress_title, disable=no_progress_bar, bytes=True\n        ) as pbar:\n            self.sftp.put(src, tmp_file, callback=pbar.update_to)\n\n        self.sftp.rename(tmp_file, dest)\n\n    def execute(self, cmd):\n        stdin, stdout, stderr = self._ssh.exec_command(cmd)\n        channel = stdout.channel\n\n        stdin.close()\n        channel.shutdown_write()\n\n        stdout_chunks = []\n        stderr_chunks = []\n        while (\n            not channel.closed\n            or channel.recv_ready()\n            or channel.recv_stderr_ready()\n        ):\n            import select\n\n            got_chunk = False\n            readq, _, _ = select.select([stdout.channel], [], [], self.timeout)\n            for c in readq:\n                if c.recv_ready():\n                    stdout_chunks.append(stdout.channel.recv(len(c.in_buffer)))\n                    got_chunk = True\n\n                if c.recv_stderr_ready():\n                    stderr_len = len(c.in_stderr_buffer)\n                    s = stderr.channel.recv_stderr(stderr_len)\n                    stderr_chunks.append(s)\n                    got_chunk = True\n\n            if (\n                not got_chunk\n                and stdout.channel.exit_status_ready()\n                and not stderr.channel.recv_stderr_ready()\n                and not stdout.channel.recv_ready()\n            ):\n                stdout.channel.shutdown_read()\n                stdout.channel.close()\n                break\n\n        stdout.close()\n        stderr.close()\n\n        ret = stdout.channel.recv_exit_status()\n        if ret != 0:\n            err = b\"\".join(stderr_chunks).decode(\"utf-8\")\n            raise RemoteCmdError(\"ssh\", cmd, ret, err)\n\n        return b\"\".join(stdout_chunks).decode(\"utf-8\")\n\n    @cached_property\n    def uname(self):\n        return self.execute(\"uname\").strip()\n\n    def md5(self, path):\n        \"\"\"\n        Use different md5 commands depending on the OS:\n\n         - Darwin's `md5` returns BSD-style checksums by default\n         - Linux's `md5sum` needs the `--tag` flag for a similar output\n\n         Example:\n              MD5 (foo.txt) = f3d220a856b52aabbf294351e8a24300\n        \"\"\"\n        if self.uname == \"Linux\":\n            md5 = self.execute(\"md5sum \" + path).split()[0]\n        elif self.uname == \"Darwin\":\n            md5 = self.execute(\"md5 \" + path).split()[-1]\n        else:\n            raise DvcException(\n                \"'{}' is not supported as a SSH remote\".format(self.uname)\n            )\n\n        assert len(md5) == 32\n        return md5\n\n    def copy(self, src, dest):\n        self.execute(\"cp {} {}\".format(src, dest))\n\n    def open_max_sftp_channels(self):\n        # If there are more than 1 it means we've already opened max amount\n        if len(self._sftp_channels) <= 1:\n            while True:\n                try:\n                    self._sftp_channels.append(self._ssh.open_sftp())\n                except paramiko.ssh_exception.ChannelException:\n                    if not self._sftp_channels:\n                        raise\n                    break\n        return self._sftp_channels\n\n    def open(self, *args, **kwargs):\n        return self.sftp.open(*args, **kwargs)\n\n    def symlink(self, src, dest):\n        self.sftp.symlink(src, dest)\n\n    def reflink(self, src, dest):\n        if self.uname == \"Linux\":\n            return self.execute(\"cp --reflink {} {}\".format(src, dest))\n\n        if self.uname == \"Darwin\":\n            return self.execute(\"cp -c {} {}\".format(src, dest))\n\n        raise DvcException(\n            \"'{}' is not supported as a SSH remote\".format(self.uname)\n        )\n\n    def hardlink(self, src, dest):\n        self.execute(\"ln {} {}\".format(src, dest))\n", "path": "dvc/remote/ssh/connection.py"}]}