diff --git a/.gitignore b/.gitignore index f5905ad..9d61207 100644 --- a/.gitignore +++ b/.gitignore @@ -168,5 +168,5 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ -_identifierAllocation.db -data/ \ No newline at end of file +data/ +_map.db \ No newline at end of file diff --git a/README.md b/README.md index 9ff4b1b..36f8173 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,35 @@ -Dieses Projekt demonstriert die Umsetzung eines einfaches Dienstes mit Python 3, Flask, sqlite3 und Caddy welches ermöglicht das Hochladen/Herunterladen von Dateien über Weboberfläche oder API. +Dieses Projekt demonstriert die Umsetzung eines einfaches Dienstes mit flask (Routing), jinja2 (Templates) und sqlite3 in Python welches ermöglicht das Hochladen/Herunterladen von Dateien über Weboberfläche oder URL abruf. ## Pre-Installation 1. Installiere Abhängigkeiten. - Debian-basierte Distributionen: sudo apt install caddy python + pip install flask jinja2 sqlite3 - pip install flask sqlite3 +2. Klone die Repository + git clone https://git.raizen.me/raizen/E3FI2_Kamil_Filehosting.git -2. Editiere die Konfigurationsdatei von Caddy - nano /etc/caddy/Caddyfile +3. Führe das Skript aus + flask --app main.py run + oder + python -m flask --app main.py run +4. In Webbrowser http://127.0.0.1:5000 öffen + + +## Bedienung +Aus Perspektive der Nutzer: + 1. Rufe index auf + 2. Häge die Datei an das Formular, drücke auf Upload + 3. Eine Übersicht mit Downloadlink wird angezeigt + +Aus Perspektive der API: + 1. Übergebe die Datei an das /upload Endpoint (format: file -> binary data, filename -> name der datei) + 2. Aufruf von Downloads genau so wie Nutzer + + +## Struktur + +(host)/ -> Formular bei dem die Datei angehängt wird (nicht für API gedacht) +(host)/upload -> Formular oder API sendet die Datei an dieses Endpoint, als Rückgabe wird eine übersicht mit dem Link ausgegeben. +(host)/file/(id) -> Dateiübersicht die Name, Größe, Uploaddatum sowie den Downloadlink enthält (könnte für API nützlich sein) +(host)/file/(id)/download -> Startet den download von der Datei diff --git a/_map.db b/_map.db index 744bf75..71ca0fa 100644 Binary files a/_map.db and b/_map.db differ diff --git a/main.py b/main.py index e32f00c..a0c5924 100644 --- a/main.py +++ b/main.py @@ -1,63 +1,110 @@ -from flask import send_file, Flask, request -#from werkzeug.middleware.proxy_fix import ProxyFix # Für Einsatz in Produktion -import sqlite3, hashlib, random, os, pathlib +from flask import send_file, Flask, request, jsonify +#from werkzeug.middleware.proxy_fix import ProxyFix # Für Einsatz in Produktion hinter einem Webserver +import sqlite3, hashlib, random, os, pathlib, jinja2 + +prefixes = ["B", "KB", "MB", "GB", "TB"] app = Flask(__name__) -#app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1) # Für Einsatz in Produktion +#app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1) # Für Einsatz in Produktion hinter einem Webserver -db = sqlite3.connect("_map.db", check_same_thread=False) -dbCursor = db.cursor() -dbCursor.execute("CREATE TABLE IF NOT EXISTS _idToFile (id TEXT PRIMARY KEY, destination TEXT, uploadTime DATETIME, fileName TEXT, usesLeft INT);") +jinjaEnv = jinja2.Environment(loader=jinja2.PackageLoader("main", "pages")) +downloadTemplate = jinjaEnv.get_template("download.html") +indexTemplate = jinjaEnv.get_template("index.html") +errorTemplate = jinjaEnv.get_template("error.html") +uploadedTemplate = jinjaEnv.get_template("uploaded.html") -if pathlib.Path("./data").is_dir() == False: + +db = sqlite3.connect("_map.db", check_same_thread=False) # Erzeuge oder/und baue eine Verbindung auf zu der Datenbank (check_same_thread muss False sein da Flask mehrere eigene Threads verwendet) +dbCursor = db.cursor() # Keine Ahnung warum explizit ein Zeiger erzeugt werden muss, die meisten der Bibliotheken verwalten das von alleine +dbCursor.execute("CREATE TABLE IF NOT EXISTS _idToFile (id TEXT PRIMARY KEY, destination TEXT, uploadTime DATETIME, fileName TEXT);") # Selbsterklärend + +# Erzeuge ein Ordner (falls dieser nicht exisitert) wo die Dateien abgelegt werden +if pathlib.Path("./data").is_dir() == False: os.mkdir("data") +# Flask dings zeug @app.route("/") def mainpage(): - return send_file("index.html") + return indexTemplate.render() @app.route("/upload", methods = ["POST"]) def uploader(): - file = request.files['file'] # - name = file.filename.rsplit(".", 1) + file = request.files['file'] # Hole die Datei als FileStorage Objekt + name = file.filename.rsplit(".", 1) # idValue = "" idIsUnique = False - while idIsUnique == False: + # Solange die ID nicht eindeutig ist, versuche eine neue zu erzeugen und überprüfe es (wird in 99,99% Fällen nur ein mal durchgelaufen) + while idIsUnique == False: - if idValue != "": + if idValue != "": # Ich könnte das if weglassen aber keine Ahnung ich finde das so besser idValue = "" + # Fancy weg um eine 14-stellige ID zu erzeugen, wahrscheinlich gibt es fertige Funktionen aber wollte trotzdem eigene haben for _ in range(14): - vArray = [""] * 3 - vArray[0] = chr(random.randint(48, 57)) # 0 - 9 - vArray[1] = chr(random.randint(65, 90)) # A - Z - vArray[2] = chr(random.randint(97, 122)) # a - z + vArray = [""] * 3 # Fancy weg um ein Array mit der Länge 3 zu erzeugen + vArray[0] = chr(random.randint(48, 57)) # 0 - 9 in Unicode Charset + vArray[1] = chr(random.randint(65, 90)) # A - Z in Unicode Charset + vArray[2] = chr(random.randint(97, 122)) # a - z in Unicode Charset - selector = random.randint(0,2) - idValue += vArray[selector] + # Aus 3 generierten Werten wähle einen aus und füge es am Ende des Strings an + selector = random.randint(0,2) + idValue += vArray[selector] - dbCursor.execute("SELECT * FROM _idToFile WHERE id=?;", (idValue,)) + # Überprüfe ob die erstellte ID bereits existiert, falls ja, dann führe die Schleife nochmal durch + dbCursor.execute("SELECT * FROM _idToFile WHERE id=?;", (idValue,)) if dbCursor.fetchone() == None: idIsUnique = True if (len(name) > 1): # Dateinamen die ein Suffix besitzen - path = f"./data/{hashlib.sha256((name[0] + idValue).encode()).hexdigest()}.{name[1]}" + path = f"./data/{hashlib.sha256((name[0] + idValue).encode()).hexdigest()}.{name[1]}" # Wirkt wie schwarze Magie ist aber sehr simpel else: # Dateinamen die kein Suffix besitzen path = f"./data/{hashlib.sha256((name[0] + idValue).encode()).hexdigest()}" file.save(path) - dbCursor.execute("INSERT INTO _idToFile VALUES (?, ?, datetime('now'), ?, ?);", (idValue, path, file.filename, 10)) - db.commit() + dbCursor.execute("INSERT INTO _idToFile VALUES (?, ?, datetime('now'), ?);", (idValue, path, file.filename,)) # Füge neuen Eintrag in die Datenbank + db.commit() # Bestätige die Transaktion damit der Eintrag tatsächlich gespeichert wird. Das beste ist ja, dass ich es nur bei INSERT tätigen muss - return "sank you" + if request.user_agent.string != "API": + return uploadedTemplate.render(link=("/file/" + idValue)) + else: + return jsonify(status=100,id=idValue) -@app.route("/download/") -def downloader(identifier): - dbCursor.execute("SELECT destination FROM _idToFile WHERE id=?;", (identifier,)) +# Nicht die eleganteste Lösung fürs Download aber ich hab kein Bock es anders zu lösen + +# Das hier ist quasi das "Entry Point", es wird eineeine Seite zurück gegeben mit Infos über die Datei sowie einen Download Link +@app.route("/file/") +def fileInfo(identifier): + dbCursor.execute("SELECT destination, uploadTime, filename FROM _idToFile WHERE id=?;", (identifier,)) # Hole die Infos für die Template + data = dbCursor.fetchone() + if data != None: + sizeBytes = os.path.getsize(data[0]) + factor = 0 # 0 -> 1 = Kilo -> 2 = Mega -> 3 = Giga, usw.. + + while ((sizeBytes / 1024) > 1): + sizeBytes = sizeBytes / 1024 + factor += 1 + + if request.user_agent.string != "API": + return downloadTemplate.render(filename=data[2], uploadDate=data[1], size=("%.2f" % sizeBytes + " " + prefixes[factor]), downloadPath=("/file/" + identifier + "/download")) + else: + return jsonify(status=100, name=data[2], size=("%.2f" % sizeBytes + " " + prefixes[factor]), uploadDate=data[1]) + else: + if request.user_agent.string != "API": + return errorTemplate.render(error="keine Datei gefunden") + else: + return jsonify(status=301) + +# Das Download Link +@app.route("/file//download") +def fileDownload(identifier): + dbCursor.execute("SELECT destination FROM _idToFile WHERE id=?;", (identifier,)) # Das Komma muss so bleiben sonst funktioniert """"prepared statement"""" nicht path = dbCursor.fetchone() if path != None: return send_file(path[0]) else: - return "no file, now fuck off" \ No newline at end of file + if request.user_agent.string != "API": + return errorTemplate.render(error="keine Datei gefunden") + else: + return jsonify(status=301) \ No newline at end of file diff --git a/pages/download.html b/pages/download.html new file mode 100644 index 0000000..169d12c --- /dev/null +++ b/pages/download.html @@ -0,0 +1,15 @@ + + + + + + Download Info + + +

{{ filename }}

+

Größe: {{ size }}

+

Uploaddatum: {{ uploadDate }}

+
+ Download + + \ No newline at end of file diff --git a/pages/error.html b/pages/error.html new file mode 100644 index 0000000..9ff7074 --- /dev/null +++ b/pages/error.html @@ -0,0 +1,12 @@ + + + + + + CHIEF! Somethings wrong! + + +

WEE WOO WEE WOO! WE FOUND AN ERROR!

+

{{ error }}

+ + \ No newline at end of file diff --git a/index.html b/pages/index.html similarity index 66% rename from index.html rename to pages/index.html index ced6a0a..3139083 100644 --- a/index.html +++ b/pages/index.html @@ -9,11 +9,6 @@

- - -
- -
diff --git a/pages/uploaded.html b/pages/uploaded.html new file mode 100644 index 0000000..1fbd6bf --- /dev/null +++ b/pages/uploaded.html @@ -0,0 +1,15 @@ + + + + + + Document + + +

File has been uploaded

+

+

Link: {{ link }}

+
+ Return + + \ No newline at end of file