mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-11 17:32:11 +00:00
Added a result type with failure reasons for file extraction
This commit is contained in:
parent
4790439f9e
commit
8d91b1cda8
@ -716,7 +716,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileContent = await this.RustService.ReadArbitraryFileData(document.FilePath, int.MaxValue);
|
var fileContent = (await this.RustService.ReadArbitraryFileData(document.FilePath, int.MaxValue)).Content;
|
||||||
sb.AppendLine($"""
|
sb.AppendLine($"""
|
||||||
|
|
||||||
## DOCUMENT {numDocuments}:
|
## DOCUMENT {numDocuments}:
|
||||||
|
|||||||
@ -382,7 +382,7 @@ public partial class SlideAssistant : AssistantBaseCore<SettingsDialogSlideBuild
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileContent = await this.RustService.ReadArbitraryFileData(document.FilePath, int.MaxValue);
|
var fileContent = (await this.RustService.ReadArbitraryFileData(document.FilePath, int.MaxValue)).Content;
|
||||||
sb.AppendLine($"""
|
sb.AppendLine($"""
|
||||||
|
|
||||||
## DOCUMENT {numDocuments}:
|
## DOCUMENT {numDocuments}:
|
||||||
|
|||||||
@ -299,7 +299,7 @@ public sealed class ContentText : IContent
|
|||||||
sb.AppendLine($"File path: {document.FilePath}");
|
sb.AppendLine($"File path: {document.FilePath}");
|
||||||
sb.AppendLine("File content:");
|
sb.AppendLine("File content:");
|
||||||
sb.AppendLine("````");
|
sb.AppendLine("````");
|
||||||
sb.AppendLine(await Program.RUST_SERVICE.ReadArbitraryFileData(document.FilePath, int.MaxValue));
|
sb.AppendLine((await Program.RUST_SERVICE.ReadArbitraryFileData(document.FilePath, int.MaxValue)).Content);
|
||||||
sb.AppendLine("````");
|
sb.AppendLine("````");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,11 +6,20 @@ namespace AIStudio.Tools;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public enum FileExtractionErrorCode
|
public enum FileExtractionErrorCode
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// No failure happened.
|
||||||
|
/// </summary>
|
||||||
|
NONE,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A code this version does not know, e.g. from a newer runtime.
|
/// A code this version does not know, e.g. from a newer runtime.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
UNKNOWN,
|
UNKNOWN,
|
||||||
|
|
||||||
|
//
|
||||||
|
// Codes reported by the Rust runtime:
|
||||||
|
//
|
||||||
|
|
||||||
INVALID_REQUEST,
|
INVALID_REQUEST,
|
||||||
FILE_NOT_FOUND,
|
FILE_NOT_FOUND,
|
||||||
FILE_NOT_READABLE,
|
FILE_NOT_READABLE,
|
||||||
@ -23,4 +32,28 @@ public enum FileExtractionErrorCode
|
|||||||
NO_TEXT_EXTRACTED,
|
NO_TEXT_EXTRACTED,
|
||||||
UNSUPPORTED,
|
UNSUPPORTED,
|
||||||
INTERNAL,
|
INTERNAL,
|
||||||
|
|
||||||
|
//
|
||||||
|
// Codes reported by the app itself:
|
||||||
|
//
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The runtime answered with an unsuccessful HTTP status.
|
||||||
|
/// </summary>
|
||||||
|
REQUEST_FAILED,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reading the file took longer than the app is willing to wait.
|
||||||
|
/// </summary>
|
||||||
|
TIMEOUT,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The runtime sent something the app could not deserialize.
|
||||||
|
/// </summary>
|
||||||
|
INVALID_RESPONSE,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The extraction finished without reporting a failure, but produced no content at all.
|
||||||
|
/// </summary>
|
||||||
|
NO_CONTENT,
|
||||||
}
|
}
|
||||||
23
app/MindWork AI Studio/Tools/FileExtractionOutcome.cs
Normal file
23
app/MindWork AI Studio/Tools/FileExtractionOutcome.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
namespace AIStudio.Tools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How reading a file ended.
|
||||||
|
/// </summary>
|
||||||
|
public enum FileExtractionOutcome
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The whole file was read.
|
||||||
|
/// </summary>
|
||||||
|
SUCCESS,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parts of the file could not be read, e.g. single pages of a PDF, while the remaining
|
||||||
|
/// content is still usable.
|
||||||
|
/// </summary>
|
||||||
|
PARTIAL,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The file could not be read. There is no content the app is allowed to use.
|
||||||
|
/// </summary>
|
||||||
|
FAILED,
|
||||||
|
}
|
||||||
36
app/MindWork AI Studio/Tools/FileExtractionResult.cs
Normal file
36
app/MindWork AI Studio/Tools/FileExtractionResult.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
namespace AIStudio.Tools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The result of reading a file through the Rust runtime.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Content and failure travel together on purpose. When reading a file returns a bare string, a
|
||||||
|
/// failed extraction is indistinguishable from an empty document, and the empty document reaches
|
||||||
|
/// the AI as if that were the content of the user's file.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="Outcome">How the extraction ended.</param>
|
||||||
|
/// <param name="Content">The extracted content. Empty when the extraction failed.</param>
|
||||||
|
/// <param name="ErrorCode">Why the extraction failed or lost parts of the file.</param>
|
||||||
|
/// <param name="ErrorMessage">The technical failure description, meant for logs and diagnostics.</param>
|
||||||
|
/// <param name="FailedPages">The pages which could not be read, when known.</param>
|
||||||
|
public readonly record struct FileExtractionResult(FileExtractionOutcome Outcome, string Content, FileExtractionErrorCode ErrorCode, string? ErrorMessage, IReadOnlyList<int> FailedPages)
|
||||||
|
{
|
||||||
|
private static readonly int[] NO_FAILED_PAGES = [];
|
||||||
|
|
||||||
|
public static FileExtractionResult Success(string content) => new(FileExtractionOutcome.SUCCESS, content, FileExtractionErrorCode.NONE, null, NO_FAILED_PAGES);
|
||||||
|
|
||||||
|
public static FileExtractionResult Partial(string content, IReadOnlyList<int> failedPages) => new(FileExtractionOutcome.PARTIAL, content, FileExtractionErrorCode.PAGE_EXTRACTION_FAILED, null, failedPages);
|
||||||
|
|
||||||
|
public static FileExtractionResult Failed(FileExtractionErrorCode errorCode, string? errorMessage) => new(FileExtractionOutcome.FAILED, string.Empty, errorCode, errorMessage, NO_FAILED_PAGES);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the whole file was read.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSuccess => this.Outcome is FileExtractionOutcome.SUCCESS;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the content may be handed to the AI, i.e. the extraction
|
||||||
|
/// either succeeded or lost only parts of the file.
|
||||||
|
/// </summary>
|
||||||
|
public bool HasUsableContent => this.Outcome is FileExtractionOutcome.SUCCESS or FileExtractionOutcome.PARTIAL;
|
||||||
|
}
|
||||||
@ -5,36 +5,60 @@ namespace AIStudio.Tools.Services;
|
|||||||
|
|
||||||
public sealed partial class RustService
|
public sealed partial class RustService
|
||||||
{
|
{
|
||||||
public async Task<string> ReadArbitraryFileData(string path, int maxChunks, bool extractImages = false)
|
/// <summary>
|
||||||
|
/// How long one file extraction may take.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Reading a large file from a slow network share is legitimately slow, so this is well above
|
||||||
|
/// the default HTTP client timeout. It still bounds the operation, because an unbounded read
|
||||||
|
/// would keep the caller waiting forever.
|
||||||
|
/// </remarks>
|
||||||
|
private static readonly TimeSpan EXTRACTION_TIMEOUT = TimeSpan.FromMinutes(10);
|
||||||
|
|
||||||
|
public async Task<FileExtractionResult> ReadArbitraryFileData(string path, int maxChunks, bool extractImages = false)
|
||||||
{
|
{
|
||||||
var streamId = Guid.NewGuid().ToString();
|
var streamId = Guid.NewGuid().ToString();
|
||||||
var requestUri = $"/retrieval/fs/extract?path={Uri.EscapeDataString(path)}&stream_id={streamId}&extract_images={extractImages}";
|
var requestUri = $"/retrieval/fs/extract?path={Uri.EscapeDataString(path)}&stream_id={streamId}&extract_images={extractImages}";
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
|
|
||||||
var response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
using var timeoutTokenSource = new CancellationTokenSource(EXTRACTION_TIMEOUT);
|
||||||
{
|
var cancellationToken = timeoutTokenSource.Token;
|
||||||
var responseBody = await response.Content.ReadAsStringAsync();
|
|
||||||
this.logger?.LogError(
|
|
||||||
"Failed to read arbitrary file data from Rust runtime. Status: {StatusCode}, reason: '{ReasonPhrase}', path: '{Path}', body: '{Body}'",
|
|
||||||
response.StatusCode,
|
|
||||||
response.ReasonPhrase,
|
|
||||||
path,
|
|
||||||
responseBody);
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
var resultBuilder = new StringBuilder();
|
var resultBuilder = new StringBuilder();
|
||||||
|
var failedPages = new List<int>();
|
||||||
|
var hasPartialFailure = false;
|
||||||
|
var failureCode = FileExtractionErrorCode.NONE;
|
||||||
|
string? failureMessage = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync();
|
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
|
||||||
|
using var response = await this.extractionHttp.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
this.logger?.LogError(
|
||||||
|
"Failed to read arbitrary file data from Rust runtime. Status: {StatusCode}, reason: '{ReasonPhrase}', path: '{Path}', body: '{Body}'",
|
||||||
|
response.StatusCode,
|
||||||
|
response.ReasonPhrase,
|
||||||
|
path,
|
||||||
|
responseBody);
|
||||||
|
|
||||||
|
return FileExtractionResult.Failed(FileExtractionErrorCode.REQUEST_FAILED, $"The runtime answered with the status {(int)response.StatusCode} ({response.ReasonPhrase}).");
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||||
using var reader = new StreamReader(stream);
|
using var reader = new StreamReader(stream);
|
||||||
var chunkCount = 0;
|
var chunkCount = 0;
|
||||||
|
|
||||||
while (!reader.EndOfStream && chunkCount < maxChunks)
|
while (chunkCount < maxChunks)
|
||||||
{
|
{
|
||||||
var line = await reader.ReadLineAsync();
|
// We read line by line instead of checking EndOfStream: the latter blocks on a
|
||||||
|
// network stream and cannot be cancelled, which would defeat the timeout above.
|
||||||
|
var line = await reader.ReadLineAsync(cancellationToken);
|
||||||
|
if (line is null)
|
||||||
|
break;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(line))
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@ -46,34 +70,64 @@ public sealed partial class RustService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sseEvent = JsonSerializer.Deserialize<ContentStreamSseEvent>(jsonContent);
|
var sseEvent = JsonSerializer.Deserialize<ContentStreamSseEvent>(jsonContent);
|
||||||
if (sseEvent is not null)
|
if (sseEvent is null)
|
||||||
{
|
continue;
|
||||||
var processedEvent = ContentStreamSseHandler.ProcessEvent(sseEvent, extractImages);
|
|
||||||
if (processedEvent.Error is not null)
|
|
||||||
{
|
|
||||||
this.logger?.LogError(
|
|
||||||
"The runtime reported a failure while reading '{Path}': code={ErrorCode}, page={PageNumber}, partial={IsPartialFailure}, message='{Message}'",
|
|
||||||
path,
|
|
||||||
processedEvent.Error.ParsedCode,
|
|
||||||
processedEvent.Error.PageNumber,
|
|
||||||
processedEvent.Error.IsPartialFailure,
|
|
||||||
processedEvent.Error.Message);
|
|
||||||
}
|
|
||||||
else if (processedEvent.Content is not null)
|
|
||||||
resultBuilder.AppendLine(processedEvent.Content);
|
|
||||||
|
|
||||||
chunkCount++;
|
var processedEvent = ContentStreamSseHandler.ProcessEvent(sseEvent, extractImages);
|
||||||
|
if (processedEvent.Error is not null)
|
||||||
|
{
|
||||||
|
var error = processedEvent.Error;
|
||||||
|
this.logger?.LogError(
|
||||||
|
"The runtime reported a failure while reading '{Path}': code={ErrorCode}, page={PageNumber}, partial={IsPartialFailure}, message='{Message}'",
|
||||||
|
path,
|
||||||
|
error.ParsedCode,
|
||||||
|
error.PageNumber,
|
||||||
|
error.IsPartialFailure,
|
||||||
|
error.Message);
|
||||||
|
|
||||||
|
//
|
||||||
|
// A partial failure costs us one part of the file, e.g. a single PDF page,
|
||||||
|
// but keeps the rest usable. Any other failure means what we collected is
|
||||||
|
// not the document the user picked, so we must not pass it on as content.
|
||||||
|
//
|
||||||
|
if (error.IsPartialFailure)
|
||||||
|
{
|
||||||
|
hasPartialFailure = true;
|
||||||
|
if (error.PageNumber is { } pageNumber)
|
||||||
|
failedPages.Add(pageNumber);
|
||||||
|
}
|
||||||
|
else if (failureCode is FileExtractionErrorCode.NONE)
|
||||||
|
{
|
||||||
|
failureCode = error.ParsedCode;
|
||||||
|
failureMessage = error.Message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else if (processedEvent.Content is not null)
|
||||||
|
resultBuilder.AppendLine(processedEvent.Content);
|
||||||
|
|
||||||
|
chunkCount++;
|
||||||
}
|
}
|
||||||
catch (JsonException)
|
catch (JsonException e)
|
||||||
{
|
{
|
||||||
this.logger?.LogError("Failed to deserialize SSE event: {JsonContent}", jsonContent);
|
this.logger?.LogError(e, "Failed to deserialize SSE event while reading '{Path}': {JsonContent}", path, jsonContent);
|
||||||
|
|
||||||
|
if (failureCode is FileExtractionErrorCode.NONE)
|
||||||
|
{
|
||||||
|
failureCode = FileExtractionErrorCode.INVALID_RESPONSE;
|
||||||
|
failureMessage = "The runtime sent a response the app was not able to read.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch (OperationCanceledException) when (timeoutTokenSource.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
this.logger?.LogError("Reading the file '{Path}' timed out after {Timeout}.", path, EXTRACTION_TIMEOUT);
|
||||||
|
return FileExtractionResult.Failed(FileExtractionErrorCode.TIMEOUT, $"Reading the file timed out after {EXTRACTION_TIMEOUT.TotalMinutes:0} minutes.");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
this.logger?.LogError(e, "Error reading file data from stream: {Path}", path);
|
this.logger?.LogError(e, "Error reading file data from stream: {Path}", path);
|
||||||
|
return FileExtractionResult.Failed(FileExtractionErrorCode.INTERNAL, e.Message);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -81,7 +135,25 @@ public sealed partial class RustService
|
|||||||
if (!string.IsNullOrWhiteSpace(finalContentChunk))
|
if (!string.IsNullOrWhiteSpace(finalContentChunk))
|
||||||
resultBuilder.AppendLine(finalContentChunk);
|
resultBuilder.AppendLine(finalContentChunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resultBuilder.ToString();
|
if (failureCode is not FileExtractionErrorCode.NONE)
|
||||||
|
return FileExtractionResult.Failed(failureCode, failureMessage);
|
||||||
|
|
||||||
|
var content = resultBuilder.ToString();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Nothing failed, yet nothing came out either. We report this as a failure as well:
|
||||||
|
// handing an empty document to the AI looks like a file without content, and the user
|
||||||
|
// would never learn that reading the file did not work.
|
||||||
|
//
|
||||||
|
if (string.IsNullOrWhiteSpace(content))
|
||||||
|
{
|
||||||
|
this.logger?.LogWarning("Reading the file '{Path}' produced no content at all.", path);
|
||||||
|
return FileExtractionResult.Failed(FileExtractionErrorCode.NO_CONTENT, "Reading the file produced no content.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasPartialFailure
|
||||||
|
? FileExtractionResult.Partial(content, failedPages)
|
||||||
|
: FileExtractionResult.Success(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -17,6 +17,19 @@ public sealed partial class RustService : BackgroundService
|
|||||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(RustService).Namespace, nameof(RustService));
|
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(RustService).Namespace, nameof(RustService));
|
||||||
|
|
||||||
private readonly HttpClient http;
|
private readonly HttpClient http;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A dedicated client for file extraction.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Extraction needs its own client because <see cref="HttpClient.Timeout"/> is a client-wide
|
||||||
|
/// setting which also covers reading the streamed response body. A per-request cancellation
|
||||||
|
/// token can only shorten that limit, never extend it. Reading a large file from a slow
|
||||||
|
/// network share legitimately exceeds the default limit, so this client has no timeout of its
|
||||||
|
/// own and the extraction bounds each request itself.
|
||||||
|
/// </remarks>
|
||||||
|
private readonly HttpClient extractionHttp;
|
||||||
|
|
||||||
private readonly SemaphoreSlim fileDialogLock = new(1, 1);
|
private readonly SemaphoreSlim fileDialogLock = new(1, 1);
|
||||||
private readonly SemaphoreSlim userLanguageLock = new(1, 1);
|
private readonly SemaphoreSlim userLanguageLock = new(1, 1);
|
||||||
private readonly SemaphoreSlim userNameLock = new(1, 1);
|
private readonly SemaphoreSlim userNameLock = new(1, 1);
|
||||||
@ -42,26 +55,37 @@ public sealed partial class RustService : BackgroundService
|
|||||||
{
|
{
|
||||||
this.apiPort = apiPort;
|
this.apiPort = apiPort;
|
||||||
this.certificateFingerprint = certificateFingerprint;
|
this.certificateFingerprint = certificateFingerprint;
|
||||||
|
|
||||||
|
// The default timeout of HttpClient, kept explicit so the difference to the
|
||||||
|
// extraction client below is visible:
|
||||||
|
this.http = CreateHttpClient(apiPort, certificateFingerprint, TimeSpan.FromSeconds(100));
|
||||||
|
this.extractionHttp = CreateHttpClient(apiPort, certificateFingerprint, Timeout.InfiniteTimeSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpClient CreateHttpClient(string apiPort, string certificateFingerprint, TimeSpan timeout)
|
||||||
|
{
|
||||||
var certificateValidationHandler = new HttpClientHandler
|
var certificateValidationHandler = new HttpClientHandler
|
||||||
{
|
{
|
||||||
ServerCertificateCustomValidationCallback = (_, certificate, _, _) =>
|
ServerCertificateCustomValidationCallback = (_, certificate, _, _) =>
|
||||||
{
|
{
|
||||||
if(certificate is null)
|
if(certificate is null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var currentCertificateFingerprint = certificate.GetCertHashString(HashAlgorithmName.SHA256);
|
var currentCertificateFingerprint = certificate.GetCertHashString(HashAlgorithmName.SHA256);
|
||||||
return currentCertificateFingerprint == certificateFingerprint;
|
return currentCertificateFingerprint == certificateFingerprint;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
this.http = new HttpClient(certificateValidationHandler)
|
var client = new HttpClient(certificateValidationHandler)
|
||||||
{
|
{
|
||||||
BaseAddress = new Uri($"https://127.0.0.1:{apiPort}"),
|
BaseAddress = new Uri($"https://127.0.0.1:{apiPort}"),
|
||||||
DefaultRequestVersion = Version.Parse("2.0"),
|
DefaultRequestVersion = Version.Parse("2.0"),
|
||||||
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher,
|
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher,
|
||||||
|
Timeout = timeout,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.http.DefaultRequestHeaders.AddApiToken();
|
client.DefaultRequestHeaders.AddApiToken();
|
||||||
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetLogger(ILogger<RustService> logService)
|
public void SetLogger(ILogger<RustService> logService)
|
||||||
|
|||||||
@ -46,6 +46,6 @@ public static class UserFile
|
|||||||
}
|
}
|
||||||
|
|
||||||
var fileContent = await rustService.ReadArbitraryFileData(filePath, int.MaxValue);
|
var fileContent = await rustService.ReadArbitraryFileData(filePath, int.MaxValue);
|
||||||
return fileContent;
|
return fileContent.Content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user