DataUpdater.cs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. using InABox.Configuration;
  2. using InABox.Core;
  3. using Microsoft.CodeAnalysis.Scripting;
  4. using System;
  5. using System.Collections.Generic;
  6. using System.Diagnostics.CodeAnalysis;
  7. using System.Linq;
  8. using System.Text;
  9. using System.Text.RegularExpressions;
  10. using System.Threading.Tasks;
  11. namespace InABox.Database
  12. {
  13. public class DatabaseVersion : BaseObject, IGlobalConfigurationSettings
  14. {
  15. public string Version { get; set; }
  16. public DatabaseVersion()
  17. {
  18. Version = "0.00";
  19. }
  20. }
  21. public class VersionNumber
  22. {
  23. public int MajorVersion { get; set; }
  24. public int MinorVersion { get; set; }
  25. public string Release { get; set; }
  26. public bool IsDevelopmentVersion { get; set; }
  27. public VersionNumber(int majorVersion, int minorVersion, string release = "", bool isDevelopmentVersion = false)
  28. {
  29. MajorVersion = majorVersion;
  30. MinorVersion = minorVersion;
  31. Release = release;
  32. IsDevelopmentVersion = isDevelopmentVersion;
  33. }
  34. private static Regex _format = new(@"^(\d+)\.(\d+)([a-zA-Z]*)$");
  35. public static VersionNumber Parse(string versionStr)
  36. {
  37. if(versionStr == "???")
  38. {
  39. return new(0, 0, "", true);
  40. }
  41. var match = _format.Match(versionStr);
  42. if (!match.Success)
  43. throw new FormatException($"'{versionStr}' is not a valid version!");
  44. return new(int.Parse(match.Groups[1].Value), int.Parse(match.Groups[2].Value), match.Groups[3].Value, false);
  45. }
  46. public static bool TryParse(string versionStr, [NotNullWhen(true)] out VersionNumber? version)
  47. {
  48. if (versionStr == "???")
  49. {
  50. version = new(0, 0, "", true);
  51. return true;
  52. }
  53. var match = _format.Match(versionStr);
  54. if (!match.Success)
  55. {
  56. version = null;
  57. return false;
  58. }
  59. version = new(int.Parse(match.Groups[1].Value), int.Parse(match.Groups[2].Value), match.Groups[3].Value, false);
  60. return true;
  61. }
  62. public static bool operator <(VersionNumber a, VersionNumber b)
  63. {
  64. if (a.IsDevelopmentVersion)
  65. {
  66. return false;
  67. }
  68. else if (b.IsDevelopmentVersion)
  69. {
  70. return true;
  71. }
  72. return a.MajorVersion < b.MajorVersion ||
  73. (a.MajorVersion == b.MajorVersion &&
  74. (a.MinorVersion < b.MinorVersion ||
  75. (a.MinorVersion == b.MinorVersion && string.Compare(a.Release, b.Release, StringComparison.Ordinal) < 0)));
  76. }
  77. public static bool operator >(VersionNumber a, VersionNumber b)
  78. {
  79. return b < a;
  80. }
  81. public static bool operator <=(VersionNumber a, VersionNumber b)
  82. {
  83. return !(b < a);
  84. }
  85. public static bool operator >=(VersionNumber a, VersionNumber b)
  86. {
  87. return !(a < b);
  88. }
  89. public override bool Equals(object? obj)
  90. {
  91. if(obj is VersionNumber v)
  92. {
  93. return this == v;
  94. }
  95. return false;
  96. }
  97. public override int GetHashCode()
  98. {
  99. if (IsDevelopmentVersion)
  100. return 0;
  101. return MajorVersion ^ MinorVersion ^ Release.GetHashCode();
  102. }
  103. public static bool operator ==(VersionNumber a, VersionNumber b)
  104. {
  105. if (a.IsDevelopmentVersion)
  106. return b.IsDevelopmentVersion;
  107. if (b.IsDevelopmentVersion)
  108. return false;
  109. return a.MajorVersion == b.MajorVersion && a.MinorVersion == b.MinorVersion && a.Release == b.Release;
  110. }
  111. public static bool operator !=(VersionNumber a, VersionNumber b)
  112. {
  113. if (a.IsDevelopmentVersion)
  114. return !b.IsDevelopmentVersion;
  115. if (b.IsDevelopmentVersion)
  116. return true;
  117. return a.MajorVersion != b.MajorVersion || a.MinorVersion != b.MinorVersion || a.Release != b.Release;
  118. }
  119. public override string ToString()
  120. {
  121. return IsDevelopmentVersion ? "???" : $"{MajorVersion}.{MinorVersion:D2}{Release}";
  122. }
  123. }
  124. public static class DataUpdater
  125. {
  126. private static Dictionary<VersionNumber, List<DatabaseUpdateScript>> updateScripts = new();
  127. /// <summary>
  128. /// Register a migration script to run when updating to this version.
  129. ///
  130. /// <para>The <paramref name="action"/> should probably be repeatable;
  131. /// 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.
  132. /// </para>
  133. /// </summary>
  134. /// <param name="version">The version to update to.</param>
  135. /// <param name="action">The action to be run.</param>
  136. public static void RegisterUpdateScript<TUpdater>()
  137. where TUpdater : DatabaseUpdateScript, new()
  138. {
  139. var updater = new TUpdater();
  140. if(!updateScripts.TryGetValue(updater.Version, out var list))
  141. {
  142. list = new();
  143. updateScripts[updater.Version] = list;
  144. }
  145. list.Add(updater);
  146. }
  147. private static bool MigrateDatabase(VersionNumber fromVersion, VersionNumber toVersion, out VersionNumber newVersion)
  148. {
  149. var versionNumbers = updateScripts.Keys.ToList();
  150. versionNumbers.Sort((x, y) => x == y ? 0 : x < y ? -1 : 1);
  151. newVersion = fromVersion;
  152. int? index = null;
  153. foreach (var (i, number) in versionNumbers.Select((x, i) => new Tuple<int, VersionNumber>(i, x)))
  154. {
  155. if (number > fromVersion)
  156. {
  157. index = i;
  158. break;
  159. }
  160. }
  161. if(index != null && fromVersion < toVersion)
  162. {
  163. Logger.Send(LogType.Information, "", $"Updating database from {fromVersion} to {toVersion}");
  164. for (int i = (int)index; i < versionNumbers.Count; i++)
  165. {
  166. var version = versionNumbers[i];
  167. if (toVersion < version)
  168. {
  169. break;
  170. }
  171. Logger.Send(LogType.Information, "", $"Executing update to {version}");
  172. foreach(var updater in updateScripts[version])
  173. {
  174. if (!updater.Update())
  175. {
  176. Logger.Send(LogType.Error, "", $"Script failed, cancelling migration");
  177. return false;
  178. }
  179. }
  180. newVersion = version;
  181. }
  182. Logger.Send(LogType.Information, "", $"Data migration complete!");
  183. }
  184. newVersion = toVersion;
  185. return true;
  186. }
  187. private static DatabaseVersion GetVersionSettings()
  188. {
  189. var result = DbFactory.Provider.Query(new Filter<GlobalSettings>(x => x.Section).IsEqualTo(nameof(DatabaseVersion)))
  190. .Rows.FirstOrDefault()?.ToObject<GlobalSettings>();
  191. if(result != null)
  192. {
  193. return Serialization.Deserialize<DatabaseVersion>(result.Contents);
  194. }
  195. var settings = new GlobalSettings() { Section = nameof(DatabaseVersion), Key = "" };
  196. var dbVersion = new DatabaseVersion() { Version = "6.30b" };
  197. settings.Contents = Serialization.Serialize(dbVersion);
  198. DbFactory.Provider.Save(settings);
  199. return dbVersion;
  200. }
  201. private static VersionNumber GetDatabaseVersion()
  202. {
  203. var dbVersion = GetVersionSettings();
  204. return VersionNumber.Parse(dbVersion.Version);
  205. }
  206. private static void UpdateVersionNumber(VersionNumber version)
  207. {
  208. if (version.IsDevelopmentVersion)
  209. {
  210. return;
  211. }
  212. var dbVersion = GetVersionSettings();
  213. dbVersion.Version = version.ToString();
  214. var result = DbFactory.Provider.Query(new Filter<GlobalSettings>(x => x.Section).IsEqualTo(nameof(DatabaseVersion)))
  215. .Rows.FirstOrDefault()?.ToObject<GlobalSettings>() ?? new GlobalSettings() { Section = nameof(DatabaseVersion), Key = "" };
  216. result.OriginalValues["Contents"] = result.Contents;
  217. result.Contents = Serialization.Serialize(dbVersion);
  218. DbFactory.Provider.Save(result);
  219. }
  220. /// <summary>
  221. /// Migrates the database to the current version.
  222. /// </summary>
  223. /// <returns><c>false</c> if the migration fails.</returns>
  224. public static bool MigrateDatabase()
  225. {
  226. try
  227. {
  228. var from = GetDatabaseVersion();
  229. var to = VersionNumber.Parse(CoreUtils.GetVersion());
  230. var success = MigrateDatabase(from, to, out var newVersion);
  231. if (newVersion != from)
  232. {
  233. UpdateVersionNumber(newVersion);
  234. }
  235. return success;
  236. }
  237. catch(Exception e)
  238. {
  239. Logger.Send(LogType.Error, "", $"Error while migrating database: {CoreUtils.FormatException(e)}");
  240. return false;
  241. }
  242. }
  243. }
  244. }