Compare commits
5 Commits
665aa3bf28
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 52f04c2839 | |||
| 2de8711ab7 | |||
| 9dbd3ab0b4 | |||
| 638dc0dd2f | |||
| b749817304 |
@@ -16,8 +16,8 @@ DB_PASSWORD=Vlado9674+
|
|||||||
# =========================
|
# =========================
|
||||||
# INDEXER
|
# INDEXER
|
||||||
# =========================
|
# =========================
|
||||||
ROOT_PATH=u:\Dropbox\Ordinace\
|
ROOT_PATH=z:\Dropbox\Ordinace\
|
||||||
ROOT_NAME=DropboxOrdinace
|
ROOT_NAME=DropboxOrdinace
|
||||||
BATCH_SIZE=1000
|
BATCH_SIZE=1000
|
||||||
BACKUP_PATH=u:\OnedriveOrdinace\OneDrive\DropBoxBackupClaude\
|
BACKUP_PATH=w:\Onedrive\DropBoxBackupClaude\
|
||||||
BACKUP_PASSWORD=Vlado7309208104++
|
BACKUP_PASSWORD=Vlado7309208104++
|
||||||
|
|||||||
@@ -51,3 +51,8 @@ cache/
|
|||||||
# MySQL dumps / exports
|
# MySQL dumps / exports
|
||||||
# ===============================
|
# ===============================
|
||||||
*.sql
|
*.sql
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# Claude Code
|
||||||
|
# ===============================
|
||||||
|
.claude/worktrees/
|
||||||
|
|||||||
+17
-3
@@ -1,10 +1,24 @@
|
|||||||
|
import ctypes
|
||||||
|
|
||||||
from blake3 import blake3
|
from blake3 import blake3
|
||||||
|
|
||||||
|
# Windows atributy pro cloud/placeholder soubory
|
||||||
|
_FILE_ATTRIBUTE_OFFLINE = 0x00001000
|
||||||
|
_FILE_ATTRIBUTE_RECALL_ON_OPEN = 0x00040000
|
||||||
|
_FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS = 0x00400000
|
||||||
|
_CLOUD_MASK = _FILE_ATTRIBUTE_OFFLINE | _FILE_ATTRIBUTE_RECALL_ON_OPEN | _FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS
|
||||||
|
|
||||||
|
|
||||||
|
def is_cloud_placeholder(path: str) -> bool:
|
||||||
|
"""Vrátí True pokud soubor není lokálně stažený (Dropbox/OneDrive placeholder)."""
|
||||||
|
attrs = ctypes.windll.kernel32.GetFileAttributesW(path)
|
||||||
|
if attrs == 0xFFFFFFFF: # INVALID_FILE_ATTRIBUTES
|
||||||
|
return False
|
||||||
|
return bool(attrs & _CLOUD_MASK)
|
||||||
|
|
||||||
|
|
||||||
def blake3_file(path, chunk_size=1024 * 1024):
|
def blake3_file(path, chunk_size=1024 * 1024):
|
||||||
"""
|
"""Spočítá BLAKE3 hash souboru po blocích (bez načtení do paměti)."""
|
||||||
Spočítá BLAKE3 hash souboru po blocích (bez načtení do paměti)
|
|
||||||
"""
|
|
||||||
h = blake3()
|
h = blake3()
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
for chunk in iter(lambda: f.read(chunk_size), b""):
|
for chunk in iter(lambda: f.read(chunk_size), b""):
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from indexer.db import (
|
|||||||
)
|
)
|
||||||
from indexer.events import batch_log_events
|
from indexer.events import batch_log_events
|
||||||
from indexer.backup import ensure_backed_up
|
from indexer.backup import ensure_backed_up
|
||||||
|
from indexer.hasher import is_cloud_placeholder
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -72,15 +73,23 @@ def main():
|
|||||||
files_to_backup = []
|
files_to_backup = []
|
||||||
|
|
||||||
# 5a) NEW files — compute BLAKE3, batch INSERT
|
# 5a) NEW files — compute BLAKE3, batch INSERT
|
||||||
|
skipped_files = []
|
||||||
|
new_files = []
|
||||||
if new_paths:
|
if new_paths:
|
||||||
print(f" Hashing {len(new_paths)} new files...")
|
print(f" Hashing {len(new_paths)} new files...")
|
||||||
new_files = []
|
new_files = []
|
||||||
for p in new_paths:
|
for p in new_paths:
|
||||||
f = fs[p]
|
f = fs[p]
|
||||||
|
if is_cloud_placeholder(f["full_path"]):
|
||||||
|
reason = "not synced (cloud placeholder)"
|
||||||
|
print(f" WARN: skip {p}: {reason}")
|
||||||
|
skipped_files.append((p, reason))
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
content_hash = blake3_file(f["full_path"])
|
content_hash = blake3_file(f["full_path"])
|
||||||
except (FileNotFoundError, PermissionError, OSError) as e:
|
except (FileNotFoundError, PermissionError, OSError) as e:
|
||||||
print(f" WARN: skip {p}: {e}")
|
print(f" WARN: skip {p}: {e}")
|
||||||
|
skipped_files.append((p, str(e)))
|
||||||
continue
|
continue
|
||||||
new_files.append({
|
new_files.append({
|
||||||
"relative_path": p,
|
"relative_path": p,
|
||||||
@@ -168,10 +177,11 @@ def main():
|
|||||||
# ── 7. Finalize ──
|
# ── 7. Finalize ──
|
||||||
stats = {
|
stats = {
|
||||||
"total": len(fs),
|
"total": len(fs),
|
||||||
"new": len(new_paths),
|
"new": len(new_files) if new_paths else 0,
|
||||||
"modified": len(modified_paths),
|
"modified": len(modified_paths),
|
||||||
"deleted": len(deleted_paths),
|
"deleted": len(deleted_paths),
|
||||||
"unchanged": len(unchanged_paths),
|
"unchanged": len(unchanged_paths),
|
||||||
|
"skipped": len(skipped_files),
|
||||||
}
|
}
|
||||||
finalize_run(cur, run_id, stats)
|
finalize_run(cur, run_id, stats)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -196,13 +206,23 @@ def main():
|
|||||||
print(f"Modified : {stats['modified']}")
|
print(f"Modified : {stats['modified']}")
|
||||||
print(f"Deleted : {stats['deleted']}")
|
print(f"Deleted : {stats['deleted']}")
|
||||||
print(f"Unchanged: {stats['unchanged']}")
|
print(f"Unchanged: {stats['unchanged']}")
|
||||||
|
if skipped_files:
|
||||||
|
print(f"Skipped : {len(skipped_files)} (hash failed)")
|
||||||
|
print("-" * 60)
|
||||||
|
for path, reason in skipped_files:
|
||||||
|
print(f" SKIP: {path}")
|
||||||
|
print(f" {reason}")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
# ── 8. Generate Excel report ──
|
# ── 8. Generate Excel report ──
|
||||||
|
report_path = None
|
||||||
try:
|
try:
|
||||||
from report import generate_report
|
from report import generate_report
|
||||||
|
|
||||||
report_dir = r"u:\Dropbox\!!!Days\Downloads Z230"
|
report_dir = r"z:\Dropbox\!!!Days\Downloads Z230"
|
||||||
|
for f in os.listdir(report_dir):
|
||||||
|
if f.endswith("DropboxBackupReport.xlsx"):
|
||||||
|
os.remove(os.path.join(report_dir, f))
|
||||||
timestamp = datetime.now().strftime("%Y-%m-%d %H_%M")
|
timestamp = datetime.now().strftime("%Y-%m-%d %H_%M")
|
||||||
report_path = os.path.join(report_dir, f"{timestamp} DropboxBackupReport.xlsx")
|
report_path = os.path.join(report_dir, f"{timestamp} DropboxBackupReport.xlsx")
|
||||||
print(f"\n[8] Generating report...")
|
print(f"\n[8] Generating report...")
|
||||||
@@ -210,6 +230,70 @@ def main():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" WARN: Report generation failed: {e}")
|
print(f" WARN: Report generation failed: {e}")
|
||||||
|
|
||||||
|
# ── 9. Send email notification ──
|
||||||
|
try:
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, r"C:\Reporting\knihovny")
|
||||||
|
from EmailMessagingGraph import send_mail
|
||||||
|
|
||||||
|
ts = datetime.now().strftime("%d.%m.%Y %H:%M")
|
||||||
|
changes = stats['new'] + stats['modified'] + stats['deleted']
|
||||||
|
report_line = f"<tr><td>Report</td><td>{report_path}</td></tr>" if report_path else ""
|
||||||
|
|
||||||
|
skipped_row = ""
|
||||||
|
skipped_detail = ""
|
||||||
|
if skipped_files:
|
||||||
|
skipped_row = f"<tr style='background:#fff3cd;color:#856404;'><td><b>Preskocene</b></td><td>{len(skipped_files):,}</td></tr>"
|
||||||
|
rows = "".join(f"<tr><td>{p}</td><td>{r}</td></tr>" for p, r in skipped_files)
|
||||||
|
skipped_detail = f"""
|
||||||
|
<h3 style="color:#856404;">⚠ Preskocene soubory ({len(skipped_files)})</h3>
|
||||||
|
<table border="0" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:12px;">
|
||||||
|
<tr style="background:#f0f4fa;"><td><b>Soubor</b></td><td><b>Duvod</b></td></tr>
|
||||||
|
{rows}
|
||||||
|
</table>"""
|
||||||
|
|
||||||
|
def _file_section(title, color, paths):
|
||||||
|
if not paths:
|
||||||
|
return ""
|
||||||
|
rows = "".join(f"<tr><td style='padding:2px 8px;font-size:12px;'>{p}</td></tr>" for p in sorted(paths))
|
||||||
|
return f"""
|
||||||
|
<h3 style="color:{color};margin-top:18px;">{title} ({len(paths)})</h3>
|
||||||
|
<table border="0" cellpadding="2" cellspacing="0" style="border-collapse:collapse;width:100%;font-family:monospace;">
|
||||||
|
{rows}
|
||||||
|
</table>"""
|
||||||
|
|
||||||
|
new_paths_ok = [nf["relative_path"] for nf in new_files]
|
||||||
|
files_detail = (
|
||||||
|
_file_section("✓ Nove soubory", "#2a7a2a", new_paths_ok)
|
||||||
|
+ _file_section("✎ Zmenene soubory", "#a07000", list(modified_paths))
|
||||||
|
+ _file_section("✗ Smazane soubory", "#a00000", list(deleted_paths))
|
||||||
|
)
|
||||||
|
|
||||||
|
body = f"""
|
||||||
|
<html><body style="font-family:Segoe UI,Arial,sans-serif;font-size:14px;color:#222;">
|
||||||
|
<h2 style="color:#2e6da4;">✓ Dropbox Ordinace Backup – {ts}</h2>
|
||||||
|
<table border="0" cellpadding="6" cellspacing="0" style="border-collapse:collapse;min-width:350px;">
|
||||||
|
<tr style="background:#f0f4fa;"><td><b>Run #</b></td><td>{run_id}</td></tr>
|
||||||
|
<tr><td><b>Celkem souboru</b></td><td>{stats['total']:,}</td></tr>
|
||||||
|
<tr style="background:#f0f4fa;color:#2a7a2a;"><td><b>Nove</b></td><td>{stats['new']:,}</td></tr>
|
||||||
|
<tr style="color:#a07000;"><td><b>Zmenene</b></td><td>{stats['modified']:,}</td></tr>
|
||||||
|
<tr style="background:#f0f4fa;color:#a00000;"><td><b>Smazane</b></td><td>{stats['deleted']:,}</td></tr>
|
||||||
|
<tr><td><b>Nezmenene</b></td><td>{stats['unchanged']:,}</td></tr>
|
||||||
|
<tr style="background:#f0f4fa;"><td><b>Zmen celkem</b></td><td>{changes:,}</td></tr>
|
||||||
|
{skipped_row}
|
||||||
|
{report_line}
|
||||||
|
</table>
|
||||||
|
{files_detail}
|
||||||
|
{skipped_detail}
|
||||||
|
<p style="color:#888;font-size:12px;margin-top:20px;">REPORTER • {ts}</p>
|
||||||
|
</body></html>
|
||||||
|
"""
|
||||||
|
subject = f"Dropbox Backup #{run_id} \u2013 {ts} ({changes} zmen)"
|
||||||
|
send_mail("vladimir.buzalka@buzalka.cz", subject, body, html=True)
|
||||||
|
print(f"\n[9] Email odeslan na vladimir.buzalka@buzalka.cz")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" WARN: Email failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ from indexer.config import BACKUP_PATH, BACKUP_PASSWORD
|
|||||||
from indexer.db import get_connection
|
from indexer.db import get_connection
|
||||||
from indexer.backup import blob_path
|
from indexer.backup import blob_path
|
||||||
|
|
||||||
DEFAULT_OUTPUT_DIR = r"U:\recovery"
|
DEFAULT_OUTPUT_DIR = r"\\tower\Pomoc\Recovery"
|
||||||
|
|
||||||
|
|
||||||
def show_last_runs(n: int = 10):
|
def show_last_runs(n: int = 10):
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ def generate_report(output_path: str):
|
|||||||
cell.alignment = Alignment(horizontal="center")
|
cell.alignment = Alignment(horizontal="center")
|
||||||
cell.border = thin_border
|
cell.border = thin_border
|
||||||
|
|
||||||
for row_idx, ev in enumerate(all_events, 2):
|
for row_idx, ev in enumerate(reversed(all_events), 2):
|
||||||
run_id, started, event_type, rel_path, file_name, directory, old_size, new_size = ev
|
run_id, started, event_type, rel_path, file_name, directory, old_size, new_size = ev
|
||||||
|
|
||||||
size_change = ""
|
size_change = ""
|
||||||
@@ -115,7 +115,7 @@ def generate_report(output_path: str):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
REPORT_DIR = r"u:\Dropbox\!!!Days\Downloads Z230"
|
REPORT_DIR = r"z:\Dropbox\!!!Days\Downloads Z230"
|
||||||
timestamp = dt.now().strftime("%Y-%m-%d %H_%M")
|
timestamp = dt.now().strftime("%Y-%m-%d %H_%M")
|
||||||
default_name = f"{timestamp} DropboxBackupReport.xlsx"
|
default_name = f"{timestamp} DropboxBackupReport.xlsx"
|
||||||
output = sys.argv[1] if len(sys.argv) > 1 else os.path.join(REPORT_DIR, default_name)
|
output = sys.argv[1] if len(sys.argv) > 1 else os.path.join(REPORT_DIR, default_name)
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@echo off
|
||||||
|
cd /d C:\Reporting\DropboxBackup
|
||||||
|
C:\Reporting\Python\python.exe main.py
|
||||||
Reference in New Issue
Block a user