diff --git a/gaseous-server/Classes/Roms.cs b/gaseous-server/Classes/Roms.cs index 81ddfd4..22aceec 100644 --- a/gaseous-server/Classes/Roms.cs +++ b/gaseous-server/Classes/Roms.cs @@ -10,23 +10,29 @@ namespace gaseous_server.Classes public class Roms { public class InvalidRomId : Exception - { - public InvalidRomId(long Id) : base("Unable to find ROM by id " + Id) - {} - } + { + public InvalidRomId(long Id) : base("Unable to find ROM by id " + Id) + { } + } + + public class InvalidRomHash : Exception + { + public InvalidRomHash(String Hash) : base("Unable to find ROM by hash " + Hash) + { } + } public static GameRomObject GetRoms(long GameId, long PlatformId = -1, string NameSearch = "", int pageNumber = 0, int pageSize = 0, string userid = "") { GameRomObject GameRoms = new GameRomObject(); - Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = ""; + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = ""; string sqlCount = ""; string sqlPlatform = ""; Dictionary dbDict = new Dictionary(); - dbDict.Add("id", GameId); + dbDict.Add("id", GameId); dbDict.Add("userid", userid); - + string NameSearchWhere = ""; if (NameSearch.Length > 0) { @@ -37,13 +43,16 @@ namespace gaseous_server.Classes // platform query sqlPlatform = "SELECT DISTINCT Games_Roms.PlatformId, Platform.`Name` FROM Games_Roms LEFT JOIN Platform ON Games_Roms.PlatformId = Platform.Id WHERE GameId = @id ORDER BY Platform.`Name`;"; - if (PlatformId == -1) { + if (PlatformId == -1) + { // data query sql = "SELECT DISTINCT Games_Roms.*, Platform.`Name` AS platformname, GameState.RomId AS SavedStateRomId FROM Games_Roms LEFT JOIN Platform ON Games_Roms.PlatformId = Platform.Id LEFT JOIN GameState ON (Games_Roms.Id = GameState.RomId AND GameState.UserId = @userid AND GameState.IsMediaGroup = 0) WHERE Games_Roms.GameId = @id" + NameSearchWhere + " ORDER BY Platform.`Name`, Games_Roms.`Name` LIMIT 1000;"; - + // count query sqlCount = "SELECT COUNT(Games_Roms.Id) AS RomCount FROM Games_Roms WHERE Games_Roms.GameId = @id" + NameSearchWhere + ";"; - } else { + } + else + { // data query sql = "SELECT DISTINCT Games_Roms.*, Platform.`Name` AS platformname, GameState.RomId AS SavedStateRomId FROM Games_Roms LEFT JOIN Platform ON Games_Roms.PlatformId = Platform.Id LEFT JOIN GameState ON (Games_Roms.Id = GameState.RomId AND GameState.UserId = @userid AND GameState.IsMediaGroup = 0) WHERE Games_Roms.GameId = @id AND Games_Roms.PlatformId = @platformid" + NameSearchWhere + " ORDER BY Platform.`Name`, Games_Roms.`Name` LIMIT 1000;"; @@ -52,12 +61,12 @@ namespace gaseous_server.Classes dbDict.Add("platformid", PlatformId); } - DataTable romDT = db.ExecuteCMD(sql, dbDict); + DataTable romDT = db.ExecuteCMD(sql, dbDict); Dictionary rowCount = db.ExecuteCMDDict(sqlCount, dbDict)[0]; DataTable platformDT = db.ExecuteCMD(sqlPlatform, dbDict); - if (romDT.Rows.Count > 0) - { + if (romDT.Rows.Count > 0) + { // set count of roms GameRoms.Count = int.Parse((string)rowCount["RomCount"]); @@ -73,12 +82,12 @@ namespace gaseous_server.Classes } return GameRoms; - } - else - { - throw new Games.InvalidGameId(GameId); - } - } + } + else + { + throw new Games.InvalidGameId(GameId); + } + } public static GameRomItem GetRom(long RomId) { @@ -100,6 +109,26 @@ namespace gaseous_server.Classes } } + public static GameRomItem GetRom(string MD5) + { + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "SELECT Games_Roms.*, Platform.`Name` AS platformname FROM Games_Roms LEFT JOIN Platform ON Games_Roms.PlatformId = Platform.Id WHERE Games_Roms.MD5 = @id"; + Dictionary dbDict = new Dictionary(); + dbDict.Add("id", MD5); + DataTable romDT = db.ExecuteCMD(sql, dbDict); + + if (romDT.Rows.Count > 0) + { + DataRow romDR = romDT.Rows[0]; + GameRomItem romItem = BuildRom(romDR); + return romItem; + } + else + { + throw new InvalidRomHash(MD5); + } + } + public static GameRomItem UpdateRom(long RomId, long PlatformId, long GameId) { // ensure metadata for platformid is present @@ -108,10 +137,10 @@ namespace gaseous_server.Classes // ensure metadata for gameid is present IGDB.Models.Game game = Classes.Metadata.Games.GetGame(GameId, false, false, false); - Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "UPDATE Games_Roms SET PlatformId=@platformid, GameId=@gameid WHERE Id = @id"; - Dictionary dbDict = new Dictionary(); - dbDict.Add("id", RomId); + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "UPDATE Games_Roms SET PlatformId=@platformid, GameId=@gameid WHERE Id = @id"; + Dictionary dbDict = new Dictionary(); + dbDict.Add("id", RomId); dbDict.Add("platformid", PlatformId); dbDict.Add("gameid", GameId); db.ExecuteCMD(sql, dbDict); @@ -119,7 +148,7 @@ namespace gaseous_server.Classes GameRomItem rom = GetRom(RomId); return rom; - } + } public static void DeleteRom(long RomId) { @@ -137,7 +166,7 @@ namespace gaseous_server.Classes dbDict.Add("id", RomId); db.ExecuteCMD(sql, dbDict); } - } + } private static GameRomItem BuildRom(DataRow romDR) { @@ -151,27 +180,27 @@ namespace gaseous_server.Classes } GameRomItem romItem = new GameRomItem - { - Id = (long)romDR["id"], - PlatformId = (long)romDR["platformid"], + { + Id = (long)romDR["id"], + PlatformId = (long)romDR["platformid"], Platform = (string)romDR["platformname"], - GameId = (long)romDR["gameid"], - Name = (string)romDR["name"], - Size = (long)romDR["size"], - Crc = ((string)romDR["crc"]).ToLower(), - Md5 = ((string)romDR["md5"]).ToLower(), - Sha1 = ((string)romDR["sha1"]).ToLower(), - DevelopmentStatus = (string)romDR["developmentstatus"], - Attributes = Newtonsoft.Json.JsonConvert.DeserializeObject>>((string)Common.ReturnValueIfNull(romDR["attributes"], "[ ]")), - RomType = (HasheousClient.Models.LookupResponseModel.RomItem.RomTypes)(int)romDR["romtype"], - RomTypeMedia = (string)romDR["romtypemedia"], - MediaLabel = (string)romDR["medialabel"], - Path = (string)romDR["path"], + GameId = (long)romDR["gameid"], + Name = (string)romDR["name"], + Size = (long)romDR["size"], + Crc = ((string)romDR["crc"]).ToLower(), + Md5 = ((string)romDR["md5"]).ToLower(), + Sha1 = ((string)romDR["sha1"]).ToLower(), + DevelopmentStatus = (string)romDR["developmentstatus"], + Attributes = Newtonsoft.Json.JsonConvert.DeserializeObject>>((string)Common.ReturnValueIfNull(romDR["attributes"], "[ ]")), + RomType = (HasheousClient.Models.LookupResponseModel.RomItem.RomTypes)(int)romDR["romtype"], + RomTypeMedia = (string)romDR["romtypemedia"], + MediaLabel = (string)romDR["medialabel"], + Path = (string)romDR["path"], SignatureSource = (gaseous_server.Models.Signatures_Games.RomItem.SignatureSourceType)(Int32)romDR["metadatasource"], SignatureSourceGameTitle = (string)Common.ReturnValueIfNull(romDR["MetadataGameName"], ""), HasSaveStates = hasSaveStates, Library = GameLibrary.GetLibrary((int)romDR["LibraryId"]) - }; + }; // check for a web emulator and update the romItem foreach (Models.PlatformMapping.PlatformMapItem platformMapping in Models.PlatformMapping.PlatformMap) @@ -185,8 +214,8 @@ namespace gaseous_server.Classes } } - return romItem; - } + return romItem; + } public class GameRomObject { @@ -198,13 +227,13 @@ namespace gaseous_server.Classes { public long PlatformId { get; set; } public string Platform { get; set; } - public Models.PlatformMapping.PlatformMapItem.WebEmulatorItem? Emulator { get; set; } - public long GameId { get; set; } + public Models.PlatformMapping.PlatformMapItem.WebEmulatorItem? Emulator { get; set; } + public long GameId { get; set; } public string? Path { get; set; } - public string? SignatureSourceGameTitle { get; set;} + public string? SignatureSourceGameTitle { get; set; } public bool HasSaveStates { get; set; } = false; public GameLibrary.LibraryItem Library { get; set; } - } - } + } + } } diff --git a/gaseous-server/Controllers/V1.1/StateManagerController.cs b/gaseous-server/Controllers/V1.1/StateManagerController.cs index db47d7e..ba311c9 100644 --- a/gaseous-server/Controllers/V1.1/StateManagerController.cs +++ b/gaseous-server/Controllers/V1.1/StateManagerController.cs @@ -6,6 +6,7 @@ using Authentication; using Microsoft.AspNetCore.Identity; using System.Data; using Asp.Versioning; +using System.IO.Compression; namespace gaseous_server.Controllers.v1_1 { @@ -13,7 +14,7 @@ namespace gaseous_server.Controllers.v1_1 [ApiVersion("1.0")] [ApiVersion("1.1")] [ApiController] - public class StateManagerController: ControllerBase + public class StateManagerController : ControllerBase { private readonly UserManager _userManager; private readonly SignInManager _signInManager; @@ -39,7 +40,7 @@ namespace gaseous_server.Controllers.v1_1 var user = await _userManager.GetUserAsync(User); byte[] CompressedState = Common.Compress(uploadState.StateByteArray); - + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); string sql = "INSERT INTO GameState (UserId, RomId, IsMediaGroup, StateDateTime, Name, Screenshot, State, Zipped) VALUES (@userid, @romid, @ismediagroup, @statedatetime, @name, @screenshot, @state, @zipped); SELECT LAST_INSERT_ID();"; Dictionary dbDict = new Dictionary @@ -86,7 +87,7 @@ namespace gaseous_server.Controllers.v1_1 { "ismediagroup", IsMediaGroup } }; DataTable data = db.ExecuteCMD(sql, dbDict); - + List gameStates = new List(); foreach (DataRow row in data.Rows) { @@ -116,7 +117,7 @@ namespace gaseous_server.Controllers.v1_1 { "ismediagroup", IsMediaGroup } }; DataTable data = db.ExecuteCMD(sql, dbDict); - + if (data.Rows.Count == 0) { // invalid match - return not found @@ -150,7 +151,7 @@ namespace gaseous_server.Controllers.v1_1 { "ismediagroup", IsMediaGroup } }; db.ExecuteNonQuery(sql, dbDict); - + return Ok(); } @@ -175,7 +176,7 @@ namespace gaseous_server.Controllers.v1_1 { "name", model.Name } }; db.ExecuteNonQuery(sql, dbDict); - + return Ok(); } @@ -200,7 +201,7 @@ namespace gaseous_server.Controllers.v1_1 { "ismediagroup", IsMediaGroup } }; DataTable data = db.ExecuteCMD(sql, dbDict); - + if (data.Rows.Count == 0) { // invalid match - return not found @@ -233,11 +234,11 @@ namespace gaseous_server.Controllers.v1_1 [ProducesResponseType(StatusCodes.Status404NotFound)] [Route("{RomId}/{StateId}/State/")] [Route("{RomId}/{StateId}/State/savestate.state")] - public async Task GetStateDataAsync(long RomId, long StateId, bool IsMediaGroup = false) + public async Task GetStateDataAsync(long RomId, long StateId, bool IsMediaGroup = false, bool StateOnly = false) { var user = await _userManager.GetUserAsync(User); Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT Zipped, State FROM GameState WHERE Id = @id AND RomId = @romid AND IsMediaGroup = @ismediagroup AND UserId = @userid;"; + string sql = "SELECT * FROM GameState WHERE Id = @id AND RomId = @romid AND IsMediaGroup = @ismediagroup AND UserId = @userid;"; Dictionary dbDict = new Dictionary { { "id", StateId }, @@ -246,7 +247,7 @@ namespace gaseous_server.Controllers.v1_1 { "ismediagroup", IsMediaGroup } }; DataTable data = db.ExecuteCMD(sql, dbDict); - + if (data.Rows.Count == 0) { // invalid match - return not found @@ -254,7 +255,9 @@ namespace gaseous_server.Controllers.v1_1 } else { - string filename = "savestate.state"; + // get rom data + Roms.GameRomItem romItem = Roms.GetRom(RomId); + byte[] bytes; if ((bool)data.Rows[0]["Zipped"] == false) { @@ -264,7 +267,86 @@ namespace gaseous_server.Controllers.v1_1 { bytes = Common.Decompress((byte[])data.Rows[0]["State"]); } - string contentType = "application/octet-stream"; + + string contentType = ""; + string filename = ((DateTime)data.Rows[0]["StateDateTime"]).ToString("yyyy-MM-ddTHH-mm-ss") + "-" + Path.GetFileNameWithoutExtension(romItem.Name); + + + if (StateOnly == true) + { + contentType = "application/octet-stream"; + filename = filename + ".state"; + } + else + { + contentType = "application/zip"; + filename = filename + ".zip"; + + Dictionary RomInfo = new Dictionary + { + { "Name", romItem.Name }, + { "StateDateTime", data.Rows[0]["StateDateTime"] }, + { "StateName", data.Rows[0]["Name"] } + }; + if ((int)data.Rows[0]["IsMediaGroup"] == 0) + { + RomInfo.Add("MD5", romItem.Md5); + RomInfo.Add("SHA1", romItem.Sha1); + RomInfo.Add("Type", "ROM"); + } + else + { + RomInfo.Add("Type", "Media Group"); + RomInfo.Add("MediaGroupId", (long)data.Rows[0]["RomId"]); + } + string RomInfoString = Newtonsoft.Json.JsonConvert.SerializeObject(RomInfo, Newtonsoft.Json.Formatting.Indented, new Newtonsoft.Json.JsonSerializerSettings { NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore }); + + // compile zip file + using (var compressedFileStream = new MemoryStream()) + { + List> Attachments = new List>(); + Attachments.Add(new Dictionary + { + { "Name", "savestate.state" }, + { "Body", bytes } + }); + // check if value is dbnull + if (data.Rows[0]["Screenshot"] != DBNull.Value) + { + Attachments.Add(new Dictionary + { + { "Name", "screenshot.jpg" }, + { "Body", (byte[])data.Rows[0]["Screenshot"] } + }); + } + Attachments.Add(new Dictionary + { + { "Name", "rominfo.json" }, + { "Body", System.Text.Encoding.UTF8.GetBytes(RomInfoString) } + }); + + //Create an archive and store the stream in memory. + using (var zipArchive = new ZipArchive(compressedFileStream, ZipArchiveMode.Create, false)) + { + foreach (var Attachment in Attachments) + { + //Create a zip entry for each attachment + var zipEntry = zipArchive.CreateEntry(Attachment["Name"].ToString()); + + //Get the stream of the attachment + using (var originalFileStream = new MemoryStream((byte[])Attachment["Body"])) + using (var zipEntryStream = zipEntry.Open()) + { + //Copy the attachment stream to the zip entry stream + originalFileStream.CopyTo(zipEntryStream); + } + } + } + + //return new FileContentResult(compressedFileStream.ToArray(), "application/zip") { FileDownloadName = filename }; + bytes = compressedFileStream.ToArray(); + } + } var cd = new System.Net.Mime.ContentDisposition { @@ -279,6 +361,156 @@ namespace gaseous_server.Controllers.v1_1 } } + [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] + [HttpPost] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [RequestSizeLimit(long.MaxValue)] + [Consumes("multipart/form-data")] + [DisableRequestSizeLimit, RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue, ValueLengthLimit = int.MaxValue)] + [Route("Upload")] + public async Task UploadStateDataAsync(IFormFile file, long RomId = 0, bool IsMediaGroup = false) + { + // get user + var user = await _userManager.GetUserAsync(User); + + if (file.Length > 0) + { + MemoryStream fileContent = new MemoryStream(); + file.CopyTo(fileContent); + + // test if file is a zip file + try + { + using (var zipArchive = new ZipArchive(fileContent, ZipArchiveMode.Read, false)) + { + foreach (var entry in zipArchive.Entries) + { + if (entry.FullName == "rominfo.json") + { + using (var stream = entry.Open()) + using (var reader = new StreamReader(stream)) + { + string RomInfoString = reader.ReadToEnd(); + Dictionary RomInfo = Newtonsoft.Json.JsonConvert.DeserializeObject>(RomInfoString); + + // get rom data + Roms.GameRomItem romItem; + + try + { + romItem = Roms.GetRom((string)RomInfo["MD5"]); + } + catch (Roms.InvalidRomHash) + { + return NotFound(); + } + + // get state data + byte[] StateData = null; + byte[] ScreenshotData = null; + string StateName = RomInfo["StateName"].ToString(); + DateTime StateDateTime = DateTime.Parse(RomInfo["StateDateTime"].ToString()); + IsMediaGroup = RomInfo["Type"].ToString() == "Media Group" ? true : false; + + if (zipArchive.GetEntry("savestate.state") != null) + { + using (var stateStream = zipArchive.GetEntry("savestate.state").Open()) + using (var stateReader = new MemoryStream()) + { + stateStream.CopyTo(stateReader); + StateData = stateReader.ToArray(); + } + } + if (zipArchive.GetEntry("screenshot.jpg") != null) + { + using (var screenshotStream = zipArchive.GetEntry("screenshot.jpg").Open()) + using (var screenshotReader = new MemoryStream()) + { + screenshotStream.CopyTo(screenshotReader); + ScreenshotData = screenshotReader.ToArray(); + } + } + + // save state + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "INSERT INTO GameState (UserId, RomId, IsMediaGroup, StateDateTime, Name, Screenshot, State, Zipped) VALUES (@userid, @romid, @ismediagroup, @statedatetime, @name, @screenshot, @state, @zipped); SELECT LAST_INSERT_ID();"; + Dictionary dbDict = new Dictionary + { + { "userid", user.Id }, + { "romid", romItem.Id }, + { "ismediagroup", IsMediaGroup }, + { "statedatetime", StateDateTime }, + { "name", StateName }, + { "screenshot", ScreenshotData }, + { "state", Common.Compress(StateData) }, + { "zipped", true } + }; + DataTable data = db.ExecuteCMD(sql, dbDict); + + RomInfo.Add("RomId", romItem.Id); + RomInfo.Add("Management", "Managed"); + return Ok(RomInfo); + } + } + } + } + + return BadRequest("File is not a valid Gaseous state file."); + } + catch + { + // not a zip file + if (RomId != 0) + { + // get rom data + Roms.GameRomItem romItem; + + try + { + romItem = Roms.GetRom(RomId); + } + catch (Roms.InvalidRomHash) + { + return NotFound(); + } + + // save state + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "INSERT INTO GameState (UserId, RomId, IsMediaGroup, StateDateTime, Name, Screenshot, State, Zipped) VALUES (@userid, @romid, @ismediagroup, @statedatetime, @name, @screenshot, @state, @zipped); SELECT LAST_INSERT_ID();"; + Dictionary dbDict = new Dictionary + { + { "userid", user.Id }, + { "romid", RomId }, + { "ismediagroup", IsMediaGroup }, + { "statedatetime", DateTime.UtcNow }, + { "name", "" }, + { "screenshot", null }, + { "state", Common.Compress(fileContent.ToArray()) }, + { "zipped", true } + }; + DataTable data = db.ExecuteCMD(sql, dbDict); + + return Ok(new Dictionary + { + { "RomId", RomId }, + { "Management", "Unmanaged" } + }); + } + else + { + return BadRequest("No rom id provided."); + } + } + } + else + { + return BadRequest("File is empty."); + } + } + private Models.GameStateItem BuildGameStateItem(DataRow dr) { bool HasScreenshot = true; diff --git a/gaseous-server/wwwroot/pages/dialogs/emulatorloadstate.html b/gaseous-server/wwwroot/pages/dialogs/emulatorloadstate.html index 33ee3a5..c17b6fb 100644 --- a/gaseous-server/wwwroot/pages/dialogs/emulatorloadstate.html +++ b/gaseous-server/wwwroot/pages/dialogs/emulatorloadstate.html @@ -1,6 +1,9 @@
+
+ Upload state file: +
\ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/emulator.html b/gaseous-server/wwwroot/pages/emulator.html index fd1f62d..57bf530 100644 --- a/gaseous-server/wwwroot/pages/emulator.html +++ b/gaseous-server/wwwroot/pages/emulator.html @@ -13,7 +13,7 @@ if (IsMediaGroupInt == 1) { IsMediaGroup = true; } var StateUrl = undefined; if (getQueryString('stateid', 'int')) { - StateUrl = '/api/v1.1/StateManager/' + romId + '/' + getQueryString('stateid', 'int') + '/State/savestate.state?IsMediaGroup=' + IsMediaGroup; + StateUrl = '/api/v1.1/StateManager/' + romId + '/' + getQueryString('stateid', 'int') + '/State/savestate.state?StateOnly=true&IsMediaGroup=' + IsMediaGroup; } var gameData; var artworks = null; @@ -60,7 +60,7 @@ case 'EmulatorJS': console.log("Emulator: " + getQueryString('engine', 'string')); console.log("Core: " + getQueryString('core', 'string')); - + $('#emulator').load('/emulators/EmulatorJS.html?v=' + AppVersion); break; } @@ -95,11 +95,11 @@ '/api/v1.1/Statistics/Games/' + gameId + '/' + SessionId, 'PUT', function (success) { - + } ); } } - + setInterval(SaveStatistics, 60000); - + \ No newline at end of file