diff --git a/.env b/.env
index 104d37f..2840c46 100644
--- a/.env
+++ b/.env
@@ -1,14 +1,14 @@
# JupyterHub Notebook Viewer Configuration
# Core Settings
-JUPYTERHUB_SHARED_DIR=/shared
-APP_TITLE=JupyterHub Notebook Viewer
+JUPYTERHUB_SHARED_DIR=./shared
+APP_TITLE="IFN Shared HUB"
# Flask Settings
FLASK_HOST=0.0.0.0
-FLASK_PORT=5000
+FLASK_PORT=5001
FLASK_DEBUG=True
-FLASK_SECRET_KEY=your-secret-key-change-in-production
+FLASK_SECRET_KEY="hoeuhfou0a9ufm08fwncznf0aauuf"
# File Handling
MAX_FILE_SIZE=16777216
@@ -19,5 +19,3 @@ ALLOWED_EXTENSIONS=.ipynb,.py,.md
ENABLE_DOWNLOAD=True
ENABLE_API=True
-# UI Settings
-THEME=dark
diff --git a/.env.example b/.env.example
index 3555705..7f59b17 100644
--- a/.env.example
+++ b/.env.example
@@ -19,6 +19,3 @@ ALLOWED_EXTENSIONS=.ipynb,.py,.md
# Feature Toggles
ENABLE_DOWNLOAD=True
ENABLE_API=True
-
-# UI Settings
-THEME=dark # dark or light
diff --git a/__pycache__/app.cpython-311.pyc b/__pycache__/app.cpython-311.pyc
new file mode 100644
index 0000000..60d1027
Binary files /dev/null and b/__pycache__/app.cpython-311.pyc differ
diff --git a/app.py b/app.py
index 3aea62c..ddf6c2e 100644
--- a/app.py
+++ b/app.py
@@ -1,5 +1,6 @@
import os
import json
+from datetime import datetime
from pathlib import Path
from flask import Flask, render_template, request, jsonify, send_file, abort
import nbformat
@@ -25,26 +26,27 @@ app.config.update({
'ENABLE_DOWNLOAD': os.environ.get('ENABLE_DOWNLOAD', 'True').lower() == 'true',
'ENABLE_API': os.environ.get('ENABLE_API', 'True').lower() == 'true',
'APP_TITLE': os.environ.get('APP_TITLE', 'JupyterHub Notebook Viewer'),
- 'THEME': os.environ.get('THEME', 'dark'), # dark or light
})
def get_notebook_files(directory):
"""Recursively get all notebook files from directory"""
notebooks = []
allowed_extensions = app.config['ALLOWED_EXTENSIONS']
-
+ dir = request.args.get('dir', '')
+ print(dir)
try:
for root, dirs, files in os.walk(directory):
for file in files:
if any(file.endswith(ext) for ext in allowed_extensions):
full_path = os.path.join(root, file)
relative_path = os.path.relpath(full_path, directory)
+ mtime = datetime.fromtimestamp(os.path.getmtime(full_path)).strftime("%d.%m.%Y %H:%M")
notebooks.append({
'name': file,
- 'path': relative_path,
+ 'path': os.path.join(dir, relative_path),
'full_path': full_path,
- 'size': os.path.getsize(full_path),
- 'modified': os.path.getmtime(full_path)
+ 'size': round(os.path.getsize(full_path) / 1024, 2),
+ 'modified': mtime
})
except (OSError, PermissionError) as 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:
return f"
Error converting notebook: {str(e)}
"
-@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)
-
- # Breadcrumb navigation
+def get_breadcrumbs(current_dir):
breadcrumbs = []
if current_dir:
parts = current_dir.split(os.sep)
@@ -105,6 +95,21 @@ def index():
'name': part,
'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',
notebooks=notebooks,
@@ -125,17 +130,15 @@ def view_notebook(notebook_path):
if not any(full_path.endswith(ext) for ext in app.config['ALLOWED_EXTENSIONS']):
abort(403)
- # Security check - ensure path is within shared directory
- if not os.path.commonpath([full_path, app.config['SHARED_DIRECTORY']]) == app.config['SHARED_DIRECTORY']:
- abort(403)
-
+ breadcrumbs = get_breadcrumbs(notebook_path)
html_content = convert_notebook_to_html(full_path)
-
return render_template('notebook.html',
- html_content=html_content,
- notebook_name=os.path.basename(notebook_path),
- notebook_path=notebook_path,
- config=app.config)
+ html_content=html_content,
+ notebook_name=os.path.basename(notebook_path),
+ notebook_path=notebook_path,
+ breadcrumbs=breadcrumbs,
+ current_dir=notebook_path,
+ config=app.config)
@app.route('/download/')
def download_notebook(notebook_path):
@@ -153,7 +156,7 @@ def download_notebook(notebook_path):
abort(403)
# 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)
return send_file(full_path, as_attachment=True)
@@ -184,247 +187,30 @@ def api_notebooks():
@app.errorhandler(404)
def not_found(error):
return render_template('error.html',
- error_code=404,
- error_message="Notebook or directory not found"), 404
+ error_code=404,
+ error_message="Notebook or directory not found",
+ current_dir="404"), 404
@app.errorhandler(403)
def forbidden(error):
return render_template('error.html',
- error_code=403,
- error_message="Access forbidden"), 403
+ error_code=403,
+ error_message="Access forbidden",
+ current_dir="403"), 403
@app.errorhandler(500)
def server_error(error):
- return render_template('error.html',
- error_code=500,
- error_message="Internal server error"), 500
+ return render_template('error.html',
+ error_code=500,
+ error_message="Internal server error",
+ current_dir="500"), 500
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'''
-
-
-
-
- {{{{ config.APP_TITLE }}}}
-
-
-
-
-
-
-
-
- {{% if breadcrumbs %}}
-
- {{% endif %}}
-
-
- {{% if current_dir %}}
- Notebooks in {{{{ current_dir }}}}
- {{% else %}}
- All Notebooks
- {{% endif %}}
-
-
-
- {{% if directories %}}
-
-
-
Directories
- {{% for dir in directories %}}
-
- {{% endfor %}}
-
-
- {{% endif %}}
-
-
-
-
-
Notebooks
- {{% if notebooks %}}
- {{% for notebook in notebooks %}}
-
-
-
-
-
- {{{{ notebook.name }}}}
-
-
-
- Size: {{{{ "%.1f"|format(notebook.size/1024) }}}} KB |
- Modified: {{{{ notebook.modified|int|datetime }}}}
-
-
-
-
-
- View
-
- {{% if config.ENABLE_DOWNLOAD %}}
-
- Download
-
- {{% endif %}}
-
-
-
-
- {{% endfor %}}
- {{% if notebooks|length == config.NOTEBOOKS_PER_PAGE %}}
-
- Showing first {{{{ config.NOTEBOOKS_PER_PAGE }}}} notebooks. Configure NOTEBOOKS_PER_PAGE to show more.
-
- {{% endif %}}
- {{% else %}}
-
- No notebooks found in this directory.
-
- {{% endif %}}
-
-
-
-
-
-
-'''
-
- notebook_template = f'''
-
-
-
-
- {{{{ notebook_name }}}} - {{{{ config.APP_TITLE }}}}
-
-
-
-
-
-
-
-
-
-
-
- {{{{ notebook_name }}}}
-
-
- {{{{ html_content|safe }}}}
-
-
-
-
-
-
-
-'''
-
- error_template = f'''
-
-
-
-
- Error {{{{ error_code }}}} - {{{{ config.APP_TITLE if config else 'JupyterHub Notebook Viewer' }}}}
-
-
-
-
-
-
-
-
-
-
-
-
Error {{{{ error_code }}}}
-
{{{{ error_message }}}}
-
- Go Home
-
-
-
-
-
-
-'''
-
- # 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
@app.template_filter('datetime')
def datetime_filter(timestamp):
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"Shared directory: {app.config['SHARED_DIRECTORY']}")
diff --git a/flake.nix b/flake.nix
index c5c3696..ca28533 100644
--- a/flake.nix
+++ b/flake.nix
@@ -85,7 +85,7 @@
netcat
# Process management
- supervisor
+ #supervisor
];
# Shell script for easy setup
diff --git a/src/app.py b/src/app.py
new file mode 100644
index 0000000..5aea239
--- /dev/null
+++ b/src/app.py
@@ -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)
diff --git a/src/static/style.css b/src/static/style.css
new file mode 100644
index 0000000..ad4feb3
--- /dev/null
+++ b/src/static/style.css
@@ -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;
+}
diff --git a/src/templates/_breadcrumbs.html b/src/templates/_breadcrumbs.html
new file mode 100644
index 0000000..2bd3e99
--- /dev/null
+++ b/src/templates/_breadcrumbs.html
@@ -0,0 +1,10 @@
+
+ {% for crumb in breadcrumbs %}
+ {% if not crumb.is_active %}
+
{{ crumb.name }}
+
/
+ {% else %}
+
{{ crumb.name }}
+ {% endif %}
+ {% endfor %}
+
diff --git a/src/templates/index.html b/src/templates/index.html
new file mode 100644
index 0000000..34c28ad
--- /dev/null
+++ b/src/templates/index.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+ {{ app_title }}
+
+
+
+
+
+
+ All Notebooks
+
+ {% include '_breadcrumbs.html' %}
+ This is a basic Flask application.
+
+
+
+
diff --git a/static/style.css b/static/style.css
new file mode 100644
index 0000000..670bd5a
--- /dev/null
+++ b/static/style.css
@@ -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;
+}
diff --git a/templates/_breadcrumb.html b/templates/_breadcrumb.html
new file mode 100644
index 0000000..b7c9e2b
--- /dev/null
+++ b/templates/_breadcrumb.html
@@ -0,0 +1,12 @@
+
+
diff --git a/templates/_folder.html b/templates/_folder.html
new file mode 100644
index 0000000..b457937
--- /dev/null
+++ b/templates/_folder.html
@@ -0,0 +1,7 @@
+
diff --git a/templates/_navbar.html b/templates/_navbar.html
new file mode 100644
index 0000000..c1234f8
--- /dev/null
+++ b/templates/_navbar.html
@@ -0,0 +1,19 @@
+
diff --git a/templates/_notebook.html b/templates/_notebook.html
new file mode 100644
index 0000000..4632039
--- /dev/null
+++ b/templates/_notebook.html
@@ -0,0 +1,21 @@
+
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..1f94ca3
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+ {{ config.APP_TITLE }}
+
+
+ {% include '_navbar.html' %}
+
+
+ {% include '_breadcrumb.html' %}
+ {% block content %}
+ {% endblock %}
+
+
+
diff --git a/templates/error.html b/templates/error.html
new file mode 100644
index 0000000..13b4fe4
--- /dev/null
+++ b/templates/error.html
@@ -0,0 +1,9 @@
+{% extends "base.html" %}
+{% block content %}
+
+
+
Error {{error_code}}
+ {{error_message}}
+
+{% endblock %}
+
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..e69502d
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,27 @@
+{% extends "base.html" %}
+{% block content %}
+
+ {% if directories %}
+
+
Directories
+
+ {% for dir in directories %}
+ {% include '_folder.html' %}
+ {% endfor %}
+
+
+
+
+ {% endif %}
+
+
+ {% if notebooks %}
+
+
Notebooks
+ {% for notebook in notebooks %}
+ {% include '_notebook.html' %}
+ {% endfor %}
+
+ {% endif %}
+{% endblock %}
+
diff --git a/templates/notebook.html b/templates/notebook.html
new file mode 100644
index 0000000..2c7d905
--- /dev/null
+++ b/templates/notebook.html
@@ -0,0 +1,14 @@
+{% extends 'base.html' %}
+{% block content %}
+
+ {{ html_content|safe }}
+{% endblock %}