gates / gates (push) Successful in 6m30s
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
121 lines
6.1 KiB
Python
121 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
"""k3.py create|state|pause|resume [drive] — K3 (a safe removal), run INSIDE the Tester 1 guest.
|
|
|
|
create makes ONE torrent of our own random bytes (3 MB, private, no tracker) per client on Media C and adds it so it
|
|
seeds: qBittorrent (category tv-drive-c → media_c/userdata/media/torrents/tv), Transmission
|
|
(media_c/userdata/media/transmission/tv).
|
|
state every torrent: client, drive, state, progress — the „did anything else stop / re-check" record.
|
|
pause pause ONLY the torrents whose save folder is on <drive> (qBittorrent by save_path, Transmission by downloadDir).
|
|
resume start them again.
|
|
Passwords: the deploy secrets read from the containers' environment; never printed.
|
|
"""
|
|
import base64, hashlib, json, os, re, subprocess, sys, time, urllib.error, urllib.parse, urllib.request
|
|
|
|
DRV = sys.argv[2] if len(sys.argv) > 2 else "media_c"
|
|
M = f"/mnt/felhom-drives/{DRV}/userdata/media"
|
|
|
|
|
|
def sh(c):
|
|
return subprocess.run(c, shell=True, capture_output=True, text=True).stdout.strip()
|
|
|
|
|
|
def ip(c):
|
|
return sh(f"docker inspect {c} -f '{{{{range .NetworkSettings.Networks}}}}{{{{.IPAddress}}}}{{{{end}}}}'")
|
|
|
|
|
|
def be(x):
|
|
if isinstance(x, int): return b"i%de" % x
|
|
if isinstance(x, bytes): return b"%d:%s" % (len(x), x)
|
|
if isinstance(x, str): return be(x.encode())
|
|
if isinstance(x, list): return b"l" + b"".join(be(i) for i in x) + b"e"
|
|
return b"d" + b"".join(be(k) + be(x[k]) for k in sorted(x)) + b"e"
|
|
|
|
|
|
def mktorrent(d):
|
|
L = 262144
|
|
files = sorted(os.listdir(d)); data = b"".join(open(os.path.join(d, f), "rb").read() for f in files)
|
|
info = {"name": os.path.basename(d), "piece length": L, "private": 1,
|
|
"pieces": b"".join(hashlib.sha1(data[i:i+L]).digest() for i in range(0, len(data), L)),
|
|
"files": [{"length": os.path.getsize(os.path.join(d, f)), "path": [f]} for f in files]}
|
|
return be({"info": info, "created by": "felhom K3 (own random bytes)"})
|
|
|
|
|
|
QB = f"http://{ip('qbittorrent')}:8080"
|
|
qpw = sh("docker exec qbittorrent printenv QBT_ADMIN_PASSWORD")
|
|
req = urllib.request.Request(QB + "/api/v2/auth/login", data=urllib.parse.urlencode({"username": "admin", "password": qpw}).encode(), headers={"Referer": QB})
|
|
with urllib.request.urlopen(req) as r:
|
|
m = re.search(r"((?:QBT_)?SID[A-Za-z0-9_]*)=([^;]+)", r.headers.get("Set-Cookie", ""))
|
|
QH = {"Referer": QB, "Cookie": f"{m.group(1)}={m.group(2)}"}
|
|
|
|
|
|
def qb(path, form=None, files=None):
|
|
if files:
|
|
bnd = "felhomk3"
|
|
body = b""
|
|
for k, v in (form or {}).items():
|
|
body += f"--{bnd}\r\nContent-Disposition: form-data; name=\"{k}\"\r\n\r\n{v}\r\n".encode()
|
|
for k, (fn, data) in files.items():
|
|
body += f"--{bnd}\r\nContent-Disposition: form-data; name=\"{k}\"; filename=\"{fn}\"\r\nContent-Type: application/x-bittorrent\r\n\r\n".encode() + data + b"\r\n"
|
|
body += f"--{bnd}--\r\n".encode()
|
|
r = urllib.request.Request(QB + path, data=body, headers=dict(QH, **{"Content-Type": f"multipart/form-data; boundary={bnd}"}))
|
|
elif form is not None:
|
|
r = urllib.request.Request(QB + path, data=urllib.parse.urlencode(form).encode(), headers=QH)
|
|
else:
|
|
r = urllib.request.Request(QB + path, headers=QH)
|
|
with urllib.request.urlopen(r, timeout=60) as x:
|
|
t = x.read().decode()
|
|
try: return json.loads(t)
|
|
except ValueError: return t
|
|
|
|
|
|
TR = f"http://{ip('transmission')}:9091/transmission/rpc"
|
|
tpw = sh("docker exec transmission printenv PASS")
|
|
TA = {"Authorization": "Basic " + base64.b64encode(f"admin:{tpw}".encode()).decode()}
|
|
|
|
|
|
def tr(method, args=None):
|
|
h = dict(TA)
|
|
for _ in range(2):
|
|
r = urllib.request.Request(TR, data=json.dumps({"method": method, "arguments": args or {}}).encode(), headers=h)
|
|
try:
|
|
with urllib.request.urlopen(r, timeout=60) as x: return json.load(x)
|
|
except urllib.error.HTTPError as e:
|
|
if e.code == 409: h["X-Transmission-Session-Id"] = e.headers.get("X-Transmission-Session-Id"); continue
|
|
raise
|
|
|
|
|
|
def state():
|
|
out = []
|
|
for t in qb("/api/v2/torrents/info"):
|
|
out.append(("qbit", t["save_path"].split("/")[3], t["name"][:30], t["state"], round(t["progress"], 4)))
|
|
for t in tr("torrent-get", {"fields": ["name", "downloadDir", "status", "percentDone", "error", "errorString"]})["arguments"]["torrents"]:
|
|
st = {0: "stopped", 1: "check-wait", 2: "checking", 3: "dl-wait", 4: "downloading", 5: "seed-wait", 6: "seeding"}.get(t["status"], t["status"])
|
|
out.append(("trans", t["downloadDir"].split("/")[3], t["name"][:30], st + (" ERR " + t["errorString"][:30] if t["error"] else ""), round(t["percentDone"], 4)))
|
|
return sorted(out)
|
|
|
|
|
|
step = sys.argv[1]
|
|
if step == "create":
|
|
for client, sub in (("qbit", "torrents/tv"), ("trans", "transmission/tv")):
|
|
d = f"{M}/{sub}/k3-own-bytes-{client}"
|
|
os.makedirs(d, exist_ok=True)
|
|
with open(f"{d}/data.bin", "wb") as f: f.write(os.urandom(3_000_000))
|
|
sh(f"chown -R 1000:1000 '{d}'")
|
|
tor = mktorrent(d)
|
|
if client == "qbit":
|
|
print("qbit add ->", qb("/api/v2/torrents/add", {"category": (sys.argv[3] if len(sys.argv) > 3 else "tv-drive-c")}, {"torrents": ("k3.torrent", tor)}))
|
|
else:
|
|
print("trans add ->", json.dumps(tr("torrent-add", {"metainfo": base64.b64encode(tor).decode(), "download-dir": f"{M}/transmission/tv"}))[:160])
|
|
time.sleep(20)
|
|
for s in state(): print(" ", s)
|
|
elif step == "state":
|
|
print(f"--- {time.strftime('%H:%M:%S')}")
|
|
for s in state(): print(" ", s)
|
|
elif step in ("pause", "resume"):
|
|
hs = [t["hash"] for t in qb("/api/v2/torrents/info") if t["save_path"].startswith(M)]
|
|
print(f"qbit {step} {len(hs)} torrent(s) on {DRV} ->", qb(f"/api/v2/torrents/{'stop' if step == 'pause' else 'start'}", {"hashes": "|".join(hs)}) if hs else "none")
|
|
ids = [t["id"] for t in tr("torrent-get", {"fields": ["id", "downloadDir"]})["arguments"]["torrents"] if t["downloadDir"].startswith(M)]
|
|
print(f"trans {step} {len(ids)} torrent(s) on {DRV} ->", tr("torrent-stop" if step == "pause" else "torrent-start", {"ids": ids})["result"] if ids else "none")
|
|
time.sleep(5)
|
|
for s in state(): print(" ", s)
|