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 json
|
||||||
import os
|
import os
|
||||||
import auth
|
import auth
|
||||||
|
import helperfunctions as hf
|
||||||
|
|
||||||
file_access_temp = []
|
file_access_temp = []
|
||||||
|
folder_today = []
|
||||||
|
folder_yesterday = []
|
||||||
|
|
||||||
app_config = auth.return_app_config()
|
app_config = auth.return_app_config()
|
||||||
|
|
||||||
@ -98,11 +101,13 @@ def parse_timestamp(ts_str):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
def log_file_access(rel_path, filesize, mime, ip_address, user_agent, device_id, cached):
|
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."""
|
"""Insert a file access record into the database and prune entries older than 10 minutes,
|
||||||
global file_access_temp
|
and track today’s files separately in folder_today."""
|
||||||
# Create a timezone-aware timestamp (local time with offset)
|
global file_access_temp, folder_today, folder_yesterday
|
||||||
timestamp = datetime.now(timezone.utc).astimezone()
|
|
||||||
iso_ts = timestamp.isoformat()
|
# Create a timezone-aware timestamp
|
||||||
|
now = datetime.now(timezone.utc).astimezone()
|
||||||
|
iso_ts = now.isoformat()
|
||||||
|
|
||||||
# Convert the IP address to a location
|
# Convert the IP address to a location
|
||||||
city, country = lookup_location(ip_address)
|
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 (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
''', (iso_ts, rel_path, filesize, mime, city, country, user_agent, device_id, cached))
|
''', (iso_ts, rel_path, filesize, mime, city, country, user_agent, device_id, cached))
|
||||||
|
|
||||||
# Remove entries older than 10 minutes using our robust parser.
|
# Prune temp entries older than 10 minutes
|
||||||
cutoff_time = datetime.now(timezone.utc).astimezone() - timedelta(minutes=10)
|
cutoff = now - timedelta(minutes=10)
|
||||||
file_access_temp[:] = [
|
file_access_temp[:] = [
|
||||||
entry for entry in 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
|
# Keep only today's entries in folder_today
|
||||||
file_access_temp.insert(0, [iso_ts, rel_path, filesize, mime, f"{city}, {country}", user_agent, device_id, cached])
|
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
|
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():
|
def return_file_access():
|
||||||
"""Return recent file access logs from memory (the last 10 minutes)."""
|
"""Return recent file access logs from memory (the last 10 minutes)."""
|
||||||
global file_access_temp
|
global file_access_temp
|
||||||
@ -140,6 +227,7 @@ def return_file_access():
|
|||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def songs_dashboard():
|
def songs_dashboard():
|
||||||
# — SESSION & PARAM HANDLING (unchanged) —
|
# — SESSION & PARAM HANDLING (unchanged) —
|
||||||
if 'songs_dashboard_timeframe' not in session:
|
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 functools import lru_cache
|
||||||
from urllib.parse import urlparse, unquote
|
from urllib.parse import urlparse, unquote
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
import qrcode
|
import qrcode
|
||||||
import base64
|
import base64
|
||||||
@ -22,8 +23,10 @@ import search
|
|||||||
import auth
|
import auth
|
||||||
import analytics as a
|
import analytics as a
|
||||||
import folder_secret_config_editor as fsce
|
import folder_secret_config_editor as fsce
|
||||||
|
import helperfunctions as hf
|
||||||
|
|
||||||
app_config = auth.return_app_config()
|
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_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)
|
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')
|
resized_img.save(img_byte_arr, format='PNG')
|
||||||
return img_byte_arr.getvalue()
|
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):
|
def list_directory_contents(directory, subpath):
|
||||||
"""
|
"""
|
||||||
List only the immediate contents of the given directory.
|
List only the immediate contents of the given directory.
|
||||||
@ -220,6 +244,7 @@ def serve_sw():
|
|||||||
@app.route('/api/path/<path:subpath>')
|
@app.route('/api/path/<path:subpath>')
|
||||||
@auth.require_secret
|
@auth.require_secret
|
||||||
def api_browse(subpath):
|
def api_browse(subpath):
|
||||||
|
|
||||||
if subpath == '': # root directory
|
if subpath == '': # root directory
|
||||||
foldernames = []
|
foldernames = []
|
||||||
for foldername, _ in session['folders'].items():
|
for foldername, _ in session['folders'].items():
|
||||||
@ -228,7 +253,26 @@ def api_browse(subpath):
|
|||||||
return jsonify({
|
return jsonify({
|
||||||
'breadcrumbs': generate_breadcrumbs(),
|
'breadcrumbs': generate_breadcrumbs(),
|
||||||
'directories': foldernames,
|
'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('/')
|
root, *relative_parts = subpath.split('/')
|
||||||
@ -236,6 +280,12 @@ def api_browse(subpath):
|
|||||||
directory = os.path.join(base_path, *relative_parts)
|
directory = os.path.join(base_path, *relative_parts)
|
||||||
|
|
||||||
playfile = None
|
playfile = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
directory = check_path(directory)
|
||||||
|
except (ValueError, PermissionError) as e:
|
||||||
|
return jsonify({'error': str(e)}), 403
|
||||||
|
|
||||||
# Check if the constructed directory exists.
|
# Check if the constructed directory exists.
|
||||||
if not os.path.isdir(directory):
|
if not os.path.isdir(directory):
|
||||||
# Assume the last segment is a filename; remove it.
|
# Assume the last segment is a filename; remove it.
|
||||||
@ -254,7 +304,9 @@ def api_browse(subpath):
|
|||||||
response = {
|
response = {
|
||||||
'breadcrumbs': breadcrumbs,
|
'breadcrumbs': breadcrumbs,
|
||||||
'directories': directories,
|
'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.
|
# If a filename was selected include it.
|
||||||
@ -271,6 +323,11 @@ def serve_file(subpath):
|
|||||||
base_path = session['folders'].get(root)
|
base_path = session['folders'].get(root)
|
||||||
full_path = os.path.join(base_path or '', *relative_parts)
|
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):
|
if not os.path.isfile(full_path):
|
||||||
app.logger.error(f"File not found: {full_path}")
|
app.logger.error(f"File not found: {full_path}")
|
||||||
return "File not found", 404
|
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
|
import sqlite3
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import re
|
import re
|
||||||
|
import helperfunctions as hf
|
||||||
|
|
||||||
SEARCH_DB_NAME = 'search.db'
|
SEARCH_DB_NAME = 'search.db'
|
||||||
ACCESS_LOG_DB_NAME = 'access_log.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
|
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():
|
def updatefileindex():
|
||||||
cursor = search_db.cursor()
|
cursor = search_db.cursor()
|
||||||
|
|
||||||
@ -215,7 +171,7 @@ def updatefileindex():
|
|||||||
titel = None
|
titel = None
|
||||||
name = 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))
|
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))
|
current_keys.add((relative_path, entry.name))
|
||||||
|
|||||||
@ -111,6 +111,12 @@ function renderContent(data) {
|
|||||||
contentHTML += '</div>';
|
contentHTML += '</div>';
|
||||||
} else {
|
} else {
|
||||||
contentHTML += '<ul>';
|
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 => {
|
data.directories.forEach(dir => {
|
||||||
if (admin_enabled && data.breadcrumbs.length != 1 && dir.share) {
|
if (admin_enabled && data.breadcrumbs.length != 1 && dir.share) {
|
||||||
share_link = `<a href="#" class="create-share" data-url="${dir.path}">⚙️</a>`;
|
share_link = `<a href="#" class="create-share" data-url="${dir.path}">⚙️</a>`;
|
||||||
@ -213,7 +219,12 @@ function loadDirectory(subpath) {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
clearTimeout(spinnerTimer);
|
clearTimeout(spinnerTimer);
|
||||||
hideSpinner();
|
hideSpinner();
|
||||||
renderContent(data);
|
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) {
|
if (data.playfile) {
|
||||||
const playFileLink = document.querySelector(`.play-file[data-url="${data.playfile}"]`);
|
const playFileLink = document.querySelector(`.play-file[data-url="${data.playfile}"]`);
|
||||||
if (playFileLink) {
|
if (playFileLink) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user