Repository: geopython/pywps
Issue: Should `FileStorage.do_store` create the `outputpath` directory ?

# Description

I'm a bit confused on whether or not PyWPS is expected to create the `outputpath` directory if it does not exist. 

The documentation says *server path where to store output files.*

If the directory does not exist, PyPWS fails in this call [here](https://github.com/geopython/pywps/blob/ced1484299761a834cc1cc854f4d9e9c2856e8b2/pywps/inout/storage/file.py#L85)
but it feels like this failure could be avoided by computing the free space on the drive where the target resides, or the *closest* existing parent directory.  Later in the code, `os.makedirs` is called, suggesting an intent to create recursively the target path. 

So it's not clear to me exactly what the desired behavior is. 

# Environment

- operating system: Ubuntu
- Python version: 3.9
- PyWPS version: 4.4



Changed files:
pywps/inout/storage/file.py, tests/test_storage.py

diff --git a/pywps/inout/storage/file.py b/pywps/inout/storage/file.py
index cd7bfb7..e8a467b 100644
--- a/pywps/inout/storage/file.py
+++ b/pywps/inout/storage/file.py
@@ -69,6 +69,14 @@ class FileStorage(CachedStorage):
         self.copy_function = copy_function
 
     def _do_store(self, output):
+        """Copy output to final storage location.
+
+        - Create output directory
+        - Check available file space
+        - Create output file name, taking care of possible duplicates
+        - Copy / link output in work directory to output directory
+        - Return store type, output path and output URL
+        """
         import platform
         import math
         import tempfile
@@ -78,6 +86,11 @@ class FileStorage(CachedStorage):
 
         request_uuid = output.uuid or uuid.uuid1()
 
+        # Create a target folder for each request
+        target = os.path.join(self.target, str(request_uuid))
+        if not os.path.exists(target):
+            os.makedirs(target)
+
         # st.blksize is not available in windows, skips the validation on windows
         if platform.system() != 'Windows':
             file_block_size = os.stat(file_name).st_blksize
@@ -91,11 +104,6 @@ class FileStorage(CachedStorage):
             if avail_size < actual_file_size:
                 raise NotEnoughStorage('Not enough space in {} to store {}'.format(self.target, file_name))
 
-        # create a target folder for each request
-        target = os.path.join(self.target, str(request_uuid))
-        if not os.path.exists(target):
-            os.makedirs(target)
-
         # build output name
         output_name, suffix = _build_output_name(output)
         # build tempfile in case of duplicates
@@ -116,7 +124,7 @@ class FileStorage(CachedStorage):
         url = self.url("{}/{}".format(request_uuid, just_file_name))
         LOGGER.info('File output URI: {}'.format(url))
 
-        return (STORE_TYPE.PATH, output_name, url)
+        return STORE_TYPE.PATH, output_name, url
 
     @staticmethod
     def copy(src, dst, copy_function=None):
@@ -134,7 +142,7 @@ class FileStorage(CachedStorage):
             try:
                 os.link(src, dst)
             except Exception:
-                LOGGER.warn("Could not create hardlink. Fallback to copy.")
+                LOGGER.warning("Could not create hardlink. Fallback to copy.")
                 FileStorage.copy(src, dst)
         else:
             shutil.copy2(src, dst)
diff --git a/tests/test_storage.py b/tests/test_storage.py
index da4c80b..309f657 100644
--- a/tests/test_storage.py
+++ b/tests/test_storage.py
@@ -2,6 +2,7 @@
 # Copyright 2018 Open Source Geospatial Foundation and others    #
 # licensed under MIT, Please consult LICENSE.txt for details     #
 ##################################################################
+import pytest
 
 from pywps.inout.storage.builder import StorageBuilder
 from pywps.inout.storage.file import FileStorage
@@ -9,26 +10,45 @@ from pywps.inout.storage.s3 import S3Storage
 
 from pywps import configuration
 
+from pathlib import Path
 import unittest
+import tempfile
 
-class StorageBuilderTests(unittest.TestCase):
+
+@pytest.fixture
+def fake_output(tmp_path):
+    class FakeOutput(object):
+        """Fake output object for testing."""
+        def __init__(self):
+            self.identifier = "fake_output"
+            self.file = self._get_file()
+            self.uuid = None
+
+        def _get_file(self):
+            fn = tmp_path / 'file.tiff'
+            fn.touch()
+            return str(fn.absolute())
+
+    return FakeOutput()
+
+
+class TestStorageBuilder():
 
     def test_default_storage(self):
         storage = StorageBuilder.buildStorage()
-        self.assertIsInstance(storage, FileStorage)
-
+        assert isinstance(storage, FileStorage)
 
     def test_s3_storage(self):
         configuration.CONFIG.set('server', 'storagetype', 's3')
         storage = StorageBuilder.buildStorage()
-        self.assertIsInstance(storage, S3Storage)
-
-def load_tests(loader=None, tests=None, pattern=None):
-    """Load local tests
-    """
-    if not loader:
-        loader = unittest.TestLoader()
-    suite_list = [
-        loader.loadTestsFromTestCase(StorageBuilderTests)
-    ]
-    return unittest.TestSuite(suite_list)
\\ No newline at end of file
+        assert isinstance(storage, S3Storage)
+
+    def test_recursive_directory_creation(self, fake_output):
+        """Test that outputpath is created."""
+        configuration.CONFIG.set('server', 'storagetype', 'file')
+        outputpath = Path(tempfile.gettempdir()) / "a" / "b" / "c"
+        configuration.CONFIG.set('server', 'outputpath', str(outputpath))
+        storage = StorageBuilder.buildStorage()
+
+        storage.store(fake_output)
+        assert outputpath.exists()