SHA256
Updated to work a lot better.
This commit is contained in:
@@ -8,11 +8,12 @@ import subprocess
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from flask import Flask, render_template, request, jsonify
|
from flask import Flask, render_template, request, jsonify, send_file
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
from apscheduler.triggers.cron import CronTrigger
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
import markdown
|
import markdown
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from whoosh.index import create_in, open_dir
|
from whoosh.index import create_in, open_dir
|
||||||
from whoosh.fields import Schema, TEXT, ID
|
from whoosh.fields import Schema, TEXT, ID
|
||||||
from whoosh.qparser import QueryParser
|
from whoosh.qparser import QueryParser
|
||||||
@@ -48,6 +49,88 @@ def create_search_index():
|
|||||||
|
|
||||||
ix = create_search_index()
|
ix = create_search_index()
|
||||||
|
|
||||||
|
def is_hidden_path(file_path):
|
||||||
|
"""Check if a file path contains any hidden directories or files (starting with dot)."""
|
||||||
|
path_obj = Path(file_path) if isinstance(file_path, str) else file_path
|
||||||
|
# Check all parts of the path including the filename
|
||||||
|
return any(part.startswith('.') for part in path_obj.parts)
|
||||||
|
|
||||||
|
def rewrite_image_paths(md_content, repo, filepath):
|
||||||
|
"""Rewrite relative image paths to be served by Flask."""
|
||||||
|
# Get the directory of the current file (relative to repo root)
|
||||||
|
file_dir = os.path.dirname(filepath)
|
||||||
|
|
||||||
|
# Pattern to match markdown image syntax: 
|
||||||
|
def replace_image_path(match):
|
||||||
|
alt_text = match.group(1)
|
||||||
|
img_path = match.group(2)
|
||||||
|
|
||||||
|
# Skip absolute URLs and data URIs
|
||||||
|
if img_path.startswith(('http://', 'https://', 'data:')):
|
||||||
|
return match.group(0)
|
||||||
|
|
||||||
|
# Resolve relative paths
|
||||||
|
if img_path.startswith('/'):
|
||||||
|
# Absolute path from repo root
|
||||||
|
resolved_path = img_path.lstrip('/')
|
||||||
|
else:
|
||||||
|
# Relative path - resolve it relative to the file's directory
|
||||||
|
if file_dir:
|
||||||
|
resolved_path = os.path.normpath(os.path.join(file_dir, img_path))
|
||||||
|
else:
|
||||||
|
resolved_path = img_path
|
||||||
|
|
||||||
|
# Ensure forward slashes for URL
|
||||||
|
resolved_path = resolved_path.replace(os.sep, '/')
|
||||||
|
image_url = f"/image/{repo}/{resolved_path}"
|
||||||
|
|
||||||
|
return f""
|
||||||
|
|
||||||
|
# Replace all markdown image references
|
||||||
|
md_content = re.sub(r'!\[([^\]]*)\]\(([^\)]+)\)', replace_image_path, md_content)
|
||||||
|
return md_content
|
||||||
|
|
||||||
|
def build_directory_tree(repo_path):
|
||||||
|
"""Build a hierarchical tree of markdown files organized by directory."""
|
||||||
|
tree = {}
|
||||||
|
|
||||||
|
if not os.path.exists(repo_path):
|
||||||
|
logger.warning(f"Repo path does not exist: {repo_path}")
|
||||||
|
return tree
|
||||||
|
|
||||||
|
file_count = 0
|
||||||
|
for md_file in Path(repo_path).rglob('*'):
|
||||||
|
if md_file.suffix.lower() not in MARKDOWN_EXTENSIONS:
|
||||||
|
continue
|
||||||
|
if is_hidden_path(md_file):
|
||||||
|
logger.debug(f"Skipping hidden path: {md_file}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
file_count += 1
|
||||||
|
rel_path = md_file.relative_to(repo_path)
|
||||||
|
parts = rel_path.parts[:-1] # All parts except filename
|
||||||
|
filename = md_file.stem # Name without extension
|
||||||
|
|
||||||
|
logger.debug(f"Adding to tree: {rel_path} (name: {filename})")
|
||||||
|
|
||||||
|
# Navigate/create nested dict structure
|
||||||
|
current = tree
|
||||||
|
for part in parts:
|
||||||
|
if part not in current:
|
||||||
|
current[part] = {}
|
||||||
|
current = current[part]
|
||||||
|
|
||||||
|
# Add file to current level
|
||||||
|
if '_files' not in current:
|
||||||
|
current['_files'] = []
|
||||||
|
current['_files'].append({
|
||||||
|
'name': filename,
|
||||||
|
'path': str(rel_path)
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(f"Built tree for {repo_path}: found {file_count} markdown files")
|
||||||
|
return tree
|
||||||
|
|
||||||
def load_repositories():
|
def load_repositories():
|
||||||
"""Load repository configuration from file."""
|
"""Load repository configuration from file."""
|
||||||
if not os.path.exists(CONFIG_FILE):
|
if not os.path.exists(CONFIG_FILE):
|
||||||
@@ -115,7 +198,7 @@ def update_search_index():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
for md_file in repo_dir.rglob('*'):
|
for md_file in repo_dir.rglob('*'):
|
||||||
if md_file.suffix.lower() in MARKDOWN_EXTENSIONS:
|
if md_file.suffix.lower() in MARKDOWN_EXTENSIONS and not is_hidden_path(md_file):
|
||||||
try:
|
try:
|
||||||
content = md_file.read_text(encoding='utf-8', errors='ignore')
|
content = md_file.read_text(encoding='utf-8', errors='ignore')
|
||||||
# Extract title from filename or first heading
|
# Extract title from filename or first heading
|
||||||
@@ -181,24 +264,43 @@ def index():
|
|||||||
|
|
||||||
for repo_name in repos.keys():
|
for repo_name in repos.keys():
|
||||||
repo_path = os.path.join(REPOS_DIR, repo_name)
|
repo_path = os.path.join(REPOS_DIR, repo_name)
|
||||||
markdown_files = []
|
file_tree = build_directory_tree(repo_path)
|
||||||
|
|
||||||
if os.path.exists(repo_path):
|
|
||||||
for md_file in Path(repo_path).rglob('*'):
|
|
||||||
if md_file.suffix.lower() in MARKDOWN_EXTENSIONS:
|
|
||||||
rel_path = str(md_file.relative_to(repo_path))
|
|
||||||
markdown_files.append({
|
|
||||||
'name': md_file.stem,
|
|
||||||
'path': rel_path
|
|
||||||
})
|
|
||||||
|
|
||||||
repo_list.append({
|
repo_list.append({
|
||||||
'name': repo_name,
|
'name': repo_name,
|
||||||
'files': sorted(markdown_files, key=lambda x: x['path'])
|
'tree': file_tree
|
||||||
})
|
})
|
||||||
|
|
||||||
return render_template('index.html', repositories=repo_list)
|
return render_template('index.html', repositories=repo_list)
|
||||||
|
|
||||||
|
@app.route('/image/<repo>/<path:filepath>')
|
||||||
|
def serve_image(repo, filepath):
|
||||||
|
"""Serve images from repository directories."""
|
||||||
|
# Security: prevent directory traversal
|
||||||
|
if '..' in filepath or filepath.startswith('/'):
|
||||||
|
return "Invalid path", 400
|
||||||
|
|
||||||
|
file_path = os.path.join(REPOS_DIR, repo, filepath)
|
||||||
|
|
||||||
|
# Ensure the file is within the repo directory
|
||||||
|
try:
|
||||||
|
file_path = os.path.realpath(file_path)
|
||||||
|
repo_path = os.path.realpath(os.path.join(REPOS_DIR, repo))
|
||||||
|
if not file_path.startswith(repo_path):
|
||||||
|
return "Access denied", 403
|
||||||
|
except Exception:
|
||||||
|
return "Invalid path", 400
|
||||||
|
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
logger.warning(f"Image not found: {file_path}")
|
||||||
|
return "Image not found", 404
|
||||||
|
|
||||||
|
try:
|
||||||
|
return send_file(file_path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error serving image {file_path}: {e}")
|
||||||
|
return f"Error serving image: {e}", 500
|
||||||
|
|
||||||
@app.route('/api/search', methods=['GET'])
|
@app.route('/api/search', methods=['GET'])
|
||||||
def search():
|
def search():
|
||||||
"""Search markdown files."""
|
"""Search markdown files."""
|
||||||
@@ -249,6 +351,9 @@ def view_file(repo, filepath):
|
|||||||
with open(file_path, 'r', encoding='utf-8') as f:
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
md_content = f.read()
|
md_content = f.read()
|
||||||
|
|
||||||
|
# Rewrite image paths to be served by Flask
|
||||||
|
md_content = rewrite_image_paths(md_content, repo, filepath)
|
||||||
|
|
||||||
# Convert markdown to HTML
|
# Convert markdown to HTML
|
||||||
html_content = markdown.markdown(
|
html_content = markdown.markdown(
|
||||||
md_content,
|
md_content,
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
{
|
{
|
||||||
"my-wiki": {
|
"Biscuits": {
|
||||||
"url": "https://github.com/username/my-wiki.git",
|
"url": "https://git.biscuitsdiscgolf.com/biscuits/markmycontent.git",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"schedule": "0 */6 * * *"
|
"schedule": "0 */6 * * *"
|
||||||
},
|
|
||||||
"project-docs": {
|
|
||||||
"url": "https://github.com/username/project-docs.git",
|
|
||||||
"enabled": true,
|
|
||||||
"schedule": "0 9 * * *"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
# Optional override file for local development
|
# Optional override file for local development
|
||||||
# Copy to docker-compose.override.yml and customize
|
# Copy to docker-compose.override.yml and customize
|
||||||
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
markmywords:
|
markmywords:
|
||||||
# Enable debug mode
|
# Enable debug mode
|
||||||
|
|||||||
+2
-14
@@ -1,5 +1,3 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
markmywords:
|
markmywords:
|
||||||
build: .
|
build: .
|
||||||
@@ -8,22 +6,12 @@ services:
|
|||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
volumes:
|
volumes:
|
||||||
# Configuration directory with repositories.json
|
# Configuration directory with repositories.json
|
||||||
- ./config:/config:ro
|
- ./config:/config:rw
|
||||||
# Data directory for cloned repositories and search index
|
# Data directory for cloned repositories and search index
|
||||||
- markmywords_data:/data
|
- ./data:/data:rw
|
||||||
environment:
|
environment:
|
||||||
# Set Flask environment
|
# Set Flask environment
|
||||||
FLASK_ENV: production
|
FLASK_ENV: production
|
||||||
# Python unbuffered output for real-time logs
|
# Python unbuffered output for real-time logs
|
||||||
PYTHONUNBUFFERED: 1
|
PYTHONUNBUFFERED: 1
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
|
||||||
- markmywords
|
|
||||||
|
|
||||||
networks:
|
|
||||||
markmywords:
|
|
||||||
driver: bridge
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
markmywords_data:
|
|
||||||
driver: local
|
|
||||||
|
|||||||
@@ -237,6 +237,65 @@ main {
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tree View Styles */
|
||||||
|
.file-tree {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-list {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-list .tree-list {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-folder,
|
||||||
|
.tree-file {
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-toggle {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
user-select: none;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--primary-color);
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-toggle:hover {
|
||||||
|
color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
details > summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
details > summary::before {
|
||||||
|
content: "▶ ";
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open] > summary::before {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-children {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
border-left: 2px solid var(--border-color);
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 3rem;
|
padding: 3rem;
|
||||||
|
|||||||
+4
-12
@@ -28,23 +28,15 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="repositories-section">
|
<section class="repositories-section">
|
||||||
<h2>Repositories</h2>
|
<h2>Site Navigation</h2>
|
||||||
{% if repositories %}
|
{% if repositories %}
|
||||||
<div class="repo-grid">
|
<div class="repo-grid">
|
||||||
{% for repo in repositories %}
|
{% for repo in repositories %}
|
||||||
<div class="repo-card">
|
<div class="repo-card">
|
||||||
<h3>{{ repo.name }}</h3>
|
<h3>{{ repo.name }}</h3>
|
||||||
{% if repo.files %}
|
{% if repo.tree %}
|
||||||
<div class="file-list">
|
<div class="file-tree">
|
||||||
<ul>
|
{% include 'tree_render.html' %}
|
||||||
{% for file in repo.files %}
|
|
||||||
<li>
|
|
||||||
<a href="/view/{{ repo.name }}/{{ file.path }}" class="file-link">
|
|
||||||
📄 {{ file.name }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="no-files">No markdown files found</p>
|
<p class="no-files">No markdown files found</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user