batman
This commit is contained in:
commit
42f1d1012f
18 changed files with 399 additions and 0 deletions
22
app/__init__.py
Normal file
22
app/__init__.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
from flask import Flask
|
||||
import os
|
||||
from app.config import Config
|
||||
|
||||
def create_app(config_class=Config):
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(config_class)
|
||||
|
||||
# Ensure upload directory exists
|
||||
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
||||
|
||||
# Create empty URI map file if it doesn't exist
|
||||
uri_map_path = os.path.join(app.config['UPLOAD_FOLDER'], 'uri_map.json')
|
||||
if not os.path.exists(uri_map_path):
|
||||
with open(uri_map_path, 'w') as f:
|
||||
f.write('{}')
|
||||
|
||||
# Register blueprints
|
||||
from app.routes.main import main_bp
|
||||
app.register_blueprint(main_bp)
|
||||
|
||||
return app
|
BIN
app/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/config.cpython-312.pyc
Normal file
BIN
app/__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
7
app/config.py
Normal file
7
app/config.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
import os
|
||||
|
||||
class Config:
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-key-please-change')
|
||||
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'uploads')
|
||||
DEBUG = True
|
||||
MAX_CONTENT_LENGTH = 16 * 1024 * 1024
|
0
app/routes/__init__.py
Normal file
0
app/routes/__init__.py
Normal file
BIN
app/routes/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
app/routes/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/main.cpython-312.pyc
Normal file
BIN
app/routes/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
47
app/routes/main.py
Normal file
47
app/routes/main.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
from flask import Blueprint, render_template, request, current_app, redirect, url_for, abort, flash
|
||||
import os
|
||||
from app.utils.file_handler import save_file, read_markdown_file, get_file_by_uri, load_uri_map
|
||||
|
||||
main_bp = Blueprint('main', __name__)
|
||||
|
||||
@main_bp.route('/', methods=['GET'])
|
||||
def index():
|
||||
# List all available documents
|
||||
uri_map = load_uri_map()
|
||||
documents = [{'uri': uri, 'filename': info['filename']} for uri, info in uri_map.items()]
|
||||
return render_template('index.html', documents=documents)
|
||||
|
||||
@main_bp.route('/upload', methods=['GET', 'POST'])
|
||||
def upload_file():
|
||||
if request.method == 'POST':
|
||||
if 'file' not in request.files:
|
||||
flash('No file part')
|
||||
return redirect(request.url)
|
||||
|
||||
file = request.files['file']
|
||||
|
||||
if file.filename == '':
|
||||
flash('No file selected')
|
||||
return redirect(request.url)
|
||||
|
||||
if file and file.filename.endswith('.md'):
|
||||
custom_uri = request.form.get('custom_uri', '').strip()
|
||||
uri, filename = save_file(file, custom_uri if custom_uri else None)
|
||||
return redirect(url_for('main.view_file', uri=uri))
|
||||
else:
|
||||
flash('Only markdown files are allowed')
|
||||
|
||||
return render_template('upload.html')
|
||||
|
||||
@main_bp.route('/view/<uri>')
|
||||
def view_file(uri):
|
||||
file_info = get_file_by_uri(uri)
|
||||
|
||||
if not file_info:
|
||||
abort(404)
|
||||
|
||||
html_content = read_markdown_file(file_info['path'])
|
||||
return render_template('viewer.html',
|
||||
filename=file_info['filename'],
|
||||
content=html_content,
|
||||
uri=uri)
|
91
app/templates/index.html
Normal file
91
app/templates/index.html
Normal file
|
@ -0,0 +1,91 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Markdown Viewer</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/digitallytailored/classless@latest/classless.min.css">
|
||||
<style>
|
||||
.file-list {
|
||||
margin: 2em 0;
|
||||
}
|
||||
.file-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75em;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.file-item:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.file-link {
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
.file-uri {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.copy-button {
|
||||
background: #f0f0f0;
|
||||
border: 1px solid #ddd;
|
||||
padding: 0.25em 0.5em;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3em;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
margin: 2em 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Markdown Viewer</h1>
|
||||
|
||||
<div class="actions">
|
||||
<a href="{{ url_for('main.upload_file') }}" class="button">Upload New Document</a>
|
||||
</div>
|
||||
|
||||
{% if documents %}
|
||||
<div class="file-list">
|
||||
<h2>Available Documents</h2>
|
||||
|
||||
{% for doc in documents %}
|
||||
<div class="file-item">
|
||||
<div>
|
||||
<a href="{{ url_for('main.view_file', uri=doc.uri) }}" class="file-link">
|
||||
{{ doc.filename }}
|
||||
</a>
|
||||
<span class="file-uri">URI: {{ doc.uri }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{ url_for('main.view_file', uri=doc.uri) }}" class="button">View</a>
|
||||
<button class="copy-button" onclick="copyToClipboard('{{ url_for('main.view_file', uri=doc.uri, _external=True) }}')">
|
||||
Copy Link
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<h3>No documents available</h3>
|
||||
<p>Upload a markdown file to get started!</p>
|
||||
<a href="{{ url_for('main.upload_file') }}" class="button">Upload Now</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
function copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
alert('Link copied to clipboard!');
|
||||
}, function() {
|
||||
alert('Failed to copy link');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
39
app/templates/upload.html
Normal file
39
app/templates/upload.html
Normal file
|
@ -0,0 +1,39 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Upload Markdown File</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/digitallytailored/classless@latest/classless.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Upload Markdown File</h1>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="messages">
|
||||
{% for message in messages %}
|
||||
<p class="alert">{{ message }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div>
|
||||
<label for="file">Select a markdown file:</label>
|
||||
<input type="file" name="file" id="file" accept=".md">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="custom_uri">Custom URI (optional):</label>
|
||||
<input type="text" name="custom_uri" id="custom_uri" placeholder="e.g., my-document">
|
||||
<small>Leave blank for an auto-generated URI</small>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input type="submit" value="Upload">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<a href="{{ url_for('main.index') }}">Back to Home</a>
|
||||
</body>
|
||||
</html>
|
69
app/templates/viewer.html
Normal file
69
app/templates/viewer.html
Normal file
|
@ -0,0 +1,69 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ filename }} - Markdown Viewer</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/digitallytailored/classless@latest/classless.min.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!--<link rel="stylesheet" href="github-markdown.css">-->
|
||||
<style>
|
||||
pre {
|
||||
background-color: #f5f5f5;
|
||||
padding: 1em;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.document-info {
|
||||
margin-bottom: 2em;
|
||||
padding: 1em;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.markdown-body {
|
||||
box-sizing: border-box;
|
||||
min-width: 200px;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 45px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.markdown-body {
|
||||
padding: 15px;
|
||||
}
|
||||
} </style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document-info">
|
||||
<h1>{{ filename }}</h1>
|
||||
<p>
|
||||
<strong>Permanent Link:</strong>
|
||||
<code>{{ url_for('main.view_file', uri=uri, _external=True) }}</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
{{ content|safe }}
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<a href="{{ url_for('main.upload_file') }}">Upload Another Document</a> |
|
||||
<a href="{{ url_for('main.index') }}">Home</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
0
app/utils/__init__.py
Normal file
0
app/utils/__init__.py
Normal file
BIN
app/utils/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
app/utils/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/utils/__pycache__/file_handler.cpython-312.pyc
Normal file
BIN
app/utils/__pycache__/file_handler.cpython-312.pyc
Normal file
Binary file not shown.
67
app/utils/file_handler.py
Normal file
67
app/utils/file_handler.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
import os
|
||||
import markdown
|
||||
import secrets
|
||||
from flask import current_app
|
||||
from werkzeug.utils import secure_filename
|
||||
import json
|
||||
|
||||
def get_uri_map_path():
|
||||
"""Get the path to the URI mapping file"""
|
||||
return os.path.join(current_app.config['UPLOAD_FOLDER'], 'uri_map.json')
|
||||
|
||||
def load_uri_map():
|
||||
"""Load the URI mapping from file"""
|
||||
map_path = get_uri_map_path()
|
||||
if os.path.exists(map_path):
|
||||
with open(map_path, 'r') as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
def save_uri_map(uri_map):
|
||||
"""Save the URI mapping to file"""
|
||||
with open(get_uri_map_path(), 'w') as f:
|
||||
json.dump(uri_map, f, indent=2)
|
||||
|
||||
def generate_unique_uri():
|
||||
"""Generate a unique URI for a file"""
|
||||
uri_map = load_uri_map()
|
||||
while True:
|
||||
uri = secrets.token_urlsafe(6) # Generate a short random string
|
||||
if uri not in uri_map:
|
||||
return uri
|
||||
|
||||
def save_file(file, custom_uri=None):
|
||||
"""Save the uploaded file to the upload folder and assign a URI"""
|
||||
# Secure the filename first
|
||||
filename = secure_filename(file.filename)
|
||||
|
||||
# Generate or use custom URI
|
||||
uri = custom_uri if custom_uri else generate_unique_uri()
|
||||
|
||||
# Save the file with its original name
|
||||
filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], filename)
|
||||
file.save(filepath)
|
||||
|
||||
# Update the URI map
|
||||
uri_map = load_uri_map()
|
||||
uri_map[uri] = {
|
||||
'filename': filename,
|
||||
'path': filepath
|
||||
}
|
||||
save_uri_map(uri_map)
|
||||
|
||||
return uri, filename
|
||||
|
||||
def get_file_by_uri(uri):
|
||||
"""Get file info by URI"""
|
||||
uri_map = load_uri_map()
|
||||
if uri in uri_map:
|
||||
return uri_map[uri]
|
||||
return None
|
||||
|
||||
def read_markdown_file(filepath):
|
||||
"""Read and convert markdown file to HTML"""
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
md_content = f.read()
|
||||
html_content = markdown.markdown(md_content, extensions=['tables', 'fenced_code'])
|
||||
return html_content
|
7
run.py
Normal file
7
run.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from app import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
||||
|
44
uploads/docker.md
Normal file
44
uploads/docker.md
Normal file
|
@ -0,0 +1,44 @@
|
|||
# Nice containers for beginners
|
||||
|
||||
#### **Quellen**
|
||||
|
||||
- [selfh.st](https://selfh.st/apps/) <- viele opensource projekte (einige davon dockerized)
|
||||
|
||||
## **Reverseproxies:**
|
||||
|
||||
> Hier wird zumindest einer benötigt, um SSL-Verschlüsselung und HTTPS auf den Websites zu aktivieren
|
||||
> Caddy ist zwar an sich einfacher, weil es nur eine einzige config datei hat, aber zum lernen würde ich den
|
||||
> nginxproxymanager empfehlen, da er eine kleine webui hat und man das konzept so gut lernen und verstehen kann
|
||||
|
||||
- [Nginx Reverseproxy-Manager](https://nginxproxymanager.com/guide/)
|
||||
- [Caddy](https://hub.docker.com/_/caddy/)
|
||||
|
||||
## **Dashboards:**
|
||||
|
||||
> Pick whatever you like. Homepage wird in einer config datei (yml) konfiguriert. Daher würde ich Dashy oder Flame
|
||||
> eher für den Anfang empfehlen
|
||||
|
||||
- [Dashy](https://github.com/Lissy93/dashy)
|
||||
- [Flame](https://github.com/pawelmalak/flame)
|
||||
- [Homepage](https://gethomepage.dev/)
|
||||
|
||||
## **Document-Management:**
|
||||
|
||||
> filebrowser und mydrive sind zwei sehr einfache file share services, die mit einer schönen kleinen weboberfläche kommen
|
||||
> Nextcloud ist etwas mächtiger, zwar auch deutlich hübscher und mit Android/iOS app etc. Aber eben auch etwas komplexer. Ist aber machbar.
|
||||
> PaperlessNGX ist ein Dokumentenspeichersystem, hauptsächlich für pdf dateien. Ich lasse mir dort täglich und regelmäßig aus meinen
|
||||
> Mailpostfächern die .pdf dateien die als Rechnung deklariert sind automatisch in mein system einspielen. Automatisierungstechnisch ein Traum!
|
||||
|
||||
- [filebrowser](https://github.com/hurlenko/filebrowser-docker)
|
||||
- [my-drive](https://github.com/subnub/myDrive)
|
||||
- [nextcloud](https://hub.docker.com/_/nextcloud)
|
||||
- [PaperlessNGX](https://docs.paperless-ngx.com/setup/#docker)
|
||||
- [Immich](https://immich.app/docs/install/docker-compose) <- google photos clone, für zuhause! (Bessere Suchfunktion, Personenerkennung, Geodaten auf einer Map...)
|
||||
|
||||
## **Sonstige**
|
||||
|
||||
- [Bitwarden](https://github.com/dani-garcia/vaultwarden) <- das ist vaultwarden. Bitwarden + ohne zu Zahlen ^^
|
||||
|
||||
- [Excalidraw](https://hub.docker.com/r/excalidraw/excalidraw)
|
||||
- [PDF-Tools](https://github.com/Stirling-Tools/Stirling-PDF)
|
||||
- [IT-Tools](https://github.com/CorentinTh/it-tools)
|
6
uploads/uri_map.json
Normal file
6
uploads/uri_map.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"docker": {
|
||||
"filename": "docker.md",
|
||||
"path": "/home/pika/projects/flask/markdownViewer/uploads/docker.md"
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue