| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270 | using InABox.Configuration;using InABox.Core;using Microsoft.CodeAnalysis.Scripting;using System;using System.Collections.Generic;using System.Diagnostics.CodeAnalysis;using System.Linq;using System.Text;using System.Text.RegularExpressions;using System.Threading.Tasks;namespace InABox.Database{    public class DatabaseVersion : BaseObject, GlobalConfigurationSettings    {        public string Version { get; set; }        public DatabaseVersion()        {            Version = "0.00";        }    }    public class VersionNumber    {        public int MajorVersion { get; set; }        public int MinorVersion { get; set; }        public string Release { get; set; }        public bool IsDevelopmentVersion { get; set; }        private VersionNumber(int majorVersion, int minorVersion, string release, bool isDevelopmentVersion)        {            MajorVersion = majorVersion;            MinorVersion = minorVersion;            Release = release;            IsDevelopmentVersion = isDevelopmentVersion;        }        private static Regex _format = new(@"^(\d+)\.(\d+)([a-zA-Z]*)$");        public static VersionNumber Parse(string versionStr)        {            if(versionStr == "???")            {                return new(0, 0, "", true);            }            var match = _format.Match(versionStr);            if (!match.Success)                throw new FormatException($"'{versionStr}' is not a valid version!");            return new(int.Parse(match.Groups[1].Value), int.Parse(match.Groups[2].Value), match.Groups[3].Value, false);        }        public static bool TryParse(string versionStr, [NotNullWhen(true)] out VersionNumber? version)        {            if (versionStr == "???")            {                version = new(0, 0, "", true);                return true;            }            var match = _format.Match(versionStr);            if (!match.Success)            {                version = null;                return false;            }            version = new(int.Parse(match.Groups[1].Value), int.Parse(match.Groups[2].Value), match.Groups[3].Value, false);            return true;        }        public static bool operator <(VersionNumber a, VersionNumber b)        {            if (a.IsDevelopmentVersion)            {                return false;            }            else if (b.IsDevelopmentVersion)            {                return true;            }            return a.MajorVersion < b.MajorVersion ||                 (a.MajorVersion == b.MajorVersion &&                    (a.MinorVersion < b.MinorVersion ||                         (a.MinorVersion == b.MinorVersion && string.Compare(a.Release, b.Release, StringComparison.Ordinal) < 0)));        }        public static bool operator >(VersionNumber a, VersionNumber b)        {            return b < a;        }        public static bool operator <=(VersionNumber a, VersionNumber b)        {            return !(b < a);        }        public static bool operator >=(VersionNumber a, VersionNumber b)        {            return !(a < b);        }        public override bool Equals(object? obj)        {            if(obj is VersionNumber v)            {                return this == v;            }            return false;        }        public override int GetHashCode()        {            if (IsDevelopmentVersion)                return 0;            return MajorVersion ^ MinorVersion ^ Release.GetHashCode();        }        public static bool operator ==(VersionNumber a, VersionNumber b)        {            if (a.IsDevelopmentVersion)                return b.IsDevelopmentVersion;            if (b.IsDevelopmentVersion)                return false;            return a.MajorVersion == b.MajorVersion && a.MinorVersion == b.MinorVersion && a.Release == b.Release;        }        public static bool operator !=(VersionNumber a, VersionNumber b)        {            if (a.IsDevelopmentVersion)                return !b.IsDevelopmentVersion;            if (b.IsDevelopmentVersion)                return true;            return a.MajorVersion != b.MajorVersion || a.MinorVersion != b.MinorVersion || a.Release != b.Release;        }        public override string ToString()        {            return IsDevelopmentVersion ? "???" : $"{MajorVersion}.{MinorVersion}{Release}";        }    }    public static class DataUpdater    {        private static Dictionary<VersionNumber, List<Func<bool>>> updateScripts = new();        /// <summary>        /// Register a migration script to run when updating to this version.        ///         /// <para>The <paramref name="action"/> should probably be repeatable;         /// that is, if you run it a second time, it only updates data that needed updating. This way if it accidentally somehow gets run twice, there is no issue.        /// </para>        /// </summary>        /// <param name="version">The version to update to.</param>        /// <param name="action">The action to be run.</param>        public static void RegisterUpdateScript(string version, Func<bool> action)        {            var versionNumber = VersionNumber.Parse(version);            if(!updateScripts.TryGetValue(versionNumber, out var list))            {                list = new();                updateScripts[versionNumber] = list;            }            list.Add(action);        }        private static bool MigrateDatabase(VersionNumber fromVersion, VersionNumber toVersion, out VersionNumber newVersion)        {            var versionNumbers = updateScripts.Keys.ToList();            versionNumbers.Sort((x, y) => x == y ? 0 : x < y ? -1 : 1);            newVersion = fromVersion;            int? index = null;            foreach (var (i, number) in versionNumbers.Select((x, i) => new Tuple<int, VersionNumber>(i, x)))            {                if (number > fromVersion)                {                    index = i;                    break;                }            }            if(index != null && fromVersion < toVersion)            {                Logger.Send(LogType.Information, "", $"Updating database from {fromVersion} to {toVersion}");                for (int i = (int)index; i < versionNumbers.Count; i++)                {                    var version = versionNumbers[i];                    if (toVersion < version)                    {                        break;                    }                    Logger.Send(LogType.Information, "", $"Executing update to {version}");                    foreach(var script in updateScripts[version])                    {                        if (!script())                        {                            Logger.Send(LogType.Error, "", $"Script failed, cancelling migration");                            return false;                        }                    }                    newVersion = version;                }                Logger.Send(LogType.Information, "", $"Data migration complete!");            }            newVersion = toVersion;            return true;        }        private static DatabaseVersion GetVersionSettings()        {            var result = DbFactory.Provider.Query(new Filter<GlobalSettings>(x => x.Section).IsEqualTo(nameof(DatabaseVersion)))                .Rows.FirstOrDefault()?.ToObject<GlobalSettings>();            if(result != null)            {                return Serialization.Deserialize<DatabaseVersion>(result.Contents);            }            var settings = new GlobalSettings() { Section = nameof(DatabaseVersion), Key = "" };            var dbVersion = new DatabaseVersion() { Version = "6.30b" };            settings.Contents = Serialization.Serialize(dbVersion);            DbFactory.Provider.Save(settings);            return dbVersion;        }        private static VersionNumber GetDatabaseVersion()        {            var dbVersion = GetVersionSettings();            return VersionNumber.Parse(dbVersion.Version);        }        private static void UpdateVersionNumber(VersionNumber version)        {            if (version.IsDevelopmentVersion)            {                return;            }            var dbVersion = GetVersionSettings();            dbVersion.Version = version.ToString();            var result = DbFactory.Provider.Query(new Filter<GlobalSettings>(x => x.Section).IsEqualTo(nameof(DatabaseVersion)))                .Rows.FirstOrDefault()?.ToObject<GlobalSettings>() ?? new GlobalSettings() { Section = nameof(DatabaseVersion), Key = "" };            result.OriginalValues["Contents"] = result.Contents;            result.Contents = Serialization.Serialize(dbVersion);            DbFactory.Provider.Save(result);        }        /// <summary>        /// Migrates the database to the current version.        /// </summary>        /// <returns><c>false</c> if the migration fails.</returns>        public static bool MigrateDatabase()        {            try            {                var from = GetDatabaseVersion();                var to = VersionNumber.Parse(CoreUtils.GetVersion());                var success = MigrateDatabase(from, to, out var newVersion);                if (newVersion != from)                {                    UpdateVersionNumber(newVersion);                }                return success;            }            catch(Exception e)            {                Logger.Send(LogType.Error, "", $"Error while migrating database: {CoreUtils.FormatException(e)}");                return false;            }        }    }}
 |