import os import sqlite3 import tarfile import subprocess import shutil from datetime import datetime # --- НАСТРОЙКИ ПУТЕЙ --- # Эти пути соответствуют внутренностям Docker-контейнера раннера DATA_DIR = "/vw-data" DB_FILE = os.path.join(DATA_DIR, "db.sqlite3") ATTACHMENTS_DIR = os.path.join(DATA_DIR, "attachments") # Временная папка для обработки (внутри контейнера раннера) BACKUP_TEMP_DIR = "/tmp/vw_backup_processing" # Имена крипто-пультов rclone, которые вы настроили RCLONE_REMOTES = [ "google-crypt:/vaultwarden-backups", "yandex-crypt:/vaultwarden-backups" ] def safe_sqlite_backup(src_path, dest_path): """Делает безопасный слепок запущенной базы данных SQLite""" print(f"[*] Создание безопасного дампа базы данных из {src_path}...") src = sqlite3.connect(src_path) dest = sqlite3.connect(dest_path) with dest: src.backup(dest) src.close() dest.close() print("[+] Дамп базы успешно создан.") def create_backup_archive(): """Собирает базу и вложения в один tar.gz архив""" if os.path.exists(BACKUP_TEMP_DIR): shutil.rmtree(BACKUP_TEMP_DIR) os.makedirs(BACKUP_TEMP_DIR, exist_ok=True) # 1. Дамп БД temp_db_path = os.path.join(BACKUP_TEMP_DIR, "db.sqlite3") safe_sqlite_backup(DB_FILE, temp_db_path) # 2. Формирование архива timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") archive_name = f"vaultwarden_{timestamp}.tar.gz" archive_path = os.path.join(BACKUP_TEMP_DIR, archive_name) print(f"[*] Упаковка файлов в архив {archive_name}...") with tarfile.open(archive_path, "w:gz") as tar: # Добавляем БД в корень архива tar.add(temp_db_path, arcname="db.sqlite3") # Если есть вложения (файлы в паролях), добавляем и их if os.path.exists(ATTACHMENTS_DIR): tar.add(ATTACHMENTS_DIR, arcname="attachments") print("[+] Вложения добавлены в архив.") # Удаляем сырой дамп, оставляем только архив os.remove(temp_db_path) print("[+] Архив успешно сформирован.") return archive_path def upload_to_clouds(archive_path): """Отправляет архив через Rclone в облака""" filename = os.path.basename(archive_path) all_success = True for remote in RCLONE_REMOTES: print(f"[*] Синхронизация с облаком {remote}...") # Вызываем команду rclone copy cmd = ["rclone", "copy", archive_path, remote] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode == 0: print(f"[+] Успешно загружено в {remote}") else: print(f"[-] ОШИБКА загрузки в {remote}:") print(result.stderr) all_success = False return all_success if __name__ == "__main__": print("=== Старт процесса резервного копирования Vaultwarden ===") if not os.path.exists(DB_FILE): print(f"[!] КРИТИЧЕСКАЯ ОШИБКА: Файл БД {DB_FILE} не найден. Проверьте монтирование Volumes.") exit(1) try: archive_file = create_backup_archive() is_upload_ok = upload_to_clouds(archive_file) # Очистка следов после работы print("[*] Очистка временных файлов...") shutil.rmtree(BACKUP_TEMP_DIR) if is_upload_ok: print("=== Бэкап успешно завершен ===") else: print("=== Бэкап завершен с ошибками при отправке ===") exit(1) # Уронит пайплайн в красный крестик в Gitea, если облако отвалилось except Exception as e: print(f"[!] Произошел непредвиденный сбой: {e}") exit(1)