diff --git a/LANCommander.Launcher.Models/CommandLineOptions.cs b/LANCommander.Launcher.Models/CommandLineOptions.cs index 9772010a..181c52bb 100644 --- a/LANCommander.Launcher.Models/CommandLineOptions.cs +++ b/LANCommander.Launcher.Models/CommandLineOptions.cs @@ -8,6 +8,12 @@ using System.Threading.Tasks; namespace LANCommander.Launcher.Models { + public enum ImportArchiveType { + Game, + Redistributable, + Server + } + [Verb("RunScript", HelpText = "Run a script for a game")] public class RunScriptCommandLineOptions { @@ -44,8 +50,18 @@ namespace LANCommander.Launcher.Models public Guid GameId { get; set; } } - [Verb("Import", HelpText = "Import library items from the server")] - public class ImportCommandLineOptions { } + [Verb("Sync", HelpText = "Sync library items from the server")] + public class SyncCommandLineOptions { } + + [Verb("Import", HelpText = "Upload and import an archive to the server (Admin Only)")] + public class ImportCommandLineOptions + { + [Option("Path", HelpText = "Path to the archive file", Required = true)] + public string Path { get; set; } + + [Option("Type", HelpText = "The type of archive to import", Required = true)] + public ImportArchiveType Type { get; set; } + } [Verb("Login", HelpText = "Login to a LANCommander server")] public class LoginCommandLineOptions diff --git a/LANCommander.Launcher.Services/CommandLineService.cs b/LANCommander.Launcher.Services/CommandLineService.cs index deb706ec..ce3e2a01 100644 --- a/LANCommander.Launcher.Services/CommandLineService.cs +++ b/LANCommander.Launcher.Services/CommandLineService.cs @@ -36,7 +36,7 @@ namespace LANCommander.Launcher.Services RunScriptCommandLineOptions, InstallCommandLineOptions, UninstallCommandLineOptions, - ImportCommandLineOptions, + SyncCommandLineOptions, LoginCommandLineOptions, LogoutCommandLineOptions, ChangeAliasCommandLineOptions @@ -45,6 +45,7 @@ namespace LANCommander.Launcher.Services await result.WithParsedAsync(RunScript); await result.WithParsedAsync(Install); await result.WithParsedAsync(Uninstall); + await result.WithParsedAsync(Sync); await result.WithParsedAsync(Import); await result.WithParsedAsync(Login); await result.WithParsedAsync(Logout); @@ -120,7 +121,7 @@ namespace LANCommander.Launcher.Services } } - private async Task Import(ImportCommandLineOptions options) + private async Task Sync(SyncCommandLineOptions options) { Console.WriteLine("Importing games from server..."); diff --git a/LANCommander.SDK/Client.cs b/LANCommander.SDK/Client.cs index f0d6a8cb..a03bb12a 100644 --- a/LANCommander.SDK/Client.cs +++ b/LANCommander.SDK/Client.cs @@ -6,6 +6,7 @@ using RestSharp; using RestSharp.Interceptors; using Semver; using System; +using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.IO; @@ -312,6 +313,41 @@ namespace LANCommander.SDK return response; } + internal async Task ChunkedUploadRequestAsync(string fileName, Stream stream, bool ignoreVersion = false) + { + var maxChunkSize = 1024 * 1024 * 50; + var initResponse = await PostRequestAsync("/Upload/Init", ignoreVersion); + + var buffer = new byte[maxChunkSize]; + + while (stream.Position < stream.Length) + { + var chunkRequest = new UploadChunkRequest(); + + chunkRequest.Start = stream.Position; + + if (stream.Position + maxChunkSize > stream.Length) + { + var bytes = stream.Length - stream.Position; + + buffer = new byte[bytes]; + + await stream.ReadAsync(buffer, 0, (int)(stream.Length - stream.Position)); + } + else + await stream.ReadAsync(buffer, 0, maxChunkSize); + + chunkRequest.End = stream.Position; + chunkRequest.Total = stream.Length; + chunkRequest.File = buffer; + chunkRequest.Key = initResponse.Key; + + await PostRequestAsync("/Upload/Chunk", chunkRequest, ignoreVersion); + } + + return initResponse.Key; + } + public async Task AuthenticateAsync(string username, string password, bool ignoreVersion = false) { var request = new RestRequest("/api/Auth", Method.Post); diff --git a/LANCommander.SDK/GameService.cs b/LANCommander.SDK/GameService.cs index 360d88be..af6ec8a3 100644 --- a/LANCommander.SDK/GameService.cs +++ b/LANCommander.SDK/GameService.cs @@ -614,6 +614,17 @@ namespace LANCommander.SDK Reader?.Cancel(); } + public async Task ImportAsync(string archivePath) + { + using (var fs = new FileStream(archivePath, FileMode.Open, FileAccess.Read)) + { + var objectKey = await Client.ChunkedUploadRequestAsync("", fs); + + if (objectKey != Guid.Empty) + await Client.PostRequestAsync($"/api/Games/Import/{objectKey}"); + } + } + public static string GetMetadataDirectoryPath(string installDirectory, Guid gameId) { if (String.IsNullOrWhiteSpace(installDirectory)) diff --git a/LANCommander.SDK/Models/UploadChunkRequest.cs b/LANCommander.SDK/Models/UploadChunkRequest.cs new file mode 100644 index 00000000..b913663e --- /dev/null +++ b/LANCommander.SDK/Models/UploadChunkRequest.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LANCommander.SDK.Models +{ + internal class UploadChunkRequest + { + internal byte[] File { get; set; } + internal long Start { get; set; } + internal long End { get; set; } + internal long Total { get; set; } + internal Guid Key { get; set; } + } +} diff --git a/LANCommander.SDK/Models/UploadInitResponse.cs b/LANCommander.SDK/Models/UploadInitResponse.cs new file mode 100644 index 00000000..b1cb1e00 --- /dev/null +++ b/LANCommander.SDK/Models/UploadInitResponse.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LANCommander.SDK.Models +{ + internal class UploadInitResponse + { + public Guid Key { get; set; } + } +} diff --git a/LANCommander.Server/Controllers/Api/GamesController.cs b/LANCommander.Server/Controllers/Api/GamesController.cs index 6b3bd27c..b0b9e5d4 100644 --- a/LANCommander.Server/Controllers/Api/GamesController.cs +++ b/LANCommander.Server/Controllers/Api/GamesController.cs @@ -110,5 +110,21 @@ namespace LANCommander.Server.Controllers.Api return File(new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read), "application/octet-stream", $"{game.Title.SanitizeFilename()}.zip"); } + + [Authorize(Roles = "Administrator")] + [HttpPost("Import/{objectKey}")] + public async Task Import(Guid objectKey) + { + try + { + var game = await GameService.Import(objectKey); + + return Ok(); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } } }