Skip to content

Commit 1c5c41a

Browse files
authored
Merge pull request #72 from michalpokusa/template-example
Addition of usage with `adafruit_templateengine` and other minor changes
2 parents 5b0f3b4 + 3d01ec4 commit 1c5c41a

File tree

6 files changed

+161
-10
lines changed

6 files changed

+161
-10
lines changed

adafruit_httpserver/request.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ def get(
6464
def get_list(self, field_name: str, *, safe=True) -> List[str]:
6565
return super().get_list(field_name, safe=safe)
6666

67+
def __str__(self) -> str:
68+
return "&".join(
69+
f"{field_name}={value}"
70+
for field_name in self.fields
71+
for value in self.get_list(field_name)
72+
)
73+
6774

6875
class File:
6976
"""
@@ -466,9 +473,7 @@ def _parse_request_header(
466473

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

469-
if "?" not in path:
470-
path += "?"
471-
476+
path = path if "?" in path else path + "?"
472477
path, query_string = path.split("?", 1)
473478

474479
query_params = QueryParams(query_string)

adafruit_httpserver/server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -313,11 +313,10 @@ def _handle_request(
313313
raise ServingFilesDisabledError
314314

315315
# Method is GET or HEAD, try to serve a file from the filesystem.
316-
if request.method in [GET, HEAD]:
316+
if request.method in (GET, HEAD):
317317
return FileResponse(
318318
request,
319319
filename=request.path,
320-
root_path=self.root_path,
321320
head_only=request.method == HEAD,
322321
)
323322

@@ -512,7 +511,8 @@ def _debug_response_sent(response: "Response", time_elapsed: float):
512511
# pylint: disable=protected-access
513512
client_ip = response._request.client_address[0]
514513
method = response._request.method
515-
path = response._request.path
514+
query_params = response._request.query_params
515+
path = response._request.path + (f"?{query_params}" if query_params else "")
516516
req_size = len(response._request.raw_request)
517517
status = response._status
518518
res_size = response._size

docs/examples.rst

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ By default ``FileResponse`` looks for the file in the server's ``root_path`` dir
6868
.. literalinclude:: ../examples/home.html
6969
:language: html
7070
:caption: www/home.html
71-
:lines: 5-
71+
:lines: 7-
7272
:linenos:
7373

7474
Tasks between requests
@@ -170,6 +170,31 @@ Tested on ESP32-S2 Feather.
170170
:emphasize-lines: 26-28,41,52,68,74
171171
:linenos:
172172

173+
Templates
174+
---------
175+
176+
With the help of the ``adafruit_templateengine`` library, it is possible to achieve somewhat of a
177+
server-side rendering of HTML pages.
178+
179+
Instead of using string formatting, you can use templates, which can include more complex logic like loops and conditionals.
180+
This makes it very easy to create dynamic pages, witout using JavaScript and exposing any API endpoints.
181+
182+
Templates also allow splitting the code into multiple files, that can be reused in different places.
183+
You can find more information about the template syntax in the
184+
`adafruit_templateengine documentation <https://docs.circuitpython.org/projects/templateengine/en/latest/>`_.
185+
186+
.. literalinclude:: ../examples/directory_listing.tpl.html
187+
:caption: examples/directory_listing.tpl.html
188+
:language: django
189+
:lines: 9-
190+
:emphasize-lines: 1-2,6,10,15-23,27
191+
:linenos:
192+
193+
.. literalinclude:: ../examples/httpserver_templates.py
194+
:caption: examples/httpserver_templates.py
195+
:emphasize-lines: 12-15,49-55
196+
:linenos:
197+
173198
Form data parsing
174199
---------------------
175200

examples/directory_listing.tpl.html

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<!--
2+
SPDX-FileCopyrightText: 2023 Michal Pokusa
3+
4+
SPDX-License-Identifier: Unlicense
5+
-->
6+
7+
<html lang="en">
8+
9+
{% exec path = context.get("path") %}
10+
{% exec items = context.get("items") %}
11+
12+
<head>
13+
<meta charset="UTF-8">
14+
<title>Directory listing for /{{ path }}</title>
15+
</head>
16+
17+
<body>
18+
<h1>Directory listing for /{{ path }}</h1>
19+
20+
<input type="text" placeholder="Search...">
21+
22+
<ul>
23+
{# Going to parent directory if not alredy in #}
24+
{% if path %}
25+
<li><a href="?path=/{{ "".join(path.split('/')[:-1]) }}">..</a></li>
26+
{% endif %}
27+
28+
{# Listing items #}
29+
{% for item in items %}
30+
<li><a href="?path={{ f'/{path}/{item}' if path else f'/{item}' }}">{{ item }}</a></li>
31+
{% endfor %}
32+
33+
</ul>
34+
35+
{# Script for filtering items #}
36+
<script>
37+
const search = document.querySelector('input');
38+
const items = document.querySelectorAll('li');
39+
40+
search.addEventListener('keyup', (e) => {
41+
const term = e.target.value.toLowerCase();
42+
43+
items.forEach(item => {
44+
const text = item.innerText.toLowerCase();
45+
46+
if (text.indexOf(term) != -1) {
47+
item.style.display = 'list-item';
48+
} else {
49+
item.style.display = 'none';
50+
}
51+
});
52+
});
53+
</script>
54+
</body>
55+
56+
</html>

examples/home.html

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
# SPDX-FileCopyrightText: 2023 Michał Pokusa
2-
#
3-
# SPDX-License-Identifier: Unlicense
1+
<!--
2+
SPDX-FileCopyrightText: 2023 Michal Pokusa
3+
4+
SPDX-License-Identifier: Unlicense
5+
-->
46

57
<html lang="en">
68
<head>

examples/httpserver_templates.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# SPDX-FileCopyrightText: 2023 Michal Pokusa
2+
#
3+
# SPDX-License-Identifier: Unlicense
4+
import os
5+
import re
6+
7+
import socketpool
8+
import wifi
9+
10+
from adafruit_httpserver import Server, Request, Response, FileResponse
11+
12+
try:
13+
from adafruit_templateengine import render_template
14+
except ImportError as e:
15+
raise ImportError("This example requires adafruit_templateengine library.") from e
16+
17+
18+
pool = socketpool.SocketPool(wifi.radio)
19+
server = Server(pool, "/static", debug=True)
20+
21+
# Create /static directory if it doesn't exist
22+
try:
23+
os.listdir("/static")
24+
except OSError as e:
25+
raise OSError("Please create a /static directory on the CIRCUITPY drive.") from e
26+
27+
28+
def is_file(path: str):
29+
return (os.stat(path.rstrip("/"))[0] & 0b_11110000_00000000) == 0b_10000000_00000000
30+
31+
32+
@server.route("/")
33+
def directory_listing(request: Request):
34+
path = request.query_params.get("path", "").replace("%20", " ")
35+
36+
# Preventing path traversal by removing all ../ from path
37+
path = re.sub(r"\/(\.\.)\/|\/(\.\.)|(\.\.)\/", "/", path).strip("/")
38+
39+
# If path is a file, return it as a file response
40+
if is_file(f"/static/{path}"):
41+
return FileResponse(request, path)
42+
43+
items = sorted(
44+
[
45+
item + ("" if is_file(f"/static/{path}/{item}") else "/")
46+
for item in os.listdir(f"/static/{path}")
47+
],
48+
key=lambda item: not item.endswith("/"),
49+
)
50+
51+
# Otherwise, return a directory listing
52+
return Response(
53+
request,
54+
render_template(
55+
"directory_listing.tpl.html",
56+
context={"path": path, "items": items},
57+
),
58+
content_type="text/html",
59+
)
60+
61+
62+
# Start the server.
63+
server.serve_forever(str(wifi.radio.ipv4_address))

0 commit comments

Comments
 (0)