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.

777 lines
31 KiB

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