- CsvValidationService: CsvHelper-based parsing with DetectDelimiter, BOM detection, per-row validation for BulkMemberRow/BulkSiteRow/FolderStructureRow - TemplateRepository: atomic JSON write (tmp + File.Move) with SemaphoreSlim, supports GetAll/GetById/Save/Delete/Rename operations - CsvValidationServiceTests: 9 passing tests (email validation, delimiter detection, BOM handling, folder/site/member validation) - TemplateRepositoryTests: 6 passing tests (round-trip, GetAll, delete, rename, empty directory, non-existent id) - All previously-skipped scaffold tests now active and passing (15 total)
102 lines
3.0 KiB
C#
102 lines
3.0 KiB
C#
using System.IO;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using SharepointToolbox.Core.Models;
|
|
|
|
namespace SharepointToolbox.Infrastructure.Persistence;
|
|
|
|
public class TemplateRepository
|
|
{
|
|
private readonly string _directoryPath;
|
|
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
WriteIndented = true,
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
PropertyNameCaseInsensitive = true,
|
|
};
|
|
|
|
public TemplateRepository(string directoryPath)
|
|
{
|
|
_directoryPath = directoryPath;
|
|
}
|
|
|
|
public async Task<List<SiteTemplate>> GetAllAsync()
|
|
{
|
|
if (!Directory.Exists(_directoryPath))
|
|
return new List<SiteTemplate>();
|
|
|
|
var templates = new List<SiteTemplate>();
|
|
foreach (var file in Directory.GetFiles(_directoryPath, "*.json"))
|
|
{
|
|
try
|
|
{
|
|
var json = await File.ReadAllTextAsync(file, Encoding.UTF8);
|
|
var template = JsonSerializer.Deserialize<SiteTemplate>(json, JsonOptions);
|
|
if (template != null)
|
|
templates.Add(template);
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
// Skip corrupted template files
|
|
}
|
|
}
|
|
return templates.OrderByDescending(t => t.CapturedAt).ToList();
|
|
}
|
|
|
|
public async Task<SiteTemplate?> GetByIdAsync(string id)
|
|
{
|
|
var filePath = GetFilePath(id);
|
|
if (!File.Exists(filePath))
|
|
return null;
|
|
|
|
var json = await File.ReadAllTextAsync(filePath, Encoding.UTF8);
|
|
return JsonSerializer.Deserialize<SiteTemplate>(json, JsonOptions);
|
|
}
|
|
|
|
public async Task SaveAsync(SiteTemplate template)
|
|
{
|
|
await _writeLock.WaitAsync();
|
|
try
|
|
{
|
|
if (!Directory.Exists(_directoryPath))
|
|
Directory.CreateDirectory(_directoryPath);
|
|
|
|
var json = JsonSerializer.Serialize(template, JsonOptions);
|
|
var filePath = GetFilePath(template.Id);
|
|
var tmpPath = filePath + ".tmp";
|
|
|
|
await File.WriteAllTextAsync(tmpPath, json, Encoding.UTF8);
|
|
|
|
// Validate round-trip before replacing
|
|
JsonDocument.Parse(await File.ReadAllTextAsync(tmpPath, Encoding.UTF8)).Dispose();
|
|
|
|
File.Move(tmpPath, filePath, overwrite: true);
|
|
}
|
|
finally
|
|
{
|
|
_writeLock.Release();
|
|
}
|
|
}
|
|
|
|
public Task DeleteAsync(string id)
|
|
{
|
|
var filePath = GetFilePath(id);
|
|
if (File.Exists(filePath))
|
|
File.Delete(filePath);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public async Task RenameAsync(string id, string newName)
|
|
{
|
|
var template = await GetByIdAsync(id);
|
|
if (template == null)
|
|
throw new InvalidOperationException($"Template not found: {id}");
|
|
|
|
template.Name = newName;
|
|
await SaveAsync(template);
|
|
}
|
|
|
|
private string GetFilePath(string id) => Path.Combine(_directoryPath, $"{id}.json");
|
|
}
|