diff --git a/GeoLite2-City.mmdb b/GeoLite2-City.mmdb new file mode 100644 index 0000000..e1018b9 Binary files /dev/null and b/GeoLite2-City.mmdb differ diff --git a/app.py b/app.py index c360b1c..e5b3889 100755 --- a/app.py +++ b/app.py @@ -9,9 +9,12 @@ from datetime import datetime, date, timedelta from urllib.parse import unquote import diskcache import json +import geoip2.database +from werkzeug.middleware.proxy_fix import ProxyFix cache = diskcache.Cache('./filecache', size_limit= 32 * 1024**3) # 32 GB limit app = Flask(__name__) +app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1) # Use a raw string for the default FILE_ROOT path. app.config['FILE_ROOT'] = r'/mp3_root' @@ -199,16 +202,22 @@ def api_browse(subpath): 'files': files }) -@app.route("/access-log") +def lookup_location(ip, reader): + try: + response = reader.city(ip) + country = response.country.name if response.country.name else "Unknown" + city = response.city.name if response.city.name else "Unknown" + return country, city + except Exception: + return "Unknown", "Unknown" + +@app.route("/dashboard") @require_secret -def access_log(): - # Get timeframe filter from query parameter; default to "today" +def dashboard(): timeframe = request.args.get('timeframe', 'today') now = datetime.now() - # Determine the start time based on the requested timeframe if timeframe == 'today': - # Beginning of today start = now.replace(hour=0, minute=0, second=0, microsecond=0) elif timeframe == '7days': start = now - timedelta(days=7) @@ -217,12 +226,12 @@ def access_log(): elif timeframe == '365days': start = now - timedelta(days=365) else: - # Default to today if an unknown timeframe is passed start = now.replace(hour=0, minute=0, second=0, microsecond=0) - # Query the access log database for file access counts since the 'start' time conn = sqlite3.connect('access_log.db') cursor = conn.cursor() + + # Raw file access counts for the table (top files) cursor.execute(''' SELECT full_path, COUNT(*) as access_count FROM file_access_log @@ -231,9 +240,86 @@ def access_log(): ORDER BY access_count DESC ''', (start.isoformat(),)) rows = cursor.fetchall() + + # Daily access trend for a line chart + cursor.execute(''' + SELECT date(timestamp) as date, COUNT(*) as count + FROM file_access_log + WHERE timestamp >= ? + GROUP BY date + ORDER BY date + ''', (start.isoformat(),)) + daily_access_data = [dict(date=row[0], count=row[1]) for row in cursor.fetchall()] + + # Top files for bar chart (limit to 10) + cursor.execute(''' + SELECT full_path, COUNT(*) as access_count + FROM file_access_log + WHERE timestamp >= ? + GROUP BY full_path + ORDER BY access_count DESC + LIMIT 10 + ''', (start.isoformat(),)) + top_files_data = [dict(full_path=row[0], access_count=row[1]) for row in cursor.fetchall()] + + # User agent distribution (limit to 10) + cursor.execute(''' + SELECT user_agent, COUNT(*) as count + FROM file_access_log + WHERE timestamp >= ? + GROUP BY user_agent + ORDER BY count DESC + LIMIT 10 + ''', (start.isoformat(),)) + user_agent_data = [dict(user_agent=row[0], count=row[1]) for row in cursor.fetchall()] + + # Referrer distribution (limit to 10) + cursor.execute(''' + SELECT referrer, COUNT(*) as count + FROM file_access_log + WHERE timestamp >= ? + GROUP BY referrer + ORDER BY count DESC + LIMIT 10 + ''', (start.isoformat(),)) + referrer_data = [dict(referrer=row[0] if row[0] else "Direct/None", count=row[1]) for row in cursor.fetchall()] + + # Aggregate IP addresses with counts + cursor.execute(''' + SELECT ip_address, COUNT(*) as count + FROM file_access_log + WHERE timestamp >= ? + GROUP BY ip_address + ORDER BY count DESC + ''', (start.isoformat(),)) + ip_rows = cursor.fetchall() + + # Initialize GeoIP2 reader once for efficiency + reader = geoip2.database.Reader('GeoLite2-City.mmdb') + ip_data = [] + for ip, count in ip_rows: + country, city = lookup_location(ip, reader) + ip_data.append(dict(ip=ip, count=count, country=country, city=city)) + reader.close() + + # Summary stats + total_accesses = sum([row[1] for row in rows]) + unique_files = len(rows) + cursor.execute('SELECT COUNT(DISTINCT ip_address) FROM file_access_log WHERE timestamp >= ?', (start.isoformat(),)) + unique_ips = cursor.fetchone()[0] conn.close() - return render_template("access_log.html", rows=rows, timeframe=timeframe) + return render_template("dashboard.html", + timeframe=timeframe, + rows=rows, + daily_access_data=daily_access_data, + top_files_data=top_files_data, + user_agent_data=user_agent_data, + referrer_data=referrer_data, + ip_data=ip_data, + total_accesses=total_accesses, + unique_files=unique_files, + unique_ips=unique_ips) def log_file_access(full_path): """ @@ -278,8 +364,10 @@ def serve_file(filename): app.logger.error(f"File not found: {full_path}") return "File not found", 404 - # Exclude HEAD requests from logging - if request.method != 'HEAD': + # HEAD request are coming in to initiate server caching. + # only log initial hits and not the reload of further file parts + range_header = request.headers.get('Range') + if request.method != 'HEAD' and (not range_header or range_header.startswith("bytes=0-")): log_file_access(full_path) mime, _ = mimetypes.guess_type(full_path) diff --git a/docker-compose.yml b/docker-compose.yml index 00c2e96..ac08c07 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,12 @@ services: - ./:/app - ./filecache:/app/filecache - ./templates:/app/templates - - /mnt/app.bethaus:/mp3_root:ro + - ./GeoLite2-City.mmdb:/app/GeoLite2-City.mmdb:ro + - type: bind + source: /mnt/app.bethaus + target: /mp3_root + bind: + propagation: rshared environment: - FLASK_APP=app.py - FLASK_RUN_HOST=0.0.0.0 diff --git a/requirements.txt b/requirements.txt index 62ac035..fc1ad63 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ flask pillow diskcache +geoip2 diff --git a/static/app.js b/static/app.js index d535fba..4649836 100644 --- a/static/app.js +++ b/static/app.js @@ -119,6 +119,8 @@ function attachEventListeners() { const newPath = this.getAttribute('data-path'); loadDirectory(newPath); history.pushState({ subpath: newPath }, '', newPath ? '/path/' + newPath : '/'); + // Scroll to the top of the page after click: + window.scrollTo({ top: 0, behavior: 'smooth' }); }); }); diff --git a/static/styles.css b/static/styles.css index e534ce8..8a5e44b 100644 --- a/static/styles.css +++ b/static/styles.css @@ -15,12 +15,12 @@ body { display: grid; grid-template-rows: auto 1fr auto; min-height: 100%; + padding-bottom: 200px; } .container { width: 90%; max-width: 900px; margin: 0 auto; - padding-bottom: 200px; } .container a { diff --git a/templates/access_log.html b/templates/access_log.html deleted file mode 100644 index 271ae14..0000000 --- a/templates/access_log.html +++ /dev/null @@ -1,46 +0,0 @@ - - -
- -| File Path | -Access Count | -
|---|---|
| {{ row[0] }} | -{{ row[1] }} | -
| No data available for the selected timeframe. | -|
{{ total_accesses }}
+{{ unique_files }}
+{{ unique_ips }}
+| File Path | +Access Count | +
|---|---|
| {{ row[0] }} | +{{ row[1] }} | +
| No data available for the selected timeframe. | +|
| IP Address | +Access Count | +City | +Country | +
|---|---|---|---|
| {{ ip.ip }} | +{{ ip.count }} | +{{ ip.city }} | +{{ ip.country }} | +
| No IP access data available for the selected timeframe. | +|||