150 lines
7.1 KiB
C#
150 lines
7.1 KiB
C#
using System.IO;
|
|
using System.Net;
|
|
using ParadoxSaveParser.WebAPI.BackgroundTasks;
|
|
using ParadoxSaveParser.WebAPI.Database;
|
|
using ParadoxSaveParser.WebAPI.HttpHelpers;
|
|
using ParadoxSaveParser.WebAPI.SaveDataFilters;
|
|
|
|
namespace ParadoxSaveParser.WebAPI.Routes;
|
|
|
|
public class UploadSaveHandler : RouteHandlerBase
|
|
{
|
|
private const int MaxOriginalFileNameLength = 255;
|
|
|
|
private readonly BackgroundJobManager _bgJobManager;
|
|
private readonly Dictionary<Game, ISaveDataFilter> _saveFilters;
|
|
private readonly Config.SaveUploadConfig _uploadConfig;
|
|
|
|
public UploadSaveHandler(
|
|
CancellationToken cancelAllToken,
|
|
BackgroundJobManager bgJobManager,
|
|
Dictionary<Game, ISaveDataFilter> saveFilters,
|
|
Config.SaveUploadConfig uploadConfig)
|
|
: base(cancelAllToken)
|
|
{
|
|
_bgJobManager = bgJobManager;
|
|
_saveFilters = saveFilters;
|
|
_uploadConfig = uploadConfig;
|
|
}
|
|
|
|
public override async Task<HttpStatusCode> HandleRequest(
|
|
HttpListenerContext ctx, ContextLogger requestLogger)
|
|
{
|
|
string? contentType = ctx.Request.ContentType;
|
|
string? mediaType = contentType?.Split(';')[0].Trim();
|
|
if (!string.Equals(mediaType, "application/zstd", StringComparison.OrdinalIgnoreCase))
|
|
return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken,
|
|
new ErrorMessage(HttpStatusCode.BadRequest,
|
|
$"Invalid request Content-Type: '{contentType}'"));
|
|
|
|
var gameStrOrError = RequestHelper.GetQueryValue(ctx, "game");
|
|
if (gameStrOrError.HasError)
|
|
return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken, gameStrOrError.Error!);
|
|
if (!Enum.TryParse(gameStrOrError.Value, ignoreCase: true, out Game game))
|
|
return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken,
|
|
new ErrorMessage(HttpStatusCode.BadRequest,
|
|
$"Invalid requested game: '{gameStrOrError.Value}'"));
|
|
if (!_saveFilters.ContainsKey(game) || !GameSaveEntryNames.Map.ContainsKey(game))
|
|
return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken,
|
|
new ErrorMessage(HttpStatusCode.BadRequest,
|
|
$"Game {game} is not supported"));
|
|
|
|
var fileTypeStrOrError = RequestHelper.GetQueryValue(ctx, "originalFileType");
|
|
if (fileTypeStrOrError.HasError)
|
|
return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken, fileTypeStrOrError.Error!);
|
|
OriginalFileType? originalFileType = fileTypeStrOrError.Value!.ToLowerInvariant() switch
|
|
{
|
|
"text" => OriginalFileType.Text,
|
|
"zip" => OriginalFileType.Zip,
|
|
_ => null
|
|
};
|
|
if (originalFileType is null)
|
|
return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken,
|
|
new ErrorMessage(HttpStatusCode.BadRequest,
|
|
$"Invalid originalFileType: '{fileTypeStrOrError.Value}'"));
|
|
|
|
var fileNameOrError = RequestHelper.GetQueryValue(ctx, "originalFileName");
|
|
if (fileNameOrError.HasError)
|
|
return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken, fileNameOrError.Error!);
|
|
string originalFileName = fileNameOrError.Value!;
|
|
if (originalFileName.Length > MaxOriginalFileNameLength)
|
|
return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken,
|
|
new ErrorMessage(HttpStatusCode.BadRequest,
|
|
$"originalFileName is longer than {MaxOriginalFileNameLength} characters"));
|
|
|
|
// Content-Length is unknown (-1) for chunked requests, so body size is checked during copying too
|
|
if (ctx.Request.ContentLength64 > _uploadConfig.MaxSizeCompressedBytes)
|
|
return await ResponseUploadTooLarge(ctx, requestLogger);
|
|
|
|
var meta = await Program.DB.CreateMetadata(game, originalFileType.Value, originalFileName);
|
|
try
|
|
{
|
|
bool fitsLimit;
|
|
await using (var saveFile = File.OpenWrite(meta.GetSaveFilePath()))
|
|
{
|
|
await using (var remoteStream = ctx.Request.InputStream)
|
|
fitsLimit = await CopyWithLimit(remoteStream, saveFile, _uploadConfig.MaxSizeCompressedBytes);
|
|
}
|
|
if (!fitsLimit)
|
|
{
|
|
await Program.DB.DeleteMetadata(meta, "upload is too large");
|
|
return await ResponseUploadTooLarge(ctx, requestLogger);
|
|
}
|
|
|
|
long gameDataSize;
|
|
try
|
|
{
|
|
gameDataSize = await SaveArchive.GetGameDataSize(meta.GetSaveFilePath(), game, _cancelAllToken);
|
|
}
|
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
|
{
|
|
await Program.DB.DeleteMetadata(meta, "invalid archive");
|
|
return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken,
|
|
new ErrorMessage(HttpStatusCode.BadRequest,
|
|
$"Invalid save archive: {ex.Message}"));
|
|
}
|
|
if (gameDataSize > _uploadConfig.MaxSizePlainTextBytes)
|
|
{
|
|
await Program.DB.DeleteMetadata(meta, "game data is too large");
|
|
return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken,
|
|
new ErrorMessage(HttpStatusCode.RequestEntityTooLarge,
|
|
$"Uncompressed game data size ({gameDataSize} bytes) exceeds limit "
|
|
+ $"({_uploadConfig.MaxSizePlainTextBytes} bytes)"));
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
await Program.DB.DeleteMetadata(meta, "upload failed");
|
|
throw;
|
|
}
|
|
await Program.DB.UpdateMetadataStatus(meta, SaveFileProcessingStatus.Uploaded);
|
|
|
|
_bgJobManager.StartNewParsingOperation(meta, _saveFilters[meta.game], _cancelAllToken);
|
|
dynamic responseData = new { meta.id };
|
|
return await ReturnHelper.ResponseJson(ctx, requestLogger, _cancelAllToken, responseData);
|
|
}
|
|
|
|
private Task<HttpStatusCode> ResponseUploadTooLarge(HttpListenerContext ctx, ContextLogger requestLogger)
|
|
{
|
|
return ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken,
|
|
new ErrorMessage(HttpStatusCode.RequestEntityTooLarge,
|
|
$"Upload size exceeds limit ({_uploadConfig.MaxSizeCompressedBytes} bytes)"));
|
|
}
|
|
|
|
/// <returns>false if source is longer than maxBytes</returns>
|
|
private async Task<bool> CopyWithLimit(Stream source, Stream dest, long maxBytes)
|
|
{
|
|
byte[] buffer = new byte[81920];
|
|
long totalBytes = 0;
|
|
int readBytes;
|
|
while ((readBytes = await source.ReadAsync(buffer, _cancelAllToken)) > 0)
|
|
{
|
|
totalBytes += readBytes;
|
|
if (totalBytes > maxBytes)
|
|
return false;
|
|
await dest.WriteAsync(buffer.AsMemory(0, readBytes), _cancelAllToken);
|
|
}
|
|
return true;
|
|
}
|
|
}
|