start project
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
"""Uploaded-file deletion (project-owner follow-up request).
|
||||
|
||||
Deleting a LogFile is more than one DELETE statement: log_entries,
|
||||
bot_hits, and suspicious_events all reference log_file_id and must be
|
||||
removed explicitly — this project's SQLite connections don't have
|
||||
`PRAGMA foreign_keys=ON`, so the `ondelete="CASCADE"` in the migrations
|
||||
(Ch06) is declarative documentation only, not an enforced behavior.
|
||||
Relying on it would silently leave orphaned rows behind.
|
||||
|
||||
The harder part is the rollup tables (request_stats_hourly/daily,
|
||||
referrer/browser/human-path/ip-path/ip-status): none of them have a
|
||||
log_file_id column (Ch06: they're site-wide, since two files can share a
|
||||
date). So instead of deleting rollup rows for the deleted file's dates
|
||||
directly (which could wipe out another file's contribution to the same
|
||||
date), this captures the affected dates BEFORE deleting, then calls
|
||||
aggregator.compute_rollups_for_range() AFTER deleting — which recomputes
|
||||
each affected day from whatever log_entries remain, correctly handling
|
||||
both "another file still covers this day" and "no file covers this day
|
||||
anymore" (the latter now handled correctly by the aggregator.py fix that
|
||||
shipped alongside this feature).
|
||||
|
||||
KNOWN LIMITATION (flagged, not fixed): ip_registry (total_requests,
|
||||
first_seen, last_seen, verification cache) and blocklist_suggestions are
|
||||
NOT per-file and are NOT recomputed on deletion — doing so correctly
|
||||
would mean re-scanning all remaining log_entries for every affected IP,
|
||||
which is unbounded work for a single delete action and would violate the
|
||||
same "no raw-row scan on a hot path" reasoning Ch03 applies elsewhere.
|
||||
After deleting a file, IP History numbers may include a deleted file's
|
||||
historical contribution until a full site-wide recompute is added as a
|
||||
separate feature. Doesn't affect correctness of Overview/SEO's date-
|
||||
scoped numbers, which is what this feature was actually asked to fix.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from app.extensions import db
|
||||
from app.models.bot_hit import BotHit
|
||||
from app.models.log_entry import LogEntry
|
||||
from app.models.log_file import LogFile
|
||||
from app.models.suspicious_event import SuspiciousEvent
|
||||
from app.services import aggregator
|
||||
from app.utils.upload_paths import upload_path_for
|
||||
|
||||
|
||||
@dataclass
|
||||
class BulkDeleteResult:
|
||||
deleted: list[int] = field(default_factory=list)
|
||||
skipped: list[dict] = field(default_factory=list) # [{"id": ..., "reason": ...}]
|
||||
|
||||
|
||||
def delete_log_files(log_file_ids: list[int]) -> BulkDeleteResult:
|
||||
"""Delete each id in `log_file_ids`; recompute rollups once at the end
|
||||
for the full span of dates any deleted file touched (cheaper than
|
||||
recomputing per-file when multiple files share dates).
|
||||
"""
|
||||
result = BulkDeleteResult()
|
||||
all_touched_dates: set = set()
|
||||
|
||||
for log_file_id in log_file_ids:
|
||||
log_file = db.session.get(LogFile, log_file_id)
|
||||
if log_file is None:
|
||||
result.skipped.append({"id": log_file_id, "reason": "not found"})
|
||||
continue
|
||||
if log_file.status == "processing":
|
||||
result.skipped.append({"id": log_file_id, "reason": "currently being analyzed"})
|
||||
continue
|
||||
|
||||
touched_dates = {
|
||||
row.date() for row, in db.session.query(LogEntry.timestamp)
|
||||
.filter(LogEntry.log_file_id == log_file_id).distinct()
|
||||
}
|
||||
# distinct() on a full timestamp rarely collapses much; reduce to
|
||||
# calendar dates in Python since SQLite's DATE() in a DISTINCT
|
||||
# clause is a bit more awkward to express portably here.
|
||||
all_touched_dates |= touched_dates
|
||||
|
||||
db.session.query(SuspiciousEvent).filter(SuspiciousEvent.log_file_id == log_file_id).delete()
|
||||
db.session.query(BotHit).filter(BotHit.log_file_id == log_file_id).delete()
|
||||
db.session.query(LogEntry).filter(LogEntry.log_file_id == log_file_id).delete()
|
||||
|
||||
raw_path = upload_path_for(log_file)
|
||||
if raw_path.exists():
|
||||
raw_path.unlink()
|
||||
|
||||
db.session.delete(log_file)
|
||||
db.session.commit()
|
||||
result.deleted.append(log_file_id)
|
||||
|
||||
if all_touched_dates:
|
||||
aggregator.compute_rollups_for_range(min(all_touched_dates), max(all_touched_dates))
|
||||
|
||||
return result
|
||||
Reference in New Issue
Block a user