diff --git a/main.py b/main.py index 070a5e0..8b42703 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,5 @@ -import os +import argparse +import os import re import sys import time @@ -132,10 +133,24 @@ class BackupManager: self.page.goto(self.backup_url, wait_until="networkidle") self.page.wait_for_timeout(10000) + def _refresh_backup_list(self): + """Обновить список бэкапов кликом по ссылке 'обновить список / статусы'""" + self.page.click("a:has-text('обновить список / статусы')") + self.page.wait_for_timeout(10000) + def _extract_timestamp(self, name: str) -> str: match = re.search(r"(\d{4}_\d{2}_\d{2}_\d{2}_\d{2}_\d{2})", name) return match.group(1) if match else "" + def _is_file_ready(self, backup_file: BackupFile) -> bool: + """Проверяет что файл готов (не в процессе создания)""" + size = backup_file.size.lower() + # Готовый файл: "1033513 KB" + # В процессе: "файл формируется, 50048 KB" + if "формируется" in size or "создаётся" in size or "progress" in size: + return False + return True + def parse_backup_table(self) -> list[BackupFile]: rows_data = self.page.evaluate("""() => { const table = document.querySelector('table.gb-ttc-2'); @@ -261,6 +276,15 @@ class BackupManager: # СОЗДАНИЕ БЭКАПОВ # ===================================================== + def _expected_new_files(self, backup_data: bool, backup_mysql: bool) -> int: + """Сколько новых файлов ожидаем""" + count = 0 + if backup_data: + count += 1 # ftp_... + if backup_mysql: + count += 2 # sqldump_... (raw + zip) + return count + def create_backup( self, backup_data: bool = False, @@ -286,9 +310,11 @@ class BackupManager: self._go_to_backups() # Запоминаем количество бэкапов до создания - files_before = self.parse_backup_table() - count_before = len(files_before) - print(f"[*] Бэкапов до создания: {count_before}") + ready_files_before = [ + f for f in self.parse_backup_table() if self._is_file_ready(f) + ] + count_before = len(ready_files_before) + print(f"[*] Готовых бэкапов до создания: {count_before}") types = [] if backup_data: @@ -336,20 +362,6 @@ class BackupManager: max_wait=max_wait, ) - def _expected_new_files(self, backup_data: bool, backup_mysql: bool) -> int: - """Сколько новых файлов ожидаем""" - count = 0 - if backup_data: - count += 1 # ftp_... - if backup_mysql: - count += 2 # sqldump_... (raw + zip) - return count - - def _refresh_backup_list(self): - """Обновить список бэкапов кликом по ссылке 'обновить список / статусы'""" - self.page.click("a:has-text('обновить список / статусы')") - self.page.wait_for_timeout(10000) - def _wait_for_backup_completion( self, count_before: int, expected_new: int, poll_interval: int, max_wait: int ) -> bool: @@ -402,44 +414,6 @@ class BackupManager: print(f" Следующая проверка через {poll_interval} сек...") time.sleep(poll_interval) - def _is_file_ready(self, backup_file: BackupFile) -> bool: - """Проверяет что файл готов (не в процессе создания)""" - size = backup_file.size.lower() - # Готовый файл: "1033513 KB" - # В процессе: "файл формируется, 50048 KB" - if "формируется" in size or "создаётся" in size or "progress" in size: - return False - return True - - # def _check_task_status(self) -> list[str]: - # """Проверяет статус заданий в таблице Schedule/Operation""" - # statuses = self.page.evaluate("""() => { - # const tables = document.querySelectorAll('table'); - # const results = []; - - # for (const table of tables) { - # const headers = table.querySelectorAll('th'); - # const headerTexts = Array.from(headers).map(h => h.innerText.trim()); - - # if (headerTexts.some(h => h.includes('Schedule') || h.includes('Operation'))) { - # const rows = table.querySelectorAll('tr'); - # for (const row of rows) { - # const cells = row.querySelectorAll('td'); - # if (cells.length >= 3) { - # const schedule = cells[0]?.innerText?.trim() || ''; - # const server = cells[1]?.innerText?.trim() || ''; - # const operation = cells[2]?.innerText?.trim() || ''; - # if (schedule || operation) { - # results.push(`${schedule} | ${server} | ${operation}`); - # } - # } - # } - # } - # } - # return results; - # }""") - # return statuses - def create_backup_mysql_only(self, wait: bool = True): """Создать только бэкап базы данных""" return self.create_backup( @@ -701,36 +675,144 @@ class BackupManager: print("\n[OK] Готово") +def parse_args(): + parser = argparse.ArgumentParser( + description="Менеджер резервных копий 1gb.ru", + formatter_class=argparse.RawTextHelpFormatter, + ) + + parser.add_argument( + "--action", + choices=["create", "list", "download", "delete", "create-download"], + default="list", + help="""Действие: + list — показать список бэкапов (по умолчанию) + create — создать бэкап + download — скачать последний бэкап + create-download — создать и скачать + delete — удалить старые бэкапы""", + ) + + parser.add_argument( + "--type", + choices=["all", "data", "db"], + default="all", + help="""Тип бэкапа (для create/create-download/download): + db — только база данных MySQL (по умолчанию) + data — только данные сайта FTP + all — данные + база""", + ) + + parser.add_argument( + "--keep", + type=int, + default=3, + help="Сколько бэкапов ОСТАВИТЬ при удалении (по умолчанию: 3)", + ) + + parser.add_argument( + "--year", + type=int, + default=2026, + help="За какой год удалять бэкапы (по умолчанию: 2026)", + ) + + parser.add_argument( + "--save-dir", + type=str, + default="F:/test_folder/MyReports", + help="Папка для скачивания (по умолчанию: F:/test_folder/MyReports)", + ) + + parser.add_argument( + "--no-wait", action="store_true", help="Не ждать завершения создания бэкапа" + ) + + parser.add_argument("--username", type=str, default="minzdravmo", help="Логин") + + parser.add_argument( + "--password", type=str, default="KB3ZS7*sv6j4zYr@bPdeFWQN38rbtQv", help="Пароль" + ) + + parser.add_argument("--account", type=str, default="564622", help="ID аккаунта") + + return parser.parse_args() + + # === ИСПОЛЬЗОВАНИЕ === if __name__ == "__main__": + args = parse_args() + + backup_data = args.type in ("all", "data") + backup_mysql = args.type in ("all", "db") + mgr = BackupManager( - username="minzdravmo", - password="KB3ZS7*sv6j4zYr@bPdeFWQN38rbtQv", - account_id="564622", + username=args.username, password=args.password, account_id=args.account ) + mgr.enable_output_log("F:/test_folder/MyReports/backup.log") try: mgr.start() + if args.action == "list": + mgr.print_backups() + + elif args.action == "create": + mgr.print_backups() + print(f"\n{'=' * 70}") + mgr.create_backup( + backup_data=backup_data, + backup_mysql=backup_mysql, + wait_for_completion=not args.no_wait, + ) + mgr.print_backups() + + elif args.action == "download": + mgr.download_latest( + save_dir=args.save_dir, + include_data=backup_data, + include_sql_raw=backup_mysql, + include_sql_zip=backup_mysql, + ) + + elif args.action == "create-download": + mgr.create_and_download( + save_dir=args.save_dir, + backup_data=backup_data, + backup_mysql=backup_mysql, + ) + elif args.action == "delete": + groups = mgr.get_grouped_backups() + year_groups = [g for g in groups if g.timestamp.startswith(str(args.year))] + total = len(year_groups) + + if total <= args.keep: + print( + f"[*] Бэкапов за {args.year}: {total}, оставляем: {args.keep}. Удалять нечего." + ) + else: + delete_count = total - args.keep + mgr.delete_old_backups(delete_count=delete_count, year=args.year) + # 1. Показать текущие бэкапы - groups = mgr.print_backups() - print("\n" + "=" * 70) + # groups = mgr.print_backups() + # print("\n" + "=" * 70) # 2. Создать бэкап только базы данных # mgr.create_backup_mysql_only(wait=True) # 2. Создать бэкап и сразу скачать - mgr.create_and_download( - save_dir="F:/test_folder/MyReports", - backup_data=True, # данные FTP (закомментировано) - backup_mysql=True, # база данных - ) + # mgr.create_and_download( + # save_dir="F:/test_folder/MyReports", + # backup_data=True, # данные FTP (закомментировано) + # backup_mysql=True, # база данных + # ) # 3. Показать, что удаляем и удалить - mgr.print_deletion_plan(delete_count=1, year=2026) - mgr.delete_old_backups(delete_count=1, year=2026) + # mgr.print_deletion_plan(delete_count=1, year=2026) + # mgr.delete_old_backups(delete_count=1, year=2026) # Скачать только последний бэкап (без создания нового) # mgr.download_latest(save_dir="./backups") @@ -751,3 +833,28 @@ if __name__ == "__main__": finally: mgr.stop() + + +# # Показать список бэкапов +# python main.py --action list + +# # Создать бэкап только БД +# python main.py --action create --type db + +# # Создать бэкап всего (данные + БД) +# python main.py --action create --type all + +# # Создать и сразу скачать БД +# python main.py --action create-download --type db --save-dir ./backups + +# # Скачать последний бэкап +# python main.py --action download --type db + +# # Удалить старые, оставить 3 за 2026 год +# python main.py --action delete --keep 3 --year 2026 + +# # Удалить старые, оставить 1 +# python main.py --action delete --keep 1 --year 2026 + +# # Полный цикл: создать БД + скачать + удалить старые (оставить 3) +# python main.py --action create-download --type db && python main.py --action delete --keep 3