diff --git a/.hemtt/scripts/copy_so.rhai b/.hemtt/scripts/copy_so.rhai index dd6a9f8..ee61b09 100644 --- a/.hemtt/scripts/copy_so.rhai +++ b/.hemtt/scripts/copy_so.rhai @@ -1 +1 @@ -HEMTT_RFS.join("extension").join("bin").join("Release").join("net8.0").join("win-x64").join("publish").join("ArmaRAMDb_x64.so").copy(HEMTT_RFS.join("ArmaRAMDb_x64.so")); \ No newline at end of file +HEMTT_RFS.join("extension").join("bin").join("Release").join("net8.0").join("linux-x64").join("publish").join("ArmaRAMDb_x64.so").copy(HEMTT_RFS.join("ArmaRAMDb_x64.so")); \ No newline at end of file diff --git a/ArmaRAMDb_x64.dll b/ArmaRAMDb_x64.dll index 03a2b8f..62c61b0 100644 Binary files a/ArmaRAMDb_x64.dll and b/ArmaRAMDb_x64.dll differ diff --git a/ArmaRAMDb_x64.so b/ArmaRAMDb_x64.so index 4e4c400..f285569 100644 Binary files a/ArmaRAMDb_x64.so and b/ArmaRAMDb_x64.so differ diff --git a/addons/db/functions/fnc_save.sqf b/addons/db/functions/fnc_save.sqf index 6c86c58..a3ae3e6 100644 --- a/addons/db/functions/fnc_save.sqf +++ b/addons/db/functions/fnc_save.sqf @@ -16,16 +16,25 @@ * Save to disc. * * Arguments: - * N/A + * 0: Type of save (default: "xml") + * 1: Create backup (default: false) * * Return Value: * N/A * * Examples: * [] call ramdb_db_fnc_save (Server or Singleplayer Only) + * ["rdb", true] call ramdb_db_fnc_save (Server or Singleplayer Only) * [] remoteExecCall ["ramdb_db_fnc_save", 2, false] (Multiplayer Only) + * ["rdb", true] remoteExecCall ["ramdb_db_fnc_save", 2, false] (Multiplayer Only) * * Public: Yes */ -"ArmaRAMDb" callExtension ["save", []]; \ No newline at end of file + params [["_type", "xml", [""]], ["_createBackup", false, [false]]]; + +if (_type == "xml") then { + "ArmaRAMDb" callExtension ["save", []]; +} else { + "ArmaRAMDb" callExtension ["saverdb", [_createBackup]]; +}; \ No newline at end of file diff --git a/config.xml b/config.xml index ddf75b2..326796e 100644 --- a/config.xml +++ b/config.xml @@ -1,5 +1,13 @@ - false - false + + true + + true + + true + + 60 + + 10 \ No newline at end of file diff --git a/extension/bin/Release/net8.0/linux-x64/publish/ArmaRAMDb_x64.so b/extension/bin/Release/net8.0/linux-x64/publish/ArmaRAMDb_x64.so index 4e4c400..f285569 100644 Binary files a/extension/bin/Release/net8.0/linux-x64/publish/ArmaRAMDb_x64.so and b/extension/bin/Release/net8.0/linux-x64/publish/ArmaRAMDb_x64.so differ diff --git a/extension/bin/Release/net8.0/win-x64/publish/ArmaRAMDb_x64.dll b/extension/bin/Release/net8.0/win-x64/publish/ArmaRAMDb_x64.dll index 03a2b8f..62c61b0 100644 Binary files a/extension/bin/Release/net8.0/win-x64/publish/ArmaRAMDb_x64.dll and b/extension/bin/Release/net8.0/win-x64/publish/ArmaRAMDb_x64.dll differ diff --git a/extension/src/Main.cs b/extension/src/Main.cs index 8ad92ed..f821349 100644 --- a/extension/src/Main.cs +++ b/extension/src/Main.cs @@ -51,14 +51,47 @@ namespace ArmaRAMDb if (File.Exists(Environment.CurrentDirectory + "\\" + str)) { - List strList = []; - List list = XElement.Load(Environment.CurrentDirectory + "\\" + str).Elements().Select(eintrag => (string)eintrag).ToList(); - if (bool.TryParse(list[0], out bool res0)) - ARDB_CONTEXTLOG = res0; - if (bool.TryParse(list[1], out bool res1)) - ARDB_DEBUG = res1; - - Log($"Config file found! Context Mode: {ARDB_CONTEXTLOG}! Debug Mode: {ARDB_DEBUG}!", "action"); + try + { + var configXml = XElement.Load(Environment.CurrentDirectory + "\\" + str); + List settings = configXml.Elements().Select(element => (string)element).ToList(); + + // Parse existing settings + if (settings.Count >= 2) + { + if (bool.TryParse(settings[0], out bool res0)) + ARDB_CONTEXTLOG = res0; + if (bool.TryParse(settings[1], out bool res1)) + ARDB_DEBUG = res1; + } + + // Parse new backup settings + if (settings.Count >= 5) + { + if (bool.TryParse(settings[2], out bool autoBackup)) + RAMDb.AutoBackupEnabled = autoBackup; + + if (int.TryParse(settings[3], out int backupFreq) && backupFreq > 0) + RAMDb.BackupFrequencyMinutes = backupFreq; + + if (int.TryParse(settings[4], out int maxBackups) && maxBackups > 0) + RAMDb.MaxBackupsToKeep = maxBackups; + } + + Log($"Config file found! Context Mode: {ARDB_CONTEXTLOG}! Debug Mode: {ARDB_DEBUG}! " + + $"Auto Backup: {RAMDb.AutoBackupEnabled} (every {RAMDb.BackupFrequencyMinutes} min, keep {RAMDb.MaxBackupsToKeep})", "action"); + + // Initialize automatic backup if enabled + if (RAMDb.AutoBackupEnabled) + { + RAMDb.InitializeAutoBackup(); + } + } + catch (Exception ex) + { + Log($"Error reading config file: {ex.Message}", "error"); + Log("Default Settings Loaded.", "action"); + } } else { @@ -269,6 +302,14 @@ namespace ArmaRAMDb HandleListSetOperation(args, argc); WriteOutput(output, "Async"); return 200; + case "loadrdb": + LoadRdb(); + WriteOutput(output, "Data loaded"); + return 100; + case "saverdb": + SaveRdb(args); + WriteOutput(output, "Data saved"); + return 100; case "load": LoadData(); WriteOutput(output, "Data loaded"); @@ -284,8 +325,20 @@ namespace ArmaRAMDb case "version": WriteOutput(output, ARDB_VERSION); return 100; + case "listbackups": + string backupsList = HandleListBackupsOperation(); + WriteOutput(output, backupsList); + return 100; + case "restorebackup": + bool success = HandleRestoreBackupOperation(args); + WriteOutput(output, success.ToString().ToLower()); + return 100; + case "deletebackup": + bool deleted = HandleDeleteBackupOperation(args); + WriteOutput(output, deleted.ToString().ToLower()); + return 100; default: - WriteOutput(output, "Available functions: del, get, hdel, hdelid, hget, hgetid, hgetall, hgetallid, hrem, hremid, hset, hsetid, isloaded, listadd, listidx, listrem, listrng, listset, load, save, set, version"); + WriteOutput(output, "Available functions: del, get, hdel, hdelid, hget, hgetid, hgetall, hgetallid, hrem, hremid, hset, hsetid, isloaded, listadd, listidx, listrem, listrng, listset, load, save, set, version, listbackups, restorebackup, deletebackup"); return -1; } } @@ -325,6 +378,27 @@ namespace ArmaRAMDb Marshal.Copy(bytes, 0, (nint)output, bytes.Length); } + private static void SaveRdb(List args) + { + var db = new RAMDb(); + + // Convert string to boolean properly + bool createBackup = args.Count > 0 && + (args[0].Trim('"').Equals("true", StringComparison.CurrentCultureIgnoreCase) || args[0].Trim('"') == "1"); + + db.ExportToRdb(createBackup); + Log($"Data saved to RDB{(createBackup ? " with backup" : "")}", "action"); + } + + private static void LoadRdb() + { + var db = new RAMDb(); + + db.ImportFromRdb(); + ARDB_ISLOADED = true; + Log("Data loaded from RDB", "action"); + } + private static void SaveData() { var db = new RAMDb(); @@ -579,5 +653,81 @@ namespace ArmaRAMDb await KeyValueStore.DeleteAsync(args[0].Trim('"')); }); } + + private static string HandleListBackupsOperation() + { + var db = new RAMDb(); + var backups = db.ListBackups(); + + if (backups.Count > 0) + { + // Format backup list for Arma by joining filenames with proper array syntax + var backupFileNames = backups.Select(Path.GetFileName).ToList(); + var formattedResult = string.Join("\",\"", backupFileNames); + + Log($"Listed {backups.Count} available backups", "action"); + return $"[\"{formattedResult}\"]"; + } + else + { + // Return empty array if no backups exist + Log("No backups available", "action"); + return "[]"; + } + } + + private static bool HandleRestoreBackupOperation(List args) + { + if (args.Count < 1) return false; + + string backupFileName = args[0].Trim('"'); + var db = new RAMDb(); + var backups = db.ListBackups(); + + // Find the full path based on filename + var backupPath = backups.FirstOrDefault(b => Path.GetFileName(b).Equals(backupFileName, StringComparison.OrdinalIgnoreCase)); + + if (backupPath != null) + { + bool success = RAMDb.RestoreFromBackup(backupPath); + Log($"Restore from backup {backupFileName}: {(success ? "successful" : "failed")}", "action"); + return success; + } + + Log($"Backup {backupFileName} not found", "error"); + return false; + } + + private static bool HandleDeleteBackupOperation(List args) + { + if (args.Count < 1) return false; + + string backupFileName = args[0].Trim('"'); + var db = new RAMDb(); + var backups = db.ListBackups(); + + // Find the full path based on filename + var backupPath = backups.FirstOrDefault(b => Path.GetFileName(b).Equals(backupFileName, StringComparison.OrdinalIgnoreCase)); + + if (backupPath != null) + { + try + { + File.Delete(backupPath); + Log($"Deleted backup: {backupFileName}", "action"); + return true; + } + catch (Exception ex) + { + Log($"Failed to delete backup {backupFileName}: {ex.Message}", "error"); + } + } + else + { + Log($"Backup {backupFileName} not found", "error"); + } + + return false; + } } } \ No newline at end of file diff --git a/extension/src/RAMDb.cs b/extension/src/RAMDb.cs index 50a388a..0ba557c 100644 --- a/extension/src/RAMDb.cs +++ b/extension/src/RAMDb.cs @@ -5,20 +5,303 @@ using System.Collections.Concurrent; namespace ArmaRAMDb #pragma warning restore IDE0130 // Namespace does not match folder structure { - internal class RAMDb(string path = RAMDb.DEFAULT_STORAGE_PATH) : IDisposable + internal class RAMDb(string path = RAMDb.DEFAULT_XML_PATH, string rdbPath = RAMDb.DEFAULT_RDB_PATH) : IDisposable { - private const string DEFAULT_STORAGE_PATH = "@ramdb\\ArmaRAMDb.xml"; - private readonly string _storagePath = Path.Combine(Environment.CurrentDirectory, path); + private const string DEFAULT_XML_PATH = "@ramdb\\ArmaRAMDb.xml"; + private const string DEFAULT_RDB_PATH = "@ramdb\\ArmaRAMDb.rdb"; + private readonly string _xmlPath = Path.Combine(Environment.CurrentDirectory, path); + private readonly string _rdbPath = Path.Combine(Environment.CurrentDirectory, rdbPath); private XDocument _document; public static readonly ConcurrentDictionary _keyValues = new(); public static readonly ConcurrentDictionary> _hashTables = new(); public static readonly ConcurrentDictionary> _lists = new(); + // Add these properties to the RAMDb class + public static bool AutoBackupEnabled { get; set; } = false; + public static int BackupFrequencyMinutes { get; set; } = 60; // Default 1 hour + public static int MaxBackupsToKeep { get; set; } = 10; + private static Timer _backupTimer; + + public void ImportFromRdb() + { + try + { + if (File.Exists(_rdbPath)) + { + using var stream = new FileStream(_rdbPath, FileMode.Open); + using var reader = new BinaryReader(stream); + + // Read version (for future compatibility) + int version = reader.ReadInt32(); + if (version != 1) + { + Main.Log($"Unsupported RDB format version: {version}", "warning"); + return; + } + + // Clear existing collections + _keyValues.Clear(); + _hashTables.Clear(); + _lists.Clear(); + + // Read KeyValues + int keyValueCount = reader.ReadInt32(); + for (int i = 0; i < keyValueCount; i++) + { + string key = reader.ReadString(); + string value = reader.ReadString(); + _keyValues.TryAdd(key, value); + Main.Log($"Loaded key-value: {key} = {value[..Math.Min(50, value.Length)]}...", "debug"); + } + + // Read HashTables + int tableCount = reader.ReadInt32(); + for (int i = 0; i < tableCount; i++) + { + string tableName = reader.ReadString(); + Main.Log($"Loading table: {tableName}", "debug"); + + var concurrentDict = new ConcurrentDictionary(); + int entryCount = reader.ReadInt32(); + + for (int j = 0; j < entryCount; j++) + { + string key = reader.ReadString(); + string value = reader.ReadString(); + concurrentDict.TryAdd(key, value); + Main.Log($"Loaded entry: {key} = {value[..Math.Min(50, value.Length)]}...", "debug"); + } + + _hashTables.TryAdd(tableName, concurrentDict); + } + + // Read Lists + int listCount = reader.ReadInt32(); + for (int i = 0; i < listCount; i++) + { + string listName = reader.ReadString(); + Main.Log($"Loading list: {listName}", "debug"); + + var items = new List(); + int itemCount = reader.ReadInt32(); + + for (int j = 0; j < itemCount; j++) + { + string value = reader.ReadString(); + items.Add(value); + Main.Log($"Loaded item: {value[..Math.Min(50, value.Length)]}...", "debug"); + } + + _lists.TryAdd(listName, items); + } + + Main.Log("RDB import complete", "debug"); + } + } + catch (Exception ex) + { + Main.Log($"Error during RDB import: {ex.Message}", "error"); + } + } + + public void ExportToRdb(bool createBackup = false) + { + try + { + // Save to the standard location + Directory.CreateDirectory(Path.GetDirectoryName(_rdbPath)); + + using (var stream = new FileStream(_rdbPath, FileMode.Create)) + using (var writer = new BinaryWriter(stream)) + { + // Add version number + writer.Write(1); // Version 1 of format + + // Write data as before + WriteDataToBinaryWriter(writer); + } + + // Create a backup copy with timestamp if requested + if (createBackup) + { + string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + string backupDirectory = Path.Combine(Path.GetDirectoryName(_rdbPath), "backups"); + string backupFileName = Path.GetFileNameWithoutExtension(_rdbPath) + "_" + timestamp + Path.GetExtension(_rdbPath); + string backupPath = Path.Combine(backupDirectory, backupFileName); + + Directory.CreateDirectory(backupDirectory); + + using (var stream = new FileStream(backupPath, FileMode.Create)) + using (var writer = new BinaryWriter(stream)) + { + // Write the same data to the backup file + WriteDataToBinaryWriter(writer); + } + + Main.Log($"Created backup at: {backupPath}", "debug"); + } + + Main.Log("RDB export complete", "debug"); + } + catch (Exception ex) + { + Main.Log($"Error during RDB export: {ex.Message}", "error"); + } + } + + // Extract the data writing logic to a separate method to avoid code duplication + private static void WriteDataToBinaryWriter(BinaryWriter writer) + { + // Write KeyValues + writer.Write(_keyValues.Count); + foreach (var pair in _keyValues) + { + writer.Write(pair.Key); + writer.Write(pair.Value); + } + + // Write HashTables + writer.Write(_hashTables.Count); + foreach (var table in _hashTables) + { + writer.Write(table.Key); + writer.Write(table.Value.Count); + + foreach (var entry in table.Value) + { + writer.Write(entry.Key); + writer.Write(entry.Value); + } + } + + // Write Lists + writer.Write(_lists.Count); + foreach (var list in _lists) + { + writer.Write(list.Key); + writer.Write(list.Value.Count); + + foreach (var item in list.Value) + { + writer.Write(item); + } + } + } + + // Add a method to list available backups + public List ListBackups() + { + string backupDirectory = Path.Combine(Path.GetDirectoryName(_rdbPath), "backups"); + List backups = []; + + if (Directory.Exists(backupDirectory)) + { + backups = [.. Directory.GetFiles(backupDirectory, "*.rdb").OrderByDescending(file => file)]; + } + + return backups; + } + + // Add a method to restore from a specific backup + public static bool RestoreFromBackup(string backupPath) + { + if (File.Exists(backupPath)) + { + try + { + using var stream = new FileStream(backupPath, FileMode.Open); + using var reader = new BinaryReader(stream); + + // Read version (for future compatibility) + int version = reader.ReadInt32(); + if (version != 1) + { + Main.Log($"Unsupported RDB format version in backup: {version}", "warning"); + return false; + } + + // Clear existing collections + _keyValues.Clear(); + _hashTables.Clear(); + _lists.Clear(); + + // Call a shared method for reading data + ReadDataFromBinaryReader(reader); + + Main.Log($"Restored from backup: {backupPath}", "info"); + return true; + } + catch (Exception ex) + { + Main.Log($"Failed to restore from backup: {ex.Message}", "error"); + } + } + + return false; + } + + // Add a shared method for reading data (similar to WriteDataToBinaryWriter) + private static void ReadDataFromBinaryReader(BinaryReader reader) + { + // Read KeyValues + int keyValueCount = reader.ReadInt32(); + for (int i = 0; i < keyValueCount; i++) + { + string key = reader.ReadString(); + string value = reader.ReadString(); + _keyValues.TryAdd(key, value); + Main.Log($"Loaded key-value: {key} = {value[..Math.Min(50, value.Length)]}...", "debug"); + } + + // Read HashTables + int tableCount = reader.ReadInt32(); + for (int i = 0; i < tableCount; i++) + { + string tableName = reader.ReadString(); + Main.Log($"Loading table: {tableName}", "debug"); + + var concurrentDict = new ConcurrentDictionary(); + int entryCount = reader.ReadInt32(); + + for (int j = 0; j < entryCount; j++) + { + string key = reader.ReadString(); + string value = reader.ReadString(); + concurrentDict.TryAdd(key, value); + Main.Log($"Loaded entry: {key} = {value[..Math.Min(50, value.Length)]}...", "debug"); + } + + _hashTables.TryAdd(tableName, concurrentDict); + } + + // Read Lists + int listCount = reader.ReadInt32(); + for (int i = 0; i < listCount; i++) + { + string listName = reader.ReadString(); + Main.Log($"Loading list: {listName}", "debug"); + + var items = new List(); + int itemCount = reader.ReadInt32(); + + for (int j = 0; j < itemCount; j++) + { + string value = reader.ReadString(); + items.Add(value); + Main.Log($"Loaded item: {value[..Math.Min(50, value.Length)]}...", "debug"); + } + + _lists.TryAdd(listName, items); + } + + Main.Log("RDB import complete", "debug"); + } + public void ImportFromXml() { - if (File.Exists(_storagePath)) + if (File.Exists(_xmlPath)) { - _document = XDocument.Load(_storagePath); + _document = XDocument.Load(_xmlPath); LoadIntoMemory(); } } @@ -101,13 +384,74 @@ namespace ArmaRAMDb private void SaveDocument() { - Directory.CreateDirectory(Path.GetDirectoryName(_storagePath)); - _document.Save(_storagePath); - } + Directory.CreateDirectory(Path.GetDirectoryName(_xmlPath)); + _document.Save(_xmlPath); + } + + // Add a method to start the automatic backup timer + public static void InitializeAutoBackup() + { + if (AutoBackupEnabled) + { + _backupTimer?.Dispose(); + + _backupTimer = new Timer(BackupTimerCallback, null, + TimeSpan.Zero, + TimeSpan.FromMinutes(BackupFrequencyMinutes)); + + Main.Log($"Automatic backup initialized (every {BackupFrequencyMinutes} minutes)", "info"); + } + } + + // Timer callback method + private static void BackupTimerCallback(object state) + { + try + { + // Create a new instance to perform the backup + var db = new RAMDb(); + db.ExportToRdb(true); + + // Manage backup rotation + ManageBackupRotation(); + + Main.Log($"Automatic backup created at {DateTime.Now}", "info"); + } + catch (Exception ex) + { + Main.Log($"Automatic backup failed: {ex.Message}", "error"); + } + } + + // Method to clean up old backups + private static void ManageBackupRotation() + { + try + { + var db = new RAMDb(); + var backups = db.ListBackups(); + + // Keep only the number of backups specified in config + if (backups.Count > MaxBackupsToKeep) + { + for (int i = MaxBackupsToKeep; i < backups.Count; i++) + { + File.Delete(backups[i]); + Main.Log($"Deleted old backup: {backups[i]}", "info"); + } + } + } + catch (Exception ex) + { + Main.Log($"Backup rotation failed: {ex.Message}", "error"); + } + } public void Dispose() { + _backupTimer?.Dispose(); ExportToXml(); + ExportToRdb(createBackup: true); // Create a backup on normal shutdown } } } \ No newline at end of file