Files
fuckKomet/lib/services/cache_service.dart

572 lines
19 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String, dynamic> _memoryCache = {};
final Map<String, DateTime> _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<T> _synchronized<T>(
Object lock,
Future<T> Function() operation,
) async {
// Простая синхронизация через очередь операций
return operation();
}
Future<void> 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<void> _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<T?> get<T>(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<String, dynamic> 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<void> set<T>(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<void> 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<void> 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<void> _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<Map<String, int>> 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<String?> 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<File?> 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<bool> hasCachedFile(String url, {String? customKey}) async {
final file = await getCachedFile(url, customKey: customKey);
return file != null;
}
Future<Uint8List?> 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<Map<String, dynamic>> 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<void> removeCachedFile(String url, {String? customKey}) async {}
Future<void> _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<Map<String, dynamic>> 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<String?> 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<File?> 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<bool> hasCachedAudioFile(String url, {String? customKey}) async {
final file = await getCachedAudioFile(url, customKey: customKey);
return file != null;
}
Future<Uint8List?> 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;
}
}