z230
This commit is contained in:
@@ -8,7 +8,11 @@
|
|||||||
"Bash(pip install:*)",
|
"Bash(pip install:*)",
|
||||||
"Bash(tasklist:*)",
|
"Bash(tasklist:*)",
|
||||||
"Bash(wmic process:*)",
|
"Bash(wmic process:*)",
|
||||||
"Bash(taskkill:*)"
|
"Bash(taskkill:*)",
|
||||||
|
"Bash(C:Pythonpython.exe -c \"import pymysql; conn = pymysql.connect\\(host=''192.168.1.76'', port=3306, user=''root'', password='''', db=''OrdinaceDropBoxBackup''\\); cur = conn.cursor\\(\\); cur.execute\\(''SELECT * FROM runs''\\); print\\(cur.fetchall\\(\\)\\); cur.execute\\(''SELECT COUNT\\(*\\) FROM files''\\); print\\(''files count:'', cur.fetchone\\(\\)\\); conn.close\\(\\)\")",
|
||||||
|
"Bash(/c/Python/python.exe -c \"import pymysql; conn = pymysql.connect\\(host=''192.168.1.76'', port=3306, user=''root'', password='''', db=''OrdinaceDropBoxBackup''\\); cur = conn.cursor\\(\\); cur.execute\\(''SELECT * FROM runs''\\); print\\(''RUNS:'', cur.fetchall\\(\\)\\); cur.execute\\(''SELECT COUNT\\(*\\) FROM files''\\); print\\(''FILES count:'', cur.fetchone\\(\\)\\); conn.close\\(\\)\")",
|
||||||
|
"Bash(/c/Python/python.exe -c \"import pymysql; conn = pymysql.connect\\(host=''192.168.1.76'', port=3306, user=''root'', password=''Vlado9674+'', db=''OrdinaceDropBoxBackup''\\); cur = conn.cursor\\(\\); cur.execute\\(''SELECT * FROM runs''\\); rows = cur.fetchall\\(\\); print\\(''RUNS:''\\); [print\\(r\\) for r in rows]; cur.execute\\(''SELECT COUNT\\(*\\) FROM files''\\); print\\(''FILES count:'', cur.fetchone\\(\\)[0]\\); cur.execute\\(''SELECT COUNT\\(*\\) FROM file_events''\\); print\\(''EVENTS count:'', cur.fetchone\\(\\)[0]\\); conn.close\\(\\)\")",
|
||||||
|
"Bash(/c/Python/python.exe:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
backup_report.xlsx
Normal file
BIN
backup_report.xlsx
Normal file
Binary file not shown.
@@ -29,5 +29,5 @@ BACKUP_PATH = os.getenv("BACKUP_PATH")
|
|||||||
# Behaviour
|
# Behaviour
|
||||||
# =========================
|
# =========================
|
||||||
|
|
||||||
DRY_RUN = os.getenv("DRY_RUN", "1") == "1"
|
DRY_RUN = os.getenv("DRY_RUN", "true").lower() in ("1", "true", "yes")
|
||||||
BATCH_SIZE = int(os.getenv("BATCH_SIZE", 1000))
|
BATCH_SIZE = int(os.getenv("BATCH_SIZE", 1000))
|
||||||
|
|||||||
@@ -79,10 +79,15 @@ def batch_insert_files(cur, files_list: list, run_id: int) -> dict:
|
|||||||
f["size"], f["mtime"], f["content_hash"], run_id, run_id)
|
f["size"], f["mtime"], f["content_hash"], run_id, run_id)
|
||||||
for f in chunk]
|
for f in chunk]
|
||||||
)
|
)
|
||||||
# pymysql executemany: lastrowid = first id in batch
|
# Fetch real IDs — lastrowid+j is unreliable with executemany
|
||||||
first_id = cur.lastrowid
|
paths = [f["relative_path"] for f in chunk]
|
||||||
for j, f in enumerate(chunk):
|
placeholders = ",".join(["%s"] * len(paths))
|
||||||
path_to_id[f["relative_path"]] = first_id + j
|
cur.execute(
|
||||||
|
f"SELECT id, relative_path FROM files WHERE relative_path IN ({placeholders})",
|
||||||
|
paths,
|
||||||
|
)
|
||||||
|
for row in cur.fetchall():
|
||||||
|
path_to_id[row[1]] = row[0]
|
||||||
return path_to_id
|
return path_to_id
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,14 @@ def scan_files(root_path: str) -> dict:
|
|||||||
continue
|
continue
|
||||||
rel_path = os.path.relpath(full_path, root_path).replace("\\", "/")
|
rel_path = os.path.relpath(full_path, root_path).replace("\\", "/")
|
||||||
rel_dir = os.path.relpath(root, root_path).replace("\\", "/")
|
rel_dir = os.path.relpath(root, root_path).replace("\\", "/")
|
||||||
|
# Truncate microseconds — MySQL DATETIME rounds to whole seconds,
|
||||||
|
# which causes false "modified" detections on every run.
|
||||||
|
mtime = datetime.fromtimestamp(stat.st_mtime).replace(microsecond=0)
|
||||||
result[rel_path] = {
|
result[rel_path] = {
|
||||||
"full_path": full_path,
|
"full_path": full_path,
|
||||||
"file_name": name,
|
"file_name": name,
|
||||||
"directory": rel_dir,
|
"directory": rel_dir,
|
||||||
"size": stat.st_size,
|
"size": stat.st_size,
|
||||||
"mtime": datetime.fromtimestamp(stat.st_mtime),
|
"mtime": mtime,
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
15
main.py
15
main.py
@@ -1,3 +1,6 @@
|
|||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from indexer.config import ROOT_PATH, ROOT_NAME, DRY_RUN, BACKUP_PATH
|
from indexer.config import ROOT_PATH, ROOT_NAME, DRY_RUN, BACKUP_PATH
|
||||||
from indexer.scanner import scan_files
|
from indexer.scanner import scan_files
|
||||||
from indexer.hasher import blake3_file
|
from indexer.hasher import blake3_file
|
||||||
@@ -195,6 +198,18 @@ def main():
|
|||||||
print(f"Unchanged: {stats['unchanged']}")
|
print(f"Unchanged: {stats['unchanged']}")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
|
# ── 8. Generate Excel report ──
|
||||||
|
try:
|
||||||
|
from report import generate_report
|
||||||
|
|
||||||
|
report_dir = r"u:\Dropbox\!!!Days\Downloads Z230"
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%d %H_%M")
|
||||||
|
report_path = os.path.join(report_dir, f"{timestamp} DropboxBackupReport.xlsx")
|
||||||
|
print(f"\n[8] Generating report...")
|
||||||
|
generate_report(report_path)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" WARN: Report generation failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
122
report.py
Normal file
122
report.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
"""
|
||||||
|
Generate Excel report of backup runs and file events.
|
||||||
|
|
||||||
|
Usage: python report.py [output.xlsx]
|
||||||
|
|
||||||
|
Single sheet with all events from all runs.
|
||||||
|
Skips runs where total events > THRESHOLD (mass initial imports).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime as dt
|
||||||
|
import pymysql
|
||||||
|
from openpyxl import Workbook
|
||||||
|
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||||||
|
from openpyxl.utils import get_column_letter
|
||||||
|
from indexer.config import DB_CONFIG
|
||||||
|
|
||||||
|
THRESHOLD = 5000 # skip runs with more events than this
|
||||||
|
|
||||||
|
|
||||||
|
def generate_report(output_path: str):
|
||||||
|
conn = pymysql.connect(**DB_CONFIG)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# ── 1. Load runs ──
|
||||||
|
cur.execute("SELECT * FROM runs ORDER BY id")
|
||||||
|
runs = cur.fetchall()
|
||||||
|
|
||||||
|
# ── 2. Collect all events from non-skipped runs ──
|
||||||
|
all_events = []
|
||||||
|
skipped_runs = []
|
||||||
|
for run in runs:
|
||||||
|
run_id, started, finished, status, total, new, mod, deleted, unchanged = run
|
||||||
|
total_changes = new + mod + deleted
|
||||||
|
if total_changes > THRESHOLD:
|
||||||
|
skipped_runs.append(run_id)
|
||||||
|
continue
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT fe.event_type, f.relative_path, f.file_name, f.directory,
|
||||||
|
fe.old_size, fe.new_size
|
||||||
|
FROM file_events fe
|
||||||
|
JOIN files f ON fe.file_id = f.id
|
||||||
|
WHERE fe.run_id = %s
|
||||||
|
ORDER BY fe.event_type, f.relative_path""",
|
||||||
|
(run_id,)
|
||||||
|
)
|
||||||
|
for ev in cur.fetchall():
|
||||||
|
all_events.append((run_id, started, *ev))
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# ── 3. Build Excel — single sheet ──
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "Events"
|
||||||
|
|
||||||
|
header_font = Font(bold=True, color="FFFFFF", size=11)
|
||||||
|
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
|
||||||
|
thin_border = Border(
|
||||||
|
left=Side(style="thin"), right=Side(style="thin"),
|
||||||
|
top=Side(style="thin"), bottom=Side(style="thin"),
|
||||||
|
)
|
||||||
|
|
||||||
|
type_fills = {
|
||||||
|
"CREATED": PatternFill(start_color="E2EFDA", end_color="E2EFDA", fill_type="solid"),
|
||||||
|
"MODIFIED": PatternFill(start_color="FFF2CC", end_color="FFF2CC", fill_type="solid"),
|
||||||
|
"DELETED": PatternFill(start_color="FCE4EC", end_color="FCE4EC", fill_type="solid"),
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = ["Run #", "Run Time", "Event", "Path", "File Name", "Directory",
|
||||||
|
"Old Size", "New Size", "Size Change"]
|
||||||
|
for col, h in enumerate(headers, 1):
|
||||||
|
cell = ws.cell(row=1, column=col, value=h)
|
||||||
|
cell.font = header_font
|
||||||
|
cell.fill = header_fill
|
||||||
|
cell.alignment = Alignment(horizontal="center")
|
||||||
|
cell.border = thin_border
|
||||||
|
|
||||||
|
for row_idx, ev in enumerate(all_events, 2):
|
||||||
|
run_id, started, event_type, rel_path, file_name, directory, old_size, new_size = ev
|
||||||
|
|
||||||
|
size_change = ""
|
||||||
|
if old_size is not None and new_size is not None:
|
||||||
|
diff = new_size - old_size
|
||||||
|
if diff != 0:
|
||||||
|
size_change = f"{'+' if diff > 0 else ''}{diff:,}"
|
||||||
|
elif new_size is not None:
|
||||||
|
size_change = f"+{new_size:,}"
|
||||||
|
elif old_size is not None:
|
||||||
|
size_change = f"-{old_size:,}"
|
||||||
|
|
||||||
|
values = [run_id, started, event_type, rel_path, file_name, directory,
|
||||||
|
old_size, new_size, size_change]
|
||||||
|
|
||||||
|
fill = type_fills.get(event_type)
|
||||||
|
for col, val in enumerate(values, 1):
|
||||||
|
cell = ws.cell(row=row_idx, column=col, value=val)
|
||||||
|
cell.border = thin_border
|
||||||
|
if fill:
|
||||||
|
cell.fill = fill
|
||||||
|
|
||||||
|
# Auto-width
|
||||||
|
widths = [8, 18, 10, 60, 30, 40, 12, 12, 14]
|
||||||
|
for col, w in enumerate(widths, 1):
|
||||||
|
ws.column_dimensions[get_column_letter(col)].width = w
|
||||||
|
|
||||||
|
# Autofilter
|
||||||
|
ws.auto_filter.ref = f"A1:{get_column_letter(len(headers))}{len(all_events) + 1}"
|
||||||
|
|
||||||
|
wb.save(output_path)
|
||||||
|
print(f"Report saved to {output_path}")
|
||||||
|
print(f" Runs total: {len(runs)}, skipped: {len(skipped_runs)} (threshold: {THRESHOLD})")
|
||||||
|
print(f" Events: {len(all_events)} rows")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
REPORT_DIR = r"u:\Dropbox\!!!Days\Downloads Z230"
|
||||||
|
timestamp = dt.now().strftime("%Y-%m-%d %H_%M")
|
||||||
|
default_name = f"{timestamp} DropboxBackupReport.xlsx"
|
||||||
|
output = sys.argv[1] if len(sys.argv) > 1 else os.path.join(REPORT_DIR, default_name)
|
||||||
|
generate_report(output)
|
||||||
Reference in New Issue
Block a user