diff --git a/analytics.py b/analytics.py index 8c6cbf1..dd3617a 100644 --- a/analytics.py +++ b/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) @@ -113,18 +118,100 @@ def log_file_access(rel_path, filesize, mime, ip_address, user_agent, device_id, (timestamp, rel_path, filesize, mime, city, country, user_agent, device_id, cached) 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 + ] + + # 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 ] - # 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 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: diff --git a/app.py b/app.py index 997cf0f..6284744 100755 --- a/app.py +++ b/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/') @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. @@ -270,6 +322,11 @@ def serve_file(subpath): root, *relative_parts = subpath.split('/') 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}") diff --git a/helperfunctions.py b/helperfunctions.py new file mode 100644 index 0000000..2ca5124 --- /dev/null +++ b/helperfunctions.py @@ -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 \ No newline at end of file diff --git a/index_for_search.py b/index_for_search.py index 1ad7ff5..804336d 100755 --- a/index_for_search.py +++ b/index_for_search.py @@ -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)) diff --git a/static/app.js b/static/app.js index c3ff875..621180a 100644 --- a/static/app.js +++ b/static/app.js @@ -111,6 +111,12 @@ function renderContent(data) { contentHTML += ''; } else { contentHTML += '