DataUpdater.cs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  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, GlobalConfigurationSettings
  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. private VersionNumber(int majorVersion, int minorVersion, string release, bool isDevelopmentVersion)
  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}{Release}";
  122. }
  123. }
  124. public static class DataUpdater
  125. {
  126. private static Dictionary<VersionNumber, List<Func<bool>>> 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(string version, Func<bool> action)
  137. {
  138. var versionNumber = VersionNumber.Parse(version);
  139. if(!updateScripts.TryGetValue(versionNumber, out var list))
  140. {
  141. list = new();
  142. updateScripts[versionNumber] = list;
  143. }
  144. list.Add(action);
  145. }
  146. private static bool MigrateDatabase(VersionNumber fromVersion, VersionNumber toVersion, out VersionNumber newVersion)
  147. {
  148. var versionNumbers = updateScripts.Keys.ToList();
  149. versionNumbers.Sort((x, y) => x == y ? 0 : x < y ? -1 : 1);
  150. newVersion = fromVersion;
  151. int? index = null;
  152. foreach (var (i, number) in versionNumbers.Select((x, i) => new Tuple<int, VersionNumber>(i, x)))
  153. {
  154. if (number > fromVersion)
  155. {
  156. index = i;
  157. break;
  158. }
  159. }
  160. if(index != null && fromVersion < toVersion)
  161. {
  162. Logger.Send(LogType.Information, "", $"Updating database from {fromVersion} to {toVersion}");
  163. for (int i = (int)index; i < versionNumbers.Count; i++)
  164. {
  165. var version = versionNumbers[i];
  166. if (toVersion < version)
  167. {
  168. break;
  169. }
  170. Logger.Send(LogType.Information, "", $"Executing update to {version}");
  171. foreach(var script in updateScripts[version])
  172. {
  173. if (!script())
  174. {
  175. Logger.Send(LogType.Error, "", $"Script failed, cancelling migration");
  176. return false;
  177. }
  178. }
  179. newVersion = version;
  180. }
  181. Logger.Send(LogType.Information, "", $"Data migration complete!");
  182. }
  183. newVersion = toVersion;
  184. return true;
  185. }
  186. private static DatabaseVersion GetVersionSettings()
  187. {
  188. var result = DbFactory.Provider.Query(new Filter<GlobalSettings>(x => x.Section).IsEqualTo(nameof(DatabaseVersion)))
  189. .Rows.FirstOrDefault()?.ToObject<GlobalSettings>();
  190. if(result != null)
  191. {
  192. return Serialization.Deserialize<DatabaseVersion>(result.Contents);
  193. }
  194. var settings = new GlobalSettings() { Section = nameof(DatabaseVersion), Key = "" };
  195. var dbVersion = new DatabaseVersion() { Version = "6.30b" };
  196. settings.Contents = Serialization.Serialize(dbVersion);
  197. DbFactory.Provider.Save(settings);
  198. return dbVersion;
  199. }
  200. private static VersionNumber GetDatabaseVersion()
  201. {
  202. var dbVersion = GetVersionSettings();
  203. return VersionNumber.Parse(dbVersion.Version);
  204. }
  205. private static void UpdateVersionNumber(VersionNumber version)
  206. {
  207. if (version.IsDevelopmentVersion)
  208. {
  209. return;
  210. }
  211. var dbVersion = GetVersionSettings();
  212. dbVersion.Version = version.ToString();
  213. var result = DbFactory.Provider.Query(new Filter<GlobalSettings>(x => x.Section).IsEqualTo(nameof(DatabaseVersion)))
  214. .Rows.FirstOrDefault()?.ToObject<GlobalSettings>() ?? new GlobalSettings() { Section = nameof(DatabaseVersion), Key = "" };
  215. result.OriginalValues["Contents"] = result.Contents;
  216. result.Contents = Serialization.Serialize(dbVersion);
  217. DbFactory.Provider.Save(result);
  218. }
  219. /// <summary>
  220. /// Migrates the database to the current version.
  221. /// </summary>
  222. /// <returns><c>false</c> if the migration fails.</returns>
  223. public static bool MigrateDatabase()
  224. {
  225. try
  226. {
  227. var from = GetDatabaseVersion();
  228. var to = VersionNumber.Parse(CoreUtils.GetVersion());
  229. var success = MigrateDatabase(from, to, out var newVersion);
  230. if (newVersion != from)
  231. {
  232. UpdateVersionNumber(newVersion);
  233. }
  234. return success;
  235. }
  236. catch(Exception e)
  237. {
  238. Logger.Send(LogType.Error, "", $"Error while migrating database: {CoreUtils.FormatException(e)}");
  239. return false;
  240. }
  241. }
  242. }
  243. }