106 lines
4.3 KiB
Python
106 lines
4.3 KiB
Python
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) |