Merge branch 'master' of https://gitea.centx.de/lelo/bethaus-app
This commit is contained in:
commit
1c9fe28fc1
108
analytics.py
108
analytics.py
@ -7,8 +7,11 @@ from collections import defaultdict
|
||||
import json
|
||||
import os
|
||||
import auth
|
||||
import helperfunctions as hf
|
||||
|
||||
file_access_temp = []
|
||||
folder_today = []
|
||||
folder_yesterday = []
|
||||
|
||||
app_config = auth.return_app_config()
|
||||
|
||||
@ -98,11 +101,13 @@ def parse_timestamp(ts_str):
|
||||
raise
|
||||
|
||||
def log_file_access(rel_path, filesize, mime, ip_address, user_agent, device_id, cached):
|
||||
"""Insert a file access record into the database and prune entries older than 10 minutes."""
|
||||
global file_access_temp
|
||||
# Create a timezone-aware timestamp (local time with offset)
|
||||
timestamp = datetime.now(timezone.utc).astimezone()
|
||||
iso_ts = timestamp.isoformat()
|
||||
"""Insert a file access record into the database and prune entries older than 10 minutes,
|
||||
and track today’s files separately in folder_today."""
|
||||
global file_access_temp, folder_today, folder_yesterday
|
||||
|
||||
# Create a timezone-aware timestamp
|
||||
now = datetime.now(timezone.utc).astimezone()
|
||||
iso_ts = now.isoformat()
|
||||
|
||||
# Convert the IP address to a location
|
||||
city, country = lookup_location(ip_address)
|
||||
@ -114,17 +119,99 @@ def log_file_access(rel_path, filesize, mime, ip_address, user_agent, device_id,
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (iso_ts, rel_path, filesize, mime, city, country, user_agent, device_id, cached))
|
||||
|
||||
# Remove entries older than 10 minutes using our robust parser.
|
||||
cutoff_time = datetime.now(timezone.utc).astimezone() - timedelta(minutes=10)
|
||||
# Prune temp entries older than 10 minutes
|
||||
cutoff = now - timedelta(minutes=10)
|
||||
file_access_temp[:] = [
|
||||
entry for entry in file_access_temp
|
||||
if parse_timestamp(entry[0]) >= cutoff_time
|
||||
if parse_timestamp(entry[0]) >= cutoff
|
||||
]
|
||||
|
||||
# Add the new entry at the beginning of the list
|
||||
file_access_temp.insert(0, [iso_ts, rel_path, filesize, mime, f"{city}, {country}", user_agent, device_id, cached])
|
||||
# Keep only today's entries in folder_today
|
||||
today_str = iso_ts.split('T', 1)[0]
|
||||
folder_today[:] = [
|
||||
entry for entry in folder_today
|
||||
if entry['date_str'] == today_str
|
||||
]
|
||||
|
||||
# Keep only yesterday's entries in folder_yesterday
|
||||
yesterday_str = (now - timedelta(days=1)).isoformat().split('T', 1)[0]
|
||||
folder_yesterday[:] = [
|
||||
entry for entry in folder_yesterday
|
||||
if entry['date_str'] == yesterday_str
|
||||
]
|
||||
|
||||
# If this new access is from today, record it
|
||||
# Compare the helper’s YYYY-MM-DD string to today’s ISO date string
|
||||
date_from_path = hf.extract_date_from_string(rel_path)
|
||||
|
||||
if date_from_path == today_str:
|
||||
# get just the folder part (everything before the final '/')
|
||||
folder_path = rel_path.rsplit('/', 1)[0] if '/' in rel_path else rel_path
|
||||
# only append if that folder isn't already in folder_today
|
||||
if not any(entry['rel_path'] == folder_path for entry in folder_today):
|
||||
folder_today.append({'date_str': today_str, 'rel_path': folder_path})
|
||||
|
||||
# If this new access is from yesterday, record it
|
||||
if date_from_path == yesterday_str:
|
||||
# get just the folder part (everything before the final '/')
|
||||
folder_path = rel_path.rsplit('/', 1)[0] if '/' in rel_path else rel_path
|
||||
# only append if that folder isn't already in folder_yesterday
|
||||
if not any(entry['rel_path'] == folder_path for entry in folder_yesterday):
|
||||
folder_yesterday.append({'date_str': yesterday_str, 'rel_path': folder_path})
|
||||
|
||||
# Finally, insert the new access at the top of the temp log
|
||||
file_access_temp.insert(0, [
|
||||
iso_ts,
|
||||
rel_path,
|
||||
filesize,
|
||||
mime,
|
||||
f"{city}, {country}",
|
||||
user_agent,
|
||||
device_id,
|
||||
cached
|
||||
])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def return_folder_today():
|
||||
"""
|
||||
Return only those folder_today entries whose first segment
|
||||
(up to the first '/') is in session['folders'].keys().
|
||||
"""
|
||||
valid_keys = set(session.get('folders', {}).keys())
|
||||
|
||||
filtered = []
|
||||
for entry in folder_today:
|
||||
# get the part before the first slash
|
||||
top_level = entry['rel_path'].split('/', 1)[0]
|
||||
|
||||
# include only if this segment is one of the session keys
|
||||
if top_level in valid_keys:
|
||||
filtered.append(entry)
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
def return_folder_yesterday():
|
||||
"""
|
||||
Return only those folder_yesterday entries whose first segment
|
||||
(up to the first '/') is in session['folders'].keys().
|
||||
"""
|
||||
valid_keys = set(session.get('folders', {}).keys())
|
||||
|
||||
filtered = []
|
||||
for entry in folder_yesterday:
|
||||
# get the part before the first slash
|
||||
top_level = entry['rel_path'].split('/', 1)[0]
|
||||
|
||||
# include only if this segment is one of the session keys
|
||||
if top_level in valid_keys:
|
||||
filtered.append(entry)
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
def return_file_access():
|
||||
"""Return recent file access logs from memory (the last 10 minutes)."""
|
||||
global file_access_temp
|
||||
@ -140,6 +227,7 @@ def return_file_access():
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
def songs_dashboard():
|
||||
# — SESSION & PARAM HANDLING (unchanged) —
|
||||
if 'songs_dashboard_timeframe' not in session:
|
||||
|
||||
61
app.py
61
app.py
@ -15,6 +15,7 @@ import geoip2.database
|
||||
from functools import lru_cache
|
||||
from urllib.parse import urlparse, unquote
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
from pathlib import Path
|
||||
import re
|
||||
import qrcode
|
||||
import base64
|
||||
@ -22,8 +23,10 @@ import search
|
||||
import auth
|
||||
import analytics as a
|
||||
import folder_secret_config_editor as fsce
|
||||
import helperfunctions as hf
|
||||
|
||||
app_config = auth.return_app_config()
|
||||
BASE_DIR = os.path.realpath(app_config['BASE_DIR'])
|
||||
|
||||
cache_audio = diskcache.Cache('./filecache_audio', size_limit= app_config['filecache_size_limit_audio'] * 1024**3)
|
||||
cache_image = diskcache.Cache('./filecache_image', size_limit= app_config['filecache_size_limit_image'] * 1024**3)
|
||||
@ -90,6 +93,27 @@ def get_cached_image(size):
|
||||
resized_img.save(img_byte_arr, format='PNG')
|
||||
return img_byte_arr.getvalue()
|
||||
|
||||
def check_path(access_path: str) -> Path:
|
||||
"""
|
||||
Take an absolute access_path, then ensure it lives inside BASE_DIR.
|
||||
Raises ValueError or PermissionError on failure.
|
||||
"""
|
||||
p = Path(access_path)
|
||||
if not p.is_absolute():
|
||||
raise ValueError(f"Path {access_path} is not a valid absolute path")
|
||||
|
||||
# Resolve symlinks & eliminate “..” components
|
||||
candidate = p.resolve()
|
||||
base = Path(BASE_DIR).resolve()
|
||||
|
||||
try:
|
||||
# Will raise ValueError if candidate is not under base
|
||||
candidate.relative_to(base)
|
||||
except ValueError:
|
||||
raise PermissionError(f"Access to {access_path} is forbidden")
|
||||
|
||||
return candidate
|
||||
|
||||
def list_directory_contents(directory, subpath):
|
||||
"""
|
||||
List only the immediate contents of the given directory.
|
||||
@ -220,6 +244,7 @@ def serve_sw():
|
||||
@app.route('/api/path/<path:subpath>')
|
||||
@auth.require_secret
|
||||
def api_browse(subpath):
|
||||
|
||||
if subpath == '': # root directory
|
||||
foldernames = []
|
||||
for foldername, _ in session['folders'].items():
|
||||
@ -228,7 +253,26 @@ def api_browse(subpath):
|
||||
return jsonify({
|
||||
'breadcrumbs': generate_breadcrumbs(),
|
||||
'directories': foldernames,
|
||||
'files': []
|
||||
'files': [],
|
||||
'folder_today': a.return_folder_today(),
|
||||
'folder_yesterday': a.return_folder_yesterday()
|
||||
})
|
||||
|
||||
if subpath == 'heute' or subpath == 'gestern':
|
||||
foldernames = []
|
||||
if len(a.return_folder_today()) > 0:
|
||||
for item in a.return_folder_today():
|
||||
foldernames.append({'name': item['rel_path'], 'path': item['rel_path']})
|
||||
elif len(a.return_folder_yesterday()) > 0:
|
||||
for item in a.return_folder_yesterday():
|
||||
foldernames.append({'name': item['rel_path'], 'path': item['rel_path']})
|
||||
|
||||
return jsonify({
|
||||
'breadcrumbs': generate_breadcrumbs(subpath),
|
||||
'directories': foldernames,
|
||||
'files': [],
|
||||
'folder_today': [],
|
||||
'folder_yesterday': []
|
||||
})
|
||||
|
||||
root, *relative_parts = subpath.split('/')
|
||||
@ -236,6 +280,12 @@ def api_browse(subpath):
|
||||
directory = os.path.join(base_path, *relative_parts)
|
||||
|
||||
playfile = None
|
||||
|
||||
try:
|
||||
directory = check_path(directory)
|
||||
except (ValueError, PermissionError) as e:
|
||||
return jsonify({'error': str(e)}), 403
|
||||
|
||||
# Check if the constructed directory exists.
|
||||
if not os.path.isdir(directory):
|
||||
# Assume the last segment is a filename; remove it.
|
||||
@ -254,7 +304,9 @@ def api_browse(subpath):
|
||||
response = {
|
||||
'breadcrumbs': breadcrumbs,
|
||||
'directories': directories,
|
||||
'files': files
|
||||
'files': files,
|
||||
'folder_today': a.return_folder_today(),
|
||||
'folder_yesterday': a.return_folder_yesterday()
|
||||
}
|
||||
|
||||
# If a filename was selected include it.
|
||||
@ -271,6 +323,11 @@ def serve_file(subpath):
|
||||
base_path = session['folders'].get(root)
|
||||
full_path = os.path.join(base_path or '', *relative_parts)
|
||||
|
||||
try:
|
||||
full_path = check_path(full_path)
|
||||
except (ValueError, PermissionError) as e:
|
||||
return jsonify({'error': str(e)}), 403
|
||||
|
||||
if not os.path.isfile(full_path):
|
||||
app.logger.error(f"File not found: {full_path}")
|
||||
return "File not found", 404
|
||||
|
||||
46
helperfunctions.py
Normal file
46
helperfunctions.py
Normal file
@ -0,0 +1,46 @@
|
||||
from datetime import datetime
|
||||
import re
|
||||
|
||||
def extract_date_from_string(string_with_date):
|
||||
# grab X.Y.Z where X,Y,Z are 1–4 digits
|
||||
m = re.search(r'(\d{1,4}\.\d{1,2}\.\d{1,4})', string_with_date)
|
||||
if not m:
|
||||
return None
|
||||
|
||||
date_str = m.group(1)
|
||||
parts = date_str.split('.')
|
||||
|
||||
# 1) Unambiguous “last group = YYYY”
|
||||
if len(parts) == 3 and len(parts[2]) == 4:
|
||||
fmt = '%d.%m.%Y'
|
||||
|
||||
# 2) Unambiguous “first group = YYYY”
|
||||
elif len(parts) == 3 and len(parts[0]) == 4:
|
||||
fmt = '%Y.%m.%d'
|
||||
|
||||
# 3) Ambiguous “XX.XX.XX” → prefer DD.MM.YY, fallback to YY.MM.DD
|
||||
elif len(parts) == 3 and all(len(p) == 2 for p in parts):
|
||||
# try last-group-as-year first
|
||||
try:
|
||||
dt = datetime.strptime(date_str, '%d.%m.%y')
|
||||
return dt.strftime('%Y-%m-%d')
|
||||
except ValueError:
|
||||
# fallback to first-group-as-year
|
||||
fmt = '%y.%m.%d'
|
||||
|
||||
else:
|
||||
# optional: handle ISO with dashes
|
||||
if '-' in date_str:
|
||||
try:
|
||||
dt = datetime.strptime(date_str, '%Y-%m-%d')
|
||||
return dt.strftime('%Y-%m-%d')
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
|
||||
# parse with whichever fmt we settled on
|
||||
try:
|
||||
dt = datetime.strptime(date_str, fmt)
|
||||
return dt.strftime('%Y-%m-%d')
|
||||
except ValueError:
|
||||
return None
|
||||
@ -3,6 +3,7 @@ import json
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
import re
|
||||
import helperfunctions as hf
|
||||
|
||||
SEARCH_DB_NAME = 'search.db'
|
||||
ACCESS_LOG_DB_NAME = 'access_log.db'
|
||||
@ -70,51 +71,6 @@ def get_hit_count(relative_path):
|
||||
return row["hit_count"] if row else 0
|
||||
|
||||
|
||||
def extract_date_from_string(string_with_date):
|
||||
# grab X.Y.Z where X,Y,Z are 1–4 digits
|
||||
m = re.search(r'(\d{1,4}\.\d{1,2}\.\d{1,4})', string_with_date)
|
||||
if not m:
|
||||
return None
|
||||
|
||||
date_str = m.group(1)
|
||||
parts = date_str.split('.')
|
||||
|
||||
# 1) Unambiguous “last group = YYYY”
|
||||
if len(parts) == 3 and len(parts[2]) == 4:
|
||||
fmt = '%d.%m.%Y'
|
||||
|
||||
# 2) Unambiguous “first group = YYYY”
|
||||
elif len(parts) == 3 and len(parts[0]) == 4:
|
||||
fmt = '%Y.%m.%d'
|
||||
|
||||
# 3) Ambiguous “XX.XX.XX” → prefer DD.MM.YY, fallback to YY.MM.DD
|
||||
elif len(parts) == 3 and all(len(p) == 2 for p in parts):
|
||||
# try last-group-as-year first
|
||||
try:
|
||||
dt = datetime.strptime(date_str, '%d.%m.%y')
|
||||
return dt.strftime('%Y-%m-%d')
|
||||
except ValueError:
|
||||
# fallback to first-group-as-year
|
||||
fmt = '%y.%m.%d'
|
||||
|
||||
else:
|
||||
# optional: handle ISO with dashes
|
||||
if '-' in date_str:
|
||||
try:
|
||||
dt = datetime.strptime(date_str, '%Y-%m-%d')
|
||||
return dt.strftime('%Y-%m-%d')
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
|
||||
# parse with whichever fmt we settled on
|
||||
try:
|
||||
dt = datetime.strptime(date_str, fmt)
|
||||
return dt.strftime('%Y-%m-%d')
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def updatefileindex():
|
||||
cursor = search_db.cursor()
|
||||
|
||||
@ -215,7 +171,7 @@ def updatefileindex():
|
||||
titel = None
|
||||
name = None
|
||||
|
||||
performance_date = extract_date_from_string(relative_path)
|
||||
performance_date = hf.extract_date_from_string(relative_path)
|
||||
|
||||
scanned_files.append((relative_path, foldername, entry.name, filetype, category, titel, name, performance_date, site, transcript, hit_count))
|
||||
current_keys.add((relative_path, entry.name))
|
||||
|
||||
@ -111,6 +111,12 @@ function renderContent(data) {
|
||||
contentHTML += '</div>';
|
||||
} else {
|
||||
contentHTML += '<ul>';
|
||||
if (data.breadcrumbs.length === 1 && Array.isArray(data.folder_today) && data.folder_today.length > 0) {
|
||||
contentHTML += `<li class="directory-item"><a href="#" class="directory-link" data-path="heute">📅 Heute</a></li>`;
|
||||
} else if (data.breadcrumbs.length === 1 && Array.isArray(data.folder_yesterday) && data.folder_yesterday.length > 0) {
|
||||
contentHTML += `<li class="directory-item"><a href="#" class="directory-link" data-path="gestern">📅 Gestern</a></li>`;
|
||||
}
|
||||
console.log(data.folder_today, data.folder_yesterday);
|
||||
data.directories.forEach(dir => {
|
||||
if (admin_enabled && data.breadcrumbs.length != 1 && dir.share) {
|
||||
share_link = `<a href="#" class="create-share" data-url="${dir.path}">⚙️</a>`;
|
||||
@ -213,7 +219,12 @@ function loadDirectory(subpath) {
|
||||
.then(data => {
|
||||
clearTimeout(spinnerTimer);
|
||||
hideSpinner();
|
||||
if (data.breadcrumbs) {
|
||||
renderContent(data);
|
||||
} else if (data.error) {
|
||||
document.getElementById('content').innerHTML = `<div class="alert alert-warning">${data.error}</div>`;
|
||||
return;
|
||||
}
|
||||
if (data.playfile) {
|
||||
const playFileLink = document.querySelector(`.play-file[data-url="${data.playfile}"]`);
|
||||
if (playFileLink) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user