You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

772 lines
31 KiB

6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Diagnostics;
  5. using System.Diagnostics.CodeAnalysis;
  6. using System.IO;
  7. using System.Linq;
  8. using System.Runtime.Serialization;
  9. using System.Security.Cryptography;
  10. using System.Threading;
  11. using System.Threading.Tasks;
  12. using Ionic.Zip;
  13. using IPA.Config;
  14. using IPA.Loader;
  15. using IPA.Loader.Features;
  16. using IPA.Utilities;
  17. using Newtonsoft.Json;
  18. using SemVer;
  19. using UnityEngine;
  20. using UnityEngine.Networking;
  21. using static IPA.Loader.PluginManager;
  22. using Logger = IPA.Logging.Logger;
  23. using Version = SemVer.Version;
  24. namespace IPA.Updating.BeatMods
  25. {
  26. [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
  27. internal class Updater : MonoBehaviour
  28. {
  29. public static Updater Instance;
  30. internal static bool ModListPresent = false;
  31. public void Awake()
  32. {
  33. try
  34. {
  35. if (Instance != null)
  36. Destroy(this);
  37. else
  38. {
  39. Instance = this;
  40. DontDestroyOnLoad(this);
  41. if (!ModListPresent && SelfConfig.SelfConfigRef.Value.Updates.AutoCheckUpdates)
  42. CheckForUpdates();
  43. }
  44. }
  45. catch (Exception e)
  46. {
  47. Logger.updater.Error(e);
  48. }
  49. }
  50. internal delegate void CheckUpdatesComplete(List<DependencyObject> toUpdate);
  51. public void CheckForUpdates(CheckUpdatesComplete onComplete = null) => StartCoroutine(CheckForUpdatesCoroutine(onComplete));
  52. internal class DependencyObject
  53. {
  54. public string Name { get; set; }
  55. public Version Version { get; set; }
  56. public Version ResolvedVersion { get; set; }
  57. public Range Requirement { get; set; }
  58. public Range Conflicts { get; set; } // a range of versions that are not allowed to be downloaded
  59. public bool Resolved { get; set; }
  60. public bool Has { get; set; }
  61. public HashSet<string> Consumers { get; set; } = new HashSet<string>();
  62. public bool MetaRequestFailed { get; set; }
  63. public PluginLoader.PluginInfo LocalPluginMeta { get; set; }
  64. public bool IsLegacy { get; set; } = false;
  65. public override string ToString()
  66. {
  67. return $"{Name}@{Version}{(Resolved ? $" -> {ResolvedVersion}" : "")} - ({Requirement} ! {Conflicts}) {(Has ? " Already have" : "")}";
  68. }
  69. }
  70. public static void ResetRequestCache()
  71. {
  72. requestCache.Clear();
  73. modCache.Clear();
  74. modVersionsCache.Clear();
  75. }
  76. private static readonly Dictionary<string, string> requestCache = new Dictionary<string, string>();
  77. private static IEnumerator GetBeatModsEndpoint(string url, Ref<string> result)
  78. {
  79. if (requestCache.TryGetValue(url, out string value))
  80. {
  81. result.Value = value;
  82. }
  83. else
  84. {
  85. using (var request = UnityWebRequest.Get(ApiEndpoint.ApiBase + url))
  86. {
  87. yield return request.SendWebRequest();
  88. if (request.isNetworkError)
  89. {
  90. result.Error = new NetworkException($"Network error while trying to download: {request.error}");
  91. yield break;
  92. }
  93. if (request.isHttpError)
  94. {
  95. if (request.responseCode == 404)
  96. {
  97. result.Error = new NetworkException("Not found");
  98. yield break;
  99. }
  100. result.Error = new NetworkException($"Server returned error {request.error} while getting data");
  101. yield break;
  102. }
  103. result.Value = request.downloadHandler.text;
  104. requestCache[url] = result.Value;
  105. }
  106. }
  107. }
  108. private static readonly Dictionary<string, ApiEndpoint.Mod> modCache = new Dictionary<string, ApiEndpoint.Mod>();
  109. internal static IEnumerator GetModInfo(string modName, string ver, Ref<ApiEndpoint.Mod> result)
  110. {
  111. var uri = string.Format(ApiEndpoint.GetModInfoEndpoint, Uri.EscapeDataString(modName), Uri.EscapeDataString(ver));
  112. if (modCache.TryGetValue(uri, out ApiEndpoint.Mod value))
  113. {
  114. result.Value = value;
  115. }
  116. else
  117. {
  118. Ref<string> reqResult = new Ref<string>("");
  119. yield return GetBeatModsEndpoint(uri, reqResult);
  120. try
  121. {
  122. result.Value = JsonConvert.DeserializeObject<List<ApiEndpoint.Mod>>(reqResult.Value).First();
  123. modCache[uri] = result.Value;
  124. }
  125. catch (Exception e)
  126. {
  127. result.Error = new Exception("Error decoding response", e);
  128. }
  129. }
  130. }
  131. private static readonly Dictionary<string, List<ApiEndpoint.Mod>> modVersionsCache = new Dictionary<string, List<ApiEndpoint.Mod>>();
  132. internal static IEnumerator GetModVersionsMatching(string modName, Range range, Ref<List<ApiEndpoint.Mod>> result)
  133. {
  134. var uri = string.Format(ApiEndpoint.GetModsByName, Uri.EscapeDataString(modName));
  135. if (modVersionsCache.TryGetValue(uri, out List<ApiEndpoint.Mod> value))
  136. {
  137. result.Value = value;
  138. }
  139. else
  140. {
  141. Ref<string> reqResult = new Ref<string>("");
  142. yield return GetBeatModsEndpoint(uri, reqResult);
  143. try
  144. {
  145. result.Value = JsonConvert.DeserializeObject<List<ApiEndpoint.Mod>>(reqResult.Value)
  146. .Where(m => range.IsSatisfied(m.Version)).ToList();
  147. modVersionsCache[uri] = result.Value;
  148. }
  149. catch (Exception e)
  150. {
  151. result.Error = new Exception("Error decoding response", e);
  152. }
  153. }
  154. }
  155. internal IEnumerator CheckForUpdatesCoroutine(CheckUpdatesComplete onComplete)
  156. {
  157. var depList = new Ref<List<DependencyObject>>(new List<DependencyObject>());
  158. foreach (var plugin in BSMetas)
  159. { // initialize with data to resolve (1.1)
  160. if (plugin.Metadata.Id != null)
  161. { // updatable
  162. var msinfo = plugin.Metadata;
  163. var dep = new DependencyObject
  164. {
  165. Name = msinfo.Id,
  166. Version = msinfo.Version,
  167. Requirement = new Range($">={msinfo.Version}"),
  168. LocalPluginMeta = plugin
  169. };
  170. if (msinfo.Features.FirstOrDefault(f => f is NoUpdateFeature) != null)
  171. { // disable updating, by only matching self, so that dependencies can still be resolved
  172. dep.Requirement = new Range(msinfo.Version.ToString());
  173. }
  174. depList.Value.Add(dep);
  175. }
  176. }
  177. foreach (var meta in PluginLoader.ignoredPlugins)
  178. { // update ignored
  179. if (meta.Id != null)
  180. { // updatable
  181. var dep = new DependencyObject
  182. {
  183. Name = meta.Id,
  184. Version = meta.Version,
  185. Requirement = new Range($">={meta.Version}"),
  186. LocalPluginMeta = new PluginLoader.PluginInfo
  187. {
  188. Metadata = meta,
  189. Plugin = null
  190. }
  191. };
  192. if (meta.Features.FirstOrDefault(f => f is NoUpdateFeature) != null)
  193. { // disable updating, by only matching self
  194. dep.Requirement = new Range(meta.Version.ToString());
  195. }
  196. depList.Value.Add(dep);
  197. }
  198. }
  199. foreach (var meta in DisabledPlugins)
  200. { // update ignored
  201. if (meta.Id != null)
  202. { // updatable
  203. var dep = new DependencyObject
  204. {
  205. Name = meta.Id,
  206. Version = meta.Version,
  207. Requirement = new Range($">={meta.Version}"),
  208. LocalPluginMeta = new PluginLoader.PluginInfo
  209. {
  210. Metadata = meta,
  211. Plugin = null
  212. }
  213. };
  214. if (meta.Features.FirstOrDefault(f => f is NoUpdateFeature) != null)
  215. { // disable updating, by only matching self
  216. dep.Requirement = new Range(meta.Version.ToString());
  217. }
  218. depList.Value.Add(dep);
  219. }
  220. }
  221. #pragma warning disable CS0618 // Type or member is obsolete
  222. foreach (var plug in Plugins)
  223. { // throw these in the updater on the off chance that they are set up properly
  224. try
  225. {
  226. var dep = new DependencyObject
  227. {
  228. Name = plug.Name,
  229. Version = new Version(plug.Version),
  230. Requirement = new Range($">={plug.Version}"),
  231. IsLegacy = true,
  232. LocalPluginMeta = null
  233. };
  234. depList.Value.Add(dep);
  235. }
  236. catch (Exception e)
  237. {
  238. Logger.updater.Warn($"Error trying to add legacy plugin {plug.Name} to updater");
  239. Logger.updater.Warn(e);
  240. }
  241. }
  242. #pragma warning restore CS0618 // Type or member is obsolete
  243. foreach (var dep in depList.Value)
  244. Logger.updater.Debug($"Phantom Dependency: {dep}");
  245. yield return ResolveDependencyRanges(depList);
  246. foreach (var dep in depList.Value)
  247. Logger.updater.Debug($"Dependency: {dep}");
  248. yield return ResolveDependencyPresence(depList);
  249. foreach (var dep in depList.Value)
  250. Logger.updater.Debug($"Dependency: {dep}");
  251. CheckDependencies(depList);
  252. onComplete?.Invoke(depList);
  253. if (!ModListPresent && SelfConfig.SelfConfigRef.Value.Updates.AutoUpdate)
  254. StartDownload(depList.Value);
  255. }
  256. internal IEnumerator ResolveDependencyRanges(Ref<List<DependencyObject>> list)
  257. {
  258. for (int i = 0; i < list.Value.Count; i++)
  259. { // Grab dependencies (1.2)
  260. var dep = list.Value[i];
  261. var mod = new Ref<ApiEndpoint.Mod>(null);
  262. yield return GetModInfo(dep.Name, "", mod);
  263. try { mod.Verify(); }
  264. catch (Exception e)
  265. {
  266. Logger.updater.Error($"Error getting info for {dep.Name}");
  267. if (SelfConfig.SelfConfigRef.Value.Debug.ShowHandledErrorStackTraces)
  268. Logger.updater.Error(e);
  269. dep.MetaRequestFailed = true;
  270. continue;
  271. }
  272. list.Value.AddRange(mod.Value.Dependencies.Select(m => new DependencyObject
  273. {
  274. Name = m.Name,
  275. Requirement = new Range($"^{m.Version}"),
  276. Consumers = new HashSet<string> { dep.Name }
  277. }));
  278. // currently no conflicts exist in BeatMods
  279. //list.Value.AddRange(mod.Value.Links.Dependencies.Select(d => new DependencyObject { Name = d.Name, Requirement = d.VersionRange, Consumers = new HashSet<string> { dep.Name } }));
  280. //list.Value.AddRange(mod.Value.Links.Conflicts.Select(d => new DependencyObject { Name = d.Name, Conflicts = d.VersionRange, Consumers = new HashSet<string> { dep.Name } }));
  281. }
  282. var depNames = new HashSet<string>();
  283. var final = new List<DependencyObject>();
  284. foreach (var dep in list.Value)
  285. { // agregate ranges and the like (1.3)
  286. if (!depNames.Contains(dep.Name))
  287. { // should add it
  288. depNames.Add(dep.Name);
  289. final.Add(dep);
  290. }
  291. else
  292. {
  293. var toMod = final.First(d => d.Name == dep.Name);
  294. if (dep.Requirement != null)
  295. {
  296. toMod.Requirement = toMod.Requirement.Intersect(dep.Requirement);
  297. foreach (var consume in dep.Consumers)
  298. toMod.Consumers.Add(consume);
  299. }
  300. if (dep.Conflicts != null)
  301. {
  302. toMod.Conflicts = toMod.Conflicts == null
  303. ? dep.Conflicts
  304. : new Range($"{toMod.Conflicts} || {dep.Conflicts}");
  305. }
  306. }
  307. }
  308. list.Value = final;
  309. }
  310. internal IEnumerator ResolveDependencyPresence(Ref<List<DependencyObject>> list)
  311. {
  312. foreach(var dep in list.Value)
  313. {
  314. dep.Has = dep.Version != null; // dep.Version is only not null if its already installed
  315. if (dep.MetaRequestFailed)
  316. {
  317. Logger.updater.Warn($"{dep.Name} info request failed, not trying again");
  318. continue;
  319. }
  320. var modsMatching = new Ref<List<ApiEndpoint.Mod>>(null);
  321. yield return GetModVersionsMatching(dep.Name, dep.Requirement, modsMatching);
  322. try { modsMatching.Verify(); }
  323. catch (Exception e)
  324. {
  325. Logger.updater.Error($"Error getting mod list for {dep.Name}");
  326. if (SelfConfig.SelfConfigRef.Value.Debug.ShowHandledErrorStackTraces)
  327. Logger.updater.Error(e);
  328. dep.MetaRequestFailed = true;
  329. continue;
  330. }
  331. var ver = modsMatching.Value
  332. .Where(nullCheck => nullCheck != null) // entry is not null
  333. .Where(versionCheck => versionCheck.GameVersion == BeatSaber.GameVersion) // game version matches
  334. .Where(approvalCheck => approvalCheck.Status == ApiEndpoint.Mod.ApprovedStatus) // version approved
  335. // TODO: fix; it seems wrong somehow
  336. .Where(conflictsCheck => dep.Conflicts == null || !dep.Conflicts.IsSatisfied(conflictsCheck.Version)) // not a conflicting version
  337. .Select(mod => mod.Version).Max(); // (2.1) get the max version
  338. dep.Resolved = ver != null;
  339. if (dep.Resolved) dep.ResolvedVersion = ver; // (2.2)
  340. dep.Has = dep.Resolved && dep.Version == dep.ResolvedVersion;
  341. }
  342. }
  343. internal void CheckDependencies(Ref<List<DependencyObject>> list)
  344. {
  345. var toDl = new List<DependencyObject>();
  346. foreach (var dep in list.Value)
  347. { // figure out which ones need to be downloaded (3.1)
  348. if (dep.Resolved)
  349. {
  350. Logger.updater.Debug($"Resolved: {dep}");
  351. if (!dep.Has)
  352. {
  353. Logger.updater.Debug($"To Download: {dep}");
  354. toDl.Add(dep);
  355. }
  356. }
  357. else if (!dep.Has)
  358. {
  359. if (dep.Version != null && dep.Requirement.IsSatisfied(dep.Version))
  360. Logger.updater.Notice($"Mod {dep.Name} running a newer version than is on BeatMods ({dep.Version})");
  361. else
  362. Logger.updater.Warn($"Could not resolve dependency {dep}");
  363. }
  364. }
  365. Logger.updater.Debug($"To Download {string.Join(", ", toDl.Select(d => $"{d.Name}@{d.ResolvedVersion}"))}");
  366. list.Value = toDl;
  367. }
  368. internal delegate void DownloadStart(DependencyObject obj);
  369. internal delegate void DownloadProgress(DependencyObject obj, long totalBytes, long currentBytes, double progress);
  370. internal delegate void DownloadFailed(DependencyObject obj, string error);
  371. internal delegate void DownloadFinish(DependencyObject obj);
  372. /// <summary>
  373. /// This will still be called even if there was an error. Called after all three download/install attempts, or after a successful installation.
  374. /// ALWAYS called.
  375. /// </summary>
  376. /// <param name="obj"></param>
  377. /// <param name="didError"></param>
  378. internal delegate void InstallFinish(DependencyObject obj, bool didError);
  379. /// <summary>
  380. /// This can be called multiple times
  381. /// </summary>
  382. /// <param name="obj"></param>
  383. /// <param name="error"></param>
  384. internal delegate void InstallFailed(DependencyObject obj, Exception error);
  385. internal void StartDownload(IEnumerable<DependencyObject> download, DownloadStart downloadStart = null,
  386. DownloadProgress downloadProgress = null, DownloadFailed downloadFail = null, DownloadFinish downloadFinish = null,
  387. InstallFailed installFail = null, InstallFinish installFinish = null)
  388. {
  389. foreach (var item in download)
  390. StartCoroutine(UpdateModCoroutine(item, downloadStart, downloadProgress, downloadFail, downloadFinish, installFail, installFinish));
  391. }
  392. private static IEnumerator UpdateModCoroutine(DependencyObject item, DownloadStart downloadStart,
  393. DownloadProgress progress, DownloadFailed dlFail, DownloadFinish finish,
  394. InstallFailed installFail, InstallFinish installFinish)
  395. { // (3.2)
  396. Logger.updater.Debug($"Release: {BeatSaber.ReleaseType}");
  397. var mod = new Ref<ApiEndpoint.Mod>(null);
  398. yield return GetModInfo(item.Name, item.ResolvedVersion.ToString(), mod);
  399. try { mod.Verify(); }
  400. catch (Exception e)
  401. {
  402. Logger.updater.Error($"Error occurred while trying to get information for {item}");
  403. if (SelfConfig.SelfConfigRef.Value.Debug.ShowHandledErrorStackTraces)
  404. Logger.updater.Error(e);
  405. yield break;
  406. }
  407. var releaseName = BeatSaber.ReleaseType == BeatSaber.Release.Steam
  408. ? ApiEndpoint.Mod.DownloadsObject.TypeSteam : ApiEndpoint.Mod.DownloadsObject.TypeOculus;
  409. var platformFile = mod.Value.Downloads.First(f => f.Type == ApiEndpoint.Mod.DownloadsObject.TypeUniversal || f.Type == releaseName);
  410. string url = ApiEndpoint.BeatModBase + platformFile.Path;
  411. Logger.updater.Debug($"URL = {url}");
  412. const int maxTries = 3;
  413. int tries = maxTries;
  414. while (tries > 0)
  415. {
  416. if (tries-- != maxTries)
  417. Logger.updater.Debug("Re-trying download...");
  418. using (var stream = new MemoryStream())
  419. using (var request = UnityWebRequest.Get(url))
  420. using (var taskTokenSource = new CancellationTokenSource())
  421. {
  422. var dlh = new StreamDownloadHandler(stream, (int i1, int i2, double d) => progress?.Invoke(item, i1, i2, d));
  423. request.downloadHandler = dlh;
  424. downloadStart?.Invoke(item);
  425. Logger.updater.Debug("Sending request");
  426. //Logger.updater.Debug(request?.downloadHandler?.ToString() ?? "DLH==NULL");
  427. yield return request.SendWebRequest();
  428. Logger.updater.Debug("Download finished");
  429. if (request.isNetworkError)
  430. {
  431. Logger.updater.Error("Network error while trying to update mod");
  432. Logger.updater.Error(request.error);
  433. dlFail?.Invoke(item, request.error);
  434. taskTokenSource.Cancel();
  435. continue;
  436. }
  437. if (request.isHttpError)
  438. {
  439. Logger.updater.Error("Server returned an error code while trying to update mod");
  440. Logger.updater.Error(request.error);
  441. dlFail?.Invoke(item, request.error);
  442. taskTokenSource.Cancel();
  443. continue;
  444. }
  445. finish?.Invoke(item);
  446. stream.Seek(0, SeekOrigin.Begin); // reset to beginning
  447. var downloadTask = Task.Run(() =>
  448. { // use slightly more multi threaded approach than co-routines
  449. // ReSharper disable once AccessToDisposedClosure
  450. ExtractPluginAsync(stream, item, platformFile);
  451. }, taskTokenSource.Token);
  452. while (!(downloadTask.IsCompleted || downloadTask.IsCanceled || downloadTask.IsFaulted))
  453. yield return null; // pause co-routine until task is done
  454. if (downloadTask.IsFaulted)
  455. {
  456. if (downloadTask.Exception != null && downloadTask.Exception.InnerExceptions.Any(e => e is BeatmodsInterceptException))
  457. { // any exception is an intercept exception
  458. Logger.updater.Error($"BeatMods did not return expected data for {item.Name}");
  459. }
  460. else
  461. Logger.updater.Error($"Error downloading mod {item.Name}");
  462. if (SelfConfig.SelfConfigRef.Value.Debug.ShowHandledErrorStackTraces)
  463. Logger.updater.Error(downloadTask.Exception);
  464. installFail?.Invoke(item, downloadTask.Exception);
  465. continue;
  466. }
  467. break;
  468. }
  469. }
  470. if (tries == 0)
  471. {
  472. Logger.updater.Warn($"Plugin download failed {maxTries} times, not re-trying");
  473. installFinish?.Invoke(item, true);
  474. }
  475. else
  476. {
  477. Logger.updater.Debug("Download complete");
  478. installFinish?.Invoke(item, false);
  479. }
  480. }
  481. internal class StreamDownloadHandler : DownloadHandlerScript
  482. {
  483. internal int length;
  484. internal int cLen;
  485. internal Action<int, int, double> progress;
  486. public MemoryStream Stream { get; set; }
  487. public StreamDownloadHandler(MemoryStream stream, Action<int, int, double> progress = null)
  488. {
  489. Stream = stream;
  490. this.progress = progress;
  491. }
  492. protected override void ReceiveContentLength(int contentLength)
  493. {
  494. Stream.Capacity = length = contentLength;
  495. cLen = 0;
  496. Logger.updater.Debug($"Got content length: {contentLength}");
  497. }
  498. protected override void CompleteContent()
  499. {
  500. Logger.updater.Debug("Download complete");
  501. }
  502. protected override bool ReceiveData(byte[] rData, int dataLength)
  503. {
  504. if (rData == null || rData.Length < 1)
  505. {
  506. Logger.updater.Debug("CustomWebRequest :: ReceiveData - received a null/empty buffer");
  507. return false;
  508. }
  509. cLen += dataLength;
  510. Stream.Write(rData, 0, dataLength);
  511. progress?.Invoke(length, cLen, ((double)cLen) / length);
  512. return true;
  513. }
  514. protected override byte[] GetData() { return null; }
  515. protected override float GetProgress()
  516. {
  517. return 0f;
  518. }
  519. public override string ToString()
  520. {
  521. return $"{base.ToString()} ({Stream})";
  522. }
  523. }
  524. private static void ExtractPluginAsync(MemoryStream stream, DependencyObject item, ApiEndpoint.Mod.DownloadsObject fileInfo)
  525. { // (3.3)
  526. Logger.updater.Debug($"Extracting ZIP file for {item.Name}");
  527. /*var data = stream.GetBuffer();
  528. SHA1 sha = new SHA1CryptoServiceProvider();
  529. var hash = sha.ComputeHash(data);
  530. if (!Utils.UnsafeCompare(hash, fileInfo.Hash))
  531. throw new Exception("The hash for the file doesn't match what is defined");*/
  532. var targetDir = Path.Combine(BeatSaber.InstallPath, "IPA", Path.GetRandomFileName() + "_Pending");
  533. Directory.CreateDirectory(targetDir);
  534. var eventualOutput = Path.Combine(BeatSaber.InstallPath, "IPA", "Pending");
  535. if (!Directory.Exists(eventualOutput))
  536. Directory.CreateDirectory(eventualOutput);
  537. try
  538. {
  539. bool shouldDeleteOldFile = !(item.LocalPluginMeta?.Metadata.IsSelf).Unwrap();
  540. using (var zipFile = ZipFile.Read(stream))
  541. {
  542. Logger.updater.Debug("Streams opened");
  543. foreach (var entry in zipFile)
  544. {
  545. if (entry.IsDirectory)
  546. {
  547. Logger.updater.Debug($"Creating directory {entry.FileName}");
  548. Directory.CreateDirectory(Path.Combine(targetDir, entry.FileName));
  549. }
  550. else
  551. {
  552. using (var ostream = new MemoryStream((int)entry.UncompressedSize))
  553. {
  554. entry.Extract(ostream);
  555. ostream.Seek(0, SeekOrigin.Begin);
  556. var md5 = new MD5CryptoServiceProvider();
  557. var fileHash = md5.ComputeHash(ostream);
  558. try
  559. {
  560. if (!Utils.UnsafeCompare(fileHash, fileInfo.Hashes.Where(h => h.File == entry.FileName).Select(h => h.Hash).First()))
  561. throw new Exception("The hash for the file doesn't match what is defined");
  562. }
  563. catch (KeyNotFoundException)
  564. {
  565. throw new BeatmodsInterceptException("BeatMods did not send the hashes for the zip's content!");
  566. }
  567. ostream.Seek(0, SeekOrigin.Begin);
  568. FileInfo targetFile = new FileInfo(Path.Combine(targetDir, entry.FileName));
  569. Directory.CreateDirectory(targetFile.DirectoryName ?? throw new InvalidOperationException());
  570. if (item.LocalPluginMeta != null &&
  571. Utils.GetRelativePath(targetFile.FullName, targetDir) == Utils.GetRelativePath(item.LocalPluginMeta?.Metadata.File.FullName, BeatSaber.InstallPath))
  572. shouldDeleteOldFile = false; // overwriting old file, no need to delete
  573. /*if (targetFile.Exists)
  574. backup.Add(targetFile);
  575. else
  576. newFiles.Add(targetFile);*/
  577. Logger.updater.Debug($"Extracting file {targetFile.FullName}");
  578. targetFile.Delete();
  579. using (var fstream = targetFile.Create())
  580. ostream.CopyTo(fstream);
  581. }
  582. }
  583. }
  584. }
  585. if (shouldDeleteOldFile && item.LocalPluginMeta != null)
  586. File.AppendAllLines(Path.Combine(targetDir, SpecialDeletionsFile), new[] { Utils.GetRelativePath(item.LocalPluginMeta?.Metadata.File.FullName, BeatSaber.InstallPath) });
  587. }
  588. catch (Exception)
  589. { // something failed; restore
  590. Directory.Delete(targetDir, true); // delete extraction site
  591. throw;
  592. }
  593. if ((item.LocalPluginMeta?.Metadata.IsSelf).Unwrap())
  594. { // currently updating self, so copy to working dir and update
  595. NeedsManualRestart = true; // flag so that ModList keeps the restart button hidden
  596. Utils.CopyAll(new DirectoryInfo(targetDir), new DirectoryInfo(BeatSaber.InstallPath));
  597. var deleteFile = Path.Combine(BeatSaber.InstallPath, SpecialDeletionsFile);
  598. if (File.Exists(deleteFile)) File.Delete(deleteFile);
  599. Process.Start(new ProcessStartInfo
  600. {
  601. // will never actually be null
  602. FileName = item.LocalPluginMeta?.Metadata.File.FullName ?? throw new InvalidOperationException(),
  603. Arguments = $"-nw={Process.GetCurrentProcess().Id}",
  604. UseShellExecute = false
  605. });
  606. }
  607. else
  608. Utils.CopyAll(new DirectoryInfo(targetDir), new DirectoryInfo(eventualOutput), SpecialDeletionsFile);
  609. Directory.Delete(targetDir, true); // delete extraction site
  610. Logger.updater.Debug("Extractor exited");
  611. }
  612. internal static bool NeedsManualRestart = false;
  613. internal const string SpecialDeletionsFile = "$$delete";
  614. }
  615. [Serializable]
  616. internal class NetworkException : Exception
  617. {
  618. public NetworkException()
  619. {
  620. }
  621. public NetworkException(string message) : base(message)
  622. {
  623. }
  624. public NetworkException(string message, Exception innerException) : base(message, innerException)
  625. {
  626. }
  627. protected NetworkException(SerializationInfo info, StreamingContext context) : base(info, context)
  628. {
  629. }
  630. }
  631. [Serializable]
  632. internal class BeatmodsInterceptException : Exception
  633. {
  634. public BeatmodsInterceptException()
  635. {
  636. }
  637. public BeatmodsInterceptException(string message) : base(message)
  638. {
  639. }
  640. public BeatmodsInterceptException(string message, Exception innerException) : base(message, innerException)
  641. {
  642. }
  643. protected BeatmodsInterceptException(SerializationInfo info, StreamingContext context) : base(info, context)
  644. {
  645. }
  646. }
  647. }