Repository: CS-SI/eodag (python)

Issue:
flatten_top_directories on single file
The following error is raised when trying to use `flatten_top_directories()` on a directory containing a single file or when downloading a product having a single asset and the download plugin configured with `flatten_top_dirs: true`:
```
Traceback (most recent call last):
  File "/home/sylvain/workspace/eodag-debug/20230207_download_issue.py", line 78, in <module>
    downloaded_path = dag.download(search_results[0])
  File "/home/sylvain/workspace/eodag/eodag/api/core.py", line 1753, in download
    path = product.download(
  File "/home/sylvain/workspace/eodag/eodag/api/product/_product.py", line 313, in download
    fs_path = self.downloader.download(
  File "/home/sylvain/workspace/eodag/eodag/plugins/download/http.py", line 260, in download
    fs_path = self._download_assets(
  File "/home/sylvain/workspace/eodag/eodag/plugins/download/http.py", line 607, in _download_assets
    flatten_top_directories(fs_dir_path)
  File "/home/sylvain/workspace/eodag/eodag/utils/__init__.py", line 1159, in flatten_top_directories
    shutil.copytree(common_subdirs_path, tmp_path)
  File "/usr/lib/python3.8/shutil.py", line 555, in copytree
    with os.scandir(src) as itr:
NotADirectoryError: [Errno 20] Not a directory: '/tmp/foo_path'
```

Pull request:
fix: flatten_top_directories on single file
Fixes #633 
Prevents error raised when trying to use `flatten_top_directories()` on a directory containing a single file or when downloading a product having a single asset with download plugin configured with `flatten_top_dirs: true`.

Patch:
diff --git a/eodag/utils/__init__.py b/eodag/utils/__init__.py
index 35841aaf..d968c733 100644
--- a/eodag/utils/__init__.py
+++ b/eodag/utils/__init__.py
@@ -1148,6 +1148,9 @@ def flatten_top_directories(nested_dir_root, common_subdirs_path=None):
         subpaths_list = [p for p in Path(nested_dir_root).glob("**/*") if p.is_file()]
         common_subdirs_path = os.path.commonpath(subpaths_list)
 
+    if Path(common_subdirs_path).is_file():
+        common_subdirs_path = os.path.dirname(common_subdirs_path)
+
     if nested_dir_root != common_subdirs_path:
         logger.debug(f"Flatten {common_subdirs_path} to {nested_dir_root}")
         tmp_path = mkdtemp()


Test patch:
diff --git a/tests/units/test_download_plugins.py b/tests/units/test_download_plugins.py
index b3514aec..e15a565b 100644
--- a/tests/units/test_download_plugins.py
+++ b/tests/units/test_download_plugins.py
@@ -487,7 +487,7 @@ class TestDownloadPluginHttpRetry(BaseDownloadPluginTest):
                 self.plugin.download(
                     self.product,
                     outputs_prefix=self.output_dir,
-                    wait=0.1 / 60,
+                    wait=0.01 / 60,
                     timeout=0.2 / 60,
                 )
 
diff --git a/tests/units/test_utils.py b/tests/units/test_utils.py
index 04b4ad9b..90697d31 100644
--- a/tests/units/test_utils.py
+++ b/tests/units/test_utils.py
@@ -241,6 +241,20 @@ class TestUtils(unittest.TestCase):
             self.assertIn(Path(nested_dir_root) / "c2" / "bar", dir_content)
             self.assertIn(Path(nested_dir_root) / "c2" / "baz", dir_content)
 
+    def test_flatten_top_dirs_single_file(self):
+        """flatten_top_dirs must flatten directory structure containing a single file"""
+        with TemporaryDirectory() as nested_dir_root:
+            os.makedirs(os.path.join(nested_dir_root, "a", "b", "c1"))
+            # create empty file
+            open(os.path.join(nested_dir_root, "a", "b", "c1", "foo"), "a").close()
+
+            flatten_top_directories(nested_dir_root)
+
+            dir_content = list(Path(nested_dir_root).glob("**/*"))
+
+            self.assertEqual(len(dir_content), 1)
+            self.assertIn(Path(nested_dir_root) / "foo", dir_content)
+
     def test_flatten_top_dirs_given_subdir(self):
         """flatten_top_dirs must flatten directory structure using given subdirectory"""
         with TemporaryDirectory() as nested_dir_root: