feat: ADD 'new' function to generate new instance to avoid git clone #1

This commit is contained in:
Caffeine Fueled 2026-07-04 08:54:05 +02:00
parent 8e91571121
commit 0cbd56b689
Signed by: cf7
GPG key ID: CA295D643074C68C
2 changed files with 119 additions and 13 deletions

View file

@ -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 Python script to /usr/local/bin so it survives volume mounts
COPY picopaper.py /usr/local/bin/picopaper.py 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 # Set working directory
WORKDIR /app WORKDIR /app
@ -24,5 +34,8 @@ ENV PYTHONPATH=/app
# Run as root to allow writing to mounted volumes # Run as root to allow writing to mounted volumes
# (The mounted volume will have host user permissions) # (The mounted volume will have host user permissions)
# Generate the site on container start # With no command the site is generated; pass `new` to scaffold a blog instead:
CMD ["python3", "/usr/local/bin/picopaper.py"] # 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 []

View file

@ -2,22 +2,32 @@
import os import os
import re import re
import sys
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
import markdown import markdown
from config import (BLOG_TITLE, BLOG_DESCRIPTION, THEME, EXCLUDE_SECTIONS_FROM_MAIN, # Blog configuration lives in config.py in the working directory. It is absent
NAVBAR_ITEMS, HIDE_LOGO, HIDE_TITLE, LOGO_PATH, # when scaffolding a new blog into an empty directory (the `new` command), so a
ENABLE_RSS_FEED, RSS_FEED_PATH, # missing config.py must not stop this module from loading — the build command
BASE_URL, AUTHOR_NAME, AUTHOR_EMAIL, FEED_MAX_ITEMS, # checks CONFIG_AVAILABLE below and reports it clearly.
BLOGROLL_PATH, ROOT_PAGE) 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. # 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. # Default to enabled so the privacy feature works without a config update.
try: if CONFIG_AVAILABLE:
from config import STRIP_IMAGE_EXIF try:
except ImportError: from config import STRIP_IMAGE_EXIF
STRIP_IMAGE_EXIF = True except ImportError:
STRIP_IMAGE_EXIF = True
# Raster image formats whose metadata (EXIF/GPS/XMP/text chunks) is stripped on copy # 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'} 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}/") 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(): def main():
generator = SSGGGenerator() args = sys.argv[1:]
generator.generate() 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__': if __name__ == '__main__':
main() main()