decouple fileservice to be able to run standalone

This commit is contained in:
Stanley Dimant
2022-08-23 02:28:04 +02:00
parent bdfe51c15c
commit a63174009f
11 changed files with 297 additions and 76 deletions

View File

@@ -2,13 +2,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Google.Protobuf;
using MareSynchronos.API; using MareSynchronos.API;
using MareSynchronosShared.Authentication; using MareSynchronosShared.Authentication;
using MareSynchronosShared.Metrics;
using MareSynchronosShared.Models; using MareSynchronosShared.Models;
using MareSynchronosShared.Protos; using MareSynchronosShared.Protos;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@@ -20,8 +18,6 @@ namespace MareSynchronosServer.Hubs
{ {
public partial class MareHub public partial class MareHub
{ {
private string BasePath => _configuration["CacheDirectory"];
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
[HubMethodName(Api.SendFileAbortUpload)] [HubMethodName(Api.SendFileAbortUpload)]
public async Task AbortUpload() public async Task AbortUpload()
@@ -40,20 +36,9 @@ namespace MareSynchronosServer.Hubs
_logger.LogInformation("User {AuthenticatedUserId} deleted all their files", AuthenticatedUserId); _logger.LogInformation("User {AuthenticatedUserId} deleted all their files", AuthenticatedUserId);
var ownFiles = await _dbContext.Files.Where(f => f.Uploaded && f.Uploader.UID == AuthenticatedUserId).ToListAsync().ConfigureAwait(false); var ownFiles = await _dbContext.Files.Where(f => f.Uploaded && f.Uploader.UID == AuthenticatedUserId).ToListAsync().ConfigureAwait(false);
foreach (var file in ownFiles) var request = new DeleteFilesRequest();
{ request.Hash.AddRange(ownFiles.Select(f => f.Hash));
var fi = new FileInfo(Path.Combine(BasePath, file.Hash)); _ = await _fileServiceClient.DeleteFilesAsync(request).ConfigureAwait(false);
if (fi.Exists)
{
await _metricsClient.DecGaugeAsync(new GaugeRequest()
{GaugeName = MetricsAPI.GaugeFilesTotalSize, Value = fi.Length}).ConfigureAwait(false);
await _metricsClient.DecGaugeAsync(new GaugeRequest()
{ GaugeName = MetricsAPI.GaugeFilesTotal, Value = 1}).ConfigureAwait(false);
fi.Delete();
}
}
_dbContext.Files.RemoveRange(ownFiles);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
} }
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)] [Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
@@ -64,37 +49,24 @@ namespace MareSynchronosServer.Hubs
var forbiddenFiles = await _dbContext.ForbiddenUploadEntries. var forbiddenFiles = await _dbContext.ForbiddenUploadEntries.
Where(f => hashes.Contains(f.Hash)).ToListAsync().ConfigureAwait(false); Where(f => hashes.Contains(f.Hash)).ToListAsync().ConfigureAwait(false);
List<DownloadFileDto> response = new(); List<DownloadFileDto> response = new();
foreach (var hash in hashes)
{
var fileInfo = new FileInfo(Path.Combine(BasePath, hash));
long fileSize = 0;
try
{
fileSize = fileInfo.Length;
}
catch
{
// file doesn't exist anymore
}
var forbiddenFile = forbiddenFiles.SingleOrDefault(f => f.Hash == hash); FileSizeRequest request = new FileSizeRequest();
var downloadFile = allFiles.SingleOrDefault(f => f.Hash == hash); var grpcResponse = await _fileServiceClient.GetFileSizesAsync(request).ConfigureAwait(false);
foreach (var hash in grpcResponse.HashToFileSize)
{
var forbiddenFile = forbiddenFiles.SingleOrDefault(f => f.Hash == hash.Key);
var downloadFile = allFiles.SingleOrDefault(f => f.Hash == hash.Key);
response.Add(new DownloadFileDto response.Add(new DownloadFileDto
{ {
FileExists = fileInfo.Exists, FileExists = hash.Value > 0,
ForbiddenBy = forbiddenFile?.ForbiddenBy ?? string.Empty, ForbiddenBy = forbiddenFile?.ForbiddenBy ?? string.Empty,
IsForbidden = forbiddenFile != null, IsForbidden = forbiddenFile != null,
Hash = hash, Hash = hash.Key,
Size = fileSize, Size = hash.Value,
Url = _configuration["CdnFullUrl"] + hash.ToUpperInvariant() Url = _configuration["CdnFullUrl"] + hash.Key.ToUpperInvariant()
}); });
if (!fileInfo.Exists && downloadFile != null)
{
_dbContext.Files.Remove(downloadFile);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
}
} }
return response; return response;
@@ -169,8 +141,8 @@ namespace MareSynchronosServer.Hubs
if (relatedFile == null) return; if (relatedFile == null) return;
var forbiddenFile = _dbContext.ForbiddenUploadEntries.SingleOrDefault(f => f.Hash == hash); var forbiddenFile = _dbContext.ForbiddenUploadEntries.SingleOrDefault(f => f.Hash == hash);
if (forbiddenFile != null) return; if (forbiddenFile != null) return;
var finalFileName = Path.Combine(BasePath, hash);
var tempFileName = finalFileName + ".tmp"; var tempFileName = Path.GetTempFileName();
using var fileStream = new FileStream(tempFileName, FileMode.OpenOrCreate); using var fileStream = new FileStream(tempFileName, FileMode.OpenOrCreate);
long length = 0; long length = 0;
try try
@@ -223,17 +195,10 @@ namespace MareSynchronosServer.Hubs
return; return;
} }
File.Move(tempFileName, finalFileName, true); UploadFileRequest req = new();
relatedFile = _dbContext.Files.Single(f => f.Hash == hash); req.FileData = ByteString.CopyFrom(await File.ReadAllBytesAsync(tempFileName).ConfigureAwait(false));
relatedFile.Uploaded = true; File.Delete(tempFileName);
_ = await _fileServiceClient.UploadFileAsync(req).ConfigureAwait(false);
await _metricsClient.IncGaugeAsync(new GaugeRequest()
{ GaugeName = MetricsAPI.GaugeFilesTotalSize, Value = length }).ConfigureAwait(false);
await _metricsClient.IncGaugeAsync(new GaugeRequest()
{ GaugeName = MetricsAPI.GaugeFilesTotal, Value = 1 }).ConfigureAwait(false);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
_logger.LogInformation("File {hash} added to DB", hash);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -1,7 +1,6 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Security.Cryptography;
using System.Threading.Tasks; using System.Threading.Tasks;
using MareSynchronos.API; using MareSynchronos.API;
using MareSynchronosShared.Authentication; using MareSynchronosShared.Authentication;
@@ -22,22 +21,24 @@ namespace MareSynchronosServer.Hubs
{ {
private readonly MetricsService.MetricsServiceClient _metricsClient; private readonly MetricsService.MetricsServiceClient _metricsClient;
private readonly AuthService.AuthServiceClient _authServiceClient; private readonly AuthService.AuthServiceClient _authServiceClient;
private readonly FileService.FileServiceClient _fileServiceClient;
private readonly SystemInfoService _systemInfoService; private readonly SystemInfoService _systemInfoService;
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private readonly IHttpContextAccessor contextAccessor; private readonly IHttpContextAccessor contextAccessor;
private readonly ILogger<MareHub> _logger; private readonly ILogger<MareHub> _logger;
private readonly MareDbContext _dbContext; private readonly MareDbContext _dbContext;
public MareHub(MetricsService.MetricsServiceClient metricsClient, AuthService.AuthServiceClient authServiceClient, FileService.FileServiceClient fileServiceClient,
public MareHub(MetricsService.MetricsServiceClient metricsClient, AuthService.AuthServiceClient authServiceClient,
MareDbContext mareDbContext, ILogger<MareHub> logger, SystemInfoService systemInfoService, IConfiguration configuration, IHttpContextAccessor contextAccessor) MareDbContext mareDbContext, ILogger<MareHub> logger, SystemInfoService systemInfoService, IConfiguration configuration, IHttpContextAccessor contextAccessor)
{ {
_metricsClient = metricsClient; _metricsClient = metricsClient;
_authServiceClient = authServiceClient; _authServiceClient = authServiceClient;
_fileServiceClient = fileServiceClient;
_systemInfoService = systemInfoService; _systemInfoService = systemInfoService;
_configuration = configuration; _configuration = configuration;
this.contextAccessor = contextAccessor; this.contextAccessor = contextAccessor;
_logger = logger; _logger = logger;
_dbContext = mareDbContext; _dbContext = mareDbContext;
_staticFileAddress = new Uri(_configuration["StaticFileServiceAddress"]);
} }
[HubMethodName(Api.InvokeHeartbeat)] [HubMethodName(Api.InvokeHeartbeat)]

View File

@@ -53,6 +53,10 @@ namespace MareSynchronosServer
{ {
c.Address = new Uri(Configuration.GetValue<string>("ServiceAddress")); c.Address = new Uri(Configuration.GetValue<string>("ServiceAddress"));
}); });
services.AddGrpcClient<FileService.FileServiceClient>(c =>
{
c.Address = new Uri(Configuration.GetValue<string>("StaticFileServiceAddress"));
});
services.AddDbContextPool<MareDbContext>(options => services.AddDbContextPool<MareDbContext>(options =>
{ {

View File

@@ -25,9 +25,8 @@
}, },
"DbContextPoolSize": 2000, "DbContextPoolSize": 2000,
"CdnFullUrl": "https://<url or ip to your server>/cache/", "CdnFullUrl": "https://<url or ip to your server>/cache/",
"CacheDirectory": "G:\\ServerTest", // do not delete this key and set it to the path where the files will be stored
"ServiceAddress": "http://localhost:5002", "ServiceAddress": "http://localhost:5002",
"StaticFileServiceAddress": "http://localhost:5001",
"AllowedHosts": "*", "AllowedHosts": "*",
"Kestrel": { "Kestrel": {
"Endpoints": { "Endpoints": {
@@ -51,14 +50,8 @@
"RealIpHeader": "X-Real-IP", "RealIpHeader": "X-Real-IP",
"ClientIdHeader": "X-ClientId", "ClientIdHeader": "X-ClientId",
"HttpStatusCode": 429, "HttpStatusCode": 429,
"IpWhitelist": [ "127.0.0.1", "::1/10", "192.168.0.0/24" ], "IpWhitelist": [ ],
"GeneralRules": [ "GeneralRules": [ ]
{
"Endpoint": "*",
"Period": "1s",
"Limit": 2
}
]
}, },
"IPRateLimitPolicies": { "IPRateLimitPolicies": {
"IpRules": [] "IpRules": []

View File

@@ -1,7 +1,4 @@
using MareSynchronosServices; using MareSynchronosServices;
using MareSynchronosServices.Metrics;
using MareSynchronosShared.Data;
using Microsoft.EntityFrameworkCore;
public class Program public class Program
{ {

View File

@@ -18,10 +18,8 @@
}, },
"DbContextPoolSize": 1024, "DbContextPoolSize": 1024,
"DiscordBotToken": "", "DiscordBotToken": "",
"UnusedFileRetentionPeriodInDays": 7,
"PurgeUnusedAccounts": true, "PurgeUnusedAccounts": true,
"PurgeUnusedAccountsPeriodInDays": 14, "PurgeUnusedAccountsPeriodInDays": 14,
"CacheSizeHardLimitInGiB": -1,
"CacheDirectory": "G:\\ServerTest", // do not delete this key and set it to the path where the files will be stored "CacheDirectory": "G:\\ServerTest", // do not delete this key and set it to the path where the files will be stored
"FailedAuthForTempBan": 5, "FailedAuthForTempBan": 5,
"TempBanDurationInMinutes": 30, "TempBanDurationInMinutes": 30,

View File

@@ -17,8 +17,32 @@ service MetricsService {
rpc IncGauge (GaugeRequest) returns (Empty); rpc IncGauge (GaugeRequest) returns (Empty);
} }
service FileService {
rpc UploadFile (UploadFileRequest) returns (Empty);
rpc GetFileSizes (FileSizeRequest) returns (FileSizeResponse);
rpc DeleteFiles (DeleteFilesRequest) returns (Empty);
}
message Empty { } message Empty { }
message UploadFileRequest {
string hash = 1;
string uploader = 2;
bytes fileData = 3;
}
message DeleteFilesRequest {
repeated string hash = 1;
}
message FileSizeRequest {
repeated string hash = 1;
}
message FileSizeResponse {
map<string, int64> hashToFileSize = 1;
}
message GaugeRequest { message GaugeRequest {
string gaugeName = 1; string gaugeName = 1;
double value = 2; double value = 2;

View File

@@ -0,0 +1,120 @@
using MareSynchronosShared.Data;
using MareSynchronosShared.Metrics;
using static MareSynchronosShared.Protos.MetricsService;
namespace MareSynchronosStaticFilesServer;
public class CleanupService : IHostedService, IDisposable
{
private readonly MetricsServiceClient _metrics;
private readonly ILogger<CleanupService> _logger;
private readonly IServiceProvider _services;
private readonly IConfiguration _configuration;
private Timer? _timer;
public CleanupService(MetricsServiceClient metrics, ILogger<CleanupService> logger, IServiceProvider services, IConfiguration configuration)
{
_metrics = metrics;
_logger = logger;
_services = services;
_configuration = configuration;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Cleanup Service started");
_timer = new Timer(CleanUp, null, TimeSpan.Zero, TimeSpan.FromMinutes(10));
return Task.CompletedTask;
}
private void CleanUp(object? state)
{
if (!int.TryParse(_configuration["UnusedFileRetentionPeriodInDays"], out var filesOlderThanDays))
{
filesOlderThanDays = 7;
}
using var scope = _services.CreateScope();
using var dbContext = scope.ServiceProvider.GetService<MareDbContext>()!;
_logger.LogInformation("Cleaning up files older than {filesOlderThanDays} days", filesOlderThanDays);
try
{
var prevTime = DateTime.Now.Subtract(TimeSpan.FromDays(filesOlderThanDays));
var allFiles = dbContext.Files.ToList();
var cachedir = _configuration["CacheDirectory"];
foreach (var file in allFiles.Where(f => f.Uploaded))
{
var fileName = Path.Combine(cachedir, file.Hash);
var fi = new FileInfo(fileName);
if (!fi.Exists)
{
_logger.LogInformation("File does not exist anymore: {fileName}", fileName);
dbContext.Files.Remove(file);
}
else if (fi.LastAccessTime < prevTime)
{
_metrics.DecGauge(new() { GaugeName = MetricsAPI.GaugeFilesTotalSize, Value = fi.Length });
_metrics.DecGauge(new() { GaugeName = MetricsAPI.GaugeFilesTotal, Value = 1 });
_logger.LogInformation("File outdated: {fileName}", fileName);
dbContext.Files.Remove(file);
fi.Delete();
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error during file cleanup");
}
var cacheSizeLimitInGiB = _configuration.GetValue<double>("CacheSizeHardLimitInGiB", -1);
try
{
if (cacheSizeLimitInGiB > 0)
{
_logger.LogInformation("Cleaning up files beyond the cache size limit");
var allLocalFiles = Directory.EnumerateFiles(_configuration["CacheDirectory"]).Select(f => new FileInfo(f)).ToList().OrderBy(f => f.LastAccessTimeUtc).ToList();
var totalCacheSizeInBytes = allLocalFiles.Sum(s => s.Length);
long cacheSizeLimitInBytes = (long)(cacheSizeLimitInGiB * 1024 * 1024 * 1024);
HashSet<string> removedHashes = new();
while (totalCacheSizeInBytes > cacheSizeLimitInBytes && allLocalFiles.Any())
{
var oldestFile = allLocalFiles.First();
removedHashes.Add(oldestFile.Name.ToLower());
allLocalFiles.Remove(oldestFile);
totalCacheSizeInBytes -= oldestFile.Length;
_metrics.DecGauge(new() { GaugeName = MetricsAPI.GaugeFilesTotalSize, Value = oldestFile.Length });
_metrics.DecGauge(new() { GaugeName = MetricsAPI.GaugeFilesTotal, Value = 1 });
oldestFile.Delete();
}
dbContext.Files.RemoveRange(dbContext.Files.Where(f => removedHashes.Contains(f.Hash.ToLower())));
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error during cache size limit cleanup");
}
_logger.LogInformation($"Cleanup complete");
dbContext.SaveChanges();
}
public Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}

View File

@@ -0,0 +1,95 @@
using Grpc.Core;
using MareSynchronosShared.Data;
using MareSynchronosShared.Metrics;
using MareSynchronosShared.Protos;
using Microsoft.EntityFrameworkCore;
using System.Security.Policy;
namespace MareSynchronosStaticFilesServer;
public class FileService : MareSynchronosShared.Protos.FileService.FileServiceBase
{
private readonly string _basePath;
private readonly MareDbContext _mareDbContext;
private readonly ILogger<FileService> _logger;
private readonly MetricsService.MetricsServiceClient _metricsClient;
public FileService(MareDbContext mareDbContext, IConfiguration configuration, ILogger<FileService> logger, MetricsService.MetricsServiceClient metricsClient)
{
_basePath = configuration["CacheDirectory"];
_mareDbContext = mareDbContext;
_logger = logger;
_metricsClient = metricsClient;
}
public override async Task<Empty> UploadFile(UploadFileRequest request, ServerCallContext context)
{
var filePath = Path.Combine(_basePath, request.Hash);
var file = await _mareDbContext.Files.SingleOrDefaultAsync(f => f.Hash == request.Hash && f.UploaderUID == request.Uploader);
if (file != null)
{
var byteData = request.FileData.ToArray();
await File.WriteAllBytesAsync(filePath, byteData);
file.Uploaded = true;
await _metricsClient.IncGaugeAsync(new GaugeRequest()
{ GaugeName = MetricsAPI.GaugeFilesTotalSize, Value = byteData.Length }).ConfigureAwait(false);
await _metricsClient.IncGaugeAsync(new GaugeRequest()
{ GaugeName = MetricsAPI.GaugeFilesTotal, Value = 1 }).ConfigureAwait(false);
await _mareDbContext.SaveChangesAsync().ConfigureAwait(false);
_logger.LogInformation("User {user} uploaded file {hash}", request.Uploader, request.Hash);
}
return new Empty();
}
public override async Task<Empty> DeleteFiles(DeleteFilesRequest request, ServerCallContext context)
{
foreach (var hash in request.Hash)
{
try
{
FileInfo fi = new FileInfo(Path.Combine(_basePath, hash));
fi.Delete();
var file = await _mareDbContext.Files.SingleOrDefaultAsync(f => f.Hash == hash);
if (file != null)
{
_mareDbContext.Files.Remove(file);
await _metricsClient.DecGaugeAsync(new GaugeRequest()
{ GaugeName = MetricsAPI.GaugeFilesTotalSize, Value = fi.Length }).ConfigureAwait(false);
await _metricsClient.DecGaugeAsync(new GaugeRequest()
{ GaugeName = MetricsAPI.GaugeFilesTotal, Value = 1 }).ConfigureAwait(false);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not delete file for hash {hash}", hash);
}
}
await _mareDbContext.SaveChangesAsync().ConfigureAwait(false);
return new Empty();
}
public override Task<FileSizeResponse> GetFileSizes(FileSizeRequest request, ServerCallContext context)
{
FileSizeResponse response = new();
foreach (var hash in request.Hash)
{
FileInfo fi = new(Path.Combine(_basePath, hash));
if (fi.Exists)
{
response.HashToFileSize.Add(hash, fi.Length);
}
else
{
response.HashToFileSize.Add(hash, 0);
}
}
return Task.FromResult(response);
}
}

View File

@@ -1,6 +1,5 @@
using MareSynchronosShared.Authentication; using MareSynchronosShared.Authentication;
using MareSynchronosShared.Data; using MareSynchronosShared.Data;
using MareSynchronosShared.Models;
using MareSynchronosShared.Protos; using MareSynchronosShared.Protos;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@@ -28,6 +27,19 @@ public class Startup
{ {
c.Address = new Uri(Configuration.GetValue<string>("ServiceAddress")); c.Address = new Uri(Configuration.GetValue<string>("ServiceAddress"));
}); });
services.AddGrpcClient<MetricsService.MetricsServiceClient>(c =>
{
c.Address = new Uri(Configuration.GetValue<string>("ServiceAddress"));
});
services.AddDbContextPool<MareDbContext>(options =>
{
options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder =>
{
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
}).UseSnakeCaseNamingConvention();
options.EnableThreadSafetyChecks(false);
}, Configuration.GetValue("DbContextPoolSize", 1024));
services.AddAuthentication(options => services.AddAuthentication(options =>
{ {
@@ -35,6 +47,11 @@ public class Startup
}) })
.AddScheme<AuthenticationSchemeOptions, SecretKeyGrpcAuthenticationHandler>(SecretKeyGrpcAuthenticationHandler.AuthScheme, options => { }); .AddScheme<AuthenticationSchemeOptions, SecretKeyGrpcAuthenticationHandler>(SecretKeyGrpcAuthenticationHandler.AuthScheme, options => { });
services.AddAuthorization(options => options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()); services.AddAuthorization(options => options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());
services.AddGrpc(o =>
{
o.MaxReceiveMessageSize = null;
});
} }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
@@ -46,12 +63,17 @@ public class Startup
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.UseStaticFiles(new StaticFileOptions() app.UseStaticFiles(new StaticFileOptions()
{ {
FileProvider = new PhysicalFileProvider(Configuration["CacheDirectory"]), FileProvider = new PhysicalFileProvider(Configuration["CacheDirectory"]),
RequestPath = "/cache", RequestPath = "/cache",
ServeUnknownFileTypes = true ServeUnknownFileTypes = true
}); });
app.UseEndpoints(e =>
{
e.MapGrpcService<FileService>();
});
} }
} }

View File

@@ -12,6 +12,8 @@
} }
} }
}, },
"CacheSizeHardLimitInGiB": -1,
"UnusedFileRetentionPeriodInDays": 7,
"AllowedHosts": "*", "AllowedHosts": "*",
"CacheDirectory": "G:\\ServerTest", // do not delete this key and set it to the path where the files will be stored "CacheDirectory": "G:\\ServerTest", // do not delete this key and set it to the path where the files will be stored
"ServiceAddress": "http://localhost:5002" "ServiceAddress": "http://localhost:5002"