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 @@ + 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 @@ +
+
+ + {{ notebook.name }} + + +
+
+

Last Modified: {{ notebook.modified }}

+

Size: {{ notebook.size }} KB

+
+ {% if config.ENABLE_DOWNLOAD %} + + Download + + {% endif %} +
+ + +
+
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 %} +
+

+ {{ notebook_name }} +

+ {% if config.ENABLE_DOWNLOAD %} + + Download + + {% endif %} +
+ {{ html_content|safe }} +{% endblock %}