Collect authenticated Qinglong scripts batch
This commit is contained in:
236
脚本库/web版/账密/Bitwarden备份/2026-05-23_Sync_Password_e63fb7c9.py
Normal file
236
脚本库/web版/账密/Bitwarden备份/2026-05-23_Sync_Password_e63fb7c9.py
Normal 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)
|
||||
Reference in New Issue
Block a user