configure grpc

This commit is contained in:
2026-01-17 18:08:16 -05:00
parent fc80e50c26
commit b762139243
27 changed files with 467 additions and 500 deletions

1
AZKiServer/.dockerignore Normal file
View File

@@ -0,0 +1 @@
wwwroot

1
AZKiServer/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
wwwroot/*

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>AZKiServer</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Protobuf Include="Protos\*.proto" GrpcServices="Server" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.64.0" />
<PackageReference Include="MaybeError" Version="1.2.0" />
<PackageReference Include="MongoDB.Driver" Version="3.5.2" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using MongoDB.Bson;
namespace AZKiServer.Models;
public class BsonIdModelBinderProvider : IModelBinderProvider
{
public IModelBinder? GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(ObjectId))
return new BsonIdModelBinder();
return default;
}
}
public class BsonIdModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (value == ValueProviderResult.None)
return Task.CompletedTask;
if (ObjectId.TryParse(value.FirstValue, out var id))
bindingContext.Result = ModelBindingResult.Success(id);
else
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,78 @@
using MaybeError;
using MaybeError.Errors;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System.Text.RegularExpressions;
namespace AZKiServer.Models;
public partial class MediaEntry
{
[BsonId]
public ObjectId Id { get; set; }
public int Version { get; set; }
public MediaType Type { get; set; }
public required string Filepath { get; set; }
public DateTime Date { get; set; }
public int CameraId { get; set; }
public static Maybe<MediaEntry> Parse(string relativePath)
{
var filename = Path.GetFileName(relativePath);
var match = FileParser().Match(filename);
if (!match.Success)
return new Error("Failed to parse file name");
try
{
var src = match.Groups["src"];
var cam = match.Groups["cam"];
var date = match.Groups["date"];
var ext = match.Groups["ext"];
return new MediaEntry
{
CameraId = int.Parse(cam.Value),
Filepath = relativePath,
Date = ParseDate(date.Value),
Type = ext.Value switch
{
"mp4" => MediaType.Video,
_ => MediaType.Image
}
};
}catch(Exception ex)
{
return ex;
}
}
private static DateTime ParseDate(string dateString)
{
var year = dateString[0..4];
var month = dateString[4..6];
var day = dateString[6..8];
var hour = dateString[8..10];
var minute = dateString[10..12];
var sec = dateString[12..];
return new DateTime(int.Parse(year), int.Parse(month), int.Parse(day), int.Parse(hour), int.Parse(minute), int.Parse(sec), DateTimeKind.Local);
}
[GeneratedRegex("(?'src'.+)_(?'cam'\\d+)_(?'date'\\d+).(?'ext'\\w+)")]
private static partial Regex FileParser();
}
[Flags]
public enum MediaType
{
None = 0,
Video = 1,
Image = 2,
All = Video | Image
}

45
AZKiServer/Program.cs Normal file
View File

@@ -0,0 +1,45 @@
using AZKiServer.Models;
using AZKiServer.Services;
using MongoDB.Driver;
var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;
var dbString = config["DB_STRING"];
var dbClient = new MongoClient(dbString);
var db = dbClient.GetDatabase("AZKi");
// Add services to the container.
builder.Services.AddSingleton(dbClient);
builder.Services.AddSingleton<IMongoDatabase>(db);
builder.Services.AddGrpc();
builder.Services.AddControllers(opt => opt.ModelBinderProviders.Add(new BsonIdModelBinderProvider()));
builder.Services.AddHostedService<FileScannerService>();
builder.Services.AddTransient<MediaService>();
builder.Services.AddCors(o =>
{
o.AddPolicy("AllowAll", p =>
{
p.AllowAnyOrigin();
p.AllowAnyMethod();
p.AllowAnyHeader();
});
o.AddPolicy("RPC", p =>
{
p.AllowAnyMethod();
p.AllowAnyHeader();
p.WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding", "Grpc-Accept-Encoding");
p.AllowAnyOrigin();
});
});
var app = builder.Build();
// Configure the HTTP request pipeline.
//app.MapGrpcService<GreeterService>();
app.MapFallbackToFile("index.html");
app.Run();

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5177",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7231;http://localhost:5177",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,12 @@
syntax = "proto3";
option csharp_namespace = "AZKiServer.RPC";
package azki;
import "google/protobuf/empty.proto";
import "Protos/types.proto";
service AZKi{
rpc GetMediaEntriesInRange(MediaRangeRequest) returns (MediaList);
}

View File

@@ -0,0 +1,33 @@
syntax = "proto3";
option csharp_namespace = "AZKiServer.RPC";
package azki;
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
enum MediaType {
None = 0;
Image = 1;
Video = 2;
All = 3;
}
message MediaList {
repeated MediaEntry entries = 1;
}
message MediaRangeRequest{
MediaType type = 1;
google.protobuf.Timestamp from = 2;
google.protobuf.Timestamp to = 3;
}
message MediaEntry {
int32 version = 1;
string id = 2;
MediaType type = 3;
string filePath = 4;
int32 cameraId = 5;
google.protobuf.Timestamp date = 6;
}

View File

@@ -0,0 +1,41 @@
namespace AZKiServer.RPC;
public static class ConversionExtensions
{
public static Models.MediaType FromRpc(this MediaType type)
{
return type switch
{
MediaType.None => Models.MediaType.None,
MediaType.Image => Models.MediaType.Image,
MediaType.Video => Models.MediaType.Video,
MediaType.All => Models.MediaType.All,
_ => throw new NotSupportedException()
};
}
public static MediaType ToRpc(this Models.MediaType type)
{
return type switch
{
Models.MediaType.None => MediaType.None,
Models.MediaType.Image => MediaType.Image,
Models.MediaType.Video => MediaType.Video,
Models.MediaType.All => MediaType.All,
_ => throw new NotSupportedException()
};
}
public static MediaEntry ToRpc(this Models.MediaEntry entry)
{
return new MediaEntry
{
Version = entry.Version,
CameraId = entry.CameraId,
Date = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(entry.Date),
FilePath = entry.Filepath,
Id = entry.Id.ToString(),
Type = entry.Type.ToRpc()
};
}
}

View File

@@ -0,0 +1,20 @@
using AZKiServer.Models;
using AZKiServer.RPC;
using Google.Protobuf;
using Grpc.Core;
namespace AZKiServer.Services;
public class AZKiRpcService(MediaService mediaService) : RPC.AZKi.AZKiBase
{
public override async Task<MediaList> GetMediaEntriesInRange(MediaRangeRequest request, ServerCallContext context)
{
var from = request.From.ToDateTime();
var to = request.To.ToDateTime();
var items = await mediaService.GetEntriesInRangeAsync(request.Type.FromRpc(), from, to);
var result = new MediaList();
result.Entries.AddRange(items.Select(e => e.ToRpc()));
return result;
}
}

View File

@@ -0,0 +1,55 @@
using AZKiServer.Models;
namespace AZKiServer.Services;
public class FileScannerService(MediaService mediaService, IConfiguration config, ILogger<FileScannerService> logger) : IHostedService, IDisposable
{
private Timer? _timer;
public void Dispose()
{
_timer?.Dispose();
}
public Task StartAsync(CancellationToken cancellationToken)
{
var path = config["SCAN_PATH"];
if (string.IsNullOrWhiteSpace(path))
return Task.CompletedTask;
_timer = new Timer((_) =>
{
ScanFilesAsync(path).Wait();
}, null, TimeSpan.FromMinutes(1), TimeSpan.FromHours(1));
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Dispose();
return Task.CompletedTask;
}
private async Task ScanFilesAsync(string path)
{
try
{
var files = Directory.GetFiles(path, "*", SearchOption.AllDirectories);
var existingFiles = await mediaService.GetExistingFilePathsAsync();
var entries = new List<MediaEntry>();
foreach (var filePath in files)
{
var relativePath = Path.GetRelativePath(path, filePath);
if (existingFiles.Contains(relativePath))
continue;
entries.Add(MediaEntry.Parse(relativePath));
}
await mediaService.AddMediaBulkAsync(entries);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to read directory contents");
}
}
}

View File

@@ -0,0 +1,44 @@
using AZKiServer.Models;
using MongoDB.Driver;
using System.Collections.Frozen;
namespace AZKiServer.Services;
public class MediaService(IMongoDatabase db)
{
public readonly IMongoCollection<MediaEntry> _entries = db.GetCollection<MediaEntry>("media");
public async Task<FrozenSet<string>> GetExistingFilePathsAsync(CancellationToken cancellationToken = default)
{
var files = await _entries.Find("{}").Project(m => m.Filepath).ToListAsync(cancellationToken);
return files.ToFrozenSet();
}
public async Task AddMediaBulkAsync(List<MediaEntry> entries, CancellationToken cancellationToken = default)
{
await _entries.InsertManyAsync(entries, cancellationToken: cancellationToken);
}
public async Task<List<MediaEntry>> GetEntriesInRangeAsync(MediaType mediaType, DateTime from, DateTime to)
{
var filter = Builders<MediaEntry>.Filter
.And([
Builders<MediaEntry>.Filter.BitsAnySet(m => m.Type, (long)mediaType),
Builders<MediaEntry>.Filter.Gte(m => m.Date, from),
Builders<MediaEntry>.Filter.Lte(m => m.Date, to),
]);
return _entries.Find(filter).ToList();
}
public class IndexCreation : BackgroundService
{
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,102 @@
namespace AZKiServer.Utilz;
public enum DateInterval
{
Daily,
Weekly,
Monthly,
Yearly,
All,
}
public static class DateUtils
{
public static TimeSpan ToUnixTime(this DateTime date)
{
return date - DateTime.UnixEpoch;
}
public static DateTime SnapToYear(this DateTime date)
{
return new DateTime(date.Year, 1, 1, 0, 0, 0, date.Kind);
}
public static DateTimeOffset SnapToYear(this DateTimeOffset date)
{
return new DateTimeOffset(date.Year, 1, 1, 0, 0, 0, date.Offset);
}
public static DateTime SnapToMonth(this DateTime date)
{
return new DateTime(date.Year, date.Month, 1, 0, 0, 0, date.Kind);
}
public static DateTimeOffset SnapToMonth(this DateTimeOffset date)
{
return new DateTimeOffset(date.Year, date.Month, 1, 0, 0, 0, date.Offset);
}
public static DateTime SnapToWeek(this DateTime date)
{
var d = new DateTime(date.Year, date.Month, date.Day, 0, 0, 0, date.Kind);
if (date.DayOfWeek == DayOfWeek.Sunday)
return d;
else
return d.AddDays(-(int)date.DayOfWeek);
}
public static DateTimeOffset SnapToWeek(this DateTimeOffset date)
{
var d = new DateTimeOffset(date.Year, date.Month, date.Day, 0, 0, 0, date.Offset);
if (date.DayOfWeek == DayOfWeek.Sunday)
return d;
else
return d.AddDays(-(int)date.DayOfWeek);
}
public static (DateTime from, DateTime to) ToInterval(this DateTime date, DateInterval internval)
{
return internval switch
{
DateInterval.Daily => (date, date.AddDays(1)),
DateInterval.Weekly => (date, date.AddDays(7)),
DateInterval.Monthly => (date, date.AddMonths(1)),
DateInterval.Yearly => (date, date.AddYears(1)),
DateInterval.All => (DateTime.MinValue, DateTime.MaxValue),
_ => throw new InvalidOperationException(),
};
}
public static (DateTimeOffset from, DateTimeOffset to) ToInterval(this DateTimeOffset date, DateInterval internval)
{
return internval switch
{
DateInterval.Daily => (date, date.AddDays(1)),
DateInterval.Weekly => (date, date.AddDays(7)),
DateInterval.Monthly => (date, date.AddMonths(1)),
DateInterval.Yearly => (date, date.AddYears(1)),
DateInterval.All => (DateTime.MinValue, DateTime.MaxValue),
_ => throw new InvalidOperationException(),
};
}
public static TimeSpan Days(this int days) => TimeSpan.FromDays(days);
public static TimeSpan Days(this float days) => TimeSpan.FromDays(days);
public static TimeSpan Hours(this int hours) => TimeSpan.FromHours(hours);
public static TimeSpan Hours(this float hours) => TimeSpan.FromHours(hours);
public static TimeSpan Minutes(this int minutes) => TimeSpan.FromMinutes(minutes);
public static TimeSpan Minutes(this float minutes) => TimeSpan.FromMinutes(minutes);
public static TimeSpan Seconds(this int seconds) => TimeSpan.FromSeconds(seconds);
public static TimeSpan Seconds(this float seconds) => TimeSpan.FromSeconds(seconds);
public static TimeSpan Miliseconds(this int miliseconds) => TimeSpan.FromMilliseconds(miliseconds);
public static TimeSpan Miliseconds(this float miliseconds) => TimeSpan.FromMilliseconds(miliseconds);
}

View File

@@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"SCAN_LOCATION": "O:/cctv",
"DB_STRING": "mongodb://NinoIna:27017"
}

View File

@@ -0,0 +1,14 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http2"
}
}
}