import 'dart:io'; import 'package:fpdart/fpdart.dart'; import 'package:umbrix/core/utils/exception_handler.dart'; import 'package:umbrix/features/log/data/log_parser.dart'; import 'package:umbrix/features/log/data/log_path_resolver.dart'; import 'package:umbrix/features/log/model/log_entity.dart'; import 'package:umbrix/features/log/model/log_failure.dart'; import 'package:umbrix/singbox/service/singbox_service.dart'; import 'package:umbrix/utils/custom_loggers.dart'; abstract interface class LogRepository { TaskEither init(); Stream>> watchLogs(); TaskEither clearLogs(); } class LogRepositoryImpl with ExceptionHandler, InfraLogger implements LogRepository { LogRepositoryImpl({ required this.singbox, required this.logPathResolver, }); final SingboxService singbox; final LogPathResolver logPathResolver; // Ограничения на размер файлов логов static const int _maxLogFileSize = 5 * 1024 * 1024; // 5 МБ static const int _maxBackupFiles = 2; // Храним только 2 backup файла @override TaskEither init() { return exceptionHandler( () async { if (!await logPathResolver.directory.exists()) { await logPathResolver.directory.create(recursive: true); } // Инициализация core логов с ротацией await _initLogFileWithRotation(logPathResolver.coreFile()); // Инициализация app логов с ротацией await _initLogFileWithRotation(logPathResolver.appFile()); return right(unit); }, LogUnexpectedFailure.new, ); } /// Инициализация файла логов с автоматической ротацией Future _initLogFileWithRotation(File logFile) async { try { if (await logFile.exists()) { final fileSize = await logFile.length(); // Если файл превышает лимит - делаем ротацию if (fileSize > _maxLogFileSize) { loggy.info('Log file too large: ${fileSize / 1024 / 1024}MB, rotating...'); await _rotateLogFile(logFile); } else { // Просто очищаем если размер нормальный await logFile.writeAsString(""); } } else { await logFile.create(recursive: true); } } catch (e, st) { loggy.warning('Error initializing log file: $e', e, st); // В случае ошибки просто создаём новый файл await logFile.create(recursive: true); } } /// Ротация файла логов (создаём backup и очищаем текущий) Future _rotateLogFile(File logFile) async { try { final basePath = logFile.path; // Удаляем самый старый backup если есть for (int i = _maxBackupFiles; i > 0; i--) { final oldBackup = File('$basePath.$i'); if (i == _maxBackupFiles && await oldBackup.exists()) { await oldBackup.delete(); loggy.debug('Deleted old backup: $basePath.$i'); } // Переименовываем backups (example.log.1 → example.log.2) if (i > 1) { final prevBackup = File('$basePath.${i - 1}'); if (await prevBackup.exists()) { await prevBackup.rename('$basePath.$i'); } } } // Текущий файл → backup.1 if (await logFile.exists()) { await logFile.rename('$basePath.1'); loggy.debug('Rotated log file to: $basePath.1'); } // Создаём новый пустой файл await logFile.create(); loggy.info('Log file rotation completed successfully'); } catch (e, st) { loggy.warning('Error rotating log file: $e', e, st); // В случае ошибки просто перезаписываем файл await logFile.writeAsString(""); } } @override Stream>> watchLogs() { return singbox.watchLogs(logPathResolver.coreFile().path).map((event) => event.map(LogParser.parseSingbox).toList()).handleExceptions( (error, stackTrace) { loggy.warning("error watching logs", error, stackTrace); return LogFailure.unexpected(error, stackTrace); }, ); } @override TaskEither clearLogs() { return exceptionHandler( () => singbox.clearLogs().mapLeft(LogFailure.unexpected).run(), LogFailure.unexpected, ); } }