using Kit.Helpers.SqlHelperAbstract; using Microsoft.Extensions.Logging; using Server.WatchBody.Core.Dal.Ref.Repository; using Server.WatchBody.Core.Dal.Src.Repository; using Server.WatchBody.Core.Programs; namespace Server.WatchBody.Import; /// /// Обход Import.Dir (spec 04 m06): подпапки = имена атлетов; .fit/.tcx/ /// .gpx → WorkoutImporter, program-import.json → ProgramPackageImporter; /// обработанные → ArchiveDir, сбойные остаются (🛡4 — пакет /// продолжается). /// public class ImportService(IConnectionString connectionString, ILogger? logger = null) { private static readonly string[] ActivityExtensions = [".fit", ".tcx", ".gpx"]; private const string PackageName = "program-import.json"; /// (обработано, сбойных). Каталог импорта не существует → (0, 0). public (int Processed, int Failed) Run(string importDir, string archiveDir) { if (!Directory.Exists(importDir)) { Log($"каталог импорта {importDir} не существует — новых файлов нет"); return (0, 0); } Directory.CreateDirectory(archiveDir); // имена атлетов могут повторяться (общая тестовая БД) — первый var athletes = new AthleteRepository(connectionString).Select(null) .GroupBy(a => a.Name, StringComparer.Ordinal) .ToDictionary(g => g.Key, g => g.First().Id, StringComparer.Ordinal); var importer = new ImportContext(connectionString); var processed = 0; var failed = 0; foreach (var folder in Directory.GetDirectories(importDir)) { var athleteName = Path.GetFileName(folder); if (!athletes.TryGetValue(athleteName, out var athleteId)) { failed++; Log($"атлет '{athleteName}' не найден — файлы папки не обработаны"); continue; } foreach (var file in Directory.GetFiles(folder)) { var name = Path.GetFileName(file); try { ProcessFile(importer, athleteId, file, athleteName); var target = ArchivePath(archiveDir, athleteName, name); Directory.CreateDirectory(Path.GetDirectoryName(target)!); System.IO.File.Move(file, target); processed++; Log($"обработан: {athleteName}/{name}"); } catch (Exception e) { failed++; Log($"СБОЙ {athleteName}/{name}: {e.Message}"); Console.Error.WriteLine($"import error: {athleteName}/{name}: {e}"); } } } foreach (var file in Directory.GetFiles(importDir)) Log($"пропущен (не в подпапке-атлете): {Path.GetFileName(file)}"); return (processed, failed); } private static void ProcessFile(ImportContext ctx, int athleteId, string path, string athleteName) { var extension = Path.GetExtension(path).ToLowerInvariant(); var name = Path.GetFileName(path); switch (extension) { case ".fit": ctx.Workouts.Import(athleteId, ctx.Fit.Parse(path)); break; case ".tcx" or ".gpx": ctx.Workouts.Import(athleteId, ctx.Xml.Parse(path)); break; case ".json" when name.Equals(PackageName, StringComparison.OrdinalIgnoreCase): ctx.Packages.Import(athleteId, System.IO.File.ReadAllText(path)); break; default: throw new FitParseException($"неизвестный тип файла импорта: {name}"); } } private static string ArchivePath(string archiveDir, string athleteName, string fileName) { var target = Path.Combine(archiveDir, athleteName, fileName); if (!System.IO.File.Exists(target)) return target; var suffix = $"-{DateTime.Now:HHmmss}"; return Path.Combine(archiveDir, athleteName, Path.GetFileNameWithoutExtension(fileName) + suffix + Path.GetExtension(fileName)); } private void Log(string message) => logger?.LogInformation("{Message}", message); /// Ленивые зависимости одного прогона (соединение общее). private sealed class ImportContext(IConnectionString cs) { public readonly FitParser Fit = new(); public readonly TcxGpxParser Xml = new(); public readonly WorkoutImporter Workouts = new( new WorkoutRepository(cs)); public readonly ProgramPackageImporter Packages = new( new ProgramRepository(cs), new ExerciseRepository(cs), new ExerciseGroupRepository(cs), new ProgramStore(new ProgramRepository(cs), new ProgramVersionRepository(cs))); } }