"""Account result persistence and pending recovery helpers.""" import json import os import tempfile from datetime import datetime, timezone from filelock import FileLock def append_account_line(path, email, password, sso): with open(path, "a", encoding="utf-8") as handle: handle.write(f"{email}----{password}----{sso}\n") handle.flush() os.fsync(handle.fileno()) def save_mail_credential(base_dir, email, credential): path = os.path.join(base_dir, "mail_credentials.txt") with open(path, "a", encoding="utf-8") as handle: handle.write(f"{email}\t{credential}\n") handle.flush() os.fsync(handle.fileno()) return True def queue_unsaved_account(path, payload, error): pending_path = path + ".pending.jsonl" record = dict(payload) record["save_error"] = str(error) record["queued_at"] = datetime.now(timezone.utc).isoformat() with open(pending_path, "a", encoding="utf-8") as handle: handle.write(json.dumps(record, ensure_ascii=False) + "\n") handle.flush() os.fsync(handle.fileno()) try: os.chmod(pending_path, 0o600) except Exception: pass return True def _existing_account_keys(target_path): keys = set() if not os.path.isfile(target_path): return keys with open(target_path, "r", encoding="utf-8", errors="replace") as handle: for raw_line in handle: parts = raw_line.rstrip("\n").split("----", 2) if len(parts) == 3: keys.add((parts[0].strip(), parts[2].strip())) return keys def retry_pending_file(pending_path, output_path=None, log_callback=None): logger = log_callback or (lambda message: None) pending_path = os.path.realpath(os.path.abspath(os.path.expanduser(str(pending_path)))) if not os.path.isfile(pending_path): raise FileNotFoundError(f"pending 文件不存在: {pending_path}") suffix = ".pending.jsonl" if output_path: target_path = os.path.realpath(os.path.abspath(os.path.expanduser(str(output_path)))) elif pending_path.endswith(suffix): target_path = os.path.realpath(pending_path[:-len(suffix)]) else: target_path = os.path.realpath(pending_path + ".recovered.txt") if os.path.normcase(pending_path) == os.path.normcase(target_path): raise ValueError("pending 输入文件与输出文件不能是同一个文件") lock_path = pending_path + ".lock" with FileLock(lock_path, timeout=30): if not os.path.isfile(pending_path): return {"restored": 0, "remaining": 0, "output_path": target_path} with open(pending_path, "r", encoding="utf-8") as handle: lines = handle.readlines() existing = _existing_account_keys(target_path) unresolved = [] restored = 0 for line_number, raw_line in enumerate(lines, 1): stripped = raw_line.strip() if not stripped: continue try: record = json.loads(stripped) if not isinstance(record, dict): raise ValueError("record must be a JSON object") email = str(record.get("email") or "").strip() password = str(record.get("password") or "") sso = str(record.get("sso") or "").strip() if not email or not sso: raise ValueError("record missing email or sso") key = (email, sso) if key not in existing: append_account_line(target_path, email, password, sso) existing.add(key) restored += 1 logger(f"[+] 已恢复 pending 账号: {email}") except Exception as exc: unresolved.append(raw_line if raw_line.endswith("\n") else raw_line + "\n") logger(f"[!] pending 第 {line_number} 行恢复失败: {exc}") directory = os.path.dirname(pending_path) or "." fd, temp_path = tempfile.mkstemp(prefix=".pending-retry-", suffix=".jsonl.tmp", dir=directory) try: with os.fdopen(fd, "w", encoding="utf-8") as handle: handle.writelines(unresolved) handle.flush() os.fsync(handle.fileno()) if unresolved: os.replace(temp_path, pending_path) temp_path = None try: os.chmod(pending_path, 0o600) except Exception: pass else: os.unlink(temp_path) temp_path = None try: os.unlink(pending_path) except FileNotFoundError: pass finally: if temp_path and os.path.exists(temp_path): os.unlink(temp_path) return {"restored": restored, "remaining": len(unresolved), "output_path": target_path}