227 lines
8.3 KiB
Python
227 lines
8.3 KiB
Python
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
|
|
from nbconvert import HTMLExporter
|
|
import mimetypes
|
|
from dotenv import load_dotenv
|
|
|
|
# Load environment variables from .env file
|
|
load_dotenv()
|
|
|
|
app = Flask(__name__)
|
|
|
|
# Configuration from environment variables
|
|
app.config.update({
|
|
'SHARED_DIRECTORY': os.environ.get('JUPYTERHUB_SHARED_DIR', '/shared'),
|
|
'HOST': os.environ.get('FLASK_HOST', '0.0.0.0'),
|
|
'PORT': int(os.environ.get('FLASK_PORT', 5000)),
|
|
'DEBUG': os.environ.get('FLASK_DEBUG', 'True').lower() == 'true',
|
|
'SECRET_KEY': os.environ.get('FLASK_SECRET_KEY', 'dev-key-change-in-production'),
|
|
'MAX_CONTENT_LENGTH': int(os.environ.get('MAX_FILE_SIZE', 16 * 1024 * 1024)), # 16MB default
|
|
'NOTEBOOKS_PER_PAGE': int(os.environ.get('NOTEBOOKS_PER_PAGE', 50)),
|
|
'ALLOWED_EXTENSIONS': os.environ.get('ALLOWED_EXTENSIONS', '.ipynb').split(','),
|
|
'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'),
|
|
})
|
|
|
|
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': os.path.join(dir, relative_path),
|
|
'full_path': 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}")
|
|
|
|
# Limit results based on configuration
|
|
notebooks = sorted(notebooks, key=lambda x: x['modified'], reverse=True)
|
|
return notebooks[:app.config['NOTEBOOKS_PER_PAGE']]
|
|
|
|
def get_directory_structure(directory):
|
|
"""Get directory structure for navigation"""
|
|
structure = []
|
|
try:
|
|
for item in os.listdir(directory):
|
|
item_path = os.path.join(directory, item)
|
|
if os.path.isdir(item_path):
|
|
structure.append({
|
|
'name': item,
|
|
'type': 'directory',
|
|
'path': os.path.relpath(item_path, app.config['SHARED_DIRECTORY'])
|
|
})
|
|
except (OSError, PermissionError):
|
|
pass
|
|
|
|
return sorted(structure, key=lambda x: x['name'])
|
|
|
|
def convert_notebook_to_html(notebook_path):
|
|
"""Convert Jupyter notebook to HTML"""
|
|
try:
|
|
with open(notebook_path, 'r', encoding='utf-8') as f:
|
|
notebook = nbformat.read(f, as_version=4)
|
|
|
|
html_exporter = HTMLExporter()
|
|
html_exporter.template_name = 'classic'
|
|
|
|
(body, resources) = html_exporter.from_notebook_node(notebook)
|
|
return body
|
|
except Exception as e:
|
|
return f"<div class='alert alert-danger'>Error converting notebook: {str(e)}</div>"
|
|
|
|
def get_breadcrumbs(current_dir):
|
|
breadcrumbs = []
|
|
if current_dir:
|
|
parts = current_dir.split(os.sep)
|
|
for i, part in enumerate(parts):
|
|
breadcrumbs.append({
|
|
'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,
|
|
directories=directories,
|
|
current_dir=current_dir,
|
|
breadcrumbs=breadcrumbs,
|
|
config=app.config)
|
|
|
|
@app.route('/view/<path:notebook_path>')
|
|
def view_notebook(notebook_path):
|
|
"""View a specific notebook"""
|
|
full_path = os.path.join(app.config['SHARED_DIRECTORY'], notebook_path)
|
|
|
|
if not os.path.exists(full_path):
|
|
abort(404)
|
|
|
|
# Check if file has allowed extension
|
|
if not any(full_path.endswith(ext) for ext in app.config['ALLOWED_EXTENSIONS']):
|
|
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,
|
|
breadcrumbs=breadcrumbs,
|
|
current_dir=notebook_path,
|
|
config=app.config)
|
|
|
|
@app.route('/download/<path:notebook_path>')
|
|
def download_notebook(notebook_path):
|
|
"""Download a notebook file"""
|
|
if not app.config['ENABLE_DOWNLOAD']:
|
|
abort(403, "Downloads are disabled")
|
|
|
|
full_path = os.path.join(app.config['SHARED_DIRECTORY'], notebook_path)
|
|
|
|
if not os.path.exists(full_path):
|
|
abort(404)
|
|
|
|
# Check if file has allowed extension
|
|
if not any(full_path.endswith(ext) for ext in app.config['ALLOWED_EXTENSIONS']):
|
|
abort(403)
|
|
|
|
# Security check
|
|
if os.path.commonpath([full_path, app.config['SHARED_DIRECTORY']]) == app.config['SHARED_DIRECTORY']:
|
|
abort(403)
|
|
|
|
return send_file(full_path, as_attachment=True)
|
|
|
|
@app.route('/api/notebooks')
|
|
def api_notebooks():
|
|
"""API endpoint to get notebooks as JSON"""
|
|
if not app.config['ENABLE_API']:
|
|
abort(403, "API is disabled")
|
|
|
|
current_dir = request.args.get('dir', '')
|
|
full_current_dir = os.path.join(app.config['SHARED_DIRECTORY'], current_dir)
|
|
|
|
notebooks = get_notebook_files(full_current_dir)
|
|
directories = get_directory_structure(full_current_dir)
|
|
|
|
return jsonify({
|
|
'notebooks': notebooks,
|
|
'directories': directories,
|
|
'current_dir': current_dir,
|
|
'config': {
|
|
'app_title': app.config['APP_TITLE'],
|
|
'enable_download': app.config['ENABLE_DOWNLOAD'],
|
|
'notebooks_per_page': app.config['NOTEBOOKS_PER_PAGE']
|
|
}
|
|
})
|
|
|
|
@app.errorhandler(404)
|
|
def not_found(error):
|
|
return render_template('error.html',
|
|
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",
|
|
current_dir="403"), 403
|
|
|
|
@app.errorhandler(500)
|
|
def server_error(error):
|
|
return render_template('error.html',
|
|
error_code=500,
|
|
error_message="Internal server error",
|
|
current_dir="500"), 500
|
|
|
|
if __name__ == '__main__':
|
|
# Add datetime filter
|
|
@app.template_filter('datetime')
|
|
def datetime_filter(timestamp):
|
|
from datetime import datetime
|
|
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']}")
|
|
print(f"Server running on http://{app.config['HOST']}:{app.config['PORT']}")
|
|
print(f"Debug mode: {app.config['DEBUG']}")
|
|
print(f"Downloads enabled: {app.config['ENABLE_DOWNLOAD']}")
|
|
print(f"API enabled: {app.config['ENABLE_API']}")
|
|
|
|
app.run(
|
|
debug=app.config['DEBUG'],
|
|
host=app.config['HOST'],
|
|
port=app.config['PORT']
|
|
)
|