Collect authenticated Qinglong scripts batch

This commit is contained in:
Hermes Agent
2026-05-24 03:53:42 +00:00
parent 0d80b33d25
commit 28d62fef03
86 changed files with 18420 additions and 5 deletions

View File

@@ -0,0 +1,236 @@
# Source: https://github.com/CN-Grace/QinglongScripts/blob/main/Sync_Password.py
# Raw: https://raw.githubusercontent.com/CN-Grace/QinglongScripts/main/Sync_Password.py
# Repo: CN-Grace/QinglongScripts
# Path: Sync_Password.py
# UploadedAt: 2026-05-23T15:48:24Z
# SHA256: e63fb7c92c4a77f2dc4660a1ce3f0401a8173132dc4b8d36a99c7d383f91974e
# Category: web版/账密
# Evidence: web/H5关键词 + username/password/login
#!/usr/bin/env python3
"""
cron: 0 0 * * *
new Env("Bitwarden备份")
Bitwarden 自动备份脚本(带哈希比较,避免重复同步)
- 自动登录 Bitwarden 服务器并获取所有密码数据
- 将密码数据保存到本地 JSON 文件
- 比较今天与昨天的备份文件哈希,若无变化则跳过 WebDAV 同步
- 将备份文件同步到 WebDAV 服务器(仅当有更新时)
"""
import os
import json
import hashlib
import requests
import datetime
from requests.auth import HTTPBasicAuth
from typing import Dict, Any, Tuple
from utils import log_info, log_success, log_warning, log_error, beijing_now, beijing_time_str
from notifier import send as notify_send, send_file as notify_send_file
# ==================== 用户配置 ====================
BITWARDEN_SERVER = os.environ.get("BITWARDEN_SERVER", "")
BITWARDEN_USERNAME = os.environ.get("BITWARDEN_USERNAME", "")
BITWARDEN_PASSWORD = os.environ.get("BITWARDEN_PASSWORD", "")
WEBDAV_SERVER = os.environ.get("WEBDAV_SERVER", "")
WEBDAV_USERNAME = os.environ.get("WEBDAV_USERNAME", "")
WEBDAV_PASSWORD = os.environ.get("WEBDAV_PASSWORD", "")
WEBDAV_REMOTE_DIR = os.environ.get("WEBDAV_REMOTE_DIR", "")
LOCAL_BACKUP_PATH = os.environ.get("SYNC_LOCAL_DIR", "Password/")
BACKUP_FILENAME_FORMAT = "%Y-%m-%d_bitwarden_backup.json"
def create_session() -> requests.Session:
session = requests.Session()
session.headers.update({"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "application/json, text/plain, */*"})
return session
def bitwarden_login(session: requests.Session) -> Tuple[bool, str]:
login_url = f"{BITWARDEN_SERVER.rstrip('/')}/identity/connect/token"
login_data = {
"scope": "api offline_access", "client_id": "web", "deviceType": "12",
"deviceIdentifier": "65ff3d73-fd5f-4835-8a50-fa7f41581f48", "deviceName": "edge",
"grant_type": "password", "username": BITWARDEN_USERNAME, "password": BITWARDEN_PASSWORD,
}
try:
log_info(f"正在登录 Bitwarden: {BITWARDEN_SERVER}")
resp = session.post(login_url, data=login_data, timeout=30)
resp.raise_for_status()
access_token = resp.json().get("access_token")
if access_token:
session.headers.update({"Authorization": f"Bearer {access_token}"})
log_success("Bitwarden 登录成功")
return True, "✅ Bitwarden 登录成功"
log_error("响应中未找到 access_token")
return False, "❌ Bitwarden 登录失败: 未找到 access_token"
except Exception as e:
log_error(f"Bitwarden 登录失败: {e}")
return False, f"❌ Bitwarden 登录失败: {e}"
def get_bitwarden_data(session: requests.Session) -> Tuple[bool, Any, str]:
sync_url = f"{BITWARDEN_SERVER.rstrip('/')}/api/sync"
try:
log_info("正在获取 Bitwarden 数据...")
resp = session.get(sync_url, params={"excludeDomains": "true"}, timeout=30)
resp.raise_for_status()
data = resp.json()
if data and isinstance(data, dict):
item_count = len(data.get("ciphers", []))
log_success(f"成功获取数据 ({item_count} 个密码项)")
return True, data, f"✅ 成功获取数据 ({item_count} 个密码项)"
return False, None, "⚠️ 数据为空或格式不正确"
except Exception as e:
log_error(f"获取数据失败: {e}")
return False, None, f"❌ 获取数据失败: {e}"
def save_backup_locally(data: Dict[str, Any]) -> Tuple[bool, str, str]:
if not os.path.exists(LOCAL_BACKUP_PATH):
os.makedirs(LOCAL_BACKUP_PATH, exist_ok=True)
today = beijing_now()
filename = today.strftime(BACKUP_FILENAME_FORMAT)
filepath = os.path.join(LOCAL_BACKUP_PATH, filename)
try:
with open(filepath, "w", encoding="utf-8") as f:
json.dump(data, f, indent=4, ensure_ascii=False)
file_size = os.path.getsize(filepath) / (1024 * 1024)
log_success(f"本地备份: {filename} ({file_size:.2f} MB)")
return True, filepath, f"✅ 本地备份成功: {filename} ({file_size:.2f} MB)"
except Exception as e:
log_error(f"本地备份失败: {e}")
return False, "", f"❌ 本地备份失败: {e}"
def get_file_hash(filepath: str) -> str:
hash_md5 = hashlib.md5()
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)
return hash_md5.hexdigest()
def sync_to_webdav(local_filepath: str) -> Tuple[bool, str]:
if not os.path.exists(local_filepath):
return False, "❌ WebDAV 同步失败: 本地文件不存在"
filename = os.path.basename(local_filepath)
remote_dir = WEBDAV_REMOTE_DIR.rstrip("/") if WEBDAV_REMOTE_DIR else "123Pan/Password"
remote_path = f"{WEBDAV_SERVER.rstrip('/')}/{remote_dir}/{filename}"
try:
with open(local_filepath, "rb") as f:
file_data = f.read()
log_info(f"正在上传到 WebDAV: {remote_path}")
resp = requests.put(url=remote_path, data=file_data, auth=HTTPBasicAuth(WEBDAV_USERNAME, WEBDAV_PASSWORD), headers={"Content-Type": "application/octet-stream"}, timeout=60)
if resp.status_code in (200, 201, 204):
log_success("WebDAV 同步成功")
return True, f"✅ WebDAV 同步成功: {filename}"
return False, f"❌ WebDAV 同步失败: 状态码 {resp.status_code}"
except Exception as e:
log_error(f"WebDAV 同步失败: {e}")
return False, f"❌ WebDAV 同步失败: {e}"
def build_report_content(log_messages: list) -> str:
lines = [
"🔐 Bitwarden 备份报告", "",
f"📅 备份时间: {beijing_time_str()}",
f"👤 账户: {BITWARDEN_USERNAME}", "",
"" * 18, "",
"📋 备份步骤状态:",
]
for i, msg in enumerate(log_messages, 1):
lines.append(f" {i}. {msg}")
success_count = sum(1 for m in log_messages if m.startswith(""))
info_count = sum(1 for m in log_messages if m.startswith("") or m.startswith("⚠️"))
skip_count = sum(1 for m in log_messages if m.startswith("⏭️"))
lines.extend(["", "" * 18, f"📊 统计: 成功 {success_count} | 信息 {info_count} | 跳过 {skip_count} | 总计 {len(log_messages)}", "", f"🕒 执行时间: {beijing_time_str()}"])
return "\n".join(lines)
def perform_backup() -> Tuple[bool, list, str, bool]:
log_messages = []
backup_filepath = None
has_update = True
session = create_session()
login_success, login_msg = bitwarden_login(session)
log_messages.append(login_msg)
if not login_success:
return False, log_messages, None, has_update
data_success, bitwarden_data, data_msg = get_bitwarden_data(session)
log_messages.append(data_msg)
if not data_success or bitwarden_data is None:
return False, log_messages, None, has_update
save_success, filepath, save_msg = save_backup_locally(bitwarden_data)
log_messages.append(save_msg)
backup_filepath = filepath if save_success else None
if save_success and backup_filepath:
today = beijing_now()
yesterday = today - datetime.timedelta(days=1)
yesterday_filename = yesterday.strftime(BACKUP_FILENAME_FORMAT)
yesterday_filepath = os.path.join(LOCAL_BACKUP_PATH, yesterday_filename)
if os.path.exists(yesterday_filepath):
if get_file_hash(backup_filepath) == get_file_hash(yesterday_filepath):
has_update = False
log_info("备份文件与昨天相同,无更新")
log_messages.append(" 备份文件与昨日一致,无新内容")
else:
log_info("备份文件有更新")
else:
log_info("昨日备份不存在,视为有更新")
if has_update:
sync_success, sync_msg = sync_to_webdav(backup_filepath)
log_messages.append(sync_msg)
else:
log_messages.append("⏭️ WebDAV 同步跳过: 文件无变化")
else:
log_messages.append("⏭️ WebDAV 同步跳过: 本地备份失败")
all_success = all(msg.startswith("") or msg.startswith("⏭️") or msg.startswith("") for msg in log_messages)
return all_success, log_messages, backup_filepath, has_update
def main():
log_info("=" * 50)
log_info("Bitwarden 自动备份脚本开始执行")
log_info("=" * 50)
start_time = beijing_now()
backup_success, log_messages, backup_filepath, has_update = perform_backup()
title = f"Bitwarden 备份报告"
content = build_report_content(log_messages)
if has_update and backup_filepath and os.path.exists(backup_filepath):
notify_send_file(title, content, backup_filepath)
else:
notify_send(title, content)
print(f"\n{'=' * 60}\nBitwarden 备份完成报告:\n{'=' * 60}")
for i, msg in enumerate(log_messages, 1):
print(f"{i}. {msg}")
print(f"\n执行时间: {(beijing_now() - start_time).total_seconds():.2f}")
print("=" * 60)
return 0 if backup_success else 1
if __name__ == "__main__":
import sys
try:
exit_code = main()
except KeyboardInterrupt:
log_info("用户中断执行")
exit_code = 130
except Exception as e:
log_error(f"脚本异常: {e}")
exit_code = 1
log_info(f"脚本执行结束,退出代码: {exit_code}")
sys.exit(exit_code)