Repository: iterative/dvc
Issue: Unexpected error when cache and artifacts are not on the same partition on SSH

### Setup

`pip` installed, version 0.66.1.
Platform: Ubuntu 18.04

### Steps to reproduce

1. Create SSH remote `cache`, and configure it as SSH cache.
2. Create directory on the same computer, but on *other partition*. Add some `test.txt` to this dir.
3. Add this remote directory as SSH remote called `workspace`.
4. Run `dvc add remote://workspace/test.txt -f test.dvc`.

Bash script reproducing error:

```bash
# arrange
workspace=$(mktemp -d)
cache=/tmp/ram

mkdir -p /tmp/ram
sudo mount -t tmpfs -o size=512M tmpfs /tmp/ram  # creating distinct partition for cache

cd $(mktemp -d)

dvc init --no-scm

dvc remote add workspace ssh://localhost/${workspace}
dvc remote add cache ssh://localhost/${cache}
dvc config cache.ssh cache

echo "foo" > ${workspace}/foo

# act
dvc add remote://workspace/foo

# cleanup
sudo umount /tmp/ram
rmdir /tmp/ram
```

### Observed behavior

```
100%|██████████|Add                                                                                          1/1 [00:00<00:00,  1.02file/s]
ERROR: unexpected error - Failure
```

### Expected behavior

I believe that by design DVC should raise error in this case - in order to avoid implicit movement of large files between partitions.

However, the error message should suggest solution:

```
ERROR: File move to cache failed.
Ensure that file remote://workspace/test.txt is on the same partition as SSH cache.
``` 

Some people might by mistake put file to different partition (it happened to me! - such message could save me a bit of time :wink:).

-----

Because I had already debugged/studied `--verbose` DVC to understand the source of the problem, I could try to implement such message through Pull Request.

Changed files:
dvc/remote/ssh/connection.py, tests/unit/remote/ssh/test_connection.py

diff --git a/dvc/remote/ssh/connection.py b/dvc/remote/ssh/connection.py
index dcbb1727c..b2da0b67a 100644
--- a/dvc/remote/ssh/connection.py
+++ b/dvc/remote/ssh/connection.py
@@ -185,8 +185,27 @@ class SSHConnection:
             self.sftp.get(src, dest, callback=pbar.update_to)
 
     def move(self, src, dst):
+        """Rename src to dst, if it is not possible (in case src and dst are
+        on different filesystems) and actual physical copying of data is
+        happening.
+        """
         self.makedirs(posixpath.dirname(dst))
-        self.sftp.rename(src, dst)
+
+        try:
+            self.sftp.rename(src, dst)
+        except OSError:
+            self._atomic_copy(src, dst)
+
+        self.remove(src)
+
+    def _atomic_copy(self, src, dst):
+        tmp = tmp_fname(dst)
+
+        try:
+            self.copy(src, tmp)
+            self.sftp.rename(tmp, dst)
+        finally:
+            self.remove(tmp)
 
     def upload(self, src, dest, no_progress_bar=False, progress_title=None):
         self.makedirs(posixpath.dirname(dest))
diff --git a/tests/unit/remote/ssh/test_connection.py b/tests/unit/remote/ssh/test_connection.py
index f0c12ec21..6b1300ce3 100644
--- a/tests/unit/remote/ssh/test_connection.py
+++ b/tests/unit/remote/ssh/test_connection.py
@@ -111,3 +111,9 @@ def test_hardlink(repo_dir, ssh):
 def test_copy(repo_dir, ssh):
     ssh.copy("foo", "link")
     assert filecmp.cmp("foo", "link")
+
+
+def test_move(repo_dir, ssh):
+    ssh.move("foo", "copy")
+    assert os.path.exists("copy")
+    assert not os.path.exists("foo")