This commit is contained in:
lelo 2025-05-29 10:19:36 +02:00
commit 1c9fe28fc1
5 changed files with 218 additions and 60 deletions

View File

@ -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 todays 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 helpers YYYY-MM-DD string to todays 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
View File

@ -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.
@ -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}")

46
helperfunctions.py Normal file
View 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 14 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

View File

@ -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 14 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))

View File

@ -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();
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) {
const playFileLink = document.querySelector(`.play-file[data-url="${data.playfile}"]`);
if (playFileLink) {