Ctrl K
ComunidadGuías…

src/SyncConflictResolver.cs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace Clipman
{
    internal static class SyncConflictResolver
    {
        public static void ResolveSettingsConflicts(string settingsPath)
        {
            try
            {
                var conflicts = FindConflictSiblings(settingsPath).ToList();
                if (conflicts.Count == 0) return;

                var allFiles = new List<string>();
                if (File.Exists(settingsPath)) allFiles.Add(settingsPath);
                allFiles.AddRange(conflicts);
                var newest = allFiles
                    .Select(path => new FileInfo(path))
                    .Where(info => info.Exists)
                    .OrderByDescending(info => info.LastWriteTimeUtc)
                    .FirstOrDefault();
                if (newest == null) return;

                Directory.CreateDirectory(Path.GetDirectoryName(settingsPath));
                if (!string.Equals(newest.FullName, settingsPath, StringComparison.OrdinalIgnoreCase))
                {
                    File.Copy(newest.FullName, settingsPath, true);
                }

                DeleteFiles(conflicts);
            }
            catch
            {
            }
        }

        public static void ResolveDatabaseConflicts(string databasePath)
        {
            ResolveDatabaseConflicts(databasePath, string.Empty);
        }

        public static void ResolveDatabaseConflicts(string databasePath, string password)
        {
            try
            {
                var conflicts = FindConflictSiblings(databasePath).ToList();
                if (conflicts.Count == 0) return;

                var merged = new ClipDatabase();
                if (File.Exists(databasePath))
                {
                    MergeInto(merged, ClipDatabaseFile.Load(databasePath, password));
                }

                foreach (var conflict in conflicts)
                {
                    MergeInto(merged, ClipDatabaseFile.Load(conflict, password));
                }

                NormalizeMergedDatabase(merged);
                ClipDatabaseFile.SaveAtomic(databasePath, merged, password);
                DeleteFiles(conflicts);
            }
            catch
            {
            }
        }

        public static IEnumerable<string> FindConflictSiblings(string canonicalPath)
        {
            if (string.IsNullOrWhiteSpace(canonicalPath)) yield break;
            var directory = Path.GetDirectoryName(canonicalPath);
            var fileName = Path.GetFileName(canonicalPath);
            if (string.IsNullOrWhiteSpace(directory) || string.IsNullOrWhiteSpace(fileName) || !Directory.Exists(directory))
            {
                yield break;
            }

            var baseName = Path.GetFileNameWithoutExtension(fileName);
            var extension = Path.GetExtension(fileName);
            foreach (var file in Directory.GetFiles(directory, "*" + extension))
            {
                if (string.Equals(file, canonicalPath, StringComparison.OrdinalIgnoreCase)) continue;
                if (IsConflictNameFor(file, baseName, extension))
                {
                    yield return file;
                }
            }
        }

        private static bool IsConflictNameFor(string path, string baseName, string extension)
        {
            if (!string.Equals(Path.GetExtension(path), extension, StringComparison.OrdinalIgnoreCase)) return false;
            var name = Path.GetFileNameWithoutExtension(path);
            if (string.IsNullOrWhiteSpace(name) || !name.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)) return false;

            var suffix = name.Substring(baseName.Length).Trim();
            if (suffix.Length == 0) return false;
            var lower = suffix.ToLowerInvariant();

            if (lower.Contains("conflicted copy")) return true;
            if (lower.Contains("[conflict]")) return true;
            if (lower.Contains(" conflict")) return true;
            if (lower.StartsWith("_conf(")) return true;
            if (lower.StartsWith(" _conf(")) return true;

            return LooksLikeOneDriveComputerSuffix(suffix);
        }

        private static bool LooksLikeOneDriveComputerSuffix(string suffix)
        {
            var value = suffix.Trim();
            if (value.Length < 3 || value.Length > 80) return false;
            if (!(value.StartsWith("-", StringComparison.Ordinal) ||
                  value.StartsWith("(", StringComparison.Ordinal) ||
                  value.StartsWith(" ", StringComparison.Ordinal)))
            {
                return false;
            }

            value = value.Trim(' ', '-', '(', ')');
            if (value.Length < 2 || value.Length > 64) return false;
            return value.All(ch => char.IsLetterOrDigit(ch) || ch == '-' || ch == '_');
        }

        public static void MergeInto(ClipDatabase target, ClipDatabase source)
        {
            if (target == null || source == null || source.Entries == null) return;
            if (target.Entries == null) target.Entries = new List<ClipEntry>();
            if (target.DeletedEntries == null) target.DeletedEntries = new List<DeletedClipEntry>();
            MergeDeletedEntries(target, source);
            ApplyDeletedEntries(target);
            foreach (var entry in source.Entries.Where(e => e != null && !string.IsNullOrEmpty(e.Text)))
            {
                if (IsDeleted(target, entry)) continue;
                var existing = target.Entries.FirstOrDefault(e =>
                    !string.IsNullOrWhiteSpace(entry.Id) &&
                    string.Equals(e.Id, entry.Id, StringComparison.OrdinalIgnoreCase));
                if (existing == null)
                {
                    existing = target.Entries.FirstOrDefault(e => string.Equals(e.Text, entry.Text, StringComparison.Ordinal));
                }

                if (existing == null)
                {
                    target.Entries.Add(Clone(entry));
                    continue;
                }

                MergeEntry(existing, entry);
            }
            ApplyDeletedEntries(target);
            NormalizeDeletedEntries(target);
        }

        private static void MergeEntry(ClipEntry existing, ClipEntry incoming)
        {
            if (string.Equals(existing.Text, incoming.Text, StringComparison.Ordinal))
            {
                existing.Id = CanonicalEntryId(existing.Id, incoming.Id);
            }
            var incomingWins = incoming.LastUsedUnixMs >= existing.LastUsedUnixMs;
            var incomingCreatedWins = incoming.CreatedUnixMs > existing.CreatedUnixMs;
            var incomingModifiedWins = incoming.ModifiedUnixMs > existing.ModifiedUnixMs;
            var bothLegacy = incoming.ModifiedUnixMs <= 0 && existing.ModifiedUnixMs <= 0;
            var legacyTextRepair = bothLegacy &&
                !string.IsNullOrWhiteSpace(existing.Id) &&
                string.Equals(existing.Id, incoming.Id, StringComparison.OrdinalIgnoreCase) &&
                !string.Equals(existing.Text ?? string.Empty, incoming.Text ?? string.Empty, StringComparison.Ordinal);
            if (incoming.LastUsedUnixMs > existing.LastUsedUnixMs) existing.LastUsedUnixMs = incoming.LastUsedUnixMs;
            if (incoming.CreatedUnixMs > 0 &&
                (existing.CreatedUnixMs == 0 ||
                 incomingCreatedWins ||
                 (!incomingWins && incoming.CreatedUnixMs < existing.CreatedUnixMs)))
            {
                existing.CreatedUnixMs = incoming.CreatedUnixMs;
            }
            if (incomingModifiedWins)
            {
                var textChanged = !string.Equals(existing.Text ?? string.Empty, incoming.Text ?? string.Empty, StringComparison.Ordinal);
                existing.Text = incoming.Text ?? string.Empty;
                existing.Name = (incoming.Name ?? string.Empty).Trim();
                existing.Group = (incoming.Group ?? string.Empty).Trim();
                existing.Pinned = incoming.Pinned;
                existing.IsTemplate = incoming.IsTemplate;
                existing.ManualOrder = incoming.ManualOrder;
                existing.ModifiedUnixMs = incoming.ModifiedUnixMs;
                if (textChanged)
                {
                    existing.RichText = RichTextData.Clone(incoming.RichText);
                    existing.RichTextUpdatedUnixMs = Math.Max(incoming.RichTextUpdatedUnixMs, incoming.ModifiedUnixMs);
                }
            }
            else if (bothLegacy)
            {
                if (legacyTextRepair)
                {
                    existing.Text = incoming.Text ?? string.Empty;
                    existing.RichText = RichTextData.Clone(incoming.RichText);
                    existing.RichTextUpdatedUnixMs = Math.Max(existing.RichTextUpdatedUnixMs, incoming.RichTextUpdatedUnixMs);
                }
                if (!string.IsNullOrWhiteSpace(incoming.Name) && incomingWins) existing.Name = incoming.Name.Trim();
                if (!string.IsNullOrWhiteSpace(incoming.Group) && incomingWins) existing.Group = incoming.Group.Trim();
                existing.Pinned = existing.Pinned || incoming.Pinned;
                existing.IsTemplate = existing.IsTemplate || incoming.IsTemplate;
                if (existing.ManualOrder <= 0 || (incoming.ManualOrder > 0 && incoming.ManualOrder < existing.ManualOrder)) existing.ManualOrder = incoming.ManualOrder;
            }
            if (!string.IsNullOrWhiteSpace(incoming.SourceMachine) && (incomingWins || incomingCreatedWins)) existing.SourceMachine = incoming.SourceMachine.Trim();
            if (incoming.RichTextUpdatedUnixMs > existing.RichTextUpdatedUnixMs)
            {
                existing.RichText = RichTextData.Clone(incoming.RichText);
                existing.RichTextUpdatedUnixMs = incoming.RichTextUpdatedUnixMs;
            }
        }

        private static string CanonicalEntryId(string left, string right)
        {
            if (string.IsNullOrWhiteSpace(left)) return right;
            if (string.IsNullOrWhiteSpace(right)) return left;
            var insensitive = StringComparer.OrdinalIgnoreCase.Compare(left, right);
            if (insensitive != 0) return insensitive < 0 ? left : right;
            return string.CompareOrdinal(left, right) <= 0 ? left : right;
        }

        private static void NormalizeMergedDatabase(ClipDatabase database)
        {
            if (database.Entries == null) database.Entries = new List<ClipEntry>();
            database.Version = Math.Max(1, database.Version);
            database.UpdatedUnixMs = TimeUtil.NowUnixMs();
            NormalizeDeletedEntries(database);
            ApplyDeletedEntries(database);

            var next = 1L;
            foreach (var entry in database.Entries
                .OrderBy(e => e.ManualOrder <= 0 ? long.MaxValue : e.ManualOrder)
                .ThenBy(e => e.CreatedUnixMs))
            {
                if (string.IsNullOrWhiteSpace(entry.Id)) entry.Id = Guid.NewGuid().ToString("N");
                if (entry.Name == null) entry.Name = string.Empty;
                if (entry.Group == null) entry.Group = string.Empty;
                if (entry.SourceMachine == null) entry.SourceMachine = string.Empty;
                if (entry.CreatedUnixMs == 0) entry.CreatedUnixMs = TimeUtil.NowUnixMs();
                if (entry.LastUsedUnixMs == 0) entry.LastUsedUnixMs = entry.CreatedUnixMs;
                entry.ManualOrder = next++;
            }
        }

        private static ClipEntry Clone(ClipEntry entry)
        {
            return new ClipEntry
            {
                Id = string.IsNullOrWhiteSpace(entry.Id) ? Guid.NewGuid().ToString("N") : entry.Id,
                Text = entry.Text ?? string.Empty,
                Name = entry.Name ?? string.Empty,
                Group = entry.Group ?? string.Empty,
                SourceMachine = entry.SourceMachine ?? string.Empty,
                CreatedUnixMs = entry.CreatedUnixMs,
                LastUsedUnixMs = entry.LastUsedUnixMs,
                ModifiedUnixMs = entry.ModifiedUnixMs,
                Pinned = entry.Pinned,
                IsTemplate = entry.IsTemplate,
                ManualOrder = entry.ManualOrder,
                RichText = RichTextData.Clone(entry.RichText),
                RichTextUpdatedUnixMs = entry.RichTextUpdatedUnixMs
            };
        }

        private static void MergeDeletedEntries(ClipDatabase target, ClipDatabase source)
        {
            NormalizeDeletedEntries(target);
            if (source.DeletedEntries == null) return;
            foreach (var marker in source.DeletedEntries.Where(d => d != null && !string.IsNullOrWhiteSpace(d.Id)))
            {
                var existing = target.DeletedEntries.FirstOrDefault(d =>
                    string.Equals(d.Id, marker.Id, StringComparison.OrdinalIgnoreCase));
                if (existing == null)
                {
                    target.DeletedEntries.Add(Clone(marker));
                }
                else if (marker.DeletedUnixMs > existing.DeletedUnixMs)
                {
                    existing.DeletedUnixMs = marker.DeletedUnixMs;
                    existing.TextHash = marker.TextHash ?? string.Empty;
                    existing.SourceMachine = marker.SourceMachine ?? string.Empty;
                }
                else if (string.IsNullOrWhiteSpace(existing.TextHash) && !string.IsNullOrWhiteSpace(marker.TextHash))
                {
                    existing.TextHash = marker.TextHash;
                }
            }
            NormalizeDeletedEntries(target);
        }

        private static void NormalizeDeletedEntries(ClipDatabase database)
        {
            if (database.DeletedEntries == null)
            {
                database.DeletedEntries = new List<DeletedClipEntry>();
                return;
            }
            var now = TimeUtil.NowUnixMs();
            var cutoff = now - (long)TimeSpan.FromDays(90).TotalMilliseconds;
            foreach (var marker in database.DeletedEntries.Where(d => d != null && d.DeletedUnixMs <= 0))
            {
                marker.DeletedUnixMs = now;
            }
            database.DeletedEntries = database.DeletedEntries
                .Where(d => d != null && !string.IsNullOrWhiteSpace(d.Id) && d.DeletedUnixMs >= cutoff)
                .GroupBy(d => d.Id, StringComparer.OrdinalIgnoreCase)
                .Select(group => group.OrderByDescending(d => d.DeletedUnixMs).First())
                .Where(marker => !string.IsNullOrWhiteSpace(marker.TextHash) || !(database.Entries ?? new List<ClipEntry>()).Any(entry =>
                    string.Equals(entry.Id, marker.Id, StringComparison.OrdinalIgnoreCase) &&
                    Math.Max(Math.Max(entry.CreatedUnixMs, entry.LastUsedUnixMs), entry.ModifiedUnixMs) > marker.DeletedUnixMs))
                .ToList();
        }

        private static void ApplyDeletedEntries(ClipDatabase database)
        {
            if (database.Entries == null || database.DeletedEntries == null || database.DeletedEntries.Count == 0) return;
            database.Entries.RemoveAll(entry => IsDeleted(database, entry));
        }

        private static bool IsDeleted(ClipDatabase database, ClipEntry entry)
        {
            if (entry == null || database.DeletedEntries == null) return false;
            var entryChangedUnixMs = Math.Max(Math.Max(entry.CreatedUnixMs, entry.LastUsedUnixMs), entry.ModifiedUnixMs);
            var hash = ComputeTextHash(entry.Text);
            return database.DeletedEntries.Any(marker =>
                (string.Equals(marker.Id, entry.Id, StringComparison.OrdinalIgnoreCase) &&
                 (!string.IsNullOrWhiteSpace(marker.TextHash) || marker.DeletedUnixMs <= 0 || entryChangedUnixMs <= marker.DeletedUnixMs)) ||
                (!string.IsNullOrWhiteSpace(marker.TextHash) &&
                 string.Equals(marker.TextHash, hash, StringComparison.OrdinalIgnoreCase) &&
                 (marker.DeletedUnixMs <= 0 || entryChangedUnixMs <= marker.DeletedUnixMs)));
        }

        private static string ComputeTextHash(string text)
        {
            using (var sha = SHA256.Create())
            {
                return string.Concat(sha.ComputeHash(Encoding.UTF8.GetBytes(text ?? string.Empty)).Select(b => b.ToString("x2")));
            }
        }

        private static DeletedClipEntry Clone(DeletedClipEntry marker)
        {
            return new DeletedClipEntry
            {
                Id = marker.Id ?? string.Empty,
                TextHash = marker.TextHash ?? string.Empty,
                DeletedUnixMs = marker.DeletedUnixMs,
                SourceMachine = marker.SourceMachine ?? string.Empty
            };
        }

        private static void DeleteFiles(IEnumerable<string> paths)
        {
            foreach (var path in paths)
            {
                try
                {
                    if (File.Exists(path))
                    {
                        File.Delete(path);
                    }
                }
                catch
                {
                }
            }
        }
    }
}