added Stufff
This commit is contained in:
parent
f1fe4aa05a
commit
d9b50ac103
10
.env
10
.env
@ -1,14 +1,14 @@
|
|||||||
# JupyterHub Notebook Viewer Configuration
|
# JupyterHub Notebook Viewer Configuration
|
||||||
|
|
||||||
# Core Settings
|
# Core Settings
|
||||||
JUPYTERHUB_SHARED_DIR=/shared
|
JUPYTERHUB_SHARED_DIR=./shared
|
||||||
APP_TITLE=JupyterHub Notebook Viewer
|
APP_TITLE="IFN Shared HUB"
|
||||||
|
|
||||||
# Flask Settings
|
# Flask Settings
|
||||||
FLASK_HOST=0.0.0.0
|
FLASK_HOST=0.0.0.0
|
||||||
FLASK_PORT=5000
|
FLASK_PORT=5001
|
||||||
FLASK_DEBUG=True
|
FLASK_DEBUG=True
|
||||||
FLASK_SECRET_KEY=your-secret-key-change-in-production
|
FLASK_SECRET_KEY="hoeuhfou0a9ufm08fwncznf0aauuf"
|
||||||
|
|
||||||
# File Handling
|
# File Handling
|
||||||
MAX_FILE_SIZE=16777216
|
MAX_FILE_SIZE=16777216
|
||||||
@ -19,5 +19,3 @@ ALLOWED_EXTENSIONS=.ipynb,.py,.md
|
|||||||
ENABLE_DOWNLOAD=True
|
ENABLE_DOWNLOAD=True
|
||||||
ENABLE_API=True
|
ENABLE_API=True
|
||||||
|
|
||||||
# UI Settings
|
|
||||||
THEME=dark
|
|
||||||
|
@ -19,6 +19,3 @@ ALLOWED_EXTENSIONS=.ipynb,.py,.md
|
|||||||
# Feature Toggles
|
# Feature Toggles
|
||||||
ENABLE_DOWNLOAD=True
|
ENABLE_DOWNLOAD=True
|
||||||
ENABLE_API=True
|
ENABLE_API=True
|
||||||
|
|
||||||
# UI Settings
|
|
||||||
THEME=dark # dark or light
|
|
||||||
|
BIN
__pycache__/app.cpython-311.pyc
Normal file
BIN
__pycache__/app.cpython-311.pyc
Normal file
Binary file not shown.
282
app.py
282
app.py
@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from flask import Flask, render_template, request, jsonify, send_file, abort
|
from flask import Flask, render_template, request, jsonify, send_file, abort
|
||||||
import nbformat
|
import nbformat
|
||||||
@ -25,26 +26,27 @@ app.config.update({
|
|||||||
'ENABLE_DOWNLOAD': os.environ.get('ENABLE_DOWNLOAD', 'True').lower() == 'true',
|
'ENABLE_DOWNLOAD': os.environ.get('ENABLE_DOWNLOAD', 'True').lower() == 'true',
|
||||||
'ENABLE_API': os.environ.get('ENABLE_API', 'True').lower() == 'true',
|
'ENABLE_API': os.environ.get('ENABLE_API', 'True').lower() == 'true',
|
||||||
'APP_TITLE': os.environ.get('APP_TITLE', 'JupyterHub Notebook Viewer'),
|
'APP_TITLE': os.environ.get('APP_TITLE', 'JupyterHub Notebook Viewer'),
|
||||||
'THEME': os.environ.get('THEME', 'dark'), # dark or light
|
|
||||||
})
|
})
|
||||||
|
|
||||||
def get_notebook_files(directory):
|
def get_notebook_files(directory):
|
||||||
"""Recursively get all notebook files from directory"""
|
"""Recursively get all notebook files from directory"""
|
||||||
notebooks = []
|
notebooks = []
|
||||||
allowed_extensions = app.config['ALLOWED_EXTENSIONS']
|
allowed_extensions = app.config['ALLOWED_EXTENSIONS']
|
||||||
|
dir = request.args.get('dir', '')
|
||||||
|
print(dir)
|
||||||
try:
|
try:
|
||||||
for root, dirs, files in os.walk(directory):
|
for root, dirs, files in os.walk(directory):
|
||||||
for file in files:
|
for file in files:
|
||||||
if any(file.endswith(ext) for ext in allowed_extensions):
|
if any(file.endswith(ext) for ext in allowed_extensions):
|
||||||
full_path = os.path.join(root, file)
|
full_path = os.path.join(root, file)
|
||||||
relative_path = os.path.relpath(full_path, directory)
|
relative_path = os.path.relpath(full_path, directory)
|
||||||
|
mtime = datetime.fromtimestamp(os.path.getmtime(full_path)).strftime("%d.%m.%Y %H:%M")
|
||||||
notebooks.append({
|
notebooks.append({
|
||||||
'name': file,
|
'name': file,
|
||||||
'path': relative_path,
|
'path': os.path.join(dir, relative_path),
|
||||||
'full_path': full_path,
|
'full_path': full_path,
|
||||||
'size': os.path.getsize(full_path),
|
'size': round(os.path.getsize(full_path) / 1024, 2),
|
||||||
'modified': os.path.getmtime(full_path)
|
'modified': mtime
|
||||||
})
|
})
|
||||||
except (OSError, PermissionError) as e:
|
except (OSError, PermissionError) as e:
|
||||||
app.logger.error(f"Error accessing directory {directory}: {e}")
|
app.logger.error(f"Error accessing directory {directory}: {e}")
|
||||||
@ -84,19 +86,7 @@ def convert_notebook_to_html(notebook_path):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"<div class='alert alert-danger'>Error converting notebook: {str(e)}</div>"
|
return f"<div class='alert alert-danger'>Error converting notebook: {str(e)}</div>"
|
||||||
|
|
||||||
@app.route('/')
|
def get_breadcrumbs(current_dir):
|
||||||
def index():
|
|
||||||
"""Main page showing all notebooks"""
|
|
||||||
current_dir = request.args.get('dir', '')
|
|
||||||
full_current_dir = os.path.join(app.config['SHARED_DIRECTORY'], current_dir)
|
|
||||||
|
|
||||||
if not os.path.exists(full_current_dir):
|
|
||||||
abort(404)
|
|
||||||
|
|
||||||
notebooks = get_notebook_files(full_current_dir)
|
|
||||||
directories = get_directory_structure(full_current_dir)
|
|
||||||
|
|
||||||
# Breadcrumb navigation
|
|
||||||
breadcrumbs = []
|
breadcrumbs = []
|
||||||
if current_dir:
|
if current_dir:
|
||||||
parts = current_dir.split(os.sep)
|
parts = current_dir.split(os.sep)
|
||||||
@ -105,6 +95,21 @@ def index():
|
|||||||
'name': part,
|
'name': part,
|
||||||
'path': os.sep.join(parts[:i+1])
|
'path': os.sep.join(parts[:i+1])
|
||||||
})
|
})
|
||||||
|
return breadcrumbs
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
"""Main page showing all notebooks"""
|
||||||
|
current_dir = request.args.get('dir', '')
|
||||||
|
full_current_dir = os.path.join(app.config['SHARED_DIRECTORY'], current_dir)
|
||||||
|
if not os.path.exists(full_current_dir):
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
notebooks = get_notebook_files(full_current_dir)
|
||||||
|
directories = get_directory_structure(full_current_dir)
|
||||||
|
print(directories)
|
||||||
|
# Breadcrumb navigation
|
||||||
|
breadcrumbs = get_breadcrumbs(current_dir)
|
||||||
|
|
||||||
return render_template('index.html',
|
return render_template('index.html',
|
||||||
notebooks=notebooks,
|
notebooks=notebooks,
|
||||||
@ -125,16 +130,14 @@ def view_notebook(notebook_path):
|
|||||||
if not any(full_path.endswith(ext) for ext in app.config['ALLOWED_EXTENSIONS']):
|
if not any(full_path.endswith(ext) for ext in app.config['ALLOWED_EXTENSIONS']):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
# Security check - ensure path is within shared directory
|
breadcrumbs = get_breadcrumbs(notebook_path)
|
||||||
if not os.path.commonpath([full_path, app.config['SHARED_DIRECTORY']]) == app.config['SHARED_DIRECTORY']:
|
|
||||||
abort(403)
|
|
||||||
|
|
||||||
html_content = convert_notebook_to_html(full_path)
|
html_content = convert_notebook_to_html(full_path)
|
||||||
|
|
||||||
return render_template('notebook.html',
|
return render_template('notebook.html',
|
||||||
html_content=html_content,
|
html_content=html_content,
|
||||||
notebook_name=os.path.basename(notebook_path),
|
notebook_name=os.path.basename(notebook_path),
|
||||||
notebook_path=notebook_path,
|
notebook_path=notebook_path,
|
||||||
|
breadcrumbs=breadcrumbs,
|
||||||
|
current_dir=notebook_path,
|
||||||
config=app.config)
|
config=app.config)
|
||||||
|
|
||||||
@app.route('/download/<path:notebook_path>')
|
@app.route('/download/<path:notebook_path>')
|
||||||
@ -153,7 +156,7 @@ def download_notebook(notebook_path):
|
|||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
# Security check
|
# Security check
|
||||||
if not os.path.commonpath([full_path, app.config['SHARED_DIRECTORY']]) == app.config['SHARED_DIRECTORY']:
|
if os.path.commonpath([full_path, app.config['SHARED_DIRECTORY']]) == app.config['SHARED_DIRECTORY']:
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
return send_file(full_path, as_attachment=True)
|
return send_file(full_path, as_attachment=True)
|
||||||
@ -185,246 +188,29 @@ def api_notebooks():
|
|||||||
def not_found(error):
|
def not_found(error):
|
||||||
return render_template('error.html',
|
return render_template('error.html',
|
||||||
error_code=404,
|
error_code=404,
|
||||||
error_message="Notebook or directory not found"), 404
|
error_message="Notebook or directory not found",
|
||||||
|
current_dir="404"), 404
|
||||||
|
|
||||||
@app.errorhandler(403)
|
@app.errorhandler(403)
|
||||||
def forbidden(error):
|
def forbidden(error):
|
||||||
return render_template('error.html',
|
return render_template('error.html',
|
||||||
error_code=403,
|
error_code=403,
|
||||||
error_message="Access forbidden"), 403
|
error_message="Access forbidden",
|
||||||
|
current_dir="403"), 403
|
||||||
|
|
||||||
@app.errorhandler(500)
|
@app.errorhandler(500)
|
||||||
def server_error(error):
|
def server_error(error):
|
||||||
return render_template('error.html',
|
return render_template('error.html',
|
||||||
error_code=500,
|
error_code=500,
|
||||||
error_message="Internal server error"), 500
|
error_message="Internal server error",
|
||||||
|
current_dir="500"), 500
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# Create templates directory if it doesn't exist
|
|
||||||
os.makedirs('templates', exist_ok=True)
|
|
||||||
|
|
||||||
# Determine theme classes
|
|
||||||
navbar_class = "navbar-dark bg-dark" if app.config['THEME'] == 'dark' else "navbar-light bg-light"
|
|
||||||
|
|
||||||
# Basic templates
|
|
||||||
index_template = f'''<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>{{{{ config.APP_TITLE }}}}</title>
|
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<nav class="navbar {navbar_class}">
|
|
||||||
<div class="container">
|
|
||||||
<a class="navbar-brand" href="{{{{ url_for('index') }}}}">
|
|
||||||
<i class="fas fa-book"></i> {{{{ config.APP_TITLE }}}}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="container mt-4">
|
|
||||||
<!-- Breadcrumb Navigation -->
|
|
||||||
{{% if breadcrumbs %}}
|
|
||||||
<nav aria-label="breadcrumb">
|
|
||||||
<ol class="breadcrumb">
|
|
||||||
<li class="breadcrumb-item"><a href="{{{{ url_for('index') }}}}">Home</a></li>
|
|
||||||
{{% for crumb in breadcrumbs %}}
|
|
||||||
<li class="breadcrumb-item">
|
|
||||||
<a href="{{{{ url_for('index', dir=crumb.path) }}}}">{{{{ crumb.name }}}}</a>
|
|
||||||
</li>
|
|
||||||
{{% endfor %}}
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
{{% endif %}}
|
|
||||||
|
|
||||||
<h1 class="mb-4">
|
|
||||||
{{% if current_dir %}}
|
|
||||||
Notebooks in {{{{ current_dir }}}}
|
|
||||||
{{% else %}}
|
|
||||||
All Notebooks
|
|
||||||
{{% endif %}}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<!-- Directories -->
|
|
||||||
{{% if directories %}}
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<h3><i class="fas fa-folder"></i> Directories</h3>
|
|
||||||
{{% for dir in directories %}}
|
|
||||||
<div class="card mb-2">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">
|
|
||||||
<a href="{{{{ url_for('index', dir=dir.path) }}}}" class="text-decoration-none">
|
|
||||||
<i class="fas fa-folder text-warning"></i> {{{{ dir.name }}}}
|
|
||||||
</a>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{% endfor %}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{% endif %}}
|
|
||||||
|
|
||||||
<!-- Notebooks -->
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<h3><i class="fas fa-file-code"></i> Notebooks</h3>
|
|
||||||
{{% if notebooks %}}
|
|
||||||
{{% for notebook in notebooks %}}
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row align-items-center">
|
|
||||||
<div class="col-md-8">
|
|
||||||
<h5 class="card-title">
|
|
||||||
<i class="fas fa-file-code text-primary"></i> {{{{ notebook.name }}}}
|
|
||||||
</h5>
|
|
||||||
<p class="card-text text-muted">
|
|
||||||
<small>
|
|
||||||
Size: {{{{ "%.1f"|format(notebook.size/1024) }}}} KB |
|
|
||||||
Modified: {{{{ notebook.modified|int|datetime }}}}
|
|
||||||
</small>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 text-end">
|
|
||||||
<a href="{{{{ url_for('view_notebook', notebook_path=notebook.path) }}}}"
|
|
||||||
class="btn btn-primary btn-sm me-2">
|
|
||||||
<i class="fas fa-eye"></i> View
|
|
||||||
</a>
|
|
||||||
{{% if config.ENABLE_DOWNLOAD %}}
|
|
||||||
<a href="{{{{ url_for('download_notebook', notebook_path=notebook.path) }}}}"
|
|
||||||
class="btn btn-secondary btn-sm">
|
|
||||||
<i class="fas fa-download"></i> Download
|
|
||||||
</a>
|
|
||||||
{{% endif %}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{% endfor %}}
|
|
||||||
{{% if notebooks|length == config.NOTEBOOKS_PER_PAGE %}}
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<i class="fas fa-info-circle"></i> Showing first {{{{ config.NOTEBOOKS_PER_PAGE }}}} notebooks. Configure NOTEBOOKS_PER_PAGE to show more.
|
|
||||||
</div>
|
|
||||||
{{% endif %}}
|
|
||||||
{{% else %}}
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<i class="fas fa-info-circle"></i> No notebooks found in this directory.
|
|
||||||
</div>
|
|
||||||
{{% endif %}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>'''
|
|
||||||
|
|
||||||
notebook_template = f'''<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>{{{{ notebook_name }}}} - {{{{ config.APP_TITLE }}}}</title>
|
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
.notebook-content {{
|
|
||||||
max-width: 100%;
|
|
||||||
overflow-x: auto;
|
|
||||||
}}
|
|
||||||
.notebook-content img {{
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<nav class="navbar {navbar_class}">
|
|
||||||
<div class="container">
|
|
||||||
<a class="navbar-brand" href="{{{{ url_for('index') }}}}">
|
|
||||||
<i class="fas fa-book"></i> {{{{ config.APP_TITLE }}}}
|
|
||||||
</a>
|
|
||||||
<div>
|
|
||||||
{{% if config.ENABLE_DOWNLOAD %}}
|
|
||||||
<a href="{{{{ url_for('download_notebook', notebook_path=notebook_path) }}}}"
|
|
||||||
class="btn btn-outline-light btn-sm">
|
|
||||||
<i class="fas fa-download"></i> Download
|
|
||||||
</a>
|
|
||||||
{{% endif %}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="container-fluid mt-4">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<h2 class="mb-3">
|
|
||||||
<i class="fas fa-file-code text-primary"></i> {{{{ notebook_name }}}}
|
|
||||||
</h2>
|
|
||||||
<div class="notebook-content">
|
|
||||||
{{{{ html_content|safe }}}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>'''
|
|
||||||
|
|
||||||
error_template = f'''<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Error {{{{ error_code }}}} - {{{{ config.APP_TITLE if config else 'JupyterHub Notebook Viewer' }}}}</title>
|
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<nav class="navbar {navbar_class}">
|
|
||||||
<div class="container">
|
|
||||||
<a class="navbar-brand" href="{{{{ url_for('index') }}}}">
|
|
||||||
<i class="fas fa-book"></i> {{{{ config.APP_TITLE if config else 'JupyterHub Notebook Viewer' }}}}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="container mt-5">
|
|
||||||
<div class="row justify-content-center">
|
|
||||||
<div class="col-md-6 text-center">
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
<h1><i class="fas fa-exclamation-triangle"></i></h1>
|
|
||||||
<h2>Error {{{{ error_code }}}}</h2>
|
|
||||||
<p>{{{{ error_message }}}}</p>
|
|
||||||
<a href="{{{{ url_for('index') }}}}" class="btn btn-primary">
|
|
||||||
<i class="fas fa-home"></i> Go Home
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>'''
|
|
||||||
|
|
||||||
# Write templates
|
|
||||||
with open('templates/index.html', 'w') as f:
|
|
||||||
f.write(index_template)
|
|
||||||
|
|
||||||
with open('templates/notebook.html', 'w') as f:
|
|
||||||
f.write(notebook_template)
|
|
||||||
|
|
||||||
with open('templates/error.html', 'w') as f:
|
|
||||||
f.write(error_template)
|
|
||||||
|
|
||||||
# Add datetime filter
|
# Add datetime filter
|
||||||
@app.template_filter('datetime')
|
@app.template_filter('datetime')
|
||||||
def datetime_filter(timestamp):
|
def datetime_filter(timestamp):
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
return datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S')
|
return datetime.fromtimestamp(timestamp).strftime('%d.%m.%Y %H:%M:%S')
|
||||||
|
|
||||||
print(f"Starting {app.config['APP_TITLE']}")
|
print(f"Starting {app.config['APP_TITLE']}")
|
||||||
print(f"Shared directory: {app.config['SHARED_DIRECTORY']}")
|
print(f"Shared directory: {app.config['SHARED_DIRECTORY']}")
|
||||||
|
@ -85,7 +85,7 @@
|
|||||||
netcat
|
netcat
|
||||||
|
|
||||||
# Process management
|
# Process management
|
||||||
supervisor
|
#supervisor
|
||||||
];
|
];
|
||||||
|
|
||||||
# Shell script for easy setup
|
# Shell script for easy setup
|
||||||
|
43
src/app.py
Normal file
43
src/app.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
from flask import Flask, render_template, request
|
||||||
|
import os
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
app_title = os.getenv('APP_TITLE', 'JupyShare')
|
||||||
|
|
||||||
|
|
||||||
|
def get_breadcrumbs():
|
||||||
|
path_parts = request.path.split('/')[1:] # Remove empty first element
|
||||||
|
breadcrumbs = []
|
||||||
|
accumulated_path = ''
|
||||||
|
|
||||||
|
for i, part in enumerate(path_parts):
|
||||||
|
if not part:
|
||||||
|
continue
|
||||||
|
accumulated_path += f'/{part}'
|
||||||
|
breadcrumbs.append({
|
||||||
|
'name': part.capitalize(),
|
||||||
|
'url': accumulated_path,
|
||||||
|
'is_active': (i == len(path_parts) - 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Always include home at the beginning
|
||||||
|
if not breadcrumbs or breadcrumbs[0]['name'].lower() != 'home':
|
||||||
|
breadcrumbs.insert(0, {
|
||||||
|
'name': 'Home',
|
||||||
|
'url': '/',
|
||||||
|
'is_active': len(path_parts) == 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return breadcrumbs
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def home():
|
||||||
|
print(get_breadcrumbs())
|
||||||
|
return render_template('index.html',
|
||||||
|
app_title=app_title,
|
||||||
|
breadcrumbs=get_breadcrumbs()
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(debug=True)
|
79
src/static/style.css
Normal file
79
src/static/style.css
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 80px auto 0;
|
||||||
|
padding: 20px;
|
||||||
|
border: 2px solid red;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
background-color: #333;
|
||||||
|
color: white;
|
||||||
|
padding: 15px 0;
|
||||||
|
position: fixed;
|
||||||
|
width: 100vw;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Breadcrumbs */
|
||||||
|
.breadcrumbs {
|
||||||
|
padding: 15px 0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-link {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-separator {
|
||||||
|
margin: 0 5px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-current {
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
10
src/templates/_breadcrumbs.html
Normal file
10
src/templates/_breadcrumbs.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<div class="breadcrumbs">
|
||||||
|
{% for crumb in breadcrumbs %}
|
||||||
|
{% if not crumb.is_active %}
|
||||||
|
<a href="{{ crumb.url }}" class="breadcrumb-link">{{ crumb.name }}</a>
|
||||||
|
<span class="breadcrumb-separator">/</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="breadcrumb-current">{{ crumb.name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
24
src/templates/index.html
Normal file
24
src/templates/index.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ app_title }}</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar">
|
||||||
|
<div class="nav-container">
|
||||||
|
<a href="{{ url_for('home') }}" class="logo">{{ app_title }} </a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<h1>All Notebooks</h1>
|
||||||
|
|
||||||
|
{% include '_breadcrumbs.html' %}
|
||||||
|
<p>This is a basic Flask application.</p>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
233
static/style.css
Normal file
233
static/style.css
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
/* Reset defaults */
|
||||||
|
.body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
max-width: 80vw;
|
||||||
|
margin: 20px auto;
|
||||||
|
padding: 15px;
|
||||||
|
padding-top: 0px;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: #ffD43b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.15s ease-in-out;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a:hover {
|
||||||
|
color: #0056b3;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: 10px 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navbar */
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar img {
|
||||||
|
height: 68px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-middle {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-right {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Breadcrum */
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item:not(:last-child) + .breadcrumb-item::before {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
color: #6c757d;
|
||||||
|
content: "→";
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item:last-child {
|
||||||
|
color: #495057;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item.active {
|
||||||
|
color: #6c757d;
|
||||||
|
pointer-events: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav[aria-label="breadcrumb"] {
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shared */
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row h3 {
|
||||||
|
padding: 0 15px;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row a {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card */
|
||||||
|
.card-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #6c757d;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 10px;
|
||||||
|
padding: 15px 25px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-stats p {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: right;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3 ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button i {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-button {
|
||||||
|
background-color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-button {
|
||||||
|
background-color: grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error */
|
||||||
|
.error {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error strong {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error i {
|
||||||
|
color: red;
|
||||||
|
font-size: 8rem;
|
||||||
|
}
|
12
templates/_breadcrumb.html
Normal file
12
templates/_breadcrumb.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{{ url_for('index') }}" class="a">Home</a></li>
|
||||||
|
{% for crumb in breadcrumbs %}
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{{ url_for('index', dir=crumb.path) }}" class="a">{{ crumb.name }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
<hr>
|
||||||
|
</nav>
|
||||||
|
|
7
templates/_folder.html
Normal file
7
templates/_folder.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<div class="card-container">
|
||||||
|
<div class="card">
|
||||||
|
<a href="{{ url_for('index', dir=dir.path) }}" class="a">
|
||||||
|
<i class="fas fa-folder"></i> {{ dir.name }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
19
templates/_navbar.html
Normal file
19
templates/_navbar.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<nav class="navbar">
|
||||||
|
<div class="navbar-container">
|
||||||
|
<a href="{{ url_for('index') }}">
|
||||||
|
<img src="https://www.tu-braunschweig.de/fileadmin/Logos_Einrichtungen/Institute_FK5/logo_IFN.svg"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="navbar-middle">
|
||||||
|
|
||||||
|
<h1>
|
||||||
|
{% if current_dir %}
|
||||||
|
{{ current_dir }}
|
||||||
|
{% else %}
|
||||||
|
All Notebooks
|
||||||
|
{% endif %}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
21
templates/_notebook.html
Normal file
21
templates/_notebook.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<div class="card-container">
|
||||||
|
<div class="card">
|
||||||
|
<a href="{{ url_for('view_notebook', notebook_path=notebook.path) }}" class="a">
|
||||||
|
<i class="fas fa-file-code"></i> {{ notebook.name }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<div class="card-stats">
|
||||||
|
<p>Last Modified: <strong>{{ notebook.modified }}</strong></p>
|
||||||
|
<p>Size: <strong>{{ notebook.size }} KB</strong></p>
|
||||||
|
</div>
|
||||||
|
{% if config.ENABLE_DOWNLOAD %}
|
||||||
|
<a href="{{ url_for('download_notebook', notebook_path=notebook.path) }}" class="button download-button">
|
||||||
|
<i class="fas fa-download"></i> Download
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
19
templates/base.html
Normal file
19
templates/base.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"/>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||||
|
<title>{{ config.APP_TITLE }}</title>
|
||||||
|
</head>
|
||||||
|
<body class="body">
|
||||||
|
{% include '_navbar.html' %}
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
{% include '_breadcrumb.html' %}
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
9
templates/error.html
Normal file
9
templates/error.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="error">
|
||||||
|
<i class="fas fa-triangle-exclamation"></i>
|
||||||
|
<h1>Error {{error_code}}</h1>
|
||||||
|
<strong>{{error_message}}</strong>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
27
templates/index.html
Normal file
27
templates/index.html
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<!-- Directories -->
|
||||||
|
{% if directories %}
|
||||||
|
<div class="row">
|
||||||
|
<h3><i class="fas fa-folder"></i> Directories</h3>
|
||||||
|
|
||||||
|
{% for dir in directories %}
|
||||||
|
{% include '_folder.html' %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Notebooks -->
|
||||||
|
{% if notebooks %}
|
||||||
|
<div class="row">
|
||||||
|
<h3><i class="fas fa-file-code"></i> Notebooks</h3>
|
||||||
|
{% for notebook in notebooks %}
|
||||||
|
{% include '_notebook.html' %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
14
templates/notebook.html
Normal file
14
templates/notebook.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="notebook-header">
|
||||||
|
<h2>
|
||||||
|
<i class="fas fa-file-code text-primary"></i> {{ notebook_name }}
|
||||||
|
</h2>
|
||||||
|
{% if config.ENABLE_DOWNLOAD %}
|
||||||
|
<a href="{{ url_for('download_notebook', notebook_path=notebook_path) }}" class="a button download-button">
|
||||||
|
<i class="fas fa-download"></i> Download
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{{ html_content|safe }}
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user