sync
This commit is contained in:
@@ -1,61 +1,202 @@
|
||||
import os
|
||||
import zlib
|
||||
import glob
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from core.connection import BuzzerError
|
||||
|
||||
def get_file_crc32(filepath: str) -> int:
|
||||
"""Berechnet die IEEE CRC32-Prüfsumme einer Datei in Chunks."""
|
||||
crc = 0
|
||||
with open(filepath, 'rb') as f:
|
||||
while chunk := f.read(4096):
|
||||
crc = zlib.crc32(chunk, crc)
|
||||
return crc & 0xFFFFFFFF
|
||||
TAG_MAGIC = b"TAG!"
|
||||
TAG_FOOTER_LEN = 7
|
||||
TAG_VERSION_V1 = 0x01
|
||||
TAG_TYPE_CRC32 = 0x10
|
||||
|
||||
def execute(conn, sources: list, target: str):
|
||||
# 1. Globbing auflösen
|
||||
resolved_files = [f for src in sources for f in glob.glob(src) if os.path.isfile(f)]
|
||||
|
||||
if not resolved_files:
|
||||
|
||||
def _split_audio_and_tag_blob(filepath: str) -> tuple[bytes, bytes | None]:
|
||||
with open(filepath, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
if len(data) < TAG_FOOTER_LEN:
|
||||
return data, None
|
||||
|
||||
if data[-4:] != TAG_MAGIC:
|
||||
return data, None
|
||||
|
||||
tag_total_len = int.from_bytes(data[-6:-4], byteorder="little", signed=False)
|
||||
tag_version = data[-7]
|
||||
if tag_version != TAG_VERSION_V1:
|
||||
return data, None
|
||||
|
||||
if tag_total_len < TAG_FOOTER_LEN or tag_total_len > len(data):
|
||||
return data, None
|
||||
|
||||
audio_end = len(data) - tag_total_len
|
||||
tag_payload_len = tag_total_len - TAG_FOOTER_LEN
|
||||
tag_payload = data[audio_end:audio_end + tag_payload_len]
|
||||
audio_data = data[:audio_end]
|
||||
return audio_data, tag_payload
|
||||
|
||||
|
||||
def _upsert_crc32_tag(tag_blob: bytes | None, crc32: int) -> tuple[bytes, bool]:
|
||||
crc_payload = int(crc32).to_bytes(4, byteorder="little", signed=False)
|
||||
crc_tlv = bytes([TAG_TYPE_CRC32, 0x04, 0x00]) + crc_payload
|
||||
|
||||
if not tag_blob:
|
||||
return crc_tlv, True
|
||||
|
||||
pos = 0
|
||||
out = bytearray()
|
||||
found_crc = False
|
||||
|
||||
while pos < len(tag_blob):
|
||||
if pos + 3 > len(tag_blob):
|
||||
return crc_tlv, True
|
||||
|
||||
tag_type = tag_blob[pos]
|
||||
tag_len = tag_blob[pos + 1] | (tag_blob[pos + 2] << 8)
|
||||
header = tag_blob[pos:pos + 3]
|
||||
pos += 3
|
||||
|
||||
if pos + tag_len > len(tag_blob):
|
||||
return crc_tlv, True
|
||||
|
||||
value = tag_blob[pos:pos + tag_len]
|
||||
pos += tag_len
|
||||
|
||||
if tag_type == TAG_TYPE_CRC32:
|
||||
if not found_crc:
|
||||
out.extend(crc_tlv)
|
||||
found_crc = True
|
||||
continue
|
||||
|
||||
out.extend(header)
|
||||
out.extend(value)
|
||||
|
||||
if not found_crc:
|
||||
out.extend(crc_tlv)
|
||||
|
||||
return bytes(out), True
|
||||
|
||||
|
||||
def _collect_source_files(sources: list[str], recursive: bool) -> list[dict]:
|
||||
entries = []
|
||||
|
||||
for source in sources:
|
||||
matches = glob.glob(source)
|
||||
if not matches:
|
||||
print(f"⚠️ Keine Treffer für Quelle: {source}")
|
||||
continue
|
||||
|
||||
for match in matches:
|
||||
if os.path.isfile(match):
|
||||
entries.append({"local": match, "relative": os.path.basename(match)})
|
||||
continue
|
||||
|
||||
if os.path.isdir(match):
|
||||
if recursive:
|
||||
for root, _, files in os.walk(match):
|
||||
for name in sorted(files):
|
||||
local_path = os.path.join(root, name)
|
||||
rel = os.path.relpath(local_path, match)
|
||||
entries.append({"local": local_path, "relative": rel.replace("\\", "/")})
|
||||
else:
|
||||
for name in sorted(os.listdir(match)):
|
||||
local_path = os.path.join(match, name)
|
||||
if os.path.isfile(local_path):
|
||||
entries.append({"local": local_path, "relative": name})
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def _remote_parent(path: str) -> str:
|
||||
idx = path.rfind("/")
|
||||
if idx <= 0:
|
||||
return "/"
|
||||
return path[:idx]
|
||||
|
||||
|
||||
def _ensure_remote_dir(conn, remote_dir: str) -> None:
|
||||
if not remote_dir or remote_dir == "/":
|
||||
return
|
||||
|
||||
current = ""
|
||||
for part in [p for p in remote_dir.split("/") if p]:
|
||||
current = f"{current}/{part}"
|
||||
try:
|
||||
conn.mkdir(current)
|
||||
except BuzzerError as e:
|
||||
msg = str(e)
|
||||
if "0x11" in msg or "existiert bereits" in msg:
|
||||
continue
|
||||
raise
|
||||
|
||||
|
||||
def _build_upload_plan(entries: list[dict], target: str) -> list[dict]:
|
||||
if not entries:
|
||||
return []
|
||||
|
||||
needs_dir_semantics = target.endswith("/") or len(entries) > 1 or any("/" in e["relative"] for e in entries)
|
||||
if not needs_dir_semantics:
|
||||
return [{"local": entries[0]["local"], "remote": target}]
|
||||
|
||||
base = target.rstrip("/")
|
||||
if not base:
|
||||
base = "/"
|
||||
|
||||
plan = []
|
||||
for entry in entries:
|
||||
rel = entry["relative"].lstrip("/")
|
||||
if base == "/":
|
||||
remote = f"/{rel}"
|
||||
else:
|
||||
remote = f"{base}/{rel}"
|
||||
plan.append({"local": entry["local"], "remote": remote})
|
||||
|
||||
return plan
|
||||
|
||||
|
||||
def execute(conn, sources: list[str], target: str, recursive: bool = False):
|
||||
uploads = _build_upload_plan(_collect_source_files(sources, recursive=recursive), target)
|
||||
if not uploads:
|
||||
print("Keine gültigen Dateien gefunden.")
|
||||
return
|
||||
|
||||
total_size_all = sum(os.path.getsize(f) for f in resolved_files)
|
||||
total_size_all = sum(os.path.getsize(item["local"]) for item in uploads)
|
||||
sent_all = 0
|
||||
start_time_all = time.monotonic()
|
||||
is_target_dir = target.endswith('/')
|
||||
last_ui_update = start_time_all
|
||||
|
||||
for filepath in resolved_files:
|
||||
filename = os.path.basename(filepath)
|
||||
filesize = os.path.getsize(filepath)
|
||||
crc32 = get_file_crc32(filepath)
|
||||
dest_path = f"{target}{filename}" if is_target_dir else target
|
||||
|
||||
print(f"Sende 📄 {filename} ({filesize/1024:.1f} KB) -> {dest_path}")
|
||||
|
||||
for item in uploads:
|
||||
local_path = item["local"]
|
||||
remote_path = item["remote"]
|
||||
filename = os.path.basename(local_path)
|
||||
|
||||
audio_data, tag_blob = _split_audio_and_tag_blob(local_path)
|
||||
audio_size = len(audio_data)
|
||||
|
||||
_ensure_remote_dir(conn, _remote_parent(remote_path))
|
||||
|
||||
print(f"Sende 📄 {filename} ({audio_size / 1024:.1f} KB Audio) -> {remote_path}")
|
||||
start_time_file = time.monotonic()
|
||||
sent_file = 0
|
||||
|
||||
def progress_handler(chunk_len):
|
||||
nonlocal sent_file, sent_all
|
||||
sent_file += chunk_len
|
||||
def progress_handler(chunk_len, sent_file, total_file):
|
||||
nonlocal sent_all, last_ui_update
|
||||
sent_all += chunk_len
|
||||
|
||||
elapsed = time.monotonic() - start_time_file
|
||||
speed = (sent_file / 1024) / elapsed if elapsed > 0 else 0
|
||||
|
||||
# Prozentberechnungen
|
||||
perc_file = (sent_file / filesize) * 100
|
||||
perc_all = (sent_all / total_size_all) * 100
|
||||
|
||||
# ETA (Basierend auf Gesamtgeschwindigkeit)
|
||||
elapsed_all = time.monotonic() - start_time_all
|
||||
avg_speed_all = sent_all / elapsed_all if elapsed_all > 0 else 0
|
||||
eta_sec = (total_size_all - sent_all) / avg_speed_all if avg_speed_all > 0 else 0
|
||||
|
||||
now = time.monotonic()
|
||||
if now - last_ui_update < 0.2 and sent_file < total_file:
|
||||
return
|
||||
last_ui_update = now
|
||||
|
||||
elapsed = now - start_time_file
|
||||
speed = (sent_file / 1024.0) / elapsed if elapsed > 0 else 0.0
|
||||
perc_file = (sent_file / total_file) * 100.0 if total_file > 0 else 100.0
|
||||
perc_all = (sent_all / total_size_all) * 100.0 if total_size_all > 0 else 100.0
|
||||
|
||||
elapsed_all = now - start_time_all
|
||||
avg_speed_all = sent_all / elapsed_all if elapsed_all > 0 else 0.0
|
||||
eta_sec = (total_size_all - sent_all) / avg_speed_all if avg_speed_all > 0 else 0.0
|
||||
eta_str = f"{int(eta_sec // 60):02d}:{int(eta_sec % 60):02d}"
|
||||
|
||||
# Ausgabezeile (\r überschreibt die aktuelle Zeile)
|
||||
sys.stdout.write(
|
||||
f"\r \033[90mProg: {perc_file:3.0f}% | Gesamt: {perc_all:3.0f}% | "
|
||||
f"{speed:6.1f} KB/s | ETA: {eta_str}\033[0m"
|
||||
@@ -63,20 +204,17 @@ def execute(conn, sources: list, target: str):
|
||||
sys.stdout.flush()
|
||||
|
||||
try:
|
||||
cmd = f"put {dest_path};{filesize};{crc32}\n"
|
||||
conn.serial.write(cmd.encode('utf-8'))
|
||||
conn.serial.flush()
|
||||
|
||||
# Binärtransfer mit unserem Handler
|
||||
conn.send_binary(filepath, progress_callback=progress_handler)
|
||||
|
||||
# Zeile nach Erfolg abschließen
|
||||
print(f"\r \033[32mFertig: {filename} übertragen. \033[0m")
|
||||
audio_crc32 = conn.put_file_data(remote_path, audio_data, progress_callback=progress_handler)
|
||||
|
||||
rewritten_blob, _ = _upsert_crc32_tag(tag_blob, audio_crc32)
|
||||
conn.set_tag_blob(remote_path, rewritten_blob)
|
||||
tag_note = " (CRC32-Tag gesetzt)"
|
||||
|
||||
print(f"\r \033[32mFertig: {filename} übertragen{tag_note}.{' ' * 20}\033[0m")
|
||||
except Exception as e:
|
||||
print(f"\n ❌ \033[31mFehler: {e}\033[0m")
|
||||
|
||||
total_duration = time.monotonic() - start_time_all
|
||||
total_kb = total_size_all / 1024
|
||||
avg_speed = total_kb / total_duration if total_duration > 0 else 0
|
||||
|
||||
total_kb = total_size_all / 1024.0
|
||||
avg_speed = total_kb / total_duration if total_duration > 0 else 0.0
|
||||
print(f"\nÜbertragung abgeschlossen: {total_kb:.1f} KB in {total_duration:.1f}s ({avg_speed:.1f} KB/s)")
|
||||
Reference in New Issue
Block a user