Repository: pydantic/pydantic

Issue:
Pydantic fails to read content of files in secrets_dir on Linux
### Checks

* [x] I added a descriptive title to this issue
* [x] I have searched (google, github) for similar issues and couldn't find anything
* [x] I have read and followed [the docs](https://pydantic-docs.helpmanual.io/) and still think this is a bug

<!-- Sorry to sound so draconian, but every second saved replying to issues is time spend improving pydantic :-) -->

# Bug

To reproduce, on Linux OS: 
- build a config manager that subclasses `BaseSettings`
- add `secrets_dir` config pointing to dir with text file(s), as per docs
- add class attributes to use the files in `secrets_dir`, as per docs
- run the code and inspect/print the attributes
- observe that vars coming from .env file work fine, but those from `secrets_dir` are None, or error if not Optional, and there is no user warning for the `secrets_dir` itself

Repeat the same on MacOS - it should work OK, the content is read correctly.

I have added a repo with Dockerfile replicating the setup: https://github.com/dimitri-b/pydantic-secrets-dir-issue

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.8.2
            pydantic compiled: True
                 install path: /usr/local/lib/python3.8/site-packages/pydantic
               python version: 3.8.12 (default, Sep  3 2021, 02:35:42)  [GCC 8.3.0]
                     platform: Linux-5.10.47-linuxkit-x86_64-with-glibc2.2.5
     optional deps. installed: ['dotenv', 'typing-extensions']
```
<!-- or if you're using pydantic prior to v1.3, manually include: OS, python version and pydantic version -->

<!-- Please read the [docs](https://pydantic-docs.helpmanual.io/) and search through issues to
confirm your bug hasn't already been reported. -->

<!-- Where possible please include a self-contained code snippet describing your bug: -->


```py
import pydantic

class SettingsLocal(pydantic.BaseSettings):
    NORMAL_VAR: Optional[str]
    SECRET_VAR: Optional[str]

    class Config:
        env_file = "./.env"
        secrets_dir = "./secrets-in-project"
...
```



Discussion:

Resolving patch:
diff --git a/pydantic/env_settings.py b/pydantic/env_settings.py
--- a/pydantic/env_settings.py
+++ b/pydantic/env_settings.py
@@ -269,7 +269,11 @@ def __call__(self, settings: BaseSettings) -> Dict[str, Any]:
 
         for field in settings.__fields__.values():
             for env_name in field.field_info.extra['env_names']:
-                path = secrets_path / env_name
+                path = find_case_path(secrets_path, env_name, settings.__config__.case_sensitive)
+                if not path:
+                    # path does not exist, we curently don't return a warning for this
+                    continue
+
                 if path.is_file():
                     secret_value = path.read_text().strip()
                     if field.is_complex():
@@ -279,12 +283,11 @@ def __call__(self, settings: BaseSettings) -> Dict[str, Any]:
                             raise SettingsError(f'error parsing JSON for "{env_name}"') from e
 
                     secrets[field.alias] = secret_value
-                elif path.exists():
+                else:
                     warnings.warn(
                         f'attempted to load secret file "{path}" but found a {path_type(path)} instead.',
                         stacklevel=4,
                     )
-
         return secrets
 
     def __repr__(self) -> str:
@@ -304,3 +307,15 @@ def read_env_file(
         return {k.lower(): v for k, v in file_vars.items()}
     else:
         return file_vars
+
+
+def find_case_path(dir_path: Path, file_name: str, case_sensitive: bool) -> Optional[Path]:
+    """
+    Find a file within path's directory matching filename, optionally ignoring case.
+    """
+    for f in dir_path.iterdir():
+        if f.name == file_name:
+            return f
+        elif not case_sensitive and f.name.lower() == file_name.lower():
+            return f
+    return None


Test patch:
diff --git a/tests/test_settings.py b/tests/test_settings.py
--- a/tests/test_settings.py
+++ b/tests/test_settings.py
@@ -890,6 +890,33 @@ class Config:
     assert Settings().dict() == {'foo': 'foo_secret_value_str'}
 
 
+def test_secrets_case_sensitive(tmp_path):
+    (tmp_path / 'SECRET_VAR').write_text('foo_env_value_str')
+
+    class Settings(BaseSettings):
+        secret_var: Optional[str]
+
+        class Config:
+            secrets_dir = tmp_path
+            case_sensitive = True
+
+    assert Settings().dict() == {'secret_var': None}
+
+
+def test_secrets_case_insensitive(tmp_path):
+    (tmp_path / 'SECRET_VAR').write_text('foo_env_value_str')
+
+    class Settings(BaseSettings):
+        secret_var: Optional[str]
+
+        class Config:
+            secrets_dir = tmp_path
+            case_sensitive = False
+
+    settings = Settings().dict()
+    assert settings == {'secret_var': 'foo_env_value_str'}
+
+
 def test_secrets_path_url(tmp_path):
     (tmp_path / 'foo').write_text('http://www.example.com')
     (tmp_path / 'bar').write_text('snap')
@@ -940,6 +967,7 @@ class Config:
 
     with pytest.raises(ValidationError) as exc_info:
         Settings()
+
     assert exc_info.value.errors() == [{'loc': ('foo',), 'msg': 'field required', 'type': 'value_error.missing'}]