Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
gechandesu committed Mar 23, 2021
0 parents commit 73f23a5
Show file tree
Hide file tree
Showing 18 changed files with 912 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__/
*~
*.swp
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Changelog

## v1.0 2021.03.23

### Added

- Bootstrap 5. Brand new frontend.
- Bootstrap Icons.
- `Flask.session` based authentication. Can be enabled in `config.py`. Password encrypted by `bcrypt`.
- `pass.py` to set password.
- Normal 404 error page.
- `CONTENTS.md` parser. You can navigate between articles.
- Article title parser. The title is now displayed in the title of the page.
- New shitcode. It will be refactored in next versions.

### Changed

- `contents.md` and `home.md` renamed to `CONTENTS.md` and `HOME.md`.
- `native` Pygments theme by default.
- File search algorithm changed. Now the viewing of files nested in folders works.
- The main application code has been moved to the `owl.py` module. The launch point of the application is now also the `owl.py`, and not the `wsgi.py`. It may not be the best architectural solution, but it seems to be the most concise now.

### Removed

- Old shitcode removed. See Changed.
- Old frontend and templates.

## v0.1 2020.08.15

First version released.
24 changes: 24 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to <http://unlicense.org/>
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# owl

![](https://img.shields.io/badge/owl-v1.0-%2300f)

**owl** — is the minimalistic kn**owl**edge base web app written in Python.

See full docs and demo here: [https://owl.gch.icu/docs/](https://owl.gch.icu/docs/).

Run **owl** in five lines:

```bash
python3 -m venv env
source env/bin/activate
git clone https://github.com/gechandesu/owl.git && cd owl
pip install -r requirements.txt
python owl.py
```

App is now available at [http://localhost:5000/](http://localhost:5000/).

**owl** doesn't use a database, all files are stored in plain text.

This solution is suitable for creating documentation or maintaining a personal knowledge base.

New in `v1.0`:
- This is brand new owl!
- New frontend and refactored backend.
- Bootstrap 5
- Optional authentication.

See [CHANGELOG.md](CHANGELOG.md)

# License

This software is licensed under The Unlicense. See [LICENSE](LICENSE).
16 changes: 16 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class Config(object):
DEBUG = False
SECRET_KEY = 'top_secret'
PASSWORD_FILE = '.pw'
SIGN_IN = False # Enable or disable authentication
MARKDOWN_ROOT = 'docs/' # Path to your .md files
MARKDOWN_DOWLOADS = True
# See https://github.com/trentm/python-markdown2/wiki/Extras
MARKDOWN2_EXTRAS = [
'fenced-code-blocks',
'markdown-in-html',
'code-friendly',
'header-ids',
'strike',
'tables'
]
3 changes: 3 additions & 0 deletions docs/CONTENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Contents

- [Home](/)
6 changes: 6 additions & 0 deletions docs/HOME.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# @v@ owl took off!

Read the [Docs](https://owl.gch.icu/docs/) to get started.

Also there is project's [git repository](https://gitea.gch.icu/gd/owl) ([mirror](https://github.com/gechandesu/owl)).

157 changes: 157 additions & 0 deletions owl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import os
import re
from functools import wraps
from datetime import timedelta

import pygments
from markdown2 import Markdown

from flask import Flask
from flask import request
from flask import session
from flask import redirect
from flask import render_template
from flask import send_from_directory
from flask_bcrypt import Bcrypt

from config import Config


app = Flask(__name__)
app.config.from_object(Config)
app.permanent_session_lifetime = timedelta(hours=24)

bcrypt = Bcrypt(app)

def read_file(filepath: str) -> str:
try:
with open(filepath, 'r') as file:
return file.read()
except IOError:
return 'Error: Cannot read file: {}'.format(filepath)

def render_html(filepath: str) -> str:
markdown = Markdown(extras=app.config['MARKDOWN2_EXTRAS'])
return markdown.convert(
read_file(
app.config['MARKDOWN_ROOT'] + filepath
)
)

def parse_title_from_markdown(filepath: str) -> str:
# This function parses article title from first level heading.
# It returns the occurrence of the first heading, and there
# can be nothing before it except empty lines and spaces.
article = read_file(app.config['MARKDOWN_ROOT'] + filepath)
pattern = re.compile(r'^\s*#\s.*')
if pattern.search(article):
return pattern.search(article).group().strip()[2:]
else:
return 'Error: Cannot parse title from file:'.format(filepath)

def parse_content_links(filepath: str) -> list:
# This function returns a list of links from a Markdown file.
# Only links contained in the list (ul ol li) are parsed.
r = re.compile(r'(.*(-|\+|\*|\d).?\[.*\])(\(.*\))', re.MULTILINE)
links = []
for tpl in r.findall(read_file(app.config['MARKDOWN_ROOT'] + filepath)):
for item in tpl:
if re.match(r'\(.*\)', item):
if item == '(/)':
item = '/' # This is a crutch for fixing the root url
# which for some reason continues to contain
# parentheses after re.match(r'').
if not item[1:-1].endswith('/'):
item = item[1:-1] + '/'
links.append(item)
return links

def check_password(password: str) -> bool:
if os.path.exists('.pw'):
pw_hash = read_file('.pw')
return bcrypt.check_password_hash(pw_hash, password)
else:
return False

@app.errorhandler(404)
def page_not_found(e):
return render_template('404.j2'), 404

@app.context_processor
def utility_processor():
def get_len(list: list) -> int:
return len(list)
def get_title(path: str) -> str:
return parse_title_from_markdown(path[:-1] + '.md')
return dict(get_title = get_title, get_len = get_len)

def login_required(func):
@wraps(func)
def wrap(*args, **kwargs):
if app.config['SIGN_IN']:
if 'logged_in' in session:
return func(*args, **kwargs)
else:
return redirect('/signin/')
else:
return func(*args, **kwargs)
return wrap

@app.route('/signin/', methods = ['GET', 'POST'])
def signin():
if request.method == 'POST':
if check_password(request.form['password']):
session['logged_in'] = True
return redirect('/', 302)
else:
return render_template('signin.j2', wrong_pw = True)
else:
return render_template('signin.j2')

@app.route('/signout/')
@login_required
def signout():
session.pop('logged_in', None)
return redirect('/signin/')

@app.route('/')
@login_required
def index():
return render_template(
'index.j2',
title = parse_title_from_markdown('HOME.md'),
article = render_html('HOME.md'),
contents = render_html('CONTENTS.md'),
current_path = '/',
links = parse_content_links('CONTENTS.md')
)

@app.route('/<path:path>/')
@login_required
def get_article(path):
if os.path.exists(app.config['MARKDOWN_ROOT'] + path + '.md'):
return render_template(
'index.j2',
title = parse_title_from_markdown(path + '.md'),
article = render_html(path + '.md'),
contents = render_html('CONTENTS.md'),
current_path = request.path,
links = parse_content_links('CONTENTS.md')
)
else:
return page_not_found(404)

@app.route('/<path:path>.md')
@login_required
def download_article(path):
if os.path.exists(app.config['MARKDOWN_ROOT'] + path + '.md') \
and app.config['MARKDOWN_DOWLOADS']:
return send_from_directory(
app.config['MARKDOWN_ROOT'],
path + '.md'
)
else:
return page_not_found(404)

if __name__ == '__main__':
app.run()
26 changes: 26 additions & 0 deletions pass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from getpass import getpass

from owl import app
from flask_bcrypt import Bcrypt

bcrypt = Bcrypt(app)

def generate_pw_hash(password, file):
pw_hash = bcrypt.generate_password_hash(password).decode('utf-8')
with open(file, 'w') as pwfile:
pwfile.write(pw_hash)

if __name__ == '__main__':
with app.app_context():
file = input('Enter password file name (default: .pw): ')
if not file:
file = '.pw'
password = getpass('Enter new password: ')
confirm = getpass('Confirm password: ')
if password != confirm:
print('Abort! Password mismatch.')
exit()
generate_pw_hash(password, file)
print('Success! New password file created: {}'.format(file))
if file != '.pw':
print('Don\'t forgot change password file name in `config.py`.')
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
flask>=1.1
flask-bcrypt>=0.7
markdown2>=2.3
pygments>=2.6
Loading

0 comments on commit 73f23a5

Please sign in to comment.