diff --git a/Rhea/Controllers/MetaController.cs b/Rhea/Controllers/MetaController.cs
new file mode 100644
index 0000000..ca20e99
--- /dev/null
+++ b/Rhea/Controllers/MetaController.cs
@@ -0,0 +1,17 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Rhea.Controllers;
+
+[ApiController]
+public class MetaController(RheaConfiguration cfg) : ControllerBase
+{
+ [HttpGet("/nix-cache-info")]
+ public async Task<string> GetNixCacheInfo()
+ {
+ return $"""
+ StoreDir: {cfg.StorePath}
+ WantMassQuery: 1
+ Priority: 5
+ """;
+ }
+}
\ No newline at end of file
diff --git a/Rhea/Controllers/NarController.cs b/Rhea/Controllers/NarController.cs
new file mode 100644
index 0000000..7ba44ba
--- /dev/null
+++ b/Rhea/Controllers/NarController.cs
@@ -0,0 +1,20 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Rhea.Controllers;
+
+[ApiController]
+public class NarController(RheaConfiguration cfg, StoreMetaService storeSvc) : ControllerBase
+{
+ [HttpHead("/nar/{hash}.nar.zst")]
+ public async Task CheckNixNarExists(string hash)
+ {
+ var path = await storeSvc.GetStorePathByBaseName(hash);
+ Response.StatusCode = string.IsNullOrWhiteSpace(path) ? 404 : 200;
+ }
+
+ [HttpGet("/nar/{hash}.nar.zst")]
+ public async Task<string> GetNixCacheInfo()
+ {
+ return "";
+ }
+}
\ No newline at end of file
diff --git a/Rhea/Controllers/NarInfoController.cs b/Rhea/Controllers/NarInfoController.cs
new file mode 100644
index 0000000..b81b655
--- /dev/null
+++ b/Rhea/Controllers/NarInfoController.cs
@@ -0,0 +1,32 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Rhea.Controllers;
+
+[ApiController]
+public class NarInfoController(RheaConfiguration cfg, StoreMetaService storeSvc) : ControllerBase
+{
+ [HttpHead("/{hash}.narinfo")]
+ public async Task CheckNixNarExists(string hash)
+ {
+ var path = await storeSvc.GetStorePathByBaseName(hash);
+ Response.StatusCode = string.IsNullOrWhiteSpace(path) ? 404 : 200;
+ }
+
+ [HttpGet("/{hash}.narinfo")]
+ public async Task<string> GetNixNarInfo(string hash)
+ {
+ var path = await storeSvc.GetStorePathByBaseName(hash);
+
+ return $"""
+ StorePath: {cfg.StorePath}/{path}
+ URL: nar/09d83cyl9dlfkkbspkgkk7bfydj3mvw6r1x98kvc2v8wl2xd8ldy.nar.zst
+ Compression: zstd
+ FileHash: sha256:0yzqbk88qqh0c7dswwcm27y1pxs5a83hga81vjmcs9d331psv1g8
+ FileSize: 48255244
+ NarHash: sha256:09d83cyl9dlfkkbspkgkk7bfydj3mvw6r1x98kvc2v8wl2xd8ldy
+ NarSize: 209581048
+ Sig: cache.nixos.org-1:sRYhPUkDDRCgZDlgPMvXYEgYvX3GF24wT0v6HOUrtB/4g5lZffgRksiSmsth2wKRPlMijKP/yoDkG4NZDJfJCA==
+ CA: fixed:r:sha256:09d83cyl9dlfkkbspkgkk7bfydj3mvw6r1x98kvc2v8wl2xd8ldy
+ """;
+ }
+}
\ No newline at end of file
diff --git a/Rhea/Program.cs b/Rhea/Program.cs
new file mode 100644
index 0000000..a7310b4
--- /dev/null
+++ b/Rhea/Program.cs
@@ -0,0 +1,67 @@
+using System.Diagnostics;
+using ArcaneLibs;
+using ArcaneLibs.Extensions;
+using ArcaneLibs.Extensions.Streams;
+using Rhea;
+using Rhea.Nar;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+
+builder.Services.AddControllers();
+// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
+builder.Services.AddOpenApi();
+builder.Services.AddSingleton<RheaConfiguration>();
+builder.Services.AddSingleton<StoreMetaService>();
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.MapOpenApi();
+}
+
+app.UseAuthorization();
+
+app.MapControllers();
+
+new NarWriter().GetHeader().ToArray().HexDump(64);
+
+
+foreach (var srcPath in (string[])["/etc/nix/nix.conf", FileUtils.GetBinDir() + "/Rhea.runtimeconfig.json"])
+{
+ Console.WriteLine($"Original {srcPath}:");
+
+ var pe = new ProcessStartInfo("nix", ["nar", "dump-path", srcPath])
+ {
+ RedirectStandardOutput = true
+ };
+ var origBuf = new List<byte>();
+ var pr = Process.Start(pe);
+ int b;
+ while ((b = pr.StandardOutput.BaseStream.ReadByte()) != -1) origBuf.Add((byte)b);
+ origBuf.ToArray().HexDump(64);
+
+ Console.WriteLine($"\nNew {srcPath}:");
+ List<byte> tbuf = [];
+ List<byte> buf = [];
+ foreach (var chunk in new NarWriter().GetNarStreamContents(srcPath))
+ {
+ tbuf.AddRange(chunk);
+ buf.AddRange(chunk);
+ while (buf.Count > 64)
+ {
+ byte[] data = buf[..64].ToArray();
+ buf = buf[64..];
+ data.HexDump(64);
+ }
+ }
+
+ buf.ToArray().HexDump(64);
+
+ Console.WriteLine("Is equal: " + origBuf.SequenceEqual(tbuf));
+}
+
+// app.Run();
\ No newline at end of file
diff --git a/Rhea/Properties/launchSettings.json b/Rhea/Properties/launchSettings.json
new file mode 100644
index 0000000..20f400a
--- /dev/null
+++ b/Rhea/Properties/launchSettings.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5087",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/Rhea/Rhea.csproj b/Rhea/Rhea.csproj
new file mode 100644
index 0000000..24387ad
--- /dev/null
+++ b/Rhea/Rhea.csproj
@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFramework>net10.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ <ImplicitUsings>enable</ImplicitUsings>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="ArcaneLibs" Version="1.0.1-preview.20260812-173005" />
+ <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.9"/>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Rhea.Nar\Rhea.Nar.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Rhea/Rhea.http b/Rhea/Rhea.http
new file mode 100644
index 0000000..5b6e20f
--- /dev/null
+++ b/Rhea/Rhea.http
@@ -0,0 +1,6 @@
+@NixCached_HostAddress = http://localhost:5087
+
+GET {{NixCached_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###
diff --git a/Rhea/RheaConfiguration.cs b/Rhea/RheaConfiguration.cs
new file mode 100644
index 0000000..8b18fc7
--- /dev/null
+++ b/Rhea/RheaConfiguration.cs
@@ -0,0 +1,11 @@
+namespace Rhea;
+
+public class RheaConfiguration
+{
+ public RheaConfiguration(IConfiguration config)
+ {
+ config.GetRequiredSection("Rhea").Bind(this);
+ }
+
+ public string StorePath { get; set; }
+}
\ No newline at end of file
diff --git a/Rhea/StoreMetaService.cs b/Rhea/StoreMetaService.cs
new file mode 100644
index 0000000..6b92cd6
--- /dev/null
+++ b/Rhea/StoreMetaService.cs
@@ -0,0 +1,22 @@
+using ArcaneLibs.Collections;
+
+namespace Rhea;
+
+public class StoreMetaService(RheaConfiguration cfg)
+{
+ private LruCache<string> _storePathsByBaseName = new(10000);
+
+ public async Task<string?> GetStorePathByBaseName(string baseName)
+ {
+ return await _storePathsByBaseName.GetOrAddAsync(baseName, async () =>
+ {
+ foreach (var path in Directory.EnumerateFileSystemEntries(cfg.StorePath))
+ {
+ Console.WriteLine(path[(cfg.StorePath.Length + 1)..]);
+ if (path.StartsWith(baseName + '-')) return path;
+ }
+
+ return "";
+ });
+ }
+}
\ No newline at end of file
diff --git a/Rhea/appsettings.Development.json b/Rhea/appsettings.Development.json
new file mode 100644
index 0000000..7ccad18
--- /dev/null
+++ b/Rhea/appsettings.Development.json
@@ -0,0 +1,11 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+// "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "Rhea": {
+ "StorePath": "/nix/store"
+ }
+}
diff --git a/Rhea/appsettings.json b/Rhea/appsettings.json
new file mode 100644
index 0000000..c236d5a
--- /dev/null
+++ b/Rhea/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+// "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
|