Repository: mlflow/mlflow

Issue:
[WIP] Remove double model directory
## What changes are proposed in this pull request?

When a customer uses an FTP artifact store, the artifact uri is incorrect. 

Correct: `ftp://0.0.0.0:9821/0/543736e8ba4a4b93a84662cf96b043b4/artifacts/model/data`
Incorrect: `ftp://0.0.0.0:9821/0/543736e8ba4a4b93a84662cf96b043b4/artifacts/model/model/data`

## How is this patch tested?

Manual Testing

1. Setup an FTP server and ran a keras model using the FTP as the artifact store

<img width="1843" alt="Screen Shot 2020-04-09 at 3 37 20 PM" src="https://user-images.githubusercontent.com/58712524/78946676-3873d180-7a78-11ea-936d-e10c42df90b1.png">


Automated

## Release Notes

### Is this a user-facing change?

- [x] No. You can skip the rest of this section.
- [ ] Yes. Give a description of this change to be included in the release notes for MLflow users.

(Details in 1-2 sentences. You can just refer to another PR with a description if this PR is part of a larger change.)

### What component(s) does this PR affect?

- [ ] UI
- [ ] CLI
- [ ] API
- [ ] REST-API
- [ ] Examples
- [ ] Docs
- [x] Tracking
- [ ] Projects
- [x] Artifacts
- [ ] Models
- [ ] Scoring
- [ ] Serving
- [ ] R
- [ ] Java
- [ ] Python

### How should the PR be classified in the release notes? Choose one:

- [ ] `rn/breaking-change` - The PR will be mentioned in the "Breaking Changes" section
- [ ] `rn/none` - No description will be included. The PR will be mentioned only by the PR number in the "Small Bugfixes and Documentation Updates" section
- [ ] `rn/feature` - A new user-facing feature worth mentioning in the release notes
- [x] `rn/bug-fix` - A user-facing bug fix worth mentioning in the release notes
- [ ] `rn/documentation` - A user-facing documentation change worth mentioning in the release notes



Discussion:

Resolving patch:
diff --git a/mlflow/store/artifact/ftp_artifact_repo.py b/mlflow/store/artifact/ftp_artifact_repo.py
--- a/mlflow/store/artifact/ftp_artifact_repo.py
+++ b/mlflow/store/artifact/ftp_artifact_repo.py
@@ -77,20 +77,12 @@ def log_artifacts(self, local_dir, artifact_path=None):
         dest_path = posixpath.join(self.path, artifact_path) \
             if artifact_path else self.path
 
-        dest_path = posixpath.join(
-            dest_path, os.path.split(local_dir)[1])
-        dest_path_re = os.path.split(local_dir)[1]
-        if artifact_path:
-            dest_path_re = posixpath.join(
-                artifact_path, os.path.split(local_dir)[1])
-
         local_dir = os.path.abspath(local_dir)
         for (root, _, filenames) in os.walk(local_dir):
             upload_path = dest_path
             if root != local_dir:
                 rel_path = os.path.relpath(root, local_dir)
-                rel_path = relative_path_to_artifact_path(rel_path)
-                upload_path = posixpath.join(dest_path_re, rel_path)
+                upload_path = relative_path_to_artifact_path(rel_path)
             if not filenames:
                 with self.get_ftp_client() as ftp:
                     self._mkdir(ftp, posixpath.join(self.path, upload_path))


Test patch:
diff --git a/tests/store/artifact/test_ftp_artifact_repo.py b/tests/store/artifact/test_ftp_artifact_repo.py
--- a/tests/store/artifact/test_ftp_artifact_repo.py
+++ b/tests/store/artifact/test_ftp_artifact_repo.py
@@ -176,7 +176,8 @@ def test_log_artifact_multiple_calls(ftp_mock, tmpdir):
     assert ftp_mock.storbinary.call_args_list[0][0][0] == 'STOR test2.txt'
 
 
-def test_log_artifacts(ftp_mock, tmpdir):
+@pytest.mark.parametrize("artifact_path", [None, 'dir', 'dir1/dir2'])
+def test_log_artifacts(artifact_path, ftp_mock, tmpdir):
     repo = FTPArtifactRepository("ftp://test_ftp/some/path")
 
     repo.get_ftp_client = MagicMock()
@@ -190,10 +191,14 @@ def test_log_artifacts(ftp_mock, tmpdir):
 
     ftp_mock.cwd = MagicMock(side_effect=[ftplib.error_perm, None, None, None, None, None])
 
-    repo.log_artifacts(subd.strpath)
+    repo.log_artifacts(subd.strpath, artifact_path)
+
+    arg_expected = (
+        '/some/path' if artifact_path is None else posixpath.join('/some/path', artifact_path)
+    )
+    ftp_mock.mkd.assert_any_call(arg_expected)
+    ftp_mock.cwd.assert_any_call(arg_expected)
 
-    ftp_mock.mkd.assert_any_call('/some/path/subdir')
-    ftp_mock.cwd.assert_any_call('/some/path/subdir')
     assert ftp_mock.storbinary.call_count == 3
     storbinary_call_args = sorted([ftp_mock.storbinary.call_args_list[i][0][0] for i in range(3)])
     assert storbinary_call_args == ['STOR a.txt', 'STOR b.txt', 'STOR c.txt']