configure grpc
This commit is contained in:
1
AZKiServer/.dockerignore
Normal file
1
AZKiServer/.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
wwwroot
|
||||
1
AZKiServer/.gitignore
vendored
Normal file
1
AZKiServer/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
wwwroot/*
|
||||
20
AZKiServer/AZKi Server.csproj
Normal file
20
AZKiServer/AZKi Server.csproj
Normal 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>
|
||||
32
AZKiServer/Models/BsonIdModelBinderProvider.cs
Normal file
32
AZKiServer/Models/BsonIdModelBinderProvider.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
78
AZKiServer/Models/MediaEntry.cs
Normal file
78
AZKiServer/Models/MediaEntry.cs
Normal 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
45
AZKiServer/Program.cs
Normal 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();
|
||||
23
AZKiServer/Properties/launchSettings.json
Normal file
23
AZKiServer/Properties/launchSettings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
AZKiServer/Protos/azki.proto
Normal file
12
AZKiServer/Protos/azki.proto
Normal 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);
|
||||
}
|
||||
33
AZKiServer/Protos/types.proto
Normal file
33
AZKiServer/Protos/types.proto
Normal 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;
|
||||
}
|
||||
41
AZKiServer/RPC/ConversionExtensions.cs
Normal file
41
AZKiServer/RPC/ConversionExtensions.cs
Normal 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()
|
||||
};
|
||||
}
|
||||
}
|
||||
20
AZKiServer/Services/AZKiRpcService.cs
Normal file
20
AZKiServer/Services/AZKiRpcService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
55
AZKiServer/Services/FileScannerService.cs
Normal file
55
AZKiServer/Services/FileScannerService.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
44
AZKiServer/Services/MediaService.cs
Normal file
44
AZKiServer/Services/MediaService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
102
AZKiServer/Utilz/DateUtils.cs
Normal file
102
AZKiServer/Utilz/DateUtils.cs
Normal 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);
|
||||
}
|
||||
10
AZKiServer/appsettings.Development.json
Normal file
10
AZKiServer/appsettings.Development.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"SCAN_LOCATION": "O:/cctv",
|
||||
"DB_STRING": "mongodb://NinoIna:27017"
|
||||
}
|
||||
14
AZKiServer/appsettings.json
Normal file
14
AZKiServer/appsettings.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Kestrel": {
|
||||
"EndpointDefaults": {
|
||||
"Protocols": "Http2"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user