Revert to 8c23e9d811
All checks were successful
Datadog Software Composition Analysis / Datadog SBOM Generation and Upload (push) Successful in 51s
Datadog Secrets Scanning / Datadog Static Analyzer (push) Successful in 56s
Release / build (push) Successful in 1m24s
Release / publish_head (push) Successful in 1m17s
Datadog Static Analysis / Datadog Static Analyzer (push) Successful in 5m41s

This commit is contained in:
2025-07-09 06:07:27 -04:00
parent 17145628a0
commit c12c8b0a89
11 changed files with 461 additions and 25 deletions

View File

@ -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"

View 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`).

View 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
View 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
View 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
View 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
View 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
View 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>&copy; DWS</p>
</div>
</div>
</div>
</body>
</html>

View File

@ -34,9 +34,9 @@ def generate_thumbnail(image_path, resize_percent, min_width, max_width):
if orientation == 3: if orientation == 3:
img = img.rotate(180, expand=True) img = img.rotate(180, expand=True)
elif orientation == 6: elif orientation == 6:
img = img.rotate(270, expand=True)
elif orientation == 8:
img = img.rotate(90, expand=True) img = img.rotate(90, expand=True)
elif orientation == 8:
img = img.rotate(270, expand=True)
except (AttributeError, KeyError, IndexError): except (AttributeError, KeyError, IndexError):
# cases: image don't have getexif # cases: image don't have getexif
exif = b"" exif = b""

View File

@ -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,7 +200,6 @@ 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 = ""

View File

@ -1,11 +1,9 @@
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:
@ -34,6 +32,7 @@ class RouteManager:
for part in secure_path_parts: for part in secure_path_parts:
if part == "." or part == "..": if part == "." or part == "..":
print("Illegal path nice try")
return None return None
# Reconstruct the secure path # Reconstruct the secure path
@ -46,15 +45,13 @@ class RouteManager:
for part in secure_path.parts: for part in secure_path.parts:
if part.startswith("___"): if part.startswith("___"):
print("hidden file")
raise Exception("Illegal path") 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: Path = self.config.content_dir / (path if path else "index.md")
if not path or file_path.is_dir():
file_path = file_path / "index.md"
if file_path < self.config.content_dir: if file_path < self.config.content_dir:
raise Exception("Illegal path") raise Exception("Illegal path")
@ -73,9 +70,7 @@ class RouteManager:
"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 file_path: Path = self.config.content_dir / (path if path else "index.md")
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,
@ -125,10 +120,8 @@ class RouteManager:
return ( return (
thumbnail_bytes, thumbnail_bytes,
200, 200,
{ {"Content-Type": f"image/{img_format.lower()}",
"Content-Type": f"image/{img_format.lower()}", "cache-control": "public, max-age=31536000"},
"cache-control": "public, max-age=31536000",
},
) )
return send_file(file_path) return send_file(file_path)
else: else: