app/BTCPayServer.Plugins.App/API/VSSController.cs
Dennis Reimann 72885feea7
VSS: Extend logging
Context: #208
2025-05-20 12:17:56 +02:00

174 lines
7.6 KiB
C#

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Plugins.App.Data;
using BTCPayServer.Plugins.App.Data.Models;
using Google.Protobuf;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using VSS;
using VSSProto;
namespace BTCPayServer.Plugins.App.API;
[ApiController]
[ResultOverrideFilter]
[ProtobufFormatter]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.GreenfieldAPIKeys)]
[Route("vss")]
public class VSSController(
AppPluginDbContextFactory dbContextFactory,
UserManager<ApplicationUser> userManager,
BTCPayAppState appState,
ILogger<VSSController> logger)
: Controller, IVSSAPI
{
[HttpPost(HttpVSSAPIClient.GET_OBJECT)]
[MediaTypeConstraint("application/octet-stream")]
public async Task<GetObjectResponse> GetObjectAsync(GetObjectRequest request, CancellationToken cancellationToken = default)
{
var userId = userManager.GetUserId(User);
await using var dbContext = dbContextFactory.CreateContext();
var store = await dbContext.AppStorageItems.SingleOrDefaultAsync(data =>
data.Key == request.Key && data.UserId == userId, cancellationToken: cancellationToken);
if (store == null)
{
return SetResult<GetObjectResponse>(
new NotFoundObjectResult(new ErrorResponse
{
ErrorCode = ErrorCode.NoSuchKeyException,
Message = "Key not found"
}));
}
return new GetObjectResponse
{
Value = new KeyValue
{
Key = store.Key,
Value = ByteString.CopyFrom(store.Value),
Version = store.Version
}
};
}
[HttpPost(HttpVSSAPIClient.PUT_OBJECTS)]
[MediaTypeConstraint("application/octet-stream")]
public async Task<PutObjectResponse> PutObjectAsync(PutObjectRequest request, CancellationToken cancellationToken = default)
{
var deviceId = request.GlobalVersion;
if (!await VerifyMaster(deviceId))
return SetResult<PutObjectResponse>(BadRequest(new ErrorResponse
{
ErrorCode = ErrorCode.ConflictException,
Message = "Global version mismatch"
}));
var userId = userManager.GetUserId(User)!;
// TODO: Log with debug level once we solve the issue #208
logger.LogInformation("VSS backup request for user {UserId} ({DeviceId}): {TransactionItems} / {DeleteItems}", userId, deviceId,
request.TransactionItems.Count != 0 ? string.Join(", ", request.TransactionItems.Select(data => data.Key)) : "no transaction items",
request.DeleteItems.Count != 0 ? string.Join(", ", request.DeleteItems.Select(data => data.Key)) : "no delete items");
await using var dbContext = dbContextFactory.CreateContext();
return await dbContext.Database.CreateExecutionStrategy().ExecuteAsync(async _ =>
{
await using var dbContextTransaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
try
{
if (request.TransactionItems.Count != 0)
{
var items = request.TransactionItems.Select(data => new AppStorageItemData
{
Key = data.Key,
Value = data.Value.ToByteArray(),
UserId = userId,
Version = data.Version
});
await dbContext.AppStorageItems.AddRangeAsync(items, cancellationToken);
}
var deleted = 0;
if (request.DeleteItems.Count != 0)
{
var deleteQuery = request.DeleteItems.Aggregate(
dbContext.AppStorageItems.Where(data => data.UserId == userId),
(current, key) => current.Where(data => data.Key == key.Key && data.Version == key.Version));
deleted = await deleteQuery.ExecuteDeleteAsync(cancellationToken: cancellationToken);
}
await dbContext.SaveChangesAsync(cancellationToken);
await dbContextTransaction.CommitAsync(cancellationToken);
logger.LogInformation("VSS backup request for user {UserId} ({DeviceId}) processed: {TCount} items added, {DCount} items deleted", userId, deviceId, request.TransactionItems.Count, deleted);
await appState.GracefulDisconnect(userId);
return new PutObjectResponse();
}
catch (Exception e)
{
logger.LogError(e, "VSS backup request for user {UserId} ({DeviceId}) failed: {Message}", userId, deviceId, e.Message);
await dbContextTransaction.RollbackAsync(cancellationToken);
return SetResult<PutObjectResponse>(BadRequest(new ErrorResponse
{
ErrorCode = ErrorCode.ConflictException,
Message = e.Message
}));
}
}, cancellationToken);
}
[HttpPost(HttpVSSAPIClient.DELETE_OBJECT)]
[MediaTypeConstraint("application/octet-stream")]
public async Task<DeleteObjectResponse> DeleteObjectAsync(DeleteObjectRequest request, CancellationToken cancellationToken = default)
{
var userId = userManager.GetUserId(User);
var key = request.KeyValue.Key;
var ver = request.KeyValue.Version;
logger.LogInformation("VSS delete request from user {UserId} with key {Key} and version {Version}", userId, key, ver);
await using var dbContext = dbContextFactory.CreateContext();
var store = await dbContext.AppStorageItems
.Where(data => data.Key == key && data.UserId == userId && data.Version == ver)
.ExecuteDeleteAsync(cancellationToken: cancellationToken);
logger.LogInformation("VSS delete request from user {UserId} with key {Key} and version {Version} processed: {Count} items deleted", userId, key, ver, store);
return store == 0
? SetResult<DeleteObjectResponse>(
new NotFoundObjectResult(new ErrorResponse
{
ErrorCode = ErrorCode.NoSuchKeyException,
Message = "Key not found"
}))
: new DeleteObjectResponse();
}
[HttpPost(HttpVSSAPIClient.LIST_KEY_VERSIONS)]
public async Task<ListKeyVersionsResponse> ListKeyVersionsAsync(ListKeyVersionsRequest? request = null, CancellationToken cancellationToken = default)
{
var userId = userManager.GetUserId(User);
await using var dbContext = dbContextFactory.CreateContext();
var items = await dbContext.AppStorageItems
.Where(data => data.UserId == userId && data.Key != "masterDevice")
.Select(data => new KeyValue { Key = data.Key, Version = data.Version })
.ToListAsync(cancellationToken: cancellationToken);
return new ListKeyVersionsResponse {KeyVersions = {items}};
}
private T SetResult<T>(IActionResult result)
{
HttpContext.Items["Result"] = result;
return default;
}
private async Task<bool> VerifyMaster(long deviceIdentifier)
{
var userId = userManager.GetUserId(User);
return userId != null && await appState.IsMaster(userId, deviceIdentifier);
}
}