Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Addition of usage with adafruit_templateengine and other minor changes #72

Merged
merged 12 commits into from
Nov 6, 2023
Merged
11 changes: 8 additions & 3 deletions adafruit_httpserver/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ def get(
def get_list(self, field_name: str, *, safe=True) -> List[str]:
return super().get_list(field_name, safe=safe)

def __str__(self) -> str:
return "&".join(
f"{field_name}={value}"
for field_name in self.fields
for value in self.get_list(field_name)
)


class File:
"""
Expand Down Expand Up @@ -466,9 +473,7 @@ def _parse_request_header(

method, path, http_version = start_line.strip().split()

if "?" not in path:
path += "?"

path = path if "?" in path else path + "?"
path, query_string = path.split("?", 1)

query_params = QueryParams(query_string)
Expand Down
6 changes: 3 additions & 3 deletions adafruit_httpserver/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,11 +313,10 @@ def _handle_request(
raise ServingFilesDisabledError

# Method is GET or HEAD, try to serve a file from the filesystem.
if request.method in [GET, HEAD]:
if request.method in (GET, HEAD):
return FileResponse(
request,
filename=request.path,
root_path=self.root_path,
head_only=request.method == HEAD,
)

Expand Down Expand Up @@ -512,7 +511,8 @@ def _debug_response_sent(response: "Response", time_elapsed: float):
# pylint: disable=protected-access
client_ip = response._request.client_address[0]
method = response._request.method
path = response._request.path
query_params = response._request.query_params
path = response._request.path + (f"?{query_params}" if query_params else "")
req_size = len(response._request.raw_request)
status = response._status
res_size = response._size
Expand Down
27 changes: 26 additions & 1 deletion docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ By default ``FileResponse`` looks for the file in the server's ``root_path`` dir
.. literalinclude:: ../examples/home.html
:language: html
:caption: www/home.html
:lines: 5-
:lines: 7-
:linenos:

Tasks between requests
Expand Down Expand Up @@ -170,6 +170,31 @@ Tested on ESP32-S2 Feather.
:emphasize-lines: 26-28,41,52,68,74
:linenos:

Templates
---------

With the help of the ``adafruit_templateengine`` library, it is possible to achieve somewhat of a
server-side rendering of HTML pages.

Instead of using string formatting, you can use templates, which can include more complex logic like loops and conditionals.
This makes it very easy to create dynamic pages, witout using JavaScript and exposing any API endpoints.

Templates also allow splitting the code into multiple files, that can be reused in different places.
You can find more information about the template syntax in the
`adafruit_templateengine documentation <https://docs.circuitpython.org/projects/templateengine/en/latest/>`_.

.. literalinclude:: ../examples/directory_listing.tpl.html
:caption: examples/directory_listing.tpl.html
:language: django
:lines: 9-
:emphasize-lines: 1-2,6,10,15-23,27
:linenos:

.. literalinclude:: ../examples/httpserver_templates.py
:caption: examples/httpserver_templates.py
:emphasize-lines: 12-15,49-55
:linenos:

Form data parsing
---------------------

Expand Down
56 changes: 56 additions & 0 deletions examples/directory_listing.tpl.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<!--
SPDX-FileCopyrightText: 2023 Michal Pokusa

SPDX-License-Identifier: Unlicense
-->

<html lang="en">

{% exec path = context.get("path") %}
{% exec items = context.get("items") %}

<head>
<meta charset="UTF-8">
<title>Directory listing for /{{ path }}</title>
</head>

<body>
<h1>Directory listing for /{{ path }}</h1>

<input type="text" placeholder="Search...">

<ul>
{# Going to parent directory if not alredy in #}
{% if path %}
<li><a href="?path=/{{ "".join(path.split('/')[:-1]) }}">..</a></li>
{% endif %}

{# Listing items #}
{% for item in items %}
<li><a href="?path={{ f'/{path}/{item}' if path else f'/{item}' }}">{{ item }}</a></li>
{% endfor %}

</ul>

{# Script for filtering items #}
<script>
const search = document.querySelector('input');
const items = document.querySelectorAll('li');

search.addEventListener('keyup', (e) => {
const term = e.target.value.toLowerCase();

items.forEach(item => {
const text = item.innerText.toLowerCase();

if (text.indexOf(term) != -1) {
item.style.display = 'list-item';
} else {
item.style.display = 'none';
}
});
});
</script>
</body>

</html>
8 changes: 5 additions & 3 deletions examples/home.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# SPDX-FileCopyrightText: 2023 Michał Pokusa
#
# SPDX-License-Identifier: Unlicense
<!--
SPDX-FileCopyrightText: 2023 Michal Pokusa

SPDX-License-Identifier: Unlicense
-->

<html lang="en">
<head>
Expand Down
63 changes: 63 additions & 0 deletions examples/httpserver_templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# SPDX-FileCopyrightText: 2023 Michal Pokusa
#
# SPDX-License-Identifier: Unlicense
import os
import re

import socketpool
import wifi

from adafruit_httpserver import Server, Request, Response, FileResponse

try:
from adafruit_templateengine import render_template
except ImportError as e:
raise ImportError("This example requires adafruit_templateengine library.") from e


pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, "/static", debug=True)

# Create /static directory if it doesn't exist
try:
os.listdir("/static")
except OSError as e:
raise OSError("Please create a /static directory on the CIRCUITPY drive.") from e


def is_file(path: str):
return (os.stat(path.rstrip("/"))[0] & 0b_11110000_00000000) == 0b_10000000_00000000


@server.route("/")
def directory_listing(request: Request):
path = request.query_params.get("path", "").replace("%20", " ")

# Preventing path traversal by removing all ../ from path
path = re.sub(r"\/(\.\.)\/|\/(\.\.)|(\.\.)\/", "/", path).strip("/")

# If path is a file, return it as a file response
if is_file(f"/static/{path}"):
return FileResponse(request, path)

items = sorted(
[
item + ("" if is_file(f"/static/{path}/{item}") else "/")
for item in os.listdir(f"/static/{path}")
],
key=lambda item: not item.endswith("/"),
)

# Otherwise, return a directory listing
return Response(
request,
render_template(
"directory_listing.tpl.html",
context={"path": path, "items": items},
),
content_type="text/html",
)


# Start the server.
server.serve_forever(str(wifi.radio.ipv4_address))