2026-04-05 15:19:14 +02:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.IO;
|
2026-04-25 16:12:49 +02:00
|
|
|
using System.Linq;
|
2026-04-05 15:19:14 +02:00
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using Serilog;
|
|
|
|
|
using Wino.Core.Domain.Interfaces;
|
|
|
|
|
|
|
|
|
|
namespace Wino.Mail.WinUI.Services;
|
|
|
|
|
|
|
|
|
|
public sealed class ReleaseLocalAccountDataCleanupService
|
|
|
|
|
{
|
|
|
|
|
private const string CleanupCompletedSettingKey = "ReleaseLocalAccountDataCleanup_v1_Completed";
|
|
|
|
|
private const string LegacyDatabaseFileName = "Wino180.db";
|
|
|
|
|
|
|
|
|
|
private readonly IConfigurationService _configurationService;
|
|
|
|
|
private readonly IApplicationConfiguration _applicationConfiguration;
|
2026-04-25 16:12:49 +02:00
|
|
|
private readonly INotificationBuilder _notificationBuilder;
|
2026-04-05 15:19:14 +02:00
|
|
|
private readonly ILogger _logger = Log.ForContext<ReleaseLocalAccountDataCleanupService>();
|
|
|
|
|
|
|
|
|
|
public ReleaseLocalAccountDataCleanupService(IConfigurationService configurationService,
|
2026-04-25 16:12:49 +02:00
|
|
|
IApplicationConfiguration applicationConfiguration,
|
|
|
|
|
INotificationBuilder notificationBuilder)
|
2026-04-05 15:19:14 +02:00
|
|
|
{
|
|
|
|
|
_configurationService = configurationService;
|
|
|
|
|
_applicationConfiguration = applicationConfiguration;
|
2026-04-25 16:12:49 +02:00
|
|
|
_notificationBuilder = notificationBuilder;
|
2026-04-05 15:19:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task RunIfNeededAsync()
|
|
|
|
|
{
|
|
|
|
|
if (_configurationService.Get(CleanupCompletedSettingKey, false))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var localFolderPath = _applicationConfiguration.ApplicationDataFolderPath;
|
|
|
|
|
var publisherPath = _applicationConfiguration.PublisherSharedFolderPath;
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(localFolderPath) || !Directory.Exists(localFolderPath))
|
|
|
|
|
{
|
|
|
|
|
_configurationService.Set(CleanupCompletedSettingKey, true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var cleanupTargets = new List<string>
|
|
|
|
|
{
|
|
|
|
|
Path.Combine(localFolderPath, "Mime"),
|
|
|
|
|
Path.Combine(localFolderPath, "contacts"),
|
|
|
|
|
Path.Combine(localFolderPath, "CalendarAttachments"),
|
|
|
|
|
Path.Combine(publisherPath, LegacyDatabaseFileName)
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-25 16:12:49 +02:00
|
|
|
var hadLegacyData = false;
|
|
|
|
|
|
2026-04-05 15:19:14 +02:00
|
|
|
foreach (var targetPath in cleanupTargets)
|
|
|
|
|
{
|
2026-04-25 16:12:49 +02:00
|
|
|
hadLegacyData |= await DeletePathIfExistsAsync(targetPath, localFolderPath, publisherPath).ConfigureAwait(false);
|
2026-04-05 15:19:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_configurationService.Set(CleanupCompletedSettingKey, true);
|
2026-04-25 16:12:49 +02:00
|
|
|
|
|
|
|
|
if (hadLegacyData)
|
|
|
|
|
{
|
|
|
|
|
_notificationBuilder.CreateReleaseMigrationNotification();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 15:19:14 +02:00
|
|
|
_logger.Information("Completed one-time local account data cleanup for release migration.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 16:12:49 +02:00
|
|
|
private async Task<bool> DeletePathIfExistsAsync(string targetPath, params string[] allowedRootPaths)
|
2026-04-05 15:19:14 +02:00
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var fullTargetPath = Path.GetFullPath(targetPath);
|
2026-04-25 16:12:49 +02:00
|
|
|
if (!allowedRootPaths.Any(rootPath => IsPathUnderAllowedRoot(fullTargetPath, rootPath)))
|
2026-04-05 15:19:14 +02:00
|
|
|
{
|
2026-04-25 16:12:49 +02:00
|
|
|
_logger.Warning("Skipped startup cleanup for path outside allowed roots: {TargetPath}", fullTargetPath);
|
|
|
|
|
return false;
|
2026-04-05 15:19:14 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-25 16:12:49 +02:00
|
|
|
var targetExists = Directory.Exists(fullTargetPath) || File.Exists(fullTargetPath);
|
|
|
|
|
|
2026-04-05 15:19:14 +02:00
|
|
|
if (Directory.Exists(fullTargetPath))
|
|
|
|
|
{
|
|
|
|
|
await Task.Run(() => Directory.Delete(fullTargetPath, recursive: true)).ConfigureAwait(false);
|
|
|
|
|
_logger.Information("Deleted legacy startup cleanup directory {TargetPath}", fullTargetPath);
|
2026-04-25 16:12:49 +02:00
|
|
|
return true;
|
2026-04-05 15:19:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (File.Exists(fullTargetPath))
|
|
|
|
|
{
|
|
|
|
|
File.Delete(fullTargetPath);
|
|
|
|
|
_logger.Information("Deleted legacy startup cleanup file {TargetPath}", fullTargetPath);
|
2026-04-25 16:12:49 +02:00
|
|
|
return true;
|
2026-04-05 15:19:14 +02:00
|
|
|
}
|
2026-04-25 16:12:49 +02:00
|
|
|
|
|
|
|
|
return targetExists;
|
2026-04-05 15:19:14 +02:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_logger.Warning(ex, "Failed to delete legacy startup cleanup path {TargetPath}", targetPath);
|
|
|
|
|
}
|
2026-04-25 16:12:49 +02:00
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool IsPathUnderAllowedRoot(string fullTargetPath, string rootPath)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(rootPath))
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
var fullRootPath = Path.GetFullPath(rootPath);
|
|
|
|
|
var relativePath = Path.GetRelativePath(fullRootPath, fullTargetPath);
|
|
|
|
|
|
|
|
|
|
return relativePath != "." &&
|
|
|
|
|
!relativePath.StartsWith("..", StringComparison.Ordinal) &&
|
|
|
|
|
!Path.IsPathRooted(relativePath);
|
2026-04-05 15:19:14 +02:00
|
|
|
}
|
|
|
|
|
}
|