From 0cbd56b6895da0e8b26abe6e3711630b179b3d89 Mon Sep 17 00:00:00 2001 From: CaffeineFueled Date: Sat, 4 Jul 2026 08:54:05 +0200 Subject: [PATCH] feat: ADD 'new' function to generate new instance to avoid git clone #1 --- Dockerfile | 17 +++++++- picopaper.py | 115 ++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 119 insertions(+), 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index 78f1da5..c7838dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,16 @@ RUN python3 -m pip install --no-cache-dir -r /tmp/requirements.txt --break-syste # Copy Python script to /usr/local/bin so it survives volume mounts COPY picopaper.py /usr/local/bin/picopaper.py +# Bake the default blog content into the image. The `new` command copies these +# into the working directory to scaffold a fresh blog without cloning the repo. +# Only user content is included — not picopaper.py/requirements.txt/.gitignore. +ENV PICOPAPER_SKELETON=/usr/local/share/picopaper/skeleton +COPY config.py /usr/local/share/picopaper/skeleton/ +COPY theme/ /usr/local/share/picopaper/skeleton/theme/ +COPY items/ /usr/local/share/picopaper/skeleton/items/ +COPY images/ /usr/local/share/picopaper/skeleton/images/ +COPY static/ /usr/local/share/picopaper/skeleton/static/ + # Set working directory WORKDIR /app @@ -24,5 +34,8 @@ ENV PYTHONPATH=/app # Run as root to allow writing to mounted volumes # (The mounted volume will have host user permissions) -# Generate the site on container start -CMD ["python3", "/usr/local/bin/picopaper.py"] +# With no command the site is generated; pass `new` to scaffold a blog instead: +# podman run ... picopaper:latest -> build the site (default) +# podman run ... picopaper:latest new -> create a new blog in the current dir +ENTRYPOINT ["python3", "/usr/local/bin/picopaper.py"] +CMD [] diff --git a/picopaper.py b/picopaper.py index 206aca0..5435fa3 100644 --- a/picopaper.py +++ b/picopaper.py @@ -2,22 +2,32 @@ import os import re +import sys from datetime import datetime from pathlib import Path from jinja2 import Environment, FileSystemLoader import markdown -from config import (BLOG_TITLE, BLOG_DESCRIPTION, THEME, EXCLUDE_SECTIONS_FROM_MAIN, - NAVBAR_ITEMS, HIDE_LOGO, HIDE_TITLE, LOGO_PATH, - ENABLE_RSS_FEED, RSS_FEED_PATH, - BASE_URL, AUTHOR_NAME, AUTHOR_EMAIL, FEED_MAX_ITEMS, - BLOGROLL_PATH, ROOT_PAGE) +# Blog configuration lives in config.py in the working directory. It is absent +# when scaffolding a new blog into an empty directory (the `new` command), so a +# missing config.py must not stop this module from loading — the build command +# checks CONFIG_AVAILABLE below and reports it clearly. +try: + from config import (BLOG_TITLE, BLOG_DESCRIPTION, THEME, EXCLUDE_SECTIONS_FROM_MAIN, + NAVBAR_ITEMS, HIDE_LOGO, HIDE_TITLE, LOGO_PATH, + ENABLE_RSS_FEED, RSS_FEED_PATH, + BASE_URL, AUTHOR_NAME, AUTHOR_EMAIL, FEED_MAX_ITEMS, + BLOGROLL_PATH, ROOT_PAGE) + CONFIG_AVAILABLE = True +except ImportError: + CONFIG_AVAILABLE = False # STRIP_IMAGE_EXIF is a newer setting; older config.py files may not define it. # Default to enabled so the privacy feature works without a config update. -try: - from config import STRIP_IMAGE_EXIF -except ImportError: - STRIP_IMAGE_EXIF = True +if CONFIG_AVAILABLE: + try: + from config import STRIP_IMAGE_EXIF + except ImportError: + STRIP_IMAGE_EXIF = True # Raster image formats whose metadata (EXIF/GPS/XMP/text chunks) is stripped on copy STRIP_IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.tiff', '.tif', '.bmp'} @@ -543,9 +553,92 @@ class SSGGGenerator: print(f"\n✓ Site generated successfully in {self.output_dir}/") +# Location of the default project files baked into the container image. +DEFAULT_SKELETON_DIR = '/usr/local/share/picopaper/skeleton' + +# The blog content that `new` scaffolds. Only these are copied — the CLI itself +# (picopaper.py), requirements.txt and .gitignore belong to the tool/image, not +# the user's blog, so they are intentionally excluded. +SCAFFOLD_ITEMS = ('config.py', 'theme', 'items', 'images', 'static') + + +def scaffold_new(target_dir='.'): + """Create a new picopaper blog in target_dir by copying the default files. + + The source ("skeleton") defaults to the path baked into the container image + and can be overridden with the PICOPAPER_SKELETON environment variable. + Existing files in the target are never overwritten. + """ + import shutil + + skeleton = Path(os.environ.get('PICOPAPER_SKELETON', DEFAULT_SKELETON_DIR)) + target = Path(target_dir) + + if not skeleton.is_dir(): + print(f"Error: default project files not found at {skeleton}") + print("Run this inside the picopaper container, or point PICOPAPER_SKELETON " + "at a picopaper source directory.") + return 1 + + created, skipped = [], [] + for name in SCAFFOLD_ITEMS: + source = skeleton / name + if not source.exists(): + continue + dest = target / name + if dest.exists(): + # Never overwrite the user's files — leave anything already there. + skipped.append(name) + continue + if source.is_dir(): + shutil.copytree(source, dest) + else: + shutil.copy2(source, dest) + created.append(name) + + if not created: + print("Nothing to create — this directory already contains a picopaper blog.") + return 0 + + print("✓ Created a new picopaper blog in the current directory:") + for name in created: + print(f" {name}") + if skipped: + print("\nLeft untouched (already present):") + for name in skipped: + print(f" {name}") + + print("\nNext steps:") + print(" 1. Edit config.py (blog title, description, navbar).") + print(" 2. Add or edit Markdown posts in items/.") + print(" 3. Build the site by running the container with no command:") + print(" podman run --rm --userns=keep-id -v $(pwd):/app git.uphillsecurity.com/cf7/picopaper:latest") + print("\nYour generated site will appear in output/.") + return 0 + + def main(): - generator = SSGGGenerator() - generator.generate() + args = sys.argv[1:] + command = args[0] if args else 'build' + + if command in ('build', 'generate'): + if not CONFIG_AVAILABLE: + print("Error: no config.py found in the current directory.") + print("Run 'picopaper.py new' to scaffold a blog first, then edit config.py.") + sys.exit(1) + generator = SSGGGenerator() + generator.generate() + elif command == 'new': + sys.exit(scaffold_new()) + elif command in ('help', '-h', '--help'): + print("Usage: picopaper.py [command]\n") + print("Commands:") + print(" build Generate the site from the current directory (default)") + print(" new Scaffold a new picopaper blog into the current directory") + else: + print(f"Unknown command: {command!r}") + print("Usage: picopaper.py [build|new] (run 'picopaper.py help' for details)") + sys.exit(2) if __name__ == '__main__': main()