ramdb/extension/src/RAMDb.cs
2025-03-21 13:04:47 -05:00

457 lines
17 KiB
C#

using System.Xml.Linq;
using System.Collections.Concurrent;
#pragma warning disable IDE0130 // Namespace does not match folder structure
namespace ArmaRAMDb
#pragma warning restore IDE0130 // Namespace does not match folder structure
{
internal class RAMDb(string path = RAMDb.DEFAULT_XML_PATH, string rdbPath = RAMDb.DEFAULT_RDB_PATH) : IDisposable
{
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<string, string> _keyValues = new();
public static readonly ConcurrentDictionary<string, ConcurrentDictionary<string, string>> _hashTables = new();
public static readonly ConcurrentDictionary<string, List<string>> _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<string, string>();
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<string>();
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<string> ListBackups()
{
string backupDirectory = Path.Combine(Path.GetDirectoryName(_rdbPath), "backups");
List<string> 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<string, string>();
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<string>();
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(_xmlPath))
{
_document = XDocument.Load(_xmlPath);
LoadIntoMemory();
}
}
public void ExportToXml()
{
_document = new XDocument(
new XElement("ArmaRAMDb",
new XElement("KeyValues",
from pair in _keyValues
select new XElement("Entry",
new XAttribute("Key", pair.Key),
new XAttribute("Value", pair.Value))),
new XElement("HashTables",
from table in _hashTables
select new XElement("Table",
new XAttribute("Name", table.Key),
from entry in table.Value
select new XElement("Entry",
new XAttribute("Key", entry.Key),
new XAttribute("Value", entry.Value)))),
new XElement("Lists",
from list in _lists
select new XElement("List",
new XAttribute("Name", list.Key),
from item in list.Value
select new XElement("Item", item)))
));
SaveDocument();
}
private void LoadIntoMemory()
{
Main.Log("Starting XML import", "debug");
foreach (var entry in _document.Root.Element("KeyValues").Elements("Entry"))
{
var key = entry.Attribute("Key").Value;
var value = entry.Attribute("Value").Value;
_keyValues.TryAdd(key, value);
Main.Log($"Loaded key-value: {key} = {value[..Math.Min(50, value.Length)]}...", "debug");
}
foreach (var table in _document.Root.Element("HashTables").Elements("Table"))
{
var tableName = table.Attribute("Name").Value;
Main.Log($"Loading table: {tableName}", "debug");
var concurrentDict = new ConcurrentDictionary<string, string>();
foreach (var entry in table.Elements("Entry"))
{
var key = entry.Attribute("Key").Value;
var value = entry.Attribute("Value").Value;
concurrentDict.TryAdd(key, value);
Main.Log($"Loaded entry: {key} = {value[..Math.Min(50, value.Length)]}...", "debug");
}
_hashTables.TryAdd(tableName, concurrentDict);
}
foreach (var list in _document.Root.Element("Lists").Elements("List"))
{
var listName = list.Attribute("Name").Value;
Main.Log($"Loading list: {listName}", "debug");
var items = new List<string>();
foreach (var item in list.Elements("Item"))
{
var value = item.Value;
items.Add(value);
Main.Log($"Loaded item: {value[..Math.Min(50, value.Length)]}...", "debug");
}
_lists.TryAdd(listName, items);
}
Main.Log("XML import complete", "debug");
}
private void SaveDocument()
{
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
}
}
}