updated to accept only .tar.zstd uploads
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using ParadoxSaveParser.WebAPI.BackgroundTasks;
|
||||
using ParadoxSaveParser.WebAPI.Database;
|
||||
@@ -9,24 +9,30 @@ 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,
|
||||
CancellationToken cancelAllToken,
|
||||
BackgroundJobManager bgJobManager,
|
||||
Dictionary<Game, ISaveDataFilter> saveFilters)
|
||||
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.Headers.GetValues("Content-Type")?.FirstOrDefault();
|
||||
if (contentType != "application/octet-stream")
|
||||
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}'"));
|
||||
@@ -34,18 +40,82 @@ public class UploadSaveHandler : RouteHandlerBase
|
||||
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}'"));
|
||||
var meta = await Program.DB.CreateMetadata(game);
|
||||
$"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 saveFilePath = meta.GetSaveFilePath();
|
||||
await using (var saveFile = File.OpenWrite(saveFilePath))
|
||||
var fileTypeStrOrError = RequestHelper.GetQueryValue(ctx, "originalFileType");
|
||||
if (fileTypeStrOrError.HasError)
|
||||
return await ReturnHelper.ResponseError(ctx, requestLogger, _cancelAllToken, fileTypeStrOrError.Error!);
|
||||
OriginalFileType? originalFileType = fileTypeStrOrError.Value!.ToLowerInvariant() switch
|
||||
{
|
||||
await using (var remoteStream = ctx.Request.InputStream)
|
||||
await remoteStream.CopyToAsync(saveFile, _cancelAllToken);
|
||||
"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);
|
||||
|
||||
@@ -53,4 +123,27 @@ public class UploadSaveHandler : RouteHandlerBase
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user