diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d54f0cf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,23 @@ +**/bin +**/obj +README.md +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.vs +**/.vscode +**/.settings +**/.toolstarget +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/charts +**/docker-compose* +**/compose.y*ml +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/secrets.dev.yaml +**/values.dev.yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fc9c278 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +# syntax=docker/dockerfile:1 + +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build + +COPY . /source + +WORKDIR /source/src/Server + +ARG TARGETARCH + +RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages \ + dotnet publish -a ${TARGETARCH/amd64/x64} --use-current-runtime --self-contained false -o /app + +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS final +WORKDIR /app + +COPY --from=build /app . + +USER $APP_UID + +ENTRYPOINT ["dotnet", "Server.dll"] diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..a59aa22 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,7 @@ +services: + server: + build: + context: . + target: final + ports: + - 6379:6379 \ No newline at end of file diff --git a/src/Server/ArgParser.cs b/src/Server/ArgParser.cs new file mode 100644 index 0000000..fa4b9df --- /dev/null +++ b/src/Server/ArgParser.cs @@ -0,0 +1,20 @@ +public class ArgParser +{ + public int Port { get; private set; } = 6379; + public string? Path { get; private set; } = null; + public int SaveInterval { get; private set; } = 5 * 60_000; + + public ArgParser(string[] args) + { + List arguments = new(args); + + int port = arguments.IndexOf("--port"); + if (port >= 0) Port = int.Parse(arguments[port + 1]); + + int path = arguments.IndexOf("--path"); + if (path >= 0) Path = arguments[path + 1]; + + int save = arguments.IndexOf("--save-interval"); + if (save >= 0) SaveInterval = int.Parse(arguments[save + 1]); + } +} \ No newline at end of file diff --git a/src/Server/Command.cs b/src/Server/Command.cs index d201d67..6a0aafa 100644 --- a/src/Server/Command.cs +++ b/src/Server/Command.cs @@ -135,21 +135,11 @@ public override Item execute(params string[] args) _db.Lock(); var mut = _db.Get(args[0]); - if (mut == null || mut == "f") - _db.Set(args[0], "l"); + if (mut == null) _db.Set(args[0], "locked"); _db.Unlock(); - switch (mut) - { - case null: - return new SimpleString("OK"); - case "f": - return new SimpleString("OK"); - case "l": - return new SimpleError("Key is already locked"); - default: - return new SimpleError("Key is already set"); - } + if (mut == null) return new SimpleString("OK"); + return new SimpleError("Key is already locked"); } } @@ -162,13 +152,7 @@ public override Item execute(params string[] args) if (args.Length != 1) return new SimpleError("Expected 1 argument"); - _db.Lock(); - var mut = _db.Get(args[0]); - if (mut == "l") _db.Set(args[0], "f"); - _db.Unlock(); - - if (mut != "l" && mut != "f") - return new SimpleError("Key is not a lock"); + _db.Del(args[0]); return new SimpleString("OK"); } diff --git a/src/Server/Database.cs b/src/Server/Database.cs index 5c73269..5673a17 100644 --- a/src/Server/Database.cs +++ b/src/Server/Database.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using Shared.Resp; public class Database : IDisposable { @@ -7,6 +8,7 @@ public class Database : IDisposable private readonly object _lock = new(); private readonly object _cleanLock = new(); private CancellationTokenSource _cts = new(); + private Timer? _timer; public Database() { @@ -76,12 +78,86 @@ private void Clean() } } + public Item Encode() + { + var db = new Dictionary(); + var data = new Dictionary(); + var ex = new Dictionary(); + + lock (_lock) + { + foreach (var (key, value) in _data) + data[new BulkString(key)] = new BulkString(value); + + foreach (var (key, value) in _ex) + ex[new BulkString(key)] = new Integer(value.ToUnixTimeMilliseconds()); + } + + db[new BulkString("data")] = new Map(data); + db[new BulkString("ex")] = new Map(ex); + return new Map(db); + } + + public static Database Decode(Item item) + { + if (item is not Map map) + throw new ArgumentException("Expected map"); + + var dict = new Dictionary(); + foreach (var (key, value) in map.Items) + dict[key.ToString() ?? ""] = value; + + if (!dict.TryGetValue("data", out var data) || data is not Map dataMap) + throw new ArgumentException("Expected data map"); + + if (!dict.TryGetValue("ex", out var ex) || ex is not Map exMap) + throw new ArgumentException("Expected ex map"); + + var db = new Database(); + db.Lock(); + + foreach (var (key, value) in dataMap.Items) + db._data[key.ToString() ?? ""] = value.ToString() ?? ""; + + foreach (var (key, value) in exMap.Items) + { + if (value is not Integer i) throw new ArgumentException("Expected integer"); + db._ex[key.ToString() ?? ""] = DateTimeOffset.FromUnixTimeMilliseconds(i.Value); + } + + db.Unlock(); + return db; + } + + public void Save(string path) + { + File.WriteAllText(path + ".tmp", Encode().Encode()); + File.Create(path).Close(); + File.Replace(path + ".tmp", path, null); + } + + public static Database Load(string path) + { + using var file = File.OpenRead(path); + using var reader = new StreamReader(file); + return Decode(Item.Decode(reader)); + } + + public static Database Link(string? path, int saveInterval) + { + if (path == null) return new Database(); + var db = File.Exists(path) ? Load(path) : new Database(); + db._timer = new Timer(_ => db.Save(path), null, 0, saveInterval); + return db; + } + public void Dispose() { _cts.Cancel(); _cts.Dispose(); _data.Clear(); _ex.Clear(); + _timer?.Dispose(); GC.SuppressFinalize(this); } } \ No newline at end of file diff --git a/src/Server/Program.cs b/src/Server/Program.cs index 328c3d6..590f13f 100644 --- a/src/Server/Program.cs +++ b/src/Server/Program.cs @@ -1,7 +1,6 @@ -int port = 6379; +var arguments = new ArgParser(args); -if (args.Length > 0) port = int.Parse(args[0]); +var database = Database.Link(arguments.Path, arguments.SaveInterval); +var server = new Server(arguments.Port, database); -var database = new Database(); -var server = new Server(port, database); server.Run(); \ No newline at end of file diff --git a/tests/Server.UnitTests/Database.cs b/tests/Server.UnitTests/Database.cs index 88a315b..a91843e 100644 --- a/tests/Server.UnitTests/Database.cs +++ b/tests/Server.UnitTests/Database.cs @@ -50,4 +50,32 @@ public void AutoClean() Assert.Equal(1, data?.Count); } + + [Fact] + public void EncodeDecode() + { + using var db1 = new Database(); + db1.Set("key1", "value1"); + db1.Set("key2", "value2", 500); + + using var db2 = Database.Decode(db1.Encode()); + + Assert.Equal(db1.Encode().ToString(), db2.Encode().ToString()); + } + + [Fact] + public void SaveLoad() + { + using var db1 = new Database(); + db1.Set("key1", "value1"); + db1.Set("key2", "value2", 500); + + db1.Save("db.dat"); + + using var db2 = Database.Load("db.dat"); + + Assert.Equal(db1.Encode().ToString(), db2.Encode().ToString()); + + File.Delete("db.dat"); + } } \ No newline at end of file