diff --git a/ArchiSteamFarm/IPC/Controllers/Api/StorageController.cs b/ArchiSteamFarm/IPC/Controllers/Api/StorageController.cs
new file mode 100644
index 000000000..122ac967b
--- /dev/null
+++ b/ArchiSteamFarm/IPC/Controllers/Api/StorageController.cs
@@ -0,0 +1,98 @@
+// _ _ _ ____ _ _____
+// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
+// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
+// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
+// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
+// |
+// Copyright 2015-2021 Ćukasz "JustArchi" Domeradzki
+// Contact: JustArchi@JustArchi.net
+// |
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// |
+// http://www.apache.org/licenses/LICENSE-2.0
+// |
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Net;
+using ArchiSteamFarm.Core;
+using ArchiSteamFarm.IPC.Responses;
+using Microsoft.AspNetCore.Mvc;
+using Newtonsoft.Json.Linq;
+
+namespace ArchiSteamFarm.IPC.Controllers.Api {
+ [Route("Api/Storage")]
+ public sealed class StorageController : ArchiController {
+ ///
+ /// Deletes entry under specified key from ASF's persistent KeyValue JSON storage.
+ ///
+ [HttpDelete("{key:required}")]
+ [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.OK)]
+ public ActionResult StorageDelete(string key) {
+ if (string.IsNullOrEmpty(key)) {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ if (ASF.GlobalDatabase == null) {
+ throw new InvalidOperationException(nameof(ASF.GlobalDatabase));
+ }
+
+ ASF.GlobalDatabase.DeleteFromJsonStorage(key);
+
+ return Ok(new GenericResponse(true));
+ }
+
+ ///
+ /// Loads entry under specified key from ASF's persistent KeyValue JSON storage.
+ ///
+ [HttpGet("{key:required}")]
+ [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.OK)]
+ public ActionResult StorageGet(string key) {
+ if (string.IsNullOrEmpty(key)) {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ if (ASF.GlobalDatabase == null) {
+ throw new InvalidOperationException(nameof(ASF.GlobalDatabase));
+ }
+
+ JToken? value = ASF.GlobalDatabase.LoadFromJsonStorage(key);
+
+ return Ok(new GenericResponse(true, value));
+ }
+
+ ///
+ /// Saves entry under specified key in ASF's persistent KeyValue JSON storage.
+ ///
+ [Consumes("application/json")]
+ [HttpPost("{key:required}")]
+ [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.OK)]
+ public ActionResult StoragePost(string key, [FromBody] JToken value) {
+ if (string.IsNullOrEmpty(key)) {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ if (value == null) {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ if (ASF.GlobalDatabase == null) {
+ throw new InvalidOperationException(nameof(ASF.GlobalDatabase));
+ }
+
+ if (value.Type == JTokenType.Null) {
+ ASF.GlobalDatabase.DeleteFromJsonStorage(key);
+ } else {
+ ASF.GlobalDatabase.SaveToJsonStorage(key, value);
+ }
+
+ return Ok(new GenericResponse(true));
+ }
+ }
+}
diff --git a/ArchiSteamFarm/Storage/GlobalDatabase.cs b/ArchiSteamFarm/Storage/GlobalDatabase.cs
index 0e54a8f49..6e431ec67 100644
--- a/ArchiSteamFarm/Storage/GlobalDatabase.cs
+++ b/ArchiSteamFarm/Storage/GlobalDatabase.cs
@@ -40,6 +40,7 @@ using ArchiSteamFarm.Steam;
using ArchiSteamFarm.Steam.SteamKit2;
using JetBrains.Annotations;
using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
namespace ArchiSteamFarm.Storage {
public sealed class GlobalDatabase : SerializableFile {
@@ -54,6 +55,9 @@ namespace ArchiSteamFarm.Storage {
[JsonProperty(Required = Required.DisallowNull)]
internal readonly InMemoryServerListProvider ServerListProvider = new();
+ [JsonProperty(Required = Required.DisallowNull)]
+ private readonly ConcurrentDictionary KeyValueJsonStorage = new();
+
[JsonProperty(Required = Required.DisallowNull)]
private readonly ConcurrentDictionary PackagesAccessTokens = new();
@@ -164,6 +168,18 @@ namespace ArchiSteamFarm.Storage {
return globalDatabase;
}
+ internal void DeleteFromJsonStorage(string key) {
+ if (string.IsNullOrEmpty(key)) {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ if (!KeyValueJsonStorage.TryRemove(key, out _)) {
+ return;
+ }
+
+ Utilities.InBackground(Save);
+ }
+
internal HashSet GetPackageIDs(uint appID, IEnumerable packageIDs) {
if (appID == 0) {
throw new ArgumentOutOfRangeException(nameof(appID));
@@ -186,6 +202,14 @@ namespace ArchiSteamFarm.Storage {
return result;
}
+ internal JToken? LoadFromJsonStorage(string key) {
+ if (string.IsNullOrEmpty(key)) {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ return KeyValueJsonStorage.TryGetValue(key, out JToken? value) ? value : null;
+ }
+
internal async Task OnPICSChangesRestart(uint currentChangeNumber) {
if (currentChangeNumber == 0) {
throw new ArgumentOutOfRangeException(nameof(currentChangeNumber));
@@ -280,6 +304,23 @@ namespace ArchiSteamFarm.Storage {
}
}
+ internal void SaveToJsonStorage(string key, JToken value) {
+ if (string.IsNullOrEmpty(key)) {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ if (value == null) {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ if (KeyValueJsonStorage.TryGetValue(key, out JToken? currentValue) && JToken.DeepEquals(currentValue, value)) {
+ return;
+ }
+
+ KeyValueJsonStorage[key] = value;
+ Utilities.InBackground(Save);
+ }
+
private async void OnObjectModified(object? sender, EventArgs e) {
if (string.IsNullOrEmpty(FilePath)) {
return;
@@ -291,6 +332,7 @@ namespace ArchiSteamFarm.Storage {
// ReSharper disable UnusedMember.Global
public bool ShouldSerializeBackingCellID() => BackingCellID != 0;
public bool ShouldSerializeBackingLastChangeNumber() => LastChangeNumber != 0;
+ public bool ShouldSerializeKeyValueJsonStorage() => !KeyValueJsonStorage.IsEmpty;
public bool ShouldSerializePackagesAccessTokens() => !PackagesAccessTokens.IsEmpty;
public bool ShouldSerializePackagesData() => !PackagesData.IsEmpty;
public bool ShouldSerializeServerListProvider() => ServerListProvider.ShouldSerializeServerRecords();