Compare commits
2 Commits
17145628a0
...
main
Author | SHA1 | Date | |
---|---|---|---|
c99bced56e | |||
c12c8b0a89 |
@ -1,7 +1,7 @@
|
|||||||
[paths]
|
[paths]
|
||||||
content_dir = "/home/dubey/projects/foldsitedocs/content"
|
content_dir = "/home/dubey/projects/foldsite/docs/content"
|
||||||
templates_dir = "/home/dubey/projects/foldsitedocs/templates"
|
templates_dir = "/home/dubey/projects/foldsite/docs/templates"
|
||||||
styles_dir = "/home/dubey/projects/foldsitedocs/styles"
|
styles_dir = "/home/dubey/projects/foldsite/docs/styles"
|
||||||
|
|
||||||
[server]
|
[server]
|
||||||
listen_address = "0.0.0.0"
|
listen_address = "0.0.0.0"
|
||||||
|
35
docs/content/Configuration.md
Normal file
35
docs/content/Configuration.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Configuration
|
||||||
|
|
||||||
|
## Configuration file
|
||||||
|
|
||||||
|
Foldsite is configured using a TOML file (default: `config.toml`). This file specifies paths to content, templates, and styles, as well as server settings.
|
||||||
|
|
||||||
|
Example `config.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[paths]
|
||||||
|
content_dir = "/home/myuser/site/content"
|
||||||
|
templates_dir = "/home/myuser/site/themes/cobalt/templates"
|
||||||
|
styles_dir = "/home/myuser/site/themes/cobalt/styles"
|
||||||
|
|
||||||
|
[server]
|
||||||
|
listen_address = "127.0.0.1"
|
||||||
|
listen_port = 8080
|
||||||
|
debug = false
|
||||||
|
access_log = true
|
||||||
|
max_threads = 4
|
||||||
|
admin_browser = false
|
||||||
|
admin_password = "your_admin_password"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Server Settings
|
||||||
|
|
||||||
|
- **`listen_address`**: The IP address the server listens on (default: `127.0.0.1`).
|
||||||
|
- **`listen_port`**: The port the server listens on (default: `8080`).
|
||||||
|
- **`debug`**: Enables or disables debug mode (default: `false`). In debug mode, the server will automatically reload when code changes are detected, and more detailed error messages will be shown.
|
||||||
|
- **`access_log`**: Enables or disables access logging (default: `true`). If enabled, access logs will be written to standard output.
|
||||||
|
- **`max_threads`**: The maximum number of threads to use for handling requests (default: `4`). This setting directly impacts the concurrency of the server.
|
||||||
|
- **`admin_browser`**: Enables or disables the built-in file manager (default: `false`).
|
||||||
|
- **`admin_password`**: Sets the password for the file manager. Required if `admin_browser` is `true`.
|
||||||
|
|
||||||
|
The `Configuration` class (`/foldsite/src/config/config.py`) is responsible for loading and parsing this configuration file. It also sets global variables (`CONTENT_DIR`, `TEMPLATES_DIR`, `STYLES_DIR`) for easy access to these directories throughout the application. Errors are raised if the config file is missing, invalid, or lacks required sections (like `paths` or `server`).
|
56
docs/content/Rendering Process.md
Normal file
56
docs/content/Rendering Process.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
## Rendering Process
|
||||||
|
|
||||||
|
The `RouteManager` class (`/foldsite/src/routes/routes.py`) and `render_page` function (`/foldsite/src/rendering/renderer.py`) are central to the rendering process.
|
||||||
|
|
||||||
|
### How Foldsite Determines File Types
|
||||||
|
|
||||||
|
The `determine_type` function (in `renderer.py`) is crucial for figuring out how to handle a given file or directory. It examines file extensions and directory contents to classify files into broad categories (defined in `GENERIC_FILE_MAPPING` in `/foldsite/src/rendering/__init__.py`):
|
||||||
|
|
||||||
|
* **`document`**: Files with extensions like `.md`, `.txt`, and `.html`.
|
||||||
|
* **`image`**: Files with extensions like `.png`, `.jpg`, `.jpeg`, `.gif`, and `.svg`.
|
||||||
|
* **`directory`**: Directories. If a directory contains files, the most common file extension within that directory is used to infer the directory's "type".
|
||||||
|
* **`other`**: Files that don't match any of the above categories.
|
||||||
|
* **`multimedia`**: This is a combination that contains `image`.
|
||||||
|
|
||||||
|
### Template Search
|
||||||
|
|
||||||
|
When a request comes in, Foldsite searches for an appropriate template in the `templates` directory. The search logic is implemented in `render_page` and follows a specific order, prioritizing more specific templates:
|
||||||
|
|
||||||
|
1. **Exact Path Match:** If a template exists with the exact same path relative to the `templates` directory as the requested content file (but with a `.html` extension), it's used. For example, if the request is for `/about/team.md`, and a template exists at `templates/about/team.md.html`, that template will be used.
|
||||||
|
|
||||||
|
2. **Folder-Specific Template:** If the requested path is a directory, Foldsite looks for a `__folder.html` template within that directory. For example, if the request is for `/blog/`, and `templates/blog/__folder.html` exists, it will be used.
|
||||||
|
|
||||||
|
3. **Type and Extension-Specific Templates:** Foldsite searches for templates named `__{type}.{extension}.html` within the requested directory and its parent directories, moving upwards. For instance, if requesting `/blog/post1.md`, it would look for:
|
||||||
|
* `templates/blog/__file.md.html`
|
||||||
|
* `templates/__file.md.html`
|
||||||
|
|
||||||
|
4. **Type and Category-Specific Templates:** Similar to the above, but searches for `__{type}.{category}.html`. If requesting an image at `/images/logo.png`, it looks for:
|
||||||
|
|
||||||
|
* `templates/images/__file.image.html`
|
||||||
|
* `templates/__file.image.html`
|
||||||
|
|
||||||
|
5. **Base Template:** Finally, if no other template is found, `templates/base.html` is used as a fallback. This template *must* exist; otherwise, an exception is raised.
|
||||||
|
|
||||||
|
### Style Search
|
||||||
|
|
||||||
|
CSS styles are searched similarly to templates, prioritizing specificity:
|
||||||
|
|
||||||
|
1. **Exact Path Match:** A CSS file with the exact same path as the requested content file (relative to the `styles` directory) is used. For example, `/about/team.md` would look for `styles/about/team.md.css`.
|
||||||
|
|
||||||
|
2. **Type and Extension-Specific Styles:** Searches for `__{type}.{extension}.css` in the requested directory and its parent directories. For example, `/blog/post1.md` would look for:
|
||||||
|
|
||||||
|
* `styles/blog/__file.md.css`
|
||||||
|
* `styles/__file.md.css`
|
||||||
|
|
||||||
|
3. **Type and Category-Specific Styles:** Similar to the above, but searches for `__{type}.{category}.css`.
|
||||||
|
|
||||||
|
* `styles/images/__file.image.css`
|
||||||
|
* `styles/__file.image.css`
|
||||||
|
|
||||||
|
4. **Base Style:** `styles/base.css` is always included.
|
||||||
|
|
||||||
|
The discovered styles are added to the `styles` variable, which is passed to the Jinja2 template. The order ensures that more specific styles override general ones.
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
The `render_error_page` function (in `renderer.py`) handles errors. If a requested resource is not found (404 error) or if an exception occurs during processing, this function is called. It looks for a template named `__error.html` in the `templates` directory. If found, it's used to render the error page; otherwise, a default error page is generated. The error code, message, and description are passed to the template.
|
3
docs/content/index.md
Normal file
3
docs/content/index.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Foldsite Documentation
|
||||||
|
|
||||||
|
Foldsite acts as a dynamic site generator. It takes content primarily from Markdown files, combines it with HTML templates, applies CSS styles, and serves the resulting pages. It supports features like image thumbnails, Markdown rendering with frontmatter, and a built-in file manager for administrative tasks. Foldsite is dynamic in the sense that content is rendered on demand, rather than generating static HTML up-front.
|
23
docs/styles/__file.md.css
Normal file
23
docs/styles/__file.md.css
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
article {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
article h1,
|
||||||
|
article h2,
|
||||||
|
article h3,
|
||||||
|
article h4,
|
||||||
|
article h5,
|
||||||
|
article h6 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
article p {
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
article code {
|
||||||
|
background: var(--code-background-color);
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
280
docs/styles/base.css
Normal file
280
docs/styles/base.css
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
/* http://meyerweb.com/eric/tools/css/reset/
|
||||||
|
v2.0 | 20110126
|
||||||
|
License: none (public domain)
|
||||||
|
*/
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
div,
|
||||||
|
span,
|
||||||
|
applet,
|
||||||
|
object,
|
||||||
|
iframe,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
p,
|
||||||
|
blockquote,
|
||||||
|
pre,
|
||||||
|
a,
|
||||||
|
abbr,
|
||||||
|
acronym,
|
||||||
|
address,
|
||||||
|
big,
|
||||||
|
cite,
|
||||||
|
code,
|
||||||
|
del,
|
||||||
|
dfn,
|
||||||
|
em,
|
||||||
|
img,
|
||||||
|
ins,
|
||||||
|
kbd,
|
||||||
|
q,
|
||||||
|
s,
|
||||||
|
samp,
|
||||||
|
small,
|
||||||
|
strike,
|
||||||
|
strong,
|
||||||
|
sub,
|
||||||
|
sup,
|
||||||
|
tt,
|
||||||
|
var,
|
||||||
|
b,
|
||||||
|
u,
|
||||||
|
i,
|
||||||
|
center,
|
||||||
|
dl,
|
||||||
|
dt,
|
||||||
|
dd,
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
li,
|
||||||
|
fieldset,
|
||||||
|
form,
|
||||||
|
label,
|
||||||
|
legend,
|
||||||
|
table,
|
||||||
|
caption,
|
||||||
|
tbody,
|
||||||
|
tfoot,
|
||||||
|
thead,
|
||||||
|
tr,
|
||||||
|
th,
|
||||||
|
td,
|
||||||
|
article,
|
||||||
|
aside,
|
||||||
|
canvas,
|
||||||
|
details,
|
||||||
|
embed,
|
||||||
|
figure,
|
||||||
|
figcaption,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
hgroup,
|
||||||
|
menu,
|
||||||
|
nav,
|
||||||
|
output,
|
||||||
|
ruby,
|
||||||
|
section,
|
||||||
|
summary,
|
||||||
|
time,
|
||||||
|
mark,
|
||||||
|
audio,
|
||||||
|
video {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
font-size: 100%;
|
||||||
|
font: inherit;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HTML5 display-role reset for older browsers */
|
||||||
|
article,
|
||||||
|
aside,
|
||||||
|
details,
|
||||||
|
figcaption,
|
||||||
|
figure,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
hgroup,
|
||||||
|
menu,
|
||||||
|
nav,
|
||||||
|
section {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote,
|
||||||
|
q {
|
||||||
|
quotes: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote:before,
|
||||||
|
blockquote:after,
|
||||||
|
q:before,
|
||||||
|
q:after {
|
||||||
|
content: '';
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@property --font-color {
|
||||||
|
syntax: "<color>";
|
||||||
|
inherits: true;
|
||||||
|
initial-value: oklch(25.11% 0.006 258.36);
|
||||||
|
}
|
||||||
|
|
||||||
|
@property --background-color {
|
||||||
|
syntax: "<color>";
|
||||||
|
inherits: true;
|
||||||
|
initial-value: #F6F0F0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@property --code-background-color {
|
||||||
|
syntax: "<color>";
|
||||||
|
inherits: true;
|
||||||
|
initial-value: #c7c1c1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@property --hover-color {
|
||||||
|
syntax: "<color>";
|
||||||
|
inherits: true;
|
||||||
|
initial-value: #A4B465;
|
||||||
|
}
|
||||||
|
|
||||||
|
@property --url-color {
|
||||||
|
syntax: "<color>";
|
||||||
|
inherits: true;
|
||||||
|
initial-value: #626F47;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--font-color: oklch(91.87% 0.003 264.54);
|
||||||
|
--background-color: #29261f;
|
||||||
|
--hover-color: #626F47;
|
||||||
|
--url-color: #A4B465;
|
||||||
|
--code-background-color: #3d392e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Open Sans", sans-serif;
|
||||||
|
font-optical-sizing: auto;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-variation-settings:
|
||||||
|
"wdth" 100;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--font-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--url-color);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.25s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--hover-color);
|
||||||
|
transition: all 0.25s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:visited {
|
||||||
|
color: #C14600;
|
||||||
|
transition: all 0.25s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:visited:hover {
|
||||||
|
color: var(--hover-color);
|
||||||
|
transition: all 0.25s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (font-size-adjust: 1) {
|
||||||
|
.content {
|
||||||
|
font-size-adjust: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: square;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
line-height: 160%;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
line-height: calc(1ex / 0.32);
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
max-width: 80ch;
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content h1 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
line-height: calc(1ex / 0.42);
|
||||||
|
margin: calc(1ex / 0.42) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content h2 {
|
||||||
|
font-size: 2em;
|
||||||
|
line-height: calc(1ex / 0.42);
|
||||||
|
margin: calc(1ex / 0.42) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content h3 {
|
||||||
|
font-size: 1.75em;
|
||||||
|
line-height: calc(1ex / 0.38);
|
||||||
|
margin: calc(1ex / 0.38) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content h4 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
line-height: calc(1ex / 0.37);
|
||||||
|
margin: calc(1ex / 0.37) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content p {
|
||||||
|
font-size: 1em;
|
||||||
|
line-height: calc(1ex / 0.32);
|
||||||
|
margin: calc(1ex / 0.32) 0;
|
||||||
|
text-align: justify;
|
||||||
|
hyphens: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
padding-top: 4rem;
|
||||||
|
line-height: calc(1ex / 0.32);
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin: auto;
|
||||||
|
}
|
7
docs/templates/__file.md.html
vendored
Normal file
7
docs/templates/__file.md.html
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<article>
|
||||||
|
{{ content|safe }}
|
||||||
|
</article>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/dark.min.css">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/dockerfile.min.js"></script>
|
||||||
|
<script>hljs.highlightAll();</script>
|
43
docs/templates/base.html
vendored
Normal file
43
docs/templates/base.html
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Outfit:wght@100..900&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='base.css') }}">
|
||||||
|
{% for style in styles %}
|
||||||
|
<link rel="stylesheet" href="/styles{{ style }}" type="text/css">
|
||||||
|
{% endfor %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="holder">
|
||||||
|
<div class="sidebar"> <!-- Changed <sidebar> to <div> -->
|
||||||
|
<ul>
|
||||||
|
<li><a href="/">⌂ Home</a></li>
|
||||||
|
<hr>
|
||||||
|
{% for f in get_folder_contents() %}
|
||||||
|
{% if not f.is_dir %}
|
||||||
|
{% if f.proper_name == "index" %}
|
||||||
|
{% else %}
|
||||||
|
<li><a href="/{{ f.path }}">{{ f.proper_name }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="content"> <!-- <main> tag remains the same -->
|
||||||
|
{{ content|safe }}
|
||||||
|
<div class="footer">
|
||||||
|
<p>© DWS</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -6,6 +6,34 @@ TEMPLATES_DIR = None
|
|||||||
STYLES_DIR = None
|
STYLES_DIR = None
|
||||||
|
|
||||||
class Configuration:
|
class Configuration:
|
||||||
|
"""
|
||||||
|
Configuration class for loading and validating application settings from a TOML file.
|
||||||
|
This class encapsulates the logic for reading configuration data from a specified TOML file,
|
||||||
|
validating the presence of required sections and keys, and exposing configuration values as
|
||||||
|
instance attributes. The configuration file is expected to contain at least two sections:
|
||||||
|
'paths' (with 'content_dir', 'templates_dir', and 'styles_dir') and 'server' (with optional
|
||||||
|
server-related settings).
|
||||||
|
Attributes:
|
||||||
|
config_path (str or Path): Path to the TOML configuration file.
|
||||||
|
content_dir (Path): Directory containing content files (required).
|
||||||
|
templates_dir (Path): Directory containing template files (required).
|
||||||
|
styles_dir (Path): Directory containing style files (required).
|
||||||
|
listen_address (str): Address for the server to listen on (default: "127.0.0.1").
|
||||||
|
listen_port (int): Port for the server to listen on (default: 8080).
|
||||||
|
debug (bool): Enable or disable debug mode (default: False).
|
||||||
|
access_log (bool): Enable or disable access logging (default: True).
|
||||||
|
max_threads (int): Maximum number of server threads (default: 4).
|
||||||
|
admin_browser (bool): Enable or disable admin browser access (default: False).
|
||||||
|
admin_password (str): Password for admin access (optional).
|
||||||
|
Methods:
|
||||||
|
load_config():
|
||||||
|
Loads and validates configuration data from the TOML file specified by `config_path`.
|
||||||
|
Raises FileNotFoundError if the file does not exist, tomllib.TOMLDecodeError if the file
|
||||||
|
is not valid TOML, or ValueError if required sections or keys are missing.
|
||||||
|
set_globals():
|
||||||
|
Sets global variables CONTENT_DIR, TEMPLATES_DIR, and STYLES_DIR based on the loaded
|
||||||
|
configuration values.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, config_path):
|
def __init__(self, config_path):
|
||||||
self.config_path = config_path
|
self.config_path = config_path
|
||||||
@ -23,6 +51,19 @@ class Configuration:
|
|||||||
self.admin_password: str = None
|
self.admin_password: str = None
|
||||||
|
|
||||||
def load_config(self):
|
def load_config(self):
|
||||||
|
"""
|
||||||
|
Loads and validates configuration data from a TOML file specified by `self.config_path`.
|
||||||
|
This method reads the configuration file, parses its contents, and sets various instance attributes
|
||||||
|
based on the configuration values. It expects the configuration file to contain at least two sections:
|
||||||
|
'paths' and 'server'. The 'paths' section must include 'content_dir', 'templates_dir', and 'styles_dir'.
|
||||||
|
The 'server' section may include 'listen_address', 'listen_port', 'debug', 'access_log', 'max_threads',
|
||||||
|
'admin_browser', and 'admin_password'. If any required section or key is missing, or if the file is
|
||||||
|
not found or is invalid TOML, an appropriate exception is raised.
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If the configuration file does not exist.
|
||||||
|
tomllib.TOMLDecodeError: If the configuration file is not valid TOML.
|
||||||
|
ValueError: If required sections or keys are missing in the configuration file.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
with open(self.config_path, "rb") as f:
|
with open(self.config_path, "rb") as f:
|
||||||
self.config_data = tomllib.load(f)
|
self.config_data = tomllib.load(f)
|
||||||
@ -62,10 +103,3 @@ class Configuration:
|
|||||||
self.admin_browser = server.get("admin_browser", self.admin_browser)
|
self.admin_browser = server.get("admin_browser", self.admin_browser)
|
||||||
self.admin_password = server.get("admin_password", self.admin_password)
|
self.admin_password = server.get("admin_password", self.admin_password)
|
||||||
|
|
||||||
def set_globals(self):
|
|
||||||
global CONTENT_DIR, TEMPLATES_DIR, STYLES_DIR
|
|
||||||
CONTENT_DIR = self.content_dir
|
|
||||||
TEMPLATES_DIR = self.templates_dir
|
|
||||||
STYLES_DIR = self.styles_dir
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -90,39 +90,55 @@ class TemplateHelpers:
|
|||||||
return [f for f in files if not f.name.startswith("___")]
|
return [f for f in files if not f.name.startswith("___")]
|
||||||
|
|
||||||
def _build_metadata_for_file(self, path: str, categories: list[str] = []):
|
def _build_metadata_for_file(self, path: str, categories: list[str] = []):
|
||||||
|
"""
|
||||||
|
Builds and returns metadata for a given file based on specified categories.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): The relative path to the file within the content directory.
|
||||||
|
categories (list[str], optional): A list of category strings to determine the type of metadata to extract.
|
||||||
|
Supported categories include "image" and "document".
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ImageMetadata | FileMetadata | None:
|
||||||
|
- If "image" is in categories and the file is a valid image, returns an ImageMetadata object containing
|
||||||
|
width, height, alt text, and EXIF data.
|
||||||
|
- If "document" is in categories and the file is a document (e.g., Markdown), returns a FileMetadata object
|
||||||
|
with type-specific metadata such as frontmatter, content, raw content, plain text, and preview.
|
||||||
|
- Returns None if the file cannot be processed or if no supported category matches.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- For images, EXIF orientation is handled to ensure correct width and height.
|
||||||
|
- For Markdown documents, frontmatter and content are extracted and a text preview is generated.
|
||||||
|
- Prints an error message and returns None if image processing fails.
|
||||||
|
"""
|
||||||
file_path = self.config.content_dir / path
|
file_path = self.config.content_dir / path
|
||||||
for k in categories:
|
for k in categories:
|
||||||
if k == "image":
|
if k == "image":
|
||||||
img = Image.open(file_path)
|
try:
|
||||||
exif = img._getexif()
|
with Image.open(file_path) as img:
|
||||||
# Conver exif to dict
|
|
||||||
orientation = exif.get(274, 1) if exif else 1
|
|
||||||
width, height = img.width, img.height
|
width, height = img.width, img.height
|
||||||
if orientation in [5, 6, 7, 8]:
|
exif_raw = img._getexif()
|
||||||
width, height = height, width
|
|
||||||
|
|
||||||
exif = {}
|
exif = {}
|
||||||
try:
|
|
||||||
img = Image.open(file_path)
|
|
||||||
exif_raw = img._getexif()
|
|
||||||
if exif_raw:
|
if exif_raw:
|
||||||
|
orientation = exif_raw.get(0x0112, 1)
|
||||||
|
if orientation in [5, 6, 7, 8]:
|
||||||
|
width, height = height, width
|
||||||
exif = {
|
exif = {
|
||||||
ExifTags.TAGS[k]: v
|
ExifTags.TAGS[k]: v
|
||||||
for k, v in exif_raw.items()
|
for k, v in exif_raw.items()
|
||||||
if k in ExifTags.TAGS
|
if k in ExifTags.TAGS
|
||||||
}
|
}
|
||||||
except Exception as e:
|
|
||||||
print(f"Error processing image {file_path}: {e}")
|
|
||||||
|
|
||||||
date_taken = exif.get("DateTimeOriginal")
|
|
||||||
if not date_taken:
|
|
||||||
date_taken = format_date(file_path.stat().st_ctime)
|
|
||||||
return ImageMetadata(
|
return ImageMetadata(
|
||||||
width=width,
|
width=width,
|
||||||
height=height,
|
height=height,
|
||||||
alt=file_path.name,
|
alt=file_path.name,
|
||||||
exif=exif,
|
exif=exif,
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing image {file_path}: {e}")
|
||||||
|
return None
|
||||||
elif k == "document":
|
elif k == "document":
|
||||||
ret = None
|
ret = None
|
||||||
with open(file_path, "r") as fdoc:
|
with open(file_path, "r") as fdoc:
|
||||||
@ -174,7 +190,7 @@ class TemplateHelpers:
|
|||||||
categories=[],
|
categories=[],
|
||||||
date_modified=format_date(f.stat().st_mtime),
|
date_modified=format_date(f.stat().st_mtime),
|
||||||
date_created=format_date(f.stat().st_ctime),
|
date_created=format_date(f.stat().st_ctime),
|
||||||
size_kb=f.stat().st_size / 1024,
|
size_kb=int(f.stat().st_size / 1024),
|
||||||
metadata=None,
|
metadata=None,
|
||||||
dir_item_count=len(list(f.glob("*"))) if f.is_dir() else 0,
|
dir_item_count=len(list(f.glob("*"))) if f.is_dir() else 0,
|
||||||
is_dir=f.is_dir(),
|
is_dir=f.is_dir(),
|
||||||
@ -233,7 +249,7 @@ class TemplateHelpers:
|
|||||||
IOError: If an I/O error occurs while reading the file.
|
IOError: If an I/O error occurs while reading the file.
|
||||||
"""
|
"""
|
||||||
file_path = self.config.content_dir / path
|
file_path = self.config.content_dir / path
|
||||||
with open(file_path, "r") as f:
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
content = f.read(100)
|
content = f.read(100)
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
@ -69,7 +69,6 @@ def render_error_page(
|
|||||||
error_message: str,
|
error_message: str,
|
||||||
error_description: str,
|
error_description: str,
|
||||||
template_path: Path = Path("./"),
|
template_path: Path = Path("./"),
|
||||||
currentPath: str = "",
|
|
||||||
):
|
):
|
||||||
inp = DEFAULT_ERROR_TEMPLATE
|
inp = DEFAULT_ERROR_TEMPLATE
|
||||||
if (template_path / "__error.html").exists():
|
if (template_path / "__error.html").exists():
|
||||||
@ -85,7 +84,6 @@ def render_error_page(
|
|||||||
(template_path / "base.html").read_text(),
|
(template_path / "base.html").read_text(),
|
||||||
content=content,
|
content=content,
|
||||||
styles=["/base.css", "/__error.css"],
|
styles=["/base.css", "/__error.css"],
|
||||||
currentPath=currentPath,
|
|
||||||
),
|
),
|
||||||
error_code,
|
error_code,
|
||||||
)
|
)
|
||||||
@ -127,7 +125,6 @@ def render_page(
|
|||||||
error_message="Not Found",
|
error_message="Not Found",
|
||||||
error_description="The requested resource was not found on this server.",
|
error_description="The requested resource was not found on this server.",
|
||||||
template_path=template_path,
|
template_path=template_path,
|
||||||
currentPath=str(path.relative_to(base_path)),
|
|
||||||
)
|
)
|
||||||
target_path = path
|
target_path = path
|
||||||
target_file = path
|
target_file = path
|
||||||
@ -203,26 +200,35 @@ def render_page(
|
|||||||
"Not Found",
|
"Not Found",
|
||||||
"The requested resource was not found on this server.",
|
"The requested resource was not found on this server.",
|
||||||
template_path,
|
template_path,
|
||||||
currentPath=str(relative_path),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
content = ""
|
content = ""
|
||||||
|
c_frontmatter = None
|
||||||
if "document" in category and type == "file":
|
if "document" in category and type == "file":
|
||||||
content, c_frontmatter, obj = render_markdown(target_file)
|
content, c_frontmatter, obj = render_markdown(target_file)
|
||||||
|
|
||||||
if not (template_path / "base.html").exists():
|
if not (template_path / "base.html").exists():
|
||||||
raise Exception("Base template not found")
|
raise Exception("Base template not found")
|
||||||
|
|
||||||
templates.append(template_path / "base.html")
|
|
||||||
|
|
||||||
# Filter templates to only those that exist
|
# The first found template is the most specific one for the content.
|
||||||
for template in templates:
|
page_template_path = templates[0]
|
||||||
content = render_template_string(
|
|
||||||
template.read_text(),
|
template_vars = {
|
||||||
content=content,
|
"content": content,
|
||||||
styles=styles,
|
"styles": styles,
|
||||||
currentPath=str(relative_path),
|
"currentPath": str(relative_path),
|
||||||
metadata=c_frontmatter if "document" in category and type == "file" else None,
|
"metadata": c_frontmatter if "document" in category and type == "file" else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# First, render the specific page template.
|
||||||
|
final_content = render_template_string(
|
||||||
|
page_template_path.read_text(), **template_vars
|
||||||
)
|
)
|
||||||
|
|
||||||
return content
|
# Now, render the base template, providing the result of the page
|
||||||
|
# template as the 'content' variable.
|
||||||
|
template_vars["content"] = final_content
|
||||||
|
return render_template_string(
|
||||||
|
(template_path / "base.html").read_text(), **template_vars
|
||||||
|
)
|
||||||
|
@ -1,81 +1,111 @@
|
|||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from flask import request, send_file
|
|
||||||
|
|
||||||
from src.config.config import Configuration
|
from src.config.config import Configuration
|
||||||
|
from src.rendering.renderer import render_page, render_error_page
|
||||||
|
from flask import send_file, request
|
||||||
from src.rendering.image import generate_thumbnail
|
from src.rendering.image import generate_thumbnail
|
||||||
from src.rendering.renderer import render_error_page, render_page
|
import os
|
||||||
|
|
||||||
|
|
||||||
class RouteManager:
|
class RouteManager:
|
||||||
|
"""
|
||||||
|
RouteManager is responsible for handling and validating file system paths for serving content, styles, and static files in a web application. It ensures that all requested paths are securely resolved within configured base directories, prevents path traversal attacks, and restricts access to hidden files or folders.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config (Configuration): The configuration object containing directory paths for content, templates, and styles.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
_validate_and_sanitize_path(base_dir, requested_path_str):
|
||||||
|
Validates and sanitizes a requested path to ensure it is within the specified base directory and not a hidden file/folder. Returns a resolved Path object or None if invalid.
|
||||||
|
|
||||||
|
_ensure_route(path):
|
||||||
|
Ensures the given path is valid and returns the corresponding Path object. Raises an Exception if the path is illegal.
|
||||||
|
|
||||||
|
default_route(path):
|
||||||
|
Handles the default route for serving content files. Returns a rendered page or an error page if the path is invalid or not found.
|
||||||
|
|
||||||
|
get_style(path):
|
||||||
|
Serves style files from the styles directory. Returns the file or an error page if the path is invalid or not found.
|
||||||
|
|
||||||
|
get_static(path):
|
||||||
|
Serves static files from the content directory. If the file is an image, generates and returns a thumbnail. Returns the file or an error page if the path is invalid or not found.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, config: Configuration):
|
def __init__(self, config: Configuration):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
def _validate_and_sanitize_path(self, base_dir, requested_path):
|
def _validate_and_sanitize_path(self, base_dir, requested_path_str: str):
|
||||||
"""
|
"""
|
||||||
Validate and sanitize the requested path to ensure it does not traverse above the base directory.
|
Validates and sanitizes a requested file system path to ensure it is safe and allowed.
|
||||||
|
|
||||||
:param base_dir: The base directory that the requested path should be within.
|
This method resolves the requested path relative to a given base directory, ensuring:
|
||||||
:param requested_path: The requested file path to validate.
|
- The resolved path exists.
|
||||||
:return: A secure version of the requested path if valid, otherwise None.
|
- The resolved path is within the base directory (prevents directory traversal attacks).
|
||||||
|
- The path does not access hidden files or directories (those starting with '___').
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_dir (str or Path): The base directory against which the requested path is resolved.
|
||||||
|
requested_path_str (str): The user-supplied path to validate and sanitize.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path or None: The resolved and validated Path object if the path is safe and allowed;
|
||||||
|
otherwise, None if the path is invalid, does not exist, attempts traversal,
|
||||||
|
or accesses hidden files/directories.
|
||||||
"""
|
"""
|
||||||
# Normalize both paths
|
try:
|
||||||
base_dir = Path(base_dir)
|
base_dir = Path(base_dir).resolve(strict=True)
|
||||||
requested_path: Path = base_dir / requested_path
|
# a requested path of "" or "." should resolve to the base directory
|
||||||
|
if not requested_path_str:
|
||||||
|
requested_path_str = "."
|
||||||
|
secure_path = (base_dir / requested_path_str).resolve(strict=True)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None # Path does not exist
|
||||||
|
|
||||||
# Check if the requested path is within the base directory
|
# The most important check: ensure the resolved path is inside the base directory.
|
||||||
if requested_path < base_dir:
|
if not secure_path.is_relative_to(base_dir):
|
||||||
|
print(f"Illegal path traversal attempt: {requested_path_str}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Ensure the path does not contain any '..' or '.' components
|
# Check for hidden files/folders (starting with '___')
|
||||||
secure_path = os.path.relpath(requested_path, base_dir)
|
relative_parts = secure_path.relative_to(base_dir).parts
|
||||||
secure_path_parts = secure_path.split(os.sep)
|
# Also check the final component for the case where path is the base_dir itself.
|
||||||
|
if any(
|
||||||
for part in secure_path_parts:
|
part.startswith("___") for part in relative_parts
|
||||||
if part == "." or part == "..":
|
) or secure_path.name.startswith("___"):
|
||||||
|
print(f"Illegal access to hidden path: {requested_path_str}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Reconstruct the secure path
|
|
||||||
secure_path = os.path.join(base_dir, *secure_path_parts)
|
|
||||||
secure_path = Path(secure_path)
|
|
||||||
|
|
||||||
# Check if path exists
|
|
||||||
if not secure_path.exists():
|
|
||||||
raise Exception("Illegal path")
|
|
||||||
|
|
||||||
for part in secure_path.parts:
|
|
||||||
if part.startswith("___"):
|
|
||||||
raise Exception("Illegal path")
|
|
||||||
|
|
||||||
return secure_path
|
return secure_path
|
||||||
|
|
||||||
def _ensure_route(self, path: str):
|
def _ensure_route(self, path: str):
|
||||||
file_path: Path = self.config.content_dir / path
|
file_path = self._validate_and_sanitize_path(self.config.content_dir, path)
|
||||||
if not path or file_path.is_dir():
|
if not file_path:
|
||||||
file_path = file_path / "index.md"
|
|
||||||
|
|
||||||
if file_path < self.config.content_dir:
|
|
||||||
raise Exception("Illegal path")
|
|
||||||
|
|
||||||
if not self._validate_and_sanitize_path(
|
|
||||||
self.config.content_dir, str(file_path)
|
|
||||||
):
|
|
||||||
raise Exception("Illegal path")
|
raise Exception("Illegal path")
|
||||||
|
return file_path
|
||||||
|
|
||||||
def default_route(self, path: str):
|
def default_route(self, path: str):
|
||||||
|
"""
|
||||||
|
Handles the default route for serving content pages.
|
||||||
|
|
||||||
|
Attempts to resolve the given path to a file within the content directory.
|
||||||
|
If the path is empty, defaults to "index.md". If the file is not found or an error occurs,
|
||||||
|
renders a 404 error page. Otherwise, renders the requested page using the specified
|
||||||
|
template and style directories.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): The requested path to resolve and serve.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: The rendered page or an error page if the file is not found.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
self._ensure_route(path)
|
file_path = self._ensure_route(path if path else "index.md")
|
||||||
except Exception as e:
|
except Exception as _:
|
||||||
return render_error_page(
|
return render_error_page(
|
||||||
404,
|
404,
|
||||||
"Not Found",
|
"Not Found",
|
||||||
"The requested resource was not found on this server.",
|
"The requested resource was not found on this server.",
|
||||||
self.config.templates_dir,
|
self.config.templates_dir,
|
||||||
)
|
)
|
||||||
file_path: Path = self.config.content_dir / path
|
|
||||||
if not path or file_path.is_dir():
|
|
||||||
file_path = file_path / "index.md"
|
|
||||||
return render_page(
|
return render_page(
|
||||||
file_path,
|
file_path,
|
||||||
base_path=self.config.content_dir,
|
base_path=self.config.content_dir,
|
||||||
@ -84,19 +114,45 @@ class RouteManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_style(self, path: str):
|
def get_style(self, path: str):
|
||||||
try:
|
"""
|
||||||
self._validate_and_sanitize_path(self.config.styles_dir, path)
|
Retrieves and serves a style file from the configured styles directory.
|
||||||
except Exception as e:
|
|
||||||
|
Args:
|
||||||
|
path (str): The relative path to the requested style file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: A Flask response object containing the requested file if found,
|
||||||
|
or an error page with a 404 status code if the file does not exist.
|
||||||
|
"""
|
||||||
|
file_path = self._validate_and_sanitize_path(self.config.styles_dir, path)
|
||||||
|
if not file_path:
|
||||||
return render_error_page(
|
return render_error_page(
|
||||||
404,
|
404,
|
||||||
"Not Found",
|
"Not Found",
|
||||||
f"The requested resource was not found on this server. {e}",
|
"The requested resource was not found on this server.",
|
||||||
self.config.templates_dir,
|
self.config.templates_dir,
|
||||||
)
|
)
|
||||||
file_path: Path = self.config.styles_dir / path
|
|
||||||
if file_path.exists():
|
|
||||||
return send_file(file_path)
|
return send_file(file_path)
|
||||||
else:
|
|
||||||
|
def get_static(self, path: str):
|
||||||
|
"""
|
||||||
|
Serves static files from the configured content directory.
|
||||||
|
|
||||||
|
If the requested file is an image (JPEG, PNG, or GIF), generates and returns a thumbnail
|
||||||
|
with a maximum width specified by the 'max_width' query parameter (default: 2048).
|
||||||
|
Otherwise, serves the file as-is.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): The relative path to the requested static file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response:
|
||||||
|
- If the file is not found or invalid, returns a rendered 404 error page.
|
||||||
|
- If the file is an image, returns the thumbnail bytes with appropriate headers.
|
||||||
|
- Otherwise, returns the file using Flask's send_file.
|
||||||
|
"""
|
||||||
|
file_path = self._validate_and_sanitize_path(self.config.content_dir, path)
|
||||||
|
if not file_path:
|
||||||
return render_error_page(
|
return render_error_page(
|
||||||
404,
|
404,
|
||||||
"Not Found",
|
"Not Found",
|
||||||
@ -104,18 +160,6 @@ class RouteManager:
|
|||||||
self.config.templates_dir,
|
self.config.templates_dir,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_static(self, path: str):
|
|
||||||
try:
|
|
||||||
self._validate_and_sanitize_path(self.config.content_dir, path)
|
|
||||||
except Exception as e:
|
|
||||||
return render_error_page(
|
|
||||||
404,
|
|
||||||
"Not Found",
|
|
||||||
"The requested resource was not found on this server.",
|
|
||||||
self.config.templates_dir,
|
|
||||||
)
|
|
||||||
file_path: Path = self.config.content_dir / path
|
|
||||||
if file_path.exists():
|
|
||||||
# Check to see if the file is an image, if it is, render a thumbnail
|
# Check to see if the file is an image, if it is, render a thumbnail
|
||||||
if file_path.suffix.lower() in [".jpg", ".jpeg", ".png", ".gif"]:
|
if file_path.suffix.lower() in [".jpg", ".jpeg", ".png", ".gif"]:
|
||||||
max_width = request.args.get("max_width", default=2048, type=int)
|
max_width = request.args.get("max_width", default=2048, type=int)
|
||||||
@ -131,10 +175,3 @@ class RouteManager:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
return send_file(file_path)
|
return send_file(file_path)
|
||||||
else:
|
|
||||||
return render_error_page(
|
|
||||||
404,
|
|
||||||
"Not Found",
|
|
||||||
"The requested resource was not found on this server.",
|
|
||||||
self.config.templates_dir,
|
|
||||||
)
|
|
||||||
|
@ -6,7 +6,29 @@ import multiprocessing
|
|||||||
|
|
||||||
|
|
||||||
class Server(BaseApplication):
|
class Server(BaseApplication):
|
||||||
|
"""
|
||||||
|
Server class for managing a Flask web application with Gunicorn integration.
|
||||||
|
This class extends BaseApplication to provide a configurable server environment
|
||||||
|
for Flask applications. It supports custom template functions, dynamic worker/thread
|
||||||
|
configuration, and flexible server options.
|
||||||
|
Attributes:
|
||||||
|
debug (bool): Enables or disables debug mode for the Flask app.
|
||||||
|
host (str): The hostname or IP address to bind the server to.
|
||||||
|
port (int): The port number to listen on.
|
||||||
|
app (Flask): The Flask application instance.
|
||||||
|
application (Flask): Alias for the Flask application instance.
|
||||||
|
options (dict): Gunicorn server options such as bind address, reload, threads, and access log.
|
||||||
|
Methods:
|
||||||
|
__init__(self, debug=True, host="0.0.0.0", port=8080, template_functions=None, workers=..., access_log=True, options=None):
|
||||||
|
Initializes the Server instance with the specified configuration and registers template functions.
|
||||||
|
register_template_function(self, name, func):
|
||||||
|
Registers a Python function to be available in Jinja2 templates.
|
||||||
|
load_config(self):
|
||||||
|
Loads configuration options from self.options into the Gunicorn config object.
|
||||||
|
load(self):
|
||||||
|
Returns the Flask application instance managed by the server.
|
||||||
|
register_route(self, route, func, defaults=None):
|
||||||
|
"""
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
debug: bool = True,
|
debug: bool = True,
|
||||||
@ -32,17 +54,42 @@ class Server(BaseApplication):
|
|||||||
"threads": workers,
|
"threads": workers,
|
||||||
"accesslog": "-" if access_log else None,
|
"accesslog": "-" if access_log else None,
|
||||||
}
|
}
|
||||||
for name, func in template_functions.items():
|
|
||||||
self.register_template_function(name, func)
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
for name, func in template_functions.items():
|
for name, func in template_functions.items():
|
||||||
self.register_template_function(name, func)
|
self.register_template_function(name, func)
|
||||||
super(Server, self).__init__()
|
|
||||||
|
|
||||||
def register_template_function(self, name, func):
|
def register_template_function(self, name, func):
|
||||||
|
"""
|
||||||
|
Register a function to be available in Jinja2 templates.
|
||||||
|
|
||||||
|
This method adds a Python function to the Jinja2 environment's globals,
|
||||||
|
making it available for use in all templates rendered by the application.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
----------
|
||||||
|
name : str
|
||||||
|
The name under which the function will be accessible in templates
|
||||||
|
func : callable
|
||||||
|
The Python function to register
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
--------
|
||||||
|
>>> server.register_template_function('format_date', lambda d: d.strftime('%Y-%m-%d'))
|
||||||
|
>>> # In template: {{ format_date(some_date) }}
|
||||||
|
"""
|
||||||
self.app.jinja_env.globals.update({name: func})
|
self.app.jinja_env.globals.update({name: func})
|
||||||
|
|
||||||
def load_config(self):
|
def load_config(self):
|
||||||
|
"""
|
||||||
|
Loads configuration options from self.options into self.cfg.
|
||||||
|
|
||||||
|
This method filters out options that are not in self.cfg.settings or have None values.
|
||||||
|
The filtered options are then set in the configuration object (self.cfg) with lowercase keys.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
config = {
|
config = {
|
||||||
key: value
|
key: value
|
||||||
for key, value in self.options.items()
|
for key, value in self.options.items()
|
||||||
@ -52,7 +99,24 @@ class Server(BaseApplication):
|
|||||||
self.cfg.set(key.lower(), value)
|
self.cfg.set(key.lower(), value)
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
|
"""
|
||||||
|
Returns the application instance associated with the server.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Application: The application object managed by the server.
|
||||||
|
"""
|
||||||
return self.application
|
return self.application
|
||||||
|
|
||||||
def register_route(self, route, func, defaults=None):
|
def register_route(self, route, func, defaults=None):
|
||||||
|
"""
|
||||||
|
Registers a new route with the Flask application.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
route (str): The URL route to register.
|
||||||
|
func (callable): The view function to associate with the route.
|
||||||
|
defaults (dict, optional): A dictionary of default values for the route variables. Defaults to None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
self.app.add_url_rule(route, func.__name__, func, defaults=defaults)
|
self.app.add_url_rule(route, func.__name__, func, defaults=defaults)
|
||||||
|
Reference in New Issue
Block a user