Repository: sanic-org/sanic

Issue:
`RouteExists` thrown when registering a directory and a missing file to the same route.
**Describe the bug**
When registering a route to a file and a directory, if the file doesn't exist a `sanic_routing.exceptions.RouteExists` error is raised. This may be misleading and unintended.

**Code snippet**

`example.html` does not exist in this example. The directory  `./resources/web` does.

```python
app.static('/', './resources/web')
app.static('/', './resources/web/example.html')
```

**Expected behavior**
Currently, if a non existent file is registered to a route, no error is thrown on startup and any attempt to retrieve registered resource raises a `FileNotFoundError: [Errno 2] No such file or directory` and a `404 Not Found` error is displayed in the browser. Instead of this happening, as mentioned before a `sanic_routing.exceptions.RouteExists` is raised on startup which may be misleading and unintended.

**Environment (please complete the following information):**
 - OS: Ubuntu 20.04.2 LTS



Discussion:
This is not really a bug, and is an intended outcome. Sanic is checking to see if a file like that exists. If not, it thinks you are trying to serve from a directory. Just by looking at the path, there is no way to determine if you intend to serve a file or a directory.

I think this is the correct behavior. If anything, perhaps what you really want is an explicit method to control this:

```python
app.static('/', './resources/web', as="dir")
app.static('/', './resources/web/example.html', as="file")
```

With an explicit kwarg, we would not need to run `path.isfile`. This is not really new behavior. If Sanic 20.12 is not functioning this way, I think that is more a problem on the earlier version than with the current.

---

A bit of behind the scenes info...

Let's look at what these two are doing:

```python
app.static("/", "./resources/web")
# Sanic checks this, sees that it is not a file and then converts to:
# "/<__file_uri__:path>"
# meaning that it will look for anything that matches using the `path` param type

app.static('/', './resources/web/example.html')
# If this is indeed a file, Sanic knows that you meant for an explicit path "/"
# There is no ambiguity, and the path does not need to be altered
# If the file does not exist, then like the first example, it tries to convert it to a `path` type
# But, since you already have a path expansion on that base path, there is now ambiguity
# and Sanic raises RouteExists
```
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If this  is incorrect, please respond with an update. Thank you for your contributions.


Resolving patch:
diff --git a/sanic/mixins/routes.py b/sanic/mixins/routes.py
--- a/sanic/mixins/routes.py
+++ b/sanic/mixins/routes.py
@@ -592,6 +592,7 @@ def static(
         strict_slashes=None,
         content_type=None,
         apply=True,
+        resource_type=None,
     ):
         """
         Register a root to serve files from. The input can either be a
@@ -641,6 +642,7 @@ def static(
             host,
             strict_slashes,
             content_type,
+            resource_type,
         )
         self._future_statics.add(static)
 
@@ -836,8 +838,27 @@ def _register_static(
         name = static.name
         # If we're not trying to match a file directly,
         # serve from the folder
-        if not path.isfile(file_or_directory):
+        if not static.resource_type:
+            if not path.isfile(file_or_directory):
+                uri += "/<__file_uri__:path>"
+        elif static.resource_type == "dir":
+            if path.isfile(file_or_directory):
+                raise TypeError(
+                    "Resource type improperly identified as directory. "
+                    f"'{file_or_directory}'"
+                )
             uri += "/<__file_uri__:path>"
+        elif static.resource_type == "file" and not path.isfile(
+            file_or_directory
+        ):
+            raise TypeError(
+                "Resource type improperly identified as file. "
+                f"'{file_or_directory}'"
+            )
+        elif static.resource_type != "file":
+            raise ValueError(
+                "The resource_type should be set to 'file' or 'dir'"
+            )
 
         # special prefix for static files
         # if not static.name.startswith("_static_"):
diff --git a/sanic/models/futures.py b/sanic/models/futures.py
--- a/sanic/models/futures.py
+++ b/sanic/models/futures.py
@@ -52,6 +52,7 @@ class FutureStatic(NamedTuple):
     host: Optional[str]
     strict_slashes: Optional[bool]
     content_type: Optional[bool]
+    resource_type: Optional[str]
 
 
 class FutureSignal(NamedTuple):


Test patch:
diff --git a/tests/test_static.py b/tests/test_static.py
--- a/tests/test_static.py
+++ b/tests/test_static.py
@@ -523,3 +523,56 @@ def test_multiple_statics(app, static_file_directory):
     assert response.body == get_file_content(
         static_file_directory, "python.png"
     )
+
+
+def test_resource_type_default(app, static_file_directory):
+    app.static("/static", static_file_directory)
+    app.static("/file", get_file_path(static_file_directory, "test.file"))
+
+    _, response = app.test_client.get("/static")
+    assert response.status == 404
+
+    _, response = app.test_client.get("/file")
+    assert response.status == 200
+    assert response.body == get_file_content(
+        static_file_directory, "test.file"
+    )
+
+
+def test_resource_type_file(app, static_file_directory):
+    app.static(
+        "/file",
+        get_file_path(static_file_directory, "test.file"),
+        resource_type="file",
+    )
+
+    _, response = app.test_client.get("/file")
+    assert response.status == 200
+    assert response.body == get_file_content(
+        static_file_directory, "test.file"
+    )
+
+    with pytest.raises(TypeError):
+        app.static("/static", static_file_directory, resource_type="file")
+
+
+def test_resource_type_dir(app, static_file_directory):
+    app.static("/static", static_file_directory, resource_type="dir")
+
+    _, response = app.test_client.get("/static/test.file")
+    assert response.status == 200
+    assert response.body == get_file_content(
+        static_file_directory, "test.file"
+    )
+
+    with pytest.raises(TypeError):
+        app.static(
+            "/file",
+            get_file_path(static_file_directory, "test.file"),
+            resource_type="dir",
+        )
+
+
+def test_resource_type_unknown(app, static_file_directory, caplog):
+    with pytest.raises(ValueError):
+        app.static("/static", static_file_directory, resource_type="unknown")