Initial commit
This commit is contained in:
commit
ee54dae729
12
.env.example
Normal file
12
.env.example
Normal file
@ -0,0 +1,12 @@
|
||||
# Rename file to .env and adapt variables
|
||||
# Environment variables for book stack update (bsupdate.py)
|
||||
BOOKSTACK_URL="https://wiki.my-book-stack.net"
|
||||
BOOKSTACK_TOKEN_ID="token-generated-by-book-stack"
|
||||
BOOKSTACK_TOKEN_SECRET="token-generated-by-book-stack"
|
||||
PAGE_SLUG="name-of-page"
|
||||
BOOK_SLUG="name-of-book"
|
||||
INSERT_MARKER_LINKS="## Links"
|
||||
INSERT_MARKER_TEXT="## Text"
|
||||
|
||||
# Environment variables for req-short.py
|
||||
API_TOKEN="something-i-created-by-myself"
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
.env
|
||||
docker-compose.yml
|
||||
data/*
|
||||
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@ -0,0 +1,21 @@
|
||||
# Basis-Image mit Python
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Installiere Abhängigkeiten
|
||||
RUN pip install --no-cache-dir --upgrade pip
|
||||
|
||||
# Arbeitsverzeichnis im Container
|
||||
WORKDIR /app
|
||||
|
||||
# requirements.txt kopieren und installieren
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# App-Dateien kopieren
|
||||
COPY *.py .
|
||||
|
||||
# Port freigeben
|
||||
EXPOSE 8050
|
||||
|
||||
# Container-Startbefehl
|
||||
CMD ["python", "req-short.py"]
|
||||
180
bsupdate.py
Normal file
180
bsupdate.py
Normal file
@ -0,0 +1,180 @@
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from urllib.parse import urlparse
|
||||
import os
|
||||
|
||||
BOOKSTACK_URL = os.getenv("BOOKSTACK_URL","https://my-book-stack.com")
|
||||
BOOKSTACK_TOKEN_ID = os.getenv("BOOKSTACK_TOKEN_ID", "TOKEN-ID")
|
||||
BOOKSTACK_TOKEN_SECRET = os.getenv("BOOKSTACK_TOKEN_SECRET", "TOKEN-SECRET")
|
||||
PAGE_SLUG = os.getenv("PAGE_SLUG","page-name-of-book")
|
||||
BOOK_SLUG = os.getenv("BOOK_SLUG","book-name")
|
||||
INSERT_MARKER_LINK = os.getenv("INSERT_MARKER_LINK", "## Unsortierte Links")
|
||||
INSERT_MARKER_TEXT = os.getenv("INSERT_MARKER_TEXT", "## Textmerker")
|
||||
|
||||
HEADERS = {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Token {BOOKSTACK_TOKEN_ID}:{BOOKSTACK_TOKEN_SECRET}"
|
||||
}
|
||||
|
||||
|
||||
|
||||
def is_valid_url(url):
|
||||
parsed = urlparse(url)
|
||||
return all([parsed.scheme in ("http", "https"), parsed.netloc])
|
||||
|
||||
|
||||
def get_page_id_by_slug(book_slug, page_slug):
|
||||
res = requests.get(f"{BOOKSTACK_URL}/api/pages", headers=HEADERS)
|
||||
print(HEADERS)
|
||||
print(res)
|
||||
pages = res.json().get("data", [])
|
||||
for p in pages:
|
||||
if p["book_slug"] == book_slug and p["slug"] == page_slug:
|
||||
return p["id"]
|
||||
|
||||
raise("Book not found")
|
||||
|
||||
|
||||
|
||||
|
||||
def get_page_title_from_url(url):
|
||||
try:
|
||||
response = requests.get(url, timeout=10)
|
||||
soup = BeautifulSoup(response.text, "html.parser")
|
||||
title_tag = soup.find("title")
|
||||
return title_tag.text.strip() if title_tag else url
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Abrufen des Titels: {e}")
|
||||
return url
|
||||
|
||||
|
||||
def get_clean_title_from_url(url, max_len=80):
|
||||
try:
|
||||
res = requests.get(url, timeout=10)
|
||||
res.raise_for_status()
|
||||
soup = BeautifulSoup(res.text, "html.parser")
|
||||
|
||||
# 1. og:title
|
||||
og_title = soup.find("meta", property="og:title")
|
||||
if og_title and og_title.get("content"):
|
||||
title = og_title["content"]
|
||||
|
||||
# 2. <meta name="title">
|
||||
elif soup.find("meta", attrs={"name": "title"}):
|
||||
title = soup.find("meta", attrs={"name": "title"}).get("content", "")
|
||||
|
||||
# 3. <title>
|
||||
elif soup.title and soup.title.string:
|
||||
title = soup.title.string
|
||||
|
||||
# 4. Fallback: Domain
|
||||
else:
|
||||
return urlparse(url).netloc
|
||||
|
||||
title = title.strip()
|
||||
|
||||
# Optional: Aufteilen an Trennzeichen wie " | " oder " – "
|
||||
for sep in ["|", "–", "-", "•"]:
|
||||
if sep in title:
|
||||
title = title.split(sep)[0].strip()
|
||||
break
|
||||
|
||||
# Länge kürzen
|
||||
if len(title) > max_len:
|
||||
title = title[:max_len].rstrip() + "…"
|
||||
|
||||
return title
|
||||
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Abrufen oder Parsen des Titels: {e}")
|
||||
return urlparse(url).netloc # Fallback auf Domain
|
||||
|
||||
|
||||
def append_link_to_bookstack_page(url):
|
||||
strPageId = get_page_id_by_slug(BOOK_SLUG, PAGE_SLUG)
|
||||
|
||||
# Hole aktuellen Seiteninhalt
|
||||
page = requests.get(f"{BOOKSTACK_URL}/api/pages/{strPageId}", headers=HEADERS).json()
|
||||
current_content = page["markdown"]
|
||||
|
||||
if url in current_content:
|
||||
print("Text bereits vorhanden.")
|
||||
return
|
||||
|
||||
title = get_page_title_from_url(url)
|
||||
new_entry = f"- [{title}]({url})<br>{url}"
|
||||
|
||||
# Füge Link unterhalb von INSERT_MARKER_LINK ein
|
||||
|
||||
if INSERT_MARKER_LINK in current_content:
|
||||
parts = current_content.split(INSERT_MARKER_LINK)
|
||||
updated_content = parts[0] + INSERT_MARKER_LINK + "\n" + new_entry + parts[1]
|
||||
else:
|
||||
# Falls Marker nicht vorhanden, füge ihn am Ende hinzu
|
||||
updated_content = current_content + "\n\n## Unsortierte Links\n\n" + new_entry
|
||||
|
||||
# Seite aktualisieren
|
||||
update_data = {
|
||||
"name": page["name"],
|
||||
"markdown": updated_content
|
||||
}
|
||||
|
||||
response = requests.put(
|
||||
f"{BOOKSTACK_URL}/api/pages/{strPageId}",
|
||||
headers=HEADERS,
|
||||
json=update_data
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
print("Link erfolgreich hinzugefügt.")
|
||||
else:
|
||||
print("Fehler beim Aktualisieren der Seite:", response.text)
|
||||
|
||||
def append_text_to_bockstack_page(strText):
|
||||
strPageId = get_page_id_by_slug(BOOK_SLUG, PAGE_SLUG)
|
||||
|
||||
# Hole aktuellen Seiteninhalt
|
||||
page = requests.get(f"{BOOKSTACK_URL}/api/pages/{strPageId}", headers=HEADERS).json()
|
||||
current_content = page["markdown"]
|
||||
|
||||
if strText in current_content:
|
||||
print("Text bereits vorhanden.")
|
||||
return
|
||||
|
||||
new_entry = strText + "\n\n"
|
||||
|
||||
# Füge Text unterhalb von INSERT_MARKER_TEXT ein
|
||||
if INSERT_MARKER_TEXT in current_content:
|
||||
parts = current_content.split(INSERT_MARKER_TEXT)
|
||||
updated_content = parts[0] + INSERT_MARKER_TEXT + "\n\n" + new_entry + "\n" + parts[1]
|
||||
else:
|
||||
# Falls Marker nicht vorhanden, füge ihn am Ende hinzu
|
||||
updated_content = current_content + "\n\n## Textmerker\n\n" + new_entry
|
||||
|
||||
# Seite aktualisieren
|
||||
update_data = {
|
||||
"name": page["name"],
|
||||
"markdown": updated_content
|
||||
}
|
||||
|
||||
response = requests.put(
|
||||
f"{BOOKSTACK_URL}/api/pages/{strPageId}",
|
||||
headers=HEADERS,
|
||||
json=update_data
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
print("Link erfolgreich hinzugefügt.")
|
||||
else:
|
||||
print("Fehler beim Aktualisieren der Seite:", response.text)
|
||||
|
||||
|
||||
def add_link_or_text(strText):
|
||||
if (is_valid_url(strText)):
|
||||
append_link_to_bookstack_page(strText)
|
||||
else:
|
||||
append_text_to_bockstack_page(strText)
|
||||
pass
|
||||
|
||||
|
||||
31
docker-compose.example
Normal file
31
docker-compose.example
Normal file
@ -0,0 +1,31 @@
|
||||
# adapt file to your needs especially traefik (reverse proxy options)
|
||||
# rename file to docker-compose.yml
|
||||
services:
|
||||
req_short:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: req_short
|
||||
env_file:
|
||||
- .env
|
||||
#ports:
|
||||
# - "8050:8050"
|
||||
volumes:
|
||||
- ./data/uploads:/app/uploads
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
# if using traefik
|
||||
- "traefik.http.routers.wiki.rule=Host(`wiki.my-book-stack.com`)"
|
||||
- "traefik.http.routers.wiki.tls.certResolver=default"
|
||||
- "traefik.http.routers.wiki.tls=true"
|
||||
- "traefik.http.services.wiki.loadbalancer.server.port=8050"
|
||||
|
||||
# network needed if using traefik
|
||||
networks:
|
||||
- router
|
||||
|
||||
# only needed for traefik
|
||||
networks:
|
||||
router:
|
||||
name: router-network
|
||||
external: true
|
||||
83
readme.md
Normal file
83
readme.md
Normal file
@ -0,0 +1,83 @@
|
||||
Ich möchte eine Möglichkeit haben, Informationen von meinem Smartphone aus mit dem Wiki oder etwas anderem zu teilen.
|
||||
|
||||
Generelle Aufgaben:
|
||||
|
||||
- Link aus Webbrowser teilen -> automatische Eintragung ins Wiki
|
||||
- Datei aus z.B. Fotoverwaltung teilen -> autmoatische Ablage auf dem Server
|
||||
- Wiki-Artikel schneller erstellen (Bilder)
|
||||
|
||||
## Konzept
|
||||
|
||||
- Teilen über Android mittels App [HTTP Request Shortcuts](https://play.google.com/store/apps/details?id=ch.rmy.android.http_shortcuts)
|
||||
- Python Gegenstück auf Server um Dateien und Texte entgegen zu nehmen (Flask)
|
||||
|
||||
### Konfiguration HTTP Request Shortcuts
|
||||
|
||||
Für das Basisbeispiel in welchem wir zum einen Dateien und zum anderen Links bzw. Texte teilen möchten, benötigen wir zwei Konfigurationseinträge bzw. HTTP Shortcuts.
|
||||
|
||||
#### Datei teilen
|
||||
|
||||
Sobald dieser Eintrag in die App hinzugefügt wurde, kann jede Datei auf den Server übertragen werden. Hierzu muss nur in der entsprechenden App "Teilen" ausgewählt und als Ziel HTTP Request Shortcuts gewählt werden.
|
||||
|
||||
1. Neuen Eintrag über + hinzufügen
|
||||
2. Auswahl *Von Grund auf erstellen*
|
||||
3. Shortcut-Name: *File to Server*
|
||||
4. Allgemeine Einstellungen:
|
||||
1. Methode: *Post*
|
||||
2. URL: https://https://my-book-stack.com/upload
|
||||
5. Request Header:
|
||||
1. mittels + Eintrag für API-Token hinzufügen (siehe API_TOKEN Umgebungsvariable)
|
||||
2. Header: *X-API-Token*
|
||||
3. Wert: *my-secret-token*
|
||||
6. Request Body:
|
||||
1. Request-Body-Typ: *Parameterliste (form-data)*
|
||||
2. Eintrat mit + hinzufügen
|
||||
1. *Einzelne Datei* auswählen
|
||||
2. Parametername: *file*
|
||||
3. Daten-Quelle: *Dateiauswahl öffnen*
|
||||
7. Response-Einstellungen - Was passiert, wenn das Teilen ausgeführt wurde?
|
||||
1. Bei Erfolg: *Benutzerdefinierte Nachricht anzeigen*
|
||||
2. Nachricht: *Wurde mit Server geteilt.*
|
||||
3. Anzeige-Typ: *Toast-PopUp*
|
||||
|
||||
Alle nicht aufgeführten Einstellungen entsprechen den Standardeinstellungen und wurden nicht angepasst.
|
||||
|
||||
#### Text / Link teilen
|
||||
|
||||
Um einen Text teilen zu können, muss zuvor noch eine Variable angelegt werden.
|
||||
|
||||
1. drei Punkte oben Rechts auswählen (App)
|
||||
2. *{} Variablen* auswählen
|
||||
3. mit + neue Variable hinzufügen
|
||||
4. *Statische Variable* wählen
|
||||
1. Name: *shared_text*
|
||||
2. *"Teilen..."* erlauben anwählen
|
||||
|
||||
Im Anschluss einen neuen Eintrag hinzufügen. Dies geht am schnellsten in dem man den bereits angelegten Shortcut Dupliziert.
|
||||
|
||||
1. vorhandenen Eintrag lange drücken
|
||||
2. Im Kontextmenü *Duplizieren* auswählen
|
||||
3. Shortcut-Name ändern z.B.: *Link to Server*
|
||||
4. Request-Body anpassen:
|
||||
1. Vorhandenen Eintrag löschen
|
||||
2. Neuen Eintrag über + hinzufügen
|
||||
1. Parametertyp: *Text*
|
||||
2. Parametername: *text*
|
||||
3. Wert: {{shared_text}} (mit allen Klammern)
|
||||
|
||||
Nun sollte es möglich sein, auch aus dem Browser heraus direkt einen Link zu teilen.
|
||||
|
||||
### Python Gegenstück
|
||||
|
||||
Das Programm req-short.py hat die folgenden Aufgaben:
|
||||
|
||||
- Kann Dateien am Endpunkt /upload entgegennehmen und im Ordner uploads speichern
|
||||
- Kann einen Text am Endpunkt /upload entgegennehmen und speichert diesen in log.txt
|
||||
- Zeigt eine Galerie der hochgeladenen Dateien bzw. nur der Bilder an
|
||||
- Der Upload ist über ein "API_TOKEN" gesichert (nur Basis)
|
||||
|
||||
Die Anwendung muss hinter einem Reverse-Proxy gehostet werden, um einen sicheren Zugriff über SSL zu haben.
|
||||
|
||||
Um einen geteilten Link bzw. einen Text in Book Stack zu hinterlegen wird bsupdate.py verwendet. Hierzu müssen die .env Variablen gesetzt werden.
|
||||
|
||||
|
||||
93
req-short.py
Normal file
93
req-short.py
Normal file
@ -0,0 +1,93 @@
|
||||
from flask import Flask, request, jsonify, abort, send_from_directory, render_template_string
|
||||
import os
|
||||
from datetime import datetime
|
||||
from werkzeug.utils import secure_filename
|
||||
import bsupdate as bu
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
UPLOAD_FOLDER = "UPLOAD_FOLDER"
|
||||
API_TOKEN = os.getenv("API_TOKEN", "my-secret-token")
|
||||
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "webp"}
|
||||
|
||||
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
||||
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
|
||||
|
||||
# Authentifizierung
|
||||
def check_auth():
|
||||
token = request.headers.get("X-API-Token")
|
||||
if token != API_TOKEN:
|
||||
abort(401, description="Unauthorized")
|
||||
|
||||
# Dateiendung prüfen
|
||||
def allowed_file(filename):
|
||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||
|
||||
# Upload-Endpunkt
|
||||
@app.route("/upload", methods=["POST"])
|
||||
def upload():
|
||||
check_auth()
|
||||
|
||||
text = request.form.get("text")
|
||||
uploaded_file = request.files.get("file")
|
||||
|
||||
if uploaded_file and uploaded_file.filename != "":
|
||||
filename = secure_filename(uploaded_file.filename)
|
||||
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
full_filename = f"{timestamp}_{filename}"
|
||||
path = os.path.join(app.config["UPLOAD_FOLDER"], full_filename)
|
||||
uploaded_file.save(path)
|
||||
print(f"Gespeichert: {path}")
|
||||
else:
|
||||
print("Keine Datei erhalten.")
|
||||
|
||||
if text:
|
||||
bu.add_link_or_text(text)
|
||||
# with open(os.path.join(app.config["UPLOAD_FOLDER"], "log.txt"), "a") as f:
|
||||
# f.write(f"{datetime.now()}: {text}\n")
|
||||
print(f"Text erhalten: {text}")
|
||||
|
||||
return jsonify({"status": "OK"}), 200
|
||||
|
||||
# Galerie anzeigen
|
||||
@app.route("/")
|
||||
def gallery():
|
||||
files = os.listdir(app.config["UPLOAD_FOLDER"])
|
||||
image_files = [
|
||||
f for f in sorted(files, reverse=True)
|
||||
if allowed_file(f)
|
||||
]
|
||||
html = render_template_string("""
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Bilder-Galerie</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; padding: 2em; }
|
||||
img { max-width: 300px; margin: 1em; }
|
||||
.grid { display: flex; flex-wrap: wrap; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hochgeladene Bilder</h1>
|
||||
<div class="grid">
|
||||
{% for filename in images %}
|
||||
<div>
|
||||
<img src="/uploads/{{ filename }}" alt="{{ filename }}">
|
||||
<p>{{ filename }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
""", images=image_files)
|
||||
return html
|
||||
|
||||
# Bilder statisch servieren
|
||||
@app.route("/uploads/<path:filename>")
|
||||
def uploaded_file(filename):
|
||||
return send_from_directory(app.config["UPLOAD_FOLDER"], filename)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8050)
|
||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
flask
|
||||
requests
|
||||
beautifulsoup4
|
||||
Loading…
Reference in New Issue
Block a user