feat(backend): integrate SurrealDB storage with embedded RocksDB
Add StorageService backed by SurrealDB using the embedded RocksDB provider that persists to ./data (never in memory). Support an optional remote server via SurrealDb:RemoteUrl for frontend sync. Expose readiness and storage probe endpoints under /api/system and add a helper script to start a local SurrealDB server. Set the backend version to 0.0.1.
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TarninaHub.Models;
|
||||||
|
using TarninaHub.Services;
|
||||||
|
|
||||||
|
namespace TarninaHub.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/system")]
|
||||||
|
public class SystemController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IStorageService _storageService;
|
||||||
|
|
||||||
|
public SystemController(IStorageService storageService)
|
||||||
|
{
|
||||||
|
_storageService = storageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("storage")]
|
||||||
|
public async Task<ActionResult<StorageStatusResponse>> GetStorage(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var info = _storageService.GetInfo();
|
||||||
|
var probeCount = await _storageService.GetProbeCountAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Ok(new StorageStatusResponse(
|
||||||
|
info.Engine,
|
||||||
|
info.DataDirectory,
|
||||||
|
info.Namespace,
|
||||||
|
info.Database,
|
||||||
|
probeCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("storage/probes")]
|
||||||
|
public async Task<ActionResult<StorageProbeResponse>> CreateProbe(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var probe = await _storageService.AddProbeAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Ok(new StorageProbeResponse(probe.Label, probe.CreatedAt));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
namespace TarninaHub.Models;
|
||||||
|
|
||||||
|
public sealed class StorageProbe
|
||||||
|
{
|
||||||
|
public string Label { get; set; } = "probe";
|
||||||
|
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record StorageInfo(
|
||||||
|
string Engine,
|
||||||
|
string DataDirectory,
|
||||||
|
string Namespace,
|
||||||
|
string Database);
|
||||||
|
|
||||||
|
public sealed record StorageStatusResponse(
|
||||||
|
string Engine,
|
||||||
|
string DataDirectory,
|
||||||
|
string Namespace,
|
||||||
|
string Database,
|
||||||
|
int ProbeCount);
|
||||||
|
|
||||||
|
public sealed record StorageProbeResponse(
|
||||||
|
string Label,
|
||||||
|
DateTimeOffset CreatedAt);
|
||||||
@@ -1,12 +1,55 @@
|
|||||||
|
using TarninaHub.Services;
|
||||||
|
using TarninaHub.Settings;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
var surrealSettings = builder.Configuration.GetSection("SurrealDb").Get<SurrealDbSettings>()
|
||||||
|
?? new SurrealDbSettings();
|
||||||
|
|
||||||
|
builder.Services.Configure<SurrealDbSettings>(builder.Configuration.GetSection("SurrealDb"));
|
||||||
|
|
||||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<IStorageService, StorageService>();
|
||||||
|
|
||||||
|
var surrealOptions = new SurrealDbOptions
|
||||||
|
{
|
||||||
|
Endpoint = BuildEndpoint(surrealSettings),
|
||||||
|
Namespace = surrealSettings.Namespace,
|
||||||
|
Database = surrealSettings.Database,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(surrealSettings.RemoteUrl))
|
||||||
|
{
|
||||||
|
// Embedded RocksDB, persisted next to the running app (./data), never in memory.
|
||||||
|
var dataDirectory = Path.Combine(AppContext.BaseDirectory, surrealSettings.DataDirectory);
|
||||||
|
Directory.CreateDirectory(dataDirectory);
|
||||||
|
|
||||||
|
builder.Services
|
||||||
|
.AddSurreal(surrealOptions, ServiceLifetime.Singleton)
|
||||||
|
.AddRocksDbProvider();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Remote SurrealDB server on its own dedicated port (used for sync with the frontend).
|
||||||
|
surrealOptions.Username = surrealSettings.Username;
|
||||||
|
surrealOptions.Password = surrealSettings.Password;
|
||||||
|
|
||||||
|
builder.Services.AddSurreal(surrealOptions, ServiceLifetime.Singleton);
|
||||||
|
}
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
using (var scope = app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var storageService = scope.ServiceProvider.GetRequiredService<IStorageService>();
|
||||||
|
|
||||||
|
await storageService.InitializeAsync(CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
@@ -25,3 +68,15 @@ app.MapControllers();
|
|||||||
app.MapFallbackToFile("index.html");
|
app.MapFallbackToFile("index.html");
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
|
static string BuildEndpoint(SurrealDbSettings settings)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(settings.RemoteUrl))
|
||||||
|
{
|
||||||
|
return settings.RemoteUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dataDirectory = Path.Combine(AppContext.BaseDirectory, settings.DataDirectory);
|
||||||
|
|
||||||
|
return $"rocksdb://{dataDirectory.Replace('\\', '/')}";
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using TarninaHub.Models;
|
||||||
|
|
||||||
|
namespace TarninaHub.Services;
|
||||||
|
|
||||||
|
public interface IStorageService
|
||||||
|
{
|
||||||
|
Task InitializeAsync(CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
StorageInfo GetInfo();
|
||||||
|
|
||||||
|
Task<int> GetProbeCountAsync(CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<StorageProbe> AddProbeAsync(CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using SurrealDb.Net;
|
||||||
|
using TarninaHub.Models;
|
||||||
|
using TarninaHub.Settings;
|
||||||
|
|
||||||
|
namespace TarninaHub.Services;
|
||||||
|
|
||||||
|
public sealed class StorageService : IStorageService
|
||||||
|
{
|
||||||
|
private const string ProbeTable = "storage_probe";
|
||||||
|
|
||||||
|
private readonly ISurrealDbClient _client;
|
||||||
|
private readonly SurrealDbSettings _settings;
|
||||||
|
private readonly string _dataDirectory;
|
||||||
|
|
||||||
|
public StorageService(ISurrealDbClient client, IOptions<SurrealDbSettings> settings)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_settings = settings.Value;
|
||||||
|
_dataDirectory = Path.Combine(AppContext.BaseDirectory, _settings.DataDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InitializeAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _client.RawQuery(
|
||||||
|
$"DEFINE TABLE IF NOT EXISTS {ProbeTable} SCHEMALESS;",
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StorageInfo GetInfo()
|
||||||
|
{
|
||||||
|
var isRemote = !string.IsNullOrWhiteSpace(_settings.RemoteUrl);
|
||||||
|
|
||||||
|
return new StorageInfo(
|
||||||
|
isRemote ? "remote" : "rocksdb",
|
||||||
|
isRemote ? _settings.RemoteUrl! : _dataDirectory,
|
||||||
|
_settings.Namespace,
|
||||||
|
_settings.Database);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetProbeCountAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var probes = await _client.Select<StorageProbe>(ProbeTable, cancellationToken);
|
||||||
|
|
||||||
|
return probes.Count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<StorageProbe> AddProbeAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var probe = new StorageProbe
|
||||||
|
{
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
};
|
||||||
|
|
||||||
|
return _client.Create(ProbeTable, probe, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
namespace TarninaHub.Settings;
|
||||||
|
|
||||||
|
public sealed class SurrealDbSettings
|
||||||
|
{
|
||||||
|
public string Namespace { get; set; } = "hubmi";
|
||||||
|
|
||||||
|
public string Database { get; set; } = "hubmi";
|
||||||
|
|
||||||
|
public string DataDirectory { get; set; } = "data";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When set (e.g. ws://127.0.0.1:8010/rpc), the backend connects to a
|
||||||
|
/// dedicated SurrealDB server instead of the embedded RocksDB engine.
|
||||||
|
/// The server must persist to ./data, not to memory.
|
||||||
|
/// </summary>
|
||||||
|
public string? RemoteUrl { get; set; }
|
||||||
|
|
||||||
|
public string? Username { get; set; }
|
||||||
|
|
||||||
|
public string? Password { get; set; }
|
||||||
|
}
|
||||||
@@ -4,10 +4,14 @@
|
|||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Version>0.0.1</Version>
|
||||||
|
<InformationalVersion>0.0.1</InformationalVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
|
||||||
|
<PackageReference Include="SurrealDb.Embedded.RocksDb" Version="1.0.0" />
|
||||||
|
<PackageReference Include="SurrealDb.Net" Version="1.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -4,5 +4,13 @@
|
|||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"SurrealDb": {
|
||||||
|
"Namespace": "hubmi",
|
||||||
|
"Database": "hubmi",
|
||||||
|
"DataDirectory": "data",
|
||||||
|
"RemoteUrl": "ws://127.0.0.1:8010/rpc",
|
||||||
|
"Username": "root",
|
||||||
|
"Password": "root"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,10 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*",
|
||||||
|
"SurrealDb": {
|
||||||
|
"Namespace": "hubmi",
|
||||||
|
"Database": "hubmi",
|
||||||
|
"DataDirectory": "data"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Uruchamia SurrealDB jako serwer na dedykowanym porcie (tylko dla bazy),
|
||||||
|
z trwałym magazynem RocksDB w katalogu ./data (nie w pamięci).
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Port (domyślnie 8010) służy wyłącznie SurrealDB i jest używany zarówno przez
|
||||||
|
backend (HubMi.Api), jak i przez frontend do synchronizacji (WebSocket /rpc).
|
||||||
|
Dane trwale zapisywane są w backend/data (RocksDB).
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[int]$Port = 8010,
|
||||||
|
[string]$Bind = "127.0.0.1",
|
||||||
|
[string]$Username = "root",
|
||||||
|
[string]$Password = "root",
|
||||||
|
[string]$DataPath = (Join-Path $PSScriptRoot "..\backend\data")
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$surreal = (Get-Command surreal -ErrorAction SilentlyContinue)?.Source
|
||||||
|
if (-not $surreal) {
|
||||||
|
throw "Nie znaleziono 'surreal' w PATH. Zainstaluj SurrealDB: https://surrealdb.com/install"
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedData = [System.IO.Path]::GetFullPath($DataPath)
|
||||||
|
New-Item -ItemType Directory -Force -Path $resolvedData | Out-Null
|
||||||
|
|
||||||
|
Write-Host "SurrealDB: bind $Bind`:$Port, rocksdb:$resolvedData"
|
||||||
|
|
||||||
|
& $surreal start `
|
||||||
|
--user $Username `
|
||||||
|
--pass $Password `
|
||||||
|
--bind "${Bind}:${Port}" `
|
||||||
|
--log info `
|
||||||
|
"rocksdb:$resolvedData"
|
||||||
Reference in New Issue
Block a user