so ich denke das sollte für ne 3 reichen lol
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -168,5 +168,5 @@ cython_debug/
|
|||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
_identifierAllocation.db
|
|
||||||
data/
|
data/
|
||||||
|
_map.db
|
||||||
33
README.md
33
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
|
## Pre-Installation
|
||||||
|
|
||||||
1. Installiere Abhängigkeiten.
|
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
|
3. Führe das Skript aus
|
||||||
nano /etc/caddy/Caddyfile
|
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
|
||||||
|
|||||||
93
main.py
93
main.py
@@ -1,63 +1,110 @@
|
|||||||
from flask import send_file, Flask, request
|
from flask import send_file, Flask, request, jsonify
|
||||||
#from werkzeug.middleware.proxy_fix import ProxyFix # Für Einsatz in Produktion
|
#from werkzeug.middleware.proxy_fix import ProxyFix # Für Einsatz in Produktion hinter einem Webserver
|
||||||
import sqlite3, hashlib, random, os, pathlib
|
import sqlite3, hashlib, random, os, pathlib, jinja2
|
||||||
|
|
||||||
|
prefixes = ["B", "KB", "MB", "GB", "TB"]
|
||||||
|
|
||||||
app = Flask(__name__)
|
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)
|
jinjaEnv = jinja2.Environment(loader=jinja2.PackageLoader("main", "pages"))
|
||||||
dbCursor = db.cursor()
|
downloadTemplate = jinjaEnv.get_template("download.html")
|
||||||
dbCursor.execute("CREATE TABLE IF NOT EXISTS _idToFile (id TEXT PRIMARY KEY, destination TEXT, uploadTime DATETIME, fileName TEXT, usesLeft INT);")
|
indexTemplate = jinjaEnv.get_template("index.html")
|
||||||
|
errorTemplate = jinjaEnv.get_template("error.html")
|
||||||
|
uploadedTemplate = jinjaEnv.get_template("uploaded.html")
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
if pathlib.Path("./data").is_dir() == False:
|
||||||
os.mkdir("data")
|
os.mkdir("data")
|
||||||
|
|
||||||
|
# Flask dings zeug
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def mainpage():
|
def mainpage():
|
||||||
return send_file("index.html")
|
return indexTemplate.render()
|
||||||
|
|
||||||
@app.route("/upload", methods = ["POST"])
|
@app.route("/upload", methods = ["POST"])
|
||||||
def uploader():
|
def uploader():
|
||||||
file = request.files['file'] #
|
file = request.files['file'] # Hole die Datei als FileStorage Objekt
|
||||||
name = file.filename.rsplit(".", 1)
|
name = file.filename.rsplit(".", 1) #
|
||||||
|
|
||||||
idValue = ""
|
idValue = ""
|
||||||
idIsUnique = False
|
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:
|
while idIsUnique == False:
|
||||||
|
|
||||||
if idValue != "":
|
if idValue != "": # Ich könnte das if weglassen aber keine Ahnung ich finde das so besser
|
||||||
idValue = ""
|
idValue = ""
|
||||||
|
|
||||||
|
# Fancy weg um eine 14-stellige ID zu erzeugen, wahrscheinlich gibt es fertige Funktionen aber wollte trotzdem eigene haben
|
||||||
for _ in range(14):
|
for _ in range(14):
|
||||||
vArray = [""] * 3
|
vArray = [""] * 3 # Fancy weg um ein Array mit der Länge 3 zu erzeugen
|
||||||
vArray[0] = chr(random.randint(48, 57)) # 0 - 9
|
vArray[0] = chr(random.randint(48, 57)) # 0 - 9 in Unicode Charset
|
||||||
vArray[1] = chr(random.randint(65, 90)) # A - Z
|
vArray[1] = chr(random.randint(65, 90)) # A - Z in Unicode Charset
|
||||||
vArray[2] = chr(random.randint(97, 122)) # a - z
|
vArray[2] = chr(random.randint(97, 122)) # a - z in Unicode Charset
|
||||||
|
|
||||||
|
# Aus 3 generierten Werten wähle einen aus und füge es am Ende des Strings an
|
||||||
selector = random.randint(0,2)
|
selector = random.randint(0,2)
|
||||||
idValue += vArray[selector]
|
idValue += vArray[selector]
|
||||||
|
|
||||||
|
# Ü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,))
|
dbCursor.execute("SELECT * FROM _idToFile WHERE id=?;", (idValue,))
|
||||||
if dbCursor.fetchone() == None:
|
if dbCursor.fetchone() == None:
|
||||||
idIsUnique = True
|
idIsUnique = True
|
||||||
|
|
||||||
if (len(name) > 1): # Dateinamen die ein Suffix besitzen
|
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
|
else: # Dateinamen die kein Suffix besitzen
|
||||||
path = f"./data/{hashlib.sha256((name[0] + idValue).encode()).hexdigest()}"
|
path = f"./data/{hashlib.sha256((name[0] + idValue).encode()).hexdigest()}"
|
||||||
|
|
||||||
file.save(path)
|
file.save(path)
|
||||||
dbCursor.execute("INSERT INTO _idToFile VALUES (?, ?, datetime('now'), ?, ?);", (idValue, path, file.filename, 10))
|
dbCursor.execute("INSERT INTO _idToFile VALUES (?, ?, datetime('now'), ?);", (idValue, path, file.filename,)) # Füge neuen Eintrag in die Datenbank
|
||||||
db.commit()
|
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/<string:identifier>")
|
# Nicht die eleganteste Lösung fürs Download aber ich hab kein Bock es anders zu lösen
|
||||||
def downloader(identifier):
|
|
||||||
dbCursor.execute("SELECT destination FROM _idToFile WHERE id=?;", (identifier,))
|
# 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/<string:identifier>")
|
||||||
|
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/<string:identifier>/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()
|
path = dbCursor.fetchone()
|
||||||
if path != None:
|
if path != None:
|
||||||
return send_file(path[0])
|
return send_file(path[0])
|
||||||
else:
|
else:
|
||||||
return "no file, now fuck off"
|
if request.user_agent.string != "API":
|
||||||
|
return errorTemplate.render(error="keine Datei gefunden")
|
||||||
|
else:
|
||||||
|
return jsonify(status=301)
|
||||||
15
pages/download.html
Normal file
15
pages/download.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Download Info</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{ filename }}</h1>
|
||||||
|
<p>Größe: {{ size }}</p>
|
||||||
|
<p>Uploaddatum: {{ uploadDate }}</p>
|
||||||
|
<br>
|
||||||
|
<a href="{{ downloadPath }}">Download</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
12
pages/error.html
Normal file
12
pages/error.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>CHIEF! Somethings wrong!</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>WEE WOO WEE WOO! WE FOUND AN ERROR!</h1>
|
||||||
|
<p>{{ error }}</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -9,11 +9,6 @@
|
|||||||
<form action="./upload" method="post" enctype="multipart/form-data">
|
<form action="./upload" method="post" enctype="multipart/form-data">
|
||||||
<input type="file" name="file"/>
|
<input type="file" name="file"/>
|
||||||
<br>
|
<br>
|
||||||
<input type="checkbox" id="useLimt">
|
|
||||||
<label for="useLimit">Enable download limit?</label>
|
|
||||||
<br>
|
|
||||||
<input type="number" min="1" id="useLimitNumber">
|
|
||||||
<br>
|
|
||||||
<input type="submit" value="Upload">
|
<input type="submit" value="Upload">
|
||||||
</form>
|
</form>
|
||||||
</body>
|
</body>
|
||||||
15
pages/uploaded.html
Normal file
15
pages/uploaded.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Document</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>File has been uploaded</h1>
|
||||||
|
<p></p>
|
||||||
|
<p>Link: <a href="{{ link }}">{{ link }}</a></p>
|
||||||
|
<br>
|
||||||
|
<a href="./">Return</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user