/
d.alekseev
/
SmartMenuBot
Обзор
Документация
Войти
/
d.alekseev
/
SmartMenuBot
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
ConsoleBot/Infrastructure/DataAccess/FileToDoRepository.cs
395 строк
14 KB
d.alekseev
feat: нотификации и фоновые задачи доставки уведомлений
26 фев 2026, 10:55
26 фев 2026, 10:55
4338916
Код
Авторство
О чём код?
using System.Text.Json; using SmartMenuBot.Core.DataAccess; using SmartMenuBot.Core.Entities; using SmartMenuBot.Core.Exceptions; using SmartMenuBot.Infrastructure.DataAccess.StorageModels; namespace SmartMenuBot.Infrastructure.DataAccess { public class FileToDoRepository : IToDoRepository { private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; private readonly string _baseDirectoryPath; private readonly string _indexFilePath; private readonly SemaphoreSlim _gate = new(1, 1); public FileToDoRepository(string baseDirectoryPath) { if (string.IsNullOrWhiteSpace(baseDirectoryPath)) throw new ArgumentException("Базовый путь не может быть пустым", nameof(baseDirectoryPath)); _baseDirectoryPath = baseDirectoryPath; _indexFilePath = Path.Combine(_baseDirectoryPath, "index.json"); Directory.CreateDirectory(_baseDirectoryPath); } public async Task AddAsync(ToDoItem item, CancellationToken ct) { ArgumentNullException.ThrowIfNull(item); await _gate.WaitAsync(ct); try { var filePath = GetTaskFilePath(item.User.UserId, item.Id); Directory.CreateDirectory(GetUserDirectoryPath(item.User.UserId)); var stored = MapToStored(item); await WriteJsonFileAsync(filePath, stored, ct); var indexEntries = await LoadOrBuildIndexLockedAsync(ct); indexEntries.RemoveAll(e => e.ToDoItemId == item.Id); indexEntries.Add(new ToDoIndexEntry { ToDoItemId = item.Id, UserId = item.User.UserId }); await SaveIndexLockedAsync(indexEntries, ct); } finally { _gate.Release(); } } public async Task<int> CountActiveAsync(Guid userId, CancellationToken ct) { await _gate.WaitAsync(ct); try { var items = await GetAllByUserIdLockedAsync(userId, ct); return items.Count(i => i.State == ToDoItemState.Active); } finally { _gate.Release(); } } public async Task DeleteAsync(Guid id, CancellationToken ct) { await _gate.WaitAsync(ct); try { var indexEntries = await LoadOrBuildIndexLockedAsync(ct); var indexEntry = indexEntries.FirstOrDefault(e => e.ToDoItemId == id); if (indexEntry is null) return; var filePath = GetTaskFilePath(indexEntry.UserId, id); if (File.Exists(filePath)) File.Delete(filePath); indexEntries.RemoveAll(e => e.ToDoItemId == id); await SaveIndexLockedAsync(indexEntries, ct); } finally { _gate.Release(); } } public async Task<bool> ExistsByNameAsync(Guid userId, string name, CancellationToken ct) { await _gate.WaitAsync(ct); try { var items = await GetAllByUserIdLockedAsync(userId, ct); return items.Any(i => i.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); } finally { _gate.Release(); } } public async Task<ToDoItem?> GetAsync(Guid id, CancellationToken ct) { await _gate.WaitAsync(ct); try { return await GetByIdLockedAsync(id, ct); } finally { _gate.Release(); } } public async Task<IReadOnlyList<ToDoItem>> GetActiveByUserIdAsync(Guid userId, CancellationToken ct) { await _gate.WaitAsync(ct); try { var allItems = await GetAllByUserIdLockedAsync(userId, ct); return allItems.Where(i => i.State == ToDoItemState.Active).ToList().AsReadOnly(); } finally { _gate.Release(); } } public async Task<IReadOnlyList<ToDoItem>> GetAllByUserIdAsync(Guid userId, CancellationToken ct) { await _gate.WaitAsync(ct); try { return await GetAllByUserIdLockedAsync(userId, ct); } finally { _gate.Release(); } } public async Task<IReadOnlyList<ToDoItem>> GetActiveWithDeadline(Guid userId, DateTime from, DateTime to, CancellationToken ct) { await _gate.WaitAsync(ct); try { var allItems = await GetAllByUserIdLockedAsync(userId, ct); return allItems .Where(item => item.State == ToDoItemState.Active && item.Deadline >= from && item.Deadline < to) .OrderBy(item => item.Deadline) .ToList() .AsReadOnly(); } finally { _gate.Release(); } } public async Task<IReadOnlyList<ToDoItem>> FindAsync(Guid userId, Func<ToDoItem, bool> predicate, CancellationToken ct) { ArgumentNullException.ThrowIfNull(predicate); await _gate.WaitAsync(ct); try { var allItems = await GetAllByUserIdLockedAsync(userId, ct); return allItems.Where(predicate).ToList().AsReadOnly(); } finally { _gate.Release(); } } public async Task UpdateAsync(ToDoItem item, CancellationToken ct) { ArgumentNullException.ThrowIfNull(item); await _gate.WaitAsync(ct); try { var indexEntries = await LoadOrBuildIndexLockedAsync(ct); var indexEntry = indexEntries.FirstOrDefault(e => e.ToDoItemId == item.Id); if (indexEntry is null) throw new EntityNotFoundException(item.Id, "Задача"); var currentPath = GetTaskFilePath(indexEntry.UserId, item.Id); if (!File.Exists(currentPath)) throw new EntityNotFoundException(item.Id, "Задача"); var newPath = GetTaskFilePath(item.User.UserId, item.Id); Directory.CreateDirectory(GetUserDirectoryPath(item.User.UserId)); await WriteJsonFileAsync(newPath, MapToStored(item), ct); if (!string.Equals(currentPath, newPath, StringComparison.Ordinal)) File.Delete(currentPath); if (indexEntry.UserId != item.User.UserId) { indexEntries.RemoveAll(e => e.ToDoItemId == item.Id); indexEntries.Add(new ToDoIndexEntry { ToDoItemId = item.Id, UserId = item.User.UserId }); await SaveIndexLockedAsync(indexEntries, ct); } } finally { _gate.Release(); } } private async Task<ToDoItem?> GetByIdLockedAsync(Guid toDoItemId, CancellationToken ct) { var indexEntries = await LoadOrBuildIndexLockedAsync(ct); var indexEntry = indexEntries.FirstOrDefault(e => e.ToDoItemId == toDoItemId); if (indexEntry is null) return null; var filePath = GetTaskFilePath(indexEntry.UserId, toDoItemId); if (!File.Exists(filePath)) return null; var stored = await ReadJsonFileAsync<StoredToDoItem>(filePath, ct); return stored is null ? null : MapToDomain(stored); } private async Task<IReadOnlyList<ToDoItem>> GetAllByUserIdLockedAsync(Guid userId, CancellationToken ct) { var userDir = GetUserDirectoryPath(userId); if (!Directory.Exists(userDir)) return Array.Empty<ToDoItem>(); var result = new List<ToDoItem>(); var files = Directory.GetFiles(userDir, "*.json", SearchOption.TopDirectoryOnly); foreach (var file in files) { ct.ThrowIfCancellationRequested(); var stored = await ReadJsonFileAsync<StoredToDoItem>(file, ct); if (stored is null) continue; result.Add(MapToDomain(stored)); } return result.AsReadOnly(); } private async Task<List<ToDoIndexEntry>> LoadOrBuildIndexLockedAsync(CancellationToken ct) { if (File.Exists(_indexFilePath)) { var fromFile = await ReadJsonFileAsync<List<ToDoIndexEntry>>(_indexFilePath, ct); if (fromFile is not null) return fromFile; } var rebuilt = await BuildIndexByDirectoryScanLockedAsync(ct); await SaveIndexLockedAsync(rebuilt, ct); return rebuilt; } private async Task<List<ToDoIndexEntry>> BuildIndexByDirectoryScanLockedAsync(CancellationToken ct) { var indexEntries = new List<ToDoIndexEntry>(); var seen = new HashSet<Guid>(); if (!Directory.Exists(_baseDirectoryPath)) return indexEntries; var userDirectories = Directory.GetDirectories(_baseDirectoryPath, "*", SearchOption.TopDirectoryOnly); foreach (var userDirectory in userDirectories) { ct.ThrowIfCancellationRequested(); var userDirectoryName = Path.GetFileName(userDirectory); if (!Guid.TryParse(userDirectoryName, out var userId)) continue; var taskFiles = Directory.GetFiles(userDirectory, "*.json", SearchOption.TopDirectoryOnly); foreach (var taskFile in taskFiles) { ct.ThrowIfCancellationRequested(); var taskFileName = Path.GetFileNameWithoutExtension(taskFile); if (!Guid.TryParse(taskFileName, out var toDoItemId)) continue; if (!seen.Add(toDoItemId)) continue; indexEntries.Add(new ToDoIndexEntry { ToDoItemId = toDoItemId, UserId = userId }); } } return indexEntries; } private async Task SaveIndexLockedAsync(List<ToDoIndexEntry> indexEntries, CancellationToken ct) { await WriteJsonFileAsync(_indexFilePath, indexEntries, ct); } private static async Task<T?> ReadJsonFileAsync<T>(string path, CancellationToken ct) { var json = await File.ReadAllTextAsync(path, ct); return JsonSerializer.Deserialize<T>(json, JsonOptions); } private static async Task WriteJsonFileAsync<T>(string path, T data, CancellationToken ct) { var json = JsonSerializer.Serialize(data, JsonOptions); var temporaryPath = $"{path}.tmp"; await File.WriteAllTextAsync(temporaryPath, json, ct); if (File.Exists(path)) File.Delete(path); File.Move(temporaryPath, path); } private string GetUserDirectoryPath(Guid userId) { return Path.Combine(_baseDirectoryPath, userId.ToString()); } private string GetTaskFilePath(Guid userId, Guid toDoItemId) { return Path.Combine(GetUserDirectoryPath(userId), $"{toDoItemId}.json"); } private static StoredToDoItem MapToStored(ToDoItem toDoItem) { return new StoredToDoItem { Id = toDoItem.Id, UserId = toDoItem.User.UserId, UserTelegramUserId = toDoItem.User.TelegramUserId, UserTelegramUserName = toDoItem.User.TelegramUserName, UserRegisteredAt = toDoItem.User.RegisteredAt, Name = toDoItem.Name, CreatedAt = toDoItem.CreatedAt, Deadline = toDoItem.Deadline, ListId = toDoItem.List?.Id, ListName = toDoItem.List?.Name, ListCreatedAt = toDoItem.List?.CreatedAt, State = toDoItem.State, StateChangedAt = toDoItem.StateChangedAt }; } private static ToDoItem MapToDomain(StoredToDoItem stored) { var user = ToDoUser.Restore( stored.UserId, stored.UserTelegramUserId, stored.UserTelegramUserName, stored.UserRegisteredAt); ToDoList? list = null; if (stored.ListId.HasValue && !string.IsNullOrWhiteSpace(stored.ListName) && stored.ListCreatedAt.HasValue) { list = ToDoList.Restore( stored.ListId.Value, stored.ListName, user, stored.ListCreatedAt.Value); } return ToDoItem.Restore( stored.Id, user, stored.Name, stored.CreatedAt, stored.Deadline ?? stored.CreatedAt, stored.State, stored.StateChangedAt, list); } } }