From 08e60efabcd40b2bdc6531308ad6a7a14edac58f Mon Sep 17 00:00:00 2001 From: Ville Rantanen Date: Fri, 23 Sep 2022 20:32:27 +0300 Subject: [PATCH] dockerized structure --- README.md | 3 +- code/Dockerfile | 10 + requirements.txt => code/requirements.txt | 0 revprox.py => code/revprox.py | 0 schema.sql => code/schema.sql | 0 code/shop.py | 613 ++++++++++++++++++ {static => code/static}/favicon.ico | Bin {static => code/static}/script.js | 0 {static => code/static}/style.css | 0 {static => code/static}/table.js | 0 {templates => code/templates}/layout.html | 0 {templates => code/templates}/list_shops.html | 0 {templates => code/templates}/login.html | 0 {templates => code/templates}/profile.html | 0 {templates => code/templates}/register.html | 0 {templates => code/templates}/show_shop.html | 0 debug.py | 4 - docker-compose.example | 15 + run.example | 5 - shop.py | 540 --------------- 20 files changed, 640 insertions(+), 550 deletions(-) create mode 100644 code/Dockerfile rename requirements.txt => code/requirements.txt (100%) rename revprox.py => code/revprox.py (100%) rename schema.sql => code/schema.sql (100%) create mode 100644 code/shop.py rename {static => code/static}/favicon.ico (100%) rename {static => code/static}/script.js (100%) rename {static => code/static}/style.css (100%) rename {static => code/static}/table.js (100%) rename {templates => code/templates}/layout.html (100%) rename {templates => code/templates}/list_shops.html (100%) rename {templates => code/templates}/login.html (100%) rename {templates => code/templates}/profile.html (100%) rename {templates => code/templates}/register.html (100%) rename {templates => code/templates}/show_shop.html (100%) delete mode 100755 debug.py create mode 100644 docker-compose.example delete mode 100755 run.example delete mode 100644 shop.py diff --git a/README.md b/README.md index 3be3bf9..6a4dfb3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ A simple shopping list app: -* Runs on gunicorn+flask (python2.7) +* Runs on gunicorn+flask (python3) +* Dockerizes * Multi-user * Data saved as flat files, can contain markdown * Syntaxes parsed: diff --git a/code/Dockerfile b/code/Dockerfile new file mode 100644 index 0000000..f47f8bf --- /dev/null +++ b/code/Dockerfile @@ -0,0 +1,10 @@ +#FROM alpine:3.10 +FROM python:3 +COPY requirements.txt /requirements.txt +RUN python3 -m venv /venv && \ + . /venv/bin/activate && \ + pip3 install --upgrade pip && \ + pip3 install -r /requirements.txt +COPY . /code/ +WORKDIR /code +CMD . /venv/bin/activate && ./start.me diff --git a/requirements.txt b/code/requirements.txt similarity index 100% rename from requirements.txt rename to code/requirements.txt diff --git a/revprox.py b/code/revprox.py similarity index 100% rename from revprox.py rename to code/revprox.py diff --git a/schema.sql b/code/schema.sql similarity index 100% rename from schema.sql rename to code/schema.sql diff --git a/code/shop.py b/code/shop.py new file mode 100644 index 0000000..58b000b --- /dev/null +++ b/code/shop.py @@ -0,0 +1,613 @@ +# -*- coding: utf-8 -*- +# all the imports +import sqlite3, time, datetime, hashlib, os, re +from shutil import copyfile, move +from flask import ( + Flask, + request, + session, + g, + redirect, + url_for, + abort, + render_template, + flash, +) +from revprox import ReverseProxied + +# configuration +DATABASE = "/data/shop.db" +DATADIR = "/data" +DEBUG = False +SECRET_KEY = os.getenv("SECRET_KEY", "development key") +USERNAME = os.getenv("ADMIN_USER", "admin") +PASSWORD = os.getenv("ADMIN_PASSWD", "default") +URLFINDER = re.compile("((news|telnet|nttp|file|http|ftp|https)://[^ ]+)") +URLPARSER = re.compile(r"(\[)([^\]]+)(\])\(([^\)]+)\)") +BOLDFINDER = re.compile(r"\*([^\*]+)\*") +CODEFINDER = re.compile(r"\`([^\`]+)\`") + +# create our little application :) +app = Flask(__name__) +app.config.from_object(__name__) +app.config["SESSION_COOKIE_NAME"] = os.getenv("SESSION_COOKIE_NAME", "mdshop") +app.config["register"] = os.getenv("ENABLE_REGISTER", "true") != "false" +print(app.config) +app.wsgi_app = ReverseProxied(app.wsgi_app) + + +def connect_db(): + if not os.path.exists(app.config["DATABASE"]): + db = sqlite3.connect(app.config["DATABASE"]) + for command in open("schema.sql", "rt").read().split(";"): + db.execute(command) + db.commit() + return sqlite3.connect(app.config["DATABASE"]) + + +def password_hash(s): + return hashlib.sha224(s.encode("utf8")).hexdigest() + + +def get_username(id): + cur = g.db.execute("select * from users") + for row in cur.fetchall(): + if id == row[0]: + return row[1] + return None + + +def get_userid(name): + cur = g.db.execute("select * from users") + for row in cur.fetchall(): + if name == row[1]: + return row[0] + return None + + +def get_shares(id): + cur = g.db.execute("select * from shares") + shares = [] + for row in cur.fetchall(): + if id == row[1]: + shares.append(row[0]) + return shares + + +def get_shop_date(id): + date = "" + cur = g.db.execute("select * from shops") + for row in cur.fetchall(): + if id == row[0]: + data_dir = os.path.join(DATADIR, get_username(row[2])) + data_file = os.path.join(data_dir, row[1] + ".md") + if os.path.exists(data_file): + date = datetime.datetime.fromtimestamp( + os.path.getmtime(data_file) + ).strftime("%m/%d %H:%M") + return date + + +def get_shop_backup_date(id): + date = "" + cur = g.db.execute("select * from shops") + for row in cur.fetchall(): + if id == row[0]: + data_dir = os.path.join(DATADIR, get_username(row[2])) + data_file = os.path.join(data_dir, row[1] + ".md.bkp") + if os.path.exists(data_file): + date = datetime.datetime.fromtimestamp( + os.path.getmtime(data_file) + ).strftime("%m/%d %H:%M") + return date + + +def scan_for_new_documents(id): + user = get_username(id) + data_dir = os.path.join(DATADIR, user) + if not os.path.exists(data_dir): + return + cur = g.db.execute("select * from shops") + existing = [] + non_existing = [] + for row in cur.fetchall(): + if row[2] != id: + continue + existing.append(row[1]) + for row in os.listdir(data_dir): + if row.endswith(".md"): + if row[:-3] not in existing: + non_existing.append(row[:-3]) + for shop in non_existing: + g.db.execute("insert into shops (shop,owner) values (?, ?)", [shop, id]) + g.db.commit() + + +def markdown_parse(s): + s = s.decode("utf8") + s = BOLDFINDER.sub(r'*\1*', s) + s = CODEFINDER.sub(r'`\1`', s) + return s + + +def urlify(s): + if URLPARSER.search(s): + return URLPARSER.sub(r'[\2]', s) + return URLFINDER.sub(r'\1', s) + + +@app.before_request +def before_request(): + g.db = connect_db() + + +@app.teardown_request +def teardown_request(exception): + db = getattr(g, "db", None) + if db is not None: + db.close() + + +@app.route("/shop/") +def show_shop(shopid): + if not session.get("logged_in"): + return redirect(url_for("login", error=None)) + try: + shopid = int(shopid) + except ValueError: + return redirect(url_for("login", error=None)) + has_access = False + cur = g.db.execute("select * from shops") + shared = get_shares(session.get("user")) + for row in cur.fetchall(): + if row[0] == shopid: + if row[2] == session.get("user") or row[0] in shared: + has_access = True + shopname = row[1] + break + if not has_access: + return redirect(url_for("list_shops")) + data_dir = os.path.join(DATADIR, get_username(row[2])) + data_file = os.path.join(data_dir, row[1] + ".md") + if not os.path.exists(data_file): + open(data_file, "wt").close() + entries = [] + content = open(data_file, "rt").read() # .decode('utf-8') + for i, row in enumerate(open(data_file, "rt").read().split("\n")): + # any parsing magick would be here + row = row.rstrip() + if row == "": + continue + icon = " " + extra_class = "noitem" + if "[ ]" in row: + icon = u" " + extra_class = "" + if "[x]" in row: + icon = u"\u2714" + extra_class = "" + row = urlify(row).encode("ascii", "xmlcharrefreplace") + row = markdown_parse(row) + if row.startswith("#"): + row = "" + row + "" + if row.startswith(">"): + row = "" + row + "" + entries.append(dict(row=i, text=row, icon=icon, extra_class=extra_class)) + shared_to = [] + cur = g.db.execute("select * from shares") + for row in cur.fetchall(): + if row[0] == shopid: + shared_to.append(get_username(row[1])) + # invalidate autosort in 60 minutes: + if session.get("sort_update"): + if time.time() - session.get("sort_update") > 3600: + session["sort_view"] = False + session["sort_update"] = time.time() + + return render_template( + "show_shop.html", + entries=entries, + shop=shopname, + shopid=shopid, + content=content, + shares=shared_to, + date=get_shop_date(shopid), + date_bkp=get_shop_backup_date(shopid), + autosort=session.get("sort_view", False), + ) + + +@app.route("/") +def list_shops(): + if not session.get("logged_in"): + return redirect(url_for("login", error=None)) + scan_for_new_documents(session.get("user")) + cur = g.db.execute("select * from shops order by shop") + entries = [] + for row in cur.fetchall(): + if session.get("user") == row[2]: # owner + date = get_shop_date(row[0]) + entries.append( + dict(shop=row[1], shopid=row[0], owner=get_username(row[2]), date=date) + ) + cur = g.db.execute("select * from shops order by shop") + shares = get_shares(session.get("user")) + for row in cur.fetchall(): + if row[0] in shares: # Has been shared to + date = get_shop_date(row[0]) + entries.append( + dict(shop=row[1], shopid=row[0], owner=get_username(row[2]), date=date) + ) + + return render_template("list_shops.html", entries=entries) + + +@app.route("/add", methods=["POST"]) +def add_items(): + if not session.get("logged_in"): + abort(401) + shopid = int(request.form["shopid"]) + ownerid = g.db.execute( + "select owner from shops where id=?", (request.form["shopid"],) + ).fetchall()[0][0] + shopname = g.db.execute( + "select shop from shops where id=?", (request.form["shopid"],) + ).fetchall()[0][0] + ownername = get_username(ownerid) + data_dir = os.path.join(DATADIR, ownername) + data_file = os.path.join(data_dir, shopname + ".md") + count = 0 + contents_file = open(data_file, "at") + for row in request.form["add_md"].split("\n"): + if row.strip() == "": + continue + count += 1 + contents_file.write("[ ] %s\n" % row.strip()) + contents_file.close() + flash("Added %d items" % (count)) + return redirect(url_for("show_shop", shopid=shopid)) + + +@app.route("/edit", methods=["POST"]) +def edit_md(): + if not session.get("logged_in"): + abort(401) + shopid = int(request.form["shopid"]) + ownerid = g.db.execute( + "select owner from shops where id=?", (request.form["shopid"],) + ).fetchall()[0][0] + shopname = g.db.execute( + "select shop from shops where id=?", (request.form["shopid"],) + ).fetchall()[0][0] + ownername = get_username(ownerid) + data_dir = os.path.join(DATADIR, ownername) + data_file = os.path.join(data_dir, shopname + ".md") + backup = data_file + ".bkp" + copyfile(data_file, backup) + contents_file = open(data_file, "wt") + contents_list = request.form["edit_md"].split("\n") + while contents_list[-1].strip() == "": + contents_list.pop() + for row in contents_list: + contents_file.write("%s\n" % (row,)) + contents_file.close() + flash("Saved new file.") + return redirect(url_for("show_shop", shopid=shopid)) + + +@app.route("/restore", methods=["POST"]) +def restore_md(): + if not session.get("logged_in"): + abort(401) + shopid = int(request.form["shopid"]) + ownerid = g.db.execute( + "select owner from shops where id=?", (request.form["shopid"],) + ).fetchall()[0][0] + shopname = g.db.execute( + "select shop from shops where id=?", (request.form["shopid"],) + ).fetchall()[0][0] + ownername = get_username(ownerid) + data_dir = os.path.join(DATADIR, ownername) + data_file = os.path.join(data_dir, shopname + ".md") + backup = data_file + ".bkp" + backup_tmp = data_file + ".tmp" + if not os.path.exists(backup): + flash("Backup does not exist") + return redirect(url_for("show_shop", shopid=shopid)) + copyfile(data_file, backup_tmp) + copyfile(backup, data_file) + move(backup_tmp, backup) + flash("Backup restored") + return redirect(url_for("show_shop", shopid=shopid)) + + +@app.route("/toggle", methods=["POST"]) +def toggle_item(): + if not session.get("logged_in"): + abort(401) + shopid = int(request.form["shopid"]) + ownerid = g.db.execute( + "select owner from shops where id=?", (request.form["shopid"],) + ).fetchall()[0][0] + shopname = g.db.execute( + "select shop from shops where id=?", (request.form["shopid"],) + ).fetchall()[0][0] + ownername = get_username(ownerid) + req_row = None + for key in request.form: + if key.startswith("item"): + req_row = int(key[4:]) + if key == "toggleAll": + # Special meaning: toggle all rows + req_row = -1 + if key == "unTickAll": + # Special meaning: untick all rows + req_row = -2 + if req_row == None: + return redirect(url_for("show_shop", shopid=shopid)) + data_dir = os.path.join(DATADIR, ownername) + data_file = os.path.join(data_dir, shopname + ".md") + backup = data_file + ".bkp" + contents_file = open(data_file, "rt") + contents = contents_file.read().split("\n") + contents_file.close() + changed = False + for i, row in enumerate(contents): + if i == req_row or req_row < 0: + if req_row != -2: # no ticking if unticking all + if "[ ]" in row: + contents[i] = row.replace("[ ]", "[x]") + if "[x]" in row: + contents[i] = row.replace("[x]", "[ ]") + if row != contents[i]: + changed = True + if changed: + if req_row == -1 or req_row == -2: + copyfile(data_file, backup) + contents_file = open(data_file, "wt") + contents_file.write("\n".join(contents)) + contents_file.close() + return redirect(url_for("show_shop", shopid=shopid)) + + +@app.route("/remove_toggled", methods=["POST"]) +def remove_toggled(): + if not session.get("logged_in"): + abort(401) + shopid = int(request.form["shopid"]) + ownerid = g.db.execute( + "select owner from shops where id=?", (request.form["shopid"],) + ).fetchall()[0][0] + shopname = g.db.execute( + "select shop from shops where id=?", (request.form["shopid"],) + ).fetchall()[0][0] + ownername = get_username(ownerid) + data_dir = os.path.join(DATADIR, ownername) + data_file = os.path.join(data_dir, shopname + ".md") + backup = data_file + ".bkp" + contents_file = open(data_file, "rt") + contents = [] + changed = False + for i, row in enumerate(contents_file.read().split("\n")): + if "[x]" not in row: + contents.append(row) + else: + changed = True + contents_file.close() + if changed: + copyfile(data_file, backup) + contents_file = open(data_file, "wt") + contents_file.write("\n".join(contents)) + contents_file.close() + # ~ flash('successfully posted %s (%d)'%(row,req_row)) + return redirect(url_for("show_shop", shopid=shopid)) + + +@app.route("/add_shop", methods=["POST"]) +def add_shop(): + if not session.get("logged_in"): + abort(401) + import re, string + + pattern = re.compile("[\W]+") + shopname = pattern.sub("", request.form["shop"]) + if shopname == "": + flash("Shop name empty!") + return redirect(url_for("list_shops")) + cur = g.db.execute("select * from shops order by shop") + for row in cur.fetchall(): + if shopname == row[1]: + flash("Shop already exists! " + shopname) + return redirect(url_for("list_shops")) + g.db.execute( + "insert into shops (shop,owner) values (?, ?)", [shopname, session["user"]] + ) + g.db.commit() + new_dir = os.path.join(DATADIR, get_username(session["user"])) + new_file = os.path.join(new_dir, shopname + ".md") + if not os.path.exists(new_dir): + os.mkdir(new_dir) + open(new_file, "at") + flash("successfully created new shop: " + shopname) + return redirect(url_for("list_shops")) + + +@app.route("/add_share", methods=["POST"]) +def add_share(): + if not session.get("logged_in"): + abort(401) + import re, string + + pattern = re.compile("[\W]+") + username = pattern.sub("", request.form["share"]) + shopid = pattern.sub("", request.form["shopid"]) + if username == "": + flash("User name empty!") + return redirect(url_for("show_shop", shopid=shopid)) + userid = get_userid(username) + if userid == None: + flash("No such user!") + return redirect(url_for("show_shop", shopid=shopid)) + ownerid = g.db.execute( + "select owner from shops where id=?", (request.form["shopid"],) + ).fetchall()[0][0] + if session.get("user") != ownerid: + flash("Not your shop!") + return redirect(url_for("show_shop", shopid=shopid)) + g.db.execute("insert into shares (shopid,userid) values (?, ?)", [shopid, userid]) + g.db.commit() + flash("Shared to %s" % (username)) + return redirect(url_for("show_shop", shopid=shopid)) + + +@app.route("/remove_share", methods=["POST"]) +def remove_share(): + if not session.get("logged_in"): + abort(401) + import re, string + + pattern = re.compile("[\W]+") + username = pattern.sub("", request.form["user"]) + shopid = pattern.sub("", request.form["shopid"]) + if username == "": + flash("User name empty!") + return redirect(url_for("show_shop", shopid=shopid)) + userid = get_userid(username) + if userid == None: + flash("No such user!") + return redirect(url_for("show_shop", shopid=shopid)) + ownerid = g.db.execute( + "select owner from shops where id=?", (request.form["shopid"],) + ).fetchall()[0][0] + if session.get("user") != ownerid: + flash("Not your shop!") + return redirect(url_for("show_shop", shopid=shopid)) + + g.db.execute("delete from shares where shopid=? and userid=?", [shopid, userid]) + g.db.commit() + return redirect(url_for("show_shop", shopid=shopid)) + + +@app.route("/remove_shop", methods=["POST"]) +def remove_shop(): + if not session.get("logged_in"): + abort(401) + shopid = int(request.form["shopid"]) + ownerid = g.db.execute( + "select owner from shops where id=?", (request.form["shopid"],) + ).fetchall()[0][0] + shopname = g.db.execute( + "select shop from shops where id=?", (request.form["shopid"],) + ).fetchall()[0][0] + ownername = get_username(ownerid) + data_dir = os.path.join(DATADIR, ownername) + data_file = os.path.join(data_dir, shopname + ".md") + if session.get("user") != ownerid: + flash("Not your shop!") + return redirect(url_for("show_shop", shopid=shopid)) + # remove shop DB + g.db.execute("delete from shops where id=?", [shopid]) + g.db.commit() + # backup data, and remove + backup = data_file + ".bkp" + copyfile(data_file, backup) + os.remove(data_file) + # remove shares + g.db.execute("delete from shares where shopid=?", [shopid]) + g.db.commit() + flash("successfully deleted shop %s" % (shopname)) + return redirect(url_for("list_shops")) + + +@app.route("/sort_flip", methods=["POST"]) +def sort_flip(): + if not session.get("sort_view"): + session["sort_view"] = True + session["sort_update"] = time.time() + else: + session["sort_view"] = False + session["sort_update"] = time.time() + shopid = int(request.form["shopid"]) + return redirect(url_for("show_shop", shopid=shopid)) + + +@app.route("/login", methods=["GET", "POST"]) +def login(): + error = None + if request.method == "POST": + cur = g.db.execute("select * from users") + for row in cur.fetchall(): + if request.form["username"] == row[1]: + if password_hash(request.form["password"]) == row[2]: + session["logged_in"] = True + session["user"] = row[0] + # scan_for_new_documents(row[0]) + return redirect(url_for("list_shops")) + error = "Invalid user/pass" + return render_template("login.html", error=error) + + +@app.route("/register", methods=["GET", "POST"]) +def register(): + error = None + if not app.config["register"]: + return "" + if request.method == "POST": + import re, string + + pattern = re.compile("[\W]+") + username = pattern.sub("", request.form["username"]) + password = password_hash(request.form["password"]) + if len(username) == 0: + error = "No username given" + return render_template("register.html", error=error) + if len(request.form["password"]) < 5: + error = "Password too short" + return render_template("register.html", error=error) + cur = g.db.execute("select * from users") + for row in cur.fetchall(): + if username == row[1]: + error = "Username already exists" + return render_template("register.html", error=error) + g.db.execute( + "insert into users (user,pass) values (?, ?)", [username, password] + ) + g.db.commit() + flash('successfully registered user "%s". Now login.' % username) + return redirect(url_for("login")) + return render_template("register.html", error=error) + + +@app.route("/profile", methods=["GET", "POST"]) +def profile(): + if not session.get("logged_in"): + return redirect(url_for("login")) + error = None + user = get_username(session.get("user")) + if request.method == "POST": + import re, string + + pattern = re.compile("[\W]+") + password = password_hash(request.form["password"]) + if len(request.form["password"]) < 5: + error = "Password too short" + return render_template("profile.html", error=error, user=user) + g.db.execute( + "update users set pass=? where id=?", [password, session.get("user")] + ) + g.db.commit() + flash("successfully updated profile.") + return redirect(url_for("profile")) + return render_template("profile.html", error=error, user=user) + + +@app.route("/logout") +def logout(): + session.pop("logged_in", None) + session.pop("user", None) + flash("You were logged out") + return render_template("login.html", error=None) + + +if __name__ == "__main__": + app.run() diff --git a/static/favicon.ico b/code/static/favicon.ico similarity index 100% rename from static/favicon.ico rename to code/static/favicon.ico diff --git a/static/script.js b/code/static/script.js similarity index 100% rename from static/script.js rename to code/static/script.js diff --git a/static/style.css b/code/static/style.css similarity index 100% rename from static/style.css rename to code/static/style.css diff --git a/static/table.js b/code/static/table.js similarity index 100% rename from static/table.js rename to code/static/table.js diff --git a/templates/layout.html b/code/templates/layout.html similarity index 100% rename from templates/layout.html rename to code/templates/layout.html diff --git a/templates/list_shops.html b/code/templates/list_shops.html similarity index 100% rename from templates/list_shops.html rename to code/templates/list_shops.html diff --git a/templates/login.html b/code/templates/login.html similarity index 100% rename from templates/login.html rename to code/templates/login.html diff --git a/templates/profile.html b/code/templates/profile.html similarity index 100% rename from templates/profile.html rename to code/templates/profile.html diff --git a/templates/register.html b/code/templates/register.html similarity index 100% rename from templates/register.html rename to code/templates/register.html diff --git a/templates/show_shop.html b/code/templates/show_shop.html similarity index 100% rename from templates/show_shop.html rename to code/templates/show_shop.html diff --git a/debug.py b/debug.py deleted file mode 100755 index 153eb17..0000000 --- a/debug.py +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/python -from shop import app -print('http://127.0.0.1:5000/') -app.run(debug=True,host="0.0.0.0") diff --git a/docker-compose.example b/docker-compose.example new file mode 100644 index 0000000..ab0236f --- /dev/null +++ b/docker-compose.example @@ -0,0 +1,15 @@ +version: "3.5" + +services: + app: + build: + context: code + ports: + - "127.0.0.1:8166:8166" + volumes: + - ./data/:/data/ + environment: + - SECRET_KEY=gifodjgoifdjgoejr903 + user: "1000:1000" + restart: unless-stopped + diff --git a/run.example b/run.example deleted file mode 100755 index 67cb308..0000000 --- a/run.example +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -export SESSION_COOKIE_NAME=mdshop -#export ENABLE_REGISTER=false -exec gunicorn -b 127.0.0.1:8000 -w 2 shop:app diff --git a/shop.py b/shop.py deleted file mode 100644 index 4ac7c98..0000000 --- a/shop.py +++ /dev/null @@ -1,540 +0,0 @@ -# -*- coding: utf-8 -*- -# all the imports -import sqlite3, time, datetime, hashlib, os, re -from shutil import copyfile, move -from flask import Flask, request, session, g, redirect, url_for, \ - abort, render_template, flash -from revprox import ReverseProxied - -# configuration -DATABASE = 'shop.db' -DATADIR = 'data' -DEBUG = False -SECRET_KEY = 'development key' -USERNAME = 'admin' -PASSWORD = 'default' -URLFINDER = re.compile("((news|telnet|nttp|file|http|ftp|https)://[^ ]+)") -URLPARSER = re.compile(r'(\[)([^\]]+)(\])\(([^\)]+)\)') -BOLDFINDER = re.compile(r'\*([^\*]+)\*') -CODEFINDER = re.compile(r'\`([^\`]+)\`') - -# create our little application :) -app = Flask(__name__) -app.config.from_object(__name__) -app.config['SESSION_COOKIE_NAME'] = os.getenv('SESSION_COOKIE_NAME', 'mdshop') -app.config['register'] = os.getenv('ENABLE_REGISTER', 'true') != 'false' -print(app.config) -app.wsgi_app = ReverseProxied(app.wsgi_app) - -def connect_db(): - if not os.path.exists(app.config['DATABASE']): - db=sqlite3.connect(app.config['DATABASE']) - for command in open('schema.sql','rt').read().split(';'): - db.execute(command) - db.commit() - return sqlite3.connect(app.config['DATABASE']) - -def password_hash(s): - return hashlib.sha224(s.encode('utf8')).hexdigest() - -def get_username(id): - cur = g.db.execute('select * from users') - for row in cur.fetchall(): - if id==row[0]: - return row[1] - return None - -def get_userid(name): - cur = g.db.execute('select * from users') - for row in cur.fetchall(): - if name==row[1]: - return row[0] - return None - -def get_shares(id): - cur = g.db.execute('select * from shares') - shares=[] - for row in cur.fetchall(): - if id==row[1]: - shares.append(row[0]) - return shares - -def get_shop_date(id): - date="" - cur = g.db.execute('select * from shops') - for row in cur.fetchall(): - if id==row[0]: - data_dir=os.path.join(DATADIR, get_username(row[2])) - data_file=os.path.join(data_dir, row[1]+".md") - if os.path.exists(data_file): - date=datetime.datetime.fromtimestamp( - os.path.getmtime(data_file)).strftime('%m/%d %H:%M') - return date - -def get_shop_backup_date(id): - date="" - cur = g.db.execute('select * from shops') - for row in cur.fetchall(): - if id==row[0]: - data_dir=os.path.join(DATADIR, get_username(row[2])) - data_file=os.path.join(data_dir, row[1]+".md.bkp") - if os.path.exists(data_file): - date=datetime.datetime.fromtimestamp( - os.path.getmtime(data_file)).strftime('%m/%d %H:%M') - return date - -def scan_for_new_documents(id): - user=get_username(id) - data_dir=os.path.join(DATADIR, user) - if not os.path.exists(data_dir): - return - cur = g.db.execute('select * from shops') - existing=[] - non_existing=[] - for row in cur.fetchall(): - if row[2]!=id: - continue - existing.append(row[1]) - for row in os.listdir(data_dir): - if row.endswith(".md"): - if row[:-3] not in existing: - non_existing.append(row[:-3]) - for shop in non_existing: - g.db.execute('insert into shops (shop,owner) values (?, ?)', - [shop, id]) - g.db.commit() - -def markdown_parse(s): - s = s.decode('utf8') - s=BOLDFINDER.sub(r'*\1*',s) - s=CODEFINDER.sub(r'`\1`',s) - return s - -def urlify(s): - if URLPARSER.search(s): - return URLPARSER.sub(r'[\2]',s) - return URLFINDER.sub(r'\1', s) - -@app.before_request -def before_request(): - g.db = connect_db() -@app.teardown_request -def teardown_request(exception): - db = getattr(g, 'db', None) - if db is not None: - db.close() - -@app.route('/shop/') -def show_shop(shopid): - if not session.get('logged_in'): - return redirect(url_for('login',error=None)) - try: - shopid=int(shopid) - except ValueError: - return redirect(url_for('login',error=None)) - has_access=False - cur = g.db.execute('select * from shops') - shared=get_shares(session.get('user')) - for row in cur.fetchall(): - if row[0]==shopid: - if row[2]==session.get('user') or row[0] in shared: - has_access=True - shopname=row[1] - break - if not has_access: - return redirect(url_for('list_shops')) - data_dir=os.path.join(DATADIR, get_username(row[2])) - data_file=os.path.join(data_dir, row[1]+".md") - if not os.path.exists(data_file): - open(data_file, 'wt').close() - entries=[] - content=open(data_file, 'rt').read()#.decode('utf-8') - for i,row in enumerate(open( data_file, 'rt').read().split("\n")): - # any parsing magick would be here - row=row.rstrip() - if row=="": - continue - icon=" " - extra_class="noitem" - if "[ ]" in row: - icon=u" " - extra_class="" - if "[x]" in row: - icon=u"\u2714" - extra_class="" - row=urlify(row).encode('ascii', 'xmlcharrefreplace') - row=markdown_parse(row) - if row.startswith("#"): - row=""+row+"" - if row.startswith(">"): - row=""+row+"" - entries.append( dict(row=i, text=row, icon=icon, extra_class=extra_class) ) - shared_to=[] - cur = g.db.execute('select * from shares') - for row in cur.fetchall(): - if row[0]==shopid: - shared_to.append(get_username(row[1])) - # invalidate autosort in 60 minutes: - if session.get('sort_update'): - if time.time() - session.get('sort_update') > 3600: - session['sort_view'] = False - session['sort_update'] = time.time() - - return render_template( - 'show_shop.html', - entries = entries, - shop = shopname, - shopid = shopid, - content = content, - shares = shared_to, - date = get_shop_date(shopid), - date_bkp = get_shop_backup_date(shopid), - autosort = session.get('sort_view', False) - ) - -@app.route('/') -def list_shops(): - if not session.get('logged_in'): - return redirect(url_for('login',error=None)) - scan_for_new_documents(session.get('user')) - cur = g.db.execute('select * from shops order by shop') - entries=[] - for row in cur.fetchall(): - if session.get('user')==row[2]: # owner - date=get_shop_date(row[0]) - entries.append( dict(shop=row[1], shopid=row[0], owner=get_username(row[2]),date=date) ) - cur = g.db.execute('select * from shops order by shop') - shares=get_shares(session.get('user')) - for row in cur.fetchall(): - if row[0] in shares: # Has been shared to - date=get_shop_date(row[0]) - entries.append( dict(shop=row[1], shopid=row[0], owner=get_username(row[2]),date=date) ) - - return render_template('list_shops.html', entries=entries) - -@app.route('/add', methods=['POST']) -def add_items(): - if not session.get('logged_in'): - abort(401) - shopid=int(request.form['shopid']) - ownerid=g.db.execute('select owner from shops where id=?',(request.form['shopid'],)).fetchall()[0][0] - shopname=g.db.execute('select shop from shops where id=?',(request.form['shopid'],)).fetchall()[0][0] - ownername=get_username(ownerid) - data_dir=os.path.join(DATADIR, ownername) - data_file=os.path.join(data_dir, shopname+".md") - count=0 - contents_file=open(data_file,'at') - for row in request.form['add_md'].split("\n"): - if row.strip()=="": - continue - count+=1 - contents_file.write("[ ] %s\n"%row.strip()) - contents_file.close() - flash('Added %d items'%(count)) - return redirect(url_for('show_shop',shopid=shopid)) - -@app.route('/edit', methods=['POST']) -def edit_md(): - if not session.get('logged_in'): - abort(401) - shopid=int(request.form['shopid']) - ownerid=g.db.execute('select owner from shops where id=?',(request.form['shopid'],)).fetchall()[0][0] - shopname=g.db.execute('select shop from shops where id=?',(request.form['shopid'],)).fetchall()[0][0] - ownername=get_username(ownerid) - data_dir=os.path.join(DATADIR, ownername) - data_file=os.path.join(data_dir, shopname+".md") - backup=data_file+".bkp" - copyfile(data_file, backup) - contents_file=open(data_file,'wt') - contents_list=request.form['edit_md'].split("\n") - while contents_list[-1].strip()=='': - contents_list.pop() - for row in contents_list: - contents_file.write("%s\n"%(row,)) - contents_file.close() - flash('Saved new file.') - return redirect(url_for('show_shop',shopid=shopid)) - -@app.route('/restore', methods=['POST']) -def restore_md(): - if not session.get('logged_in'): - abort(401) - shopid=int(request.form['shopid']) - ownerid=g.db.execute('select owner from shops where id=?',(request.form['shopid'],)).fetchall()[0][0] - shopname=g.db.execute('select shop from shops where id=?',(request.form['shopid'],)).fetchall()[0][0] - ownername=get_username(ownerid) - data_dir=os.path.join(DATADIR, ownername) - data_file=os.path.join(data_dir, shopname+".md") - backup=data_file+".bkp" - backup_tmp=data_file+".tmp" - if not os.path.exists(backup): - flash('Backup does not exist') - return redirect(url_for('show_shop',shopid=shopid)) - copyfile(data_file,backup_tmp) - copyfile(backup, data_file) - move(backup_tmp, backup) - flash('Backup restored') - return redirect(url_for('show_shop',shopid=shopid)) - -@app.route('/toggle', methods=['POST']) -def toggle_item(): - if not session.get('logged_in'): - abort(401) - shopid=int(request.form['shopid']) - ownerid=g.db.execute('select owner from shops where id=?',(request.form['shopid'],)).fetchall()[0][0] - shopname=g.db.execute('select shop from shops where id=?',(request.form['shopid'],)).fetchall()[0][0] - ownername=get_username(ownerid) - req_row=None - for key in request.form: - if key.startswith('item'): - req_row=int(key[4:]) - if key=='toggleAll': - # Special meaning: toggle all rows - req_row=-1 - if key=='unTickAll': - # Special meaning: untick all rows - req_row=-2 - if req_row==None: - return redirect(url_for('show_shop',shopid=shopid)) - data_dir=os.path.join(DATADIR, ownername) - data_file=os.path.join(data_dir, shopname+".md") - backup=data_file+".bkp" - contents_file=open(data_file,'rt') - contents=contents_file.read().split("\n") - contents_file.close() - changed=False - for i,row in enumerate(contents): - if i==req_row or req_row<0: - if req_row!=-2: # no ticking if unticking all - if '[ ]' in row: - contents[i]=row.replace('[ ]','[x]') - if '[x]' in row: - contents[i]=row.replace('[x]','[ ]') - if row!=contents[i]: - changed=True - if changed: - if req_row==-1 or req_row==-2: - copyfile(data_file, backup) - contents_file=open(data_file,'wt') - contents_file.write("\n".join(contents)) - contents_file.close() - return redirect(url_for('show_shop',shopid=shopid)) - -@app.route('/remove_toggled', methods=['POST']) -def remove_toggled(): - if not session.get('logged_in'): - abort(401) - shopid=int(request.form['shopid']) - ownerid=g.db.execute('select owner from shops where id=?',(request.form['shopid'],)).fetchall()[0][0] - shopname=g.db.execute('select shop from shops where id=?',(request.form['shopid'],)).fetchall()[0][0] - ownername=get_username(ownerid) - data_dir=os.path.join(DATADIR, ownername) - data_file=os.path.join(data_dir, shopname+".md") - backup=data_file+".bkp" - contents_file=open(data_file,'rt') - contents=[] - changed=False - for i,row in enumerate(contents_file.read().split("\n")): - if '[x]' not in row: - contents.append(row) - else: - changed=True - contents_file.close() - if changed: - copyfile(data_file, backup) - contents_file=open(data_file,'wt') - contents_file.write("\n".join(contents)) - contents_file.close() - #~ flash('successfully posted %s (%d)'%(row,req_row)) - return redirect(url_for('show_shop',shopid=shopid)) - -@app.route('/add_shop', methods=['POST']) -def add_shop(): - if not session.get('logged_in'): - abort(401) - import re, string - pattern = re.compile('[\W]+') - shopname=pattern.sub('', request.form['shop']) - if shopname=="": - flash('Shop name empty!') - return redirect(url_for('list_shops')) - cur = g.db.execute('select * from shops order by shop') - for row in cur.fetchall(): - if shopname==row[1]: - flash('Shop already exists! '+shopname) - return redirect(url_for('list_shops')) - g.db.execute('insert into shops (shop,owner) values (?, ?)', - [shopname, session['user']]) - g.db.commit() - new_dir=os.path.join(DATADIR, get_username(session['user'])) - new_file=os.path.join(new_dir, shopname+".md") - if not os.path.exists(new_dir): - os.mkdir(new_dir) - open( new_file, 'at') - flash('successfully created new shop: '+shopname) - return redirect(url_for('list_shops')) - -@app.route('/add_share', methods=['POST']) -def add_share(): - if not session.get('logged_in'): - abort(401) - import re, string - pattern = re.compile('[\W]+') - username=pattern.sub('', request.form['share']) - shopid=pattern.sub('', request.form['shopid']) - if username=="": - flash('User name empty!') - return redirect(url_for('show_shop',shopid=shopid)) - userid=get_userid(username) - if userid==None: - flash('No such user!') - return redirect(url_for('show_shop',shopid=shopid)) - ownerid=g.db.execute('select owner from shops where id=?',(request.form['shopid'],)).fetchall()[0][0] - if session.get('user')!=ownerid: - flash('Not your shop!') - return redirect(url_for('show_shop',shopid=shopid)) - g.db.execute('insert into shares (shopid,userid) values (?, ?)', - [shopid, userid]) - g.db.commit() - flash('Shared to %s'%(username)) - return redirect(url_for('show_shop',shopid=shopid)) - -@app.route('/remove_share', methods=['POST']) -def remove_share(): - if not session.get('logged_in'): - abort(401) - import re, string - pattern = re.compile('[\W]+') - username=pattern.sub('', request.form['user']) - shopid=pattern.sub('', request.form['shopid']) - if username=="": - flash('User name empty!') - return redirect(url_for('show_shop',shopid=shopid)) - userid=get_userid(username) - if userid==None: - flash('No such user!') - return redirect(url_for('show_shop',shopid=shopid)) - ownerid=g.db.execute('select owner from shops where id=?',(request.form['shopid'],)).fetchall()[0][0] - if session.get('user')!=ownerid: - flash('Not your shop!') - return redirect(url_for('show_shop',shopid=shopid)) - - g.db.execute('delete from shares where shopid=? and userid=?', - [shopid, userid]) - g.db.commit() - return redirect(url_for('show_shop',shopid=shopid)) - -@app.route('/remove_shop', methods=['POST']) -def remove_shop(): - if not session.get('logged_in'): - abort(401) - shopid=int(request.form['shopid']) - ownerid=g.db.execute('select owner from shops where id=?',(request.form['shopid'],)).fetchall()[0][0] - shopname=g.db.execute('select shop from shops where id=?',(request.form['shopid'],)).fetchall()[0][0] - ownername=get_username(ownerid) - data_dir=os.path.join(DATADIR, ownername) - data_file=os.path.join(data_dir, shopname+".md") - if session.get('user')!=ownerid: - flash('Not your shop!') - return redirect(url_for('show_shop',shopid=shopid)) - # remove shop DB - g.db.execute('delete from shops where id=?', - [shopid]) - g.db.commit() - # backup data, and remove - backup=data_file+".bkp" - copyfile(data_file, backup) - os.remove(data_file) - # remove shares - g.db.execute('delete from shares where shopid=?', - [shopid]) - g.db.commit() - flash('successfully deleted shop %s'%(shopname)) - return redirect(url_for('list_shops')) - -@app.route('/sort_flip',methods=['POST']) -def sort_flip(): - if not session.get('sort_view'): - session['sort_view'] = True - session['sort_update'] = time.time() - else: - session['sort_view'] = False - session['sort_update'] = time.time() - shopid = int(request.form['shopid']) - return redirect( - url_for('show_shop', shopid = shopid) - ) - -@app.route('/login', methods=['GET', 'POST']) -def login(): - error = None - if request.method == 'POST': - cur = g.db.execute('select * from users') - for row in cur.fetchall(): - if request.form['username'] == row[1]: - if password_hash(request.form['password']) == row[2]: - session['logged_in'] = True - session['user'] = row[0] - # scan_for_new_documents(row[0]) - return redirect(url_for('list_shops')) - error='Invalid user/pass' - return render_template('login.html', error=error) - -@app.route('/register', methods=['GET', 'POST']) -def register(): - error = None - if not app.config['register']: - return "" - if request.method == 'POST': - import re, string - pattern = re.compile('[\W]+') - username=pattern.sub('', request.form['username']) - password=password_hash(request.form['password']) - if len(username)==0: - error="No username given" - return render_template('register.html', error=error) - if len(request.form['password'])<5: - error="Password too short" - return render_template('register.html', error=error) - cur = g.db.execute('select * from users') - for row in cur.fetchall(): - if username == row[1]: - error="Username already exists" - return render_template('register.html', error=error) - g.db.execute('insert into users (user,pass) values (?, ?)', - [username,password]) - g.db.commit() - flash('successfully registered user "%s". Now login.'%username) - return redirect(url_for('login')) - return render_template('register.html', error=error) - -@app.route('/profile', methods=['GET', 'POST']) -def profile(): - if not session.get('logged_in'): - return redirect(url_for('login')) - error = None - user=get_username(session.get('user')) - if request.method == 'POST': - import re, string - pattern = re.compile('[\W]+') - password=password_hash(request.form['password']) - if len(request.form['password'])<5: - error="Password too short" - return render_template('profile.html', error=error,user=user) - g.db.execute('update users set pass=? where id=?', - [password,session.get('user')]) - g.db.commit() - flash('successfully updated profile.') - return redirect(url_for('profile')) - return render_template('profile.html', error=error,user=user) - -@app.route('/logout') -def logout(): - session.pop('logged_in', None) - session.pop('user', None) - flash('You were logged out') - return render_template('login.html', error=None) - - -if __name__ == '__main__': - app.run()