Skip to content

Commit

Permalink
Docs (#159)
Browse files Browse the repository at this point in the history
Much better docs route UX. Limit is visible, and we get snack bar
notifications - reporting success and failure.
  • Loading branch information
IEvangelist authored Sep 14, 2023
1 parent f3c23f8 commit 92ae671
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 40 deletions.
5 changes: 5 additions & 0 deletions app/backend/Extensions/WebApplicationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,15 @@ private static async Task<IResult> OnPostChatAsync(
private static async Task<IResult> OnPostDocumentAsync(
[FromForm] IFormFileCollection files,
[FromServices] AzureBlobStorageService service,
[FromServices] ILogger<AzureBlobStorageService> logger,
CancellationToken cancellationToken)
{
logger.LogInformation("Upload documents");

var response = await service.UploadFilesAsync(files, cancellationToken);

logger.LogInformation("Upload documents: {x}", response);

return TypedResults.Ok(response);
}

Expand Down
79 changes: 48 additions & 31 deletions app/backend/Services/AzureBlobStorageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,50 +12,67 @@ internal sealed class AzureBlobStorageService

internal async Task<UploadDocumentsResponse> UploadFilesAsync(IEnumerable<IFormFile> files, CancellationToken cancellationToken)
{
var uploadedFiles = new List<string>();
foreach (var file in files)
try
{
var fileName = file.FileName;

await using var stream = file.OpenReadStream();

using var documents = PdfReader.Open(stream, PdfDocumentOpenMode.Import);
for (int i = 0; i < documents.PageCount; i++)
var uploadedFiles = new List<string>();
foreach (var file in files)
{
var documentName = BlobNameFromFilePage(fileName, i);
var blobClient = _container.GetBlobClient(documentName);
if (await blobClient.ExistsAsync(cancellationToken))
{
continue;
}
var fileName = file.FileName;

var tempFileName = Path.GetTempFileName();
await using var stream = file.OpenReadStream();

try
using var documents = PdfReader.Open(stream, PdfDocumentOpenMode.Import);
for (int i = 0; i < documents.PageCount; i++)
{
using var document = new PdfDocument();
document.AddPage(documents.Pages[i]);
document.Save(tempFileName);
var documentName = BlobNameFromFilePage(fileName, i);
var blobClient = _container.GetBlobClient(documentName);
if (await blobClient.ExistsAsync(cancellationToken))
{
continue;
}

await using var tempStream = File.OpenRead(tempFileName);
await blobClient.UploadAsync(tempStream, new BlobHttpHeaders
var tempFileName = Path.GetTempFileName();

try
{
ContentType = "application/pdf"
}, cancellationToken: cancellationToken);
using var document = new PdfDocument();
document.AddPage(documents.Pages[i]);
document.Save(tempFileName);

uploadedFiles.Add(documentName);
}
finally
{
File.Delete(tempFileName);
await using var tempStream = File.OpenRead(tempFileName);
await blobClient.UploadAsync(tempStream, new BlobHttpHeaders
{
ContentType = "application/pdf"
}, cancellationToken: cancellationToken);

uploadedFiles.Add(documentName);
}
finally
{
File.Delete(tempFileName);
}
}
}
}

return new UploadDocumentsResponse(uploadedFiles.ToArray());
if (uploadedFiles.Count is 0)
{
return UploadDocumentsResponse.FromError("""
No files were uploaded. Either the files already exist or the files are not PDFs.
""");
}

return new UploadDocumentsResponse(uploadedFiles.ToArray());
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
{
return UploadDocumentsResponse.FromError(ex.ToString());
}
#pragma warning restore CA1031 // Do not catch general exception types
}

private static string BlobNameFromFilePage(string filename, int page = 0) => Path.GetExtension(filename).ToLower() is ".pdf"
private static string BlobNameFromFilePage(string filename, int page = 0) =>
Path.GetExtension(filename).ToLower() is ".pdf"
? $"{Path.GetFileNameWithoutExtension(filename)}-{page}.pdf"
: Path.GetFileName(filename);
}
7 changes: 5 additions & 2 deletions app/frontend/Pages/Docs.razor
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
</CardHeaderActions>
</MudCardHeader>
<MudCardContent>
<MudText Class="pb-4">Select up to ten PDF documents to upload, or explore the existing documents that have already been processed.</MudText>
<MudText Class="pb-4">
Select up to ten PDF documents to upload, or explore the existing documents that have already been processed.
Each file cannot exceed a file size of @(MaxIndividualFileSize.ToHumanReadableSize())
</MudText>
<MudFileUpload @ref="_fileUpload" T="IReadOnlyList<IBrowserFile>"
Accept=".pdf" MaximumFileCount="10" FilesChanged=@(files => StateHasChanged())
Required="true" RequiredError="You must select at least one PDF file to upload.">
Expand Down Expand Up @@ -81,7 +84,7 @@
Color="Color.Primary"
Disabled=@(!FilesSelected)
Size="Size.Large" Class="ml-auto mr-2 mb-2"
OnClick="@(async () => await SubmitFilesForUploadAsync())">
OnClick="@(async _ => await SubmitFilesForUploadAsync())">
Upload File(s)
</MudButton>
</MudCardActions>
Expand Down
44 changes: 39 additions & 5 deletions app/frontend/Pages/Docs.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ namespace ClientApp.Pages;

public sealed partial class Docs : IDisposable
{
private const long MaxIndividualFileSize = 1_024L * 1_024;

private MudForm _form = null!;
private MudFileUpload<IReadOnlyList<IBrowserFile>> _fileUpload = null!;
private Task _getDocumentsTask = null!;
Expand All @@ -20,6 +22,12 @@ public sealed partial class Docs : IDisposable
[Inject]
public required IDialogService Dialog { get; set; }

[Inject]
public required ISnackbar Snackbar { get; set; }

[Inject]
public required ILogger<Docs> Logger { get; set; }

private bool FilesSelected => _fileUpload is { Files.Count: > 0 };

protected override void OnInitialized() =>
Expand All @@ -28,7 +36,7 @@ protected override void OnInitialized() =>
_getDocumentsTask = GetDocumentsAsync();

private bool OnFilter(DocumentResponse document) => document is not null
&& (string.IsNullOrWhiteSpace(_filter) || document.Name.Contains(_filter, StringComparison.OrdinalIgnoreCase));
&& (string.IsNullOrWhiteSpace(_filter) || document.Name.Contains(_filter, StringComparison.OrdinalIgnoreCase));

private async Task GetDocumentsAsync()
{
Expand All @@ -54,11 +62,37 @@ await Client.GetDocumentsAsync(_cancellationTokenSource.Token)

private async Task SubmitFilesForUploadAsync()
{
await _form.Validate();

if (_form.IsValid && _fileUpload is { Files.Count: > 0 })
if (_fileUpload is { Files.Count: > 0 })
{
await Client.UploadDocumentsAsync(_fileUpload.Files);
var result = await Client.UploadDocumentsAsync(
_fileUpload.Files, MaxIndividualFileSize);

Logger.LogInformation("Result: {x}", result);

if (result.IsSuccessful)
{
Snackbar.Add(
$"Uploaded {result.UploadedFiles.Length} documents.",
Severity.Success,
static options =>
{
options.ShowCloseIcon = true;
options.VisibleStateDuration = 10_000;
});

await _fileUpload.ResetAsync();
}
else
{
Snackbar.Add(
result.Error,
Severity.Error,
static options =>
{
options.ShowCloseIcon = true;
options.VisibleStateDuration = 10_000;
});
}
}
}

Expand Down
4 changes: 3 additions & 1 deletion app/frontend/Services/ApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ public sealed class ApiClient
return await response.Content.ReadFromJsonAsync<ImageResponse>();
}

public async Task<UploadDocumentsResponse> UploadDocumentsAsync(IReadOnlyList<IBrowserFile> files)
public async Task<UploadDocumentsResponse> UploadDocumentsAsync(
IReadOnlyList<IBrowserFile> files,
long maxAllowedSize)
{
try
{
Expand Down
3 changes: 2 additions & 1 deletion app/shared/Shared/Models/UploadDocumentsResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ public record class UploadDocumentsResponse(
UploadedFiles.Length: > 0
};

public static UploadDocumentsResponse FromError(string error) => new(Array.Empty<string>(), error);
public static UploadDocumentsResponse FromError(string error) =>
new(Array.Empty<string>(), error);
}

0 comments on commit 92ae671

Please sign in to comment.