ADD export function to tables
This commit is contained in:
parent
7db919bcb7
commit
a7cc072777
3 changed files with 184 additions and 6 deletions
107
main.py
107
main.py
|
@ -3,7 +3,7 @@ import re
|
||||||
import io
|
import io
|
||||||
import datetime
|
import datetime
|
||||||
from fastapi import FastAPI, Request, HTTPException, Query, UploadFile, File, Form
|
from fastapi import FastAPI, Request, HTTPException, Query, UploadFile, File, Form
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse, Response
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
@ -350,6 +350,60 @@ def delete_upload(upload_id):
|
||||||
print(f"Error deleting upload {upload_id}: {e}")
|
print(f"Error deleting upload {upload_id}: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# CSV Export Functions
|
||||||
|
def domains_to_csv(domains: List[Dict]) -> str:
|
||||||
|
"""Convert domains data to CSV format"""
|
||||||
|
csv_output = io.StringIO()
|
||||||
|
|
||||||
|
if not domains:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Determine fields based on data
|
||||||
|
# Always include the full_domain field
|
||||||
|
fields = ["full_domain", "timestamp"]
|
||||||
|
if "base_domain" in domains[0]:
|
||||||
|
fields.insert(1, "base_domain")
|
||||||
|
|
||||||
|
# Add headers
|
||||||
|
writer = csv.DictWriter(csv_output, fieldnames=fields, extrasaction='ignore')
|
||||||
|
writer.writeheader()
|
||||||
|
|
||||||
|
# Add data
|
||||||
|
for domain in domains:
|
||||||
|
# Create a row dict with formatted timestamp
|
||||||
|
row = {k: domain.get(k) for k in fields}
|
||||||
|
if "timestamp" in row and row["timestamp"]:
|
||||||
|
# Format timestamp nicely for CSV
|
||||||
|
row["timestamp"] = row["timestamp"].replace('T', ' ').split('.')[0]
|
||||||
|
writer.writerow(row)
|
||||||
|
|
||||||
|
return csv_output.getvalue()
|
||||||
|
|
||||||
|
def dns_records_to_csv(records: List[Dict]) -> str:
|
||||||
|
"""Convert DNS records data to CSV format"""
|
||||||
|
csv_output = io.StringIO()
|
||||||
|
|
||||||
|
if not records:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Define the fields to include in the CSV
|
||||||
|
fields = ["domain", "ttl", "record_class", "record_type", "record_data", "timestamp"]
|
||||||
|
|
||||||
|
# Add headers
|
||||||
|
writer = csv.DictWriter(csv_output, fieldnames=fields, extrasaction='ignore')
|
||||||
|
writer.writeheader()
|
||||||
|
|
||||||
|
# Add data
|
||||||
|
for record in records:
|
||||||
|
# Create a row dict with formatted timestamp
|
||||||
|
row = {k: record.get(k) for k in fields}
|
||||||
|
if "timestamp" in row and row["timestamp"]:
|
||||||
|
# Format timestamp nicely for CSV
|
||||||
|
row["timestamp"] = row["timestamp"].replace('T', ' ').split('.')[0]
|
||||||
|
writer.writerow(row)
|
||||||
|
|
||||||
|
return csv_output.getvalue()
|
||||||
|
|
||||||
# Routes
|
# Routes
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def home(
|
async def home(
|
||||||
|
@ -471,6 +525,57 @@ async def dns_records(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@app.get("/export-domains-csv")
|
||||||
|
async def export_domains_csv(
|
||||||
|
upload_id: Optional[str] = None,
|
||||||
|
base_domains_only: Optional[bool] = False
|
||||||
|
):
|
||||||
|
"""Export domains as CSV"""
|
||||||
|
domains = load_domains(upload_id, base_domains_only)
|
||||||
|
csv_content = domains_to_csv(domains)
|
||||||
|
|
||||||
|
# Generate a filename with timestamp
|
||||||
|
filename = f"domains_export_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||||
|
|
||||||
|
# Return the CSV as a downloadable file
|
||||||
|
return Response(
|
||||||
|
content=csv_content,
|
||||||
|
media_type="text/csv",
|
||||||
|
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/export-dns-csv")
|
||||||
|
async def export_dns_csv(
|
||||||
|
upload_id: Optional[str] = None,
|
||||||
|
record_type: Optional[str] = None,
|
||||||
|
record_class: Optional[str] = None,
|
||||||
|
domain: Optional[str] = None,
|
||||||
|
deduplicate: Optional[bool] = True
|
||||||
|
):
|
||||||
|
"""Export DNS records as CSV"""
|
||||||
|
# Get entries with applied filters
|
||||||
|
entries = load_dns_entries(upload_id, deduplicate)
|
||||||
|
|
||||||
|
# Apply additional filters if provided
|
||||||
|
if record_type:
|
||||||
|
entries = [e for e in entries if e.get("record_type") == record_type]
|
||||||
|
if record_class:
|
||||||
|
entries = [e for e in entries if e.get("record_class") == record_class]
|
||||||
|
if domain:
|
||||||
|
entries = [e for e in entries if domain.lower() in e.get("domain", "").lower()]
|
||||||
|
|
||||||
|
csv_content = dns_records_to_csv(entries)
|
||||||
|
|
||||||
|
# Generate a filename with timestamp
|
||||||
|
filename = f"dns_records_export_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||||
|
|
||||||
|
# Return the CSV as a downloadable file
|
||||||
|
return Response(
|
||||||
|
content=csv_content,
|
||||||
|
media_type="text/csv",
|
||||||
|
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||||
|
)
|
||||||
|
|
||||||
# API Routes
|
# API Routes
|
||||||
@app.get("/api/uploads", response_model=List[Dict])
|
@app.get("/api/uploads", response_model=List[Dict])
|
||||||
async def get_all_uploads():
|
async def get_all_uploads():
|
||||||
|
|
|
@ -184,6 +184,35 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.table-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.table-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.btn-export {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 8px 15px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.btn-export:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -242,11 +271,20 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<h2>DNS Records <span class="count-badge">{{ entries|length }}</span>
|
<div class="table-header">
|
||||||
{% if deduplicate %}
|
<h2>DNS Records <span class="count-badge">{{ entries|length }}</span>
|
||||||
<small class="dedup-note">(Showing most recent entries for each Domain+Class+Type+TTL+Data combination)</small>
|
{% if deduplicate %}
|
||||||
|
<small class="dedup-note">(Showing most recent entries for each Domain+Class+Type+TTL+Data combination)</small>
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
|
{% if entries %}
|
||||||
|
<div class="table-actions">
|
||||||
|
<a href="/export-dns-csv?{% if request.query_params.get('upload_id') %}upload_id={{ request.query_params.get('upload_id') }}{% endif %}{% if request.query_params.get('record_type') %}&record_type={{ request.query_params.get('record_type') }}{% endif %}{% if request.query_params.get('record_class') %}&record_class={{ request.query_params.get('record_class') }}{% endif %}{% if request.query_params.get('domain') %}&domain={{ request.query_params.get('domain') }}{% endif %}{% if request.query_params.get('deduplicate') == 'false' %}&deduplicate=false{% endif %}" class="btn-export" title="Export to CSV">
|
||||||
|
<span class="icon">📥</span> Export CSV
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h2>
|
</div>
|
||||||
|
|
||||||
{% if entries %}
|
{% if entries %}
|
||||||
<table id="dns-table">
|
<table id="dns-table">
|
||||||
|
|
|
@ -187,6 +187,34 @@
|
||||||
background-color: #e53935;
|
background-color: #e53935;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
.table-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.table-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.btn-export {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 8px 15px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.btn-export:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -292,7 +320,14 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if domains %}
|
{% if domains %}
|
||||||
<p>Found {{ domains|length }} domains{% if request.query_params.get('upload_id') %} in this upload{% endif %}.</p>
|
<div class="table-header">
|
||||||
|
<p>Found {{ domains|length }} domains{% if request.query_params.get('upload_id') %} in this upload{% endif %}.</p>
|
||||||
|
<div class="table-actions">
|
||||||
|
<a href="/export-domains-csv?{% if request.query_params.get('upload_id') %}upload_id={{ request.query_params.get('upload_id') }}{% endif %}{% if request.query_params.get('base_domains_only') == 'true' %}&base_domains_only=true{% endif %}" class="btn-export" title="Export to CSV">
|
||||||
|
<span class="icon">📥</span> Export CSV
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue