From 2460ef593a17eecad863e8702904292cc9706d9e Mon Sep 17 00:00:00 2001 From: Dirk Engling Date: Mon, 20 Apr 2020 00:52:33 +0200 Subject: Kickoff --- Filer.py | 164 +++++++++++++++++++++++++++++++++++++++++++++++++ Makefile | 11 ++++ requirements.txt | 8 +++ static/style.css | 86 ++++++++++++++++++++++++++ templates/admin.html | 67 ++++++++++++++++++++ templates/mandant.html | 37 +++++++++++ 6 files changed, 373 insertions(+) create mode 100755 Filer.py create mode 100644 Makefile create mode 100644 requirements.txt create mode 100644 static/style.css create mode 100644 templates/admin.html create mode 100644 templates/mandant.html diff --git a/Filer.py b/Filer.py new file mode 100755 index 0000000..b7defd8 --- /dev/null +++ b/Filer.py @@ -0,0 +1,164 @@ +#!venv/bin/python + +import os +import hashlib +import base64 +import time +from shutil import rmtree + +from flask import Flask, render_template, jsonify, request, redirect, send_from_directory +from flask_dropzone import Dropzone +from argparse import ArgumentParser +from werkzeug.utils import secure_filename + +app = Flask(__name__) +app.config['SECRET_KEY'] = 'Silence is golden. Gerd Eist.' +#app.jinja_env.trim_blocks = True +#app.jinja_env.lstrip_blocks = True + +app.config['PREFERRED_URL_SCHEME'] = 'https' +app.config['DROPZONE_SERVE_LOCAL'] = True +app.config['DROPZONE_MAX_FILE_SIZE'] = 128 +app.config['DROPZONE_UPLOAD_MULTIPLE'] = True +app.config['DROPZONE_PARALLEL_UPLOADS'] = 10 + +app.config['DROPZONE_ALLOWED_FILE_CUSTOM'] = True +app.config['DROPZONE_ALLOWED_FILE_TYPE'] = '' + +app.config['DROPZONE_DEFAULT_MESSAGE'] = 'Ziehe die Dateien hier hin, um sie hochzuladen oder klicken Sie zur Auswahl.' + +dropzone = Dropzone(app) + +basedir = 'Daten' + +#### ADMIN FACING DIRECTORY LISTS #### +#### +#### +@app.route("/admin", methods=['GET']) +def admin(): + url_root = request.url_root.replace('http://', 'https://', 1) + users = os.listdir(os.path.join(basedir, 'Mandanten')) + return render_template('admin.html', users = users, tree = make_tree(basedir, 'Public'), url_root = url_root) + +@app.route("/admin/Dokumente/", methods=['GET']) +def admin_dokumente(user): + return render_template('mandant.html', admin = 'admin/', user = user, tree = make_tree(basedir, os.path.join('Dokumente', user))) + +@app.route("/admin/del-user/", methods=['POST']) +def admin_deluser(user): + method = request.form.get('_method', 'POST') + if method == 'DELETE': + rmtree(os.path.join(basedir, 'Dokumente', secure_filename(user))) + os.unlink(os.path.join(basedir, 'Mandanten', secure_filename(user))) + return redirect('/admin') + +@app.route("/admin/new-user", methods=['POST']) +def admin_newuser(): + password = request.form.get('password', '') + user = request.form.get('user', '') + if not password or not user: + return "Username or password missing", 400 + directory = secure_filename(user) + + salt = os.urandom(4) + sha = hashlib.sha1(password.encode('utf-8')) + sha.update(salt) + + digest_salt_b64 = base64.b64encode(sha.digest()+ salt) + tagged_digest_salt = '{{SSHA}}{}'.format(digest_salt_b64.decode('ascii')) + + try: + if not os.path.exists(os.path.join(basedir, 'Dokumente', directory)): + os.mkdir(os.path.join(basedir, 'Dokumente', directory)) + with open(os.path.join(basedir, 'Mandanten', directory), 'w+', encoding='utf-8') as htpasswd: + htpasswd.write("{}:{}\n".format(user, tagged_digest_salt)) + except OSError as error: + return "Couldn't create user scope", 500 + return redirect('/admin') + +#### USER FACING DIRECTORY LIST #### +#### +#### +@app.route("/Dokumente/", methods=['GET']) +def mandant(user): + return render_template('mandant.html', admin = '', user = user, tree = make_tree(basedir, os.path.join('Dokumente', user))) + +#### UPLOAD FILE ROUTES #### +#### +#### +@app.route('/Dokumente/', methods=['POST']) +@app.route('/admin/Dokumente/', methods=['POST']) +def upload_mandant(user): + for key, f in request.files.items(): + if key.startswith('file'): + username = secure_filename(user) + filename = secure_filename(f.filename) + f.save(os.path.join(basedir, 'Dokumente', username, filename)) + return 'upload template' + +@app.route('/admin', methods=['POST']) +def upload_admin(): + for key, f in request.files.items(): + if key.startswith('file'): + filename = secure_filename(f.filename) + f.save(os.path.join(basedir, 'Public', filename)) + return 'upload template' + +#### DELETE FILE ROUTES #### +#### +#### +@app.route('/Dokumente//', methods=['POST']) +def delete_file_mandant(user, filename): + method = request.form.get('_method', 'POST') + if method == 'DELETE': + os.unlink(os.path.join(basedir, 'Dokumente', secure_filename(user), secure_filename(filename))) + return redirect('/Dokumente/'+user) + +@app.route('/admin/Dokumente//', methods=['POST']) +def delete_file_mandant_admin(user, filename): + method = request.form.get('_method', 'POST') + if method == 'DELETE': + os.unlink(os.path.join(basedir, 'Dokumente', secure_filename(user), secure_filename(filename))) + return redirect('/admin/Dokumente/'+user) + +@app.route('/admin/Public/', methods=['POST']) +def delete_file_admin(filename): + method = request.form.get('_method', 'POST') + if method == 'DELETE': + os.unlink(os.path.join(basedir, 'Public', secure_filename(filename))) + return redirect('/admin') + +#### SERVE FILES RULES #### +#### +#### +@app.route('/admin/Dokumente//', methods=['GET']) +@app.route('/Dokumente//', methods=['GET']) +def custom_static(user, filename): + return send_from_directory(os.path.join(basedir, 'Dokumente'), os.path.join(user, filename)) + +@app.route('/Public/') +def custom_static_public(filename): + return send_from_directory(os.path.join(basedir, 'Public'), filename) + +def make_tree(rel, path): + tree = dict(name=path, download=os.path.basename(path), children=[]) + try: lst = os.listdir(os.path.join(rel,path)) + except OSError: + pass #ignore errors + else: + for name in lst: + fn = os.path.join(path, name) + if os.path.isdir(os.path.join(rel,fn)): + tree['children'].append(make_tree(rel, fn)) + else: + ttl = 10 - int((time.time() - os.path.getmtime(os.path.join(rel,fn))) / (24*3600)) + tree['children'].append(dict(name=fn, download=name, ttl = ttl)) + return tree + +if __name__ == "__main__": + parser = ArgumentParser(description="Filer") + parser.add_argument("-H", "--host", help="Hostname of the Flask app " + "[default %s]" % "127.0.0.1", default="127.0.0.1") + parser.add_argument("-P", "--port", help="Port for the Flask app " + "[default %s]" % "5000", default="5000") + args = parser.parse_args() + + app.run(host=args.host, port=int(args.port)) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..eae1e68 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +all: install + +run: + ./venv/bin/python Filer.py -P 5000 & + +venv: + python3 -m venv ./venv + +install: venv + ./venv/bin/pip install --upgrade pip + ./venv/bin/pip install -r requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..277407d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +click +Flask +Flask-Dropzone +itsdangerous +Jinja2 +MarkupSafe +sqlite3 +Werkzeug diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..3845959 --- /dev/null +++ b/static/style.css @@ -0,0 +1,86 @@ +body { + font-family: "HelveticaNeueLight", "HelveticaNeue-Light", "Helvetica Neue Light", "HelveticaNeue", "Helvetica Neue", 'TeXGyreHerosRegular', "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif; font-weight:400; font-size:15pt; font-stretch:normal; + margin: 1em 0; + padding: 0 5%; +} + +ul { + list-style-type: none; +} + +li { + margin: 0 0 0.5em 0; +} + +button { + margin: 0 0.5em 0 0; + min-width: 5.5em; +} + +button.add, +button.edit { + box-shadow:inset 0px 1px 0px 0px #bee2f9; + background:linear-gradient(to bottom, #63b8ee 5%, #468ccf 100%); + background-color:#63b8ee; + border-radius:9px; + border:1px solid #3866a3; + display:inline-block; + cursor:pointer; + color:#ffffff; + padding:5px 6px; + text-decoration:none; + text-shadow:0px 1px 0px #7cacde; +} +button.delete { + box-shadow:inset 0px 1px 0px 0px #cf866c; + background:linear-gradient(to bottom, #d0451b 5%, #bc3315 100%); + background-color:#d0451b; + border-radius:9px; + border:1px solid #942911; + display:inline-block; + cursor:pointer; + color:#ffffff; + padding:5px 6px; + text-decoration:none; + text-shadow:0px 1px 0px #854629; +} +button.add:hover, +button.edit:hover { + background:linear-gradient(to bottom, #468ccf 5%, #63b8ee 100%); + background-color:#468ccf; +} +button.delete:hover { + background:linear-gradient(to bottom, #bc3315 5%, #d0451b 100%); + background-color:#bc3315; +} + +button.add:active, +button.edit:active, +button.delete:active { + position:relative; + top:1px; +} + +input[type="text"] { + padding: 0.3em 0.4em; + + border: 1px solid grey; + border-radius: 6px; + + font-weight: bold; + font-size: 13pt; + transition: background-color 1s; + color: #444; + min-width: 35%; +} + +.age { + display: inline-block; + text-align: right; + border-radius: 4px; + border: solid 1px black; + background-color: grey; + padding: 0 0.2em; + margin-right: 0.5em; + width: 4em; +} diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..e62979e --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,67 @@ + + + +Filer Admin + +{{ dropzone.load_css() }} +{{ dropzone.style('border: 2px dashed #0087F7; min-height: 3em;') }} + + + + +{{ dropzone.load_js() }} +

Kanzlei Hubrig – Administration

+

Mandanten

+
    +{%- for item in users %} +
  • +
    + + +
    + + {{item}} Mandanten-URL: {{url_root}}Dokumente/{{item}} +
  • +{%- endfor %} +
  • +
    + + + + + +
    +
  • +
+ +

Öffentliche Dokumente

+
    +{%- for item in tree.children recursive %} +
  • + + +
    + {{ item.download }} + {%- if item.children -%} +
      {{ loop(item.children) }}
    + {%- endif %}
  • +{%- endfor %} +
+ +

Öffentliches Dokument hochladen

+
{{ dropzone.create(action='/admin') }}
+ +{{ dropzone.config() }} + + diff --git a/templates/mandant.html b/templates/mandant.html new file mode 100644 index 0000000..16c07ab --- /dev/null +++ b/templates/mandant.html @@ -0,0 +1,37 @@ + + + +Filer + +{{ dropzone.load_css() }} +{{ dropzone.style('border: 2px dashed #0087F7; min-height: 10px;') }} + + + +{{ dropzone.load_js() }} +

Kanzlei Hubrig – Dokumenten-Austausch

+{%- if admin %} +Zurück zur Übersicht +{% endif -%} +

Dokumente

+
    +{%- for item in tree.children recursive %} +
  • + + +
    +
    {{item.ttl}} Tag{%- if not item.ttl == 1 -%}e{%- endif -%}
    + {%- if 'children' in item -%} + {{item.download}}
    +
      {{ loop(item.children) }}
    + {%- else -%} + {{ item.download }} + {%- endif %}
  • +{%- endfor %} +
+ +
{{ dropzone.create(action="/"+admin+"Dokumente/"+user) }}
+ +{{ dropzone.config() }} + + -- cgit v1.2.3