mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-12 10:13:48 +00:00
Configure data sources (#259)
This commit is contained in:
1 parent
06f66fdab2
commit
63be312bb4
47 files changed
+3388
-283
No files matched your search
@@ -0,0 +1,16 @@
|
||||
using ERI_Client.V1;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public static class AuthMethodsV1Extensions
|
||||
{
|
||||
public static string DisplayName(this AuthMethod authMethod) => authMethod switch
|
||||
{
|
||||
AuthMethod.NONE => "None",
|
||||
AuthMethod.USERNAME_PASSWORD => "Username & Password",
|
||||
AuthMethod.KERBEROS => "SSO (Kerberos)",
|
||||
AuthMethod.TOKEN => "Access Token",
|
||||
|
||||
_ => "Unknown authentication method",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace AIStudio.Tools.Rust;
|
||||
|
||||
/// <summary>
|
||||
/// Data structure for selecting a file.
|
||||
/// </summary>
|
||||
/// <param name="UserCancelled">Was the file selection canceled?</param>
|
||||
/// <param name="SelectedFilePath">The selected file, if any.</param>
|
||||
public readonly record struct FileSelectionResponse(bool UserCancelled, string SelectedFilePath);
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace AIStudio.Tools.Rust;
|
||||
|
||||
/// <summary>
|
||||
/// Data structure for selecting a file when a previous file was selected.
|
||||
/// </summary>
|
||||
/// <param name="FilePath">The path of the previous file.</param>
|
||||
public readonly record struct PreviousFile(string FilePath);
|
||||
@@ -0,0 +1,76 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
/// <summary>
|
||||
/// Try to get the API key for the given secret ID.
|
||||
/// </summary>
|
||||
/// <param name="secretId">The secret ID to get the API key for.</param>
|
||||
/// <param name="isTrying">Indicates if we are trying to get the API key. In that case, we don't log errors.</param>
|
||||
/// <returns>The requested secret.</returns>
|
||||
public async Task<RequestedSecret> GetAPIKey(ISecretId secretId, bool isTrying = false)
|
||||
{
|
||||
var secretRequest = new SelectSecretRequest($"provider::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, isTrying);
|
||||
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
if(!isTrying)
|
||||
this.logger!.LogError($"Failed to get the API key for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
||||
return new RequestedSecret(false, new EncryptedText(string.Empty), "Failed to get the API key due to an API issue.");
|
||||
}
|
||||
|
||||
var secret = await result.Content.ReadFromJsonAsync<RequestedSecret>(this.jsonRustSerializerOptions);
|
||||
if (!secret.Success && !isTrying)
|
||||
this.logger!.LogError($"Failed to get the API key for secret ID '{secretId.SecretId}': '{secret.Issue}'");
|
||||
|
||||
return secret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to store the API key for the given secret ID.
|
||||
/// </summary>
|
||||
/// <param name="secretId">The secret ID to store the API key for.</param>
|
||||
/// <param name="key">The API key to store.</param>
|
||||
/// <returns>The store secret response.</returns>
|
||||
public async Task<StoreSecretResponse> SetAPIKey(ISecretId secretId, string key)
|
||||
{
|
||||
var encryptedKey = await this.encryptor!.Encrypt(key);
|
||||
var request = new StoreSecretRequest($"provider::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, encryptedKey);
|
||||
var result = await this.http.PostAsJsonAsync("/secrets/store", request, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to store the API key for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
||||
return new StoreSecretResponse(false, "Failed to get the API key due to an API issue.");
|
||||
}
|
||||
|
||||
var state = await result.Content.ReadFromJsonAsync<StoreSecretResponse>(this.jsonRustSerializerOptions);
|
||||
if (!state.Success)
|
||||
this.logger!.LogError($"Failed to store the API key for secret ID '{secretId.SecretId}': '{state.Issue}'");
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to delete the API key for the given secret ID.
|
||||
/// </summary>
|
||||
/// <param name="secretId">The secret ID to delete the API key for.</param>
|
||||
/// <returns>The delete secret response.</returns>
|
||||
public async Task<DeleteSecretResponse> DeleteAPIKey(ISecretId secretId)
|
||||
{
|
||||
var request = new SelectSecretRequest($"provider::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, false);
|
||||
var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to delete the API key for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
||||
return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = "Failed to delete the API key due to an API issue."};
|
||||
}
|
||||
|
||||
var state = await result.Content.ReadFromJsonAsync<DeleteSecretResponse>(this.jsonRustSerializerOptions);
|
||||
if (!state.Success)
|
||||
this.logger!.LogError($"Failed to delete the API key for secret ID '{secretId.SecretId}': '{state.Issue}'");
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
public async Task<int> GetAppPort()
|
||||
{
|
||||
Console.WriteLine("Trying to get app port from Rust runtime...");
|
||||
|
||||
//
|
||||
// Note I: In the production environment, the Rust runtime is already running
|
||||
// and listening on the given port. In the development environment, the IDE
|
||||
// starts the Rust runtime in parallel with the .NET runtime. Since the
|
||||
// Rust runtime needs some time to start, we have to wait for it to be ready.
|
||||
//
|
||||
const int MAX_TRIES = 160;
|
||||
var tris = 0;
|
||||
var wait4Try = TimeSpan.FromMilliseconds(250);
|
||||
var url = new Uri($"https://127.0.0.1:{this.apiPort}/system/dotnet/port");
|
||||
while (tris++ < MAX_TRIES)
|
||||
{
|
||||
//
|
||||
// Note II: We use a new HttpClient instance for each try to avoid
|
||||
// .NET is caching the result. When we use the same HttpClient
|
||||
// instance, we would always get the same result (403 forbidden),
|
||||
// without even trying to connect to the Rust server.
|
||||
//
|
||||
|
||||
using var initialHttp = new HttpClient(new HttpClientHandler
|
||||
{
|
||||
//
|
||||
// Note III: We have to create also a new HttpClientHandler instance
|
||||
// for each try to avoid .NET is caching the result. This is necessary
|
||||
// because it gets disposed when the HttpClient instance gets disposed.
|
||||
//
|
||||
ServerCertificateCustomValidationCallback = (_, certificate, _, _) =>
|
||||
{
|
||||
if(certificate is null)
|
||||
return false;
|
||||
|
||||
var currentCertificateFingerprint = certificate.GetCertHashString(HashAlgorithmName.SHA256);
|
||||
return currentCertificateFingerprint == this.certificateFingerprint;
|
||||
}
|
||||
});
|
||||
|
||||
initialHttp.DefaultRequestVersion = Version.Parse("2.0");
|
||||
initialHttp.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
|
||||
initialHttp.DefaultRequestHeaders.AddApiToken();
|
||||
|
||||
try
|
||||
{
|
||||
var response = await initialHttp.GetAsync(url);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($"Try {tris}/{MAX_TRIES} to get the app port from Rust runtime");
|
||||
await Task.Delay(wait4Try);
|
||||
continue;
|
||||
}
|
||||
|
||||
var appPortContent = await response.Content.ReadAsStringAsync();
|
||||
var appPort = int.Parse(appPortContent);
|
||||
Console.WriteLine($"Received app port from Rust runtime: '{appPort}'");
|
||||
return appPort;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"Error: Was not able to get the app port from Rust runtime: '{e.Message}'");
|
||||
Console.WriteLine(e.InnerException);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("Failed to receive the app port from Rust runtime.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
public async Task AppIsReady()
|
||||
{
|
||||
const string URL = "/system/dotnet/ready";
|
||||
this.logger!.LogInformation("Notifying Rust runtime that the app is ready.");
|
||||
try
|
||||
{
|
||||
var response = await this.http.GetAsync(URL);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to notify Rust runtime that the app is ready: '{response.StatusCode}'");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.logger!.LogError(e, "Failed to notify the Rust runtime that the app is ready.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GetConfigDirectory()
|
||||
{
|
||||
var response = await this.http.GetAsync("/system/directories/config");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to get the config directory from Rust: '{response.StatusCode}'");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
public async Task<string> GetDataDirectory()
|
||||
{
|
||||
var response = await this.http.GetAsync("/system/directories/data");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to get the data directory from Rust: '{response.StatusCode}'");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to copy the given text to the clipboard.
|
||||
/// </summary>
|
||||
/// <param name="snackbar">The snackbar to show the result.</param>
|
||||
/// <param name="text">The text to copy to the clipboard.</param>
|
||||
public async Task CopyText2Clipboard(ISnackbar snackbar, string text)
|
||||
{
|
||||
var message = "Successfully copied the text to your clipboard";
|
||||
var iconColor = Color.Error;
|
||||
var severity = Severity.Error;
|
||||
try
|
||||
{
|
||||
var encryptedText = await text.Encrypt(this.encryptor!);
|
||||
var response = await this.http.PostAsync("/clipboard/set", new StringContent(encryptedText.EncryptedData));
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to copy the text to the clipboard due to an network error: '{response.StatusCode}'");
|
||||
message = "Failed to copy the text to your clipboard.";
|
||||
return;
|
||||
}
|
||||
|
||||
var state = await response.Content.ReadFromJsonAsync<SetClipboardResponse>(this.jsonRustSerializerOptions);
|
||||
if (!state.Success)
|
||||
{
|
||||
this.logger!.LogError("Failed to copy the text to the clipboard.");
|
||||
message = "Failed to copy the text to your clipboard.";
|
||||
return;
|
||||
}
|
||||
|
||||
iconColor = Color.Success;
|
||||
severity = Severity.Success;
|
||||
this.logger!.LogDebug("Successfully copied the text to the clipboard.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
snackbar.Add(message, severity, config =>
|
||||
{
|
||||
config.Icon = Icons.Material.Filled.ContentCopy;
|
||||
config.IconSize = Size.Large;
|
||||
config.IconColor = iconColor;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
public async Task<DirectorySelectionResponse> SelectDirectory(string title, string? initialDirectory = null)
|
||||
{
|
||||
PreviousDirectory? previousDirectory = initialDirectory is null ? null : new (initialDirectory);
|
||||
var result = await this.http.PostAsJsonAsync($"/select/directory?title={title}", previousDirectory, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to select a directory: '{result.StatusCode}'");
|
||||
return new DirectorySelectionResponse(true, string.Empty);
|
||||
}
|
||||
|
||||
return await result.Content.ReadFromJsonAsync<DirectorySelectionResponse>(this.jsonRustSerializerOptions);
|
||||
}
|
||||
|
||||
public async Task<FileSelectionResponse> SelectFile(string title, string? initialFile = null)
|
||||
{
|
||||
PreviousFile? previousFile = initialFile is null ? null : new (initialFile);
|
||||
var result = await this.http.PostAsJsonAsync($"/select/file?title={title}", previousFile, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to select a file: '{result.StatusCode}'");
|
||||
return new FileSelectionResponse(true, string.Empty);
|
||||
}
|
||||
|
||||
return await result.Content.ReadFromJsonAsync<FileSelectionResponse>(this.jsonRustSerializerOptions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
/// <summary>
|
||||
/// Try to get the secret data for the given secret ID.
|
||||
/// </summary>
|
||||
/// <param name="secretId">The secret ID to get the data for.</param>
|
||||
/// <param name="isTrying">Indicates if we are trying to get the data. In that case, we don't log errors.</param>
|
||||
/// <returns>The requested secret.</returns>
|
||||
public async Task<RequestedSecret> GetSecret(ISecretId secretId, bool isTrying = false)
|
||||
{
|
||||
var secretRequest = new SelectSecretRequest($"secret::{secretId.SecretId}::{secretId.SecretName}", Environment.UserName, isTrying);
|
||||
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
if(!isTrying)
|
||||
this.logger!.LogError($"Failed to get the secret data for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
||||
return new RequestedSecret(false, new EncryptedText(string.Empty), "Failed to get the secret data due to an API issue.");
|
||||
}
|
||||
|
||||
var secret = await result.Content.ReadFromJsonAsync<RequestedSecret>(this.jsonRustSerializerOptions);
|
||||
if (!secret.Success && !isTrying)
|
||||
this.logger!.LogError($"Failed to get the secret data for secret ID '{secretId.SecretId}': '{secret.Issue}'");
|
||||
|
||||
return secret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to store the secret data for the given secret ID.
|
||||
/// </summary>
|
||||
/// <param name="secretId">The secret ID to store the data for.</param>
|
||||
/// <param name="secretData">The data to store.</param>
|
||||
/// <returns>The store secret response.</returns>
|
||||
public async Task<StoreSecretResponse> SetSecret(ISecretId secretId, string secretData)
|
||||
{
|
||||
var encryptedSecret = await this.encryptor!.Encrypt(secretData);
|
||||
var request = new StoreSecretRequest($"secret::{secretId.SecretId}::{secretId.SecretName}", Environment.UserName, encryptedSecret);
|
||||
var result = await this.http.PostAsJsonAsync("/secrets/store", request, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to store the secret data for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
||||
return new StoreSecretResponse(false, "Failed to get the secret data due to an API issue.");
|
||||
}
|
||||
|
||||
var state = await result.Content.ReadFromJsonAsync<StoreSecretResponse>(this.jsonRustSerializerOptions);
|
||||
if (!state.Success)
|
||||
this.logger!.LogError($"Failed to store the secret data for secret ID '{secretId.SecretId}': '{state.Issue}'");
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to delete the secret data for the given secret ID.
|
||||
/// </summary>
|
||||
/// <param name="secretId">The secret ID to delete the data for.</param>
|
||||
/// <returns>The delete secret response.</returns>
|
||||
public async Task<DeleteSecretResponse> DeleteSecret(ISecretId secretId)
|
||||
{
|
||||
var request = new SelectSecretRequest($"secret::{secretId.SecretId}::{secretId.SecretName}", Environment.UserName, false);
|
||||
var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to delete the secret data for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
||||
return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = "Failed to delete the secret data due to an API issue."};
|
||||
}
|
||||
|
||||
var state = await result.Content.ReadFromJsonAsync<DeleteSecretResponse>(this.jsonRustSerializerOptions);
|
||||
if (!state.Success)
|
||||
this.logger!.LogError($"Failed to delete the secret data for secret ID '{secretId.SecretId}': '{state.Issue}'");
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
public async Task<UpdateResponse> CheckForUpdate()
|
||||
{
|
||||
try
|
||||
{
|
||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(45));
|
||||
var response = await this.http.GetFromJsonAsync<UpdateResponse>("/updates/check", this.jsonRustSerializerOptions, cts.Token);
|
||||
this.logger!.LogInformation($"Checked for an update: update available='{response.UpdateIsAvailable}'; error='{response.Error}'; next version='{response.NewVersion}'; changelog len='{response.Changelog.Length}'");
|
||||
return response;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.logger!.LogError(e, "Failed to check for an update.");
|
||||
return new UpdateResponse
|
||||
{
|
||||
Error = true,
|
||||
UpdateIsAvailable = false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task InstallUpdate()
|
||||
{
|
||||
try
|
||||
{
|
||||
var cts = new CancellationTokenSource();
|
||||
await this.http.GetAsync("/updates/install", cts.Token);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
// ReSharper disable NotAccessedPositionalProperty.Local
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
@@ -10,7 +8,7 @@ namespace AIStudio.Tools;
|
||||
/// <summary>
|
||||
/// Calling Rust functions.
|
||||
/// </summary>
|
||||
public sealed class RustService : IDisposable
|
||||
public sealed partial class RustService : IDisposable
|
||||
{
|
||||
private readonly HttpClient http;
|
||||
|
||||
@@ -60,281 +58,6 @@ public sealed class RustService : IDisposable
|
||||
{
|
||||
this.encryptor = encryptionService;
|
||||
}
|
||||
|
||||
public async Task<int> GetAppPort()
|
||||
{
|
||||
Console.WriteLine("Trying to get app port from Rust runtime...");
|
||||
|
||||
//
|
||||
// Note I: In the production environment, the Rust runtime is already running
|
||||
// and listening on the given port. In the development environment, the IDE
|
||||
// starts the Rust runtime in parallel with the .NET runtime. Since the
|
||||
// Rust runtime needs some time to start, we have to wait for it to be ready.
|
||||
//
|
||||
const int MAX_TRIES = 160;
|
||||
var tris = 0;
|
||||
var wait4Try = TimeSpan.FromMilliseconds(250);
|
||||
var url = new Uri($"https://127.0.0.1:{this.apiPort}/system/dotnet/port");
|
||||
while (tris++ < MAX_TRIES)
|
||||
{
|
||||
//
|
||||
// Note II: We use a new HttpClient instance for each try to avoid
|
||||
// .NET is caching the result. When we use the same HttpClient
|
||||
// instance, we would always get the same result (403 forbidden),
|
||||
// without even trying to connect to the Rust server.
|
||||
//
|
||||
|
||||
using var initialHttp = new HttpClient(new HttpClientHandler
|
||||
{
|
||||
//
|
||||
// Note III: We have to create also a new HttpClientHandler instance
|
||||
// for each try to avoid .NET is caching the result. This is necessary
|
||||
// because it gets disposed when the HttpClient instance gets disposed.
|
||||
//
|
||||
ServerCertificateCustomValidationCallback = (_, certificate, _, _) =>
|
||||
{
|
||||
if(certificate is null)
|
||||
return false;
|
||||
|
||||
var currentCertificateFingerprint = certificate.GetCertHashString(HashAlgorithmName.SHA256);
|
||||
return currentCertificateFingerprint == this.certificateFingerprint;
|
||||
}
|
||||
});
|
||||
|
||||
initialHttp.DefaultRequestVersion = Version.Parse("2.0");
|
||||
initialHttp.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
|
||||
initialHttp.DefaultRequestHeaders.AddApiToken();
|
||||
|
||||
try
|
||||
{
|
||||
var response = await initialHttp.GetAsync(url);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($"Try {tris}/{MAX_TRIES} to get the app port from Rust runtime");
|
||||
await Task.Delay(wait4Try);
|
||||
continue;
|
||||
}
|
||||
|
||||
var appPortContent = await response.Content.ReadAsStringAsync();
|
||||
var appPort = int.Parse(appPortContent);
|
||||
Console.WriteLine($"Received app port from Rust runtime: '{appPort}'");
|
||||
return appPort;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"Error: Was not able to get the app port from Rust runtime: '{e.Message}'");
|
||||
Console.WriteLine(e.InnerException);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("Failed to receive the app port from Rust runtime.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
public async Task AppIsReady()
|
||||
{
|
||||
const string URL = "/system/dotnet/ready";
|
||||
this.logger!.LogInformation("Notifying Rust runtime that the app is ready.");
|
||||
try
|
||||
{
|
||||
var response = await this.http.GetAsync(URL);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to notify Rust runtime that the app is ready: '{response.StatusCode}'");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.logger!.LogError(e, "Failed to notify the Rust runtime that the app is ready.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GetConfigDirectory()
|
||||
{
|
||||
var response = await this.http.GetAsync("/system/directories/config");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to get the config directory from Rust: '{response.StatusCode}'");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
public async Task<string> GetDataDirectory()
|
||||
{
|
||||
var response = await this.http.GetAsync("/system/directories/data");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to get the data directory from Rust: '{response.StatusCode}'");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to copy the given text to the clipboard.
|
||||
/// </summary>
|
||||
/// <param name="snackbar">The snackbar to show the result.</param>
|
||||
/// <param name="text">The text to copy to the clipboard.</param>
|
||||
public async Task CopyText2Clipboard(ISnackbar snackbar, string text)
|
||||
{
|
||||
var message = "Successfully copied the text to your clipboard";
|
||||
var iconColor = Color.Error;
|
||||
var severity = Severity.Error;
|
||||
try
|
||||
{
|
||||
var encryptedText = await text.Encrypt(this.encryptor!);
|
||||
var response = await this.http.PostAsync("/clipboard/set", new StringContent(encryptedText.EncryptedData));
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to copy the text to the clipboard due to an network error: '{response.StatusCode}'");
|
||||
message = "Failed to copy the text to your clipboard.";
|
||||
return;
|
||||
}
|
||||
|
||||
var state = await response.Content.ReadFromJsonAsync<SetClipboardResponse>(this.jsonRustSerializerOptions);
|
||||
if (!state.Success)
|
||||
{
|
||||
this.logger!.LogError("Failed to copy the text to the clipboard.");
|
||||
message = "Failed to copy the text to your clipboard.";
|
||||
return;
|
||||
}
|
||||
|
||||
iconColor = Color.Success;
|
||||
severity = Severity.Success;
|
||||
this.logger!.LogDebug("Successfully copied the text to the clipboard.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
snackbar.Add(message, severity, config =>
|
||||
{
|
||||
config.Icon = Icons.Material.Filled.ContentCopy;
|
||||
config.IconSize = Size.Large;
|
||||
config.IconColor = iconColor;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<UpdateResponse> CheckForUpdate()
|
||||
{
|
||||
try
|
||||
{
|
||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(45));
|
||||
var response = await this.http.GetFromJsonAsync<UpdateResponse>("/updates/check", this.jsonRustSerializerOptions, cts.Token);
|
||||
this.logger!.LogInformation($"Checked for an update: update available='{response.UpdateIsAvailable}'; error='{response.Error}'; next version='{response.NewVersion}'; changelog len='{response.Changelog.Length}'");
|
||||
return response;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.logger!.LogError(e, "Failed to check for an update.");
|
||||
return new UpdateResponse
|
||||
{
|
||||
Error = true,
|
||||
UpdateIsAvailable = false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task InstallUpdate()
|
||||
{
|
||||
try
|
||||
{
|
||||
var cts = new CancellationTokenSource();
|
||||
await this.http.GetAsync("/updates/install", cts.Token);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to get the API key for the given secret ID.
|
||||
/// </summary>
|
||||
/// <param name="secretId">The secret ID to get the API key for.</param>
|
||||
/// <param name="isTrying">Indicates if we are trying to get the API key. In that case, we don't log errors.</param>
|
||||
/// <returns>The requested secret.</returns>
|
||||
public async Task<RequestedSecret> GetAPIKey(ISecretId secretId, bool isTrying = false)
|
||||
{
|
||||
var secretRequest = new SelectSecretRequest($"provider::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, isTrying);
|
||||
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
if(!isTrying)
|
||||
this.logger!.LogError($"Failed to get the API key for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
||||
return new RequestedSecret(false, new EncryptedText(string.Empty), "Failed to get the API key due to an API issue.");
|
||||
}
|
||||
|
||||
var secret = await result.Content.ReadFromJsonAsync<RequestedSecret>(this.jsonRustSerializerOptions);
|
||||
if (!secret.Success && !isTrying)
|
||||
this.logger!.LogError($"Failed to get the API key for secret ID '{secretId.SecretId}': '{secret.Issue}'");
|
||||
|
||||
return secret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to store the API key for the given secret ID.
|
||||
/// </summary>
|
||||
/// <param name="secretId">The secret ID to store the API key for.</param>
|
||||
/// <param name="key">The API key to store.</param>
|
||||
/// <returns>The store secret response.</returns>
|
||||
public async Task<StoreSecretResponse> SetAPIKey(ISecretId secretId, string key)
|
||||
{
|
||||
var encryptedKey = await this.encryptor!.Encrypt(key);
|
||||
var request = new StoreSecretRequest($"provider::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, encryptedKey);
|
||||
var result = await this.http.PostAsJsonAsync("/secrets/store", request, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to store the API key for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
||||
return new StoreSecretResponse(false, "Failed to get the API key due to an API issue.");
|
||||
}
|
||||
|
||||
var state = await result.Content.ReadFromJsonAsync<StoreSecretResponse>(this.jsonRustSerializerOptions);
|
||||
if (!state.Success)
|
||||
this.logger!.LogError($"Failed to store the API key for secret ID '{secretId.SecretId}': '{state.Issue}'");
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to delete the API key for the given secret ID.
|
||||
/// </summary>
|
||||
/// <param name="secretId">The secret ID to delete the API key for.</param>
|
||||
/// <returns>The delete secret response.</returns>
|
||||
public async Task<DeleteSecretResponse> DeleteAPIKey(ISecretId secretId)
|
||||
{
|
||||
var request = new SelectSecretRequest($"provider::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, false);
|
||||
var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to delete the API key for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
||||
return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = "Failed to delete the API key due to an API issue."};
|
||||
}
|
||||
|
||||
var state = await result.Content.ReadFromJsonAsync<DeleteSecretResponse>(this.jsonRustSerializerOptions);
|
||||
if (!state.Success)
|
||||
this.logger!.LogError($"Failed to delete the API key for secret ID '{secretId.SecretId}': '{state.Issue}'");
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
public async Task<DirectorySelectionResponse> SelectDirectory(string title, string? initialDirectory = null)
|
||||
{
|
||||
PreviousDirectory? previousDirectory = initialDirectory is null ? null : new (initialDirectory);
|
||||
var result = await this.http.PostAsJsonAsync($"/select/directory?title={title}", previousDirectory, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to select a directory: '{result.StatusCode}'");
|
||||
return new DirectorySelectionResponse(true, string.Empty);
|
||||
}
|
||||
|
||||
return await result.Content.ReadFromJsonAsync<DirectorySelectionResponse>(this.jsonRustSerializerOptions);
|
||||
}
|
||||
|
||||
#region IDisposable
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
using ERI_Client.V1;
|
||||
|
||||
namespace AIStudio.Tools.Validation;
|
||||
|
||||
public sealed class DataSourceValidation
|
||||
{
|
||||
public Func<string> GetSecretStorageIssue { get; init; } = () => string.Empty;
|
||||
|
||||
public Func<string> GetPreviousDataSourceName { get; init; } = () => string.Empty;
|
||||
|
||||
public Func<IEnumerable<string>> GetUsedDataSourceNames { get; init; } = () => [];
|
||||
|
||||
public Func<AuthMethod> GetAuthMethod { get; init; } = () => AuthMethod.NONE;
|
||||
|
||||
public Func<bool> GetSelectedCloudEmbedding { get; init; } = () => false;
|
||||
|
||||
public Func<bool> GetTestedConnection { get; init; } = () => false;
|
||||
|
||||
public Func<bool> GetTestedConnectionResult { get; init; } = () => false;
|
||||
|
||||
public Func<IReadOnlyList<AuthMethod>> GetAvailableAuthMethods { get; init; } = () => [];
|
||||
|
||||
public string? ValidatingHostname(string hostname)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(hostname))
|
||||
return "Please enter a hostname, e.g., http://localhost:1234";
|
||||
|
||||
if(!hostname.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) && !hostname.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase))
|
||||
return "The hostname must start with either http:// or https://";
|
||||
|
||||
if(!Uri.TryCreate(hostname, UriKind.Absolute, out _))
|
||||
return "The hostname is not a valid HTTP(S) URL.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidatePort(int port)
|
||||
{
|
||||
if(port is < 1 or > 65535)
|
||||
return "The port must be between 1 and 65535.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidateUsername(string username)
|
||||
{
|
||||
if(this.GetAuthMethod() is not AuthMethod.USERNAME_PASSWORD)
|
||||
return null;
|
||||
|
||||
if(string.IsNullOrWhiteSpace(username))
|
||||
return "The username must not be empty.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidatingSecret(string secret)
|
||||
{
|
||||
var authMethod = this.GetAuthMethod();
|
||||
if(authMethod is AuthMethod.NONE or AuthMethod.KERBEROS)
|
||||
return null;
|
||||
|
||||
var secretStorageIssue = this.GetSecretStorageIssue();
|
||||
if(!string.IsNullOrWhiteSpace(secretStorageIssue))
|
||||
return secretStorageIssue;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(secret))
|
||||
return authMethod switch
|
||||
{
|
||||
AuthMethod.TOKEN => "Please enter your secure access token.",
|
||||
AuthMethod.USERNAME_PASSWORD => "Please enter your password.",
|
||||
|
||||
_ => "Please enter the secret necessary for authentication."
|
||||
};
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidatingName(string dataSourceName)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(dataSourceName))
|
||||
return "The name must not be empty.";
|
||||
|
||||
if (dataSourceName.Length > 40)
|
||||
return "The name must not exceed 40 characters.";
|
||||
|
||||
var lowerName = dataSourceName.ToLowerInvariant();
|
||||
if(lowerName != this.GetPreviousDataSourceName() && this.GetUsedDataSourceNames().Contains(lowerName))
|
||||
return "The name is already used by another data source. Please choose a different name.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidatePath(string path)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(path))
|
||||
return "The path must not be empty. Please select a directory.";
|
||||
|
||||
if(!Directory.Exists(path))
|
||||
return "The path does not exist. Please select a valid directory.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidateFilePath(string filePath)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(filePath))
|
||||
return "The file path must not be empty. Please select a file.";
|
||||
|
||||
if(!File.Exists(filePath))
|
||||
return "The file does not exist. Please select a valid file.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidateEmbeddingId(string embeddingId)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(embeddingId))
|
||||
return "Please select an embedding provider.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidateUserAcknowledgedCloudEmbedding(bool value)
|
||||
{
|
||||
if(this.GetSelectedCloudEmbedding() && !value)
|
||||
return "Please acknowledge that you are aware of the cloud embedding implications.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidateTestedConnection()
|
||||
{
|
||||
if(!this.GetTestedConnection())
|
||||
return "Please test the connection before saving.";
|
||||
|
||||
if(!this.GetTestedConnectionResult())
|
||||
return "The connection test failed. Please check the connection settings.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidateAuthMethod(AuthMethod authMethod)
|
||||
{
|
||||
if(!this.GetAvailableAuthMethods().Contains(authMethod))
|
||||
return "Please select one valid authentication method.";
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user