Repository: mandarons/icloud-drive-docker (python)

Issue:
[BUG] Folder mentioned in the "ignore" section of config.yaml gets downloaded
**Describe the bug**
Folder mentioned in the `ignore` section is still downloaded

**To Reproduce**
Added folder to the `ignore` section of `config.yaml`.

**Expected behavior**
Ignored folder should not be downloaded.

**Screenshots**
N/A

**Configuration**
N/A

**Additional context**
Here's the yaml in question. I am expecting "Music" to not be downloaded, yet it was.
```
drive:
  destination: "/media/icloud/drive"
  remove_obsolete: true
  sync_interval: 300
  filters: # Optional - use it only if you want to download specific folders.
    # File filters to be included in syncing iCloud drive content
    folders:
      - "Data\ Store"
    file_extensions:
      # File extensions to be included
      # - "pdf"
  ignore:
      - "Data Store/Music/*"
      # - "*.md"
```
I've tried variations like:
* `"Music"`
* `"Data Store/Music/"`
* `"Data\ Store/Music"`
They all result in the Music folder being downloaded.

Pull request:
Replaced ignore path match with pathlib
Fixes #180 

Patch:
diff --git a/requirements.txt b/requirements.txt
index 5b28758b..cd0d6ee1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,4 +2,3 @@ icloudpy==0.5.0
 ruamel.yaml==0.16.12
 python-magic==0.4.27
 requests~=2.28.1
-pathspec~=0.11.0
\ No newline at end of file
diff --git a/src/notify.py b/src/notify.py
index b203ab85..df03b3a0 100644
--- a/src/notify.py
+++ b/src/notify.py
@@ -41,7 +41,7 @@ def post_message_to_telegram(bot_token, chat_id, message):
     """Post message to telegram bot using API."""
     url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
     params = {"chat_id": chat_id, "text": message}
-    response = requests.post(url, params=params)
+    response = requests.post(url, params=params, timeout=10)
     if response.status_code == 200:
         return True
     else:
diff --git a/src/sync_drive.py b/src/sync_drive.py
index 41be71c5..510ab980 100644
--- a/src/sync_drive.py
+++ b/src/sync_drive.py
@@ -7,12 +7,11 @@ import re
 import time
 import unicodedata
 import zipfile
-from pathlib import Path
+from pathlib import Path, PurePath
 from shutil import copyfileobj, rmtree
 
 import magic
 from icloudpy import exceptions
-from pathspec import PathSpec
 
 from src import LOGGER, config_parser
 
@@ -22,7 +21,7 @@ def wanted_file(filters, ignore, file_path):
     if not file_path:
         return False
     if ignore:
-        if PathSpec.from_lines("gitwildmatch", ignore).match_file(file_path):
+        if ignored_path(ignore, file_path):
             LOGGER.debug(f"Skipping the unwanted file {file_path}")
             return False
     if not filters or len(filters) == 0:
@@ -37,8 +36,9 @@ def wanted_file(filters, ignore, file_path):
 def wanted_folder(filters, ignore, root, folder_path):
     """Check if folder is wanted."""
     if ignore:
-        if PathSpec.from_lines("gitwildmatch", ignore).match_file(f"{folder_path}/"):
+        if ignored_path(ignore, folder_path):
             return False
+
     if not filters or not folder_path or not root or len(filters) == 0:
         # Nothing to filter, return True
         return True
@@ -59,7 +59,15 @@ def wanted_folder(filters, ignore, root, folder_path):
     return False
 
 
-def wanted_parent_folder(filters, root, folder_path):
+def ignored_path(ignore_list, path):
+    """Check if path is ignored."""
+    for ignore in ignore_list:
+        if PurePath(path).match(ignore + "*" if ignore.endswith("/") else ignore):
+            return True
+    return False
+
+
+def wanted_parent_folder(filters, ignore, root, folder_path):
     """Check if parent folder is wanted."""
     if not filters or not folder_path or not root or len(filters) == 0:
         return True
@@ -300,6 +308,7 @@ def sync_directory(
                     filters=filters["folders"]
                     if filters and "folders" in filters
                     else None,
+                    ignore=ignore,
                     root=root,
                     folder_path=destination_path,
                 ):


Test patch:
diff --git a/tests/test_notify.py b/tests/test_notify.py
index ffd87586..d03fde3d 100644
--- a/tests/test_notify.py
+++ b/tests/test_notify.py
@@ -217,6 +217,7 @@ class TestNotify(unittest.TestCase):
             post_mock.assert_called_once_with(
                 "https://api.telegram.org/botbot_token/sendMessage",
                 params={"chat_id": "chat_id", "text": "message"},
+                timeout=10,
             )
 
     def test_post_message_to_telegram_fail(self):
@@ -229,4 +230,5 @@ class TestNotify(unittest.TestCase):
             post_mock.assert_called_once_with(
                 "https://api.telegram.org/botbot_token/sendMessage",
                 params={"chat_id": "chat_id", "text": "message"},
+                timeout=10,
             )
diff --git a/tests/test_sync_drive.py b/tests/test_sync_drive.py
index aa81fb8e..7f266ca6 100644
--- a/tests/test_sync_drive.py
+++ b/tests/test_sync_drive.py
@@ -53,6 +53,7 @@ class TestSyncDrive(unittest.TestCase):
         self.assertTrue(
             sync_drive.wanted_parent_folder(
                 filters=None,
+                ignore=None,
                 root=self.root,
                 folder_path=os.path.join(self.root, "dir1/dir11"),
             )
@@ -63,6 +64,7 @@ class TestSyncDrive(unittest.TestCase):
         self.filters["folders"] = ["dir1/dir11"]
         self.assertTrue(
             sync_drive.wanted_parent_folder(
+                ignore=None,
                 filters=self.filters["folders"],
                 root=self.root,
                 folder_path=os.path.join(self.root, "dir1/dir11/some/dirs/file.ext"),
@@ -74,6 +76,7 @@ class TestSyncDrive(unittest.TestCase):
         self.filters["folders"] = ["dir1/dir11"]
         self.assertFalse(
             sync_drive.wanted_parent_folder(
+                ignore=None,
                 filters=self.filters["folders"],
                 root=self.root,
                 folder_path=os.path.join(self.root, "dir1"),
@@ -372,7 +375,7 @@ class TestSyncDrive(unittest.TestCase):
                 filters=None,
                 ignore=self.ignore,
                 root=self.root,
-                folder_path=os.path.join(self.root, "dir1", "dir2"),
+                folder_path=os.path.join(self.root, "dir1", "dir2", "dir3"),
             )
         )
         self.assertFalse(
@@ -408,7 +411,7 @@ class TestSyncDrive(unittest.TestCase):
                 filters=None,
                 ignore=self.ignore,
                 root=self.root,
-                folder_path=os.path.join(self.root, "dir1", "dir2"),
+                folder_path=os.path.join(self.root, "dir1", "dir2", "dir3"),
             )
         )
         self.assertFalse(
@@ -437,7 +440,7 @@ class TestSyncDrive(unittest.TestCase):
                 filters=self.filters["folders"],
                 ignore=self.ignore,
                 root=self.root,
-                folder_path=os.path.join(self.root, "dir2"),
+                folder_path=os.path.join(self.root, "dir2", "dir3"),
             )
         )
 
@@ -1327,3 +1330,54 @@ class TestSyncDrive(unittest.TestCase):
                     )
                 )
             )
+
+    @patch(target="keyring.get_password", return_value=data.VALID_PASSWORD)
+    @patch(
+        target="src.config_parser.get_username", return_value=data.AUTHENTICATED_USER
+    )
+    @patch("icloudpy.ICloudPyService")
+    @patch("src.read_config")
+    def test_child_ignored_folder(
+        self, mock_read_config, mock_service, mock_get_username, mock_get_password
+    ):
+        """Test for child ignored folder."""
+        mock_service = self.service
+        config = self.config.copy()
+        config["drive"]["destination"] = self.destination_path
+        config["drive"]["ignore"] = ["icloudpy/*"]
+        mock_read_config.return_value = config
+        self.assertIsNotNone(
+            sync_drive.sync_drive(config=config, drive=mock_service.drive)
+        )
+        self.assertFalse(
+            os.path.exists(os.path.join(self.destination_path, "icloudpy", "Test"))
+        )
+        self.assertTrue(os.path.isdir(os.path.join(self.destination_path, "Obsidian")))
+        self.assertTrue(
+            os.path.isdir(os.path.join(self.destination_path, "Obsidian", "Sample"))
+        )
+        self.assertTrue(
+            os.path.isfile(
+                os.path.join(
+                    self.destination_path, "Obsidian", "Sample", "This is a title.md"
+                )
+            )
+        )
+        self.assertEqual(
+            sum(
+                f.stat().st_size
+                for f in Path(
+                    os.path.join(
+                        self.destination_path, "Obsidian", "Sample", "Project.band"
+                    )
+                ).glob("**/*")
+                if f.is_file()
+            ),
+            sum(
+                f.stat().st_size
+                for f in Path(
+                    os.path.join(tests.DATA_DIR, "Project_original.band")
+                ).glob("**/*")
+                if f.is_file()
+            ),
+        )