2025-02-09 11:36:37 +00:00
|
|
|
using System.Text;
|
|
|
|
|
|
2025-05-18 19:31:15 +00:00
|
|
|
using AIStudio.Components;
|
2025-02-09 11:36:37 +00:00
|
|
|
using AIStudio.Settings;
|
|
|
|
|
using AIStudio.Settings.DataModel;
|
|
|
|
|
|
|
|
|
|
using Microsoft.AspNetCore.Components;
|
|
|
|
|
|
|
|
|
|
using Timer = System.Timers.Timer;
|
|
|
|
|
|
|
|
|
|
namespace AIStudio.Dialogs;
|
|
|
|
|
|
2026-08-23 19:15:37 +00:00
|
|
|
public partial class DataSourceLocalDirectoryInfoDialog : MSGComponentBase
|
2025-02-09 11:36:37 +00:00
|
|
|
{
|
|
|
|
|
[CascadingParameter]
|
2025-03-12 18:12:56 +00:00
|
|
|
private IMudDialogInstance MudDialog { get; set; } = null!;
|
2025-02-09 11:36:37 +00:00
|
|
|
|
|
|
|
|
[Parameter]
|
|
|
|
|
public DataSourceLocalDirectory DataSource { get; set; }
|
|
|
|
|
|
|
|
|
|
private readonly Timer refreshTimer = new(TimeSpan.FromSeconds(1.6))
|
|
|
|
|
{
|
|
|
|
|
AutoReset = true,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
#region Overrides of ComponentBase
|
|
|
|
|
|
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
|
|
|
{
|
2026-01-09 14:41:54 +00:00
|
|
|
this.embeddingProvider = this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.DataSource.EmbeddingId) ?? EmbeddingProvider.NONE;
|
2025-02-09 11:36:37 +00:00
|
|
|
this.directoryInfo = new DirectoryInfo(this.DataSource.Path);
|
|
|
|
|
|
|
|
|
|
if (this.directoryInfo.Exists)
|
|
|
|
|
{
|
|
|
|
|
this.directorySizeTask = this.directoryInfo.DetermineContentSize(this.UpdateDirectorySize, this.UpdateDirectoryFiles, this.UpdateFileList, MAX_FILES_TO_SHOW, this.DirectoryOperationDone, this.cts.Token);
|
|
|
|
|
this.refreshTimer.Elapsed += (_, _) => this.InvokeAsync(this.StateHasChanged);
|
|
|
|
|
this.refreshTimer.Start();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await base.OnInitializedAsync();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
private const int MAX_FILES_TO_SHOW = 100;
|
|
|
|
|
|
|
|
|
|
private readonly CancellationTokenSource cts = new();
|
|
|
|
|
|
2026-01-09 14:41:54 +00:00
|
|
|
private EmbeddingProvider embeddingProvider = EmbeddingProvider.NONE;
|
2025-02-09 11:36:37 +00:00
|
|
|
private DirectoryInfo directoryInfo = null!;
|
|
|
|
|
private long directorySizeBytes;
|
|
|
|
|
private long directorySizeNumFiles;
|
|
|
|
|
private readonly StringBuilder directoryFiles = new();
|
2025-12-08 19:53:46 +00:00
|
|
|
private string directoryFilesText = string.Empty;
|
2025-02-09 11:36:37 +00:00
|
|
|
private Task directorySizeTask = Task.CompletedTask;
|
|
|
|
|
|
|
|
|
|
private bool IsOperationInProgress { get; set; } = true;
|
|
|
|
|
|
2026-06-21 13:16:37 +00:00
|
|
|
private bool IsCloudEmbedding => !this.embeddingProvider.IsTrustedForDataSourceSecurityChecks(this.SettingsManager);
|
2025-02-09 11:36:37 +00:00
|
|
|
|
|
|
|
|
private bool IsDirectoryAvailable => this.directoryInfo.Exists;
|
|
|
|
|
|
2026-09-16 15:08:37 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Takes the next file which the directory scan found.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <remarks>
|
|
|
|
|
/// This runs on the scan's own thread, and it does so deliberately, although the scan asks its
|
|
|
|
|
/// callers to reach for a dispatcher. That request is about updating the UI, and none of these
|
|
|
|
|
/// callbacks does: they write fields and nothing else.<br/><br/>
|
|
|
|
|
/// Why that holds is worth writing down, because the code does not show it. The string builder
|
|
|
|
|
/// has exactly one writer -- this method, on that one thread -- and nobody else ever reads it.
|
|
|
|
|
/// What the renderer reads is the text field beside it, and assigning a string reference is
|
|
|
|
|
/// atomic, so a render sees the whole previous text or the whole new one, never half of either.
|
|
|
|
|
/// Building that text anew costs little, because the scan stops reporting files once it has
|
|
|
|
|
/// reported a hundred. And a render happens only when the refresh timer ticks, which goes
|
|
|
|
|
/// through the dispatcher, so a reading taken a moment too early is replaced 1.6 seconds later
|
|
|
|
|
/// anyway.
|
|
|
|
|
/// </remarks>
|
2025-02-09 11:36:37 +00:00
|
|
|
private void UpdateFileList(string file)
|
|
|
|
|
{
|
|
|
|
|
this.directoryFiles.Append("- ");
|
|
|
|
|
this.directoryFiles.AppendLine(file);
|
2025-12-08 19:53:46 +00:00
|
|
|
this.directoryFilesText = this.directoryFiles.ToString();
|
2025-02-09 11:36:37 +00:00
|
|
|
}
|
|
|
|
|
|
2026-09-16 15:08:37 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Takes the size which the directory scan has added up so far.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <remarks>
|
|
|
|
|
/// Two threads call this, but never at the same time: the scan reports its progress from its
|
|
|
|
|
/// own thread, and the final figure follows once that thread has finished. A long is written in
|
|
|
|
|
/// one piece on all six targets we ship, which are 64 bit throughout, and the renderer reads it
|
|
|
|
|
/// only when the refresh timer ticks. The remark on the file list carries the reasoning these
|
|
|
|
|
/// callbacks share.
|
|
|
|
|
/// </remarks>
|
2025-02-09 11:36:37 +00:00
|
|
|
private void UpdateDirectorySize(long size)
|
|
|
|
|
{
|
|
|
|
|
this.directorySizeBytes = size;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-16 15:08:37 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Takes the number of files which the directory scan has counted so far.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <remarks>
|
|
|
|
|
/// Reported from the same two threads as the size above, and safe for the same reason.
|
|
|
|
|
/// </remarks>
|
2025-02-09 11:36:37 +00:00
|
|
|
private void UpdateDirectoryFiles(long numFiles) => this.directorySizeNumFiles = numFiles;
|
|
|
|
|
|
2026-09-16 15:08:37 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Takes the news that the directory scan has finished.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <remarks>
|
|
|
|
|
/// This one, unlike the three above, does not run on the scan's thread. The scan invokes it
|
|
|
|
|
/// after awaiting its worker, and that continuation returns to the dispatcher this dialog was
|
|
|
|
|
/// initialized on. Stopping the timer and asking for a render from here is therefore no
|
|
|
|
|
/// different from doing either in a lifecycle method.
|
|
|
|
|
/// </remarks>
|
2025-02-09 11:36:37 +00:00
|
|
|
private void DirectoryOperationDone()
|
|
|
|
|
{
|
|
|
|
|
this.refreshTimer.Stop();
|
|
|
|
|
this.IsOperationInProgress = false;
|
|
|
|
|
this.InvokeAsync(this.StateHasChanged);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string NumberFilesInDirectory => $"{this.directorySizeNumFiles:###,###,###,###}";
|
|
|
|
|
|
|
|
|
|
private void Close()
|
|
|
|
|
{
|
|
|
|
|
this.cts.Cancel();
|
|
|
|
|
this.MudDialog.Close();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-23 19:15:37 +00:00
|
|
|
#region Overrides of MSGComponentBase
|
2025-02-09 11:36:37 +00:00
|
|
|
|
2026-08-23 19:15:37 +00:00
|
|
|
protected override async ValueTask DisposeResourcesAsync()
|
2025-02-09 11:36:37 +00:00
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await this.cts.CancelAsync();
|
|
|
|
|
await this.directorySizeTask;
|
|
|
|
|
|
|
|
|
|
this.cts.Dispose();
|
|
|
|
|
this.refreshTimer.Stop();
|
|
|
|
|
this.refreshTimer.Dispose();
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// ignored
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
}
|