"""Build an instance as a static site suitable for serving via e.g. Nginx. SPDX-FileCopyrightText: © 2022 Brian S. Stephan 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)