uhhh lots of things
All checks were successful
All checks were successful
This commit is contained in:
parent
0a6d5b8a90
commit
c6f36d0408
12
config.toml
12
config.toml
@ -3,7 +3,11 @@ content_dir = "/home/dubey/projects/foldsite/example/content"
|
||||
templates_dir = "/home/dubey/projects/foldsite/example/templates"
|
||||
styles_dir = "/home/dubey/projects/foldsite/example/styles"
|
||||
|
||||
[secrets]
|
||||
password = "YiaysZ4g8QX1R8R"
|
||||
aws_secret_key = "ybCvAq1GQpYg0kEeXc2LqfJl9y6/EXAMPLEKEY"
|
||||
aws_key_id = "AKIASQ5ZB43T69DWV8BQ"
|
||||
[server]
|
||||
listen_address = "0.0.0.0"
|
||||
listen_port = 8080
|
||||
enable_admin_browser = false
|
||||
admin_password = "password"
|
||||
max_threads = 4
|
||||
debug = false
|
||||
access_log = true
|
||||
|
13
main.py
13
main.py
@ -21,7 +21,13 @@ def main():
|
||||
r = RouteManager(c)
|
||||
t = TemplateHelpers(c)
|
||||
|
||||
server = Server()
|
||||
server = Server(
|
||||
debug=c.debug,
|
||||
host=c.listen_address,
|
||||
port=c.listen_port,
|
||||
access_log=c.access_log,
|
||||
workers=c.max_threads,
|
||||
)
|
||||
|
||||
server.register_template_function("get_sibling_content_files", t.get_sibling_content_files)
|
||||
server.register_template_function("get_text_document_preview", t.get_text_document_preview)
|
||||
@ -33,8 +39,9 @@ def main():
|
||||
server.register_route("/", r.default_route, defaults={"path": ""})
|
||||
server.register_route("/<path:path>", r.default_route)
|
||||
|
||||
file_manager_bp = create_filemanager_blueprint(c.content_dir, url_prefix='/admin', auth_password="password")
|
||||
server.app.register_blueprint(file_manager_bp)
|
||||
if c.admin_browser:
|
||||
file_manager_bp = create_filemanager_blueprint(c.content_dir, url_prefix='/admin', auth_password=c.admin_password)
|
||||
server.app.register_blueprint(file_manager_bp)
|
||||
|
||||
try:
|
||||
server.run()
|
||||
|
@ -9,10 +9,19 @@ class Configuration:
|
||||
|
||||
def __init__(self, config_path):
|
||||
self.config_path = config_path
|
||||
|
||||
self.content_dir: Path = None
|
||||
self.templates_dir: Path = None
|
||||
self.styles_dir: Path = None
|
||||
|
||||
self.listen_address: str = "127.0.0.1"
|
||||
self.listen_port: int = 8080
|
||||
self.debug: bool = False
|
||||
self.access_log: bool = True
|
||||
self.max_threads: int = 4
|
||||
self.admin_browser: bool = False
|
||||
self.admin_password: str = None
|
||||
|
||||
def load_config(self):
|
||||
try:
|
||||
with open(self.config_path, "rb") as f:
|
||||
@ -40,6 +49,18 @@ class Configuration:
|
||||
if not self.styles_dir:
|
||||
raise ValueError("Config file does not contain styles_dir path")
|
||||
self.styles_dir = Path(self.styles_dir)
|
||||
|
||||
server = self.config_data.get("server", {})
|
||||
if not server:
|
||||
raise ValueError("Config file does not contain server section")
|
||||
|
||||
self.listen_address = server.get("listen_address", self.listen_address)
|
||||
self.listen_port = server.get("listen_port", self.listen_port)
|
||||
self.debug = server.get("debug", self.debug)
|
||||
self.access_log = server.get("access_log", self.access_log)
|
||||
self.max_threads = server.get("max_threads", self.max_threads)
|
||||
self.admin_browser = server.get("admin_browser", self.admin_browser)
|
||||
self.admin_password = server.get("admin_password", self.admin_password)
|
||||
|
||||
def set_globals(self):
|
||||
global CONTENT_DIR, TEMPLATES_DIR, STYLES_DIR
|
||||
|
@ -1,7 +1,11 @@
|
||||
from dataclasses import dataclass
|
||||
from src.config.config import Configuration
|
||||
from src.rendering import GENERIC_FILE_MAPPING
|
||||
from src.rendering.markdown import render_markdown, read_raw_markdown, rendered_markdown_to_plain_text
|
||||
from src.rendering.markdown import (
|
||||
render_markdown,
|
||||
read_raw_markdown,
|
||||
rendered_markdown_to_plain_text,
|
||||
)
|
||||
from enum import Enum
|
||||
|
||||
from PIL import Image
|
||||
@ -16,9 +20,22 @@ class ImageMetadata:
|
||||
alt: str
|
||||
exif: dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class MarkdownMetadata:
|
||||
fontmatter: dict
|
||||
"""
|
||||
A class to represent metadata for a Markdown file.
|
||||
|
||||
Attributes:
|
||||
----------
|
||||
frontmatter : dict
|
||||
A dictionary containing the front matter of the Markdown file.
|
||||
content : str
|
||||
The main content of the Markdown file.
|
||||
preview : str
|
||||
A preview or summary of the Markdown content.
|
||||
"""
|
||||
frontmatter: dict
|
||||
content: str
|
||||
preview: str
|
||||
|
||||
@ -30,6 +47,24 @@ class FileMetadata:
|
||||
|
||||
@dataclass
|
||||
class TemplateFile:
|
||||
"""
|
||||
A class to represent a template file with its associated metadata.
|
||||
|
||||
Attributes:
|
||||
----------
|
||||
name (str): The name of the file.
|
||||
path (str): The file path.
|
||||
proper_name (str): The proper name of the file.
|
||||
extension (str): The file extension.
|
||||
categories (list[str]): A list of categories associated with the file.
|
||||
date_modified (str): The date the file was last modified.
|
||||
date_created (str): The date the file was created.
|
||||
size_kb (int): The size of the file in kilobytes.
|
||||
metadata (ImageMetadata | FileMetadata | None): Metadata associated with the file,
|
||||
which can be either image metadata, file metadata, or None.
|
||||
dir_item_count (int): The number of items in the directory if the file is a directory.
|
||||
is_dir (bool): A flag indicating whether the file is a directory.
|
||||
"""
|
||||
name: str
|
||||
path: str
|
||||
proper_name: str
|
||||
@ -51,9 +86,10 @@ class TemplateHelpers:
|
||||
def __init__(self, config: Configuration):
|
||||
self.config: Configuration = config
|
||||
|
||||
def _filter_hidden_files(self, files):
|
||||
return [f for f in files if not f.name.startswith("___")]
|
||||
|
||||
def build_metadata_for_file(self, path: str, categories: list[str] = []):
|
||||
"""Builds metadata for a file"""
|
||||
def _build_metadata_for_file(self, path: str, categories: list[str] = []):
|
||||
file_path = self.config.content_dir / path
|
||||
for k in categories:
|
||||
if k == "image":
|
||||
@ -75,19 +111,37 @@ class TemplateHelpers:
|
||||
ret = FileMetadata(None)
|
||||
if file_path.suffix[1:].lower() == "md":
|
||||
ret.typeMeta = MarkdownMetadata({}, "", "")
|
||||
ret.typeMeta.fontmatter = frontmatter.load(file_path)
|
||||
ret.typeMeta.frontmatter = frontmatter.load(file_path)
|
||||
ret.typeMeta.content = render_markdown(file_path)
|
||||
ret.typeMeta.rawContent = read_raw_markdown(file_path)
|
||||
ret.typeMeta.rawText = rendered_markdown_to_plain_text(ret.typeMeta.content)
|
||||
ret.typeMeta.rawText = rendered_markdown_to_plain_text(
|
||||
ret.typeMeta.content
|
||||
)
|
||||
ret.typeMeta.preview = ret.typeMeta.rawText[:500] + "..."
|
||||
return ret
|
||||
return None
|
||||
|
||||
|
||||
def get_folder_contents(self, path: str = ""):
|
||||
"""Returns the contents of a folder as a list of TemplateFile objects
|
||||
"""
|
||||
Retrieve the contents of a folder and return a list of TemplateFile objects.
|
||||
|
||||
The metadata field is populated with the appropriate metadata object
|
||||
Args:
|
||||
path (str): The relative path to the folder within the content directory. Defaults to an empty string,
|
||||
which refers to the root content directory.
|
||||
|
||||
Returns:
|
||||
list: A list of TemplateFile objects representing the files and directories within the specified folder.
|
||||
|
||||
The function performs the following steps:
|
||||
1. Constructs the full path to the folder by combining the content directory with the provided path.
|
||||
2. Retrieves all files and directories within the specified folder.
|
||||
3. Iterates over each file and directory, creating a TemplateFile object with metadata such as name,
|
||||
path, proper name, extension, categories, date modified, date created, size in KB, metadata, directory
|
||||
item count, and whether it is a directory.
|
||||
4. If the item is a file, it assigns categories based on the file extension using a predefined mapping.
|
||||
5. Builds additional metadata for each file.
|
||||
6. Filters out hidden files from the list.
|
||||
7. Returns the list of TemplateFile objects.
|
||||
"""
|
||||
search_contnet_path = self.config.content_dir / path
|
||||
files = search_contnet_path.glob("*")
|
||||
@ -110,30 +164,70 @@ class TemplateHelpers:
|
||||
for k, v in GENERIC_FILE_MAPPING.items():
|
||||
if f.suffix[1:].lower() in v:
|
||||
t.categories.append(k)
|
||||
t.metadata = self.build_metadata_for_file(f, t.categories)
|
||||
t.metadata = self._build_metadata_for_file(f, t.categories)
|
||||
ret.append(t)
|
||||
ret = self._filter_hidden_files(ret)
|
||||
return ret
|
||||
|
||||
def get_sibling_content_files(self, path: str = ""):
|
||||
"""
|
||||
Retrieves a list of sibling content files in the specified directory.
|
||||
|
||||
Args:
|
||||
path (str): The relative path within the content directory to search for files.
|
||||
Defaults to an empty string, which means the root of the content directory.
|
||||
|
||||
Returns:
|
||||
list: A list of tuples, where each tuple contains the file name and its relative path
|
||||
to the content directory. Only files that do not start with "___" are included.
|
||||
"""
|
||||
search_contnet_path = self.config.content_dir / path
|
||||
files = search_contnet_path.glob("*")
|
||||
return [
|
||||
(file.name, str(file.relative_to(self.config.content_dir)))
|
||||
for file in files
|
||||
if file.is_file()
|
||||
if file.is_file() and not file.name.startswith("___")
|
||||
]
|
||||
|
||||
def get_text_document_preview(self, path: str):
|
||||
"""
|
||||
Generates a preview of the text document located at the given path.
|
||||
|
||||
This method reads the first 100 characters from the specified text file
|
||||
and returns it as a string. The file path is constructed by combining
|
||||
the content directory from the configuration with the provided path.
|
||||
|
||||
Args:
|
||||
path (str): The relative path to the text document within the content directory.
|
||||
|
||||
Returns:
|
||||
str: A string containing the first 100 characters of the text document.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the file at the specified path does not exist.
|
||||
IOError: If an I/O error occurs while reading the file.
|
||||
"""
|
||||
file_path = self.config.content_dir / path
|
||||
with open(file_path, "r") as f:
|
||||
content = f.read(100)
|
||||
return content
|
||||
|
||||
def get_sibling_content_folders(self, path: str = ""):
|
||||
"""
|
||||
Retrieves a list of sibling content folders within a specified directory.
|
||||
|
||||
Args:
|
||||
path (str): A relative path from the content directory to search within. Defaults to an empty string,
|
||||
which means the search will be conducted in the content directory itself.
|
||||
|
||||
Returns:
|
||||
list of tuple: A list of tuples where each tuple contains the folder name and its relative path
|
||||
to the content directory. Only directories that do not start with "___" are included.
|
||||
"""
|
||||
search_contnet_path = self.config.content_dir / path
|
||||
files = search_contnet_path.glob("*")
|
||||
return [
|
||||
(file.name, str(file.relative_to(self.config.content_dir)))
|
||||
for file in files
|
||||
if file.is_dir()
|
||||
if file.is_dir() and not file.name.startswith("___")
|
||||
]
|
||||
|
@ -95,6 +95,30 @@ def render_page(
|
||||
template_path: Path = Path("./"),
|
||||
style_path: Path = Path("./"),
|
||||
):
|
||||
"""
|
||||
Renders a web page based on the provided path and optional base, template, and style paths.
|
||||
|
||||
Args:
|
||||
path (Path): The path to the target file or directory to render.
|
||||
base_path (Path, optional): The base path to use for relative paths. Defaults to Path("./").
|
||||
template_path (Path, optional): The path to the directory containing HTML templates. Defaults to Path("./").
|
||||
style_path (Path, optional): The path to the directory containing CSS styles. Defaults to Path("./").
|
||||
|
||||
Returns:
|
||||
str: The rendered HTML content of the page, or an error page if the target path does not exist or no suitable template is found.
|
||||
|
||||
Raises:
|
||||
Exception: If the base template (base.html) is not found in the template_path.
|
||||
|
||||
Notes:
|
||||
- If the target path does not exist, a 404 error page is rendered.
|
||||
- If the target path is a file, the function attempts to determine its type, category, and extension.
|
||||
- The function generates a list of possible CSS styles based on the target path and its type/category.
|
||||
- The function searches for suitable HTML templates based on the target path and its type/category.
|
||||
- If no suitable template is found, the function either sends the file directly (if it's a file) or renders a 404 error page.
|
||||
- If the target file is a document, its content is rendered as Markdown.
|
||||
- The function ensures that the base template (base.html) exists before rendering the final content.
|
||||
"""
|
||||
if not path.exists():
|
||||
return render_error_page(
|
||||
error_code=404,
|
||||
@ -111,7 +135,18 @@ def render_page(
|
||||
relative_path = target_file.relative_to(base_path)
|
||||
relative_dir = target_path.relative_to(base_path)
|
||||
|
||||
# Generate the possible paths for style
|
||||
"""
|
||||
The styles are ordered in the following manner:
|
||||
|
||||
Specific style for the target path (e.g., /path/to/target.css).
|
||||
Specific styles for the type and extension in the current and parent directories
|
||||
(e.g., /path/to/__file.html.css).
|
||||
Specific styles for the type and category in the current and parent directories
|
||||
(e.g., /path/to/__file.document.css).
|
||||
Base style (/base.css).
|
||||
This ordering ensures that the most specific styles are applied first, followed by
|
||||
more general styles, and finally the base style.
|
||||
"""
|
||||
styles = []
|
||||
styles.append("/" + str(relative_path) + ".css")
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
from pathlib import Path
|
||||
from src.config.config import Configuration
|
||||
from src.rendering.renderer import render_page, render_error_page
|
||||
from flask import send_file
|
||||
from flask import send_file
|
||||
from src.rendering.image import generate_thumbnail
|
||||
from functools import lru_cache
|
||||
import os
|
||||
@ -11,7 +11,6 @@ class RouteManager:
|
||||
def __init__(self, config: Configuration):
|
||||
self.config = config
|
||||
|
||||
|
||||
def _validate_and_sanitize_path(self, base_dir, requested_path):
|
||||
"""
|
||||
Validate and sanitize the requested path to ensure it does not traverse above the base directory.
|
||||
@ -21,11 +20,11 @@ class RouteManager:
|
||||
:return: A secure version of the requested path if valid, otherwise None.
|
||||
"""
|
||||
# Normalize both paths
|
||||
base_dir = os.path.abspath(base_dir)
|
||||
requested_path = os.path.abspath(requested_path)
|
||||
base_dir = Path(base_dir)
|
||||
requested_path: Path = base_dir / requested_path
|
||||
|
||||
# Check if the requested path is within the base directory
|
||||
if not requested_path.startswith(base_dir):
|
||||
if requested_path < base_dir:
|
||||
return None
|
||||
|
||||
# Ensure the path does not contain any '..' or '.' components
|
||||
@ -33,32 +32,33 @@ class RouteManager:
|
||||
secure_path_parts = secure_path.split(os.sep)
|
||||
|
||||
for part in secure_path_parts:
|
||||
if part == '.' or part == '..':
|
||||
if part == "." or part == "..":
|
||||
print("Illegal path nice try")
|
||||
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("___"):
|
||||
print("hidden file")
|
||||
raise Exception("Illegal path")
|
||||
|
||||
return secure_path
|
||||
|
||||
def _ensure_route(self, path: str):
|
||||
"""
|
||||
Escapes the path for anything like
|
||||
a path execution or injection attack
|
||||
evaluates the path and ensures that it it does not
|
||||
go above the self.content.content_dir
|
||||
If any part of the path contains __, __{foldername}, or __{filename},
|
||||
that is a hidden file or folder and should raise an exception
|
||||
Any illegal path should raise an exception
|
||||
"""
|
||||
file_path: Path = self.config.content_dir / (path if path else "index.md")
|
||||
if file_path < self.config.content_dir:
|
||||
raise Exception("Illegal path")
|
||||
|
||||
for part in file_path.parts:
|
||||
if part.startswith("__"):
|
||||
raise Exception("Illegal path")
|
||||
|
||||
if not self._validate_and_sanitize_path(self.config.content_dir, str(file_path)):
|
||||
if not self._validate_and_sanitize_path(
|
||||
self.config.content_dir, str(file_path)
|
||||
):
|
||||
raise Exception("Illegal path")
|
||||
|
||||
def default_route(self, path: str):
|
||||
@ -80,6 +80,15 @@ class RouteManager:
|
||||
)
|
||||
|
||||
def get_style(self, path: str):
|
||||
try:
|
||||
self._validate_and_sanitize_path(self.config.styles_dir, path)
|
||||
except Exception as e:
|
||||
return render_error_page(
|
||||
404,
|
||||
"Not Found",
|
||||
f"The requested resource was not found on this server. {e}",
|
||||
self.config.templates_dir,
|
||||
)
|
||||
file_path: Path = self.config.styles_dir / path
|
||||
if file_path.exists():
|
||||
return send_file(file_path)
|
||||
@ -91,8 +100,16 @@ class RouteManager:
|
||||
self.config.templates_dir,
|
||||
)
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
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
|
||||
|
@ -13,8 +13,8 @@ class Server(BaseApplication):
|
||||
host: str = "0.0.0.0",
|
||||
port: int = 8080,
|
||||
template_functions: Dict[str, Callable] = None,
|
||||
enable_admin_browser: bool = False,
|
||||
workers: int = multiprocessing.cpu_count() // 2 + 1,
|
||||
access_log: bool = True,
|
||||
options=None,
|
||||
):
|
||||
if template_functions is None:
|
||||
@ -28,9 +28,9 @@ class Server(BaseApplication):
|
||||
self.app.secret_key = "your_secret_key"
|
||||
self.options = options or {
|
||||
"bind": f"{self.host}:{self.port}",
|
||||
"reload": True, # Enable automatic reloading
|
||||
"reload": self.debug,
|
||||
"threads": workers,
|
||||
"accesslog": "-",
|
||||
"accesslog": "-" if access_log else None,
|
||||
}
|
||||
for name, func in template_functions.items():
|
||||
self.register_template_function(name, func)
|
||||
|
Loading…
x
Reference in New Issue
Block a user