457 lines
17 KiB
C#
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
|
|
}
|
|
}
|
|
} |