| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 | using InABox.Configuration;using InABox.Core;using Microsoft.CodeAnalysis.Scripting;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, IGlobalConfigurationSettings{    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; }    public VersionNumber(int majorVersion, int minorVersion, string release = "", bool isDevelopmentVersion = false)    {        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:D2}{Release}";    }}public static class DataUpdater{    private static Dictionary<VersionNumber, List<DatabaseUpdateScript>> 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<TUpdater>()        where TUpdater : DatabaseUpdateScript, new()    {        var updater = new TUpdater();                if(!updateScripts.TryGetValue(updater.Version, out var list))        {            list = new();            updateScripts[updater.Version] = list;        }        list.Add(updater);    }    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 updater in updateScripts[version])                {                    if (!updater.Update())                    {                        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.NewProvider(Logger.Main).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.NewProvider(Logger.Main).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.NewProvider(Logger.Main).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.NewProvider(Logger.Main).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;        }    }        public static bool DoSpecificMigration(VersionNumber from, VersionNumber to)    {        try        {            var _success = MigrateDatabase(from, to, out var _);            return _success;        }        catch(Exception e)        {            Logger.Send(LogType.Error, "", $"Error while migrating database: {CoreUtils.FormatException(e)}");            return false;        }    }}
 |