webapp: export Blancco reports for the last 1/3/6/12 months
/reports/export?range=1m|3m|6m|12m&format=csv|zip. CSV has one row per erased drive (system and drive serials, standard, result, times, licence consumption id). ZIP holds the original XML/PDF reports plus summary.csv. Reports are selected by the date inside each XML, falling back to mtime only when it is missing. Exports are audit-logged.
This commit is contained in:
@@ -591,6 +591,79 @@ def blancco_reports():
|
||||
)
|
||||
|
||||
|
||||
REPORT_EXPORT_RANGES = {"1m": 1, "3m": 3, "6m": 6, "12m": 12}
|
||||
|
||||
|
||||
def _months_ago(now, months):
|
||||
# Same day-of-month N months back, clamped (Mar 31 - 1m -> Feb 28/29).
|
||||
import calendar
|
||||
y, m = divmod(now.year * 12 + now.month - 1 - months, 12)
|
||||
day = min(now.day, calendar.monthrange(y, m + 1)[1])
|
||||
return now.replace(year=y, month=m + 1, day=day)
|
||||
|
||||
|
||||
@app.route("/reports/export")
|
||||
def blancco_export_reports():
|
||||
import csv
|
||||
import io
|
||||
import zipfile
|
||||
from datetime import timezone
|
||||
|
||||
rng = request.args.get("range", "1m")
|
||||
fmt = request.args.get("format", "csv")
|
||||
if rng not in REPORT_EXPORT_RANGES or fmt not in ("csv", "zip"):
|
||||
abort(400)
|
||||
now = datetime.now(timezone.utc)
|
||||
since = _months_ago(now, REPORT_EXPORT_RANGES[rng])
|
||||
|
||||
# Select by the date inside each XML report; fall back to mtime only
|
||||
# when a report has no parseable date. PDFs (7.20 writes pdf+xml) ride
|
||||
# along with the XML of the same name.
|
||||
rows, files = [], []
|
||||
folder = config.BLANCCO_REPORTS
|
||||
names = sorted(os.listdir(folder)) if os.path.isdir(folder) else []
|
||||
for f in names:
|
||||
fpath = os.path.join(folder, f)
|
||||
if not (f.lower().endswith(".xml") and os.path.isfile(fpath)):
|
||||
continue
|
||||
try:
|
||||
data = blancco_report.parse(fpath)
|
||||
except Exception:
|
||||
data = {}
|
||||
when = blancco_report.report_datetime(data) or datetime.fromtimestamp(
|
||||
os.stat(fpath).st_mtime, timezone.utc)
|
||||
if when < since:
|
||||
continue
|
||||
rows.extend(blancco_report.export_rows(f, data, when.isoformat()))
|
||||
files.append(f)
|
||||
pdf = os.path.splitext(f)[0] + ".pdf"
|
||||
if pdf in names:
|
||||
files.append(pdf)
|
||||
|
||||
buf = io.StringIO()
|
||||
writer = csv.DictWriter(buf, fieldnames=blancco_report.EXPORT_COLUMNS)
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
stem = f"blancco-reports-{rng}-{now:%Y%m%d}"
|
||||
audit("REPORT_EXPORT", f"{rng} {fmt} {len(files)} files")
|
||||
|
||||
if fmt == "csv":
|
||||
return Response(
|
||||
buf.getvalue(),
|
||||
mimetype="text/csv",
|
||||
headers={"Content-Disposition": f"attachment; filename={stem}.csv"},
|
||||
)
|
||||
# A year of reports can run to hundreds of MB; spill to disk past 64MB.
|
||||
out = tempfile.SpooledTemporaryFile(max_size=64 * 1024 * 1024)
|
||||
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.writestr(f"{stem}/summary.csv", buf.getvalue())
|
||||
for f in files:
|
||||
zf.write(os.path.join(folder, f), f"{stem}/{f}")
|
||||
out.seek(0)
|
||||
return send_file(out, mimetype="application/zip", as_attachment=True,
|
||||
download_name=f"{stem}.zip")
|
||||
|
||||
|
||||
@app.route("/reports/download/<filename>")
|
||||
def blancco_download_report(filename):
|
||||
filename = secure_filename(filename)
|
||||
|
||||
Reference in New Issue
Block a user