
Some checks failed
Build / build (push) Failing after 1m20s
Added automatic backup system that triggers saves when data is modified. Changes include: - Added tracking of data modifications in BackupSystem.cs - Added debounce timer (5s) to prevent excessive backups during rapid changes - Modified string, list, and hash operations to track data changes - Ensures data is saved shortly after modifications without impacting performance
276 lines
10 KiB
C#
276 lines
10 KiB
C#
using System.Text;
|
|
using System.Collections.Concurrent;
|
|
|
|
namespace Firefly
|
|
{
|
|
public partial class Firefly
|
|
{
|
|
#region Hash Operations
|
|
/// <summary>
|
|
/// Handles the HSET command which sets a field in a hash.
|
|
/// </summary>
|
|
/// <param name="args">Command arguments in format: "key field value"</param>
|
|
/// <returns>1 if the field was added, 0 if it was updated</returns>
|
|
static byte[] HandleHSetCommand(string args)
|
|
{
|
|
string[] parts = SplitRespectingQuotes(args);
|
|
if (parts.Length < 3)
|
|
{
|
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'hset' command\r\n");
|
|
}
|
|
|
|
string key = parts[0];
|
|
string field = parts[1];
|
|
string value = parts.Length == 3 ? parts[2] : string.Join(" ", parts.Skip(2));
|
|
|
|
try
|
|
{
|
|
var hash = GetOrCreateHash(key);
|
|
bool isNewField = hash.TryAdd(field, value);
|
|
|
|
if (!isNewField)
|
|
hash[field] = value;
|
|
|
|
MarkDataAsModified();
|
|
|
|
return Encoding.UTF8.GetBytes($":{(isNewField ? 1 : 0)}\r\n");
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
// Handle the case where the key already exists with a different type
|
|
string? existingType = GetKeyType(key);
|
|
if (existingType != null)
|
|
{
|
|
return Encoding.UTF8.GetBytes($"-ERR key '{key}' already exists as type '{existingType}'\r\n");
|
|
}
|
|
return Encoding.UTF8.GetBytes($"-ERR {ex.Message}\r\n");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the HGET command which retrieves the value of a field in a hash.
|
|
/// </summary>
|
|
/// <param name="args">Command arguments in format: "key field"</param>
|
|
/// <returns>The value of the field, or nil if the field doesn't exist</returns>
|
|
static byte[] HandleHGetCommand(string args)
|
|
{
|
|
string[] parts = SplitRespectingQuotes(args);
|
|
if (parts.Length < 2)
|
|
{
|
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'hget' command\r\n");
|
|
}
|
|
|
|
string key = parts[0];
|
|
string field = parts[1];
|
|
|
|
if (HashStoreGet(key, out ConcurrentDictionary<string, string>? hash) &&
|
|
hash != null && hash.TryGetValue(field, out string? fieldValue))
|
|
{
|
|
return Encoding.UTF8.GetBytes($"+{fieldValue}\r\n");
|
|
}
|
|
|
|
return Encoding.UTF8.GetBytes("$-1\r\n");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the HDEL command which removes a field from a hash.
|
|
/// </summary>
|
|
/// <param name="args">Command arguments in format: "key field"</param>
|
|
/// <returns>1 if the field was removed, 0 if it didn't exist</returns>
|
|
static byte[] HandleHDelCommand(string args)
|
|
{
|
|
string[] parts = SplitRespectingQuotes(args);
|
|
if (parts.Length < 2)
|
|
{
|
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'hdel' command\r\n");
|
|
}
|
|
|
|
string key = parts[0];
|
|
string field = parts[1];
|
|
|
|
if (HashStoreGet(key, out ConcurrentDictionary<string, string>? hash) && hash != null)
|
|
{
|
|
bool removed = hash.TryRemove(field, out _);
|
|
|
|
if (removed)
|
|
MarkDataAsModified();
|
|
|
|
if (hash.IsEmpty)
|
|
HashStoreRemove(key);
|
|
|
|
return Encoding.UTF8.GetBytes($":{(removed ? 1 : 0)}\r\n");
|
|
}
|
|
|
|
return Encoding.UTF8.GetBytes(":0\r\n");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the HEXISTS command which checks if a field exists in a hash.
|
|
/// </summary>
|
|
/// <param name="args">Command arguments in format: "key field"</param>
|
|
/// <returns>1 if the field exists, 0 if it doesn't</returns>
|
|
static byte[] HandleHExistsCommand(string args)
|
|
{
|
|
string[] parts = SplitRespectingQuotes(args);
|
|
if (parts.Length < 2)
|
|
{
|
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'hexists' command\r\n");
|
|
}
|
|
|
|
string key = parts[0];
|
|
string field = parts[1];
|
|
|
|
if (HashStoreGet(key, out ConcurrentDictionary<string, string>? hash) &&
|
|
hash != null && hash.TryGetValue(field, out _))
|
|
{
|
|
return Encoding.UTF8.GetBytes(":1\r\n");
|
|
}
|
|
|
|
return Encoding.UTF8.GetBytes(":0\r\n");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the HGETALL command which retrieves all fields and values in a hash.
|
|
/// </summary>
|
|
/// <param name="args">Command arguments in format: "key"</param>
|
|
/// <returns>All fields and values in the hash, or nil if the hash doesn't exist</returns>
|
|
static byte[] HandleHGetAllCommand(string args)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(args))
|
|
{
|
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'hgetall' command\r\n");
|
|
}
|
|
|
|
string key = args.Trim();
|
|
|
|
if (HashStoreGet(key, out ConcurrentDictionary<string, string>? hash) &&
|
|
hash != null && !hash.IsEmpty)
|
|
{
|
|
var hashSnapshot = hash.ToArray();
|
|
StringBuilder response = new();
|
|
response.Append("*\r\n");
|
|
|
|
foreach (var kvp in hashSnapshot)
|
|
{
|
|
response.Append($"+{kvp.Key}\r\n");
|
|
response.Append($"+{kvp.Value}\r\n");
|
|
}
|
|
|
|
return Encoding.UTF8.GetBytes(response.ToString());
|
|
}
|
|
|
|
return Encoding.UTF8.GetBytes("*\r\n");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the HMSET command which sets multiple fields in a hash.
|
|
/// </summary>
|
|
/// <param name="args">Command arguments in format: "key field value [field value ...]"</param>
|
|
/// <returns>OK on success, error if arguments are invalid</returns>
|
|
static byte[] HandleHMSetCommand(string args)
|
|
{
|
|
string[] parts = SplitRespectingQuotes(args);
|
|
if (parts.Length < 3 || (parts.Length - 1) % 2 != 0)
|
|
{
|
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'hmset' command\r\n");
|
|
}
|
|
|
|
string key = parts[0];
|
|
var hash = GetOrCreateHash(key);
|
|
|
|
try
|
|
{
|
|
for (int i = 1; i < parts.Length; i += 2)
|
|
{
|
|
string field = parts[i];
|
|
if (i + 1 < parts.Length)
|
|
{
|
|
string value = parts[i + 1];
|
|
if (value.StartsWith('"') && value.EndsWith('"') && value.Length >= 2)
|
|
{
|
|
value = value[1..^1];
|
|
}
|
|
hash[field] = value;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Encoding.UTF8.GetBytes($"-ERR internal error: {ex.Message}\r\n");
|
|
}
|
|
|
|
MarkDataAsModified();
|
|
|
|
return Encoding.UTF8.GetBytes("+OK\r\n");
|
|
}
|
|
|
|
#region Hash Store Helpers
|
|
/// <summary>
|
|
/// Gets or creates a hash for a given key.
|
|
/// </summary>
|
|
/// <param name="key">The key to get or create the hash for</param>
|
|
/// <returns>The hash</returns>
|
|
private static ConcurrentDictionary<string, string> GetOrCreateHash(string key)
|
|
{
|
|
int shardIndex = GetShardIndex(key);
|
|
|
|
// If the key doesn't exist in this shard, check if it exists in any other store
|
|
if (!hashStoreShards[shardIndex].ContainsKey(key))
|
|
{
|
|
// Check if key exists in any other store
|
|
if (!EnsureKeyDoesNotExist(key, "hash"))
|
|
{
|
|
throw new InvalidOperationException($"Key '{key}' already exists with a different type");
|
|
}
|
|
}
|
|
|
|
var hash = hashStoreShards[shardIndex].GetOrAdd(key, _ => new ConcurrentDictionary<string, string>());
|
|
|
|
if (hash.IsEmpty)
|
|
MarkDataAsModified();
|
|
|
|
return hash;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a hash exists for a given key.
|
|
/// </summary>
|
|
/// <param name="key">The key to check the hash for</param>
|
|
/// <returns>True if the hash exists, false otherwise</returns>
|
|
private static bool HashStoreExists(string key)
|
|
{
|
|
int shardIndex = GetShardIndex(key);
|
|
return hashStoreShards[shardIndex].ContainsKey(key);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a hash for a given key.
|
|
/// </summary>
|
|
/// <param name="key">The key to get the hash for</param>
|
|
/// <param name="value">The hash</param>
|
|
/// <returns>True if the hash was found, false otherwise</returns>
|
|
private static bool HashStoreGet(string key, out ConcurrentDictionary<string, string>? value)
|
|
{
|
|
int shardIndex = GetShardIndex(key);
|
|
return hashStoreShards[shardIndex].TryGetValue(key, out value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes a hash for a given key.
|
|
/// </summary>
|
|
/// <param name="key">The key to remove the hash for</param>
|
|
/// <returns>True if the hash was removed, false otherwise</returns>
|
|
private static bool HashStoreRemove(string key)
|
|
{
|
|
int shardIndex = GetShardIndex(key);
|
|
bool result = hashStoreShards[shardIndex].TryRemove(key, out _);
|
|
|
|
if (result)
|
|
MarkDataAsModified();
|
|
|
|
return result;
|
|
}
|
|
#endregion
|
|
#endregion
|
|
}
|
|
} |