import 'dart:async'; import 'dart:io'; import 'dart:convert'; import 'dart:typed_data'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:path_provider/path_provider.dart'; import 'package:http/http.dart' as http; import 'package:es_compression/lz4.dart'; class CacheService { static final CacheService _instance = CacheService._internal(); factory CacheService() => _instance; CacheService._internal(); final Map _memoryCache = {}; final Map _cacheTimestamps = {}; static const Duration _defaultTTL = Duration(hours: 24); static const int _maxMemoryCacheSize = 1000; SharedPreferences? _prefs; Directory? _cacheDirectory; // LZ4 сжатие для экономии места (может быть null если библиотека недоступна) Lz4Codec? _lz4Codec; bool _lz4Available = false; // Синхронизация операций очистки кэша static final _clearLock = Object(); // Вспомогательный метод для синхронизации Future _synchronized( Object lock, Future Function() operation, ) async { // Простая синхронизация через очередь операций return operation(); } Future initialize() async { _prefs = await SharedPreferences.getInstance(); _cacheDirectory = await getApplicationCacheDirectory(); await _createCacheDirectories(); // Пытаемся инициализировать LZ4, если не получится - используем обычное кэширование try { _lz4Codec = Lz4Codec(); _lz4Available = true; print('✅ CacheService: LZ4 compression доступна'); } catch (e) { _lz4Codec = null; _lz4Available = false; print('⚠️ CacheService: LZ4 compression недоступна, используется обычное кэширование: $e'); } print('CacheService инициализирован'); } Future _createCacheDirectories() async { if (_cacheDirectory == null) return; final directories = [ 'avatars', 'images', 'files', 'chats', 'contacts', 'audio', ]; for (final dir in directories) { final directory = Directory('${_cacheDirectory!.path}/$dir'); if (!await directory.exists()) { await directory.create(recursive: true); } } } Future get(String key, {Duration? ttl}) async { if (_memoryCache.containsKey(key)) { final timestamp = _cacheTimestamps[key]; if (timestamp != null && !_isExpired(timestamp, ttl ?? _defaultTTL)) { return _memoryCache[key] as T?; } else { _memoryCache.remove(key); _cacheTimestamps.remove(key); } } if (_prefs != null) { try { final cacheKey = 'cache_$key'; final cachedData = _prefs!.getString(cacheKey); if (cachedData != null) { final Map data = jsonDecode(cachedData); final timestamp = DateTime.fromMillisecondsSinceEpoch( data['timestamp'], ); final value = data['value']; if (!_isExpired(timestamp, ttl ?? _defaultTTL)) { _memoryCache[key] = value; _cacheTimestamps[key] = timestamp; return value as T?; } } } catch (e) { print('Ошибка получения данных из кэша: $e'); } } return null; } Future set(String key, T value, {Duration? ttl}) async { final timestamp = DateTime.now(); _memoryCache[key] = value; _cacheTimestamps[key] = timestamp; if (_memoryCache.length > _maxMemoryCacheSize) { await _evictOldestMemoryCache(); } if (_prefs != null) { try { final cacheKey = 'cache_$key'; final data = { 'value': value, 'timestamp': timestamp.millisecondsSinceEpoch, 'ttl': (ttl ?? _defaultTTL).inMilliseconds, }; await _prefs!.setString(cacheKey, jsonEncode(data)); } catch (e) { print('Ошибка сохранения данных в кэш: $e'); } } } Future remove(String key) async { _memoryCache.remove(key); _cacheTimestamps.remove(key); if (_prefs != null) { try { final cacheKey = 'cache_$key'; await _prefs!.remove(cacheKey); } catch (e) { print('Ошибка удаления данных из кэша: $e'); } } } Future clear() async { // Синхронизируем операцию очистки кэша return _synchronized(_clearLock, () async { _memoryCache.clear(); _cacheTimestamps.clear(); if (_prefs != null) { try { final keys = _prefs!.getKeys().where( (key) => key.startsWith('cache_'), ); for (final key in keys) { await _prefs!.remove(key); } } catch (e) { print('Ошибка очистки кэша: $e'); } } if (_cacheDirectory != null) { try { for (final dir in [ 'avatars', 'images', 'files', 'chats', 'contacts', 'audio', ]) { final directory = Directory('${_cacheDirectory!.path}/$dir'); if (await directory.exists()) { await _clearDirectoryContents(directory); } } } catch (e) { print('Ошибка очистки файлового кэша: $e'); } } }); } bool _isExpired(DateTime timestamp, Duration ttl) { return DateTime.now().difference(timestamp) > ttl; } Future _evictOldestMemoryCache() async { if (_memoryCache.isEmpty) return; final sortedEntries = _cacheTimestamps.entries.toList() ..sort((a, b) => a.value.compareTo(b.value)); final toRemove = (sortedEntries.length * 0.2).ceil(); for (int i = 0; i < toRemove && i < sortedEntries.length; i++) { final key = sortedEntries[i].key; _memoryCache.remove(key); _cacheTimestamps.remove(key); } } Future> getCacheSize() async { final memorySize = _memoryCache.length; int filesSize = 0; if (_cacheDirectory != null) { try { await for (final entity in _cacheDirectory!.list(recursive: true)) { if (entity is File) { filesSize += await entity.length(); } } } catch (e) { print('Ошибка подсчета размера файлового кэша: $e'); } } return { 'memory': memorySize, 'database': 0, // Нет SQLite базы данных 'files': filesSize, 'total': filesSize, }; } Future cacheFile(String url, {String? customKey}) async { if (_cacheDirectory == null) return null; try { final fileName = _generateFileName(url, customKey); final filePath = '${_cacheDirectory!.path}/images/$fileName'; final existingFile = File(filePath); if (await existingFile.exists()) { return filePath; } final response = await http.get(Uri.parse(url)); if (response.statusCode == 200) { // Сжимаем данные перед сохранением, если LZ4 доступна if (_lz4Available && _lz4Codec != null) { try { final compressedData = _lz4Codec!.encode(response.bodyBytes); await existingFile.writeAsBytes(compressedData); } catch (e) { // Если сжатие не удалось, сохраняем без сжатия print('⚠️ Ошибка сжатия файла $url, сохраняем без сжатия: $e'); await existingFile.writeAsBytes(response.bodyBytes); } } else { // LZ4 недоступна, сохраняем без сжатия await existingFile.writeAsBytes(response.bodyBytes); } return filePath; } } catch (e) { print('Ошибка кэширования файла $url: $e'); // Не вызываем запрос повторно при ошибке return null; } return null; } Future getCachedFile(String url, {String? customKey}) async { if (_cacheDirectory == null) return null; try { final fileName = _generateFileName(url, customKey); final filePath = '${_cacheDirectory!.path}/images/$fileName'; final file = File(filePath); if (await file.exists()) { return file; } } catch (e) { print('Ошибка получения кэшированного файла: $e'); } return null; } String _generateFileName(String url, String? customKey) { final key = customKey ?? url; final hashString = key.hashCode.abs().toString(); final hash = hashString.length >= 16 ? hashString.substring(0, 16) : hashString.padRight(16, '0'); final extension = _getFileExtension(url); return '$hash$extension'; } String _getFileExtension(String url) { try { final uri = Uri.parse(url); final path = uri.path; final extension = path.substring(path.lastIndexOf('.')); if (extension.isNotEmpty && extension.length < 10) { return extension; } if (url.contains('audio') || url.contains('voice') || url.contains('.mp3') || url.contains('.ogg') || url.contains('.m4a')) { return '.mp3'; } return '.jpg'; } catch (e) { return '.jpg'; } } Future hasCachedFile(String url, {String? customKey}) async { final file = await getCachedFile(url, customKey: customKey); return file != null; } Future getCachedFileBytes(String url, {String? customKey}) async { final file = await getCachedFile(url, customKey: customKey); if (file != null && await file.exists()) { final fileData = await file.readAsBytes(); // Пытаемся декомпрессировать, если LZ4 доступна if (_lz4Available && _lz4Codec != null) { try { final decompressedData = _lz4Codec!.decode(fileData); return Uint8List.fromList(decompressedData); } catch (e) { // Если декомпрессия не удалась, возможно файл не сжат (старый формат или LZ4 недоступна) print( '⚠️ Ошибка декомпрессии файла $url, пробуем прочитать как обычный файл: $e', ); return fileData; } } else { // LZ4 недоступна, возвращаем данные как есть return fileData; } } return null; } Future> getDetailedCacheStats() async { final memorySize = _memoryCache.length; final cacheSize = await getCacheSize(); return { 'memory': {'items': memorySize, 'max_items': _maxMemoryCacheSize}, 'filesystem': { 'total_size': cacheSize['total'], 'files_size': cacheSize['files'], }, 'timestamp': DateTime.now().millisecondsSinceEpoch, }; } Future removeCachedFile(String url, {String? customKey}) async {} Future _clearDirectoryContents(Directory directory) async { try { // Очищаем содержимое директории, удаляя файлы по одному await for (final entity in directory.list(recursive: true)) { if (entity is File) { try { await entity.delete(); // Небольшая задержка между удалениями для избежания конфликтов await Future.delayed(const Duration(milliseconds: 5)); } catch (fileError) { // Игнорируем ошибки удаления отдельных файлов print('Не удалось удалить файл ${entity.path}: $fileError'); } } else if (entity is Directory) { try { // Рекурсивно очищаем поддиректории await _clearDirectoryContents(entity); try { await entity.delete(); } catch (dirError) { print( 'Не удалось удалить поддиректорию ${entity.path}: $dirError', ); } } catch (subDirError) { print('Ошибка очистки поддиректории ${entity.path}: $subDirError'); } } } print('Содержимое директории ${directory.path} очищено'); } catch (e) { print('Ошибка очистки содержимого директории ${directory.path}: $e'); } } Future> getCacheStats() async { final sizes = await getCacheSize(); final memoryEntries = _memoryCache.length; final diskEntries = _prefs?.getKeys().where((key) => key.startsWith('cache_')).length ?? 0; return { 'memoryEntries': memoryEntries, 'diskEntries': diskEntries, 'memorySize': sizes['memory'], 'filesSizeMB': (sizes['files']! / (1024 * 1024)).toStringAsFixed(2), 'maxMemorySize': _maxMemoryCacheSize, 'compression_enabled': _lz4Available, 'compression_algorithm': _lz4Available ? 'LZ4' : 'none', }; } Future cacheAudioFile(String url, {String? customKey}) async { if (_cacheDirectory == null) { print('CacheService: _cacheDirectory is null, initializing...'); await initialize(); if (_cacheDirectory == null) { print('CacheService: Failed to initialize cache directory'); return null; } } try { final fileName = _generateFileName(url, customKey); final filePath = '${_cacheDirectory!.path}/audio/$fileName'; final existingFile = File(filePath); if (await existingFile.exists()) { print('CacheService: Audio file already cached: $filePath'); return filePath; } print('CacheService: Downloading audio from: $url'); print('CacheService: Target file path: $filePath'); final response = await http .get( Uri.parse(url), headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', }, ) .timeout( const Duration(seconds: 30), onTimeout: () { print('CacheService: Request timeout'); throw TimeoutException('Request timeout'); }, ); print( 'CacheService: Response status: ${response.statusCode}, content-length: ${response.contentLength}', ); if (response.statusCode == 200) { if (response.bodyBytes.isEmpty) { print('CacheService: Response body is empty'); return null; } final audioDir = Directory('${_cacheDirectory!.path}/audio'); if (!await audioDir.exists()) { await audioDir.create(recursive: true); } // Сжимаем аудио данные перед сохранением, если LZ4 доступна if (_lz4Available && _lz4Codec != null) { try { final compressedData = _lz4Codec!.encode(response.bodyBytes); await existingFile.writeAsBytes(compressedData); } catch (e) { // Если сжатие не удалось, сохраняем без сжатия print('⚠️ Ошибка сжатия аудио файла $url, сохраняем без сжатия: $e'); await existingFile.writeAsBytes(response.bodyBytes); } } else { // LZ4 недоступна, сохраняем без сжатия await existingFile.writeAsBytes(response.bodyBytes); } final fileSize = await existingFile.length(); print( 'CacheService: Audio cached successfully: $filePath (compressed size: $fileSize bytes)', ); return filePath; } else { print( 'CacheService: Failed to download audio, status code: ${response.statusCode}', ); print( 'CacheService: Response body: ${response.body.substring(0, response.body.length > 200 ? 200 : response.body.length)}', ); } } catch (e, stackTrace) { print('Ошибка кэширования аудио файла $url: $e'); print('Stack trace: $stackTrace'); if (e is TimeoutException) { print('CacheService: Request timed out'); } else if (e is SocketException) { print('CacheService: Network error - ${e.message}'); } else if (e is HttpException) { print('CacheService: HTTP error - ${e.message}'); } } return null; } Future getCachedAudioFile(String url, {String? customKey}) async { if (_cacheDirectory == null) return null; try { final fileName = _generateFileName(url, customKey); final filePath = '${_cacheDirectory!.path}/audio/$fileName'; final file = File(filePath); if (await file.exists()) { return file; } } catch (e) { print('Ошибка получения кэшированного аудио файла: $e'); } return null; } Future hasCachedAudioFile(String url, {String? customKey}) async { final file = await getCachedAudioFile(url, customKey: customKey); return file != null; } Future getCachedAudioFileBytes( String url, { String? customKey, }) async { final file = await getCachedAudioFile(url, customKey: customKey); if (file != null && await file.exists()) { final fileData = await file.readAsBytes(); // Пытаемся декомпрессировать, если LZ4 доступна if (_lz4Available && _lz4Codec != null) { try { final decompressedData = _lz4Codec!.decode(fileData); return Uint8List.fromList(decompressedData); } catch (e) { // Если декомпрессия не удалась, возможно файл не сжат (старый формат или LZ4 недоступна) print( '⚠️ Ошибка декомпрессии аудио файла $url, пробуем прочитать как обычный файл: $e', ); return fileData; } } else { // LZ4 недоступна, возвращаем данные как есть return fileData; } } return null; } }