832 lines
35 KiB
C#
832 lines
35 KiB
C#
using Manager.DTOs;
|
|
using Manager.Services;
|
|
using ManagerService.Data;
|
|
using ManagerService.Data.SubSection;
|
|
using ManagerService.DTOs;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using NetTopologySuite.Geometries;
|
|
using NSwag.Annotations;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Text.Json;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace ManagerService.Controllers
|
|
{
|
|
[Authorize(Policy = ManagerService.Service.Security.Policies.SuperAdmin)]
|
|
[ApiController, Route("api/[controller]")]
|
|
[OpenApiTag("Migration", Description = "MongoDB → PostgreSQL data migration")]
|
|
public class MigrationController : ControllerBase
|
|
{
|
|
private readonly MyInfoMateDbContext _db;
|
|
private readonly InstanceDatabaseService _instanceSvc;
|
|
private readonly ConfigurationDatabaseService _configSvc;
|
|
private readonly SectionDatabaseService _sectionSvc;
|
|
private readonly ResourceDatabaseService _resourceSvc;
|
|
private readonly UserDatabaseService _userSvc;
|
|
private readonly DeviceDatabaseService _deviceSvc;
|
|
private readonly IHttpClientFactory _httpClientFactory;
|
|
|
|
public MigrationController(
|
|
MyInfoMateDbContext db,
|
|
InstanceDatabaseService instanceSvc,
|
|
ConfigurationDatabaseService configSvc,
|
|
SectionDatabaseService sectionSvc,
|
|
ResourceDatabaseService resourceSvc,
|
|
UserDatabaseService userSvc,
|
|
DeviceDatabaseService deviceSvc,
|
|
IHttpClientFactory httpClientFactory)
|
|
{
|
|
_db = db;
|
|
_instanceSvc = instanceSvc;
|
|
_configSvc = configSvc;
|
|
_sectionSvc = sectionSvc;
|
|
_resourceSvc = resourceSvc;
|
|
_userSvc = userSvc;
|
|
_deviceSvc = deviceSvc;
|
|
_httpClientFactory = httpClientFactory;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Run (or simulate) the MongoDB → PostgreSQL migration.
|
|
/// Use dryRun=true to validate without writing anything to the database.
|
|
/// Use instanceId to restrict to a single instance.
|
|
/// </summary>
|
|
[HttpPost("run")]
|
|
[ProducesResponseType(typeof(MigrationReportDTO), 200)]
|
|
[ProducesResponseType(typeof(string), 500)]
|
|
public async Task<ObjectResult> Run(
|
|
[FromQuery] bool dryRun = false,
|
|
[FromQuery] string instanceId = null)
|
|
{
|
|
var report = new MigrationReportDTO { DryRun = dryRun };
|
|
|
|
try
|
|
{
|
|
await MigrateInstancesAsync(report, dryRun, instanceId);
|
|
await MigrateResourcesAsync(report, dryRun, instanceId);
|
|
await MigrateUsersAsync(report, dryRun, instanceId);
|
|
await MigrateConfigurationsAsync(report, dryRun, instanceId);
|
|
await MigrateApplicationInstancesAsync(report, dryRun, instanceId);
|
|
await MigrateSectionsAsync(report, dryRun, instanceId);
|
|
await MigrateDevicesAsync(report, dryRun, instanceId);
|
|
await LinkMenuSectionsAsync(report, dryRun);
|
|
await EnrichPdfResourcesAsync(report, dryRun);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
report.FatalError = ex.Message;
|
|
}
|
|
|
|
return new OkObjectResult(report);
|
|
}
|
|
|
|
// ─── Instances ────────────────────────────────────────────────────────
|
|
|
|
private async Task MigrateInstancesAsync(MigrationReportDTO report, bool dryRun, string filterInstanceId)
|
|
{
|
|
var source = _instanceSvc.GetAll();
|
|
if (filterInstanceId != null)
|
|
source = source.Where(i => i.Id == filterInstanceId).ToList();
|
|
|
|
foreach (var old in source)
|
|
{
|
|
try
|
|
{
|
|
if (await _db.Instances.AnyAsync(x => x.Id == old.Id))
|
|
{
|
|
report.Skipped.Add($"Instance {old.Id} ({old.Name}) — already exists");
|
|
continue;
|
|
}
|
|
|
|
var entity = new Instance
|
|
{
|
|
Id = old.Id,
|
|
Name = old.Name,
|
|
DateCreation = old.DateCreation,
|
|
PinCode = old.PinCode?.ToString(),
|
|
};
|
|
|
|
if (!dryRun)
|
|
_db.Instances.Add(entity);
|
|
|
|
report.Migrated.Instances++;
|
|
report.Details.Add($"Instance {old.Id} ({old.Name})");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
report.Errors.Add($"Instance {old.Id}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
if (!dryRun) await _db.SaveChangesAsync();
|
|
}
|
|
|
|
// ─── Resources ────────────────────────────────────────────────────────
|
|
|
|
private async Task MigrateResourcesAsync(MigrationReportDTO report, bool dryRun, string filterInstanceId)
|
|
{
|
|
var instanceIds = filterInstanceId != null
|
|
? new List<string> { filterInstanceId }
|
|
: _instanceSvc.GetAll().Select(i => i.Id).ToList();
|
|
var source = instanceIds.SelectMany(id => _resourceSvc.GetAll(id)).ToList();
|
|
|
|
// Fetch sizes en parallèle par batches de 30
|
|
var httpClient = _httpClientFactory.CreateClient();
|
|
httpClient.Timeout = TimeSpan.FromSeconds(10);
|
|
|
|
var sizemap = new System.Collections.Concurrent.ConcurrentDictionary<string, long>();
|
|
var urlSource = source.Where(r => !string.IsNullOrEmpty(r.Url)).ToList();
|
|
|
|
const int batchSize = 30;
|
|
for (int i = 0; i < urlSource.Count; i += batchSize)
|
|
{
|
|
var batch = urlSource.Skip(i).Take(batchSize);
|
|
await Task.WhenAll(batch.Select(async r =>
|
|
{
|
|
try
|
|
{
|
|
var req = new HttpRequestMessage(HttpMethod.Head, r.Url);
|
|
var resp = await httpClient.SendAsync(req);
|
|
if (resp.Content.Headers.ContentLength.HasValue)
|
|
sizemap[r.Id] = resp.Content.Headers.ContentLength.Value;
|
|
}
|
|
catch { /* URL inaccessible ou expirée → SizeBytes restera 0 */ }
|
|
}));
|
|
}
|
|
|
|
foreach (var old in source)
|
|
{
|
|
try
|
|
{
|
|
if (await _db.Resources.AnyAsync(x => x.Id == old.Id))
|
|
{
|
|
report.Skipped.Add($"Resource {old.Id} ({old.Label}) — already exists");
|
|
continue;
|
|
}
|
|
|
|
var entity = new Resource
|
|
{
|
|
Id = old.Id,
|
|
Type = old.Type,
|
|
Label = old.Label,
|
|
DateCreation = old.DateCreation,
|
|
InstanceId = old.InstanceId,
|
|
Url = old.Url,
|
|
SizeBytes = sizemap.TryGetValue(old.Id, out var size) ? size : 0,
|
|
};
|
|
|
|
if (!dryRun)
|
|
_db.Resources.Add(entity);
|
|
|
|
report.Migrated.Resources++;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
report.Errors.Add($"Resource {old.Id}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
if (!dryRun) await _db.SaveChangesAsync();
|
|
}
|
|
|
|
// ─── Users ────────────────────────────────────────────────────────────
|
|
|
|
private async Task MigrateUsersAsync(MigrationReportDTO report, bool dryRun, string filterInstanceId)
|
|
{
|
|
var source = _userSvc.GetAll();
|
|
if (filterInstanceId != null)
|
|
source = source.Where(u => u.InstanceId == filterInstanceId).ToList();
|
|
|
|
foreach (var old in source)
|
|
{
|
|
try
|
|
{
|
|
if (await _db.Users.AnyAsync(x => x.Id == old.Id))
|
|
{
|
|
report.Skipped.Add($"User {old.Id} ({old.Email}) — already exists");
|
|
continue;
|
|
}
|
|
|
|
var entity = new User
|
|
{
|
|
Id = old.Id,
|
|
Email = old.Email,
|
|
Password = old.Password,
|
|
FirstName = old.FirstName,
|
|
LastName = old.LastName,
|
|
Token = old.Token,
|
|
DateCreation = old.DateCreation,
|
|
InstanceId = old.InstanceId,
|
|
Role = UserRole.ContentEditor,
|
|
};
|
|
|
|
if (!dryRun)
|
|
_db.Users.Add(entity);
|
|
|
|
report.Migrated.Users++;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
report.Errors.Add($"User {old.Id}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
if (!dryRun) await _db.SaveChangesAsync();
|
|
}
|
|
|
|
// ─── Configurations ───────────────────────────────────────────────────
|
|
|
|
private async Task MigrateConfigurationsAsync(MigrationReportDTO report, bool dryRun, string filterInstanceId)
|
|
{
|
|
var instanceIds = filterInstanceId != null
|
|
? new List<string> { filterInstanceId }
|
|
: _instanceSvc.GetAll().Select(i => i.Id).ToList();
|
|
var source = instanceIds.SelectMany(id => _configSvc.GetAll(id)).ToList();
|
|
|
|
foreach (var old in source)
|
|
{
|
|
try
|
|
{
|
|
if (await _db.Configurations.AnyAsync(x => x.Id == old.Id))
|
|
{
|
|
report.Skipped.Add($"Configuration {old.Id} ({old.Label}) — already exists");
|
|
continue;
|
|
}
|
|
|
|
var entity = new Configuration
|
|
{
|
|
Id = old.Id,
|
|
InstanceId = old.InstanceId,
|
|
Label = old.Label,
|
|
Title = old.Title ?? new List<TranslationDTO>(),
|
|
ImageId = old.ImageId,
|
|
ImageSource = old.ImageSource,
|
|
PrimaryColor = old.PrimaryColor,
|
|
SecondaryColor = old.SecondaryColor,
|
|
Languages = old.Languages ?? new List<string>(),
|
|
DateCreation = old.DateCreation,
|
|
IsOffline = old.IsOffline,
|
|
LoaderImageId = old.LoaderImageId,
|
|
LoaderImageUrl = old.LoaderImageUrl,
|
|
IsQRCode = false,
|
|
IsSearchText = false,
|
|
IsSearchNumber = false,
|
|
};
|
|
|
|
if (!dryRun)
|
|
_db.Configurations.Add(entity);
|
|
|
|
report.Migrated.Configurations++;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
report.Errors.Add($"Configuration {old.Id}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
if (!dryRun) await _db.SaveChangesAsync();
|
|
}
|
|
|
|
// ─── ApplicationInstances + AppConfigurationLinks ────────────────────
|
|
|
|
private async Task MigrateApplicationInstancesAsync(MigrationReportDTO report, bool dryRun, string filterInstanceId)
|
|
{
|
|
var instanceIds = filterInstanceId != null
|
|
? new List<string> { filterInstanceId }
|
|
: _instanceSvc.GetAll().Select(i => i.Id).ToList();
|
|
|
|
foreach (var iid in instanceIds)
|
|
{
|
|
var configs = _configSvc.GetAll(iid);
|
|
|
|
foreach (var appType in new[] { AppType.Tablet, AppType.Mobile })
|
|
{
|
|
var relevant = appType == AppType.Tablet
|
|
? configs.Where(c => c.IsTablet).ToList()
|
|
: configs.Where(c => c.IsMobile).ToList();
|
|
|
|
if (!relevant.Any()) continue;
|
|
|
|
var appInstanceId = $"migration-{appType.ToString().ToLower()}-{iid}";
|
|
|
|
try
|
|
{
|
|
if (!await _db.ApplicationInstances.AnyAsync(x => x.Id == appInstanceId))
|
|
{
|
|
var firstConfig = relevant.First();
|
|
var allLanguages = relevant
|
|
.Where(c => c.Languages != null)
|
|
.SelectMany(c => c.Languages)
|
|
.Distinct()
|
|
.ToList();
|
|
|
|
var appInstance = new ApplicationInstance
|
|
{
|
|
Id = appInstanceId,
|
|
InstanceId = iid,
|
|
AppType = appType,
|
|
Languages = allLanguages,
|
|
LoaderImageId = firstConfig.LoaderImageId,
|
|
LoaderImageUrl = firstConfig.LoaderImageUrl,
|
|
PrimaryColor = firstConfig.PrimaryColor,
|
|
SecondaryColor = firstConfig.SecondaryColor,
|
|
Configurations = new List<AppConfigurationLink>(),
|
|
};
|
|
if (!dryRun) _db.ApplicationInstances.Add(appInstance);
|
|
report.Migrated.ApplicationInstances++;
|
|
report.Details.Add($"ApplicationInstance {appType} for instance {iid}");
|
|
}
|
|
|
|
foreach (var config in relevant)
|
|
{
|
|
var linkId = $"migration-{appType.ToString().ToLower()}-link-{config.Id}";
|
|
if (await _db.AppConfigurationLinks.AnyAsync(x => x.Id == linkId))
|
|
{
|
|
report.Skipped.Add($"AppConfigurationLink {linkId} — already exists");
|
|
continue;
|
|
}
|
|
|
|
var link = new AppConfigurationLink
|
|
{
|
|
Id = linkId,
|
|
ConfigurationId = config.Id,
|
|
ApplicationInstanceId = appInstanceId,
|
|
IsActive = true,
|
|
IsDate = appType == AppType.Tablet && config.IsDate,
|
|
IsHour = appType == AppType.Tablet && config.IsHour,
|
|
IsSectionImageBackground = appType == AppType.Tablet && config.IsSectionImageBackground,
|
|
RoundedValue = appType == AppType.Tablet ? config.RoundedValue : null,
|
|
ScreenPercentageSectionsMainPage = appType == AppType.Tablet ? config.ScreenPercentageSectionsMainPage : null,
|
|
LoaderImageId = config.LoaderImageId,
|
|
LoaderImageUrl = config.LoaderImageUrl,
|
|
PrimaryColor = config.PrimaryColor,
|
|
SecondaryColor = config.SecondaryColor,
|
|
};
|
|
if (!dryRun) _db.AppConfigurationLinks.Add(link);
|
|
report.Migrated.AppConfigurationLinks++;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
report.Errors.Add($"ApplicationInstance {appType} {iid}: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!dryRun) await _db.SaveChangesAsync();
|
|
}
|
|
|
|
// ─── Sections ─────────────────────────────────────────────────────────
|
|
|
|
private async Task MigrateSectionsAsync(MigrationReportDTO report, bool dryRun, string filterInstanceId)
|
|
{
|
|
var instanceIds = filterInstanceId != null
|
|
? new List<string> { filterInstanceId }
|
|
: _instanceSvc.GetAll().Select(i => i.Id).ToList();
|
|
|
|
// GetAllFromConfigurationEvenSubsection inclut sections ET sous-sections
|
|
var configIds = instanceIds
|
|
.SelectMany(id => _configSvc.GetAll(id))
|
|
.Select(c => c.Id)
|
|
.ToList();
|
|
|
|
var source = configIds
|
|
.SelectMany(cid => _sectionSvc.GetAllFromConfigurationEvenSubsection(cid))
|
|
.GroupBy(s => s.Id).Select(g => g.First())
|
|
.ToList();
|
|
|
|
var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
|
|
|
foreach (var old in source)
|
|
{
|
|
try
|
|
{
|
|
if (await _db.Sections.AnyAsync(x => x.Id == old.Id))
|
|
{
|
|
report.Skipped.Add($"Section {old.Id} ({old.Label}, type {old.Type}) — already exists");
|
|
continue;
|
|
}
|
|
|
|
Section entity = BuildSection(old, opts, report);
|
|
if (entity == null) continue;
|
|
|
|
if (!dryRun)
|
|
_db.Sections.Add(entity);
|
|
|
|
report.Migrated.Sections++;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
report.Errors.Add($"Section {old.Id} ({old.Label}, type {old.Type}): {ex.Message}");
|
|
}
|
|
}
|
|
|
|
if (!dryRun) await _db.SaveChangesAsync();
|
|
}
|
|
|
|
private Section BuildSection(OldSection old, JsonSerializerOptions opts, MigrationReportDTO report)
|
|
{
|
|
// Champs communs à tous les sous-types
|
|
void FillBase(Section s)
|
|
{
|
|
s.Id = old.Id;
|
|
s.Label = old.Label ?? "";
|
|
s.Title = old.Title ?? new List<TranslationDTO>();
|
|
s.Description = old.Description ?? new List<TranslationDTO>();
|
|
s.Order = old.Order;
|
|
s.ConfigurationId = old.ConfigurationId;
|
|
s.ImageId = old.ImageId;
|
|
s.ImageSource = old.ImageSource;
|
|
s.Type = old.Type;
|
|
s.IsSubSection = old.IsSubSection;
|
|
s.ParentId = old.ParentId;
|
|
s.DateCreation = old.DateCreation;
|
|
s.InstanceId = old.InstanceId;
|
|
s.IsBeacon = old.IsBeacon;
|
|
s.BeaconId = old.BeaconId;
|
|
s.Latitude = old.Latitude;
|
|
s.Longitude = old.Longitude;
|
|
s.MeterZoneGPS = old.MeterZoneGPS;
|
|
s.IsActive = true;
|
|
}
|
|
|
|
switch (old.Type)
|
|
{
|
|
case SectionType.Map:
|
|
{
|
|
var dto = ParseData<OldMapDTO>(old, opts, report);
|
|
var s = new SectionMap
|
|
{
|
|
MapZoom = dto?.zoom ?? 18,
|
|
MapMapType = dto?.mapType,
|
|
MapTypeMapbox = dto?.mapTypeMapbox,
|
|
MapMapProvider = dto?.mapProvider,
|
|
MapResourceId = dto?.iconResourceId,
|
|
MapCenterLatitude = dto?.latitude,
|
|
MapCenterLongitude = dto?.longitude,
|
|
MapCategories = dto?.categories?.Select(c => new CategorieDTO
|
|
{
|
|
id = c.id,
|
|
label = c.label,
|
|
icon = c.icon,
|
|
order = c.order,
|
|
}).ToList() ?? new List<CategorieDTO>(),
|
|
MapPoints = dto?.points?.Select(p => BuildGeoPoint(p, old.Id)).ToList()
|
|
?? new List<GeoPoint>(),
|
|
};
|
|
FillBase(s);
|
|
return s;
|
|
}
|
|
|
|
case SectionType.Slider:
|
|
{
|
|
var dto = ParseData<OldSliderDTO>(old, opts, report);
|
|
var s = new SectionSlider
|
|
{
|
|
SliderContents = dto?.contents ?? new List<ContentDTO>(),
|
|
};
|
|
FillBase(s);
|
|
return s;
|
|
}
|
|
|
|
case SectionType.Video:
|
|
{
|
|
var dto = ParseData<OldVideoDTO>(old, opts, report);
|
|
var s = new SectionVideo { VideoSource = dto?.source ?? "" };
|
|
FillBase(s);
|
|
return s;
|
|
}
|
|
|
|
case SectionType.Web:
|
|
{
|
|
var dto = ParseData<OldWebDTO>(old, opts, report);
|
|
var s = new SectionWeb { WebSource = dto?.source ?? "" };
|
|
FillBase(s);
|
|
return s;
|
|
}
|
|
|
|
case SectionType.Menu:
|
|
{
|
|
// Les relations MenuSections sont gérées dans LinkMenuSectionsAsync
|
|
var s = new SectionMenu { MenuSections = new List<Section>() };
|
|
FillBase(s);
|
|
return s;
|
|
}
|
|
|
|
case SectionType.Quiz:
|
|
{
|
|
var dto = ParseData<OldQuizzDTO>(old, opts, report);
|
|
var s = new SectionQuiz
|
|
{
|
|
QuizQuestions = new List<QuizQuestion>(),
|
|
QuizBadLevel = dto?.bad_level?.label ?? new List<TranslationAndResourceDTO>(),
|
|
QuizMediumLevel = dto?.medium_level?.label ?? new List<TranslationAndResourceDTO>(),
|
|
QuizGoodLevel = dto?.good_level?.label ?? new List<TranslationAndResourceDTO>(),
|
|
QuizGreatLevel = dto?.great_level?.label ?? new List<TranslationAndResourceDTO>(),
|
|
};
|
|
FillBase(s);
|
|
return s;
|
|
}
|
|
|
|
case SectionType.Article:
|
|
{
|
|
var dto = ParseData<OldArticleDTO>(old, opts, report);
|
|
var s = new SectionArticle
|
|
{
|
|
ArticleContent = dto?.content ?? new List<TranslationDTO>(),
|
|
ArticleIsContentTop = dto?.isContentTop ?? false,
|
|
ArticleAudioIds = dto?.audioIds ?? new List<TranslationDTO>(),
|
|
ArticleIsReadAudioAuto = dto?.isReadAudioAuto ?? false,
|
|
ArticleContents = dto?.contents ?? new List<ContentDTO>(),
|
|
};
|
|
FillBase(s);
|
|
return s;
|
|
}
|
|
|
|
case SectionType.PDF:
|
|
{
|
|
var dto = ParseData<OldPdfDTO>(old, opts, report);
|
|
var s = new SectionPdf
|
|
{
|
|
PDFOrderedTranslationAndResources = dto?.pdfs?.Select(p =>
|
|
new OrderedTranslationAndResourceDTO
|
|
{
|
|
translationAndResourceDTOs = p.pdfFilesAndTitles ?? new List<TranslationAndResourceDTO>(),
|
|
order = p.order,
|
|
}).ToList() ?? new List<OrderedTranslationAndResourceDTO>(),
|
|
};
|
|
FillBase(s);
|
|
return s;
|
|
}
|
|
|
|
case SectionType.Game:
|
|
{
|
|
var dto = ParseData<OldPuzzleDTO>(old, opts, report);
|
|
var s = new SectionGame
|
|
{
|
|
GameMessageDebut = ToTranslationAndResourceList(dto?.messageDebut),
|
|
GameMessageFin = ToTranslationAndResourceList(dto?.messageFin),
|
|
GamePuzzleImageId = dto?.image?.resourceId,
|
|
GamePuzzleRows = dto?.rows ?? 3,
|
|
GamePuzzleCols = dto?.cols ?? 3,
|
|
GameType = SectionGame.GameTypes.Puzzle,
|
|
};
|
|
FillBase(s);
|
|
return s;
|
|
}
|
|
|
|
case SectionType.Agenda:
|
|
{
|
|
var dto = ParseData<OldAgendaDTO>(old, opts, report);
|
|
var s = new SectionAgenda
|
|
{
|
|
IsOnlineAgenda = true,
|
|
AgendaResourceIds = dto?.resourceIds ?? new List<TranslationDTO>(),
|
|
AgendaMapProvider = dto?.mapProvider,
|
|
EventAgendas = new List<EventAgenda>(),
|
|
};
|
|
FillBase(s);
|
|
return s;
|
|
}
|
|
|
|
case SectionType.Weather:
|
|
{
|
|
var dto = ParseData<OldWeatherDTO>(old, opts, report);
|
|
var s = new SectionWeather
|
|
{
|
|
WeatherCity = dto?.city,
|
|
WeatherUpdatedDate = dto?.updatedDate,
|
|
WeatherResult = dto?.result,
|
|
};
|
|
FillBase(s);
|
|
return s;
|
|
}
|
|
|
|
default:
|
|
report.Errors.Add($"Section {old.Id}: unknown SectionType {old.Type} — skipped");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private GeoPoint BuildGeoPoint(OldGeoPointDTO p, string sectionMapId)
|
|
{
|
|
Geometry geometry = null;
|
|
if (p.latitude != null && p.longitude != null
|
|
&& double.TryParse(p.latitude, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var lat)
|
|
&& double.TryParse(p.longitude, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var lon))
|
|
{
|
|
geometry = new Point(lon, lat) { SRID = 4326 };
|
|
}
|
|
|
|
return new GeoPoint
|
|
{
|
|
Title = p.title ?? new List<TranslationDTO>(),
|
|
Description = p.description ?? new List<TranslationDTO>(),
|
|
Contents = p.contents?.Select(c => new ContentDTO
|
|
{
|
|
resourceId = c.resourceId,
|
|
}).ToList() ?? new List<ContentDTO>(),
|
|
CategorieId = p.categorieId ?? p.categorie?.id,
|
|
Geometry = geometry,
|
|
ImageResourceId = p.imageResourceId,
|
|
ImageUrl = p.imageUrl,
|
|
Schedules = p.schedules ?? new List<TranslationDTO>(),
|
|
Prices = p.prices ?? new List<TranslationDTO>(),
|
|
Phone = p.phone ?? new List<TranslationDTO>(),
|
|
Email = p.email ?? new List<TranslationDTO>(),
|
|
Site = p.site ?? new List<TranslationDTO>(),
|
|
SectionMapId = sectionMapId,
|
|
};
|
|
}
|
|
|
|
// ─── Devices ──────────────────────────────────────────────────────────
|
|
|
|
private async Task MigrateDevicesAsync(MigrationReportDTO report, bool dryRun, string filterInstanceId)
|
|
{
|
|
var instanceIds = filterInstanceId != null
|
|
? new List<string> { filterInstanceId }
|
|
: _instanceSvc.GetAll().Select(i => i.Id).ToList();
|
|
var source = instanceIds.SelectMany(id => _deviceSvc.GetAll(id)).ToList();
|
|
|
|
foreach (var old in source)
|
|
{
|
|
try
|
|
{
|
|
if (await _db.Devices.AnyAsync(x => x.Id == old.Id))
|
|
{
|
|
report.Skipped.Add($"Device {old.Id} ({old.Name}) — already exists");
|
|
continue;
|
|
}
|
|
|
|
var entity = new Device
|
|
{
|
|
Id = old.Id,
|
|
Identifier = old.Identifier,
|
|
Name = old.Name,
|
|
IpAddressWLAN = old.IpAddressWLAN,
|
|
IpAddressETH = old.IpAddressETH,
|
|
ConfigurationId = old.ConfigurationId,
|
|
Connected = old.Connected,
|
|
DateCreation = old.DateCreation,
|
|
DateUpdate = old.DateUpdate < new DateTime(2000, 1, 1) ? DateTime.MinValue : old.DateUpdate,
|
|
BatteryLevel = old.BatteryLevel?.ToString(),
|
|
LastBatteryLevel = old.LastBatteryLevel,
|
|
ConnectionLevel = old.ConnectionLevel?.ToString(),
|
|
LastConnectionLevel = old.LastConnectionLevel,
|
|
InstanceId = old.InstanceId,
|
|
};
|
|
|
|
if (!dryRun)
|
|
_db.Devices.Add(entity);
|
|
|
|
report.Migrated.Devices++;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
report.Errors.Add($"Device {old.Id}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
if (!dryRun) await _db.SaveChangesAsync();
|
|
}
|
|
|
|
// ─── Post-processing : relier SectionMenu.MenuSections ───────────────
|
|
|
|
private async Task LinkMenuSectionsAsync(MigrationReportDTO report, bool dryRun)
|
|
{
|
|
var menus = await _db.Sections
|
|
.OfType<SectionMenu>()
|
|
.Include(m => m.MenuSections)
|
|
.ToListAsync();
|
|
|
|
foreach (var menu in menus)
|
|
{
|
|
if (menu.MenuSections.Count > 0)
|
|
{
|
|
report.Skipped.Add($"MenuLink {menu.Id} — already linked ({menu.MenuSections.Count} children)");
|
|
continue;
|
|
}
|
|
|
|
var oldSection = _sectionSvc.GetById(menu.Id);
|
|
if (oldSection?.Data == null) continue;
|
|
|
|
try
|
|
{
|
|
var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
|
var dto = JsonSerializer.Deserialize<OldMenuDTO>(oldSection.Data, opts);
|
|
if (dto?.sections == null) continue;
|
|
|
|
var childIds = dto.sections.Select(s => s.id).Where(id => id != null).ToList();
|
|
var children = await _db.Sections.Where(s => childIds.Contains(s.Id)).ToListAsync();
|
|
|
|
if (!dryRun)
|
|
{
|
|
menu.MenuSections = children;
|
|
_db.Sections.Update(menu);
|
|
}
|
|
|
|
report.Migrated.MenuLinks += children.Count;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
report.Errors.Add($"MenuLink {menu.Id}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
if (!dryRun) await _db.SaveChangesAsync();
|
|
}
|
|
|
|
// ─── Post-processing : enrichir les resource dans les JSONB PDF ─────────
|
|
|
|
private async Task EnrichPdfResourcesAsync(MigrationReportDTO report, bool dryRun)
|
|
{
|
|
var pdfSections = await _db.Sections.OfType<SectionPdf>().ToListAsync();
|
|
|
|
foreach (var pdf in pdfSections)
|
|
{
|
|
try
|
|
{
|
|
bool modified = false;
|
|
foreach (var ordered in pdf.PDFOrderedTranslationAndResources ?? new List<OrderedTranslationAndResourceDTO>())
|
|
{
|
|
foreach (var tar in ordered.translationAndResourceDTOs ?? new List<TranslationAndResourceDTO>())
|
|
{
|
|
if (tar.resource == null && tar.resourceId != null)
|
|
{
|
|
var resource = await _db.Resources.FindAsync(tar.resourceId);
|
|
if (resource != null)
|
|
{
|
|
tar.resource = resource.ToDTO();
|
|
modified = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (modified)
|
|
{
|
|
if (!dryRun) _db.Sections.Update(pdf);
|
|
report.Migrated.PdfResourcesEnriched++;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
report.Errors.Add($"EnrichPdf {pdf.Id}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
if (!dryRun) await _db.SaveChangesAsync();
|
|
}
|
|
|
|
// ─── Helpers ──────────────────────────────────────────────────────────
|
|
|
|
private T ParseData<T>(OldSection old, JsonSerializerOptions opts, MigrationReportDTO report) where T : class
|
|
{
|
|
if (string.IsNullOrEmpty(old.Data)) return null;
|
|
try
|
|
{
|
|
return JsonSerializer.Deserialize<T>(old.Data, opts);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
report.Errors.Add($"Section {old.Id} — failed to parse Data as {typeof(T).Name}: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private List<TranslationAndResourceDTO> ToTranslationAndResourceList(List<TranslationAndResourceDTO> source)
|
|
=> source ?? new List<TranslationAndResourceDTO>();
|
|
}
|
|
|
|
// ─── DTO du rapport ───────────────────────────────────────────────────────
|
|
|
|
public class MigrationReportDTO
|
|
{
|
|
public bool DryRun { get; set; }
|
|
public MigrationCountsDTO Migrated { get; set; } = new();
|
|
public List<string> Skipped { get; set; } = new();
|
|
public List<string> Errors { get; set; } = new();
|
|
public List<string> Details { get; set; } = new();
|
|
public string FatalError { get; set; }
|
|
}
|
|
|
|
public class MigrationCountsDTO
|
|
{
|
|
public int Instances { get; set; }
|
|
public int Resources { get; set; }
|
|
public int Users { get; set; }
|
|
public int Configurations { get; set; }
|
|
public int ApplicationInstances { get; set; }
|
|
public int AppConfigurationLinks { get; set; }
|
|
public int Sections { get; set; }
|
|
public int Devices { get; set; }
|
|
public int MenuLinks { get; set; }
|
|
public int PdfResourcesEnriched { get; set; }
|
|
}
|
|
}
|