Milestone 1.

This commit is contained in:
2025-08-18 20:14:56 +02:00
parent 9d1623c739
commit 1646d7b827
23 changed files with 1684 additions and 1275 deletions

40
app/utils/paths.py Normal file
View File

@@ -0,0 +1,40 @@
from __future__ import annotations
import os
import tempfile
from typing import Optional
def atomic_write(path: str, data: bytes, mode: int = 0o644) -> None:
"""
Atomically write bytes to path by writing to a temp file in the same dir and then replacing.
"""
directory = os.path.dirname(path)
os.makedirs(directory, exist_ok=True)
fd, tmp_path = tempfile.mkstemp(prefix=".tmp-", dir=directory)
try:
with os.fdopen(fd, "wb") as tmp_file:
tmp_file.write(data)
os.chmod(tmp_path, mode)
os.replace(tmp_path, path)
finally:
try:
if os.path.exists(tmp_path):
os.remove(tmp_path)
except Exception:
pass
def ensure_unique_path(path: str) -> str:
"""
If path exists, append -2, -3, ... before extension to avoid collision.
"""
if not os.path.exists(path):
return path
base, ext = os.path.splitext(path)
i = 2
while True:
candidate = f"{base}-{i}{ext}"
if not os.path.exists(candidate):
return candidate
i += 1

45
app/utils/slugs.py Normal file
View File

@@ -0,0 +1,45 @@
import re
import unicodedata
def slugify(title: str, max_length: int = 80) -> str:
"""
Convert a title to a filesystem-friendly slug:
- normalize unicode to NFKD and strip accents
- lowercase
- replace non alphanum with hyphens
- collapse duplicate hyphens
- trim hyphens at edges
- limit length (keeping whole segments if possible)
"""
if not title:
return "untitled"
# Normalize accents
normalized = unicodedata.normalize("NFKD", title)
ascii_str = normalized.encode("ascii", "ignore").decode("ascii")
# Lowercase and replace non-alphanum with hyphen
slug = re.sub(r"[^a-z0-9]+", "-", ascii_str.lower()).strip("-")
if not slug:
slug = "untitled"
# Collapse multiple hyphens
slug = re.sub(r"-{2,}", "-", slug)
# Respect max length, try not to cut segments
if len(slug) > max_length:
parts = slug.split("-")
cut = []
total = 0
for p in parts:
# +1 for the hyphen when joining (except first)
add = len(p) + (1 if cut else 0)
if total + add > max_length:
break
cut.append(p)
total += add
slug = "-".join(cut) if cut else slug[:max_length].rstrip("-")
return slug

19
app/utils/time.py Normal file
View File

@@ -0,0 +1,19 @@
from __future__ import annotations
from datetime import datetime, timezone
def now_iso_utc() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
def is_iso_utc(value: str) -> bool:
try:
# Accept the 'Z' suffix
if value.endswith("Z"):
datetime.fromisoformat(value.replace("Z", "+00:00"))
else:
datetime.fromisoformat(value)
return True
except Exception:
return False