bug des volumes utilisateurs
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -4,5 +4,8 @@
|
|||||||
/jupyterhub_volumes/jupyterhub
|
/jupyterhub_volumes/jupyterhub
|
||||||
/jupyterhub_volumes/caddy
|
/jupyterhub_volumes/caddy
|
||||||
/**/.DS_Store
|
/**/.DS_Store
|
||||||
|
/web_src/**/*.RData
|
||||||
|
/web_src/**/*.pdf
|
||||||
|
/web_src/**/*_files
|
||||||
|
/web_src/**/*_cache
|
||||||
/.luarc.json
|
/.luarc.json
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 217 KiB After Width: | Height: | Size: 350 KiB |
@@ -30,6 +30,7 @@ RUN apt-get update && apt-get install -y \
|
|||||||
r-base \
|
r-base \
|
||||||
libcurl4-openssl-dev libssl-dev libxml2-dev \
|
libcurl4-openssl-dev libssl-dev libxml2-dev \
|
||||||
curl \
|
curl \
|
||||||
|
git \
|
||||||
texlive-xetex texlive-fonts-recommended texlive-plain-generic \
|
texlive-xetex texlive-fonts-recommended texlive-plain-generic \
|
||||||
ruby ruby-dev \
|
ruby ruby-dev \
|
||||||
vim nano \
|
vim nano \
|
||||||
@@ -37,9 +38,16 @@ RUN apt-get update && apt-get install -y \
|
|||||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
# Installer R et packages
|
# Installer R et packages
|
||||||
RUN R -e "install.packages(c('IRkernel','tidyverse','vegan','ade4','BiocManager','remotes'), repos='http://cran.rstudio.com/')" && \
|
RUN R -e "install.packages(c('IRkernel','tidyverse','vegan','ade4','BiocManager','remotes','igraph'), \
|
||||||
|
dependencies=TRUE, \
|
||||||
|
repos='http://cran.rstudio.com/')" && \
|
||||||
R -e "BiocManager::install('biomformat')" && \
|
R -e "BiocManager::install('biomformat')" && \
|
||||||
R -e "remotes::install_github('metabaRfactory/metabaR')" && \
|
R -e "remotes::install_github('metabaRfactory/metabaR')" && \
|
||||||
|
R -e "remotes::install_git('https://forge.metabarcoding.org/obitools/ROBIUtils.git')" && \
|
||||||
|
R -e "remotes::install_git('https://forge.metabarcoding.org/obitools/ROBITaxonomy.git')" && \
|
||||||
|
R -e "remotes::install_git('https://forge.metabarcoding.org/obitools/ROBITools.git')" && \
|
||||||
|
R -e "remotes::install_git('https://forge.metabarcoding.org/obitools/ROBITaxonomy.git')" && \
|
||||||
|
R -e "remotes::install_git('https://forge.metabarcoding.org/MetabarcodingSchool/biodiversity-metrics.git')" && \
|
||||||
R -e "IRkernel::installspec(user = FALSE)" && \
|
R -e "IRkernel::installspec(user = FALSE)" && \
|
||||||
rm -rf /tmp/Rtmp*
|
rm -rf /tmp/Rtmp*
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ services:
|
|||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
# JupyterHub database persistence
|
# JupyterHub database persistence
|
||||||
- data:/srv/jupyterhub
|
- data:/srv/jupyterhub
|
||||||
# The Jupyter user volumes
|
# Mount the parent volumes directory to access users/, shared/, course/
|
||||||
- users:/volumes
|
- ../jupyterhub_volumes:/volumes
|
||||||
# Mount config file directly (for easy modifications)
|
# Mount config file directly (for easy modifications)
|
||||||
- ./jupyterhub_config.py:/srv/jupyterhub/jupyterhub_config.py:ro
|
- ./jupyterhub_config.py:/srv/jupyterhub/jupyterhub_config.py:ro
|
||||||
networks:
|
networks:
|
||||||
@@ -27,6 +27,8 @@ services:
|
|||||||
JUPYTERHUB_ADMIN_PASSWORD: admin2025
|
JUPYTERHUB_ADMIN_PASSWORD: admin2025
|
||||||
# Optional environment variables
|
# Optional environment variables
|
||||||
DOCKER_NOTEBOOK_DIR: /home/jovyan/work
|
DOCKER_NOTEBOOK_DIR: /home/jovyan/work
|
||||||
|
# Use PWD to get absolute path relative to docker-compose.yml location
|
||||||
|
HOST_VOLUMES_PATH: ${PWD}/../jupyterhub_volumes
|
||||||
|
|
||||||
# ---------- Nginx ----------
|
# ---------- Nginx ----------
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ c.JupyterHub.spawner_class = 'dockerspawner.DockerSpawner'
|
|||||||
c.JupyterHub.log_level = 'DEBUG'
|
c.JupyterHub.log_level = 'DEBUG'
|
||||||
c.Spawner.debug = True
|
c.Spawner.debug = True
|
||||||
|
|
||||||
VOLUMES_BASE_PATH = '/volumes'
|
VOLUMES_BASE_PATH = '/volumes/users' # Path as seen from JupyterHub container (for user dirs)
|
||||||
|
HOST_VOLUMES_PATH = os.environ.get('HOST_VOLUMES_PATH', '/volumes') # Real path on host machine (parent dir)
|
||||||
|
|
||||||
# Docker image to use for student containers
|
# Docker image to use for student containers
|
||||||
c.DockerSpawner.image = 'jupyterhub-student:latest'
|
c.DockerSpawner.image = 'jupyterhub-student:latest'
|
||||||
@@ -52,31 +53,28 @@ c.DockerSpawner.notebook_dir = notebook_dir
|
|||||||
async def create_user_dir(spawner):
|
async def create_user_dir(spawner):
|
||||||
"""Create user directory if it doesn't exist"""
|
"""Create user directory if it doesn't exist"""
|
||||||
user_dir = os.path.join(VOLUMES_BASE_PATH, spawner.user.name)
|
user_dir = os.path.join(VOLUMES_BASE_PATH, spawner.user.name)
|
||||||
spawner.log.info(f"Ensured user directory exists: {user_dir}")
|
|
||||||
Path(user_dir).mkdir(parents=True, exist_ok=True)
|
Path(user_dir).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Change owner to jovyan user (UID 1000, GID 100 in Jupyter images)
|
||||||
|
os.chown(user_dir, 1000, 100)
|
||||||
os.chmod(user_dir, 0o755)
|
os.chmod(user_dir, 0o755)
|
||||||
|
|
||||||
|
spawner.log.info(f"Created user directory with correct permissions: {user_dir}")
|
||||||
|
|
||||||
c.Spawner.pre_spawn_hook = create_user_dir
|
c.Spawner.pre_spawn_hook = create_user_dir
|
||||||
|
|
||||||
c.DockerSpawner.volumes = {
|
c.DockerSpawner.volumes = {
|
||||||
# Personal volume (persistent) - root directory
|
# Personal volume - bind mount from REAL host path
|
||||||
'obijupyterhub_shared-{username}' : '/home/jovyan/work',
|
f'{HOST_VOLUMES_PATH}/users/{{username}}': '/home/jovyan/work',
|
||||||
# Shared volume between all students - under work/
|
# Shared volume between all students - under work/
|
||||||
'obijupyterhub_shared': '/home/jovyan/work/shared',
|
f'{HOST_VOLUMES_PATH}/shared': '/home/jovyan/work/shared',
|
||||||
# Shared read-only volume for course files - under work/
|
# Shared read-only volume for course files - under work/
|
||||||
'obijupyterhub_course': {
|
f'{HOST_VOLUMES_PATH}/course': {
|
||||||
'bind': '/home/jovyan/work/course',
|
'bind': '/home/jovyan/work/course',
|
||||||
'mode': 'ro' # read-only
|
'mode': 'ro' # read-only
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.DockerSpawner.volume_driver = 'local'
|
|
||||||
c.DockerSpawner.volume_driver_opts = {
|
|
||||||
'type': 'none',
|
|
||||||
'device': '/volumes',
|
|
||||||
'o': 'bind'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Memory and CPU configuration (adjust according to your needs)
|
# Memory and CPU configuration (adjust according to your needs)
|
||||||
c.DockerSpawner.mem_limit = '2G'
|
c.DockerSpawner.mem_limit = '2G'
|
||||||
c.DockerSpawner.cpu_limit = 1.0
|
c.DockerSpawner.cpu_limit = 1.0
|
||||||
|
|||||||
@@ -49,18 +49,27 @@ docker ps -aq --filter name=jupyter- | xargs -r docker rm -f 2>/dev/null || true
|
|||||||
# Build student image
|
# Build student image
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}🔨 Building student image...${NC}"
|
echo -e "${BLUE}🔨 Building student image...${NC}"
|
||||||
docker build -t jupyterhub-student:latest -f Dockerfile .
|
#docker build -t jupyterhub-student:latest -f Dockerfile .
|
||||||
|
|
||||||
# Build hub image
|
# Build hub image
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}🔨 Building JupyterHub image...${NC}"
|
echo -e "${BLUE}🔨 Building JupyterHub image...${NC}"
|
||||||
docker build -t jupyterhub-hub:latest -f Dockerfile.hub .
|
#docker build -t jupyterhub-hub:latest -f Dockerfile.hub .
|
||||||
|
|
||||||
# Compile the web site
|
# Compile the web site
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}🔨 Building web site...${NC}"
|
echo -e "${BLUE}🔨 Building web site...${NC}"
|
||||||
pushd ../web_src
|
pushd ../web_src
|
||||||
quarto render
|
quarto render
|
||||||
|
find . -name '*.pdf' -print \
|
||||||
|
| while read pdfname ; do
|
||||||
|
dest="../jupyterhub_volumes/web/pages/${pdfname}"
|
||||||
|
dirdest=$(dirname "$dest")
|
||||||
|
mkdir -p "$dirdest"
|
||||||
|
echo "cp '${pdfname}' '${dest}'"
|
||||||
|
done \
|
||||||
|
| bash
|
||||||
|
python3 ../tools/generate_pdf_galleries.py
|
||||||
python3 ../tools/generate_pages_json.py
|
python3 ../tools/generate_pages_json.py
|
||||||
popd
|
popd
|
||||||
|
|
||||||
|
|||||||
170
tools/generate_pdf_galleries.py
Normal file
170
tools/generate_pdf_galleries.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate lightweight HTML galleries for PDF files within PAGES_DIR."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import html
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Iterable, List, Tuple
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
|
PAGES_DIR = (SCRIPT_DIR / ".." / "jupyterhub_volumes" / "web" / "pages").resolve()
|
||||||
|
|
||||||
|
GALLERY_TEMPLATE = """<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{title}</title>
|
||||||
|
<style>
|
||||||
|
:root {{
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
color: #222;
|
||||||
|
background: #f8f8f8;
|
||||||
|
}}
|
||||||
|
body {{
|
||||||
|
margin: 2rem;
|
||||||
|
}}
|
||||||
|
h1 {{
|
||||||
|
font-size: 1.4rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}}
|
||||||
|
.gallery {{
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}}
|
||||||
|
.pdf-card {{
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
box-shadow: 0 1px 4px rgb(15 15 15 / 12%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}}
|
||||||
|
.pdf-card:hover {{
|
||||||
|
box-shadow: 0 4px 12px rgb(15 15 15 / 18%);
|
||||||
|
}}
|
||||||
|
.pdf-card object {{
|
||||||
|
width: 150px;
|
||||||
|
height: 200px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fafafa;
|
||||||
|
pointer-events: none;
|
||||||
|
}}
|
||||||
|
.pdf-fallback {{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #777;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}}
|
||||||
|
.pdf-label {{
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{title}</h1>
|
||||||
|
<div class="gallery">
|
||||||
|
{cards}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
ITEM_TEMPLATE = """ <a class="pdf-card" href="{link_href}" target="_blank" rel="noopener">
|
||||||
|
<object data="{object_src}" type="application/pdf" aria-label="{label}">
|
||||||
|
<div class="pdf-fallback">
|
||||||
|
<span>{label}</span>
|
||||||
|
<small>Prévisualisation indisponible</small>
|
||||||
|
</div>
|
||||||
|
</object>
|
||||||
|
<span class="pdf-label">{label}</span>
|
||||||
|
</a>"""
|
||||||
|
|
||||||
|
|
||||||
|
def clean_label(entry_name: str) -> str:
|
||||||
|
"""Remove numeric prefixes and make a readable label."""
|
||||||
|
base = Path(entry_name).stem
|
||||||
|
base = re.sub(r"^\d+[_\-\s]*", "", base)
|
||||||
|
base = base.replace("_", " ").replace("-", " ")
|
||||||
|
base = re.sub(r"\s+", " ", base).strip()
|
||||||
|
return base.capitalize() if base else Path(entry_name).stem
|
||||||
|
|
||||||
|
|
||||||
|
def should_skip_dir(dirname: str) -> bool:
|
||||||
|
"""Return True if we should ignore the directory while walking."""
|
||||||
|
return dirname.startswith(".") or dirname.endswith("libs") or dirname == "__pycache__"
|
||||||
|
|
||||||
|
|
||||||
|
def iter_pdf_directories(root: Path) -> Iterable[Tuple[Path, List[str]]]:
|
||||||
|
"""Yield directories in root containing at least one PDF file."""
|
||||||
|
for current_dir, dirnames, filenames in os.walk(root):
|
||||||
|
dirnames[:] = [name for name in dirnames if not should_skip_dir(name)]
|
||||||
|
pdfs = sorted(name for name in filenames if name.lower().endswith(".pdf"))
|
||||||
|
if pdfs:
|
||||||
|
yield Path(current_dir), pdfs
|
||||||
|
|
||||||
|
|
||||||
|
def directory_label(directory: Path) -> str:
|
||||||
|
"""Return a human-readable label for the given directory."""
|
||||||
|
try:
|
||||||
|
relative = directory.relative_to(PAGES_DIR)
|
||||||
|
except ValueError:
|
||||||
|
relative = directory
|
||||||
|
if not relative.parts:
|
||||||
|
return "Documents"
|
||||||
|
return " / ".join(clean_label(part) for part in relative.parts)
|
||||||
|
|
||||||
|
|
||||||
|
def build_gallery_html(directory: Path, pdf_files: List[str]) -> str:
|
||||||
|
"""Create the HTML content for all PDFs in the directory."""
|
||||||
|
cards = []
|
||||||
|
for filename in pdf_files:
|
||||||
|
label = html.escape(clean_label(filename))
|
||||||
|
href = quote(filename)
|
||||||
|
object_src = f"{href}#page=1&view=Fit"
|
||||||
|
cards.append(
|
||||||
|
ITEM_TEMPLATE.format(
|
||||||
|
link_href=html.escape(href, quote=True),
|
||||||
|
object_src=html.escape(object_src, quote=True),
|
||||||
|
label=label,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
title = html.escape(directory_label(directory))
|
||||||
|
return GALLERY_TEMPLATE.format(title=title, cards="\n".join(cards))
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
if not PAGES_DIR.exists():
|
||||||
|
raise SystemExit(f"❌ PAGES_DIR inexistant: {PAGES_DIR}")
|
||||||
|
|
||||||
|
generated = 0
|
||||||
|
for directory, pdf_files in iter_pdf_directories(PAGES_DIR):
|
||||||
|
html_content = build_gallery_html(directory, pdf_files)
|
||||||
|
output_path = directory / "document.html"
|
||||||
|
output_path.write_text(html_content, encoding="utf-8")
|
||||||
|
rel_out = output_path.relative_to(PAGES_DIR)
|
||||||
|
print(f"✅ {rel_out} ({len(pdf_files)} PDF)")
|
||||||
|
generated += 1
|
||||||
|
|
||||||
|
if not generated:
|
||||||
|
print("ℹ️ Aucun PDF trouvé, aucun document.html généré.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user