VEU.WathBody/3-app/2-cli/Server.WatchBody.Import/ImportService.cs

116 lines
5.1 KiB
C#

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;
/// <summary>
/// Обход Import.Dir (spec 04 m06): подпапки = имена атлетов; .fit/.tcx/
/// .gpx → WorkoutImporter, program-import.json → ProgramPackageImporter;
/// обработанные → ArchiveDir, сбойные остаются (🛡4 — пакет
/// продолжается).
/// </summary>
public class ImportService(IConnectionString connectionString, ILogger<ImportService>? logger = null)
{
private static readonly string[] ActivityExtensions = [".fit", ".tcx", ".gpx"];
private const string PackageName = "program-import.json";
/// <summary>(обработано, сбойных). Каталог импорта не существует → (0, 0).</summary>
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);
/// <summary>Ленивые зависимости одного прогона (соединение общее).</summary>
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)));
}
}