121 lines
5.2 KiB
Python
121 lines
5.2 KiB
Python
"""Build an instance as a static site suitable for serving via e.g. Nginx.
|
|
|
|
SPDX-FileCopyrightText: © 2022 Brian S. Stephan <bss@incorporeal.org>
|
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""
|
|
import argparse
|
|
import os
|
|
import shutil
|
|
import stat
|
|
import tempfile
|
|
|
|
from termcolor import cprint
|
|
|
|
from incorporealcms import init_instance
|
|
from incorporealcms.pages import handle_markdown_file_path
|
|
|
|
|
|
def build():
|
|
"""Build the static site generated against an instance directory."""
|
|
parser = argparse.ArgumentParser(
|
|
description="Build the static site generated against an instance directory.",
|
|
)
|
|
parser.add_argument(
|
|
'instance_dir', help="path to instance directory root (NOTE: the program will go into pages/)"
|
|
)
|
|
parser.add_argument(
|
|
'output_dir', help="path to directory to output to (NOTE: the program must be able to write into its parent!)"
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
# check output path before doing work
|
|
if not os.path.isdir(args.output_dir):
|
|
# if it doesn't exist, great, we'll just move the temporary dir later;
|
|
# if it exists and is a dir, that's fine, but if it's a file, we should error
|
|
if os.path.exists(args.output_dir):
|
|
raise ValueError(f"specified output path '{args.output_dir}' exists as a file!")
|
|
|
|
output_dir = os.path.abspath(args.output_dir)
|
|
instance_dir = os.path.abspath(args.instance_dir)
|
|
pages_dir = os.path.join(instance_dir, 'pages')
|
|
|
|
# initialize configuration with the path to the instance
|
|
init_instance(instance_dir)
|
|
|
|
# putting the temporary directory next to the desired output so we can safely rename it later
|
|
tmp_output_dir = tempfile.mkdtemp(dir=os.path.dirname(output_dir))
|
|
cprint(f"creating temporary directory '{tmp_output_dir}' for writing", 'green')
|
|
|
|
# CORE CONTENT
|
|
# render and/or copy into the output dir after changing into the instance dir (to simplify paths)
|
|
os.chdir(pages_dir)
|
|
for base_dir, subdirs, files in os.walk(pages_dir):
|
|
# remove the absolute path of the pages directory from the base_dir
|
|
base_dir = os.path.relpath(base_dir, pages_dir)
|
|
# create subdirs seen here for subsequent depth
|
|
for subdir in subdirs:
|
|
dst = os.path.join(tmp_output_dir, base_dir, subdir)
|
|
if os.path.islink(os.path.join(base_dir, subdir)):
|
|
# keep the link relative to the output directory
|
|
src = symlink_to_relative_dest(pages_dir, os.path.join(base_dir, subdir))
|
|
print(f"creating directory symlink '{dst}' -> '{src}'")
|
|
os.symlink(src, dst, target_is_directory=True)
|
|
else:
|
|
print(f"creating directory '{dst}'")
|
|
os.mkdir(dst)
|
|
|
|
# process and copy files
|
|
for file_ in files:
|
|
dst = os.path.join(tmp_output_dir, base_dir, file_)
|
|
if os.path.islink(os.path.join(base_dir, file_)):
|
|
# keep the link relative to the output directory
|
|
src = symlink_to_relative_dest(pages_dir, os.path.join(base_dir, file_))
|
|
print(f"creating symlink '{dst}' -> '{src}'")
|
|
os.symlink(src, dst, target_is_directory=False)
|
|
else:
|
|
src = os.path.join(base_dir, file_)
|
|
print(f"copying file '{src}' -> '{dst}'")
|
|
shutil.copy2(src, dst)
|
|
|
|
# render markdown as HTML
|
|
if src.endswith('.md'):
|
|
rendered_file = dst.removesuffix('.md') + '.html'
|
|
try:
|
|
content = handle_markdown_file_path(src)
|
|
except UnicodeDecodeError:
|
|
# perhaps this isn't a markdown file at all for some reason; we
|
|
# copied it above so stick with tha
|
|
cprint(f"{src} has invalid bytes! skipping", 'yellow')
|
|
continue
|
|
with open(rendered_file, 'w') as dst_file:
|
|
dst_file.write(content)
|
|
|
|
# TODO: STATIC DIR
|
|
|
|
# move temporary dir to the destination
|
|
old_output_dir = f'{output_dir}-old-{os.path.basename(tmp_output_dir)}'
|
|
if os.path.exists(output_dir):
|
|
cprint(f"renaming '{output_dir}' to '{old_output_dir}'", 'green')
|
|
os.rename(output_dir, old_output_dir)
|
|
cprint(f"renaming '{tmp_output_dir}' to '{output_dir}'", 'green')
|
|
os.rename(tmp_output_dir, output_dir)
|
|
os.chmod(output_dir,
|
|
stat.S_IWUSR | stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
|
|
# TODO: unlink old dir above? arg flag?
|
|
|
|
|
|
def symlink_to_relative_dest(base_dir: str, source: str) -> str:
|
|
"""Given a symlink, make sure it points to something inside the instance and provide its real destination.
|
|
|
|
Args:
|
|
base_dir: the full absolute path of the instance's pages dir, which the symlink destination must be in.
|
|
source: the symlink to check
|
|
Returns:
|
|
what the symlink points at
|
|
"""
|
|
if not os.path.realpath(source).startswith(base_dir):
|
|
raise ValueError(f"symlink destination {os.path.realpath(source)} is outside the instance!")
|
|
# this symlink points to realpath inside base_dir, so relative to base_dir, the symlink dest is...
|
|
return os.path.relpath(os.path.realpath(source), base_dir)
|