From 32d6e109df913c0ef883e01db655ba2663837dd9 Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Tue, 28 Mar 2023 21:30:29 -0500 Subject: [PATCH] Added uploading of saves --- .gitignore | 1 + .../LANCommanderClient.cs | 23 ++++ .../LANCommanderLibraryPlugin.cs | 94 +++++++++++++- LANCommander.SDK/Enums/SavePathType.cs | 12 ++ LANCommander.SDK/Models/GameManifest.cs | 11 +- LANCommander.SDK/Models/GameSave.cs | 14 +++ LANCommander.SDK/Models/SavePath.cs | 14 +++ LANCommander/Components/SavePathEditor.razor | 1 + .../Controllers/Api/SavesController.cs | 115 ++++++++++++++++++ LANCommander/Data/Enums/SavePathType.cs | 8 -- LANCommander/Data/Models/GameSave.cs | 5 + LANCommander/Data/Models/SavePath.cs | 1 + LANCommander/Data/Models/User.cs | 5 + LANCommander/Pages/Settings/Users.razor | 4 +- LANCommander/Program.cs | 5 +- LANCommander/Services/GameService.cs | 10 ++ 16 files changed, 309 insertions(+), 14 deletions(-) create mode 100644 LANCommander.SDK/Enums/SavePathType.cs create mode 100644 LANCommander.SDK/Models/GameSave.cs create mode 100644 LANCommander.SDK/Models/SavePath.cs create mode 100644 LANCommander/Controllers/Api/SavesController.cs delete mode 100644 LANCommander/Data/Enums/SavePathType.cs diff --git a/.gitignore b/.gitignore index b02cbd6..a11f30a 100644 --- a/.gitignore +++ b/.gitignore @@ -351,3 +351,4 @@ MigrationBackup/ Upload/ LANCommander/Icon/ LANCommander/Settings.yml +LANCommander/Saves/ diff --git a/LANCommander.Playnite.Extension/LANCommanderClient.cs b/LANCommander.Playnite.Extension/LANCommanderClient.cs index e1efb6b..787148e 100644 --- a/LANCommander.Playnite.Extension/LANCommanderClient.cs +++ b/LANCommander.Playnite.Extension/LANCommanderClient.cs @@ -12,6 +12,7 @@ using System.Net.NetworkInformation; using System.Text; using System.Threading.Tasks; using System.Windows.Media.Converters; +using YamlDotNet.Core; namespace LANCommander.PlaynitePlugin { @@ -170,6 +171,28 @@ namespace LANCommander.PlaynitePlugin return DownloadRequest($"/api/Archives/Download/{id}", progressHandler, completeHandler); } + public string DownloadSave(Guid id, Action progressHandler, Action completeHandler) + { + return DownloadRequest($"/api/Saves/Download/{id}", progressHandler, completeHandler); + } + + public string DownloadLatestSave(Guid gameId, Action progressHandler, Action completeHandler) + { + return DownloadRequest($"/api/Saves/DownloadLatest/{gameId}", progressHandler, completeHandler); + } + + public GameSave UploadSave(string gameId, byte[] data) + { + var request = new RestRequest($"/api/Saves/Upload/{gameId}", Method.POST) + .AddHeader("Authorization", $"Bearer {Token.AccessToken}"); + + request.AddFile(gameId, data, gameId); + + var response = Client.Post(request); + + return response.Data; + } + public string GetKey(Guid id) { var macAddress = GetMacAddress(); diff --git a/LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs b/LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs index cc3bb0c..e1eae71 100644 --- a/LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs +++ b/LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs @@ -1,4 +1,5 @@ -using LANCommander.PlaynitePlugin.Extensions; +using ICSharpCode.SharpZipLib.Zip; +using LANCommander.PlaynitePlugin.Extensions; using LANCommander.SDK; using Playnite.SDK; using Playnite.SDK.Events; @@ -175,6 +176,7 @@ namespace LANCommander.PlaynitePlugin { var nameChangeScriptPath = PowerShellRuntime.GetScriptFilePath(args.Games.First(), SDK.Enums.ScriptType.NameChange); var keyChangeScriptPath = PowerShellRuntime.GetScriptFilePath(args.Games.First(), SDK.Enums.ScriptType.KeyChange); + var installScriptPath = PowerShellRuntime.GetScriptFilePath(args.Games.First(), SDK.Enums.ScriptType.Install); if (File.Exists(nameChangeScriptPath)) yield return new GameMenuItem @@ -215,6 +217,25 @@ namespace LANCommander.PlaynitePlugin } } }; + + if (File.Exists(installScriptPath)) + yield return new GameMenuItem + { + Description = "Run Install Script", + Action = (installArgs) => + { + Guid gameId; + + if (Guid.TryParse(installArgs.Games.First().GameId, out gameId)) + { + PowerShellRuntime.RunScript(installArgs.Games.First(), SDK.Enums.ScriptType.Install); + } + else + { + PlayniteApi.Dialogs.ShowErrorMessage("This game could not be found on the server. Your game may be corrupted."); + } + } + }; } } @@ -246,6 +267,77 @@ namespace LANCommander.PlaynitePlugin }; } + public override void OnGameStopped(OnGameStoppedEventArgs args) + { + var manifestPath = Path.Combine(args.Game.InstallDirectory, "_manifest.yml"); + + if (File.Exists(manifestPath)) + { + var deserializer = new DeserializerBuilder() + .WithNamingConvention(new PascalCaseNamingConvention()) + .Build(); + + var manifest = deserializer.Deserialize(File.ReadAllText(manifestPath)); + var temp = Path.GetTempFileName(); + + using (ZipOutputStream zipStream = new ZipOutputStream(File.Create(temp))) + { + zipStream.SetLevel(5); + + foreach (var savePath in manifest.SavePaths) + { + savePath.Path = savePath.Path.Replace('/', '\\').Replace("{InstallDir}", args.Game.InstallDirectory); + + if (Directory.Exists(savePath.Path)) + { + AddDirectoryToZip(zipStream, savePath.Path); + } + else if (File.Exists(savePath.Path)) + { + var entry = new ZipEntry(Path.Combine(savePath.Id.ToString(), Path.GetFileName(savePath.Path))); + + zipStream.PutNextEntry(entry); + + byte[] buffer = File.ReadAllBytes(savePath.Path); + + zipStream.Write(buffer, 0, buffer.Length); + zipStream.CloseEntry(); + } + } + } + + var save = LANCommander.UploadSave(args.Game.GameId, File.ReadAllBytes(temp)); + + File.Delete(temp); + } + } + + private void AddDirectoryToZip(ZipOutputStream zipStream, string path) + { + foreach (var file in Directory.GetFiles(path)) + { + var entry = new ZipEntry(Path.GetFileName(file)); + + zipStream.PutNextEntry(entry); + + byte[] buffer = File.ReadAllBytes(file); + + zipStream.Write(buffer, 0, buffer.Length); + + zipStream.CloseEntry(); + } + + foreach (var child in Directory.GetDirectories(path)) + { + ZipEntry entry = new ZipEntry(Path.GetFileName(path)); + + zipStream.PutNextEntry(entry); + zipStream.CloseEntry(); + + AddDirectoryToZip(zipStream, child); + } + } + public override IEnumerable GetTopPanelItems() { yield return new TopPanelItem diff --git a/LANCommander.SDK/Enums/SavePathType.cs b/LANCommander.SDK/Enums/SavePathType.cs new file mode 100644 index 0000000..6918179 --- /dev/null +++ b/LANCommander.SDK/Enums/SavePathType.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LANCommander.SDK.Enums +{ + public enum SavePathType + { + File, + Registry + } +} diff --git a/LANCommander.SDK/Models/GameManifest.cs b/LANCommander.SDK/Models/GameManifest.cs index ab214bc..5ccd46d 100644 --- a/LANCommander.SDK/Models/GameManifest.cs +++ b/LANCommander.SDK/Models/GameManifest.cs @@ -1,4 +1,5 @@ -using System; +using LANCommander.SDK.Enums; +using System; using System.Collections.Generic; namespace LANCommander.SDK @@ -20,6 +21,7 @@ namespace LANCommander.SDK public MultiplayerInfo LocalMultiplayer { get; set; } public MultiplayerInfo LanMultiplayer { get; set; } public MultiplayerInfo OnlineMultiplayer { get; set; } + public IEnumerable SavePaths { get; set; } public GameManifest() { } } @@ -39,4 +41,11 @@ namespace LANCommander.SDK public int MinPlayers { get; set; } public int MaxPlayers { get; set; } } + + public class SavePath + { + public Guid Id { get; set; } + public string Type { get; set; } + public string Path { get; set; } + } } diff --git a/LANCommander.SDK/Models/GameSave.cs b/LANCommander.SDK/Models/GameSave.cs new file mode 100644 index 0000000..1710086 --- /dev/null +++ b/LANCommander.SDK/Models/GameSave.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LANCommander.SDK.Models +{ + public class GameSave : BaseModel + { + public Guid GameId { get; set; } + public virtual Game Game { get; set; } + public Guid UserId { get; set; } + public virtual User User { get; set; } + } +} diff --git a/LANCommander.SDK/Models/SavePath.cs b/LANCommander.SDK/Models/SavePath.cs new file mode 100644 index 0000000..bb34a6a --- /dev/null +++ b/LANCommander.SDK/Models/SavePath.cs @@ -0,0 +1,14 @@ +using LANCommander.SDK.Enums; +using System; +using System.Collections.Generic; +using System.Text; + +namespace LANCommander.SDK.Models +{ + public class SavePath : BaseModel + { + public SavePathType Type { get; set; } + public string Path { get; set; } + public virtual Game Game { get; set; } + } +} diff --git a/LANCommander/Components/SavePathEditor.razor b/LANCommander/Components/SavePathEditor.razor index deceaa5..183c918 100644 --- a/LANCommander/Components/SavePathEditor.razor +++ b/LANCommander/Components/SavePathEditor.razor @@ -1,4 +1,5 @@ @using LANCommander.Data.Enums +@using LANCommander.SDK.Enums; diff --git a/LANCommander/Controllers/Api/SavesController.cs b/LANCommander/Controllers/Api/SavesController.cs new file mode 100644 index 0000000..362e3fc --- /dev/null +++ b/LANCommander/Controllers/Api/SavesController.cs @@ -0,0 +1,115 @@ +using LANCommander.Data; +using LANCommander.Data.Models; +using LANCommander.Extensions; +using LANCommander.Models; +using LANCommander.SDK; +using LANCommander.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Runtime.Intrinsics.X86; + +namespace LANCommander.Controllers.Api +{ + [Authorize(AuthenticationSchemes = "Bearer")] + [Route("api/[controller]")] + [ApiController] + public class SavesController : ControllerBase + { + private readonly GameService GameService; + private readonly GameSaveService GameSaveService; + private readonly UserManager UserManager; + + public SavesController(GameService gameService, GameSaveService gameSaveService, UserManager userManager) + { + GameService = gameService; + GameSaveService = gameSaveService; + UserManager = userManager; + } + + [HttpGet] + public IEnumerable Get() + { + return GameSaveService.Get(); + } + + [HttpGet("{id}")] + public async Task Get(Guid id) + { + return await GameSaveService.Get(id); + } + + [HttpGet("DownloadLatest/{gameId}")] + public async Task DownloadLatest(Guid gameId) + { + var user = await UserManager.FindByNameAsync(User.Identity.Name); + + if (user == null) + return NotFound(); + + var save = await GameSaveService + .Get(gs => gs.GameId == gameId && gs.UserId == user.Id) + .OrderByDescending(gs => gs.CreatedOn) + .FirstOrDefaultAsync(); + + if (save == null) + return NotFound(); + + var filename = save.GetUploadPath(); + + if (!System.IO.File.Exists(filename)) + return NotFound(); + + return File(new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read), "application/octet-stream", $"{save.Id.ToString().SanitizeFilename()}.zip"); + } + + [HttpGet("Download/{id}")] + public async Task Download(Guid id) + { + var save = await GameSaveService.Get(id); + + if (save == null) + return NotFound(); + + var filename = save.GetUploadPath(); + + if (!System.IO.File.Exists(filename)) + return NotFound(); + + return File(new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read), "application/octet-stream", $"{save.Id.ToString().SanitizeFilename()}.zip"); + } + + [HttpPost("Upload/{id}")] + public async Task Upload(Guid id) + { + var file = Request.Form.Files.First(); + + var user = await UserManager.FindByNameAsync(User.Identity.Name); + var game = await GameService.Get(id); + + if (game == null) + return NotFound(); + + var save = new GameSave() + { + GameId = id, + UserId = user.Id + }; + + save = await GameSaveService.Add(save); + + var saveUploadPath = Path.GetDirectoryName(save.GetUploadPath()); + + if (!Directory.Exists(saveUploadPath)) + Directory.CreateDirectory(saveUploadPath); + + using (var stream = System.IO.File.Create(save.GetUploadPath())) + { + await file.CopyToAsync(stream); + } + + return Ok(save); + } + } +} diff --git a/LANCommander/Data/Enums/SavePathType.cs b/LANCommander/Data/Enums/SavePathType.cs deleted file mode 100644 index 66502e1..0000000 --- a/LANCommander/Data/Enums/SavePathType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace LANCommander.Data.Enums -{ - public enum SavePathType - { - File, - Registry - } -} diff --git a/LANCommander/Data/Models/GameSave.cs b/LANCommander/Data/Models/GameSave.cs index 006a301..1e232d3 100644 --- a/LANCommander/Data/Models/GameSave.cs +++ b/LANCommander/Data/Models/GameSave.cs @@ -16,5 +16,10 @@ namespace LANCommander.Data.Models [ForeignKey(nameof(UserId))] [InverseProperty("GameSaves")] public virtual User? User { get; set; } + + public string GetUploadPath() + { + return Path.Combine("Saves", UserId.ToString(), GameId.ToString(), Id.ToString()); + } } } diff --git a/LANCommander/Data/Models/SavePath.cs b/LANCommander/Data/Models/SavePath.cs index 23342f0..8a77163 100644 --- a/LANCommander/Data/Models/SavePath.cs +++ b/LANCommander/Data/Models/SavePath.cs @@ -1,4 +1,5 @@ using LANCommander.Data.Enums; +using LANCommander.SDK.Enums; using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; diff --git a/LANCommander/Data/Models/User.cs b/LANCommander/Data/Models/User.cs index 9e1b21a..536c634 100644 --- a/LANCommander/Data/Models/User.cs +++ b/LANCommander/Data/Models/User.cs @@ -44,5 +44,10 @@ namespace LANCommander.Data.Models [JsonIgnore] public virtual ICollection? GameSaves { get; set; } + + public string GetGameSaveUploadPath() + { + return Path.Combine("Saves", Id.ToString()); + } } } diff --git a/LANCommander/Pages/Settings/Users.razor b/LANCommander/Pages/Settings/Users.razor index 507db07..2864c68 100644 --- a/LANCommander/Pages/Settings/Users.razor +++ b/LANCommander/Pages/Settings/Users.razor @@ -55,12 +55,12 @@ foreach (var user in UserManager.Users) { - var savePath = Path.Combine("Save", user.Id.ToString()); + var savePath = user.GetGameSaveUploadPath(); long saveSize = 0; if (Directory.Exists(savePath)) - saveSize = new DirectoryInfo(savePath).EnumerateFiles().Sum(f => f.Length); + saveSize = new DirectoryInfo(savePath).EnumerateFiles("*", SearchOption.AllDirectories).Sum(f => f.Length); UserList.Add(new UserViewModel() { diff --git a/LANCommander/Program.cs b/LANCommander/Program.cs index ebdf1b5..f7a0c98 100644 --- a/LANCommander/Program.cs +++ b/LANCommander/Program.cs @@ -114,6 +114,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddSingleton(); @@ -161,8 +162,8 @@ if (!Directory.Exists("Upload")) if (!Directory.Exists("Icon")) Directory.CreateDirectory("Icon"); -if (!Directory.Exists("Save")) - Directory.CreateDirectory("Save"); +if (!Directory.Exists("Saves")) + Directory.CreateDirectory("Saves"); if (!Directory.Exists("Snippets")) Directory.CreateDirectory("Snippets"); diff --git a/LANCommander/Services/GameService.cs b/LANCommander/Services/GameService.cs index fbf33d2..32118bb 100644 --- a/LANCommander/Services/GameService.cs +++ b/LANCommander/Services/GameService.cs @@ -99,6 +99,16 @@ namespace LANCommander.Services }; } + if (game.SavePaths != null && game.SavePaths.Count > 0) + { + manifest.SavePaths = game.SavePaths.Select(p => new SDK.SavePath() + { + Id = p.Id, + Path = p.Path, + Type = p.Type.ToString() + }); + } + return manifest; }