diff --git a/js/progressCalculation.mjs b/js/progressCalculation.mjs index 481adfb..eb6c547 100644 --- a/js/progressCalculation.mjs +++ b/js/progressCalculation.mjs @@ -13,6 +13,8 @@ export{ import { totalweight, getJsonData, findCell, runOnTree, progressClasses } from './obliviondata.mjs'; import {saveProgressToCookie} from './userdata.mjs'; import {uploadCurrentSave} from './sharing.mjs'; +import { uploadPartialSave } from './sharing.mjs'; +import { compressSaveData, decompressSaveData, saveCookie } from './userdata.mjs'; /** * Update save progress for the specified element. @@ -151,6 +153,13 @@ function updateChecklistProgressInternal(cell, newValue, skipSave){ } } else{ + // sometimes we mess up the savedata completely. + // this is a recovery. + if(savedata[cell.hive.classname] == null) + { + console.log("savedata messed up: hive doesnt exist."); + savedata[cell.hive.classname] = {}; + } savedata[cell.hive.classname][cell.id] = valueAsCorrectType; } } @@ -164,7 +173,19 @@ function updateChecklistProgressInternal(cell, newValue, skipSave){ if(!skipSave){ saveProgressToCookie(); if(settings.autoUploadCheck){ - uploadCurrentSave(false); + // idk this might result in torn savedata + uploadPartialSave(cell).then((result)=>{ + //new data: + const returnedSaveData = decompressSaveData(JSON.parse(result.response)); + const oldData = JSON.stringify(compressSaveData(savedata)); + const newData = JSON.stringify(compressSaveData(returnedSaveData)); + if(oldData != newData) + { + savedata = returnedSaveData; + saveCookie("progress",returnedSaveData); + document.dispatchEvent(new Event("progressLoad")); + } + }); } } return true; diff --git a/js/saveReader.mjs b/js/saveReader.mjs index 24fd916..8bdd867 100644 --- a/js/saveReader.mjs +++ b/js/saveReader.mjs @@ -3,6 +3,7 @@ export {parseSave} import { loadJsonData, jsondata, progressClasses, runOnTree, findCell } from './obliviondata.mjs' +import { uploadCurrentSave } from './sharing.mjs'; import { initShareSettings, stopSpectating } from './sharing.mjs'; import { saveProgressToCookie } from './userdata.mjs'; @@ -340,6 +341,7 @@ function parseSave(e){ window.savedata = dataFromSave; saveProgressToCookie(); + uploadCurrentSave(); window.location.reload(); }); } diff --git a/js/sharing.mjs b/js/sharing.mjs index ced208b..4ab9ef7 100644 --- a/js/sharing.mjs +++ b/js/sharing.mjs @@ -14,6 +14,7 @@ export { uploadSave, downloadSave, uploadCurrentSave, + uploadPartialSave, startSpectating, stopSpectating, setRemoteUrl, @@ -85,9 +86,24 @@ async function uploadSave(uploadUrl, saveData, myShareCode, myShareKey){ req.setRequestHeader("Content-Type","application/json"); req.onload = function () { + let newCode; + //first get url... + const share = "share/"; + var start = this.responseURL.indexOf(share); + if(start == -1){ + newCode = this.response; + } + else{ + start += share.length; + newCode = this.responseURL.substring(start); + newCode = newCode.substring(0, newCode.indexOf('/')); + if(newCode.length < 6){ + newCode = this.response; + } + } if(this.status == 200){ //yay. - resolve(this.response); + resolve({"code": newCode, "response":this.response}); } else{ reject(this); @@ -157,15 +173,17 @@ async function uploadCurrentSave(notifyOnUpdate = true){ return uploadSave(settings.serverUrl, compressedData, settings.myShareCode, settings.shareKey) .then((result)=>{ if(result){ - if(settings.myShareCode != result){ - console.log("my share code changed from '"+settings.myShareCode+"' to '"+result+"'"); - settings.myShareCode = result; + if(settings.myShareCode != result.code){ + console.log("my share code changed from '"+settings.myShareCode+"' to '"+result.code+"'"); + settings.myShareCode = result.code; saveCookie("settings",settings); } + //todo: parse new savedata? + //do this every time we upload: document.dispatchEvent(new Event("progressShared")); if(window.debug){ - console.log("progress shared: "+result); + console.log("progress shared: "+result.code); } if(notifyOnUpdate){ //????? @@ -175,6 +193,61 @@ async function uploadCurrentSave(notifyOnUpdate = true){ }); } +/** + * Upload partial changes to a savefile. + * @param partialJsonData jsondata hive to upload. + */ +async function uploadPartialSave(partialJsonData){ + if(settings.remoteShareCode){ + //if we're viewing remote, don't upload. + console.log("viewing remote data, will not upload."); + return; + } + initShareSettings(); + + //currently only support single element or single hive. + if(partialJsonData.hive == null) + { + console.error("cannot upload, no hive found."); + } + + //hive: + let hive = partialJsonData.hive; + if(!hive.class.containsUserProgress) + { + return; + } + + //get savedata format + let dataToUpload = savedata[hive.classname]; + let uploadPath = hive.classname; + + if(partialJsonData.elements == null && partialJsonData.id != null) + { + //leaf node. we can upload just this. + dataToUpload = savedata[hive.classname][partialJsonData.id]; + if(hive.class.standard) + { + //compress if we need to + dataToUpload = dataToUpload == 1 ? 1:0; + } + uploadPath = `${hive.classname}/${partialJsonData.id}`; + } + else{ + if(hive.class.standard) + { + let compressed = []; + for(const elementPropName in dataToUpload){ + compressed[parseInt(elementPropName)] = dataToUpload[elementPropName] == 1 ?1:0; + } + dataToUpload = compressed; + } + } + + let fullUrl = `${settings.serverUrl}/${settings.myShareCode}/d/${uploadPath}`; + return uploadSave(fullUrl, dataToUpload, settings.myShareCode, settings.shareKey); +} + /** * @returns {boolean} is user currently spectating */ @@ -223,6 +296,10 @@ var autoUpdateIntervalId = null; * @param {boolean} updateGlobalSaveData Should we decompress spectating data (true) or just write it to localStorage? */ async function startSpectating(notifyOnUpdate = true, updateGlobalSaveData = true){ + if((new Date() - settings.shareDownloadTimeInternal) < (settings.spectateAutoRefreshInterval*1000)) + { + return; + } if(window.debug){ console.log("spectate update"); } @@ -344,17 +421,69 @@ function createSpectateBanner(){ * Call this on a page to do all the sharing stuff. Create topbar, start autorefresh, etc. */ function initSharingFeature(){ - if(settings.remoteShareCode == null || settings.remoteShareCode == ""){ + if(!isSpectating() && (settings.myShareCode == null || settings.myShareCode == "")){ return; } - if(!document.getElementById("spectateBanner")){ - let spectateBanner = createSpectateBanner(); - document.getElementById("topbar")?.insertBefore(spectateBanner, document.getElementById("topbar").firstChild); - document.getElementById("sidebarFloaty")?.classList.add("screenHeight2"); + if(isSpectating()) + { + if(!document.getElementById("spectateBanner")){ + let spectateBanner = createSpectateBanner(); + document.getElementById("topbar")?.insertBefore(spectateBanner, document.getElementById("topbar").firstChild); + document.getElementById("sidebarFloaty")?.classList.add("screenHeight2"); + } + if(settings.spectateAutoRefresh == true){ + startSpectating(false, true); + } } - if(settings.spectateAutoRefresh == true){ - startSpectating(false, true); + else{ + if(settings.spectateAutoRefresh) + { + startSync(true); + } + } +} + +function startSync(updateGlobalSaveData) +{ + if((new Date() - settings.shareDownloadTimeInternal) < (settings.spectateAutoRefreshInterval*1000)) + { + return; } + let downloadUrl = settings.serverUrl + "/" + settings.myShareCode; + return downloadSave(downloadUrl) + .then((dl)=>{ + if(dl){ + //we can't serialize the date object so we convert it to a pretty print string here + let dlTime = new Date(); + settings.shareDownloadTimeInternal = dlTime.toUTCString(); + settings.shareDownloadTime = dlTime.toDateString() + " " + dlTime.toTimeString().substring(0,8); + saveCookie("settings",settings); + + saveCookie("progress",dl); + if(updateGlobalSaveData){ + savedata = decompressSaveData(dl); + upgradeSaveData(false); + + } + document.dispatchEvent(new Event("progressLoad")); + } + else{ + if(window.debug){ + console.log("304 content unchanged"); + } + } + + //AFTER everything else, attach an auto listener to update spectating. + if(autoUpdateListener == null && settings.spectateAutoRefresh == true){ + if(window.debug){ + console.log("Attaching auto update listener"); + } + autoUpdateListener = ()=>{ + startSync(true); + } + autoUpdateIntervalId = setInterval(autoUpdateListener, Math.max(settings.spectateAutoRefreshInterval*1000, 1000)); + } + }); } diff --git a/js/userdata.mjs b/js/userdata.mjs index 2e06f68..dde58d5 100644 --- a/js/userdata.mjs +++ b/js/userdata.mjs @@ -5,6 +5,7 @@ import { runOnTree, progressClasses } from "./obliviondata.mjs"; import { initShareSettings } from "./sharing.mjs"; import {clearProgressCache} from './progressCalculation.mjs' +import { downloadSave } from "./sharing.mjs"; //functions that save and load user progess and settings. export{ @@ -159,9 +160,14 @@ function decompressSaveData(compressedSaveData){ if(matchingClass != null && matchingClass.standard) { decompressedSaveData[propname] = {}; let elements = compressedSaveData[propname]; - for(let i = 0; i < elements.length; i++){ + let length = elements.length; + if(elements.length == undefined) + { + length = Object.keys(elements).length; + } + for(let i = 0; i < length; i++){ if(elements[i] != null){ - decompressedSaveData[propname][i] = (elements[i] == 1); + decompressedSaveData[propname][i] = (elements[i] == 1) || (elements[i] === true); } } } @@ -269,7 +275,13 @@ function initSettings(){ * @returns {boolean} true if progress was been successfully loaded. False if new savedata was created. */ function loadProgressFromCookie(){ - loadSettingsFromCookie(); + loadSettingsFromCookie(); + if(settings.myShareCode != null || settings.remoteShareCode != null) + { + //TODO: try reloading from remote + //TODO: what if we wipe save#s tho + //downloadSave(...) + } var compressed = loadCookie("progress"); if(compressed && Object.getOwnPropertyNames(compressed).length != 0){ @@ -286,7 +298,7 @@ function loadProgressFromCookie(){ else{ //could not find savedata. create new savedata. if(window.debug){ - console.log("could not find dsavedata. resetting progress."); + console.log("could not find savedata. resetting progress."); } resetProgress(false); return false; diff --git a/server/Controllers/DataController.cs b/server/Controllers/DataController.cs new file mode 100644 index 0000000..2eb96d1 --- /dev/null +++ b/server/Controllers/DataController.cs @@ -0,0 +1,82 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.VisualBasic; +using System; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Xml; +using System.Xml.Linq; + +namespace ShareApi.Controllers +{ + [ApiController] + [Route("share/{url}/d/{**jsonPath}")] + public class DataController : Controller + { + + + [HttpGet] + public ActionResult HandleGet(string url, string? jsonPath) + { + var saveEditor = new SaveDataEditor(url); + return Ok(saveEditor.HandleData(new HttpMethod(Request.Method), jsonPath, null)); + } + + [HttpPut] + [HttpPost] + public ActionResult Handle(ProgressUpdate update, string url, string? jsonPath) + { + ProgressUpdateValidator.Validate(update, out ValidationFailedReason validationFailedReason); + if(validationFailedReason != ValidationFailedReason.NONE) + { + ModelState.AddModelError("error", validationFailedReason.ToString()); + return BadRequest(ModelState); + } + var saveEditor = new SaveDataEditor(url, update); + if(saveEditor.ReadOnly) + { + return Unauthorized(); + } + return Ok(saveEditor.HandleData(new HttpMethod(Request.Method), jsonPath, JsonNode.Parse(update.SaveData))); + } + } + + public class JsonProxyNode + { + public JsonNode? parent; + public string Name; + public JsonNode? contents; + + public JsonProxyNode(string name) + { + this.Name = name; + } + + /// + /// Update the json tree and return the changed tree. + /// + /// + public JsonNode Commit() + { + if (parent != null) + { + if (parent.GetValueKind() == System.Text.Json.JsonValueKind.Object) + { + parent[Name] = contents; + } + else + { + parent[int.Parse(Name)] = contents; + } + + return parent.Root ?? parent; + } + else + { + return contents; + } + + } + } +} diff --git a/server/ErrorController.cs b/server/Controllers/ErrorController.cs similarity index 100% rename from server/ErrorController.cs rename to server/Controllers/ErrorController.cs diff --git a/server/Controllers/InitialUpdateController.cs b/server/Controllers/InitialUpdateController.cs new file mode 100644 index 0000000..2333393 --- /dev/null +++ b/server/Controllers/InitialUpdateController.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Mvc; +using System; + +namespace ShareApi +{ + public class Error + { + public string ErrorMessage { get; set; } = ""; + } + + /// + /// Handle a request to /share. This usually happens on the initial request. + /// + [ApiController] + [Route("share")] + public class InitialUpdateController : ControllerBase + { + [HttpPost] + public ActionResult HandleProgressUpdate(ProgressUpdate update){ + + bool passed = ProgressUpdateValidator.Validate(update, out var reason); + if (passed) + { + + if(update.Url != null) + { + //existing url. Should go to /share/{url}/d + return RedirectPermanentPreserveMethod($"share/{update.Url}/d"); + } + + //new url: + using (ProgressManagerSql sql = new ProgressManagerSql()) + { + string? shareCode = null ; + var storedShareCode = sql.SqlUrlSelect(update.Key); + if (storedShareCode == null) + { + //this is a new request. + shareCode = ProgressManager.Instance.GenerateNewUrlAndInsert(sql, update.Key); + return RedirectToActionPermanent("HandleProgressRequest","ProgressRequestHandler", new object[] { shareCode }); + } + else + { + return Ok(storedShareCode); + } + } + } + else + { + ModelState.AddModelError("error", reason.ToString()); + return BadRequest(ModelState); + } + } + } +} \ No newline at end of file diff --git a/server/ProgressRequestHandler.cs b/server/Controllers/ProgressRequestHandler.cs similarity index 74% rename from server/ProgressRequestHandler.cs rename to server/Controllers/ProgressRequestHandler.cs index f2fcb9c..fb1255f 100644 --- a/server/ProgressRequestHandler.cs +++ b/server/Controllers/ProgressRequestHandler.cs @@ -19,16 +19,9 @@ public ActionResult HandleProgressRequest(string url) else { ReadProgress result; - if(!ProgressManager.Cache.TryGetCacheOnly(url, out result)) + if(!ProgressManager.Instance.TryGetValue(url, out result)) { - //not found in cache. Find in backing store. - using (ProgressManagerSql sql = new ProgressManagerSql()) - { - if (!ProgressManager.Cache.TryGet(url, sql.SqlSaveSelect, out result)) - { - return NotFound(); - } - } + return NotFound(); } Debug.Assert(result != null, "result returned for url is null."); diff --git a/server/ViewCountHandler.cs b/server/Controllers/ViewCountHandler.cs similarity index 92% rename from server/ViewCountHandler.cs rename to server/Controllers/ViewCountHandler.cs index 3a49a41..9728763 100644 --- a/server/ViewCountHandler.cs +++ b/server/Controllers/ViewCountHandler.cs @@ -3,7 +3,7 @@ namespace ShareApi { - [Route("url/{url}/views")] + [Route("share/{url}/views")] [ApiController] public class ViewCountHandler : ControllerBase { diff --git a/server/ProgressManager.cs b/server/ProgressManager.cs index 487331d..378ca4a 100644 --- a/server/ProgressManager.cs +++ b/server/ProgressManager.cs @@ -1,4 +1,9 @@ +using Microsoft.AspNetCore.DataProtection.KeyManagement; +using Microsoft.VisualBasic; using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Nodes; namespace ShareApi { @@ -6,13 +11,11 @@ namespace ShareApi /// Handles the creation, update, and reads of saved progress items. /// public class ProgressManager{ - //TODO: move this to a better place - public static ReadCache Cache = new ReadCache(); + public static ProgressManager Instance = new ProgressManager(); + private ReadCache Cache = new ReadCache(); + private Random randomGen = new Random(); - Random randomGen; - - public ProgressManager(){ - randomGen = new Random(); + private ProgressManager(){ } /// @@ -21,7 +24,7 @@ public ProgressManager(){ /// /// /// the new url - private string GenerateNewUrlAndInsert(ProgressManagerSql sql, byte[] key){ + public string GenerateNewUrlAndInsert(ProgressManagerSql sql, byte[] key){ //6-character base64 = 6*6 = 36 bits, pad to 40 = 5 bytes byte[] bytes = new byte[5]; string newUrl; @@ -42,6 +45,23 @@ private string GenerateNewUrlAndInsert(ProgressManagerSql sql, byte[] key){ return newUrl; } + public bool TryGetValue(string shareKey, [NotNullWhen(true)] out ReadProgress? result) + { + if (!Cache.TryGetCacheOnly(shareKey, out result)) + { + //not found in cache. Find in backing store. + using (ProgressManagerSql sql = new ProgressManagerSql()) + { + if (!Cache.TryGet(shareKey, sql.SqlSaveSelect, out result)) + { + result = default; + return false; + } + } + } + return true; + } + /// /// Update or insert save data. /// @@ -49,15 +69,14 @@ private string GenerateNewUrlAndInsert(ProgressManagerSql sql, byte[] key){ /// /// /// - private bool UpdateSaveData(ProgressManagerSql sql, string url, ReadProgress data){ - var updated = sql.SqlSaveUpdate(url,data); - if(!updated){ - //need to insert - return sql.SqlSaveInsert(url, data); - } - else{ - return updated; - } + public bool UpdateSaveData(ProgressManagerSql sql, string url, ReadProgress data){ + Cache.Set(url, data, (url, data) => { sql.SqlSaveMerge(url, data); }); + return true; + } + + public bool VerifyKey(ProgressManagerSql sql, string saveId, byte[] saveKey) + { + return sql.SqlUrlSelect(saveKey) == saveId; } /// @@ -68,21 +87,10 @@ private bool UpdateSaveData(ProgressManagerSql sql, string url, ReadProgress dat public string? HandleUpdate(ProgressUpdate update){ string? url = update.Url; using(ProgressManagerSql sql = new ProgressManagerSql()) { - var storedUrl = sql.SqlUrlSelect(update.Key); - if (storedUrl == null && url == null){ - //this is a new request. - url = GenerateNewUrlAndInsert(sql, update.Key); - } - else{ - if(url != storedUrl){ - //return 401 - return null; - } - } //we have a valid URL. Cache.Set(url, new ReadProgress(update.SaveData, DateTime.UtcNow), - (url, data) => { UpdateSaveData(sql, url, data); }); + (url, data) => { sql.SqlSaveMerge(url, data); }); //return OK return url; diff --git a/server/ProgressManagerSql.cs b/server/ProgressManagerSql.cs index 9da9fef..d81efca 100644 --- a/server/ProgressManagerSql.cs +++ b/server/ProgressManagerSql.cs @@ -1,6 +1,7 @@ using System; using System.Data; using System.Data.SqlClient; +using System.Text.Json.Nodes; namespace ShareApi { @@ -16,6 +17,10 @@ public class ProgressManagerSql: IDisposable { private const string saveInsertString = "INSERT INTO saves VALUES(@col1, @col2, @accesstime)"; private const string saveUpdateString = "UPDATE saves SET saveData = @col2, accessed = @accesstime WHERE url = @col1"; private const string saveSelectString = "SELECT saveData, accessed FROM saves WHERE url = @col1"; + private const string saveMergeString = "MERGE INTO saves with(HOLDLOCK) USING (VALUES(@col1, @col2, @accesstime)) AS source(url, savedata, accessed) ON saves.url = @col1 " + + "WHEN MATCHED THEN UPDATE SET saveData = source.saveData, accessed = source.accessTime" + + "WHEN NOT MATCHED THEN INSERT (url, savedata, accessed) VALUES (@col1, @col2, @accesstime)"; + private SqlConnection conn; @@ -60,8 +65,8 @@ public bool SqlUrlInsert(byte[] key, string url){ /// /// /// - public bool SqlSaveInsert(string url, ReadProgress data){ - var cmd = new SqlCommand(saveInsertString, conn); + public bool SqlSaveMerge(string url, ReadProgress data){ + var cmd = new SqlCommand(saveMergeString, conn); cmd.Parameters.Add("@col1",SqlDbType.Char); cmd.Parameters["@col1"].Value = url; cmd.Parameters.Add("@col2",SqlDbType.VarChar); @@ -82,31 +87,6 @@ public bool SqlSaveInsert(string url, ReadProgress data){ } } - /// - /// update the save table - /// - /// - /// - /// - public bool SqlSaveUpdate(string url, ReadProgress data) { - var cmd = new SqlCommand(saveUpdateString, conn); - cmd.Parameters.Add("@col1", SqlDbType.Char); - cmd.Parameters["@col1"].Value = url; - cmd.Parameters.Add("@col2", SqlDbType.VarChar); - cmd.Parameters["@col2"].Value = data.SaveData; - cmd.Parameters.Add("@accesstime",SqlDbType.DateTime2); - cmd.Parameters["@accesstime"].Value = data.LastModified; - try - { - using (SqlDataReader reader = cmd.ExecuteReader()) - { - reader.Close(); - return reader.RecordsAffected != 0; - } - } - catch (SqlException) { return false; } - } - public string? SqlUrlSelect(byte[] key){ var cmd = new SqlCommand(urlSelectString, conn); cmd.Parameters.Add("@col1",SqlDbType.Binary); diff --git a/server/ProgressUpdate.cs b/server/ProgressUpdate.cs index a9713f4..ca9e155 100644 --- a/server/ProgressUpdate.cs +++ b/server/ProgressUpdate.cs @@ -1,4 +1,5 @@ using System; +using System.Text.Json.Nodes; namespace ShareApi { @@ -31,5 +32,11 @@ public ReadProgress(string saveData, DateTime lastModified) SaveData = saveData; LastModified = lastModified; } + + public ReadProgress(JsonNode saveData, DateTime lastModified) + { + SaveData = saveData.ToJsonString(); + LastModified = lastModified; + } } } diff --git a/server/ProgressUpdateHandler.cs b/server/ProgressUpdateHandler.cs deleted file mode 100644 index 012d4c2..0000000 --- a/server/ProgressUpdateHandler.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace ShareApi -{ - public class Error - { - public string ErrorMessage { get; set; } = ""; - } - [ApiController] - [Route("share")] - public class ProgressUpdateHandler : ControllerBase - { - private static ProgressManager mgr = new ProgressManager(); - [HttpPost] - public ActionResult HandleProgressUpdate(ProgressUpdate update){ - - bool passed = ProgressUpdateValidator.Validate(update, out var reason); - if (passed) - { - string? newurl = mgr.HandleUpdate(update); - if (newurl != null) - { - return Ok(newurl); - } - else - { - return Unauthorized(); - } - } - else - { - ModelState.AddModelError("error", reason.ToString()); - return BadRequest(ModelState); - } - } - } -} \ No newline at end of file diff --git a/server/ProgressUpdateValidator.cs b/server/ProgressUpdateValidator.cs index 07a89e3..d02fd3a 100644 --- a/server/ProgressUpdateValidator.cs +++ b/server/ProgressUpdateValidator.cs @@ -12,6 +12,9 @@ public enum ValidationFailedReason SAVE_DATA_EMPTY } + /// + /// Validate an incoming progress update struct + /// public class ProgressUpdateValidator { /// @@ -19,7 +22,7 @@ public class ProgressUpdateValidator /// /// /// - /// false if validation failed. True if sucess. + /// false if validation failed. True if success. public static bool Validate([NotNullWhen(true)] ProgressUpdate? update, out ValidationFailedReason validationFailedReason) { if (update == null) diff --git a/server/SaveDataEditor.cs b/server/SaveDataEditor.cs new file mode 100644 index 0000000..90385d3 --- /dev/null +++ b/server/SaveDataEditor.cs @@ -0,0 +1,126 @@ +using ShareApi.Controllers; +using System.Text.Json.Nodes; +using System; +using System.Net.Http; + +namespace ShareApi +{ + public class SaveDataEditor + { + private JsonNode? oldData; + private string shareCode; + private DateTime updateTime; + + ProgressManagerSql sql = new ProgressManagerSql(); + + public bool ReadOnly { get; private set; } + + /// + /// Initialize the editor and get the json data. + /// + /// + /// + public SaveDataEditor(string shareCode) + { + this.shareCode = shareCode; + if (ProgressManager.Instance.TryGetValue(shareCode, out ReadProgress? progress)) + { + oldData = JsonNode.Parse(progress.SaveData); + updateTime = progress.LastModified; + } + else + { + oldData = null; + updateTime = DateTime.UtcNow; + } + ReadOnly = true; + } + + public SaveDataEditor(string shareCode, ProgressUpdate userUpdate) : this(shareCode) + { + if (ProgressManager.Instance.VerifyKey(sql, shareCode, userUpdate.Key)) + { + ReadOnly = false; + } + } + + /// + /// Handle data get/set. + /// + /// + /// + /// + /// Updated contents of entire tree. + public JsonNode? HandleData(HttpMethod method, string? route, JsonNode? newData) + { + var node = GetNode(route?.Split('/') ?? new Span(), oldData); + + if (node != null) + { + if (method == HttpMethod.Get) + { + return node.contents; + } + else + { + if (ReadOnly) + { + return null; + } + else + { + if (method == HttpMethod.Put || method == HttpMethod.Post) + { + node.contents = newData; + var newNode = node.Commit(); + ProgressManager.Instance.UpdateSaveData(sql, shareCode, new ReadProgress(newNode, updateTime)); + return newNode; + } + } + } + } + return null; + } + + /// + /// Get node proxy. returns null if **parent** isnt defined. + /// + /// + private JsonProxyNode? GetNode(Span path, JsonNode? root) + { + if (path.Length == 0) + { + JsonProxyNode result = new JsonProxyNode(""); + result.contents = root; + return result; + } + else if (path.Length == 1) + { + JsonProxyNode result = new JsonProxyNode(path[0]); + if (root == null) + { + result.contents = null; + } + else + { + if (root.GetValueKind() == System.Text.Json.JsonValueKind.Object) + { + result.contents = root?[result.Name]; + } + else + { + result.contents = root[int.Parse(result.Name)]; + } + + } + result.parent = root; + return result; + } + else + { + return GetNode(path.Slice(1), root?[path[0]]); + } + return null; + } + } +} diff --git a/server/ShareApi.csproj b/server/ShareApi.csproj index f6f1208..0aa72a1 100644 --- a/server/ShareApi.csproj +++ b/server/ShareApi.csproj @@ -1,13 +1,13 @@  - net6.0 + net8.0 enable - - + + diff --git a/server/db.sql b/server/db.sql index 616a40d..58d164b 100644 --- a/server/db.sql +++ b/server/db.sql @@ -8,7 +8,7 @@ CREATE TABLE urls( -- could probably use a foreign key... CREATE TABLE saves ( [url] char(6) NOT NULL PRIMARY KEY, - [saveData] varchar(MAX) NULL, + [saveData] JSON NULL, [accessed] datetime NULL ) diff --git a/settings.html b/settings.html index fc328a6..cceb2bd 100644 --- a/settings.html +++ b/settings.html @@ -66,7 +66,7 @@

Settings

Share Code

You can share your progress with other people in real time, so anything you check off will appear on their spectator view! This is highly recommended for streamers so their viewers can follow their progress easily.

-

Share:

+

Share:

Spectate: — Clicking on someone else's Share URL or entering a 6 letter code and pressing enter on this box will place your in Spectator Mode.

Spectating Notes