tilde-wiki/tildewiki/main.py

235 lines
8.4 KiB
Python

import os
import stat
import subprocess
from os.path import expanduser
import click
from click import ClickException, Abort
from click.types import Path
from shutil import rmtree
from .click_types import WikiRepo
from .compilation import compile_wiki
from . import git_wrapper as git
# TODO support reading from env
SITE_NAME = 'tilde.town'
PUBLISH_PATH = '/var/www/{site_name}/wiki'.format(site_name=SITE_NAME)
PREVIEW_PATH = expanduser('~/public_html/wiki')
LOCAL_REPOSITORY_PATH = expanduser('~/wiki')
REPOSITORY_PATH = '/wiki'
WIPE_PROMPT = 'This will wipe everything at {}. Proceed?'
LOCK_PATH = '/tmp/tildewiki.lock'
DEFAULT_PATH_KWARGS = dict(
exists=True,
writable=True,
readable=True,
file_okay=False,
dir_okay=True)
class Config:
def __init__(self):
self.site_name = SITE_NAME
self.publish_path = PUBLISH_PATH
self.preview_path = PREVIEW_PATH
self.local_repo_path = LOCAL_REPOSITORY_PATH
self.repo_path = REPOSITORY_PATH
self.author_name = os.environ.get('LOGNAME')
@property
def author_email(self):
return '{}@{}'.format(self.author_name, self.site_name)
pass_config = click.make_pass_decorator(Config, ensure=True)
@click.group()
@click.option('--site-name', default=SITE_NAME, help='The root domain of the wiki.')
@click.option('--publish-path',
default=PUBLISH_PATH,
help='System level path to wiki for publishing.',
type=Path(**DEFAULT_PATH_KWARGS))
@click.option('--repo-path',
default=REPOSITORY_PATH,
help='Path to the shared wiki repository.',
type=WikiRepo(**DEFAULT_PATH_KWARGS))
@pass_config
def main(config, site_name, publish_path, repo_path):
"""This tool helps manage a wiki that exists as a git repository on a
social server."""
# TODO click does not appear to call expanduser on things. it'd be nice to
# opt into that with the Path type. Should click be patched? Or should we
# use a custom Path type?
config.site_name = site_name
config.publish_path = publish_path
config.repo_path = repo_path
@main.command()
@click.option('--local-repo-path', default=LOCAL_REPOSITORY_PATH,
help='Path to shared wiki git repository.', type=Path(file_okay=False))
@click.option('--preview-path', default=PREVIEW_PATH,
help='Local path to wiki for previewing.', type=Path(file_okay=False))
@pass_config
def init(config, local_repo_path, preview_path):
"""
Initializes a local copy of the shared wiki.
"""
if os.path.exists(os.path.join(local_repo_path)):
raise ClickException(
'{} already exists. Have you already run wiki init?'.format(
local_repo_path))
if os.path.exists(os.path.join(preview_path)):
raise ClickException(
'{} already exists. Have you already run wiki init?'.format(
preview_path))
click.echo('Cloning {} to {}...'.format(config.repo_path, local_repo_path))
git.create_repo(
config.repo_path,
config.local_repo_path,
config.author_name,
config.author_email
)
click.echo('Creating {}...'.format(preview_path))
os.makedirs(preview_path)
click.echo('Compiling wiki preview for the first time...')
_preview(preview_path, local_repo_path)
click.echo('~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~')
click.echo("Congrats, you are ready to contribute to {}'s wiki!".format(
config.site_name
))
@main.command()
@click.option('--local-repo-path', default=LOCAL_REPOSITORY_PATH,
help='Path to local clone of wiki repository.', type=WikiRepo(**DEFAULT_PATH_KWARGS))
@click.option('--preview-path',
default=PREVIEW_PATH,
help='Local path to wiki for previewing.',
type=Path(**DEFAULT_PATH_KWARGS))
@pass_config
def preview(config, preview_path, local_repo_path):
"""Compiles all the files in the local wiki repository."""
click.confirm(
WIPE_PROMPT.format(preview_path),
abort=True)
clear_directory(preview_path)
_preview(preview_path, local_repo_path)
def _on_create(file_path: str) -> None:
"""This callback takes a path to a file or directory created on disk
during compilation. We want to make sure that everything we create as part
of publish compilation is world-writable so the next user can overwrite
it."""
flags = stat.S_ISGID
flags |= stat.S_IWOTH | stat.S_IROTH
flags |= stat.S_IRUSR | stat.S_IWUSR
flags |= stat.S_IRGRP | stat.S_IWGRP
if os.path.isdir(file_path):
flags |= stat.S_IXOTH | stat.S_IXGRP | stat.S_IXUSR
os.chmod(file_path, flags)
@main.command()
@click.option('--local-repo-path', default=LOCAL_REPOSITORY_PATH,
help='Path to local clone of wiki repository.', type=WikiRepo(**DEFAULT_PATH_KWARGS))
@pass_config
def publish(config, local_repo_path):
"""Commits any local changes, syncs with the shared wiki repository (in
both directions), and recompiles the shared wiki."""
if os.path.exists(LOCK_PATH):
raise ClickException('The wiki lock file already exists. Seems like someone else is compiling.')
rm_error_paths = []
onerror = lambda f,p,e: rm_error_paths.append(p)
error = None
lockf = open(LOCK_PATH, 'w')
try:
click.echo('Committing your changes locally...')
git.make_commit(local_repo_path, config.author_name, config.author_email)
git.pull_from_origin(local_repo_path)
click.echo('Pushing your changes...')
git.push_hard(local_repo_path, config.repo_path)
click.echo('Compiling wiki to {}'.format(config.publish_path))
click.confirm(WIPE_PROMPT.format(config.publish_path), abort=True)
clear_directory(config.publish_path)
compile_wiki(config.repo_path, config.publish_path, on_create=_on_create)
except ClickException:
raise
except Abort:
raise
except Exception as e:
error = e
finally:
lockf.close()
try:
os.remove(LOCK_PATH)
except FileNotFoundError:
pass
if error is not None:
raise ClickException('Failed publishing wiki. Error: {}'.format(error))
@main.command()
@click.option('--preview', help='show pages from your local wiki', is_flag=True)
@click.option('--preview-path', default=PREVIEW_PATH,
help='Local path to wiki for previewing.', type=Path(file_okay=False))
@click.argument('path')
@pass_config
def get(config, preview, preview_path, path):
"""Given a path to a file in the wiki, open it in a browser. Uses
sensible-browser. No need to specify the extension; e.g., 'wiki get
editors/emacs' will show /wiki/editors/emacs.html in the browser."""
read_path = config.publish_path
if preview:
read_path = preview_path
path = os.path.join(read_path, path)
if os.path.exists(path)\
and os.path.isdir(path)\
and os.path.exists(os.path.join(path, 'index.html')):
path = os.path.join(path, 'index.html')
elif os.path.exists(path + '.html'):
path = path + '.html'
else:
raise ClickException("Couldn't find path {}".format(path))
subprocess.run(['sensible-browser', path])
@main.command()
@click.option('--local-repo-path', default=LOCAL_REPOSITORY_PATH,
help='Path to shared wiki git repository.', type=WikiRepo(**DEFAULT_PATH_KWARGS))
@pass_config
def sync(config, local_repo_path):
"""Syncs a local copy of the wiki with the shared copy. Resets any
outstanding changes. If those changes should be kept, publish them
first."""
if git.dirty(local_repo_path):
click.confirm("This will overwrite any changes you've made locally. Proceed?", abort=True)
git.reset_from_origin(local_repo_path)
def _preview(preview_path, local_repo_path):
compile_wiki(local_repo_path, preview_path)
click.echo('Your wiki preview is ready! navigate to ~{}/wiki'.format(
os.environ.get('LOGNAME')))
def clear_directory(path:str) -> None:
"""Given a path to a directory, deletes everything in it. Use with
caution."""
if path in ['', '/', '~', '*']:
raise ValueError('"{}" is not a valid path for clearing'.format(path))
if not os.path.isdir(path):
raise ValueError('{} is not a directory'.format(path))
for root, dirs, files in os.walk(path):
for f in files:
os.unlink(os.path.join(root, f))
for d in dirs:
rmtree(os.path.join(root, d))