From 37fdc8c90815f23ec2067b0759509dea57c5695e Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Tue, 28 Apr 2026 18:57:47 +0300
Subject: [PATCH 001/135] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20?=
=?UTF-8?q?=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B1=D0=B0?=
=?UTF-8?q?=D0=B7=D1=8B=20=D0=B8=D0=B7=20=D0=BB=D0=B0=D1=83=D0=BD=D1=87?=
=?UTF-8?q?=D0=B5=D1=80=D0=B0=20=D0=B2=D1=85=D0=BE=D0=B4=D0=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
QS.Cloud.Client/QSCloudProvider.cs | 4 +-
QS.DbManagement/IDbProvider.cs | 3 +-
QS.DbManagement/MariaDb/MariaDBProvider.cs | 53 ++++++++++++-------
QS.DbManagement/ProviderResponces.cs | 1 +
.../QS.Launcher.Avalonia.csproj | 2 +-
.../Pages/DataBase/CreateDataBaseWindow.axaml | 27 ++++++++++
.../DataBase/CreateDataBaseWindow.axaml.cs | 32 +++++++++++
.../Pages/{ => DataBase}/DataBasesView.axaml | 17 +++---
.../{ => DataBase}/DataBasesView.axaml.cs | 19 +++++--
QS.Launcher/DependencyInjection.cs | 1 +
QS.Launcher/QS.Launcher.csproj | 1 +
QS.Launcher/ViewModels/MainWindowVM.cs | 1 +
.../DataBase/CreateDataBaseVM.cs | 42 +++++++++++++++
.../{ => DataBase}/DataBasesVM.cs | 51 ++++++++++++------
.../ViewModels/PageViewModels/LoginVM.cs | 1 +
15 files changed, 207 insertions(+), 48 deletions(-)
create mode 100644 QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseWindow.axaml
create mode 100644 QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseWindow.axaml.cs
rename QS.Launcher.Avalonia/Views/Pages/{ => DataBase}/DataBasesView.axaml (83%)
rename QS.Launcher.Avalonia/Views/Pages/{ => DataBase}/DataBasesView.axaml.cs (77%)
create mode 100644 QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseVM.cs
rename QS.Launcher/ViewModels/PageViewModels/{ => DataBase}/DataBasesVM.cs (80%)
diff --git a/QS.Cloud.Client/QSCloudProvider.cs b/QS.Cloud.Client/QSCloudProvider.cs
index c6e33dd8e..6ec7e78c5 100644
--- a/QS.Cloud.Client/QSCloudProvider.cs
+++ b/QS.Cloud.Client/QSCloudProvider.cs
@@ -25,6 +25,8 @@ public class QSCloudProvider : IDbProvider {
#endregion
public string UserName { get; private set; }
+ public bool CanCreateDatabase => throw new NotImplementedException();
+
private CloudFeaturesClient featuresClient;
private LoginManagementCloudClient loginClient;
private SessionManagementCloudClient sessionClient;
@@ -49,7 +51,7 @@ public bool ChangePassword(string username, string oldPassword, string newPasswo
throw new NotImplementedException();
}
- public bool CreateDatabase(string databaseName)
+ public bool CreateDatabase(string databaseName, string title)
{
throw new NotImplementedException();
}
diff --git a/QS.DbManagement/IDbProvider.cs b/QS.DbManagement/IDbProvider.cs
index da6ea6964..d0c6fd3e6 100644
--- a/QS.DbManagement/IDbProvider.cs
+++ b/QS.DbManagement/IDbProvider.cs
@@ -11,7 +11,7 @@ public interface IDbProvider : IDisposable
bool ChangePassword(string username, string oldPassword, string newPassword);
- bool CreateDatabase(string databaseName);
+ bool CreateDatabase(string databaseName, string title);
bool DropDatabase(string databaseName);
@@ -26,5 +26,6 @@ public interface IDbProvider : IDisposable
bool IsConnected { get; }
bool IsAdmin { get; }
+ bool CanCreateDatabase { get; }
}
}
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index 1e011a88b..090b993cc 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -16,7 +16,7 @@ public class MariaDBProvider : IDbProvider {
private static readonly string[] SystemDatabases = { "information_schema", "mysql", "performance_schema", "sys" };
readonly MySqlConnection connection;
- readonly MySqlConnectionStringBuilder connectionStringBuilder;
+ readonly MySqlConnectionStringBuilder ConnectionStringBuilder;
public bool IsConnected => connection.State == ConnectionState.Open;
@@ -28,10 +28,15 @@ public class MariaDBProvider : IDbProvider {
///
public bool CanCreateDatabase { get; private set; }
+ ///
+ /// Переданный в тайтл созданой базы,
+ /// нужен потом при применения скрипта с наполнением базы
+ ///
+ public string CreatedTitle { get; private set; }
+
#region Параметры подключения
public string Server { get; }
public string UserName { get; }
- public string ProductName { get; }
private readonly string password;
#endregion
@@ -39,19 +44,29 @@ public MariaDBProvider(IList parameters, string passwo
if(parameters == null)
throw new ArgumentNullException(nameof(parameters));
- Server = parameters.First(p => p.Name == "Server").Value;
+ string serverValue = parameters.First(p => p.Name == "Server").Value;
UserName = parameters.First(p => p.Name == "Login").Value;
this.password = password;
- var builder = new MySqlConnectionStringBuilder {
- Server = Server,
+ string host = serverValue;
+ uint? port = null;
+ if(serverValue.Contains(":")) {
+ var parts = serverValue.Split(':');
+ host = parts[0];
+ if(uint.TryParse(parts[1], out var parsedPort))
+ port = parsedPort;
+ }
+ Server = serverValue;
+
+ ConnectionStringBuilder = new MySqlConnectionStringBuilder {
+ Server = host,
UserID = UserName,
Password = password,
- AllowUserVariables = true,
- ConvertZeroDateTime = true
+ AllowUserVariables = true
};
- connectionStringBuilder = builder;
- connection = new MySqlConnection(builder.ConnectionString);
+ if(port != null)
+ ConnectionStringBuilder.Port = port.Value;
+ connection = new MySqlConnection(ConnectionStringBuilder.ConnectionString);
}
#region IDbProvider
@@ -70,13 +85,13 @@ public LoginToServerResponse LoginToServer() {
CanCreateDatabase = IsAdmin || grants.Any(g =>
g.IndexOf("ALL PRIVILEGES", StringComparison.OrdinalIgnoreCase) >= 0
- || g.IndexOf("CREATE", StringComparison.OrdinalIgnoreCase) >= 0);
- //todo : разбить на токены и одним циклом по массиву прав проставить булы
+ || g.IndexOf("CREATE", StringComparison.OrdinalIgnoreCase) >= 0);
return new LoginToServerResponse {
Success = true,
IsAdmin = IsAdmin,
- NeedToUpdateLauncher = false
+ NeedToUpdateLauncher = false,
+ CanCreateDatabase = CanCreateDatabase
};
}
catch(MySqlException ex) {
@@ -125,8 +140,7 @@ public List GetUserDatabases(IApplicationInfo applicationInfo) {
result.Add(new DbInfo {
BaseName = dbName,
Title = title ?? dbName,
- Version = version,
- CanCreateDatabase = CanCreateDatabase
+ Version = version
});
}
@@ -135,15 +149,14 @@ public List GetUserDatabases(IApplicationInfo applicationInfo) {
public LoginToDatabaseResponse LoginToDatabase(DbInfo dbInfo) {
try {
- connectionStringBuilder.Database = dbInfo.BaseName;
+ ConnectionStringBuilder.Database = dbInfo.BaseName;
return new LoginToDatabaseResponse {
Success = true,
- ConnectionString = connectionStringBuilder.ConnectionString,
+ ConnectionString = ConnectionStringBuilder.ConnectionString,
Login = UserName,
Parameters = new Dictionary {
- { "BaseTitle", dbInfo.Title },
- { "SessionId", string.Empty }
+ { "BaseTitle", dbInfo.Title }
}
};
}
@@ -165,7 +178,8 @@ public bool ChangePassword(string username, string oldPassword, string newPasswo
return connection.Execute(sql) != 0;
}
- public bool CreateDatabase(string databaseName) {
+ public bool CreateDatabase(string databaseName, string title) {
+ CreatedTitle = title;
string sql = $"CREATE DATABASE IF NOT EXISTS `{databaseName}`";
return connection.Execute(sql) != 0;
}
@@ -178,7 +192,6 @@ public bool DropDatabase(string databaseName) {
public void Dispose() {
connection?.Dispose();
}
-
#endregion
}
}
diff --git a/QS.DbManagement/ProviderResponces.cs b/QS.DbManagement/ProviderResponces.cs
index b08cf0a57..b9e6dfa62 100644
--- a/QS.DbManagement/ProviderResponces.cs
+++ b/QS.DbManagement/ProviderResponces.cs
@@ -9,6 +9,7 @@ public class Response {
public class LoginToServerResponse : Response
{
+ public bool CanCreateDatabase { get; set; }
public bool IsAdmin { get; set; }
public bool NeedToUpdateLauncher { get; set; }
}
diff --git a/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj b/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj
index 0573db99d..553012330 100644
--- a/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj
+++ b/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj
@@ -53,7 +53,7 @@
LoginView.axaml
-
+
DataBasesView.axaml
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseWindow.axaml b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseWindow.axaml
new file mode 100644
index 000000000..73c0a0e0c
--- /dev/null
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseWindow.axaml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseWindow.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseWindow.axaml.cs
new file mode 100644
index 000000000..5835534e6
--- /dev/null
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseWindow.axaml.cs
@@ -0,0 +1,32 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using QS.Launcher.ViewModels.PageViewModels.DataBase;
+using System;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+
+namespace QS.Launcher.Views.Pages.DataBase;
+
+public partial class CreateDataBaseWindow : Window
+{
+ private CreateDataBaseVM ViewModel;
+ private TaskCompletionSource<(string? dbTitle, string? dbName)> Result;
+ public CreateDataBaseWindow(CreateDataBaseVM viewModel)
+ {
+ DataContext = ViewModel = viewModel;
+ InitializeComponent();
+
+ Result = new TaskCompletionSource<(string? dbTitle, string? dbName)>();
+ ViewModel.DatabaseCreated += () => {
+ Result.TrySetResult((ViewModel.DbTitle, ViewModel.DbName));
+ Close();
+ };
+
+ this.Closed += (s, e) =>
+ Result.TrySetResult((null, null));
+ }
+ public (string? dbTitle, string? dbName) GetResult(){
+ return Result.Task.Result;
+ }
+}
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBasesView.axaml b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml
similarity index 83%
rename from QS.Launcher.Avalonia/Views/Pages/DataBasesView.axaml
rename to QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml
index 436cfe89c..bb771c5f6 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBasesView.axaml
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml
@@ -6,21 +6,26 @@
xmlns:vm="clr-namespace:QS.Launcher.ViewModels.PageViewModels;assembly=QS.Launcher"
d:DesignHeight="650"
d:DesignWidth="450"
- x:DataType="vm:DataBasesVM"
+ x:DataType="vm:DataBase.DataBasesVM"
mc:Ignorable="d">
+
+
+
+
-
-
+
+
-
-
+
+
@@ -29,7 +34,7 @@
-
+
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBasesView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml.cs
similarity index 77%
rename from QS.Launcher.Avalonia/Views/Pages/DataBasesView.axaml.cs
rename to QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml.cs
index 08ca5d336..15621efb1 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBasesView.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml.cs
@@ -2,7 +2,8 @@
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input;
-using QS.Launcher.ViewModels.PageViewModels;
+using QS.Launcher.ViewModels.PageViewModels.DataBase;
+using QS.Launcher.Views.Pages.DataBase;
using System.Linq;
using System.Threading.Tasks;
@@ -17,6 +18,7 @@ public DataBasesView(DataBasesVM viewModel) {
DataContext = ViewModel = viewModel;
viewModel.StartLaunchProgram += HandleStartMainProgram;
+ viewModel.RequestShowCreateDbWindow += ShowCreateDatabaseWindowAsync;
KeyDown += (s, e) => {
if(e.Key == Key.Enter) {
@@ -26,16 +28,27 @@ public DataBasesView(DataBasesVM viewModel) {
};
}
+ private async Task<(string? dbTitle, string? dbName)> ShowCreateDatabaseWindowAsync(DataBasesVM dataBases) {
+ CreateDataBaseVM viewModel = new CreateDataBaseVM(ViewModel.Provider);
+ var window = new CreateDataBaseWindow(viewModel);
+
+ var parentWindow = TopLevel.GetTopLevel(this) as Window;
+ if(parentWindow != null) {
+ await window.ShowDialog(parentWindow);
+ }
+ return window.GetResult();
+ }
+
public async void HandleStartMainProgram(bool shouldCloseLauncher) {
logger.Info($">>> HandleStartMainProgram: shouldCloseLauncher={shouldCloseLauncher}");
-
+
loadingPanel.IsVisible = true;
cogwheel.Classes.Add("rolled");
var transition = cogwheel.Transitions.OfType().FirstOrDefault();
await Task.Delay(transition.Duration);
loadingPanel.IsVisible = false;
-
+
if(shouldCloseLauncher) {
logger.Info($">>> HandleStartMainProgram: Вызываем Shutdown!");
// NewProcessRunner: закрываем всё приложение лаунчера (Shutdown)
diff --git a/QS.Launcher/DependencyInjection.cs b/QS.Launcher/DependencyInjection.cs
index e0897f04f..2e4bc8a1a 100644
--- a/QS.Launcher/DependencyInjection.cs
+++ b/QS.Launcher/DependencyInjection.cs
@@ -4,6 +4,7 @@
using QS.Launcher.AppRunner;
using QS.Launcher.ViewModels;
using QS.Launcher.ViewModels.PageViewModels;
+using QS.Launcher.ViewModels.PageViewModels.DataBase;
namespace QS.Launcher {
public static partial class DependencyInjection {
diff --git a/QS.Launcher/QS.Launcher.csproj b/QS.Launcher/QS.Launcher.csproj
index 271188d35..3481e8522 100644
--- a/QS.Launcher/QS.Launcher.csproj
+++ b/QS.Launcher/QS.Launcher.csproj
@@ -20,6 +20,7 @@
+
diff --git a/QS.Launcher/ViewModels/MainWindowVM.cs b/QS.Launcher/ViewModels/MainWindowVM.cs
index 8c04e209f..ef09b2b60 100644
--- a/QS.Launcher/ViewModels/MainWindowVM.cs
+++ b/QS.Launcher/ViewModels/MainWindowVM.cs
@@ -2,6 +2,7 @@
using ReactiveUI;
using System;
using QS.ViewModels;
+using QS.Launcher.ViewModels.PageViewModels.DataBase;
namespace QS.Launcher.ViewModels {
public class MainWindowVM : ViewModelBase {
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseVM.cs
new file mode 100644
index 000000000..936f4c80d
--- /dev/null
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseVM.cs
@@ -0,0 +1,42 @@
+using Dapper;
+using QS.DbManagement;
+using ReactiveUI;
+using QS.Launcher.AppRunner;
+using QS.Project.Versioning;
+using System;
+using System.Windows.Input;
+using System.Reactive;
+
+namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
+ public class CreateDataBaseVM : ReactiveObject {
+ private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
+
+ public IDbProvider Provider { get; private set; }
+
+ private string dbTitle;
+ public string DbTitle {
+ get => dbTitle;
+ set => this.RaiseAndSetIfChanged(ref dbTitle, value);
+ }
+ private string dbName;
+ public string DbName {
+ get => dbName;
+ set => this.RaiseAndSetIfChanged(ref dbName, value);
+ }
+ public ReactiveCommand CreateDataBaseCommand { get; }
+ public event Action DatabaseCreated;
+
+ public CreateDataBaseVM(IDbProvider dbProvider) {
+ Provider = dbProvider;
+
+ CreateDataBaseCommand = ReactiveCommand.Create(() => {
+ if(Provider.CanCreateDatabase)
+ Provider.CreateDatabase(DbName, DbTitle);
+ else
+ throw new InvalidOperationException("пользователь не должен получать доступ к созданию базы");
+
+ DatabaseCreated?.Invoke();
+ });
+ }
+ }
+}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBasesVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
similarity index 80%
rename from QS.Launcher/ViewModels/PageViewModels/DataBasesVM.cs
rename to QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
index 29acce4bb..c9606765c 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBasesVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
@@ -1,16 +1,19 @@
using DynamicData.Kernel;
using QS.DbManagement;
using QS.Dialog;
+using QS.Launcher.AppRunner;
+using QS.Project.Versioning;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Reactive;
using System.Reactive.Linq;
+using System.Threading.Tasks;
using System.Windows.Input;
-using QS.Launcher.AppRunner;
-using QS.Project.Versioning;
+using System.Xml.Linq;
-namespace QS.Launcher.ViewModels.PageViewModels {
+namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
public class DataBasesVM : CarouselPageVM {
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
@@ -23,12 +26,12 @@ public IDbProvider Provider {
this.RaiseAndSetIfChanged(ref provider, value);
Databases = provider.GetUserDatabases(applicationInfo).AsList();
this.RaisePropertyChanged(nameof(Databases));
-
+
// Загружаем и устанавливаем последнюю выбранную базу
LoadLastSelectedDatabase();
}
}
-
+
public void SetProvider(IDbProvider dbProvider, Connection connection, Action saveConnections) {
currentConnection = connection;
saveConnectionsAction = saveConnections;
@@ -56,8 +59,10 @@ public DbInfo SelectedDatabase {
public bool VisibleShouldCloseLauncherCheckBox => launcherOptions?.IsStandalone ?? false;
public ICommand ConnectCommand { get; }
+ public ReactiveCommand OpenCreateDatabaseCommand { get; }
public event Action StartLaunchProgram;
+ public event Func> RequestShowCreateDbWindow;
IInteractiveMessage interactiveMessage;
@@ -69,19 +74,34 @@ public DataBasesVM(IAppRunner appRunner, IApplicationInfo applicationInfo, IInte
this.applicationInfo = applicationInfo ?? throw new ArgumentNullException(nameof(applicationInfo));
this.interactiveMessage = interactiveMessage ?? throw new ArgumentNullException(nameof(interactiveMessage));
this.launcherOptions = launcherOptions;
-
+
logger.Info($">>> DataBasesVM constructor: launcherOptions={launcherOptions}, IsStandalone={launcherOptions?.IsStandalone}");
- IObservable canExecute = this
+ IObservable canExecuteConnection = this
+ .WhenAnyValue(x => x.SelectedDatabase)
+ .Select(x => x != null);
+
+ IObservable canExecuteCreation = this
.WhenAnyValue(x => x.SelectedDatabase)
.Select(x => x != null);
- ConnectCommand = ReactiveCommand.Create(Connect, canExecute);
+ ConnectCommand = ReactiveCommand.Create(Connect, canExecuteConnection);
+ OpenCreateDatabaseCommand = ReactiveCommand.CreateFromTask(OpenCreateDatabaseAsync);
}
-
+ private async Task OpenCreateDatabaseAsync() {
+ if(OpenCreateDatabaseCommand != null) {
+ (string title, string name) = await RequestShowCreateDbWindow.Invoke(this);
+
+ Databases = provider.GetUserDatabases(applicationInfo).AsList();
+ this.RaisePropertyChanged(nameof(Databases));
+ SelectedDatabase = Databases.FirstOrDefault(db => db.Title == title && db.BaseName == name);
+ this.RaisePropertyChanged(nameof(SelectedDatabase));
+ }
+ }
+
private void LoadLastSelectedDatabase() {
if(Databases == null || Databases.Count == 0)
return;
-
+
// Используем LastBaseId из текущего подключения
if(currentConnection?.LastBaseId != null)
SelectedDatabase = Databases.FirstOrDefault(db => db.BaseId == currentConnection.LastBaseId.Value);
@@ -107,22 +127,21 @@ public void Connect() {
// В in-process режиме НЕ делаем shutdown (возвращаем false)
var isStandalone = launcherOptions?.IsStandalone ?? false;
logger.Info($">>> Connect: IsStandalone={isStandalone}, ShouldCloseLauncherAfterStart={ShouldCloseLauncherAfterStart}");
-
+
bool shouldCloseLauncher = isStandalone && ShouldCloseLauncherAfterStart;
-
+
logger.Info($">>> Connect: shouldCloseLauncher={shouldCloseLauncher}");
-
+
StartLaunchProgram?.Invoke(shouldCloseLauncher);
appRunner.Run(resp);
}
-
+
private void SaveLastSelectedDatabase() {
if(SelectedDatabase == null || currentConnection == null)
return;
-
// Сохраняем BaseId в текущее подключение
currentConnection.LastBaseId = SelectedDatabase.BaseId;
-
+
// Вызываем сохранение подключений
saveConnectionsAction?.Invoke();
}
diff --git a/QS.Launcher/ViewModels/PageViewModels/LoginVM.cs b/QS.Launcher/ViewModels/PageViewModels/LoginVM.cs
index 3dc7b311e..8e0c6c73f 100644
--- a/QS.Launcher/ViewModels/PageViewModels/LoginVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/LoginVM.cs
@@ -7,6 +7,7 @@
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
+using QS.Launcher.ViewModels.PageViewModels.DataBase;
namespace QS.Launcher.ViewModels.PageViewModels {
public class LoginVM : CarouselPageVM {
From c5593f6f5cda345b84991b32dba9d6ed98675ef4 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Tue, 28 Apr 2026 22:33:57 +0300
Subject: [PATCH 002/135] =?UTF-8?q?=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20?=
=?UTF-8?q?=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=20=D1=81=D0=BE=D0=B7=D0=B4?=
=?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B1=D0=B0=D0=B7=D1=8B=20=D0=B4?=
=?UTF-8?q?=D0=BB=D1=8F=20=D0=BE=D0=B1=D0=BB=D0=B0=D0=BA=D0=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Clients/DataBaseManagementCloudClient.cs | 13 +++++++++++++
QS.Cloud.Client/Protos/DataBaseManagement.proto | 16 ++++++++++++++++
QS.Cloud.Client/QS.Cloud.Client.csproj | 2 ++
QS.Cloud.Client/QSCloudProvider.cs | 5 ++++-
4 files changed, 35 insertions(+), 1 deletion(-)
create mode 100644 QS.Cloud.Client/Clients/DataBaseManagementCloudClient.cs
create mode 100644 QS.Cloud.Client/Protos/DataBaseManagement.proto
diff --git a/QS.Cloud.Client/Clients/DataBaseManagementCloudClient.cs b/QS.Cloud.Client/Clients/DataBaseManagementCloudClient.cs
new file mode 100644
index 000000000..27e07a9d9
--- /dev/null
+++ b/QS.Cloud.Client/Clients/DataBaseManagementCloudClient.cs
@@ -0,0 +1,13 @@
+using QS.Cloud.Core;
+namespace QS.Cloud.Client.Clients {
+ public class DataBaseManagementCloudClient : CloudClientByBasicAuth {
+ public DataBaseManagementCloudClient(IBasicAuthInfoProvider basicAuthInfoProvider)
+ : base(basicAuthInfoProvider, "core.cloud.qsolution.ru", 443) { }
+
+ public CreateDataBaseResponse CreateDataBase(string dbName, string dbTitle) {
+ var client = new DataBaseManagement.DataBaseManagementClient(Channel);
+ var request = new CreateDataBaseRequest { Name = dbName, Title = dbTitle };
+ return client.CreateDataBase(request, headers); ;
+ }
+ }
+}
diff --git a/QS.Cloud.Client/Protos/DataBaseManagement.proto b/QS.Cloud.Client/Protos/DataBaseManagement.proto
new file mode 100644
index 000000000..6bd23648d
--- /dev/null
+++ b/QS.Cloud.Client/Protos/DataBaseManagement.proto
@@ -0,0 +1,16 @@
+syntax = "proto3";
+
+package QS.Cloud.Core;
+
+service DataBaseManagement{
+ rpc CreateDataBase (CreateDataBaseRequest) returns (CreateDataBaseResponse);
+}
+
+message CreateDataBaseRequest{
+ string name = 1;
+ string title = 2;
+}
+
+message CreateDataBaseResponse{
+ bool succsess = 1;
+}
diff --git a/QS.Cloud.Client/QS.Cloud.Client.csproj b/QS.Cloud.Client/QS.Cloud.Client.csproj
index ec2e96649..d2eee9082 100644
--- a/QS.Cloud.Client/QS.Cloud.Client.csproj
+++ b/QS.Cloud.Client/QS.Cloud.Client.csproj
@@ -22,11 +22,13 @@
+
+
diff --git a/QS.Cloud.Client/QSCloudProvider.cs b/QS.Cloud.Client/QSCloudProvider.cs
index 6ec7e78c5..3faae9181 100644
--- a/QS.Cloud.Client/QSCloudProvider.cs
+++ b/QS.Cloud.Client/QSCloudProvider.cs
@@ -8,6 +8,7 @@
using System.Linq;
using System.Reflection;
using System;
+using QS.Cloud.Client.Clients;
namespace QS.Cloud.Client
{
@@ -30,6 +31,7 @@ public class QSCloudProvider : IDbProvider {
private CloudFeaturesClient featuresClient;
private LoginManagementCloudClient loginClient;
private SessionManagementCloudClient sessionClient;
+ private DataBaseManagementCloudClient dbClient;
private UserManagementCloudClient userClient;
@@ -39,6 +41,7 @@ public QSCloudProvider(IList parameters, string passwo
BasicAuthInfoProvider authInfo = new BasicAuthInfoProvider($@"{Account}\{UserName}", password);
loginClient = new LoginManagementCloudClient(authInfo);
+ dbClient = new DataBaseManagementCloudClient(authInfo);
}
public bool AddUser(string username, string password)
@@ -53,7 +56,7 @@ public bool ChangePassword(string username, string oldPassword, string newPasswo
public bool CreateDatabase(string databaseName, string title)
{
- throw new NotImplementedException();
+ return dbClient.CreateDataBase(databaseName, title).Succsess;
}
public void Dispose()
From 2266c4b293281064c454a3e723345c5dc138d5dc Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Thu, 30 Apr 2026 20:56:29 +0300
Subject: [PATCH 003/135] =?UTF-8?q?=D1=83=D0=BD=D0=B8=D0=B2=D0=B5=D1=80?=
=?UTF-8?q?=D1=81=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B5=20=D0=BD=D0=B0=D0=BF?=
=?UTF-8?q?=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B1=D0=B0=D0=B7?=
=?UTF-8?q?=D1=8B=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B8=D0=B7=20?=
=?UTF-8?q?=D1=81=D0=BA=D1=80=D0=B8=D0=BF=D1=82=D0=BE=D0=B2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Views больше не регистрируются, они разворачиваются через DataTemplates в LauncherApp.axaml по типу VM
Корневые страницы (Login, DataBases, BaseManagement, UserManagement) добавляются при создании и не удаляются. Дополнительные страницы добавляются в конец через PushPage и удаляются PopPage/PopToRoot
и прогресс создания базы
---
QS.Cloud.Client/QS.Cloud.Client.csproj | 1 +
QS.DbManagement/ConnectionTypeBase.cs | 29 ++-
QS.DbManagement/MariaDb/MariaDBProvider.cs | 6 +-
QS.DbManagement/QS.DbManagement.csproj | 1 +
QS.Launcher.Avalonia/DependencyInjection.cs | 10 +-
QS.Launcher.Avalonia/LauncherApp.axaml | 25 +++
.../QS.Launcher.Avalonia.csproj | 6 +
.../Services/AvaloniaUiThreadInvoker.cs | 12 ++
QS.Launcher.Avalonia/Views/MainWindow.axaml | 5 +-
.../Views/MainWindow.axaml.cs | 8 +-
.../Views/Pages/BaseManagementView.axaml.cs | 4 +-
.../DataBase/CreateDataBaseProgressView.axaml | 45 +++++
.../CreateDataBaseProgressView.axaml.cs | 19 ++
.../DataBase/CreateDataBaseSettingsView.axaml | 25 +++
.../CreateDataBaseSettingsView.axaml.cs | 9 +
.../Pages/DataBase/CreateDataBaseWindow.axaml | 27 ---
.../DataBase/CreateDataBaseWindow.axaml.cs | 32 ----
.../Pages/DataBase/DataBasesView.axaml.cs | 37 ++--
.../Views/Pages/LoginView.axaml.cs | 19 +-
.../Views/Pages/UserManagementView.axaml.cs | 8 +-
QS.Launcher/DependencyInjection.cs | 9 +-
QS.Launcher/Services/IUiThreadInvoker.cs | 11 ++
.../Services/LauncherDbCreatorInteraction.cs | 53 ++++++
QS.Launcher/ViewModels/MainWindowVM.cs | 92 ++++++++--
.../PageViewModels/CarouselPageVM.cs | 18 ++
.../DataBase/CreateDataBaseProgressVM.cs | 148 +++++++++++++++
.../DataBase/CreateDataBaseSettingsVM.cs | 59 ++++++
.../DataBase/CreateDataBaseVM.cs | 42 -----
.../PageViewModels/DataBase/DataBasesVM.cs | 88 +++++----
.../DBScripts/Controllers/IDBCreator.cs | 9 +-
.../Controllers/IDbCreatorInteraction.cs | 14 ++
.../Controllers/IDbScriptsConfiguration.cs | 14 ++
.../Controllers/IDbCreateController.cs | 2 +-
.../DBScripts/Models/MySqlDbCreateModel.cs | 168 ++++++++++--------
34 files changed, 763 insertions(+), 292 deletions(-)
create mode 100644 QS.Launcher.Avalonia/Services/AvaloniaUiThreadInvoker.cs
create mode 100644 QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml
create mode 100644 QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml.cs
create mode 100644 QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml
create mode 100644 QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml.cs
delete mode 100644 QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseWindow.axaml
delete mode 100644 QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseWindow.axaml.cs
create mode 100644 QS.Launcher/Services/IUiThreadInvoker.cs
create mode 100644 QS.Launcher/Services/LauncherDbCreatorInteraction.cs
create mode 100644 QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
create mode 100644 QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
delete mode 100644 QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseVM.cs
create mode 100644 QS.Project.Core/DBScripts/Controllers/IDbCreatorInteraction.cs
create mode 100644 QS.Project.Core/DBScripts/Controllers/IDbScriptsConfiguration.cs
diff --git a/QS.Cloud.Client/QS.Cloud.Client.csproj b/QS.Cloud.Client/QS.Cloud.Client.csproj
index d2eee9082..086526cf2 100644
--- a/QS.Cloud.Client/QS.Cloud.Client.csproj
+++ b/QS.Cloud.Client/QS.Cloud.Client.csproj
@@ -25,6 +25,7 @@
+
diff --git a/QS.DbManagement/ConnectionTypeBase.cs b/QS.DbManagement/ConnectionTypeBase.cs
index 377ecad56..e8b3f5905 100644
--- a/QS.DbManagement/ConnectionTypeBase.cs
+++ b/QS.DbManagement/ConnectionTypeBase.cs
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
+using QS.DBScripts.Controllers;
+using QS.Dialog;
namespace QS.DbManagement {
@@ -10,9 +12,34 @@ public abstract class ConnectionTypeBase {
public List Parameters { get; } = new List();
public byte[] IconBytes { get; protected set; }
-
+
public abstract bool CanConnect(IEnumerable parameters);
public abstract IDbProvider CreateProvider(IList parameters, string password = null);
+
+ ///
+ /// Заполняется композиционным корнем приложения
+ /// который один знает обо всех конкретных реализациях creator-ов и
+ /// о том, как из IDbProvider достать строку подключения
+ ///
+ /// interaction — канал диалогов с пользователем
+ /// serviceProvider — для резолва дополнительных зависимостей
+ ///
+ public Func CreatorFactory { get; set; }
+
+ public IDBCreator CreateCreator(CreatorFactoryArgs args) {
+ if(CreatorFactory == null)
+ throw new InvalidOperationException(
+ $"Для типа подключения '{ConnectionTypeName}' не задана CreatorFactory. "
+ + "Зарегистрируйте её в композиционном корне приложения.");
+ return CreatorFactory(args);
+ }
+ }
+ public class CreatorFactoryArgs {
+ public IDbProvider Provider { get; set; }
+ public IProgressBarDisplayable Progress { get; set; }
+ public IDbCreatorInteraction Interaction { get; set; }
+ public System.Threading.CancellationToken CancellationToken { get; set; }
+ public IServiceProvider ServiceProvider { get; set; }
}
}
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index 491fd201b..7a031b382 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -15,7 +15,11 @@ public class MariaDBProvider : IDbProvider {
private static readonly string[] SystemDatabases = { "information_schema", "mysql", "performance_schema", "sys" };
readonly MySqlConnection connection;
- readonly MySqlConnectionStringBuilder ConnectionStringBuilder;
+ ///
+ /// Публичный, чтобы внешние компоненты могли получить подключение
+ /// без повторного разбора параметров.
+ ///
+ public MySqlConnectionStringBuilder ConnectionStringBuilder { get; }
public bool IsConnected => connection.State == ConnectionState.Open;
diff --git a/QS.DbManagement/QS.DbManagement.csproj b/QS.DbManagement/QS.DbManagement.csproj
index 60196acee..178ef368e 100644
--- a/QS.DbManagement/QS.DbManagement.csproj
+++ b/QS.DbManagement/QS.DbManagement.csproj
@@ -26,6 +26,7 @@
+
diff --git a/QS.Launcher.Avalonia/DependencyInjection.cs b/QS.Launcher.Avalonia/DependencyInjection.cs
index 6af20d57e..652f84797 100644
--- a/QS.Launcher.Avalonia/DependencyInjection.cs
+++ b/QS.Launcher.Avalonia/DependencyInjection.cs
@@ -1,18 +1,12 @@
-using Avalonia.Controls;
using Microsoft.Extensions.DependencyInjection;
+using QS.Launcher.Services;
using QS.Launcher.Views;
-using QS.Launcher.Views.Pages;
namespace QS.Launcher;
public static partial class DependencyInjection {
public static IServiceCollection AddPages(this IServiceCollection services) {
return services
.AddSingleton()
- .AddSingleton()
- .AddSingleton()
- .AddSingleton()
- .AddSingleton();
+ .AddSingleton();
}
-
-
}
diff --git a/QS.Launcher.Avalonia/LauncherApp.axaml b/QS.Launcher.Avalonia/LauncherApp.axaml
index e0f3cd31e..c02018fd8 100644
--- a/QS.Launcher.Avalonia/LauncherApp.axaml
+++ b/QS.Launcher.Avalonia/LauncherApp.axaml
@@ -2,6 +2,10 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters1="clr-namespace:QS.Project.Converters;assembly=QS.Project.Avalonia"
+ xmlns:vm="clr-namespace:QS.Launcher.ViewModels.PageViewModels;assembly=QS.Launcher"
+ xmlns:vmDb="clr-namespace:QS.Launcher.ViewModels.PageViewModels.DataBase;assembly=QS.Launcher"
+ xmlns:views="using:QS.Launcher.Views.Pages"
+ xmlns:viewsDb="using:QS.Launcher.Views.Pages.DataBase"
x:CompileBindings="False"
RequestedThemeVariant="Light">
@@ -11,6 +15,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj b/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj
index 553012330..e8a347a5a 100644
--- a/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj
+++ b/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj
@@ -56,6 +56,12 @@
DataBasesView.axaml
+
+ CreateDataBaseSettingsView.axaml
+
+
+ CreateDataBaseProgressView.axaml
+
UserManagementView.axaml
diff --git a/QS.Launcher.Avalonia/Services/AvaloniaUiThreadInvoker.cs b/QS.Launcher.Avalonia/Services/AvaloniaUiThreadInvoker.cs
new file mode 100644
index 000000000..6ac1bc79e
--- /dev/null
+++ b/QS.Launcher.Avalonia/Services/AvaloniaUiThreadInvoker.cs
@@ -0,0 +1,12 @@
+using System;
+using Avalonia.Threading;
+using QS.Launcher.Services;
+
+namespace QS.Launcher.Services {
+ public class AvaloniaUiThreadInvoker : IUiThreadInvoker {
+ public void Post(Action action) {
+ if(action == null) return;
+ Dispatcher.UIThread.Post(action);
+ }
+ }
+}
diff --git a/QS.Launcher.Avalonia/Views/MainWindow.axaml b/QS.Launcher.Avalonia/Views/MainWindow.axaml
index b678546ae..761d4a59b 100644
--- a/QS.Launcher.Avalonia/Views/MainWindow.axaml
+++ b/QS.Launcher.Avalonia/Views/MainWindow.axaml
@@ -3,7 +3,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
- xmlns:views="clr-namespace:QS.Launcher.Views"
xmlns:vm="clr-namespace:QS.Launcher.ViewModels;assembly=QS.Launcher"
Title="QS.Лаунчер"
Width="450"
@@ -17,7 +16,9 @@
mc:Ignorable="d">
-
+
diff --git a/QS.Launcher.Avalonia/Views/MainWindow.axaml.cs b/QS.Launcher.Avalonia/Views/MainWindow.axaml.cs
index d17804705..bb6747c0d 100644
--- a/QS.Launcher.Avalonia/Views/MainWindow.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/MainWindow.axaml.cs
@@ -1,11 +1,11 @@
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using QS.Launcher.ViewModels;
-using System.Collections.Generic;
+
namespace QS.Launcher.Views;
public partial class MainWindow : Window {
- public MainWindow(MainWindowVM vm, IEnumerable pages, LauncherOptions options) {
+ public MainWindow(MainWindowVM vm, LauncherOptions options) {
InitializeComponent();
Icon = new WindowIcon(new Bitmap(new System.IO.MemoryStream(options.LogoIcon)));
@@ -13,10 +13,6 @@ public MainWindow(MainWindowVM vm, IEnumerable pages, LauncherOptio
Closing += (_, _) => vm.SaveConnections();
- foreach(var page in pages)
- carousel.Items.Add(page);
- vm.PagesCount = carousel.ItemCount;
-
DataContext = vm;
}
}
diff --git a/QS.Launcher.Avalonia/Views/Pages/BaseManagementView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/BaseManagementView.axaml.cs
index e6332d279..d96f22ca3 100644
--- a/QS.Launcher.Avalonia/Views/Pages/BaseManagementView.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/Pages/BaseManagementView.axaml.cs
@@ -1,11 +1,9 @@
using Avalonia.Controls;
-using QS.Launcher.ViewModels.PageViewModels;
namespace QS.Launcher.Views.Pages;
public partial class BaseManagementView : UserControl {
- public BaseManagementView(BaseManagementVM viewModel) {
+ public BaseManagementView() {
InitializeComponent();
- DataContext = viewModel;
}
}
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml
new file mode 100644
index 000000000..9a468a3f1
--- /dev/null
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml.cs
new file mode 100644
index 000000000..ec4b43f39
--- /dev/null
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml.cs
@@ -0,0 +1,19 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using QS.Launcher.ViewModels.PageViewModels.DataBase;
+
+namespace QS.Launcher.Views.Pages.DataBase;
+
+public partial class CreateDataBaseProgressView : UserControl {
+ public CreateDataBaseProgressView() {
+ InitializeComponent();
+ }
+
+ private void OnLoaded(object? sender, RoutedEventArgs e) {
+ cogwheel.Classes.Add("rolled");
+
+ if(DataContext is CreateDataBaseProgressVM vm)
+ vm.StartCreationCommand.Execute().Subscribe();
+ }
+}
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml
new file mode 100644
index 000000000..7092888cd
--- /dev/null
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml.cs
new file mode 100644
index 000000000..35dab5b31
--- /dev/null
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml.cs
@@ -0,0 +1,9 @@
+using Avalonia.Controls;
+
+namespace QS.Launcher.Views.Pages.DataBase;
+
+public partial class CreateDataBaseSettingsView : UserControl {
+ public CreateDataBaseSettingsView() {
+ InitializeComponent();
+ }
+}
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseWindow.axaml b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseWindow.axaml
deleted file mode 100644
index 73c0a0e0c..000000000
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseWindow.axaml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseWindow.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseWindow.axaml.cs
deleted file mode 100644
index 5835534e6..000000000
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseWindow.axaml.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
-using QS.Launcher.ViewModels.PageViewModels.DataBase;
-using System;
-using System.Threading.Tasks;
-using System.Xml.Linq;
-
-namespace QS.Launcher.Views.Pages.DataBase;
-
-public partial class CreateDataBaseWindow : Window
-{
- private CreateDataBaseVM ViewModel;
- private TaskCompletionSource<(string? dbTitle, string? dbName)> Result;
- public CreateDataBaseWindow(CreateDataBaseVM viewModel)
- {
- DataContext = ViewModel = viewModel;
- InitializeComponent();
-
- Result = new TaskCompletionSource<(string? dbTitle, string? dbName)>();
- ViewModel.DatabaseCreated += () => {
- Result.TrySetResult((ViewModel.DbTitle, ViewModel.DbName));
- Close();
- };
-
- this.Closed += (s, e) =>
- Result.TrySetResult((null, null));
- }
- public (string? dbTitle, string? dbName) GetResult(){
- return Result.Task.Result;
- }
-}
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml.cs
index 15621efb1..9d35405e8 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml.cs
@@ -3,42 +3,30 @@
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input;
using QS.Launcher.ViewModels.PageViewModels.DataBase;
-using QS.Launcher.Views.Pages.DataBase;
using System.Linq;
using System.Threading.Tasks;
namespace QS.Launcher.Views.Pages;
public partial class DataBasesView : UserControl {
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
- private DataBasesVM ViewModel;
- public DataBasesView(DataBasesVM viewModel) {
+ public DataBasesView() {
InitializeComponent();
- DataContext = ViewModel = viewModel;
-
- viewModel.StartLaunchProgram += HandleStartMainProgram;
- viewModel.RequestShowCreateDbWindow += ShowCreateDatabaseWindowAsync;
+ DataContextChanged += (_, _) => {
+ if(DataContext is DataBasesVM vm)
+ vm.StartLaunchProgram += HandleStartMainProgram;
+ };
KeyDown += (s, e) => {
if(e.Key == Key.Enter) {
- TopLevel.GetTopLevel(this).FocusManager.ClearFocus();
- viewModel.ConnectCommand.Execute(null);
+ TopLevel.GetTopLevel(this)?.FocusManager?.ClearFocus();
+ if(DataContext is DataBasesVM vm)
+ vm.ConnectCommand.Execute(null);
}
};
}
- private async Task<(string? dbTitle, string? dbName)> ShowCreateDatabaseWindowAsync(DataBasesVM dataBases) {
- CreateDataBaseVM viewModel = new CreateDataBaseVM(ViewModel.Provider);
- var window = new CreateDataBaseWindow(viewModel);
-
- var parentWindow = TopLevel.GetTopLevel(this) as Window;
- if(parentWindow != null) {
- await window.ShowDialog(parentWindow);
- }
- return window.GetResult();
- }
-
public async void HandleStartMainProgram(bool shouldCloseLauncher) {
logger.Info($">>> HandleStartMainProgram: shouldCloseLauncher={shouldCloseLauncher}");
@@ -46,17 +34,16 @@ public async void HandleStartMainProgram(bool shouldCloseLauncher) {
cogwheel.Classes.Add("rolled");
var transition = cogwheel.Transitions.OfType().FirstOrDefault();
- await Task.Delay(transition.Duration);
+ if(transition != null)
+ await Task.Delay(transition.Duration);
loadingPanel.IsVisible = false;
if(shouldCloseLauncher) {
logger.Info($">>> HandleStartMainProgram: Вызываем Shutdown!");
- // NewProcessRunner: закрываем всё приложение лаунчера (Shutdown)
(LauncherApp.Current!.ApplicationLifetime as ClassicDesktopStyleApplicationLifetime)?.Shutdown();
}
else {
logger.Info($">>> HandleStartMainProgram: Закрываем только окно");
- // InProcessRunner: закрываем только окно лаунчера
var window = TopLevel.GetTopLevel(this) as Window;
window?.Close();
}
@@ -68,7 +55,7 @@ public void Label_PointerPressed(object? sender, PointerPressedEventArgs e) {
}
private void Databases_OnDoubleTapped(object? sender, TappedEventArgs e) {
- if(databases.SelectedItem is not null)
- ViewModel.ConnectCommand.Execute(null);
+ if(databases.SelectedItem is not null && DataContext is DataBasesVM vm)
+ vm.ConnectCommand.Execute(null);
}
}
diff --git a/QS.Launcher.Avalonia/Views/Pages/LoginView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/LoginView.axaml.cs
index de2fcf8b0..00ede44dc 100644
--- a/QS.Launcher.Avalonia/Views/Pages/LoginView.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/Pages/LoginView.axaml.cs
@@ -2,10 +2,8 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media.Transformation;
-using Avalonia.Platform;
using Avalonia.Styling;
using QS.Launcher.ViewModels.PageViewModels;
-using System.Globalization;
namespace QS.Launcher.Views.Pages;
@@ -13,8 +11,8 @@ public partial class LoginView : UserControl
{
private readonly Style upStyle;
- public LoginView(LoginVM viewModel)
- {
+ public LoginView()
+ {
upStyle = new Style(x => x.OfType().Class("up")) {
Setters =
{
@@ -24,23 +22,22 @@ public LoginView(LoginVM viewModel)
},
};
- InitializeComponent();
+ InitializeComponent();
loginContainer.Styles.Add(upStyle);
- DataContext = viewModel;
-
Loaded += (s, e) => {
passwordTextBox.Focus();
};
KeyDown += (s, e) => {
- if(e.Key == Avalonia.Input.Key.Enter) {
- TopLevel.GetTopLevel(this).FocusManager.ClearFocus();
- viewModel.LoginCommand.Execute(null);
+ if(e.Key == Key.Enter) {
+ TopLevel.GetTopLevel(this)?.FocusManager?.ClearFocus();
+ if(DataContext is LoginVM vm)
+ vm.LoginCommand.Execute(null);
}
};
- }
+ }
private void ShowCreationView(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
diff --git a/QS.Launcher.Avalonia/Views/Pages/UserManagementView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/UserManagementView.axaml.cs
index ce3f1258f..cd038c5c8 100644
--- a/QS.Launcher.Avalonia/Views/Pages/UserManagementView.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/Pages/UserManagementView.axaml.cs
@@ -1,15 +1,9 @@
using Avalonia.Controls;
-using Microsoft.Extensions.DependencyInjection;
-using QS.Launcher.ViewModels.PageViewModels;
-using System;
-using System.Linq;
namespace QS.Launcher.Views.Pages;
public partial class UserManagementView : UserControl {
- public UserManagementView(UserManagementVM viewModel) {
+ public UserManagementView() {
InitializeComponent();
-
- DataContext = viewModel;
}
}
diff --git a/QS.Launcher/DependencyInjection.cs b/QS.Launcher/DependencyInjection.cs
index 2e4bc8a1a..8dcb66f2f 100644
--- a/QS.Launcher/DependencyInjection.cs
+++ b/QS.Launcher/DependencyInjection.cs
@@ -1,7 +1,9 @@
using Autofac;
using Microsoft.Extensions.DependencyInjection;
+using QS.DBScripts.Controllers;
using QS.DbManagement;
using QS.Launcher.AppRunner;
+using QS.Launcher.Services;
using QS.Launcher.ViewModels;
using QS.Launcher.ViewModels.PageViewModels;
using QS.Launcher.ViewModels.PageViewModels.DataBase;
@@ -14,7 +16,12 @@ public static IServiceCollection AddLauncherViewModels(this IServiceCollection s
.AddSingleton()
.AddSingleton()
.AddSingleton()
- .AddSingleton();
+ .AddSingleton()
+ // Wizard-страницы создания БД — Transient: новый экземпляр на каждое открытие.
+ .AddTransient()
+ .AddTransient()
+ // Сервисы лаунчера, нужные wizard-страницам.
+ .AddSingleton();
}
public static IServiceCollection AddLauncherOptions(this IServiceCollection services, LauncherOptions launcherOptions) {
diff --git a/QS.Launcher/Services/IUiThreadInvoker.cs b/QS.Launcher/Services/IUiThreadInvoker.cs
new file mode 100644
index 000000000..b7801d342
--- /dev/null
+++ b/QS.Launcher/Services/IUiThreadInvoker.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace QS.Launcher.Services {
+ ///
+ /// проксирование действия в UI-поток
+ ///
+ public interface IUiThreadInvoker {
+ /// Запланировать действие в UI-потоке без его блокировки
+ void Post(Action action);
+ }
+}
diff --git a/QS.Launcher/Services/LauncherDbCreatorInteraction.cs b/QS.Launcher/Services/LauncherDbCreatorInteraction.cs
new file mode 100644
index 000000000..fa2262cee
--- /dev/null
+++ b/QS.Launcher/Services/LauncherDbCreatorInteraction.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Threading.Tasks;
+using QS.DBScripts.Controllers;
+using QS.Dialog;
+
+namespace QS.Launcher.Services {
+ ///
+ /// проксирует вопросы и ошибки
+ /// все вызовы диалогов уходят в UI-поток через IUiThreadInvoker,
+ /// потому что creator дёргает их с фонового потока
+ ///
+ public class LauncherDbCreatorInteraction : IDbCreatorInteraction {
+ private readonly IInteractiveQuestion question;
+ private readonly IInteractiveMessage message;
+ private readonly IUiThreadInvoker uiThread;
+
+ public LauncherDbCreatorInteraction(
+ IInteractiveQuestion question,
+ IInteractiveMessage message,
+ IUiThreadInvoker uiThread)
+ {
+ this.question = question ?? throw new ArgumentNullException(nameof(question));
+ this.message = message ?? throw new ArgumentNullException(nameof(message));
+ this.uiThread = uiThread ?? throw new ArgumentNullException(nameof(uiThread));
+ }
+
+ public Task AskDropExistingDatabaseAsync(string dbName) {
+ var tcs = new TaskCompletionSource();
+ uiThread.Post(() => {
+ try {
+ bool answer = question.Question(
+ $"База с именем `{dbName}` уже существует на сервере. Удалить существующую базу перед созданием новой?",
+ "Создание базы данных");
+ tcs.TrySetResult(answer);
+ }
+ catch(Exception ex) { tcs.TrySetException(ex); }
+ });
+ return tcs.Task;
+ }
+
+ public Task ReportErrorAsync(string text, string lastExecutedStatement) {
+ var tcs = new TaskCompletionSource();
+ uiThread.Post(() => {
+ try {
+ message.ShowMessage(ImportanceLevel.Error, text, "Ошибка создания базы");
+ tcs.TrySetResult(true);
+ }
+ catch(Exception ex) { tcs.TrySetException(ex); }
+ });
+ return tcs.Task;
+ }
+ }
+}
diff --git a/QS.Launcher/ViewModels/MainWindowVM.cs b/QS.Launcher/ViewModels/MainWindowVM.cs
index ef09b2b60..af5495e60 100644
--- a/QS.Launcher/ViewModels/MainWindowVM.cs
+++ b/QS.Launcher/ViewModels/MainWindowVM.cs
@@ -1,14 +1,19 @@
-using QS.Launcher.ViewModels.PageViewModels;
-using ReactiveUI;
using System;
-using QS.ViewModels;
+using System.Collections.ObjectModel;
+using QS.Launcher.ViewModels.PageViewModels;
using QS.Launcher.ViewModels.PageViewModels.DataBase;
+using QS.ViewModels;
+using ReactiveUI;
namespace QS.Launcher.ViewModels {
+ ///
+ /// Держит коллекцию страниц для Carousel и индекс активной
+ ///
public class MainWindowVM : ViewModelBase {
- public int PagesCount { get; set; }
-
LoginVM login;
+ private readonly int rootPagesCount;
+
+ public ObservableCollection Pages { get; }
private int selectedPageIndex;
public int SelectedPageIndex {
@@ -16,16 +21,35 @@ public int SelectedPageIndex {
set => this.RaiseAndSetIfChanged(ref selectedPageIndex, value);
}
- public MainWindowVM(DataBasesVM dataBasesVM, LoginVM loginVM, BaseManagementVM baseManagementVM, UserManagementVM userManagementVM
- , IServiceProvider provider)
+ public int PagesCount {
+ get => rootPagesCount;
+ set { /* кол-во корневых страниц фиксируется в ctor */ }
+ }
+
+ public MainWindowVM(
+ DataBasesVM dataBasesVM,
+ LoginVM loginVM,
+ BaseManagementVM baseManagementVM,
+ UserManagementVM userManagementVM,
+ IServiceProvider provider)
{
- CarouselPageVM[] pages = { dataBasesVM, loginVM, baseManagementVM, userManagementVM };
- foreach (var page in pages) {
- page.NextPageCommand = ReactiveCommand.Create(NextPage);
- page.PreviousPageCommand = ReactiveCommand.Create(PreviousPage);
- page.ChangePageCommand = ReactiveCommand.Create(ChangePage);
- }
+ Pages = new ObservableCollection {
+ loginVM, dataBasesVM, baseManagementVM, userManagementVM
+ };
+ rootPagesCount = Pages.Count;
login = loginVM;
+
+ foreach(var page in Pages)
+ WirePage(page);
+ }
+
+ private void WirePage(CarouselPageVM page) {
+ page.NextPageCommand = ReactiveCommand.Create(NextPage);
+ page.PreviousPageCommand = ReactiveCommand.Create(PreviousPage);
+ page.ChangePageCommand = ReactiveCommand.Create(ChangePage);
+ page.PushPageCommand = ReactiveCommand.Create(PushPage);
+ page.PopPageCommand = ReactiveCommand.Create(PopPage);
+ page.PopToRootCommand = ReactiveCommand.Create(PopToRoot);
}
public void SaveConnections() {
@@ -33,15 +57,53 @@ public void SaveConnections() {
}
public void ChangePage(int index) {
+ if(index < 0 || index >= Pages.Count) return;
SelectedPageIndex = index;
}
public void NextPage() {
- ChangePage((SelectedPageIndex + 1) % PagesCount);
+ PopToRoot();
+ ChangePage((SelectedPageIndex + 1) % rootPagesCount);
}
public void PreviousPage() {
- ChangePage((SelectedPageIndex - 1 + PagesCount) % PagesCount);
+ PopToRoot();
+ ChangePage((SelectedPageIndex - 1 + rootPagesCount) % rootPagesCount);
+ }
+
+ public void PushPage(CarouselPageVM page) {
+ if(page == null) return;
+ WirePage(page);
+ Pages.Add(page);
+ SelectedPageIndex = Pages.Count - 1;
+ }
+
+ public void PopPage() {
+ if(Pages.Count <= rootPagesCount) return;
+ int last = Pages.Count - 1;
+ Pages.RemoveAt(last);
+ SelectedPageIndex = Pages.Count - 1;
+ }
+
+ public void PopToRoot() {
+ while(Pages.Count > rootPagesCount)
+ Pages.RemoveAt(Pages.Count - 1);
+ if(SelectedPageIndex >= rootPagesCount)
+ SelectedPageIndex = rootPagesCount - 1;
+ }
+
+ ///
+ /// Найти первую страницу указанного типа в стеке и переключиться на неё, сняв всё, что стоит выше
+ ///
+ public void PopToPage() where TPage : CarouselPageVM {
+ int targetIdx = -1;
+ for(int i = 0; i < Pages.Count; i++) {
+ if(Pages[i] is TPage) { targetIdx = i; break; }
+ }
+ if(targetIdx < 0) return;
+ while(Pages.Count > targetIdx + 1)
+ Pages.RemoveAt(Pages.Count - 1);
+ SelectedPageIndex = targetIdx;
}
}
}
diff --git a/QS.Launcher/ViewModels/PageViewModels/CarouselPageVM.cs b/QS.Launcher/ViewModels/PageViewModels/CarouselPageVM.cs
index 8cfa391a0..90524aba9 100644
--- a/QS.Launcher/ViewModels/PageViewModels/CarouselPageVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/CarouselPageVM.cs
@@ -2,11 +2,29 @@
using QS.ViewModels;
namespace QS.Launcher.ViewModels.PageViewModels {
+ ///
+ /// NextPage/PreviousPage/ChangePage — кольцевая навигация по корневым страницам
+ ///
public class CarouselPageVM : ViewModelBase {
public ICommand NextPageCommand { get; set; }
public ICommand PreviousPageCommand { get; set; }
public ICommand ChangePageCommand { get; set; }
+
+ ///
+ /// добавить страницу в конец Carousel и переключить фокус на неё
+ ///
+ public ICommand PushPageCommand { get; set; }
+
+ ///
+ /// Закрыть текущую нерутовую страницу и вернуться на предыдущую.
+ ///
+ public ICommand PopPageCommand { get; set; }
+
+ ///
+ /// Закрыть все нерутовые страницы и вернуться к корневым вкладкам.
+ ///
+ public ICommand PopToRootCommand { get; set; }
}
}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
new file mode 100644
index 000000000..77b077e07
--- /dev/null
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
@@ -0,0 +1,148 @@
+using System;
+using System.Reactive;
+using System.Threading;
+using System.Threading.Tasks;
+using QS.DbManagement;
+using QS.DBScripts.Controllers;
+using QS.Dialog;
+using QS.Launcher.Services;
+using ReactiveUI;
+
+namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
+ ///
+ /// показывает прогресс создания базы, прогресс приходит из не-UI потока, поэтому все мутации
+ /// reactive-свойств проксируются через IUiThreadInvoker
+ ///
+ public class CreateDataBaseProgressVM : CarouselPageVM, IProgressBarDisplayable {
+ private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
+
+ public IDbProvider Provider { get; }
+ public Connection Connection { get; }
+ public string DbName { get; }
+ public string DbTitle { get; }
+
+ private readonly IDbCreatorInteraction interaction;
+ private readonly IServiceProvider services;
+ private readonly IUiThreadInvoker uiThread;
+ private readonly CancellationTokenSource cts;
+
+ #region IProgressBarDisplayable backed properties
+
+ private double minValue;
+ private double maxValue = 1;
+ private double currentValue;
+ private string currentText;
+ private bool isStarted;
+
+ public double MinValue {
+ get => minValue;
+ private set => this.RaiseAndSetIfChanged(ref minValue, value);
+ }
+ public double MaxValue {
+ get => maxValue;
+ private set => this.RaiseAndSetIfChanged(ref maxValue, value);
+ }
+ public double Value {
+ get => currentValue;
+ private set => this.RaiseAndSetIfChanged(ref currentValue, value);
+ }
+ public string CurrentText {
+ get => currentText;
+ private set => this.RaiseAndSetIfChanged(ref currentText, value);
+ }
+ public bool IsStarted {
+ get => isStarted;
+ private set => this.RaiseAndSetIfChanged(ref isStarted, value);
+ }
+
+ #endregion
+
+ /// Поднимается, когда база успешно создана, на него должен быть подписан DataBasesVM
+ public event Action DatabaseCreated;
+
+ /// Поднимается, когда создание завершилось отменой
+ public event Action DatabaseCreationFailed;
+
+ public ReactiveCommand StartCreationCommand { get; }
+ public ReactiveCommand CancelCommand { get; }
+
+ public CreateDataBaseProgressVM(
+ IDbProvider provider,
+ Connection connection,
+ string dbName,
+ string dbTitle,
+ IDbCreatorInteraction interaction,
+ IUiThreadInvoker uiThread,
+ IServiceProvider services)
+ {
+ Provider = provider ?? throw new ArgumentNullException(nameof(provider));
+ Connection = connection ?? throw new ArgumentNullException(nameof(connection));
+ DbName = dbName ?? throw new ArgumentNullException(nameof(dbName));
+ DbTitle = dbTitle ?? throw new ArgumentNullException(nameof(dbTitle));
+ this.interaction = interaction ?? throw new ArgumentNullException(nameof(interaction));
+ this.uiThread = uiThread ?? throw new ArgumentNullException(nameof(uiThread));
+ this.services = services ?? throw new ArgumentNullException(nameof(services));
+ cts = new CancellationTokenSource();
+
+ StartCreationCommand = ReactiveCommand.CreateFromTask(StartCreationAsync);
+ CancelCommand = ReactiveCommand.Create(() => {
+ cts.Cancel();
+ PopToRootCommand?.Execute(null);
+ });
+ }
+
+ public async Task StartCreationAsync() {
+ try {
+ var args = new CreatorFactoryArgs {
+ Provider = Provider,
+ Progress = this,
+ Interaction = interaction,
+ CancellationToken = cts.Token,
+ ServiceProvider = services
+ };
+ IDBCreator creator = Connection.ConnectionType.CreateCreator(args);
+ bool ok = await creator.RunCreationAsync(DbName, DbTitle);
+ if(ok)
+ DatabaseCreated?.Invoke();
+ else
+ DatabaseCreationFailed?.Invoke();
+ }
+ catch(OperationCanceledException) {
+ logger.Info("Создание базы отменено.");
+ DatabaseCreationFailed?.Invoke();
+ }
+ catch(Exception ex) {
+ logger.Error(ex, "Сбой в процессе создания базы.");
+ await interaction.ReportErrorAsync(ex.Message, null);
+ DatabaseCreationFailed?.Invoke();
+ }
+ }
+
+ #region IProgressBarDisplayable
+ public void Start(double maxValue = 1, double minValue = 0, string text = null, double startValue = 0) {
+ uiThread.Post(() => {
+ MaxValue = maxValue;
+ MinValue = minValue;
+ Value = startValue;
+ if(text != null) CurrentText = text;
+ IsStarted = true;
+ });
+ }
+
+ public void Update(double curValue) => uiThread.Post(() => Value = curValue);
+
+ public void UpdateMax(double maxValue) => uiThread.Post(() => MaxValue = maxValue);
+
+ public void Update(string curText) => uiThread.Post(() => CurrentText = curText);
+
+ public void Add(double addValue = 1, string text = null) {
+ uiThread.Post(() => {
+ Value += addValue;
+ if(text != null) CurrentText = text;
+ });
+ }
+
+ public void Close() => uiThread.Post(() => IsStarted = false);
+ #endregion
+ }
+}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
new file mode 100644
index 000000000..ea1d5811a
--- /dev/null
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Reactive;
+using System.Reactive.Linq;
+using QS.DbManagement;
+using ReactiveUI;
+
+namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
+ ///
+ /// По «Далее» создаёт CreateDataBaseProgressVM и пушит её в Carousel поверх текущей
+ ///
+ public class CreateDataBaseSettingsVM : CarouselPageVM {
+ public IDbProvider Provider { get; }
+ public Connection Connection { get; }
+ private readonly IServiceProvider services;
+
+ private string dbTitle;
+ public string DbTitle {
+ get => dbTitle;
+ set => this.RaiseAndSetIfChanged(ref dbTitle, value);
+ }
+
+ private string dbName;
+ public string DbName {
+ get => dbName;
+ set => this.RaiseAndSetIfChanged(ref dbName, value);
+ }
+
+ public ReactiveCommand CreateDataBaseCommand { get; }
+ public ReactiveCommand CancelCommand { get; }
+
+ ///
+ /// Сообщает заинтересованным о том, что только что создана
+ /// progress-VM и пора подписаться на её события
+ ///
+ public event Action ProgressPageRequested;
+
+ public CreateDataBaseSettingsVM(IDbProvider provider, Connection connection, IServiceProvider services) {
+ Provider = provider ?? throw new ArgumentNullException(nameof(provider));
+ Connection = connection ?? throw new ArgumentNullException(nameof(connection));
+ this.services = services ?? throw new ArgumentNullException(nameof(services));
+
+ var canCreate = this.WhenAnyValue(x => x.DbName, x => x.DbTitle,
+ (name, title) => !string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(title));
+
+ CreateDataBaseCommand = ReactiveCommand.Create(GoToProgress, canCreate);
+ CancelCommand = ReactiveCommand.Create(() => PopPageCommand?.Execute(null));
+ }
+
+ private void GoToProgress() {
+ // Резолв через ActivatorUtilities — DI подставляет IDbCreatorInteraction/IUiThreadInvoker,
+ // а провайдер/соединение/имена приходят как runtime-аргументы.
+ var progress = Microsoft.Extensions.DependencyInjection.ActivatorUtilities
+ .CreateInstance(services, Provider, Connection, DbName, DbTitle);
+
+ ProgressPageRequested?.Invoke(progress);
+ PushPageCommand?.Execute(progress);
+ }
+ }
+}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseVM.cs
deleted file mode 100644
index 936f4c80d..000000000
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseVM.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using Dapper;
-using QS.DbManagement;
-using ReactiveUI;
-using QS.Launcher.AppRunner;
-using QS.Project.Versioning;
-using System;
-using System.Windows.Input;
-using System.Reactive;
-
-namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
- public class CreateDataBaseVM : ReactiveObject {
- private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
-
- public IDbProvider Provider { get; private set; }
-
- private string dbTitle;
- public string DbTitle {
- get => dbTitle;
- set => this.RaiseAndSetIfChanged(ref dbTitle, value);
- }
- private string dbName;
- public string DbName {
- get => dbName;
- set => this.RaiseAndSetIfChanged(ref dbName, value);
- }
- public ReactiveCommand CreateDataBaseCommand { get; }
- public event Action DatabaseCreated;
-
- public CreateDataBaseVM(IDbProvider dbProvider) {
- Provider = dbProvider;
-
- CreateDataBaseCommand = ReactiveCommand.Create(() => {
- if(Provider.CanCreateDatabase)
- Provider.CreateDatabase(DbName, DbTitle);
- else
- throw new InvalidOperationException("пользователь не должен получать доступ к созданию базы");
-
- DatabaseCreated?.Invoke();
- });
- }
- }
-}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
index c9606765c..1b7198ae1 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
@@ -1,17 +1,16 @@
-using DynamicData.Kernel;
-using QS.DbManagement;
-using QS.Dialog;
-using QS.Launcher.AppRunner;
-using QS.Project.Versioning;
-using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
-using System.Threading.Tasks;
using System.Windows.Input;
-using System.Xml.Linq;
+using DynamicData.Kernel;
+using Microsoft.Extensions.DependencyInjection;
+using QS.DbManagement;
+using QS.Dialog;
+using QS.Launcher.AppRunner;
+using QS.Project.Versioning;
+using ReactiveUI;
namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
public class DataBasesVM : CarouselPageVM {
@@ -27,11 +26,12 @@ public IDbProvider Provider {
Databases = provider.GetUserDatabases(applicationInfo).AsList();
this.RaisePropertyChanged(nameof(Databases));
- // Загружаем и устанавливаем последнюю выбранную базу
LoadLastSelectedDatabase();
}
}
+ public Connection CurrentConnection => currentConnection;
+
public void SetProvider(IDbProvider dbProvider, Connection connection, Action saveConnections) {
currentConnection = connection;
saveConnectionsAction = saveConnections;
@@ -49,9 +49,9 @@ public DbInfo SelectedDatabase {
public bool IsAdmin { get; set; } = false;
public bool ShouldCloseLauncherAfterStart { get; set; } = true;
-
+
private readonly LauncherOptions launcherOptions;
-
+
///
/// Указывает, должна ли быть видна галочка "Не закрывать лаунчер после подключения".
/// Видна только в standalone режиме (когда лаунчер - отдельное приложение).
@@ -62,40 +62,66 @@ public DbInfo SelectedDatabase {
public ReactiveCommand OpenCreateDatabaseCommand { get; }
public event Action StartLaunchProgram;
- public event Func> RequestShowCreateDbWindow;
IInteractiveMessage interactiveMessage;
+ private readonly IServiceProvider serviceProvider;
private readonly IAppRunner appRunner;
private readonly IApplicationInfo applicationInfo;
- public DataBasesVM(IAppRunner appRunner, IApplicationInfo applicationInfo, IInteractiveMessage interactiveMessage, LauncherOptions launcherOptions) {
+ public DataBasesVM(
+ IAppRunner appRunner,
+ IApplicationInfo applicationInfo,
+ IInteractiveMessage interactiveMessage,
+ LauncherOptions launcherOptions,
+ IServiceProvider serviceProvider)
+ {
this.appRunner = appRunner ?? throw new ArgumentNullException(nameof(appRunner));
this.applicationInfo = applicationInfo ?? throw new ArgumentNullException(nameof(applicationInfo));
this.interactiveMessage = interactiveMessage ?? throw new ArgumentNullException(nameof(interactiveMessage));
this.launcherOptions = launcherOptions;
-
- logger.Info($">>> DataBasesVM constructor: launcherOptions={launcherOptions}, IsStandalone={launcherOptions?.IsStandalone}");
+ this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
IObservable canExecuteConnection = this
.WhenAnyValue(x => x.SelectedDatabase)
.Select(x => x != null);
- IObservable canExecuteCreation = this
- .WhenAnyValue(x => x.SelectedDatabase)
- .Select(x => x != null);
ConnectCommand = ReactiveCommand.Create(Connect, canExecuteConnection);
- OpenCreateDatabaseCommand = ReactiveCommand.CreateFromTask(OpenCreateDatabaseAsync);
+ OpenCreateDatabaseCommand = ReactiveCommand.Create(OpenCreateDatabase);
}
- private async Task OpenCreateDatabaseAsync() {
- if(OpenCreateDatabaseCommand != null) {
- (string title, string name) = await RequestShowCreateDbWindow.Invoke(this);
- Databases = provider.GetUserDatabases(applicationInfo).AsList();
- this.RaisePropertyChanged(nameof(Databases));
- SelectedDatabase = Databases.FirstOrDefault(db => db.Title == title && db.BaseName == name);
- this.RaisePropertyChanged(nameof(SelectedDatabase));
- }
+ ///
+ /// создаёт CreateDataBaseSettingsVM возвращает фокус на DataBasesVM и обновляет список баз.
+ ///
+ private void OpenCreateDatabase() {
+ if(provider == null || currentConnection == null)
+ return;
+
+ var settings = ActivatorUtilities.CreateInstance(
+ serviceProvider, provider, currentConnection);
+
+ settings.ProgressPageRequested += progressVm => {
+ progressVm.DatabaseCreated += OnDatabaseCreatedFromWizard;
+ progressVm.DatabaseCreationFailed += () => {
+ // пользователь сам решит вернуться или попробовать снова
+ };
+ };
+
+ PushPageCommand?.Execute(settings);
+ }
+
+ private void OnDatabaseCreatedFromWizard() {
+ // Закрываем все wizard-страницы и возвращаемся на DataBasesVM.
+ PopToRootCommand?.Execute(null);
+ RefreshDatabases();
+ }
+
+ public void RefreshDatabases() {
+ if(provider == null) return;
+ Databases = provider.GetUserDatabases(applicationInfo).AsList();
+ this.RaisePropertyChanged(nameof(Databases));
+ SelectedDatabase = Databases.FirstOrDefault();
+ this.RaisePropertyChanged(nameof(SelectedDatabase));
}
private void LoadLastSelectedDatabase() {
@@ -103,7 +129,7 @@ private void LoadLastSelectedDatabase() {
return;
// Используем LastBaseId из текущего подключения
- if(currentConnection?.LastBaseId != null)
+ if(currentConnection?.LastBaseId != null)
SelectedDatabase = Databases.FirstOrDefault(db => db.BaseId == currentConnection.LastBaseId.Value);
if(SelectedDatabase == null)
@@ -119,12 +145,8 @@ public void Connect() {
return;
}
- // Сохраняем последнюю выбранную базу
SaveLastSelectedDatabase();
- // Определяем, нужно ли закрывать лаунчер через Shutdown
- // В standalone режиме учитываем галочку ShouldCloseLauncherAfterStart
- // В in-process режиме НЕ делаем shutdown (возвращаем false)
var isStandalone = launcherOptions?.IsStandalone ?? false;
logger.Info($">>> Connect: IsStandalone={isStandalone}, ShouldCloseLauncherAfterStart={ShouldCloseLauncherAfterStart}");
@@ -139,10 +161,8 @@ public void Connect() {
private void SaveLastSelectedDatabase() {
if(SelectedDatabase == null || currentConnection == null)
return;
- // Сохраняем BaseId в текущее подключение
currentConnection.LastBaseId = SelectedDatabase.BaseId;
- // Вызываем сохранение подключений
saveConnectionsAction?.Invoke();
}
}
diff --git a/QS.Project.Core/DBScripts/Controllers/IDBCreator.cs b/QS.Project.Core/DBScripts/Controllers/IDBCreator.cs
index d8c7daaaf..6ab27ea04 100644
--- a/QS.Project.Core/DBScripts/Controllers/IDBCreator.cs
+++ b/QS.Project.Core/DBScripts/Controllers/IDBCreator.cs
@@ -1,7 +1,12 @@
+using System.Threading.Tasks;
+
namespace QS.DBScripts.Controllers
{
+ ///
+ /// Универсальный контракт создания и наполнения базы данных.
+ ///
public interface IDBCreator
{
- void RunCreation(string server, string dbname);
+ Task RunCreationAsync(string dbName, string dbTitle);
}
-}
\ No newline at end of file
+}
diff --git a/QS.Project.Core/DBScripts/Controllers/IDbCreatorInteraction.cs b/QS.Project.Core/DBScripts/Controllers/IDbCreatorInteraction.cs
new file mode 100644
index 000000000..3a499361a
--- /dev/null
+++ b/QS.Project.Core/DBScripts/Controllers/IDbCreatorInteraction.cs
@@ -0,0 +1,14 @@
+using System.Threading.Tasks;
+
+namespace QS.DBScripts.Controllers
+{
+ ///
+ /// всплывающие окна у пользователя при уточнениях
+ ///
+ public interface IDbCreatorInteraction
+ {
+ Task AskDropExistingDatabaseAsync(string dbName);
+
+ Task ReportErrorAsync(string text, string lastExecutedStatement);
+ }
+}
diff --git a/QS.Project.Core/DBScripts/Controllers/IDbScriptsConfiguration.cs b/QS.Project.Core/DBScripts/Controllers/IDbScriptsConfiguration.cs
new file mode 100644
index 000000000..9732a81d4
--- /dev/null
+++ b/QS.Project.Core/DBScripts/Controllers/IDbScriptsConfiguration.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace QS.DBScripts.Controllers
+{
+ ///
+ /// Источник SQL-скриптов конкретного приложения, передаваемый в реализации IDBCreator
+ ///
+ public interface IDbScriptsConfiguration
+ {
+ Version CreationVersion { get; }
+
+ string GetCreationSqlScript();
+ }
+}
diff --git a/QS.Updater.Core/DBScripts/Controllers/IDbCreateController.cs b/QS.Updater.Core/DBScripts/Controllers/IDbCreateController.cs
index b7f14edae..98e27d3c4 100644
--- a/QS.Updater.Core/DBScripts/Controllers/IDbCreateController.cs
+++ b/QS.Updater.Core/DBScripts/Controllers/IDbCreateController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using QS.Dialog;
namespace QS.DBScripts.Controllers
diff --git a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
index 1ccec37f4..f9d5fb545 100644
--- a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
+++ b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
@@ -1,112 +1,127 @@
using System;
using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
using MySqlConnector;
using QS.DBScripts.Controllers;
+using QS.Dialog;
namespace QS.DBScripts.Models
{
- public class MySqlDbCreateModel
+ public class MySqlDbCreateModel : IDBCreator
{
static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
- private readonly IDbCreateController controller;
- private readonly CreationScript script;
- ///
- /// Если true (по умолчанию), после создания базы автоматически записывает
- /// новый GUID в таблицу base_parameters (параметр BaseGuid).
- ///
+ private readonly string connectionString;
+ private readonly IDbScriptsConfiguration scripts;
+ private readonly IProgressBarDisplayable progress;
+ private readonly IDbCreatorInteraction interaction;
+ private readonly CancellationToken cancellationToken;
+
public bool FillBaseGuid { get; set; } = true;
- public MySqlDbCreateModel(IDbCreateController controller, CreationScript script)
+ public MySqlDbCreateModel(
+ string connectionString,
+ IDbScriptsConfiguration scripts,
+ IProgressBarDisplayable progress,
+ IDbCreatorInteraction interaction,
+ CancellationToken cancellationToken)
{
- this.controller = controller ?? throw new ArgumentNullException(nameof(controller));
- this.script = script ?? throw new ArgumentNullException(nameof(script));
+ if(string.IsNullOrWhiteSpace(connectionString))
+ throw new ArgumentException("Connection string is required", nameof(connectionString));
+ this.connectionString = connectionString;
+ this.scripts = scripts ?? throw new ArgumentNullException(nameof(scripts));
+ this.progress = progress ?? throw new ArgumentNullException(nameof(progress));
+ this.interaction = interaction ?? throw new ArgumentNullException(nameof(interaction));
+ this.cancellationToken = cancellationToken;
}
- public bool RunCreation(string server, string dbname, string login, string password)
- {
- string connStr, host;
- uint port = 3306;
- string[] uriSplit = server.Split(new char[] { ':' }, 2, StringSplitOptions.RemoveEmptyEntries);
-
- if (uriSplit.Length == 0) {
- controller.WasError("Имя сервера не корректно.", null);
- return false;
- }
-
- host = uriSplit[0];
- if (uriSplit.Length > 1) {
- uint.TryParse(uriSplit[1], out port);
- }
-
- var conStrBuilder = new MySqlConnectionStringBuilder();
- conStrBuilder.Server = host;
- conStrBuilder.Port = port;
- conStrBuilder.UserID = login;
- conStrBuilder.Password = password;
- conStrBuilder.AllowUserVariables = true;
-
- connStr = conStrBuilder.ConnectionString;
+ public Task RunCreationAsync(string dbName, string dbTitle) {
+ // Тяжёлая часть с MySqlScript.Execute синхронная,
+ // поэтому уносим её на пул, чтобы не блокировать UI-поток
+ return Task.Run(() => RunCreation(dbName, dbTitle), cancellationToken);
+ }
- using (var connectionDB = new MySqlConnection(connStr)) {
- try
- {
+ public bool RunCreation(string dbName, string dbTitle) {
+ using(var connectionDB = new MySqlConnection(connectionString)) {
+ try {
logger.Info("Connecting to MySQL...");
connectionDB.Open();
+ cancellationToken.ThrowIfCancellationRequested();
logger.Info("Проверяем существует ли уже база.");
-
- var sql = "SHOW DATABASES;";
- var cmd = new MySqlCommand(sql, connectionDB);
+ var cmd = new MySqlCommand("SHOW DATABASES;", connectionDB);
bool needDropBase = false;
using (var rdr = cmd.ExecuteReader())
{
- while (rdr.Read())
+ while (rdr.Read())
{
- if (rdr[0].ToString() == dbname)
+ if (rdr[0].ToString() == dbName)
{
- if (controller.NeedDropDatabaseIfExists(dbname))
+ if (interaction.AskDropExistingDatabaseAsync(dbName).GetAwaiter().GetResult())
{
needDropBase = true;
break;
- } else
- return false;
+ }
+ return false;
}
}
}
+ cancellationToken.ThrowIfCancellationRequested();
logger.Info("Создаем новую базу.");
+ progress.Start(text: "Получаем скрипт создания базы");
- controller.Progress.Start(text: "Получаем скрипт создания базы");
-
- string sqlScript = script.GetSqlScript();
+ string sqlScript = scripts.GetCreationSqlScript();
int predictedCount = Regex.Matches(sqlScript, ";").Count;
logger.Debug("Предполагаем наличие {0} команд в скрипте.", predictedCount);
- controller.Progress.Start(maxValue: predictedCount + (needDropBase ? 2 : 1));
+ progress.Start(maxValue: predictedCount + (needDropBase ? 2 : 1));
- if (needDropBase)
+ if (needDropBase)
{
- logger.Info("Удаляем существующую базу {0}.", dbname);
- controller.Progress.Add(text: $"Удаляем существующую базу {dbname}");
- cmd.CommandText = String.Format("DROP DATABASE `{0}`", dbname);
+ logger.Info("Удаляем существующую базу {0}.", dbName);
+ progress.Add(text: $"Удаляем существующую базу {dbName}");
+ cmd.CommandText = $"DROP DATABASE `{dbName}`";
cmd.ExecuteNonQuery();
}
+ cancellationToken.ThrowIfCancellationRequested();
- controller.Progress.Add(text: $"Создаем базу {dbname}");
- cmd.CommandText = String.Format("CREATE SCHEMA `{0}` DEFAULT CHARACTER SET utf8mb4 ;", dbname);
+ progress.Add(text: $"Создаем базу {dbName}");
+ cmd.CommandText = $"CREATE SCHEMA `{dbName}` DEFAULT CHARACTER SET utf8mb4 ;";
cmd.ExecuteNonQuery();
- cmd.CommandText = String.Format("USE `{0}` ;", dbname);
+ cmd.CommandText = $"USE `{dbName}` ;";
cmd.ExecuteNonQuery();
- controller.Progress.Add(text: $"Создаем таблицы в {dbname}");
+ progress.Add(text: $"Создаем таблицы в {dbName}");
var myscript = new MySqlScript(connectionDB, sqlScript);
myscript.StatementExecuted += Myscript_StatementExecuted;
var commands = myscript.Execute();
logger.Debug("Выполнено {0} SQL-команд.", commands);
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Записываем человекочитаемое название базы, используется для отображения в списке БД
+ logger.Info("Записываем Title='{0}' в base_parameters.", dbTitle);
+ cmd.CommandText =
+ "INSERT INTO base_parameters (name, str_value) VALUES ('Title', @title) "
+ + "ON DUPLICATE KEY UPDATE str_value = @title";
+ cmd.Parameters.Clear();
+ cmd.Parameters.AddWithValue("@title", dbTitle ?? string.Empty);
+ cmd.ExecuteNonQuery();
- if (FillBaseGuid) {
+ // Версия пустой базы для апдейтера.
+ if(scripts.CreationVersion != null) {
+ logger.Info("Записываем version='{0}' в base_parameters.", scripts.CreationVersion);
+ cmd.CommandText =
+ "INSERT INTO base_parameters (name, str_value) VALUES ('version', @ver) "
+ + "ON DUPLICATE KEY UPDATE str_value = @ver";
+ cmd.Parameters.Clear();
+ cmd.Parameters.AddWithValue("@ver", scripts.CreationVersion.ToString());
+ cmd.ExecuteNonQuery();
+ }
+
+ if(FillBaseGuid) {
logger.Info("Генерируем BaseGuid");
cmd.CommandText =
"INSERT INTO base_parameters (name, str_value) VALUES ('BaseGuid', @guid)";
@@ -115,36 +130,41 @@ public bool RunCreation(string server, string dbname, string login, string passw
cmd.ExecuteNonQuery();
logger.Info("BaseGuid успешно записан.");
}
-
+ }
+ catch(OperationCanceledException) {
+ logger.Info("Создание базы отменено пользователем.");
+ throw;
}
catch(InvalidCastException ex) { //FIXME Временный для более адекватного обхода проблемы с отсутствием поддержки MariaDB 10.10. Удалить как починим работу с этой версией.
logger.Error(ex, "Ошибка подключения к серверу.");
- controller.WasError("Работа с MariaDB 10.10 пока не поддерживается. Установите версию MariaDB 10.9.", lastExecutedStatement);
+ interaction.ReportErrorAsync("Работа с MariaDB 10.10 пока не поддерживается. Установите версию MariaDB 10.9.", lastExecutedStatement)
+ .GetAwaiter().GetResult();
return false;
}
- catch (MySqlException ex)
- {
- logger.Info("Строка соединения: {0}", connStr);
- logger.Error(ex, "Ошибка подключения к серверу.");
- if (ex.Number == 1045 || ex.Number == 0)
- controller.WasError("Доступ запрещен.\nПроверьте логин и пароль.", lastExecutedStatement);
- else if (ex.Number == 1042)
- controller.WasError("Не удалось подключиться к серверу БД.", lastExecutedStatement);
+ catch(MySqlException ex) {
+ logger.Error(ex, "Ошибка работы с MySQL.");
+ string text;
+ if(ex.Number == 1045 || ex.Number == 0)
+ text = "Доступ запрещен.\nПроверьте логин и пароль.";
+ else if(ex.Number == 1042)
+ text = "Не удалось подключиться к серверу БД.";
else
- controller.WasError(ex.Message, lastExecutedStatement);
-
+ text = ex.Message;
+ interaction.ReportErrorAsync(text, lastExecutedStatement).GetAwaiter().GetResult();
return false;
- } finally {
- controller.Progress.Close();
+ }
+ finally {
+ if(progress.IsStarted)
+ progress.Close();
}
}
return true;
}
private string lastExecutedStatement;
- void Myscript_StatementExecuted(object sender, MySqlScriptEventArgs args)
+ private void Myscript_StatementExecuted(object sender, MySqlScriptEventArgs args)
{
- controller.Progress.Add();
+ progress.Add();
logger.Debug("SQL Command = {0}", args.StatementText);
lastExecutedStatement = $"[{args.Line}:{args.Position}]{args.StatementText}";
}
From a43ac621d26ce158efacabc04c41dbf35c99bde9 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Fri, 1 May 2026 22:03:23 +0300
Subject: [PATCH 004/135] =?UTF-8?q?=D0=BF=D0=BE=D1=87=D0=B8=D0=BD=D0=B8?=
=?UTF-8?q?=D0=BB=20=D0=BC=D0=B0=D0=BF=D0=BF=D0=B8=D0=BD=D0=B3=20=D1=81?=
=?UTF-8?q?=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=20+=20=D1=81=D0=BA=D1=80?=
=?UTF-8?q?=D0=B8=D0=BF=D1=82=D1=8B=20=D0=BD=D0=B0=D0=BF=D0=BE=D0=BB=D0=BD?=
=?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B1=D0=B0=D0=B7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
перевёл обратно на старый маппинг страниц
---
.../{ => DataBase}/QSCloudProvider.cs | 2 +-
.../QsCloudConnectionTypeBase.cs | 2 +-
.../DataBase/QsCloudScriptsConfiguration.cs | 30 +
QS.Cloud.Client/QS.Cloud.Client.csproj | 8 +-
QS.Cloud.Client/Scripts/new_empty.sql | 772 ++++++++++++++++++
QS.Launcher.Avalonia/DependencyInjection.cs | 6 +
QS.Launcher.Avalonia/LauncherApp.axaml | 21 -
.../Views/MainWindow.axaml.cs | 7 +-
.../Views/Pages/BaseManagementView.axaml.cs | 5 +-
.../Pages/DataBase/DataBasesView.axaml.cs | 11 +-
.../Views/Pages/LoginView.axaml.cs | 4 +-
.../Views/Pages/UserManagementView.axaml.cs | 5 +-
.../PageViewModels/BaseManagementVM.cs | 1 -
.../Controllers/IDbScriptsConfiguration.cs | 14 -
.../DBScripts/IDbScriptsConfiguration.cs | 12 +
.../DBScripts/Models/CreationScript.cs | 2 +-
.../DBScripts/Models/MySqlDbCreateModel.cs | 67 +-
17 files changed, 875 insertions(+), 94 deletions(-)
rename QS.Cloud.Client/{ => DataBase}/QSCloudProvider.cs (99%)
rename QS.Cloud.Client/{ => DataBase}/QsCloudConnectionTypeBase.cs (96%)
create mode 100644 QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs
create mode 100644 QS.Cloud.Client/Scripts/new_empty.sql
delete mode 100644 QS.Project.Core/DBScripts/Controllers/IDbScriptsConfiguration.cs
create mode 100644 QS.Updater.Core/DBScripts/IDbScriptsConfiguration.cs
diff --git a/QS.Cloud.Client/QSCloudProvider.cs b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
similarity index 99%
rename from QS.Cloud.Client/QSCloudProvider.cs
rename to QS.Cloud.Client/DataBase/QSCloudProvider.cs
index 3faae9181..5395936aa 100644
--- a/QS.Cloud.Client/QSCloudProvider.cs
+++ b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
@@ -10,7 +10,7 @@
using System;
using QS.Cloud.Client.Clients;
-namespace QS.Cloud.Client
+namespace QS.Cloud.Client.DataBase
{
public class QSCloudProvider : IDbProvider {
public string ConnectionString { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
diff --git a/QS.Cloud.Client/QsCloudConnectionTypeBase.cs b/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
similarity index 96%
rename from QS.Cloud.Client/QsCloudConnectionTypeBase.cs
rename to QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
index 504c04fe5..31a13de3e 100644
--- a/QS.Cloud.Client/QsCloudConnectionTypeBase.cs
+++ b/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
@@ -4,7 +4,7 @@
using System.Reflection;
using QS.Utilities.Extensions;
-namespace QS.Cloud.Client {
+namespace QS.Cloud.Client.DataBase {
public class QsCloudConnectionTypeBase : ConnectionTypeBase {
public QsCloudConnectionTypeBase() {
Title = "QS: Облако";
diff --git a/QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs b/QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs
new file mode 100644
index 000000000..091d50455
--- /dev/null
+++ b/QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs
@@ -0,0 +1,30 @@
+using QS.DBScripts;
+using QS.DBScripts.Models;
+using QS.Updater.DB;
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text;
+
+namespace QS.Cloud.Client.DataBase {
+ public class QsCloudScriptsConfiguration : IDbScriptsConfiguration {
+ public CreationScript MakeCreationScript() {
+ return new CreationScript(
+ Assembly.GetAssembly(typeof(QsCloudScriptsConfiguration)),
+ "QS.Cloud.Client.Scripts.new_empty.sql",
+ new Version(1, 0, 1)
+ );
+ }
+
+ public UpdateConfiguration MakeUpdateConfiguration() {
+ var configuration = new UpdateConfiguration();
+
+ configuration.AddUpdate(
+ new Version(1, 0),
+ new Version(1, 0, 1),
+ "QS.Cloud.Client.Scripts.1.0.1.sql");
+
+ return configuration;
+ }
+ }
+}
diff --git a/QS.Cloud.Client/QS.Cloud.Client.csproj b/QS.Cloud.Client/QS.Cloud.Client.csproj
index 086526cf2..ebee0064c 100644
--- a/QS.Cloud.Client/QS.Cloud.Client.csproj
+++ b/QS.Cloud.Client/QS.Cloud.Client.csproj
@@ -1,4 +1,4 @@
-
+
netstandard2.0
@@ -15,6 +15,11 @@
+
+
+
+
+
@@ -26,6 +31,7 @@
+
diff --git a/QS.Cloud.Client/Scripts/new_empty.sql b/QS.Cloud.Client/Scripts/new_empty.sql
new file mode 100644
index 000000000..0b30d71aa
--- /dev/null
+++ b/QS.Cloud.Client/Scripts/new_empty.sql
@@ -0,0 +1,772 @@
+-- phpMyAdmin SQL Dump
+-- version 5.0.4deb2~bpo10+1
+-- https://www.phpmyadmin.net/
+--
+-- Host: demeter.srv.qsolution.ru
+-- Generation Time: Apr 30, 2026 at 01:52 PM
+-- Server version: 10.3.39-MariaDB-0+deb10u2
+-- PHP Version: 7.3.31-1~deb10u7
+
+SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
+START TRANSACTION;
+SET time_zone = "+00:00";
+
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!40101 SET NAMES utf8mb4 */;
+
+--
+-- Database: `QSService`
+--
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `accounts`
+--
+
+CREATE TABLE `accounts` (
+ `id` int(10) UNSIGNED NOT NULL,
+ `login` varchar(20) NOT NULL,
+ `client_id` int(10) UNSIGNED DEFAULT NULL,
+ `customer` varchar(50) NOT NULL,
+ `email` varchar(50) DEFAULT NULL,
+ `paid_until` date DEFAULT NULL,
+ `notify_by_days` int(11) DEFAULT NULL COMMENT 'Уведомить за Н дней до окончания',
+ `bases_limit` int(10) UNSIGNED NOT NULL DEFAULT 1,
+ `users_limit` int(10) UNSIGNED NOT NULL DEFAULT 3,
+ `space_limit` int(10) UNSIGNED NOT NULL DEFAULT 500,
+ `deactivated` tinyint(1) NOT NULL DEFAULT 0
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `api_tokens`
+--
+
+CREATE TABLE `api_tokens` (
+ `id` int(10) UNSIGNED NOT NULL,
+ `base_id` int(10) UNSIGNED NOT NULL,
+ `token` char(36) NOT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `bases`
+--
+
+CREATE TABLE `bases` (
+ `id` int(10) UNSIGNED NOT NULL,
+ `account_id` int(10) UNSIGNED NOT NULL,
+ `server_id` int(10) UNSIGNED NOT NULL,
+ `base_title` varchar(64) DEFAULT NULL COMMENT 'Русское название базы для пользователя',
+ `base_name` varchar(45) NOT NULL,
+ `product_id` int(10) UNSIGNED NOT NULL,
+ `real_name` varchar(64) DEFAULT NULL,
+ `base_guid` char(36) DEFAULT NULL,
+ `wear_lk` tinyint(1) NOT NULL DEFAULT 0,
+ `number_of_lk_client` int(11) DEFAULT 0,
+ `claims_lk` tinyint(1) NOT NULL DEFAULT 0,
+ `postomats` tinyint(1) NOT NULL DEFAULT 0,
+ `catalog` tinyint(1) NOT NULL DEFAULT 0,
+ `comments` text DEFAULT NULL,
+ `ratings` tinyint(1) NOT NULL DEFAULT 0,
+ `appointment_lk` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'включение предварительной записи',
+ `washing_lk` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Включение отображения стирки в мобильном кабинете',
+ `speccoin_lk` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Включение функциональности спецкойнов',
+ `size_editing_lk` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'Редактирование размеров в мобилке',
+ `size_editing_days_before` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Запрет изменения размеров за указанное количество дней до выдачи.',
+ `postomat_email_notification` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Дублируют ли постоматы уведомления на Email.',
+ `stock_availability_enable` tinyint(1) NOT NULL DEFAULT 0,
+ `stock_availability_warehouse_id` int(10) UNSIGNED DEFAULT NULL COMMENT 'id склада по которому показывать наличие',
+ `choice_nomenclature_lk` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Выбор номенклатур сотрудником'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `bases_scripts`
+--
+
+CREATE TABLE `bases_scripts` (
+ `id` int(10) UNSIGNED NOT NULL,
+ `product_id` int(10) UNSIGNED NOT NULL,
+ `start_version` varchar(15) DEFAULT NULL,
+ `end_version` varchar(15) NOT NULL,
+ `script` mediumtext DEFAULT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `base_access`
+--
+
+CREATE TABLE `base_access` (
+ `id` int(10) UNSIGNED NOT NULL,
+ `user_id` int(10) UNSIGNED NOT NULL,
+ `base_id` int(10) UNSIGNED NOT NULL,
+ `admin` tinyint(1) NOT NULL DEFAULT 0,
+ `read_only` tinyint(1) NOT NULL DEFAULT 0,
+ `torpedo` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'База доступна в панели инструментов'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `base_parameters`
+--
+
+CREATE TABLE `base_parameters` (
+ `name` varchar(20) NOT NULL,
+ `str_value` varchar(100) DEFAULT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `bug_reports`
+--
+
+CREATE TABLE `bug_reports` (
+ `id` int(10) UNSIGNED NOT NULL,
+ `created` datetime DEFAULT NULL,
+ `last_update` datetime DEFAULT NULL,
+ `product_id` int(10) UNSIGNED NOT NULL,
+ `edition` varchar(20) DEFAULT NULL,
+ `version` varchar(16) NOT NULL,
+ `fixed_in_version` varchar(16) DEFAULT NULL COMMENT 'Версия в которой баг пофикшен',
+ `message` varchar(2000) DEFAULT NULL,
+ `stack_trace` text DEFAULT NULL,
+ `description` text DEFAULT NULL,
+ `email` varchar(600) DEFAULT NULL,
+ `count` int(10) UNSIGNED DEFAULT 1,
+ `status` enum('New','InWork','NeedInfo','Rejected','Later','Known','Unreproducable','EndOfLife','Done') DEFAULT 'New',
+ `comments` text DEFAULT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `bug_reports_messages`
+--
+
+CREATE TABLE `bug_reports_messages` (
+ `id` int(10) UNSIGNED NOT NULL,
+ `created` datetime NOT NULL,
+ `bug_reports_id` int(10) UNSIGNED NOT NULL,
+ `email` varchar(254) DEFAULT NULL,
+ `user_name` varchar(60) DEFAULT NULL,
+ `messages` text DEFAULT NULL,
+ `db_name` varchar(60) DEFAULT NULL,
+ `report_type` enum('User','Automatic','Known') NOT NULL DEFAULT 'User',
+ `log_file` mediumtext DEFAULT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `clients`
+--
+
+CREATE TABLE `clients` (
+ `id` int(10) UNSIGNED NOT NULL,
+ `name` varchar(300) NOT NULL,
+ `email` varchar(45) DEFAULT NULL,
+ `email_notifications` varchar(200) DEFAULT NULL COMMENT 'Адреса для уведомлений',
+ `city` varchar(45) DEFAULT NULL,
+ `comments` text DEFAULT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `cloud_users`
+--
+
+CREATE TABLE `cloud_users` (
+ `id` int(10) UNSIGNED NOT NULL,
+ `login` varchar(20) NOT NULL,
+ `name` varchar(80) DEFAULT NULL,
+ `password` varchar(81) NOT NULL,
+ `disabled` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Пользователь отключен',
+ `is_account_admin` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Администратор учетной записи',
+ `post` varchar(200) DEFAULT NULL COMMENT 'Должность',
+ `phone` varchar(16) DEFAULT NULL COMMENT 'Телефон',
+ `email` varchar(60) DEFAULT NULL,
+ `account_id` int(10) UNSIGNED NOT NULL,
+ `multi_ip` tinyint(1) NOT NULL DEFAULT 0,
+ `client_id` int(10) UNSIGNED DEFAULT NULL,
+ `comment` text DEFAULT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `products`
+--
+
+CREATE TABLE `products` (
+ `id` int(10) UNSIGNED NOT NULL,
+ `name` varchar(45) NOT NULL,
+ `internal_name` varchar(45) NOT NULL,
+ `not_support_ver_regexp` varchar(45) DEFAULT NULL,
+ `telegram_notify` varchar(50) DEFAULT NULL COMMENT 'Отправлять уведомления о новых ошибках в чат'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `product_editions`
+--
+
+CREATE TABLE `product_editions` (
+ `id` int(10) UNSIGNED NOT NULL,
+ `product_id` int(10) UNSIGNED NOT NULL,
+ `code_number` int(10) UNSIGNED DEFAULT NULL COMMENT 'Номер редакции, внутри продукта',
+ `code_name` varchar(10) DEFAULT NULL COMMENT 'Кодовое имя редакции.',
+ `name` varchar(100) DEFAULT NULL COMMENT 'Название редакции отображаемое для пользователя.'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `product_versions`
+--
+
+CREATE TABLE `product_versions` (
+ `id` int(10) UNSIGNED NOT NULL,
+ `product_id` int(10) UNSIGNED NOT NULL,
+ `modification` varchar(25) DEFAULT NULL,
+ `channel` enum('Current','Stable') NOT NULL DEFAULT 'Current',
+ `disable` tinyint(1) NOT NULL DEFAULT 0,
+ `version_major` int(10) UNSIGNED NOT NULL DEFAULT 0,
+ `version_minor` int(10) UNSIGNED NOT NULL DEFAULT 0,
+ `version_build` int(10) UNSIGNED NOT NULL DEFAULT 0,
+ `version_revision` int(10) UNSIGNED NOT NULL DEFAULT 0,
+ `date` date NOT NULL,
+ `link_install` varchar(256) DEFAULT NULL,
+ `link_news` varchar(256) DEFAULT NULL,
+ `changes` text DEFAULT NULL,
+ `db_update` enum('None','Required','BreakingChange') NOT NULL DEFAULT 'None',
+ `comment` text DEFAULT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `serial_numbers`
+--
+
+CREATE TABLE `serial_numbers` (
+ `id` int(10) UNSIGNED NOT NULL,
+ `client_id` int(10) UNSIGNED NOT NULL,
+ `number` varchar(50) NOT NULL,
+ `recall` tinyint(1) NOT NULL DEFAULT 0,
+ `notify_by_days` int(11) DEFAULT NULL COMMENT 'Уведомить за Н дней до окончания',
+ `active_until` date DEFAULT NULL,
+ `serial_expiry_date` date DEFAULT NULL COMMENT 'Дата окончания действия серийного номера',
+ `instance` int(10) UNSIGNED NOT NULL DEFAULT 1,
+ `comment` text DEFAULT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `servers`
+--
+
+CREATE TABLE `servers` (
+ `id` int(10) UNSIGNED NOT NULL,
+ `server_address` varchar(60) NOT NULL,
+ `service_user` varchar(16) NOT NULL,
+ `service_password` varchar(81) NOT NULL,
+ `comment` text DEFAULT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `sessions`
+--
+
+CREATE TABLE `sessions` (
+ `id` int(10) UNSIGNED NOT NULL,
+ `session_id` varchar(36) NOT NULL,
+ `user_id` int(10) UNSIGNED DEFAULT NULL,
+ `account_id` int(10) UNSIGNED NOT NULL,
+ `base_id` int(10) UNSIGNED NOT NULL,
+ `start_time` datetime NOT NULL,
+ `end_time` datetime NOT NULL,
+ `is_closed` tinyint(1) NOT NULL DEFAULT 0,
+ `login` varchar(40) DEFAULT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `telemetry_statistics`
+--
+
+CREATE TABLE `telemetry_statistics` (
+ `id` int(10) UNSIGNED NOT NULL,
+ `last_update` datetime NOT NULL,
+ `ip_address` varchar(39) DEFAULT NULL,
+ `product` varchar(20) NOT NULL,
+ `edition` varchar(20) DEFAULT NULL,
+ `version` varchar(20) NOT NULL,
+ `os` varchar(100) DEFAULT NULL,
+ `net_framework` varchar(100) DEFAULT NULL,
+ `is_demo` tinyint(1) NOT NULL DEFAULT 0,
+ `app_edition` int(10) UNSIGNED DEFAULT NULL COMMENT 'Редакция программы',
+ `base_employees` int(10) UNSIGNED DEFAULT NULL COMMENT 'Количество сотрудников в базе',
+ `counter` mediumtext NOT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `update_info`
+--
+
+CREATE TABLE `update_info` (
+ `id` int(10) UNSIGNED NOT NULL,
+ `product` varchar(25) NOT NULL,
+ `edition` varchar(25) DEFAULT NULL,
+ `serial_number` varchar(45) DEFAULT NULL,
+ `start_version_major` int(10) UNSIGNED DEFAULT 0,
+ `start_version_minor` int(10) UNSIGNED DEFAULT 0,
+ `start_version_build` int(10) UNSIGNED DEFAULT 0,
+ `start_version_revision` int(10) UNSIGNED DEFAULT 0,
+ `new_version_major` int(10) UNSIGNED DEFAULT 0,
+ `new_version_minor` int(10) UNSIGNED DEFAULT 0,
+ `new_version_build` int(10) UNSIGNED DEFAULT 0,
+ `new_version_revision` int(10) UNSIGNED DEFAULT 0,
+ `link_install` varchar(256) NOT NULL,
+ `link_news` varchar(256) DEFAULT NULL,
+ `use_common` tinyint(1) DEFAULT 0,
+ `update_description` text DEFAULT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `update_statistics`
+--
+
+CREATE TABLE `update_statistics` (
+ `id` int(10) UNSIGNED NOT NULL,
+ `product_id` int(10) UNSIGNED DEFAULT NULL,
+ `edition` varchar(25) DEFAULT NULL,
+ `serial_number` varchar(45) DEFAULT NULL,
+ `client_version` varchar(16) NOT NULL,
+ `new_version` varchar(16) DEFAULT NULL,
+ `date` datetime NOT NULL DEFAULT utc_timestamp(),
+ `client_ip` varchar(15) DEFAULT NULL,
+ `channel` enum('Current','Stable') NOT NULL DEFAULT 'Current',
+ `status` enum('NoUpdates','NeedUpdate','Expired','Recalled','LicenceNotFound') DEFAULT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `users`
+--
+
+CREATE TABLE `users` (
+ `id` int(10) UNSIGNED NOT NULL,
+ `name` varchar(45) NOT NULL,
+ `login` varchar(45) NOT NULL,
+ `deactivated` tinyint(1) NOT NULL DEFAULT 0,
+ `email` varchar(60) DEFAULT NULL,
+ `description` text DEFAULT NULL,
+ `admin` tinyint(1) NOT NULL DEFAULT 0
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+--
+-- Indexes for dumped tables
+--
+
+--
+-- Indexes for table `accounts`
+--
+ALTER TABLE `accounts`
+ ADD PRIMARY KEY (`id`),
+ ADD UNIQUE KEY `login_UNIQUE` (`login`),
+ ADD KEY `fk_accounts_1_idx` (`client_id`);
+
+--
+-- Indexes for table `api_tokens`
+--
+ALTER TABLE `api_tokens`
+ ADD PRIMARY KEY (`id`),
+ ADD UNIQUE KEY `token_UNIQUE` (`token`),
+ ADD KEY `fk_api_tokens_1_idx` (`base_id`);
+
+--
+-- Indexes for table `bases`
+--
+ALTER TABLE `bases`
+ ADD PRIMARY KEY (`id`),
+ ADD UNIQUE KEY `idx1_bases` (`base_name`,`account_id`),
+ ADD UNIQUE KEY `base_guid_UNIQUE` (`base_guid`),
+ ADD KEY `fk1_bases_idx` (`account_id`),
+ ADD KEY `fk2_bases_idx` (`server_id`),
+ ADD KEY `fk3_bases_idx` (`product_id`);
+
+--
+-- Indexes for table `bases_scripts`
+--
+ALTER TABLE `bases_scripts`
+ ADD PRIMARY KEY (`id`),
+ ADD KEY `fk1_bases_scripts_idx` (`product_id`);
+
+--
+-- Indexes for table `base_access`
+--
+ALTER TABLE `base_access`
+ ADD PRIMARY KEY (`id`),
+ ADD UNIQUE KEY `base_access_idx` (`user_id`,`base_id`),
+ ADD KEY `fk1_base_access_idx` (`user_id`),
+ ADD KEY `fk1_base_access_idx1` (`base_id`);
+
+--
+-- Indexes for table `base_parameters`
+--
+ALTER TABLE `base_parameters`
+ ADD PRIMARY KEY (`name`);
+
+--
+-- Indexes for table `bug_reports`
+--
+ALTER TABLE `bug_reports`
+ ADD PRIMARY KEY (`id`),
+ ADD KEY `fk_bug_reports_1_idx` (`product_id`),
+ ADD KEY `bug_reports_created` (`created`),
+ ADD KEY `bug_reports_last_update` (`last_update`),
+ ADD KEY `bug_reports_edition` (`edition`),
+ ADD KEY `bug_reports_version` (`version`),
+ ADD KEY `bug_reports_status` (`status`);
+
+--
+-- Indexes for table `bug_reports_messages`
+--
+ALTER TABLE `bug_reports_messages`
+ ADD PRIMARY KEY (`id`),
+ ADD KEY `fk_bug_reports_messages_1_idx` (`bug_reports_id`);
+
+--
+-- Indexes for table `clients`
+--
+ALTER TABLE `clients`
+ ADD PRIMARY KEY (`id`);
+
+--
+-- Indexes for table `cloud_users`
+--
+ALTER TABLE `cloud_users`
+ ADD PRIMARY KEY (`id`),
+ ADD UNIQUE KEY `idx1_users` (`login`,`account_id`),
+ ADD KEY `fk1_users_idx` (`account_id`),
+ ADD KEY `fk_cloud_users_1_idx` (`client_id`);
+
+--
+-- Indexes for table `products`
+--
+ALTER TABLE `products`
+ ADD PRIMARY KEY (`id`),
+ ADD UNIQUE KEY `internal_name_UNIQUE` (`internal_name`);
+
+--
+-- Indexes for table `product_editions`
+--
+ALTER TABLE `product_editions`
+ ADD PRIMARY KEY (`id`),
+ ADD KEY `fk_product_id_idx` (`product_id`);
+
+--
+-- Indexes for table `product_versions`
+--
+ALTER TABLE `product_versions`
+ ADD PRIMARY KEY (`id`),
+ ADD KEY `_idx` (`product_id`);
+
+--
+-- Indexes for table `serial_numbers`
+--
+ALTER TABLE `serial_numbers`
+ ADD PRIMARY KEY (`id`),
+ ADD UNIQUE KEY `number_UNIQUE` (`number`),
+ ADD KEY `fk_serial_numbers_1_idx` (`client_id`);
+
+--
+-- Indexes for table `servers`
+--
+ALTER TABLE `servers`
+ ADD PRIMARY KEY (`id`);
+
+--
+-- Indexes for table `sessions`
+--
+ALTER TABLE `sessions`
+ ADD PRIMARY KEY (`id`),
+ ADD UNIQUE KEY `session_id_UNIQUE` (`session_id`),
+ ADD KEY `fk1_sessions_idx` (`account_id`),
+ ADD KEY `fk2_sessions_idx` (`user_id`),
+ ADD KEY `fk3_sessions_idx` (`base_id`),
+ ADD KEY `end_time_idx` (`end_time`),
+ ADD KEY `is_closed_idx` (`is_closed`);
+
+--
+-- Indexes for table `telemetry_statistics`
+--
+ALTER TABLE `telemetry_statistics`
+ ADD PRIMARY KEY (`id`),
+ ADD KEY `index_telemetry_statistics_last_update` (`last_update`),
+ ADD KEY `index_telemetry_statistics_ip` (`ip_address`),
+ ADD KEY `index_telemetry_statistics_product` (`product`),
+ ADD KEY `index_telemetry_statistics_edition` (`edition`),
+ ADD KEY `index_telemetry_statistics_version` (`version`),
+ ADD KEY `inxex_telemetry_statistics_os` (`os`);
+
+--
+-- Indexes for table `update_info`
+--
+ALTER TABLE `update_info`
+ ADD PRIMARY KEY (`id`);
+
+--
+-- Indexes for table `update_statistics`
+--
+ALTER TABLE `update_statistics`
+ ADD PRIMARY KEY (`id`),
+ ADD UNIQUE KEY `id_UNIQUE` (`id`),
+ ADD KEY `fk_update_statistics_1_idx` (`product_id`),
+ ADD KEY `update_statistics_edition_idx` (`edition`),
+ ADD KEY `update_statistics_serial_idx` (`serial_number`),
+ ADD KEY `update_statistics_client_version_idx` (`client_version`),
+ ADD KEY `update_statistics_ip_idx` (`client_ip`),
+ ADD KEY `update_statistics_new_version_idx` (`new_version`),
+ ADD KEY `update_statistics_date_idx` (`date`),
+ ADD KEY `update_statistics_channel_idx` (`channel`);
+
+--
+-- Indexes for table `users`
+--
+ALTER TABLE `users`
+ ADD PRIMARY KEY (`id`);
+
+--
+-- AUTO_INCREMENT for dumped tables
+--
+
+--
+-- AUTO_INCREMENT for table `accounts`
+--
+ALTER TABLE `accounts`
+ MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
+
+--
+-- AUTO_INCREMENT for table `api_tokens`
+--
+ALTER TABLE `api_tokens`
+ MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
+
+--
+-- AUTO_INCREMENT for table `bases`
+--
+ALTER TABLE `bases`
+ MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
+
+--
+-- AUTO_INCREMENT for table `bases_scripts`
+--
+ALTER TABLE `bases_scripts`
+ MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
+
+--
+-- AUTO_INCREMENT for table `base_access`
+--
+ALTER TABLE `base_access`
+ MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
+
+--
+-- AUTO_INCREMENT for table `bug_reports`
+--
+ALTER TABLE `bug_reports`
+ MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
+
+--
+-- AUTO_INCREMENT for table `bug_reports_messages`
+--
+ALTER TABLE `bug_reports_messages`
+ MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
+
+--
+-- AUTO_INCREMENT for table `clients`
+--
+ALTER TABLE `clients`
+ MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
+
+--
+-- AUTO_INCREMENT for table `cloud_users`
+--
+ALTER TABLE `cloud_users`
+ MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
+
+--
+-- AUTO_INCREMENT for table `products`
+--
+ALTER TABLE `products`
+ MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
+
+--
+-- AUTO_INCREMENT for table `product_editions`
+--
+ALTER TABLE `product_editions`
+ MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
+
+--
+-- AUTO_INCREMENT for table `product_versions`
+--
+ALTER TABLE `product_versions`
+ MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
+
+--
+-- AUTO_INCREMENT for table `serial_numbers`
+--
+ALTER TABLE `serial_numbers`
+ MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
+
+--
+-- AUTO_INCREMENT for table `servers`
+--
+ALTER TABLE `servers`
+ MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
+
+--
+-- AUTO_INCREMENT for table `sessions`
+--
+ALTER TABLE `sessions`
+ MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
+
+--
+-- AUTO_INCREMENT for table `telemetry_statistics`
+--
+ALTER TABLE `telemetry_statistics`
+ MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
+
+--
+-- AUTO_INCREMENT for table `update_info`
+--
+ALTER TABLE `update_info`
+ MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
+
+--
+-- AUTO_INCREMENT for table `update_statistics`
+--
+ALTER TABLE `update_statistics`
+ MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
+
+--
+-- AUTO_INCREMENT for table `users`
+--
+ALTER TABLE `users`
+ MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
+
+--
+-- Constraints for dumped tables
+--
+
+--
+-- Constraints for table `accounts`
+--
+ALTER TABLE `accounts`
+ ADD CONSTRAINT `fk_accounts_1` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
+
+--
+-- Constraints for table `api_tokens`
+--
+ALTER TABLE `api_tokens`
+ ADD CONSTRAINT `fk_api_tokens_1` FOREIGN KEY (`base_id`) REFERENCES `bases` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
+
+--
+-- Constraints for table `bases`
+--
+ALTER TABLE `bases`
+ ADD CONSTRAINT `fk1_bases` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
+ ADD CONSTRAINT `fk2_bases` FOREIGN KEY (`server_id`) REFERENCES `servers` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
+ ADD CONSTRAINT `fk3_bases` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
+
+--
+-- Constraints for table `bases_scripts`
+--
+ALTER TABLE `bases_scripts`
+ ADD CONSTRAINT `fk1_bases_scripts` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
+
+--
+-- Constraints for table `base_access`
+--
+ALTER TABLE `base_access`
+ ADD CONSTRAINT `fk1_base_access` FOREIGN KEY (`user_id`) REFERENCES `cloud_users` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
+ ADD CONSTRAINT `fk2_base_access` FOREIGN KEY (`base_id`) REFERENCES `bases` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
+
+--
+-- Constraints for table `bug_reports`
+--
+ALTER TABLE `bug_reports`
+ ADD CONSTRAINT `fk_bug_reports_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION;
+
+--
+-- Constraints for table `bug_reports_messages`
+--
+ALTER TABLE `bug_reports_messages`
+ ADD CONSTRAINT `fk_bug_reports_messages_1` FOREIGN KEY (`bug_reports_id`) REFERENCES `bug_reports` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
+
+--
+-- Constraints for table `cloud_users`
+--
+ALTER TABLE `cloud_users`
+ ADD CONSTRAINT `fk1_users` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION,
+ ADD CONSTRAINT `fk_cloud_users_1` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
+
+--
+-- Constraints for table `product_editions`
+--
+ALTER TABLE `product_editions`
+ ADD CONSTRAINT `fk_product_id` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
+
+--
+-- Constraints for table `product_versions`
+--
+ALTER TABLE `product_versions`
+ ADD CONSTRAINT `fk_product_versions_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
+
+--
+-- Constraints for table `serial_numbers`
+--
+ALTER TABLE `serial_numbers`
+ ADD CONSTRAINT `fk_serial_numbers_1` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
+
+--
+-- Constraints for table `sessions`
+--
+ALTER TABLE `sessions`
+ ADD CONSTRAINT `fk1_sessions` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
+ ADD CONSTRAINT `fk2_sessions` FOREIGN KEY (`user_id`) REFERENCES `cloud_users` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
+ ADD CONSTRAINT `fk3_sessions` FOREIGN KEY (`base_id`) REFERENCES `bases` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
+
+--
+-- Constraints for table `update_statistics`
+--
+ALTER TABLE `update_statistics`
+ ADD CONSTRAINT `fk_update_statistics_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
+COMMIT;
+
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
diff --git a/QS.Launcher.Avalonia/DependencyInjection.cs b/QS.Launcher.Avalonia/DependencyInjection.cs
index 652f84797..b27725ca7 100644
--- a/QS.Launcher.Avalonia/DependencyInjection.cs
+++ b/QS.Launcher.Avalonia/DependencyInjection.cs
@@ -1,12 +1,18 @@
+using Avalonia.Controls;
using Microsoft.Extensions.DependencyInjection;
using QS.Launcher.Services;
using QS.Launcher.Views;
+using QS.Launcher.Views.Pages;
namespace QS.Launcher;
public static partial class DependencyInjection {
public static IServiceCollection AddPages(this IServiceCollection services) {
return services
.AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
.AddSingleton();
}
}
diff --git a/QS.Launcher.Avalonia/LauncherApp.axaml b/QS.Launcher.Avalonia/LauncherApp.axaml
index c02018fd8..f1502c652 100644
--- a/QS.Launcher.Avalonia/LauncherApp.axaml
+++ b/QS.Launcher.Avalonia/LauncherApp.axaml
@@ -15,27 +15,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/QS.Launcher.Avalonia/Views/MainWindow.axaml.cs b/QS.Launcher.Avalonia/Views/MainWindow.axaml.cs
index bb6747c0d..d377b5ad7 100644
--- a/QS.Launcher.Avalonia/Views/MainWindow.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/MainWindow.axaml.cs
@@ -1,11 +1,12 @@
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using QS.Launcher.ViewModels;
+using System.Collections.Generic;
namespace QS.Launcher.Views;
public partial class MainWindow : Window {
- public MainWindow(MainWindowVM vm, LauncherOptions options) {
+ public MainWindow(MainWindowVM vm, IEnumerable pages, LauncherOptions options) {
InitializeComponent();
Icon = new WindowIcon(new Bitmap(new System.IO.MemoryStream(options.LogoIcon)));
@@ -13,6 +14,10 @@ public MainWindow(MainWindowVM vm, LauncherOptions options) {
Closing += (_, _) => vm.SaveConnections();
+ foreach(var page in pages)
+ carousel.Items.Add(page);
+ vm.PagesCount = carousel.ItemCount;
+
DataContext = vm;
}
}
diff --git a/QS.Launcher.Avalonia/Views/Pages/BaseManagementView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/BaseManagementView.axaml.cs
index d96f22ca3..b0bbc5238 100644
--- a/QS.Launcher.Avalonia/Views/Pages/BaseManagementView.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/Pages/BaseManagementView.axaml.cs
@@ -1,9 +1,12 @@
using Avalonia.Controls;
+using QS.Launcher.ViewModels.PageViewModels;
namespace QS.Launcher.Views.Pages;
public partial class BaseManagementView : UserControl {
- public BaseManagementView() {
+ public BaseManagementView(BaseManagementVM viewModel) {
InitializeComponent();
+
+ DataContext = viewModel;
}
}
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml.cs
index 9d35405e8..157d54db3 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml.cs
@@ -10,13 +10,12 @@ namespace QS.Launcher.Views.Pages;
public partial class DataBasesView : UserControl {
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
- public DataBasesView() {
+ public DataBasesView(DataBasesVM viewModel) {
InitializeComponent();
- DataContextChanged += (_, _) => {
- if(DataContext is DataBasesVM vm)
- vm.StartLaunchProgram += HandleStartMainProgram;
- };
+ DataContext = viewModel;
+
+ viewModel.StartLaunchProgram += HandleStartMainProgram;
KeyDown += (s, e) => {
if(e.Key == Key.Enter) {
@@ -40,10 +39,12 @@ public async void HandleStartMainProgram(bool shouldCloseLauncher) {
if(shouldCloseLauncher) {
logger.Info($">>> HandleStartMainProgram: Вызываем Shutdown!");
+ // NewProcessRunner: закрываем всё приложение лаунчера (Shutdown)
(LauncherApp.Current!.ApplicationLifetime as ClassicDesktopStyleApplicationLifetime)?.Shutdown();
}
else {
logger.Info($">>> HandleStartMainProgram: Закрываем только окно");
+ // InProcessRunner: закрываем только окно лаунчера
var window = TopLevel.GetTopLevel(this) as Window;
window?.Close();
}
diff --git a/QS.Launcher.Avalonia/Views/Pages/LoginView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/LoginView.axaml.cs
index 00ede44dc..69dc133f4 100644
--- a/QS.Launcher.Avalonia/Views/Pages/LoginView.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/Pages/LoginView.axaml.cs
@@ -11,7 +11,7 @@ public partial class LoginView : UserControl
{
private readonly Style upStyle;
- public LoginView()
+ public LoginView(LoginVM viewModel)
{
upStyle = new Style(x => x.OfType().Class("up")) {
Setters =
@@ -26,6 +26,8 @@ public LoginView()
loginContainer.Styles.Add(upStyle);
+ DataContext = viewModel;
+
Loaded += (s, e) => {
passwordTextBox.Focus();
};
diff --git a/QS.Launcher.Avalonia/Views/Pages/UserManagementView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/UserManagementView.axaml.cs
index cd038c5c8..5a4b62bc7 100644
--- a/QS.Launcher.Avalonia/Views/Pages/UserManagementView.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/Pages/UserManagementView.axaml.cs
@@ -1,9 +1,12 @@
using Avalonia.Controls;
+using QS.Launcher.ViewModels.PageViewModels;
namespace QS.Launcher.Views.Pages;
public partial class UserManagementView : UserControl {
- public UserManagementView() {
+ public UserManagementView(UserManagementVM viewModel) {
InitializeComponent();
+
+ DataContext = viewModel;
}
}
diff --git a/QS.Launcher/ViewModels/PageViewModels/BaseManagementVM.cs b/QS.Launcher/ViewModels/PageViewModels/BaseManagementVM.cs
index 7844609c2..d0d0de5ae 100644
--- a/QS.Launcher/ViewModels/PageViewModels/BaseManagementVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/BaseManagementVM.cs
@@ -9,7 +9,6 @@ public class BaseManagementVM : CarouselPageVM {
public DatabaseViewModel SelectedDatabase { get; set; }
public BaseManagementVM() {
-
}
}
}
diff --git a/QS.Project.Core/DBScripts/Controllers/IDbScriptsConfiguration.cs b/QS.Project.Core/DBScripts/Controllers/IDbScriptsConfiguration.cs
deleted file mode 100644
index 9732a81d4..000000000
--- a/QS.Project.Core/DBScripts/Controllers/IDbScriptsConfiguration.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System;
-
-namespace QS.DBScripts.Controllers
-{
- ///
- /// Источник SQL-скриптов конкретного приложения, передаваемый в реализации IDBCreator
- ///
- public interface IDbScriptsConfiguration
- {
- Version CreationVersion { get; }
-
- string GetCreationSqlScript();
- }
-}
diff --git a/QS.Updater.Core/DBScripts/IDbScriptsConfiguration.cs b/QS.Updater.Core/DBScripts/IDbScriptsConfiguration.cs
new file mode 100644
index 000000000..a3bdba61a
--- /dev/null
+++ b/QS.Updater.Core/DBScripts/IDbScriptsConfiguration.cs
@@ -0,0 +1,12 @@
+using QS.DBScripts.Models;
+using QS.Updater.DB;
+
+namespace QS.DBScripts
+{
+ public interface IDbScriptsConfiguration
+ {
+ CreationScript MakeCreationScript();
+
+ UpdateConfiguration MakeUpdateConfiguration();
+ }
+}
diff --git a/QS.Updater.Core/DBScripts/Models/CreationScript.cs b/QS.Updater.Core/DBScripts/Models/CreationScript.cs
index 67e9bb0b2..e14c5e6d8 100644
--- a/QS.Updater.Core/DBScripts/Models/CreationScript.cs
+++ b/QS.Updater.Core/DBScripts/Models/CreationScript.cs
@@ -52,4 +52,4 @@ public string GetSqlScript()
"Для получения скрипта sql должен быть указано либо имя файла либо название ресурса");
}
}
-}
\ No newline at end of file
+}
diff --git a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
index f9d5fb545..e15822e62 100644
--- a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
+++ b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
@@ -1,10 +1,11 @@
+using MySqlConnector;
+using QS.DBScripts.Controllers;
+using QS.Dialog;
using System;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
-using MySqlConnector;
-using QS.DBScripts.Controllers;
-using QS.Dialog;
+using System.Xml.Linq;
namespace QS.DBScripts.Models
{
@@ -13,7 +14,7 @@ public class MySqlDbCreateModel : IDBCreator
static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly string connectionString;
- private readonly IDbScriptsConfiguration scripts;
+ private readonly CreationScript scripts;
private readonly IProgressBarDisplayable progress;
private readonly IDbCreatorInteraction interaction;
private readonly CancellationToken cancellationToken;
@@ -30,7 +31,7 @@ public MySqlDbCreateModel(
if(string.IsNullOrWhiteSpace(connectionString))
throw new ArgumentException("Connection string is required", nameof(connectionString));
this.connectionString = connectionString;
- this.scripts = scripts ?? throw new ArgumentNullException(nameof(scripts));
+ this.scripts = scripts.MakeCreationScript() ?? throw new ArgumentNullException(nameof(scripts));
this.progress = progress ?? throw new ArgumentNullException(nameof(progress));
this.interaction = interaction ?? throw new ArgumentNullException(nameof(interaction));
this.cancellationToken = cancellationToken;
@@ -47,50 +48,46 @@ public bool RunCreation(string dbName, string dbTitle) {
try {
logger.Info("Connecting to MySQL...");
connectionDB.Open();
- cancellationToken.ThrowIfCancellationRequested();
logger.Info("Проверяем существует ли уже база.");
- var cmd = new MySqlCommand("SHOW DATABASES;", connectionDB);
+
+ var sql = "SHOW DATABASES;";
+ var cmd = new MySqlCommand(sql, connectionDB);
bool needDropBase = false;
- using (var rdr = cmd.ExecuteReader())
- {
- while (rdr.Read())
- {
- if (rdr[0].ToString() == dbName)
- {
- if (interaction.AskDropExistingDatabaseAsync(dbName).GetAwaiter().GetResult())
- {
+ using(var rdr = cmd.ExecuteReader()) {
+ while(rdr.Read()) {
+ if(rdr[0].ToString() == dbName) {
+ if(interaction.AskDropExistingDatabaseAsync(dbName).GetAwaiter().GetResult()) {
needDropBase = true;
break;
}
- return false;
+ else
+ return false;
}
}
}
- cancellationToken.ThrowIfCancellationRequested();
logger.Info("Создаем новую базу.");
+
progress.Start(text: "Получаем скрипт создания базы");
- string sqlScript = scripts.GetCreationSqlScript();
+ string sqlScript = scripts.GetSqlScript();
int predictedCount = Regex.Matches(sqlScript, ";").Count;
logger.Debug("Предполагаем наличие {0} команд в скрипте.", predictedCount);
progress.Start(maxValue: predictedCount + (needDropBase ? 2 : 1));
- if (needDropBase)
- {
+ if(needDropBase) {
logger.Info("Удаляем существующую базу {0}.", dbName);
progress.Add(text: $"Удаляем существующую базу {dbName}");
- cmd.CommandText = $"DROP DATABASE `{dbName}`";
+ cmd.CommandText = String.Format("DROP DATABASE `{0}`", dbName);
cmd.ExecuteNonQuery();
}
- cancellationToken.ThrowIfCancellationRequested();
progress.Add(text: $"Создаем базу {dbName}");
- cmd.CommandText = $"CREATE SCHEMA `{dbName}` DEFAULT CHARACTER SET utf8mb4 ;";
+ cmd.CommandText = String.Format("CREATE SCHEMA `{0}` DEFAULT CHARACTER SET utf8mb4 ;", dbName);
cmd.ExecuteNonQuery();
- cmd.CommandText = $"USE `{dbName}` ;";
+ cmd.CommandText = String.Format("USE `{0}` ;", dbName);
cmd.ExecuteNonQuery();
progress.Add(text: $"Создаем таблицы в {dbName}");
@@ -99,27 +96,6 @@ public bool RunCreation(string dbName, string dbTitle) {
myscript.StatementExecuted += Myscript_StatementExecuted;
var commands = myscript.Execute();
logger.Debug("Выполнено {0} SQL-команд.", commands);
- cancellationToken.ThrowIfCancellationRequested();
-
- // Записываем человекочитаемое название базы, используется для отображения в списке БД
- logger.Info("Записываем Title='{0}' в base_parameters.", dbTitle);
- cmd.CommandText =
- "INSERT INTO base_parameters (name, str_value) VALUES ('Title', @title) "
- + "ON DUPLICATE KEY UPDATE str_value = @title";
- cmd.Parameters.Clear();
- cmd.Parameters.AddWithValue("@title", dbTitle ?? string.Empty);
- cmd.ExecuteNonQuery();
-
- // Версия пустой базы для апдейтера.
- if(scripts.CreationVersion != null) {
- logger.Info("Записываем version='{0}' в base_parameters.", scripts.CreationVersion);
- cmd.CommandText =
- "INSERT INTO base_parameters (name, str_value) VALUES ('version', @ver) "
- + "ON DUPLICATE KEY UPDATE str_value = @ver";
- cmd.Parameters.Clear();
- cmd.Parameters.AddWithValue("@ver", scripts.CreationVersion.ToString());
- cmd.ExecuteNonQuery();
- }
if(FillBaseGuid) {
logger.Info("Генерируем BaseGuid");
@@ -130,6 +106,7 @@ public bool RunCreation(string dbName, string dbTitle) {
cmd.ExecuteNonQuery();
logger.Info("BaseGuid успешно записан.");
}
+
}
catch(OperationCanceledException) {
logger.Info("Создание базы отменено пользователем.");
From 0e8244296785c503051800b35a3b65aa5b5eb051 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Sat, 2 May 2026 13:43:48 +0300
Subject: [PATCH 005/135] =?UTF-8?q?=D0=BF=D0=BE=D0=BC=D0=B5=D0=BD=D1=8F?=
=?UTF-8?q?=D0=BB=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D1=8F?=
=?UTF-8?q?=20=D0=BA=D1=80=D0=B5=D0=B9=D1=82=D0=BE=D1=80=D0=B0=20=D0=B1?=
=?UTF-8?q?=D0=B0=D0=B7=D1=8B=20=D0=B2=20=D1=82=D0=B8=D0=BF=D0=B5=20=D0=BF?=
=?UTF-8?q?=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D1=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../DataBase/QsCloudConnectionTypeBase.cs | 12 ++++++++++--
QS.DbManagement/ConnectionTypeBase.cs | 19 ++++++-------------
.../MariaDb/MariaDbConnectionTypeBase.cs | 17 ++++++++++++++++-
.../Dialog/IProgressBarDisplayable.cs | 5 +----
4 files changed, 33 insertions(+), 20 deletions(-)
diff --git a/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs b/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
index 31a13de3e..de2031830 100644
--- a/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
+++ b/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
@@ -3,16 +3,17 @@
using System.Linq;
using System.Reflection;
using QS.Utilities.Extensions;
+using QS.DBScripts.Controllers;
namespace QS.Cloud.Client.DataBase {
public class QsCloudConnectionTypeBase : ConnectionTypeBase {
public QsCloudConnectionTypeBase() {
Title = "QS: Облако";
ConnectionTypeName = "QSCloud";
-
+
Parameters.Add(new ConnectionParameter("Account","Организация"));
Parameters.Add(new ConnectionParameter("Login","Пользователь"));
-
+
IconBytes = Assembly.GetExecutingAssembly().GetResourceByteArray("QS.Cloud.Client.Icons.qscloud.ico");
}
@@ -23,5 +24,12 @@ public override bool CanConnect(IEnumerable parameters
public override IDbProvider CreateProvider(IList parameters, string password = null)
=> new QSCloudProvider(parameters, password);
+
+ public override IDBCreator CreatorFactory(CreatorFactoryArgs args)
+ => new QsCloudDbCreator(
+ args.Provider,
+ args.Progress,
+ args.Interaction,
+ args.CancellationToken);
}
}
diff --git a/QS.DbManagement/ConnectionTypeBase.cs b/QS.DbManagement/ConnectionTypeBase.cs
index e8b3f5905..995c0d9af 100644
--- a/QS.DbManagement/ConnectionTypeBase.cs
+++ b/QS.DbManagement/ConnectionTypeBase.cs
@@ -17,24 +17,17 @@ public abstract class ConnectionTypeBase {
public abstract IDbProvider CreateProvider(IList parameters, string password = null);
- ///
- /// Заполняется композиционным корнем приложения
- /// который один знает обо всех конкретных реализациях creator-ов и
- /// о том, как из IDbProvider достать строку подключения
- ///
- /// interaction — канал диалогов с пользователем
- /// serviceProvider — для резолва дополнительных зависимостей
- ///
- public Func CreatorFactory { get; set; }
+ public abstract IDBCreator CreatorFactory(CreatorFactoryArgs args);
public IDBCreator CreateCreator(CreatorFactoryArgs args) {
- if(CreatorFactory == null)
- throw new InvalidOperationException(
- $"Для типа подключения '{ConnectionTypeName}' не задана CreatorFactory. "
- + "Зарегистрируйте её в композиционном корне приложения.");
return CreatorFactory(args);
}
}
+
+ ///
+ /// interaction — канал диалогов с пользователем
+ /// serviceProvider — для резолва дополнительных зависимостей
+ ///
public class CreatorFactoryArgs {
public IDbProvider Provider { get; set; }
public IProgressBarDisplayable Progress { get; set; }
diff --git a/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs b/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs
index 960a29b4e..771eac670 100644
--- a/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs
+++ b/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs
@@ -1,7 +1,11 @@
+using Microsoft.Extensions.DependencyInjection;
+using QS.DBScripts;
+using QS.DBScripts.Controllers;
+using QS.DBScripts.Models;
+using QS.Utilities.Extensions;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
-using QS.Utilities.Extensions;
namespace QS.DbManagement
{
@@ -20,6 +24,17 @@ public override bool CanConnect(IEnumerable parameters
parameters.Any(p => p.Name == "Login" && !string.IsNullOrEmpty(p.Value));
}
+ public override IDBCreator CreatorFactory(CreatorFactoryArgs args){
+ var provider = (MariaDBProvider)args.Provider;
+ var scripts = args.ServiceProvider.GetRequiredService();
+ return new MySqlDbCreateModel(
+ provider.ConnectionStringBuilder.ConnectionString,
+ scripts,
+ args.Progress,
+ args.Interaction,
+ args.CancellationToken);
+ }
+
public override IDbProvider CreateProvider(IList parameters, string password = null)
=> new MariaDBProvider(parameters, password);
}
diff --git a/QS.Project.Core/Dialog/IProgressBarDisplayable.cs b/QS.Project.Core/Dialog/IProgressBarDisplayable.cs
index 43bab345e..8ed6f1770 100644
--- a/QS.Project.Core/Dialog/IProgressBarDisplayable.cs
+++ b/QS.Project.Core/Dialog/IProgressBarDisplayable.cs
@@ -1,9 +1,6 @@
-using System;
+using System;
namespace QS.Dialog
{
- ///
- /// Интерфейс позволяющий управлять прогресс баром не зависимо от графического тул кита.
- ///
public interface IProgressBarDisplayable
{
void Start(double maxValue = 1, double minValue = 0, string text = null, double startValue = 0);
From ab2ff0dfab3482aa917af2e8b6d73735a1bf029e Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Sat, 2 May 2026 15:55:30 +0300
Subject: [PATCH 006/135] =?UTF-8?q?=D0=BF=D0=BE=D1=87=D0=B8=D0=BD=D0=B8?=
=?UTF-8?q?=D0=BB=20=D0=BF=D1=80=D0=BE=D0=B1=D0=BB=D0=B5=D0=BC=D1=83=20?=
=?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B0=D1=87=D0=B8=20=D0=B7=D0=B0?=
=?UTF-8?q?=D0=B2=D0=B8=D1=81=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B5=D0=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
QS.DbManagement/QS.DbManagement.csproj | 1 +
QS.Launcher.Avalonia/DependencyInjection.cs | 3 +++
.../CreateDataBaseProgressView.axaml.cs | 4 ++-
.../CreateDataBaseSettingsView.axaml.cs | 5 +++-
QS.Launcher/DependencyInjection.cs | 7 +++--
.../DataBase/CreateDataBaseProgressVM.cs | 26 ++++++++++---------
.../DataBase/CreateDataBaseSettingsVM.cs | 17 +++++++-----
.../PageViewModels/DataBase/DataBasesVM.cs | 4 +--
8 files changed, 41 insertions(+), 26 deletions(-)
diff --git a/QS.DbManagement/QS.DbManagement.csproj b/QS.DbManagement/QS.DbManagement.csproj
index 178ef368e..f6c7779e5 100644
--- a/QS.DbManagement/QS.DbManagement.csproj
+++ b/QS.DbManagement/QS.DbManagement.csproj
@@ -27,6 +27,7 @@
+
diff --git a/QS.Launcher.Avalonia/DependencyInjection.cs b/QS.Launcher.Avalonia/DependencyInjection.cs
index b27725ca7..068c14555 100644
--- a/QS.Launcher.Avalonia/DependencyInjection.cs
+++ b/QS.Launcher.Avalonia/DependencyInjection.cs
@@ -3,6 +3,7 @@
using QS.Launcher.Services;
using QS.Launcher.Views;
using QS.Launcher.Views.Pages;
+using QS.Launcher.Views.Pages.DataBase;
namespace QS.Launcher;
public static partial class DependencyInjection {
@@ -13,6 +14,8 @@ public static IServiceCollection AddPages(this IServiceCollection services) {
.AddSingleton()
.AddSingleton()
.AddSingleton()
+ .AddTransient()
+ .AddTransient()
.AddSingleton();
}
}
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml.cs
index ec4b43f39..e2ffde338 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml.cs
@@ -6,8 +6,10 @@
namespace QS.Launcher.Views.Pages.DataBase;
public partial class CreateDataBaseProgressView : UserControl {
- public CreateDataBaseProgressView() {
+ public CreateDataBaseProgressView(CreateDataBaseProgressVM progressVM) {
InitializeComponent();
+
+ DataContext = progressVM;
}
private void OnLoaded(object? sender, RoutedEventArgs e) {
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml.cs
index 35dab5b31..fc36e41c4 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml.cs
@@ -1,9 +1,12 @@
using Avalonia.Controls;
+using QS.Launcher.ViewModels.PageViewModels.DataBase;
namespace QS.Launcher.Views.Pages.DataBase;
public partial class CreateDataBaseSettingsView : UserControl {
- public CreateDataBaseSettingsView() {
+ public CreateDataBaseSettingsView(CreateDataBaseSettingsVM settingsVM) {
InitializeComponent();
+
+ DataContext = settingsVM;
}
}
diff --git a/QS.Launcher/DependencyInjection.cs b/QS.Launcher/DependencyInjection.cs
index 8dcb66f2f..af7b67634 100644
--- a/QS.Launcher/DependencyInjection.cs
+++ b/QS.Launcher/DependencyInjection.cs
@@ -17,10 +17,9 @@ public static IServiceCollection AddLauncherViewModels(this IServiceCollection s
.AddSingleton()
.AddSingleton()
.AddSingleton()
- // Wizard-страницы создания БД — Transient: новый экземпляр на каждое открытие.
- .AddTransient()
- .AddTransient()
- // Сервисы лаунчера, нужные wizard-страницам.
+ // Wizard-страницы создания БД
+ .AddSingleton()
+ .AddSingleton()
.AddSingleton();
}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
index 77b077e07..697b7449b 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
@@ -16,10 +16,10 @@ namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
public class CreateDataBaseProgressVM : CarouselPageVM, IProgressBarDisplayable {
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
- public IDbProvider Provider { get; }
- public Connection Connection { get; }
- public string DbName { get; }
- public string DbTitle { get; }
+ public IDbProvider Provider { get; private set; }
+ public Connection Connection { get; private set; }
+ public string DbName { get; private set; }
+ public string DbTitle { get; private set; }
private readonly IDbCreatorInteraction interaction;
private readonly IServiceProvider services;
@@ -67,18 +67,10 @@ public bool IsStarted {
public ReactiveCommand CancelCommand { get; }
public CreateDataBaseProgressVM(
- IDbProvider provider,
- Connection connection,
- string dbName,
- string dbTitle,
IDbCreatorInteraction interaction,
IUiThreadInvoker uiThread,
IServiceProvider services)
{
- Provider = provider ?? throw new ArgumentNullException(nameof(provider));
- Connection = connection ?? throw new ArgumentNullException(nameof(connection));
- DbName = dbName ?? throw new ArgumentNullException(nameof(dbName));
- DbTitle = dbTitle ?? throw new ArgumentNullException(nameof(dbTitle));
this.interaction = interaction ?? throw new ArgumentNullException(nameof(interaction));
this.uiThread = uiThread ?? throw new ArgumentNullException(nameof(uiThread));
this.services = services ?? throw new ArgumentNullException(nameof(services));
@@ -91,6 +83,16 @@ public CreateDataBaseProgressVM(
});
}
+ public void SetDbSettings(
+ string dbName,
+ string dbTitle,
+ IDbProvider provider, Connection connection) {
+ DbName = dbName ?? throw new ArgumentNullException(nameof(dbName));
+ DbTitle = dbTitle ?? throw new ArgumentNullException(nameof(dbTitle));
+ Provider = provider ?? throw new ArgumentNullException(nameof(provider));
+ Connection = connection ?? throw new ArgumentNullException(nameof(connection));
+ }
+
public async Task StartCreationAsync() {
try {
var args = new CreatorFactoryArgs {
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
index ea1d5811a..efdd40e67 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
@@ -9,8 +9,8 @@ namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
/// По «Далее» создаёт CreateDataBaseProgressVM и пушит её в Carousel поверх текущей
///
public class CreateDataBaseSettingsVM : CarouselPageVM {
- public IDbProvider Provider { get; }
- public Connection Connection { get; }
+ public IDbProvider Provider { get; private set; }
+ public Connection Connection { get; private set; }
private readonly IServiceProvider services;
private string dbTitle;
@@ -34,9 +34,7 @@ public string DbName {
///
public event Action ProgressPageRequested;
- public CreateDataBaseSettingsVM(IDbProvider provider, Connection connection, IServiceProvider services) {
- Provider = provider ?? throw new ArgumentNullException(nameof(provider));
- Connection = connection ?? throw new ArgumentNullException(nameof(connection));
+ public CreateDataBaseSettingsVM(IServiceProvider services) {
this.services = services ?? throw new ArgumentNullException(nameof(services));
var canCreate = this.WhenAnyValue(x => x.DbName, x => x.DbTitle,
@@ -46,11 +44,18 @@ public CreateDataBaseSettingsVM(IDbProvider provider, Connection connection, ISe
CancelCommand = ReactiveCommand.Create(() => PopPageCommand?.Execute(null));
}
+ public void SetDbSettings(IDbProvider provider, Connection connection) {
+ Provider = provider ?? throw new ArgumentNullException(nameof(provider));
+ Connection = connection ?? throw new ArgumentNullException(nameof(connection));
+ }
+
private void GoToProgress() {
// Резолв через ActivatorUtilities — DI подставляет IDbCreatorInteraction/IUiThreadInvoker,
// а провайдер/соединение/имена приходят как runtime-аргументы.
var progress = Microsoft.Extensions.DependencyInjection.ActivatorUtilities
- .CreateInstance(services, Provider, Connection, DbName, DbTitle);
+ .GetServiceOrCreateInstance(services);
+
+ progress.SetDbSettings(dbName, dbTitle, Provider, Connection);
ProgressPageRequested?.Invoke(progress);
PushPageCommand?.Execute(progress);
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
index 1b7198ae1..d14c43eee 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
@@ -97,8 +97,8 @@ private void OpenCreateDatabase() {
if(provider == null || currentConnection == null)
return;
- var settings = ActivatorUtilities.CreateInstance(
- serviceProvider, provider, currentConnection);
+ var settings = ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider);
+ settings.SetDbSettings(Provider, CurrentConnection);
settings.ProgressPageRequested += progressVm => {
progressVm.DatabaseCreated += OnDatabaseCreatedFromWizard;
From 80ebe2c01fa15d40bdf220c8e79281f53b0a7fb3 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Sat, 2 May 2026 15:56:11 +0300
Subject: [PATCH 007/135] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?=
=?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20title=20=D0=BF=D1=80=D0=B8=20=D1=81?=
=?UTF-8?q?=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D0=B8=20=D0=B1=D0=B0=D0=B7?=
=?UTF-8?q?=D1=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../DataBase/QsCloudScriptsConfiguration.cs | 4 ++--
QS.Cloud.Client/Scripts/new_empty.sql | 7 +++++++
.../DBScripts/Models/MySqlDbCreateModel.cs | 12 +++++++++++-
3 files changed, 20 insertions(+), 3 deletions(-)
diff --git a/QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs b/QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs
index 091d50455..488a1f546 100644
--- a/QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs
+++ b/QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs
@@ -12,7 +12,7 @@ public CreationScript MakeCreationScript() {
return new CreationScript(
Assembly.GetAssembly(typeof(QsCloudScriptsConfiguration)),
"QS.Cloud.Client.Scripts.new_empty.sql",
- new Version(1, 0, 1)
+ new Version(1, 7)
);
}
@@ -22,7 +22,7 @@ public UpdateConfiguration MakeUpdateConfiguration() {
configuration.AddUpdate(
new Version(1, 0),
new Version(1, 0, 1),
- "QS.Cloud.Client.Scripts.1.0.1.sql");
+ "QS.Cloud.Client.Scripts.1.7.sql");
return configuration;
}
diff --git a/QS.Cloud.Client/Scripts/new_empty.sql b/QS.Cloud.Client/Scripts/new_empty.sql
index 0b30d71aa..b25952e11 100644
--- a/QS.Cloud.Client/Scripts/new_empty.sql
+++ b/QS.Cloud.Client/Scripts/new_empty.sql
@@ -126,6 +126,13 @@ CREATE TABLE `base_parameters` (
`str_value` varchar(100) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+
+INSERT INTO `base_parameters` (`name`, `str_value`) VALUES
+('ProductCode', '5'),
+('product_name', 'ClientManager'),
+('version', '1.7');
+
-- --------------------------------------------------------
--
diff --git a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
index e15822e62..dac612eb5 100644
--- a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
+++ b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
@@ -43,7 +43,7 @@ public Task RunCreationAsync(string dbName, string dbTitle) {
return Task.Run(() => RunCreation(dbName, dbTitle), cancellationToken);
}
- public bool RunCreation(string dbName, string dbTitle) {
+ public bool RunCreation(string dbName, string dbTitle = null) {
using(var connectionDB = new MySqlConnection(connectionString)) {
try {
logger.Info("Connecting to MySQL...");
@@ -107,6 +107,16 @@ public bool RunCreation(string dbName, string dbTitle) {
logger.Info("BaseGuid успешно записан.");
}
+ if(dbTitle != null) {
+ logger.Info("Генерируем BaseTitle");
+ cmd.CommandText =
+ "INSERT INTO base_parameters (name, str_value) VALUES ('BaseTitle', @title)";
+ cmd.Parameters.Clear();
+ cmd.Parameters.AddWithValue("@title", dbTitle);
+ cmd.ExecuteNonQuery();
+ logger.Info("BaseTitle успешно записан.");
+ }
+
}
catch(OperationCanceledException) {
logger.Info("Создание базы отменено пользователем.");
From a771542256311cf601eeed9f2d0320a780985cfa Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Tue, 5 May 2026 13:01:53 +0300
Subject: [PATCH 008/135] =?UTF-8?q?=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?=
=?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20IDbCreatorModel=20=D0=B4=D0=BB=D1=8F?=
=?UTF-8?q?=20QSCloud=20=D0=B8=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Делает то же, что MySqlDbCreateModel
для MariaDB, но физически выполняется на стороне облака:
первый gRPC-вызов CreateDataBase создаёт пустую схему;
второй FillDataBase прогоняет SQL-скрипт и шлёт
прогресс назад. Каждое сообщение стрима транслируется в
IProgressBarDisplayable, поэтому wizard-страница лаунчера
выглядит идентично сценарию с MariaDB.
разделил интерфейс создания базы, для гтк, и ее наполнения моделью
---
.../Clients/DataBaseManagementCloudClient.cs | 13 +-
QS.Cloud.Client/DataBase/QSCloudProvider.cs | 7 ++
.../DataBase/QsCloudConnectionTypeBase.cs | 2 +-
QS.Cloud.Client/DataBase/QsCloudDbCreator.cs | 111 ++++++++++++++++++
.../DataBase/QsCloudScriptsConfiguration.cs | 10 +-
.../Protos/DataBaseManagement.proto | 23 ++++
QS.DbManagement/ConnectionTypeBase.cs | 4 +-
.../MariaDb/MariaDbConnectionTypeBase.cs | 2 +-
.../DataBase/CreateDataBaseSettingsView.axaml | 8 +-
.../DataBase/CreateDataBaseProgressVM.cs | 2 +-
.../DataBase/CreateDataBaseSettingsVM.cs | 2 -
QS.LibsTest.Core/Launcher/ConfiguratorTest.cs | 4 +
.../DBScripts/Controllers/IDBCreator.cs | 7 +-
.../DBScripts/Controllers/IDbCreatorModel.cs | 12 ++
.../DBScripts/IDbScriptsConfiguration.cs | 2 +
.../DBScripts/Models/MySqlDbCreateModel.cs | 2 +-
16 files changed, 191 insertions(+), 20 deletions(-)
create mode 100644 QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
create mode 100644 QS.Project.Core/DBScripts/Controllers/IDbCreatorModel.cs
diff --git a/QS.Cloud.Client/Clients/DataBaseManagementCloudClient.cs b/QS.Cloud.Client/Clients/DataBaseManagementCloudClient.cs
index 27e07a9d9..841025fd1 100644
--- a/QS.Cloud.Client/Clients/DataBaseManagementCloudClient.cs
+++ b/QS.Cloud.Client/Clients/DataBaseManagementCloudClient.cs
@@ -1,4 +1,7 @@
+using System.Threading;
+using Grpc.Core;
using QS.Cloud.Core;
+
namespace QS.Cloud.Client.Clients {
public class DataBaseManagementCloudClient : CloudClientByBasicAuth {
public DataBaseManagementCloudClient(IBasicAuthInfoProvider basicAuthInfoProvider)
@@ -7,7 +10,15 @@ public DataBaseManagementCloudClient(IBasicAuthInfoProvider basicAuthInfoProvide
public CreateDataBaseResponse CreateDataBase(string dbName, string dbTitle) {
var client = new DataBaseManagement.DataBaseManagementClient(Channel);
var request = new CreateDataBaseRequest { Name = dbName, Title = dbTitle };
- return client.CreateDataBase(request, headers); ;
+ return client.CreateDataBase(request, headers);
+ }
+
+ public AsyncServerStreamingCall FillDataBase(
+ string dbName, string dbTitle, CancellationToken cancellationToken = default)
+ {
+ var client = new DataBaseManagement.DataBaseManagementClient(Channel);
+ var request = new FillDataBaseRequest { Name = dbName, Title = dbTitle };
+ return client.FillDataBase(request, headers, cancellationToken: cancellationToken);
}
}
}
diff --git a/QS.Cloud.Client/DataBase/QSCloudProvider.cs b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
index 5395936aa..edb515109 100644
--- a/QS.Cloud.Client/DataBase/QSCloudProvider.cs
+++ b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
@@ -8,6 +8,7 @@
using System.Linq;
using System.Reflection;
using System;
+using System.Threading;
using QS.Cloud.Client.Clients;
namespace QS.Cloud.Client.DataBase
@@ -58,6 +59,12 @@ public bool CreateDatabase(string databaseName, string title)
{
return dbClient.CreateDataBase(databaseName, title).Succsess;
}
+
+ public AsyncServerStreamingCall FillDataBase(
+ string databaseName, string title, CancellationToken cancellationToken = default)
+ {
+ return dbClient.FillDataBase(databaseName, title, cancellationToken);
+ }
public void Dispose()
{
diff --git a/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs b/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
index de2031830..a823a8c5d 100644
--- a/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
+++ b/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
@@ -25,7 +25,7 @@ public override bool CanConnect(IEnumerable parameters
public override IDbProvider CreateProvider(IList parameters, string password = null)
=> new QSCloudProvider(parameters, password);
- public override IDBCreator CreatorFactory(CreatorFactoryArgs args)
+ public override IDbCreatorModel CreatorFactory(CreatorFactoryArgs args)
=> new QsCloudDbCreator(
args.Provider,
args.Progress,
diff --git a/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs b/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
new file mode 100644
index 000000000..87f85863e
--- /dev/null
+++ b/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
@@ -0,0 +1,111 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using QS.Cloud.Core;
+using QS.DbManagement;
+using QS.DBScripts.Controllers;
+using QS.Dialog;
+
+namespace QS.Cloud.Client.DataBase
+{
+ public class QsCloudDbCreator : IDbCreatorModel
+ {
+ static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
+
+ private readonly QSCloudProvider provider;
+ private readonly IProgressBarDisplayable progress;
+ private readonly IDbCreatorInteraction interaction;
+ private readonly CancellationToken cancellationToken;
+
+ public QsCloudDbCreator(
+ IDbProvider provider,
+ IProgressBarDisplayable progress,
+ IDbCreatorInteraction interaction,
+ CancellationToken cancellationToken)
+ {
+ this.provider = (provider ?? throw new ArgumentNullException(nameof(provider))) as QSCloudProvider
+ ?? throw new ArgumentException("Ожидается QSCloudProvider", nameof(provider));
+ this.progress = progress ?? throw new ArgumentNullException(nameof(progress));
+ this.interaction = interaction ?? throw new ArgumentNullException(nameof(interaction));
+ this.cancellationToken = cancellationToken;
+ }
+
+ public async Task RunCreationAsync(string dbName, string dbTitle) {
+ try {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // это унарный вызов, прогресс индикативный
+ progress.Start(maxValue: 1, text: $"Создаём базу {dbTitle} в облаке");
+ bool created = await Task.Run(
+ () => provider.CreateDatabase(dbName, dbTitle),
+ cancellationToken);
+
+ if(!created) {
+ await interaction.ReportErrorAsync(
+ "Облако сообщило, что создание базы не удалось.", null);
+ return false;
+ }
+ progress.Add(text: "База создана, начинаем наполнение");
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Сервер напоняет базу и шлёт прогресс
+ bool finalSuccess = false;
+ string finalError = null;
+ using(var call = provider.FillDataBase(dbName, dbTitle, cancellationToken)) {
+ while(await call.ResponseStream.MoveNext(cancellationToken)) {
+ var msg = call.ResponseStream.Current;
+ ApplyToProgress(msg);
+
+ if(msg.Stage == FillDataBaseProgress.Types.Stage.Finished) {
+ finalSuccess = msg.Success;
+ finalError = msg.ErrorText;
+ break;
+ }
+ }
+ }
+
+ if(!finalSuccess) {
+ await interaction.ReportErrorAsync(
+ string.IsNullOrEmpty(finalError) ? "Облако не смогло наполнить базу." : finalError,
+ null);
+ return false;
+ }
+ return true;
+ }
+ catch(OperationCanceledException) {
+ logger.Info("Создание базы в облаке отменено пользователем.");
+ return false;
+ }
+ catch(Exception ex) {
+ logger.Error(ex, "Ошибка при создании базы в облаке.");
+ await interaction.ReportErrorAsync(ex.Message, null);
+ throw;
+ }
+ finally {
+ if(progress.IsStarted)
+ progress.Close();
+ }
+ }
+
+ private void ApplyToProgress(FillDataBaseProgress msg) {
+ switch(msg.Stage) {
+ //перезапускает шкалу с новым max
+ case FillDataBaseProgress.Types.Stage.Started:
+ progress.Start(
+ maxValue: msg.Max <= 0 ? 1 : msg.Max,
+ text: string.IsNullOrEmpty(msg.Text) ? null : msg.Text,
+ startValue: msg.Current);
+ break;
+ case FillDataBaseProgress.Types.Stage.Progress:
+ progress.Update(msg.Current);
+ if(!string.IsNullOrEmpty(msg.Text))
+ progress.Update(msg.Text);
+ break;
+ case FillDataBaseProgress.Types.Stage.Finished:
+ if(!string.IsNullOrEmpty(msg.Text))
+ progress.Update(msg.Text);
+ break;
+ }
+ }
+ }
+}
diff --git a/QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs b/QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs
index 488a1f546..b6345d95a 100644
--- a/QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs
+++ b/QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs
@@ -3,15 +3,23 @@
using QS.Updater.DB;
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Reflection;
using System.Text;
namespace QS.Cloud.Client.DataBase {
public class QsCloudScriptsConfiguration : IDbScriptsConfiguration {
+ private string ResourceName = "QS.Cloud.Client.Scripts.new_empty.sql";
+ public bool HasCreationScript() {
+ return Assembly.GetAssembly(typeof(QsCloudScriptsConfiguration))
+ .GetReferencedAssemblies().Select(x => x.FullName)
+ .Contains(ResourceName);
+ }
+
public CreationScript MakeCreationScript() {
return new CreationScript(
Assembly.GetAssembly(typeof(QsCloudScriptsConfiguration)),
- "QS.Cloud.Client.Scripts.new_empty.sql",
+ ResourceName,
new Version(1, 7)
);
}
diff --git a/QS.Cloud.Client/Protos/DataBaseManagement.proto b/QS.Cloud.Client/Protos/DataBaseManagement.proto
index 6bd23648d..ef24ca6a3 100644
--- a/QS.Cloud.Client/Protos/DataBaseManagement.proto
+++ b/QS.Cloud.Client/Protos/DataBaseManagement.proto
@@ -3,7 +3,11 @@ syntax = "proto3";
package QS.Cloud.Core;
service DataBaseManagement{
+ // Создать пустую базу
rpc CreateDataBase (CreateDataBaseRequest) returns (CreateDataBaseResponse);
+
+ // Наполняет базу стримит, прогресс-сообщения клиенту по мере выполнения
+ rpc FillDataBase (FillDataBaseRequest) returns (stream FillDataBaseProgress);
}
message CreateDataBaseRequest{
@@ -14,3 +18,22 @@ message CreateDataBaseRequest{
message CreateDataBaseResponse{
bool succsess = 1;
}
+
+message FillDataBaseRequest{
+ string name = 1;
+ string title = 2;
+}
+
+message FillDataBaseProgress{
+ enum Stage {
+ STARTED = 0;
+ PROGRESS = 1;
+ FINISHED = 2;
+ }
+ Stage stage = 1;
+ double current = 2;
+ double max = 3;
+ string text = 4;
+ bool success = 5;
+ string error_text = 6;
+}
diff --git a/QS.DbManagement/ConnectionTypeBase.cs b/QS.DbManagement/ConnectionTypeBase.cs
index 995c0d9af..c3caca35c 100644
--- a/QS.DbManagement/ConnectionTypeBase.cs
+++ b/QS.DbManagement/ConnectionTypeBase.cs
@@ -17,9 +17,9 @@ public abstract class ConnectionTypeBase {
public abstract IDbProvider CreateProvider(IList parameters, string password = null);
- public abstract IDBCreator CreatorFactory(CreatorFactoryArgs args);
+ public abstract IDbCreatorModel CreatorFactory(CreatorFactoryArgs args);
- public IDBCreator CreateCreator(CreatorFactoryArgs args) {
+ public IDbCreatorModel CreateCreator(CreatorFactoryArgs args) {
return CreatorFactory(args);
}
}
diff --git a/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs b/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs
index 771eac670..9560280de 100644
--- a/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs
+++ b/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs
@@ -24,7 +24,7 @@ public override bool CanConnect(IEnumerable parameters
parameters.Any(p => p.Name == "Login" && !string.IsNullOrEmpty(p.Value));
}
- public override IDBCreator CreatorFactory(CreatorFactoryArgs args){
+ public override IDbCreatorModel CreatorFactory(CreatorFactoryArgs args){
var provider = (MariaDBProvider)args.Provider;
var scripts = args.ServiceProvider.GetRequiredService();
return new MySqlDbCreateModel(
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml
index 7092888cd..0bd1897bd 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml
@@ -11,11 +11,11 @@
-
-
+
+
-
-
+
+
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
index 697b7449b..213707c8b 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
@@ -102,7 +102,7 @@ public async Task StartCreationAsync() {
CancellationToken = cts.Token,
ServiceProvider = services
};
- IDBCreator creator = Connection.ConnectionType.CreateCreator(args);
+ IDbCreatorModel creator = Connection.ConnectionType.CreateCreator(args);
bool ok = await creator.RunCreationAsync(DbName, DbTitle);
if(ok)
DatabaseCreated?.Invoke();
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
index efdd40e67..d68ab0611 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
@@ -50,8 +50,6 @@ public void SetDbSettings(IDbProvider provider, Connection connection) {
}
private void GoToProgress() {
- // Резолв через ActivatorUtilities — DI подставляет IDbCreatorInteraction/IUiThreadInvoker,
- // а провайдер/соединение/имена приходят как runtime-аргументы.
var progress = Microsoft.Extensions.DependencyInjection.ActivatorUtilities
.GetServiceOrCreateInstance(services);
diff --git a/QS.LibsTest.Core/Launcher/ConfiguratorTest.cs b/QS.LibsTest.Core/Launcher/ConfiguratorTest.cs
index b68265cf1..48c3b3b8c 100644
--- a/QS.LibsTest.Core/Launcher/ConfiguratorTest.cs
+++ b/QS.LibsTest.Core/Launcher/ConfiguratorTest.cs
@@ -6,6 +6,7 @@
using NSubstitute;
using NUnit.Framework;
using QS.DbManagement;
+using QS.DBScripts.Controllers;
using QS.Dialog;
using QS.Launcher;
@@ -428,6 +429,9 @@ public TestConnectionType(string name) {
public override IDbProvider CreateProvider(IList parameters, string password = null)
=> Substitute.For();
+
+ public override IDbCreatorModel CreatorFactory(CreatorFactoryArgs args)
+ => Substitute.For();
}
}
}
diff --git a/QS.Project.Core/DBScripts/Controllers/IDBCreator.cs b/QS.Project.Core/DBScripts/Controllers/IDBCreator.cs
index 6ab27ea04..e124aff86 100644
--- a/QS.Project.Core/DBScripts/Controllers/IDBCreator.cs
+++ b/QS.Project.Core/DBScripts/Controllers/IDBCreator.cs
@@ -1,12 +1,7 @@
-using System.Threading.Tasks;
-
namespace QS.DBScripts.Controllers
{
- ///
- /// Универсальный контракт создания и наполнения базы данных.
- ///
public interface IDBCreator
{
- Task RunCreationAsync(string dbName, string dbTitle);
+ void RunCreation(string server, string dbname);
}
}
diff --git a/QS.Project.Core/DBScripts/Controllers/IDbCreatorModel.cs b/QS.Project.Core/DBScripts/Controllers/IDbCreatorModel.cs
new file mode 100644
index 000000000..625b9dd3c
--- /dev/null
+++ b/QS.Project.Core/DBScripts/Controllers/IDbCreatorModel.cs
@@ -0,0 +1,12 @@
+using System.Threading.Tasks;
+
+namespace QS.DBScripts.Controllers
+{
+ ///
+ /// Низкоуровневая модель создания БД: знает, как физически создать и наполнить базу
+ ///
+ public interface IDbCreatorModel
+ {
+ Task RunCreationAsync(string dbName, string dbTitle);
+ }
+}
diff --git a/QS.Updater.Core/DBScripts/IDbScriptsConfiguration.cs b/QS.Updater.Core/DBScripts/IDbScriptsConfiguration.cs
index a3bdba61a..fef1a1880 100644
--- a/QS.Updater.Core/DBScripts/IDbScriptsConfiguration.cs
+++ b/QS.Updater.Core/DBScripts/IDbScriptsConfiguration.cs
@@ -8,5 +8,7 @@ public interface IDbScriptsConfiguration
CreationScript MakeCreationScript();
UpdateConfiguration MakeUpdateConfiguration();
+
+ bool HasCreationScript();
}
}
diff --git a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
index dac612eb5..e5473e3d8 100644
--- a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
+++ b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
@@ -9,7 +9,7 @@
namespace QS.DBScripts.Models
{
- public class MySqlDbCreateModel : IDBCreator
+ public class MySqlDbCreateModel : IDbCreatorModel
{
static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
From 8229cb581ec85b2b4b8c9a24b30780e46c5b924a Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Wed, 6 May 2026 21:30:20 +0300
Subject: [PATCH 009/135] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD?=
=?UTF-8?q?=D0=B8=D0=B5=20=D0=B1=D0=B0=D0=B7=D1=8B=20=D1=87=D0=B5=D1=80?=
=?UTF-8?q?=D0=B5=D0=B7=20=D0=BE=D0=B1=D0=BB=D0=B0=D0=BA=D0=BE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Clients/DataBaseManagementCloudClient.cs | 8 --
QS.Cloud.Client/DataBase/QSCloudProvider.cs | 39 ++++---
.../DataBase/QsCloudConnectionTypeBase.cs | 25 +++--
QS.Cloud.Client/DataBase/QsCloudDbCreator.cs | 101 +++++++-----------
.../Protos/DataBaseManagement.proto | 26 +----
QS.DbManagement/MariaDb/MariaDBProvider.cs | 9 +-
.../MariaDb/MariaDbConnectionTypeBase.cs | 4 +-
QS.Launcher.Avalonia/DependencyInjection.cs | 2 -
.../DataBase/CreateDataBaseProgressView.axaml | 5 +-
QS.Launcher/ViewModels/MainWindowVM.cs | 2 +-
.../PageViewModels/DataBase/DataBasesVM.cs | 2 +-
.../DBScripts/Models/MySqlDbCreateModel.cs | 26 ++++-
12 files changed, 116 insertions(+), 133 deletions(-)
diff --git a/QS.Cloud.Client/Clients/DataBaseManagementCloudClient.cs b/QS.Cloud.Client/Clients/DataBaseManagementCloudClient.cs
index 841025fd1..9bd26b15b 100644
--- a/QS.Cloud.Client/Clients/DataBaseManagementCloudClient.cs
+++ b/QS.Cloud.Client/Clients/DataBaseManagementCloudClient.cs
@@ -12,13 +12,5 @@ public CreateDataBaseResponse CreateDataBase(string dbName, string dbTitle) {
var request = new CreateDataBaseRequest { Name = dbName, Title = dbTitle };
return client.CreateDataBase(request, headers);
}
-
- public AsyncServerStreamingCall FillDataBase(
- string dbName, string dbTitle, CancellationToken cancellationToken = default)
- {
- var client = new DataBaseManagement.DataBaseManagementClient(Channel);
- var request = new FillDataBaseRequest { Name = dbName, Title = dbTitle };
- return client.FillDataBase(request, headers, cancellationToken: cancellationToken);
- }
}
}
diff --git a/QS.Cloud.Client/DataBase/QSCloudProvider.cs b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
index edb515109..77e1d8e63 100644
--- a/QS.Cloud.Client/DataBase/QSCloudProvider.cs
+++ b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
@@ -10,11 +10,18 @@
using System;
using System.Threading;
using QS.Cloud.Client.Clients;
+using QS.DBScripts.Controllers;
+using System.Threading.Tasks;
namespace QS.Cloud.Client.DataBase
{
- public class QSCloudProvider : IDbProvider {
- public string ConnectionString { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+ public class QSCloudProvider : IDbProvider, IDbCreatorModel {
+
+ ///
+ /// Публичный - в типе родключения нужен доступ, реализацию он знает и так
+ ///
+ public int BaseId { get; private set; }
+ public BasicAuthInfoProvider AuthInfo { get; private set; }
public bool IsConnected => throw new NotImplementedException();
@@ -22,27 +29,23 @@ public class QSCloudProvider : IDbProvider {
#region Параметры подключени
public string Account { get; private set; }
-
#endregion
public string UserName { get; private set; }
public bool CanCreateDatabase => throw new NotImplementedException();
- private CloudFeaturesClient featuresClient;
private LoginManagementCloudClient loginClient;
- private SessionManagementCloudClient sessionClient;
private DataBaseManagementCloudClient dbClient;
- private UserManagementCloudClient userClient;
public QSCloudProvider(IList parameters, string password = null) {
Account = parameters.First(p => p.Name == "Account").Value;
UserName = parameters.First(p => p.Name == "Login").Value;
- BasicAuthInfoProvider authInfo = new BasicAuthInfoProvider($@"{Account}\{UserName}", password);
+ AuthInfo = new BasicAuthInfoProvider($@"{Account}\{UserName}", password);
- loginClient = new LoginManagementCloudClient(authInfo);
- dbClient = new DataBaseManagementCloudClient(authInfo);
+ loginClient = new LoginManagementCloudClient(AuthInfo);
+ dbClient = new DataBaseManagementCloudClient(AuthInfo);
}
public bool AddUser(string username, string password)
@@ -57,13 +60,9 @@ public bool ChangePassword(string username, string oldPassword, string newPasswo
public bool CreateDatabase(string databaseName, string title)
{
- return dbClient.CreateDataBase(databaseName, title).Succsess;
- }
-
- public AsyncServerStreamingCall FillDataBase(
- string databaseName, string title, CancellationToken cancellationToken = default)
- {
- return dbClient.FillDataBase(databaseName, title, cancellationToken);
+ CreateDataBaseResponse response = dbClient.CreateDataBase(databaseName, title);
+ BaseId = response.BaseId;
+ return true;
}
public void Dispose()
@@ -139,6 +138,14 @@ public LoginToServerResponse LoginToServer() {
return resp;
}
+
+ public Task RunCreationAsync(string dbName, string dbTitle) {
+ throw new NotImplementedException();
+ }
+
+ public void RunCreation(string server, string dbname) {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs b/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
index a823a8c5d..bf0553cb2 100644
--- a/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
+++ b/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
@@ -1,12 +1,16 @@
+using Microsoft.Extensions.DependencyInjection;
using QS.DbManagement;
+using QS.DBScripts;
+using QS.DBScripts.Controllers;
+using QS.DBScripts.Models;
+using QS.Utilities.Extensions;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
-using QS.Utilities.Extensions;
-using QS.DBScripts.Controllers;
namespace QS.Cloud.Client.DataBase {
public class QsCloudConnectionTypeBase : ConnectionTypeBase {
+
public QsCloudConnectionTypeBase() {
Title = "QS: Облако";
ConnectionTypeName = "QSCloud";
@@ -22,14 +26,21 @@ public override bool CanConnect(IEnumerable parameters
parameters.Any(p => p.Name == "Login" && !string.IsNullOrEmpty(p.Value));
}
- public override IDbProvider CreateProvider(IList parameters, string password = null)
- => new QSCloudProvider(parameters, password);
+ public override IDbProvider CreateProvider(IList parameters, string password = null) {
+ return new QSCloudProvider(parameters, password);
+ }
- public override IDbCreatorModel CreatorFactory(CreatorFactoryArgs args)
- => new QsCloudDbCreator(
- args.Provider,
+ public override IDbCreatorModel CreatorFactory(CreatorFactoryArgs args) {
+ var provider = (QSCloudProvider)args.Provider;
+ var scripts = args.ServiceProvider.GetRequiredService();
+ var creator = new QsCloudDbCreator(
+ provider.BaseId,
+ provider.AuthInfo,
+ scripts,
args.Progress,
args.Interaction,
args.CancellationToken);
+ return creator;
+ }
}
}
diff --git a/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs b/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
index 87f85863e..426e0b2e6 100644
--- a/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
+++ b/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
@@ -1,10 +1,11 @@
-using System;
-using System.Threading;
-using System.Threading.Tasks;
using QS.Cloud.Core;
-using QS.DbManagement;
+using QS.DBScripts;
using QS.DBScripts.Controllers;
+using QS.DBScripts.Models;
using QS.Dialog;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
namespace QS.Cloud.Client.DataBase
{
@@ -12,65 +13,60 @@ public class QsCloudDbCreator : IDbCreatorModel
{
static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
- private readonly QSCloudProvider provider;
+ private readonly int baseId;
+
private readonly IProgressBarDisplayable progress;
private readonly IDbCreatorInteraction interaction;
+ private readonly IDbScriptsConfiguration configuration;
private readonly CancellationToken cancellationToken;
+ private LoginManagementCloudClient loginClient;
+
public QsCloudDbCreator(
- IDbProvider provider,
+ int baseId,
+ BasicAuthInfoProvider AuthInfo,
+ IDbScriptsConfiguration configuration,
IProgressBarDisplayable progress,
IDbCreatorInteraction interaction,
CancellationToken cancellationToken)
{
- this.provider = (provider ?? throw new ArgumentNullException(nameof(provider))) as QSCloudProvider
- ?? throw new ArgumentException("Ожидается QSCloudProvider", nameof(provider));
+ this.baseId = baseId;
+
+ loginClient = new LoginManagementCloudClient(AuthInfo);
+
+ this.configuration = configuration ?? throw new ArgumentNullException(nameof(progress));
this.progress = progress ?? throw new ArgumentNullException(nameof(progress));
this.interaction = interaction ?? throw new ArgumentNullException(nameof(interaction));
this.cancellationToken = cancellationToken;
}
+
public async Task RunCreationAsync(string dbName, string dbTitle) {
try {
- cancellationToken.ThrowIfCancellationRequested();
-
- // это унарный вызов, прогресс индикативный
- progress.Start(maxValue: 1, text: $"Создаём базу {dbTitle} в облаке");
- bool created = await Task.Run(
- () => provider.CreateDatabase(dbName, dbTitle),
- cancellationToken);
+ StartSessionResponse session = loginClient.StartSession(baseId);
- if(!created) {
- await interaction.ReportErrorAsync(
- "Облако сообщило, что создание базы не удалось.", null);
- return false;
+ if(!session.Success) {
+ await interaction.ReportErrorAsync("Ошибка в создании сесии", "Запрос в облако");
+ throw new InvalidOperationException("Ошибка в создании сесии");
+ }
+ else if(!session.IsAdmin) {
+ await interaction.ReportErrorAsync("Вы не имеете прав Администратора", "Запрос в облако, по типу SELECT cloud_users.multi_ip as multiIp, base_access.admin AS isAdmin, base_access.read_only as readOnly \" +\n\t\t\t\t\t\t\t\"FROM cloud_users \" + \n\t\t\t\t\t\t\t\"LEFT JOIN base_access ON base_access.user_id = cloud_users.id \" +\n\t\t\t\t\t\t\t\"WHERE cloud_users.id = @id AND base_access.base_id = @dbid;");
}
- progress.Add(text: "База создана, начинаем наполнение");
- cancellationToken.ThrowIfCancellationRequested();
- // Сервер напоняет базу и шлёт прогресс
- bool finalSuccess = false;
- string finalError = null;
- using(var call = provider.FillDataBase(dbName, dbTitle, cancellationToken)) {
- while(await call.ResponseStream.MoveNext(cancellationToken)) {
- var msg = call.ResponseStream.Current;
- ApplyToProgress(msg);
+ var infoProvider = new SessionInfoProvider(sessionId: session.SessionId);
+ var sessionLife = new AliveCloudClient(infoProvider);
+ sessionLife.NewMessage += (mes) => {
+ progress.Update("Сессия: " + mes + " в статусе " + sessionLife.LastStatus.ToString());
+ };
+ sessionLife.KeepAlive();
- if(msg.Stage == FillDataBaseProgress.Types.Stage.Finished) {
- finalSuccess = msg.Success;
- finalError = msg.ErrorText;
- break;
- }
- }
- }
+ var creator = new MySqlDbCreateModel(session.Db.Server, session.Db.Port, session.Db.Login, session.Db.Password, configuration, progress, interaction, cancellationToken);
+ creator.FillBaseGuid = false;
+ bool success = await creator.RunCreationAsync(dbName, dbTitle);
- if(!finalSuccess) {
- await interaction.ReportErrorAsync(
- string.IsNullOrEmpty(finalError) ? "Облако не смогло наполнить базу." : finalError,
- null);
- return false;
- }
- return true;
+ sessionLife.Dispose();
+
+ return success;
}
catch(OperationCanceledException) {
logger.Info("Создание базы в облаке отменено пользователем.");
@@ -86,26 +82,5 @@ await interaction.ReportErrorAsync(
progress.Close();
}
}
-
- private void ApplyToProgress(FillDataBaseProgress msg) {
- switch(msg.Stage) {
- //перезапускает шкалу с новым max
- case FillDataBaseProgress.Types.Stage.Started:
- progress.Start(
- maxValue: msg.Max <= 0 ? 1 : msg.Max,
- text: string.IsNullOrEmpty(msg.Text) ? null : msg.Text,
- startValue: msg.Current);
- break;
- case FillDataBaseProgress.Types.Stage.Progress:
- progress.Update(msg.Current);
- if(!string.IsNullOrEmpty(msg.Text))
- progress.Update(msg.Text);
- break;
- case FillDataBaseProgress.Types.Stage.Finished:
- if(!string.IsNullOrEmpty(msg.Text))
- progress.Update(msg.Text);
- break;
- }
- }
}
}
diff --git a/QS.Cloud.Client/Protos/DataBaseManagement.proto b/QS.Cloud.Client/Protos/DataBaseManagement.proto
index ef24ca6a3..bb5c03f91 100644
--- a/QS.Cloud.Client/Protos/DataBaseManagement.proto
+++ b/QS.Cloud.Client/Protos/DataBaseManagement.proto
@@ -5,35 +5,15 @@ package QS.Cloud.Core;
service DataBaseManagement{
// Создать пустую базу
rpc CreateDataBase (CreateDataBaseRequest) returns (CreateDataBaseResponse);
-
- // Наполняет базу стримит, прогресс-сообщения клиенту по мере выполнения
- rpc FillDataBase (FillDataBaseRequest) returns (stream FillDataBaseProgress);
}
message CreateDataBaseRequest{
string name = 1;
string title = 2;
+ uint32 product_id = 3;
}
message CreateDataBaseResponse{
- bool succsess = 1;
-}
-
-message FillDataBaseRequest{
- string name = 1;
- string title = 2;
-}
-
-message FillDataBaseProgress{
- enum Stage {
- STARTED = 0;
- PROGRESS = 1;
- FINISHED = 2;
- }
- Stage stage = 1;
- double current = 2;
- double max = 3;
- string text = 4;
- bool success = 5;
- string error_text = 6;
+ int32 base_id = 1;
+ string base_guid = 2;
}
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index 7a031b382..948b225ff 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -1,4 +1,5 @@
using Dapper;
+using FluentNHibernate.Cfg.Db;
using MySqlConnector;
using QS.DbManagement.Responces;
using QS.Project.Versioning;
@@ -15,9 +16,9 @@ public class MariaDBProvider : IDbProvider {
private static readonly string[] SystemDatabases = { "information_schema", "mysql", "performance_schema", "sys" };
readonly MySqlConnection connection;
+
///
- /// Публичный, чтобы внешние компоненты могли получить подключение
- /// без повторного разбора параметров.
+ /// Публичный - в типе родключения нужен доступ, реализацию он знает и так
///
public MySqlConnectionStringBuilder ConnectionStringBuilder { get; }
@@ -25,10 +26,6 @@ public class MariaDBProvider : IDbProvider {
public bool IsAdmin { get; private set; }
- ///
- /// Есть ли у текущего пользователя право создавать базы данных.
- /// Определяется в момент из SHOW GRANTS.
- ///
public bool CanCreateDatabase { get; private set; }
///
diff --git a/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs b/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs
index 9560280de..29b4805a3 100644
--- a/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs
+++ b/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs
@@ -27,12 +27,14 @@ public override bool CanConnect(IEnumerable parameters
public override IDbCreatorModel CreatorFactory(CreatorFactoryArgs args){
var provider = (MariaDBProvider)args.Provider;
var scripts = args.ServiceProvider.GetRequiredService();
- return new MySqlDbCreateModel(
+ var creator = new MySqlDbCreateModel(
provider.ConnectionStringBuilder.ConnectionString,
scripts,
args.Progress,
args.Interaction,
args.CancellationToken);
+ creator.FillBaseGuid = false;
+ return creator;
}
public override IDbProvider CreateProvider(IList parameters, string password = null)
diff --git a/QS.Launcher.Avalonia/DependencyInjection.cs b/QS.Launcher.Avalonia/DependencyInjection.cs
index 068c14555..d5dce86ef 100644
--- a/QS.Launcher.Avalonia/DependencyInjection.cs
+++ b/QS.Launcher.Avalonia/DependencyInjection.cs
@@ -12,8 +12,6 @@ public static IServiceCollection AddPages(this IServiceCollection services) {
.AddSingleton()
.AddSingleton()
.AddSingleton()
- .AddSingleton()
- .AddSingleton()
.AddTransient()
.AddTransient()
.AddSingleton();
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml
index 9a468a3f1..ee33ff10a 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml
@@ -24,7 +24,7 @@
@@ -36,9 +36,6 @@
Maximum="{Binding MaxValue}"
Value="{Binding Value}" />
-
-
diff --git a/QS.Launcher/ViewModels/MainWindowVM.cs b/QS.Launcher/ViewModels/MainWindowVM.cs
index af5495e60..34d9d7c4b 100644
--- a/QS.Launcher/ViewModels/MainWindowVM.cs
+++ b/QS.Launcher/ViewModels/MainWindowVM.cs
@@ -34,7 +34,7 @@ public MainWindowVM(
IServiceProvider provider)
{
Pages = new ObservableCollection {
- loginVM, dataBasesVM, baseManagementVM, userManagementVM
+ loginVM, dataBasesVM
};
rootPagesCount = Pages.Count;
login = loginVM;
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
index d14c43eee..047307e6e 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
@@ -91,7 +91,7 @@ public DataBasesVM(
}
///
- /// создаёт CreateDataBaseSettingsVM возвращает фокус на DataBasesVM и обновляет список баз.
+ /// создаёт возвращает фокус на и обновляет список баз
///
private void OpenCreateDatabase() {
if(provider == null || currentConnection == null)
diff --git a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
index e5473e3d8..48d5397d7 100644
--- a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
+++ b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
@@ -1,6 +1,9 @@
+using FluentNHibernate.Cfg.Db;
using MySqlConnector;
using QS.DBScripts.Controllers;
using QS.Dialog;
+using QS.Project.DB;
+using QS.Project.Domain;
using System;
using System.Text.RegularExpressions;
using System.Threading;
@@ -37,7 +40,28 @@ public MySqlDbCreateModel(
this.cancellationToken = cancellationToken;
}
- public Task RunCreationAsync(string dbName, string dbTitle) {
+ public MySqlDbCreateModel(
+ string server, uint port, string login, string password,
+ IDbScriptsConfiguration scripts,
+ IProgressBarDisplayable progress,
+ IDbCreatorInteraction interaction,
+ CancellationToken cancellationToken) {
+
+ this.connectionString = new MySqlConnectionStringBuilder {
+ Server = server,
+ Port = port,
+ UserID = login,
+ Password = password,
+ AllowUserVariables = true
+ }.ConnectionString;
+ this.scripts = scripts.MakeCreationScript() ?? throw new ArgumentNullException(nameof(scripts));
+ this.progress = progress ?? throw new ArgumentNullException(nameof(progress));
+ this.interaction = interaction ?? throw new ArgumentNullException(nameof(interaction));
+ this.cancellationToken = cancellationToken;
+ }
+
+
+ public Task RunCreationAsync(string dbName, string dbTitle = null) {
// Тяжёлая часть с MySqlScript.Execute синхронная,
// поэтому уносим её на пул, чтобы не блокировать UI-поток
return Task.Run(() => RunCreation(dbName, dbTitle), cancellationToken);
From 3f9ae8e9ee77145846ca28e638fb3fd46a0e0441 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Wed, 6 May 2026 22:31:31 +0300
Subject: [PATCH 010/135] =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20=D0=BB?=
=?UTF-8?q?=D0=B8=D1=88=D0=BD=D0=B8=D0=B5=20=D1=8D=D0=BB=D0=B5=D0=BC=D0=B5?=
=?UTF-8?q?=D0=BD=D1=82=D1=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
QS.Cloud.Client/DataBase/QSCloudProvider.cs | 10 +---------
QS.Cloud.Client/DataBase/QsCloudDbCreator.cs | 2 +-
.../DataBase/QsCloudScriptsConfiguration.cs | 4 ++--
QS.Cloud.Client/QS.Cloud.Client.csproj | 1 -
.../Views/Pages/BaseManagementView.axaml.cs | 1 -
QS.Launcher/ViewModels/MainWindowVM.cs | 2 +-
.../ViewModels/PageViewModels/BaseManagementVM.cs | 1 +
.../ViewModels/PageViewModels/CarouselPageVM.cs | 4 ++--
.../DataBase/CreateDataBaseSettingsVM.cs | 3 ---
QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs | 4 ----
10 files changed, 8 insertions(+), 24 deletions(-)
diff --git a/QS.Cloud.Client/DataBase/QSCloudProvider.cs b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
index 77e1d8e63..9682fb105 100644
--- a/QS.Cloud.Client/DataBase/QSCloudProvider.cs
+++ b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
@@ -15,7 +15,7 @@
namespace QS.Cloud.Client.DataBase
{
- public class QSCloudProvider : IDbProvider, IDbCreatorModel {
+ public class QSCloudProvider : IDbProvider {
///
/// Публичный - в типе родключения нужен доступ, реализацию он знает и так
@@ -138,14 +138,6 @@ public LoginToServerResponse LoginToServer() {
return resp;
}
-
- public Task RunCreationAsync(string dbName, string dbTitle) {
- throw new NotImplementedException();
- }
-
- public void RunCreation(string server, string dbname) {
- throw new NotImplementedException();
- }
}
}
diff --git a/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs b/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
index 426e0b2e6..92cca32d3 100644
--- a/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
+++ b/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
@@ -50,7 +50,7 @@ public async Task RunCreationAsync(string dbName, string dbTitle) {
throw new InvalidOperationException("Ошибка в создании сесии");
}
else if(!session.IsAdmin) {
- await interaction.ReportErrorAsync("Вы не имеете прав Администратора", "Запрос в облако, по типу SELECT cloud_users.multi_ip as multiIp, base_access.admin AS isAdmin, base_access.read_only as readOnly \" +\n\t\t\t\t\t\t\t\"FROM cloud_users \" + \n\t\t\t\t\t\t\t\"LEFT JOIN base_access ON base_access.user_id = cloud_users.id \" +\n\t\t\t\t\t\t\t\"WHERE cloud_users.id = @id AND base_access.base_id = @dbid;");
+ await interaction.ReportErrorAsync("Вы не имеете прав Администратора", "Запрос в облако");
}
var infoProvider = new SessionInfoProvider(sessionId: session.SessionId);
diff --git a/QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs b/QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs
index b6345d95a..20c1d0ac2 100644
--- a/QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs
+++ b/QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs
@@ -28,8 +28,8 @@ public UpdateConfiguration MakeUpdateConfiguration() {
var configuration = new UpdateConfiguration();
configuration.AddUpdate(
- new Version(1, 0),
- new Version(1, 0, 1),
+ new Version(1, 7),
+ new Version(1, 7, 1),
"QS.Cloud.Client.Scripts.1.7.sql");
return configuration;
diff --git a/QS.Cloud.Client/QS.Cloud.Client.csproj b/QS.Cloud.Client/QS.Cloud.Client.csproj
index ebee0064c..2ee482c3f 100644
--- a/QS.Cloud.Client/QS.Cloud.Client.csproj
+++ b/QS.Cloud.Client/QS.Cloud.Client.csproj
@@ -30,7 +30,6 @@
-
diff --git a/QS.Launcher.Avalonia/Views/Pages/BaseManagementView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/BaseManagementView.axaml.cs
index b0bbc5238..e6332d279 100644
--- a/QS.Launcher.Avalonia/Views/Pages/BaseManagementView.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/Pages/BaseManagementView.axaml.cs
@@ -6,7 +6,6 @@ namespace QS.Launcher.Views.Pages;
public partial class BaseManagementView : UserControl {
public BaseManagementView(BaseManagementVM viewModel) {
InitializeComponent();
-
DataContext = viewModel;
}
}
diff --git a/QS.Launcher/ViewModels/MainWindowVM.cs b/QS.Launcher/ViewModels/MainWindowVM.cs
index 34d9d7c4b..6039da0c0 100644
--- a/QS.Launcher/ViewModels/MainWindowVM.cs
+++ b/QS.Launcher/ViewModels/MainWindowVM.cs
@@ -23,7 +23,7 @@ public int SelectedPageIndex {
public int PagesCount {
get => rootPagesCount;
- set { /* кол-во корневых страниц фиксируется в ctor */ }
+ set { }
}
public MainWindowVM(
diff --git a/QS.Launcher/ViewModels/PageViewModels/BaseManagementVM.cs b/QS.Launcher/ViewModels/PageViewModels/BaseManagementVM.cs
index d0d0de5ae..035ef2690 100644
--- a/QS.Launcher/ViewModels/PageViewModels/BaseManagementVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/BaseManagementVM.cs
@@ -9,6 +9,7 @@ public class BaseManagementVM : CarouselPageVM {
public DatabaseViewModel SelectedDatabase { get; set; }
public BaseManagementVM() {
+
}
}
}
diff --git a/QS.Launcher/ViewModels/PageViewModels/CarouselPageVM.cs b/QS.Launcher/ViewModels/PageViewModels/CarouselPageVM.cs
index 90524aba9..1d865db8b 100644
--- a/QS.Launcher/ViewModels/PageViewModels/CarouselPageVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/CarouselPageVM.cs
@@ -18,12 +18,12 @@ public class CarouselPageVM : ViewModelBase {
public ICommand PushPageCommand { get; set; }
///
- /// Закрыть текущую нерутовую страницу и вернуться на предыдущую.
+ /// Закрыть текущую нерутовую страницу и вернуться на предыдущую
///
public ICommand PopPageCommand { get; set; }
///
- /// Закрыть все нерутовые страницы и вернуться к корневым вкладкам.
+ /// Закрыть все нерутовые страницы и вернуться к корневым вкладкам
///
public ICommand PopToRootCommand { get; set; }
}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
index d68ab0611..4a813af5b 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
@@ -5,9 +5,6 @@
using ReactiveUI;
namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
- ///
- /// По «Далее» создаёт CreateDataBaseProgressVM и пушит её в Carousel поверх текущей
- ///
public class CreateDataBaseSettingsVM : CarouselPageVM {
public IDbProvider Provider { get; private set; }
public Connection Connection { get; private set; }
diff --git a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
index 48d5397d7..5d16e3b2e 100644
--- a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
+++ b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
@@ -1,14 +1,10 @@
-using FluentNHibernate.Cfg.Db;
using MySqlConnector;
using QS.DBScripts.Controllers;
using QS.Dialog;
-using QS.Project.DB;
-using QS.Project.Domain;
using System;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
-using System.Xml.Linq;
namespace QS.DBScripts.Models
{
From 024eeda11a82fd5b3c9ffb6ddeac5329257e21b2 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Tue, 12 May 2026 00:13:32 +0300
Subject: [PATCH 011/135] =?UTF-8?q?=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE?=
=?UTF-8?q?=D0=B8=D0=BB=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=20=D1=81?=
=?UTF-8?q?=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B1=D0=B0=D0=B7?=
=?UTF-8?q?=D1=8B=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20=D0=BE=D0=B1=D0=BB?=
=?UTF-8?q?=D0=B0=D0=BA=D0=BE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Clients/DataBaseManagementCloudClient.cs | 13 ++--
QS.Cloud.Client/DataBase/QSCloudProvider.cs | 13 ++--
QS.Cloud.Client/DataBase/QsCloudDbCreator.cs | 2 +-
QS.DbManagement/IDbProvider.cs | 4 +-
QS.DbManagement/MariaDb/MariaDBProvider.cs | 2 +-
.../Services/LauncherDbCreatorInteraction.cs | 4 +-
.../DataBase/CreateDataBaseProgressVM.cs | 37 ++++++----
.../DataBase/CreateDataBaseSettingsVM.cs | 15 +++-
.../DataBase/DbCreationPhase.cs | 20 ++++++
.../Dialog/AvaloniaInteractiveQuestion.cs | 70 +++++++++++++++++--
.../Dialog/AvaloniaInteractiveService.cs | 10 +++
.../Dialog/IInteractiveQuestion.cs | 21 +++++-
.../DBScripts/Models/MySqlDbCreateModel.cs | 14 ++--
13 files changed, 182 insertions(+), 43 deletions(-)
create mode 100644 QS.Launcher/ViewModels/PageViewModels/DataBase/DbCreationPhase.cs
diff --git a/QS.Cloud.Client/Clients/DataBaseManagementCloudClient.cs b/QS.Cloud.Client/Clients/DataBaseManagementCloudClient.cs
index 9bd26b15b..9579fd835 100644
--- a/QS.Cloud.Client/Clients/DataBaseManagementCloudClient.cs
+++ b/QS.Cloud.Client/Clients/DataBaseManagementCloudClient.cs
@@ -1,15 +1,20 @@
-using System.Threading;
using Grpc.Core;
using QS.Cloud.Core;
+using QS.Project.Versioning;
+using System.Threading;
namespace QS.Cloud.Client.Clients {
public class DataBaseManagementCloudClient : CloudClientByBasicAuth {
+ private IApplicationInfo applicationInfo { get; set; }
public DataBaseManagementCloudClient(IBasicAuthInfoProvider basicAuthInfoProvider)
- : base(basicAuthInfoProvider, "core.cloud.qsolution.ru", 443) { }
+ : base(basicAuthInfoProvider, "core.cloud.qsolution.ru", 443)
+ {
+ this.applicationInfo = applicationInfo;
+ }
- public CreateDataBaseResponse CreateDataBase(string dbName, string dbTitle) {
+ public CreateDataBaseResponse CreateDataBase(string dbName, string dbTitle, IApplicationInfo applicationInfo) {
var client = new DataBaseManagement.DataBaseManagementClient(Channel);
- var request = new CreateDataBaseRequest { Name = dbName, Title = dbTitle };
+ var request = new CreateDataBaseRequest { Name = dbName, Title = dbTitle, ProductId = applicationInfo.ProductCode };
return client.CreateDataBase(request, headers);
}
}
diff --git a/QS.Cloud.Client/DataBase/QSCloudProvider.cs b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
index 9682fb105..c8c2c9738 100644
--- a/QS.Cloud.Client/DataBase/QSCloudProvider.cs
+++ b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
@@ -12,18 +12,16 @@
using QS.Cloud.Client.Clients;
using QS.DBScripts.Controllers;
using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
namespace QS.Cloud.Client.DataBase
{
public class QSCloudProvider : IDbProvider {
- ///
- /// Публичный - в типе родключения нужен доступ, реализацию он знает и так
- ///
public int BaseId { get; private set; }
public BasicAuthInfoProvider AuthInfo { get; private set; }
- public bool IsConnected => throw new NotImplementedException();
+ public bool IsConnected { get; private set; }
public bool IsAdmin { get; protected set; }
@@ -33,7 +31,7 @@ public class QSCloudProvider : IDbProvider {
#endregion
public string UserName { get; private set; }
- public bool CanCreateDatabase => throw new NotImplementedException();
+ public bool CanCreateDatabase => dbClient.CanConnect;
private LoginManagementCloudClient loginClient;
private DataBaseManagementCloudClient dbClient;
@@ -58,9 +56,10 @@ public bool ChangePassword(string username, string oldPassword, string newPasswo
throw new NotImplementedException();
}
- public bool CreateDatabase(string databaseName, string title)
+ public bool CreateDatabase(string databaseName, string title, IServiceProvider services)
{
- CreateDataBaseResponse response = dbClient.CreateDataBase(databaseName, title);
+ IApplicationInfo applicationInfo = services.GetService();
+ CreateDataBaseResponse response = dbClient.CreateDataBase(databaseName, title, applicationInfo);
BaseId = response.BaseId;
return true;
}
diff --git a/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs b/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
index 92cca32d3..7e189244f 100644
--- a/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
+++ b/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
@@ -62,7 +62,7 @@ public async Task RunCreationAsync(string dbName, string dbTitle) {
var creator = new MySqlDbCreateModel(session.Db.Server, session.Db.Port, session.Db.Login, session.Db.Password, configuration, progress, interaction, cancellationToken);
creator.FillBaseGuid = false;
- bool success = await creator.RunCreationAsync(dbName, dbTitle);
+ bool success = await creator.RunCreationAsync(session.Db.BaseName, dbTitle);
sessionLife.Dispose();
diff --git a/QS.DbManagement/IDbProvider.cs b/QS.DbManagement/IDbProvider.cs
index d0c6fd3e6..c97d2f142 100644
--- a/QS.DbManagement/IDbProvider.cs
+++ b/QS.DbManagement/IDbProvider.cs
@@ -10,8 +10,8 @@ public interface IDbProvider : IDisposable
string UserName { get; }
bool ChangePassword(string username, string oldPassword, string newPassword);
-
- bool CreateDatabase(string databaseName, string title);
+
+ bool CreateDatabase(string databaseName, string title, IServiceProvider services = null);
bool DropDatabase(string databaseName);
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index 948b225ff..880714f9c 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -178,7 +178,7 @@ public bool ChangePassword(string username, string oldPassword, string newPasswo
return connection.Execute(sql) != 0;
}
- public bool CreateDatabase(string databaseName, string title) {
+ public bool CreateDatabase(string databaseName, string title, IServiceProvider services = null) {
CreatedTitle = title;
string sql = $"CREATE DATABASE IF NOT EXISTS `{databaseName}`";
return connection.Execute(sql) != 0;
diff --git a/QS.Launcher/Services/LauncherDbCreatorInteraction.cs b/QS.Launcher/Services/LauncherDbCreatorInteraction.cs
index fa2262cee..e68e95b79 100644
--- a/QS.Launcher/Services/LauncherDbCreatorInteraction.cs
+++ b/QS.Launcher/Services/LauncherDbCreatorInteraction.cs
@@ -26,9 +26,9 @@ public LauncherDbCreatorInteraction(
public Task AskDropExistingDatabaseAsync(string dbName) {
var tcs = new TaskCompletionSource();
- uiThread.Post(() => {
+ uiThread.Post(async () => {
try {
- bool answer = question.Question(
+ bool answer = await question.QuestionAsync(
$"База с именем `{dbName}` уже существует на сервере. Удалить существующую базу перед созданием новой?",
"Создание базы данных");
tcs.TrySetResult(answer);
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
index 213707c8b..5a59100a0 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
@@ -1,4 +1,6 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Reactive;
using System.Threading;
using System.Threading.Tasks;
@@ -18,8 +20,7 @@ public class CreateDataBaseProgressVM : CarouselPageVM, IProgressBarDisplayable
public IDbProvider Provider { get; private set; }
public Connection Connection { get; private set; }
- public string DbName { get; private set; }
- public string DbTitle { get; private set; }
+ private IReadOnlyList phases = Array.Empty();
private readonly IDbCreatorInteraction interaction;
private readonly IServiceProvider services;
@@ -83,14 +84,15 @@ public CreateDataBaseProgressVM(
});
}
- public void SetDbSettings(
- string dbName,
- string dbTitle,
- IDbProvider provider, Connection connection) {
- DbName = dbName ?? throw new ArgumentNullException(nameof(dbName));
- DbTitle = dbTitle ?? throw new ArgumentNullException(nameof(dbTitle));
+ public void SetPipeline(
+ IDbProvider provider, Connection connection,
+ IEnumerable phases) {
Provider = provider ?? throw new ArgumentNullException(nameof(provider));
Connection = connection ?? throw new ArgumentNullException(nameof(connection));
+ if(phases == null) throw new ArgumentNullException(nameof(phases));
+ this.phases = phases.ToList();
+ if(this.phases.Count == 0)
+ throw new ArgumentException("Пайплайн создания базы пуст.", nameof(phases));
}
public async Task StartCreationAsync() {
@@ -102,12 +104,19 @@ public async Task StartCreationAsync() {
CancellationToken = cts.Token,
ServiceProvider = services
};
- IDbCreatorModel creator = Connection.ConnectionType.CreateCreator(args);
- bool ok = await creator.RunCreationAsync(DbName, DbTitle);
- if(ok)
- DatabaseCreated?.Invoke();
- else
- DatabaseCreationFailed?.Invoke();
+
+ for(int i = 0; i < phases.Count; i++) {
+ cts.Token.ThrowIfCancellationRequested();
+ var phase = phases[i];
+ uiThread.Post(() => CurrentText = phase.Title);
+
+ bool ok = await phase.Action(args);
+ if(!ok) {
+ DatabaseCreationFailed?.Invoke();
+ return;
+ }
+ }
+ DatabaseCreated?.Invoke();
}
catch(OperationCanceledException) {
logger.Info("Создание базы отменено.");
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
index 4a813af5b..9957dc2f4 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
@@ -1,7 +1,9 @@
using System;
using System.Reactive;
using System.Reactive.Linq;
+using System.Threading.Tasks;
using QS.DbManagement;
+using QS.DBScripts.Controllers;
using ReactiveUI;
namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
@@ -50,8 +52,19 @@ private void GoToProgress() {
var progress = Microsoft.Extensions.DependencyInjection.ActivatorUtilities
.GetServiceOrCreateInstance(services);
- progress.SetDbSettings(dbName, dbTitle, Provider, Connection);
+ var pipeline = new[] {
+ new DbCreationPhase(
+ "Создание базы данных",
+ (args) => Task.FromResult(args.Provider.CreateDatabase(DbName, DbTitle, services))),
+ new DbCreationPhase(
+ "Наполнение базы данных",
+ async args => {
+ IDbCreatorModel creator = Connection.ConnectionType.CreateCreator(args);
+ return await creator.RunCreationAsync(DbName, DbTitle);
+ })
+ };
+ progress.SetPipeline(Provider, Connection, pipeline);
ProgressPageRequested?.Invoke(progress);
PushPageCommand?.Execute(progress);
}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/DbCreationPhase.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/DbCreationPhase.cs
new file mode 100644
index 000000000..7e9085445
--- /dev/null
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/DbCreationPhase.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Threading.Tasks;
+using QS.DbManagement;
+using QS.DBScripts.Controllers;
+
+namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
+ ///
+ /// Один шаг пайплайна создания базы. Settings-VM собирает список таких фаз
+ /// и передаёт его в Progress-VM, который выполняет их последовательно.
+ ///
+ public sealed class DbCreationPhase {
+ public string Title { get; }
+ public Func> Action { get; }
+
+ public DbCreationPhase(string title, Func> action) {
+ Title = title ?? throw new ArgumentNullException(nameof(title));
+ Action = action ?? throw new ArgumentNullException(nameof(action));
+ }
+ }
+}
diff --git a/QS.Project.Avalonia/Dialog/AvaloniaInteractiveQuestion.cs b/QS.Project.Avalonia/Dialog/AvaloniaInteractiveQuestion.cs
index e40799c80..2d97cbf5c 100644
--- a/QS.Project.Avalonia/Dialog/AvaloniaInteractiveQuestion.cs
+++ b/QS.Project.Avalonia/Dialog/AvaloniaInteractiveQuestion.cs
@@ -1,14 +1,76 @@
using System;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Threading;
+using QS.Project.Avalonia;
namespace QS.Dialog;
+
public class AvaloniaInteractiveQuestion : IInteractiveQuestion {
+
public bool Question(string message, string title = null) {
- // TODO: Implement Question dialog behaviour to DialogWindow.axaml.cs
- throw new NotImplementedException();
+ if(Dispatcher.UIThread.CheckAccess())
+ throw new InvalidOperationException(
+ "Синхронный Question нельзя вызывать из UI-потока — это приведёт к дедлоку. Используйте QuestionAsync.");
+ return QuestionAsync(message, title).GetAwaiter().GetResult();
}
public string Question(string[] buttons, string message, string title = null) {
- // How to solve answer from several buttons?
- throw new NotImplementedException();
+ if(Dispatcher.UIThread.CheckAccess())
+ throw new InvalidOperationException(
+ "Синхронный Question нельзя вызывать из UI-потока — это приведёт к дедлоку. Используйте QuestionAsync.");
+ return QuestionAsync(buttons, message, title).GetAwaiter().GetResult();
+ }
+
+ public Task QuestionAsync(string message, string title = null) =>
+ Dispatcher.UIThread.InvokeAsync(async () => {
+ var dlg = new DialogWindow(message, title ?? string.Empty, ImportanceLevel.Info);
+ var tcs = new TaskCompletionSource();
+
+ var yes = new Button { Content = "Да" };
+ var no = new Button { Content = "Нет" };
+ yes.Click += (_, _) => { tcs.TrySetResult(true); dlg.Close(); };
+ no.Click += (_, _) => { tcs.TrySetResult(false); dlg.Close(); };
+ dlg.AddButton(yes);
+ dlg.AddButton(no);
+ // Закрытие крестиком / штатной кнопкой "Закрыть" => Нет
+ dlg.Closed += (_, _) => tcs.TrySetResult(false);
+
+ await ShowAsync(dlg);
+ return await tcs.Task;
+ });
+
+ public Task QuestionAsync(string[] buttons, string message, string title = null) =>
+ Dispatcher.UIThread.InvokeAsync(async () => {
+ var dlg = new DialogWindow(message, title ?? string.Empty, ImportanceLevel.Info);
+ var tcs = new TaskCompletionSource();
+
+ foreach(var label in buttons) {
+ var captured = label;
+ var btn = new Button { Content = captured };
+ btn.Click += (_, _) => { tcs.TrySetResult(captured); dlg.Close(); };
+ dlg.AddButton(btn);
+ }
+ // Закрытие крестиком => null
+ dlg.Closed += (_, _) => tcs.TrySetResult(null);
+
+ await ShowAsync(dlg);
+ return await tcs.Task;
+ });
+
+ private static Task ShowAsync(Window dlg) {
+ var owner = GetOwner();
+ if(owner != null && owner != dlg && owner.IsVisible)
+ return dlg.ShowDialog(owner);
+ dlg.Show();
+ return Task.CompletedTask;
+ }
+
+ private static Window GetOwner() {
+ if(Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ return desktop.MainWindow;
+ return null;
}
}
diff --git a/QS.Project.Avalonia/Dialog/AvaloniaInteractiveService.cs b/QS.Project.Avalonia/Dialog/AvaloniaInteractiveService.cs
index a1fe6409e..9856d4b6f 100644
--- a/QS.Project.Avalonia/Dialog/AvaloniaInteractiveService.cs
+++ b/QS.Project.Avalonia/Dialog/AvaloniaInteractiveService.cs
@@ -1,3 +1,5 @@
+using System.Threading.Tasks;
+
namespace QS.Dialog;
public class AvaloniaInteractiveService(AvaloniaInteractiveMessage interactiveMessage, AvaloniaInteractiveQuestion interactiveQuestion) : IInteractiveService {
@@ -9,6 +11,14 @@ public string Question(string[] buttons, string message, string title = null) {
return interactiveQuestion.Question(buttons, message, title);
}
+ public Task QuestionAsync(string message, string title = null) {
+ throw new System.NotImplementedException();
+ }
+
+ public Task QuestionAsync(string[] buttons, string message, string title = null) {
+ throw new System.NotImplementedException();
+ }
+
public void ShowMessage(ImportanceLevel level, string message, string title = null) {
interactiveMessage.ShowMessage(level, message, title);
}
diff --git a/QS.Project.Core/Dialog/IInteractiveQuestion.cs b/QS.Project.Core/Dialog/IInteractiveQuestion.cs
index c471a6d3e..49f9c698d 100644
--- a/QS.Project.Core/Dialog/IInteractiveQuestion.cs
+++ b/QS.Project.Core/Dialog/IInteractiveQuestion.cs
@@ -1,4 +1,6 @@
-namespace QS.Dialog
+using System.Threading.Tasks;
+
+namespace QS.Dialog
{
public interface IInteractiveQuestion
{
@@ -18,5 +20,22 @@ public interface IInteractiveQuestion
/// Заголовок окна диалога
/// Вернет заголовок кнопки которую нажал пользователь. Если пользователь закроет диалог крестиком, вернется null.
string Question(string[] buttons, string message, string title = null);
+
+ ///
+ /// Отобразит диалог с вопросом пользователю и кнопками Да Нет.
+ ///
+ /// Сообщение диалога
+ /// Заголовок окна диалога
+ /// True - Да, False - Нет(или закрытие крестиком)
+ Task QuestionAsync(string message, string title = null);
+
+ ///
+ /// Отобразит диалог с вопросом пользователю.
+ ///
+ /// Список заголовков для кнопок
+ /// Сообщение диалога
+ /// Заголовок окна диалога
+ /// Вернет заголовок кнопки которую нажал пользователь. Если пользователь закроет диалог крестиком, вернется null.
+ Task QuestionAsync(string[] buttons, string message, string title = null);
}
}
diff --git a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
index 5d16e3b2e..fb4a00696 100644
--- a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
+++ b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
@@ -74,15 +74,15 @@ public bool RunCreation(string dbName, string dbTitle = null) {
var sql = "SHOW DATABASES;";
var cmd = new MySqlCommand(sql, connectionDB);
bool needDropBase = false;
+ bool hasBase = false;
using(var rdr = cmd.ExecuteReader()) {
while(rdr.Read()) {
if(rdr[0].ToString() == dbName) {
if(interaction.AskDropExistingDatabaseAsync(dbName).GetAwaiter().GetResult()) {
needDropBase = true;
- break;
}
- else
- return false;
+ hasBase = true;
+ break;
}
}
}
@@ -104,9 +104,11 @@ public bool RunCreation(string dbName, string dbTitle = null) {
cmd.ExecuteNonQuery();
}
- progress.Add(text: $"Создаем базу {dbName}");
- cmd.CommandText = String.Format("CREATE SCHEMA `{0}` DEFAULT CHARACTER SET utf8mb4 ;", dbName);
- cmd.ExecuteNonQuery();
+ if(!hasBase || needDropBase) {
+ progress.Add(text: $"Создаем базу {dbName}");
+ cmd.CommandText = String.Format("CREATE SCHEMA `{0}` DEFAULT CHARACTER SET utf8mb4 ;", dbName);
+ cmd.ExecuteNonQuery();
+ }
cmd.CommandText = String.Format("USE `{0}` ;", dbName);
cmd.ExecuteNonQuery();
From 0dc4147d6b1c01d79f9eb9a84bc2174d410b610f Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Thu, 14 May 2026 18:05:34 +0300
Subject: [PATCH 012/135] =?UTF-8?q?=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20?=
=?UTF-8?q?=D0=B0=D0=B4=D0=B0=D0=BF=D1=82=D0=B5=D1=80=20=D0=B4=D0=BB=D1=8F?=
=?UTF-8?q?=20=D1=81=D1=82=D1=80=D0=BE=D0=B3=D0=BE=20=D0=BA=D0=BE=D0=B4?=
=?UTF-8?q?=D0=B0=20=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B5=D1=80=D0=B0=D0=B1?=
=?UTF-8?q?=D1=82=D0=B0=D0=BB=20=D0=BF=D0=BE=D0=B4=20=D0=BD=D0=BE=D0=B2?=
=?UTF-8?q?=D1=8B=D0=B9=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9?=
=?UTF-8?q?=D1=81=20=D0=B2=D0=B7=D0=B0=D0=B8=D0=BC=D0=BE=D0=B4=D0=B5=D0=B9?=
=?UTF-8?q?=D1=81=D1=82=D0=B2=D0=B8=D1=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Адаптер, превращающий пару (CreationScript, UpdateConfiguration), которые исторически регистрируются в Autofac-контейнере проекта-потребителя как AsSelf, в реализацию IDbScriptsConfiguration. Нужен для GTK-десктоп-флоу: UpdaterDBAutofacModule → UserCreateDbController → MySqlDbCreateModel требуют IDbScriptsConfiguration, а сами приложения по-прежнему поставляют только CreationScript/UpdateConfiguration
Адаптер избавляет их от необходимости менять регистрацию контейнера
UpdateConfiguration опционален — если приложение его не регистрирует, отдаём пустой
Конструирую MySqlDbCreateModel в UserCreateDbController явно — без Autofac TypedParameter, которым раньше прокидывался IDbCreateController. Теперь модель получает все сессионные зависимости в свой ctor.
---
.../GtkInteractiveService.cs | 9 ++
.../Interactive/ConsoleInteractiveQuestion.cs | 11 ++-
.../Interactive/ConsoleInteractiveService.cs | 9 ++
.../DbScriptsConfigurationAdapter.cs | 24 +++++
QS.Updater.DB.Gtk/UpdaterDBAutofacModule.cs | 13 ++-
.../Controllers/UserCreateDbController.cs | 92 ++++++++++++++-----
6 files changed, 134 insertions(+), 24 deletions(-)
create mode 100644 QS.Updater.Core/DBScripts/DbScriptsConfigurationAdapter.cs
diff --git a/QS.Project.Gtk/Project.Services.GtkUI/GtkInteractiveService.cs b/QS.Project.Gtk/Project.Services.GtkUI/GtkInteractiveService.cs
index ccafcd490..e44d53802 100644
--- a/QS.Project.Gtk/Project.Services.GtkUI/GtkInteractiveService.cs
+++ b/QS.Project.Gtk/Project.Services.GtkUI/GtkInteractiveService.cs
@@ -1,4 +1,5 @@
using QS.Dialog;
+using System.Threading.Tasks;
namespace QS.Project.Services.GtkUI {
public class GtkInteractiveService : IInteractiveService
@@ -25,5 +26,13 @@ public string Question(string[] buttons, string message, string title = null)
{
return interactiveQuestion.Question(buttons, message, title);
}
+
+ public Task QuestionAsync(string message, string title = null) {
+ throw new System.NotImplementedException();
+ }
+
+ public Task QuestionAsync(string[] buttons, string message, string title = null) {
+ throw new System.NotImplementedException();
+ }
}
}
diff --git a/QS.Project/Project.Services/Interactive/ConsoleInteractiveQuestion.cs b/QS.Project/Project.Services/Interactive/ConsoleInteractiveQuestion.cs
index c872e1a2e..3ca6bc1f2 100644
--- a/QS.Project/Project.Services/Interactive/ConsoleInteractiveQuestion.cs
+++ b/QS.Project/Project.Services/Interactive/ConsoleInteractiveQuestion.cs
@@ -1,6 +1,7 @@
-using System;
+using System;
using QS.Dialog;
using System.Linq;
+using System.Threading.Tasks;
namespace QS.Project.Services.Interactive
{
public class ConsoleInteractiveQuestion : IInteractiveQuestion
@@ -36,5 +37,13 @@ public string Question(string[] buttons, string message, string title = null)
}
return null;
}
+
+ public Task QuestionAsync(string message, string title = null) {
+ throw new NotImplementedException();
+ }
+
+ public Task QuestionAsync(string[] buttons, string message, string title = null) {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/QS.Project/Project.Services/Interactive/ConsoleInteractiveService.cs b/QS.Project/Project.Services/Interactive/ConsoleInteractiveService.cs
index 2158bdd89..8f3b69424 100644
--- a/QS.Project/Project.Services/Interactive/ConsoleInteractiveService.cs
+++ b/QS.Project/Project.Services/Interactive/ConsoleInteractiveService.cs
@@ -1,4 +1,5 @@
using QS.Dialog;
+using System.Threading.Tasks;
namespace QS.Project.Services.Interactive
{
@@ -27,5 +28,13 @@ public string Question(string[] buttons, string message, string title = null)
{
return interactiveQuestion.Question(buttons, message, title);
}
+
+ public Task QuestionAsync(string message, string title = null) {
+ throw new System.NotImplementedException();
+ }
+
+ public Task QuestionAsync(string[] buttons, string message, string title = null) {
+ throw new System.NotImplementedException();
+ }
}
}
diff --git a/QS.Updater.Core/DBScripts/DbScriptsConfigurationAdapter.cs b/QS.Updater.Core/DBScripts/DbScriptsConfigurationAdapter.cs
new file mode 100644
index 000000000..905a75275
--- /dev/null
+++ b/QS.Updater.Core/DBScripts/DbScriptsConfigurationAdapter.cs
@@ -0,0 +1,24 @@
+using System;
+using QS.DBScripts.Models;
+using QS.Updater.DB;
+
+namespace QS.DBScripts
+{
+ public class DbScriptsConfigurationAdapter : IDbScriptsConfiguration
+ {
+ private readonly CreationScript creationScript;
+ private readonly UpdateConfiguration updateConfiguration;
+
+ public DbScriptsConfigurationAdapter(CreationScript creationScript, UpdateConfiguration updateConfiguration = null)
+ {
+ this.creationScript = creationScript ?? throw new ArgumentNullException(nameof(creationScript));
+ this.updateConfiguration = updateConfiguration;
+ }
+
+ public bool HasCreationScript() => creationScript != null;
+
+ public CreationScript MakeCreationScript() => creationScript;
+
+ public UpdateConfiguration MakeUpdateConfiguration() => updateConfiguration ?? new UpdateConfiguration();
+ }
+}
diff --git a/QS.Updater.DB.Gtk/UpdaterDBAutofacModule.cs b/QS.Updater.DB.Gtk/UpdaterDBAutofacModule.cs
index 5bfe0afab..971241817 100644
--- a/QS.Updater.DB.Gtk/UpdaterDBAutofacModule.cs
+++ b/QS.Updater.DB.Gtk/UpdaterDBAutofacModule.cs
@@ -1,4 +1,5 @@
-using Autofac;
+using Autofac;
+using QS.DBScripts;
using QS.DBScripts.Controllers;
using QS.DBScripts.Models;
using QS.Updater.DB;
@@ -19,6 +20,16 @@ protected override void Load(ContainerBuilder builder)
#region Desktop
builder.RegisterType().As();
#endregion
+ builder.Register(c => {
+ var creation = c.Resolve();
+ UpdateConfiguration updates = null;
+ if(c.IsRegistered())
+ updates = c.Resolve();
+ return new DbScriptsConfigurationAdapter(creation, updates);
+ })
+ .As()
+ .SingleInstance()
+ .PreserveExistingDefaults();
#region ViewModels
builder.RegisterAssemblyTypes(System.Reflection.Assembly.GetAssembly(typeof(UpdateProcessViewModel)))
.Where(t => t.IsAssignableTo() && t.Name.EndsWith("ViewModel"))
diff --git a/QS.Updater.DB/DBScripts/Controllers/UserCreateDbController.cs b/QS.Updater.DB/DBScripts/Controllers/UserCreateDbController.cs
index ebfc35f36..531b8d3ce 100644
--- a/QS.Updater.DB/DBScripts/Controllers/UserCreateDbController.cs
+++ b/QS.Updater.DB/DBScripts/Controllers/UserCreateDbController.cs
@@ -1,5 +1,6 @@
using System;
-using Autofac;
+using System.Threading;
+using System.Threading.Tasks;
using QS.DBScripts.Models;
using QS.DBScripts.ViewModels;
using QS.Dialog;
@@ -8,22 +9,25 @@
namespace QS.DBScripts.Controllers
{
- public class UserCreateDbController : IDBCreator, IDbCreateController
+ public class UserCreateDbController : IDBCreator, IDbCreatorInteraction
{
static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly INavigationManager navigation;
- private readonly ILifetimeScope autofacScope;
private readonly IInteractiveService interactive;
private readonly IGuiDispatcher guiDispatcher;
+ private readonly IDbScriptsConfiguration scripts;
- public UserCreateDbController(INavigationManager navigation, ILifetimeScope autofacScope, IInteractiveService interactive, IGuiDispatcher guiDispatcher)
+ public UserCreateDbController(
+ INavigationManager navigation,
+ IInteractiveService interactive,
+ IGuiDispatcher guiDispatcher,
+ IDbScriptsConfiguration scripts)
{
-
this.navigation = navigation ?? throw new ArgumentNullException(nameof(navigation));
- this.autofacScope = autofacScope ?? throw new ArgumentNullException(nameof(autofacScope));
this.interactive = interactive ?? throw new ArgumentNullException(nameof(interactive));
this.guiDispatcher = guiDispatcher ?? throw new ArgumentNullException(nameof(guiDispatcher));
+ this.scripts = scripts ?? throw new ArgumentNullException(nameof(scripts));
}
public void RunCreation(string server, string dbname)
@@ -37,31 +41,76 @@ public void RunCreation(string server, string dbname)
};
}
- void StartCreation(string server, string dbname, string login, string password)
+ async void StartCreation(string server, string dbname, string login, string password)
{
- var createModel = autofacScope.Resolve(new TypedParameter(typeof(IDbCreateController), this));
+ ParseServer(server, out string host, out uint port);
+
+ bool success = false;
try {
- bool success = createModel.RunCreation(server, dbname, login, password);
- if (success)
- interactive.ShowMessage(ImportanceLevel.Info, "Создание базы успешно завершено.\nЗайдите в программу под администратором для добавления пользователей.");
+ var createModel = new MySqlDbCreateModel(
+ host, port, login, password,
+ scripts,
+ Progress,
+ interaction: this,
+ cancellationToken: CancellationToken.None);
+
+ success = await createModel.RunCreationAsync(dbname, dbTitle: null);
+ }
+ catch(Exception ex) {
+ logger.Error(ex, "Ошибка создания базы.");
+ guiDispatcher.RunInGuiTread(() => interactive.ShowMessage(ImportanceLevel.Error, ex.Message));
}
finally {
- if(progressPage != null)
- navigation.ForceClosePage(progressPage, CloseSource.FromParentPage);
+ guiDispatcher.RunInGuiTread(() => {
+ if(progressPage != null)
+ navigation.ForceClosePage(progressPage, CloseSource.FromParentPage);
+ });
+ }
+
+ if(success) {
+ guiDispatcher.RunInGuiTread(() =>
+ interactive.ShowMessage(ImportanceLevel.Info,
+ "Создание базы успешно завершено.\nЗайдите в программу под администратором для добавления пользователей."));
}
}
- #region Взаимодействие с моделью
- public void WasError(string text, string lastSqlCommand)
- {
- interactive.ShowMessage(ImportanceLevel.Error, text);
+ private static void ParseServer(string server, out string host, out uint port) {
+ port = 3306;
+ var parts = (server ?? string.Empty).Split(new[] { ':' }, 2, StringSplitOptions.RemoveEmptyEntries);
+ if(parts.Length == 0)
+ throw new InvalidOperationException("Имя сервера не корректно.");
+ host = parts[0];
+ if(parts.Length > 1)
+ uint.TryParse(parts[1], out port);
}
- public bool NeedDropDatabaseIfExists(string dbname)
- {
- return interactive.Question($"База с именем `{dbname}` уже существует на сервере. Удалить существующую базу перед созданием новой?");
+ #region IDbCreatorInteraction
+
+ public Task AskDropExistingDatabaseAsync(string dbName) {
+ var tcs = new TaskCompletionSource();
+ guiDispatcher.RunInGuiTread(() => {
+ try {
+ tcs.SetResult(interactive.Question(
+ $"База с именем `{dbName}` уже существует на сервере. Удалить существующую базу перед созданием новой?"));
+ }
+ catch(Exception ex) { tcs.SetException(ex); }
+ });
+ return tcs.Task;
}
+ public Task ReportErrorAsync(string text, string lastExecutedStatement) {
+ var tcs = new TaskCompletionSource();
+ guiDispatcher.RunInGuiTread(() => {
+ try {
+ interactive.ShowMessage(ImportanceLevel.Error, text);
+ tcs.SetResult(true);
+ }
+ catch(Exception ex) { tcs.SetException(ex); }
+ });
+ return tcs.Task;
+ }
+ #endregion
+
#region Свойства процесса
IPage progressPage;
@@ -76,6 +125,5 @@ public IProgressBarDisplayable Progress {
}
}
#endregion
- #endregion
}
-}
\ No newline at end of file
+}
From 2611bf32c8178c3ab23fa72d5813d3b8552b4de3 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Wed, 10 Jun 2026 20:56:32 +0300
Subject: [PATCH 013/135] =?UTF-8?q?=D0=B4=D0=BE=D1=80=D0=B0=D0=B1=D0=BE?=
=?UTF-8?q?=D1=82=D0=BA=D0=B8=20=D0=BF=D0=BE=20=D1=80=D0=B5=D0=B2=D1=8C?=
=?UTF-8?q?=D1=8E?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
убрана многопоточность и асинхронность, настроена конфигурация наличия возможности создать базу и мелкие доработки
---
.../Clients/Base/CloudClientByBasicAuth.cs | 19 ++++--
.../DataBase/QsCloudConnectionTypeBase.cs | 26 ++++----
QS.Cloud.Client/DataBase/QsCloudDbCreator.cs | 33 +++++-----
.../DataBase/QsCloudScriptsConfiguration.cs | 2 +-
QS.DbManagement/ConnectionTypeBase.cs | 23 +++++--
.../DbCreationPhase.cs | 9 +--
QS.DbManagement/MariaDb/MariaDBProvider.cs | 3 +-
.../MariaDb/MariaDbConnectionTypeBase.cs | 27 ++++----
QS.Launcher.Avalonia/DependencyInjection.cs | 4 +-
.../Services/AvaloniaUiThreadInvoker.cs | 12 ----
.../Views/Pages/DataBase/DataBasesView.axaml | 2 +-
.../Pages/DataBase/DataBasesView.axaml.cs | 13 ++--
QS.Launcher/QS.Launcher.csproj | 1 -
QS.Launcher/Services/IUiThreadInvoker.cs | 11 ----
.../Services/LauncherDbCreatorInteraction.cs | 39 +++---------
.../DataBase/CreateDataBaseProgressVM.cs | 62 +++++++++----------
.../DataBase/CreateDataBaseSettingsVM.cs | 7 +--
.../PageViewModels/DataBase/DataBasesVM.cs | 17 ++++-
QS.LibsTest.Core/Launcher/ConfiguratorTest.cs | 6 +-
.../Controllers/IDbCreatorInteraction.cs | 6 +-
.../DBScripts/Controllers/IDbCreatorModel.cs | 8 +--
.../Dialog/IInteractiveQuestion.cs | 17 -----
.../GtkInteractiveService.cs | 8 ---
.../Interactive/ConsoleInteractiveQuestion.cs | 8 ---
.../Interactive/ConsoleInteractiveService.cs | 9 ---
.../DBScripts/Models/MySqlDbCreateModel.cs | 13 +---
26 files changed, 156 insertions(+), 229 deletions(-)
rename {QS.Launcher/ViewModels/PageViewModels/DataBase => QS.DbManagement}/DbCreationPhase.cs (59%)
delete mode 100644 QS.Launcher.Avalonia/Services/AvaloniaUiThreadInvoker.cs
delete mode 100644 QS.Launcher/Services/IUiThreadInvoker.cs
diff --git a/QS.Cloud.Client/Clients/Base/CloudClientByBasicAuth.cs b/QS.Cloud.Client/Clients/Base/CloudClientByBasicAuth.cs
index d7babc3d9..f6aa9ab06 100644
--- a/QS.Cloud.Client/Clients/Base/CloudClientByBasicAuth.cs
+++ b/QS.Cloud.Client/Clients/Base/CloudClientByBasicAuth.cs
@@ -4,19 +4,26 @@
namespace QS.Cloud.Client
{
- public class CloudClientByBasicAuth : CloudClientServiceBase
- {
+ public class CloudClientByBasicAuth : CloudClientServiceBase {
public CloudClientByBasicAuth(IBasicAuthInfoProvider basicAuthInfoProvider, string serviceAddress, int servicePort)
- : base(serviceAddress, servicePort)
- {
+ : base(serviceAddress, servicePort) {
headers = new Metadata
{ {
"Authorization",
$"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"{basicAuthInfoProvider.UserName}:{basicAuthInfoProvider.Password}"))}"
} };
-
+
}
- public override bool CanConnect => throw new NotImplementedException();
+ public override bool CanConnect { get {
+ try {
+ Channel.ConnectAsync().GetAwaiter().GetResult();
+ }
+ catch {
+ return false;
+ }
+ return true;
+ }
+ }
}
}
diff --git a/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs b/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
index bf0553cb2..cb206c682 100644
--- a/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
+++ b/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
@@ -4,6 +4,7 @@
using QS.DBScripts.Controllers;
using QS.DBScripts.Models;
using QS.Utilities.Extensions;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
@@ -19,6 +20,18 @@ public QsCloudConnectionTypeBase() {
Parameters.Add(new ConnectionParameter("Login","Пользователь"));
IconBytes = Assembly.GetExecutingAssembly().GetResourceByteArray("QS.Cloud.Client.Icons.qscloud.ico");
+
+ CreatorFactory = args => {
+ var provider = (QSCloudProvider)args.Provider;
+ var scripts = args.ServiceProvider.GetRequiredService();
+ return new QsCloudDbCreator(
+ provider.BaseId,
+ provider.AuthInfo,
+ scripts,
+ args.Progress,
+ args.Interaction,
+ args.CancellationToken);
+ };
}
public override bool CanConnect(IEnumerable parameters) {
@@ -29,18 +42,5 @@ public override bool CanConnect(IEnumerable parameters
public override IDbProvider CreateProvider(IList parameters, string password = null) {
return new QSCloudProvider(parameters, password);
}
-
- public override IDbCreatorModel CreatorFactory(CreatorFactoryArgs args) {
- var provider = (QSCloudProvider)args.Provider;
- var scripts = args.ServiceProvider.GetRequiredService();
- var creator = new QsCloudDbCreator(
- provider.BaseId,
- provider.AuthInfo,
- scripts,
- args.Progress,
- args.Interaction,
- args.CancellationToken);
- return creator;
- }
}
}
diff --git a/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs b/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
index 7e189244f..a11307579 100644
--- a/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
+++ b/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
@@ -5,7 +5,6 @@
using QS.Dialog;
using System;
using System.Threading;
-using System.Threading.Tasks;
namespace QS.Cloud.Client.DataBase
{
@@ -14,7 +13,6 @@ public class QsCloudDbCreator : IDbCreatorModel
static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly int baseId;
-
private readonly IProgressBarDisplayable progress;
private readonly IDbCreatorInteraction interaction;
private readonly IDbScriptsConfiguration configuration;
@@ -24,33 +22,33 @@ public class QsCloudDbCreator : IDbCreatorModel
public QsCloudDbCreator(
int baseId,
- BasicAuthInfoProvider AuthInfo,
+ BasicAuthInfoProvider authInfo,
IDbScriptsConfiguration configuration,
IProgressBarDisplayable progress,
IDbCreatorInteraction interaction,
CancellationToken cancellationToken)
{
this.baseId = baseId;
-
- loginClient = new LoginManagementCloudClient(AuthInfo);
-
- this.configuration = configuration ?? throw new ArgumentNullException(nameof(progress));
+ loginClient = new LoginManagementCloudClient(authInfo);
+ this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
this.progress = progress ?? throw new ArgumentNullException(nameof(progress));
this.interaction = interaction ?? throw new ArgumentNullException(nameof(interaction));
this.cancellationToken = cancellationToken;
}
-
- public async Task RunCreationAsync(string dbName, string dbTitle) {
+ public bool RunCreation(string dbName, string dbTitle) {
try {
+ cancellationToken.ThrowIfCancellationRequested();
+
StartSessionResponse session = loginClient.StartSession(baseId);
if(!session.Success) {
- await interaction.ReportErrorAsync("Ошибка в создании сесии", "Запрос в облако");
- throw new InvalidOperationException("Ошибка в создании сесии");
+ interaction.ReportError("Ошибка в создании сессии", "Запрос в облако");
+ return false;
}
- else if(!session.IsAdmin) {
- await interaction.ReportErrorAsync("Вы не имеете прав Администратора", "Запрос в облако");
+ if(!session.IsAdmin) {
+ interaction.ReportError("Вы не имеете прав Администратора", "Запрос в облако");
+ return false;
}
var infoProvider = new SessionInfoProvider(sessionId: session.SessionId);
@@ -60,12 +58,13 @@ public async Task RunCreationAsync(string dbName, string dbTitle) {
};
sessionLife.KeepAlive();
- var creator = new MySqlDbCreateModel(session.Db.Server, session.Db.Port, session.Db.Login, session.Db.Password, configuration, progress, interaction, cancellationToken);
+ var creator = new MySqlDbCreateModel(
+ session.Db.Server, session.Db.Port, session.Db.Login, session.Db.Password,
+ configuration, progress, interaction, cancellationToken);
creator.FillBaseGuid = false;
- bool success = await creator.RunCreationAsync(session.Db.BaseName, dbTitle);
+ bool success = creator.RunCreation(session.Db.BaseName, dbTitle);
sessionLife.Dispose();
-
return success;
}
catch(OperationCanceledException) {
@@ -74,7 +73,7 @@ public async Task RunCreationAsync(string dbName, string dbTitle) {
}
catch(Exception ex) {
logger.Error(ex, "Ошибка при создании базы в облаке.");
- await interaction.ReportErrorAsync(ex.Message, null);
+ interaction.ReportError(ex.Message, null);
throw;
}
finally {
diff --git a/QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs b/QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs
index 20c1d0ac2..163e42421 100644
--- a/QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs
+++ b/QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs
@@ -12,7 +12,7 @@ public class QsCloudScriptsConfiguration : IDbScriptsConfiguration {
private string ResourceName = "QS.Cloud.Client.Scripts.new_empty.sql";
public bool HasCreationScript() {
return Assembly.GetAssembly(typeof(QsCloudScriptsConfiguration))
- .GetReferencedAssemblies().Select(x => x.FullName)
+ .GetManifestResourceNames()
.Contains(ResourceName);
}
diff --git a/QS.DbManagement/ConnectionTypeBase.cs b/QS.DbManagement/ConnectionTypeBase.cs
index c3caca35c..82ea666c5 100644
--- a/QS.DbManagement/ConnectionTypeBase.cs
+++ b/QS.DbManagement/ConnectionTypeBase.cs
@@ -1,7 +1,9 @@
-using System;
-using System.Collections.Generic;
+using Microsoft.Extensions.DependencyInjection;
+using QS.DBScripts;
using QS.DBScripts.Controllers;
using QS.Dialog;
+using System;
+using System.Collections.Generic;
namespace QS.DbManagement {
@@ -17,16 +19,29 @@ public abstract class ConnectionTypeBase {
public abstract IDbProvider CreateProvider(IList parameters, string password = null);
- public abstract IDbCreatorModel CreatorFactory(CreatorFactoryArgs args);
+ public Func CreatorFactory { get; set; }
+
+ ///
+ /// Создание базы доступно, только если задана фабрика и приложение
+ /// зарегистрировало конфигурацию скриптов с реальным скриптом создания
+ ///
+ public virtual bool SupportsDatabaseCreation(IServiceProvider services) {
+ return CreatorFactory != null
+ && services.GetService()?.HasCreationScript() == true;
+ }
public IDbCreatorModel CreateCreator(CreatorFactoryArgs args) {
+ if(CreatorFactory == null)
+ throw new InvalidOperationException(
+ $"Для типа подключения '{ConnectionTypeName}' не настроена фабрика создания БД (CreatorFactory). "
+ + "Заполните её в композиционном корне приложения.");
return CreatorFactory(args);
}
}
///
/// interaction — канал диалогов с пользователем
- /// serviceProvider — для резолва дополнительных зависимостей
+ /// serviceProvider — для получения дополнительных зависимостей
///
public class CreatorFactoryArgs {
public IDbProvider Provider { get; set; }
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/DbCreationPhase.cs b/QS.DbManagement/DbCreationPhase.cs
similarity index 59%
rename from QS.Launcher/ViewModels/PageViewModels/DataBase/DbCreationPhase.cs
rename to QS.DbManagement/DbCreationPhase.cs
index 7e9085445..0c70e4528 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/DbCreationPhase.cs
+++ b/QS.DbManagement/DbCreationPhase.cs
@@ -1,18 +1,15 @@
using System;
-using System.Threading.Tasks;
using QS.DbManagement;
-using QS.DBScripts.Controllers;
namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
///
- /// Один шаг пайплайна создания базы. Settings-VM собирает список таких фаз
- /// и передаёт его в Progress-VM, который выполняет их последовательно.
+ /// Один шаг пайплайна создания базы
///
public sealed class DbCreationPhase {
public string Title { get; }
- public Func> Action { get; }
+ public Func Action { get; }
- public DbCreationPhase(string title, Func> action) {
+ public DbCreationPhase(string title, Func action) {
Title = title ?? throw new ArgumentNullException(nameof(title));
Action = action ?? throw new ArgumentNullException(nameof(action));
}
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index 880714f9c..7565a4b0d 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -1,5 +1,4 @@
using Dapper;
-using FluentNHibernate.Cfg.Db;
using MySqlConnector;
using QS.DbManagement.Responces;
using QS.Project.Versioning;
@@ -29,7 +28,7 @@ public class MariaDBProvider : IDbProvider {
public bool CanCreateDatabase { get; private set; }
///
- /// Переданный в тайтл созданой базы,
+ /// Переданный в тайтл созданой базы,
/// нужен потом при применения скрипта с наполнением базы
///
public string CreatedTitle { get; private set; }
diff --git a/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs b/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs
index 29b4805a3..47dd17c1e 100644
--- a/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs
+++ b/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs
@@ -1,8 +1,8 @@
using Microsoft.Extensions.DependencyInjection;
using QS.DBScripts;
-using QS.DBScripts.Controllers;
using QS.DBScripts.Models;
using QS.Utilities.Extensions;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
@@ -17,6 +17,16 @@ public MariaDbConnectionTypeBase() {
Parameters.Add(new ConnectionParameter("Server", "Адрес сервера"));
Parameters.Add(new ConnectionParameter("Login", "Пользователь"));
IconBytes = Assembly.GetExecutingAssembly().GetResourceByteArray("QS.DbManagement.Assets.mariadb.ico");
+
+
+ CreatorFactory = args => {
+ var p = (MariaDBProvider)args.Provider;
+ var scripts = args.ServiceProvider.GetRequiredService();
+ return new MySqlDbCreateModel(
+ p.ConnectionStringBuilder.ConnectionString,
+ scripts, args.Progress, args.Interaction, args.CancellationToken) { FillBaseGuid = false };
+ };
+
}
public override bool CanConnect(IEnumerable parameters) {
@@ -24,20 +34,7 @@ public override bool CanConnect(IEnumerable parameters
parameters.Any(p => p.Name == "Login" && !string.IsNullOrEmpty(p.Value));
}
- public override IDbCreatorModel CreatorFactory(CreatorFactoryArgs args){
- var provider = (MariaDBProvider)args.Provider;
- var scripts = args.ServiceProvider.GetRequiredService();
- var creator = new MySqlDbCreateModel(
- provider.ConnectionStringBuilder.ConnectionString,
- scripts,
- args.Progress,
- args.Interaction,
- args.CancellationToken);
- creator.FillBaseGuid = false;
- return creator;
- }
-
- public override IDbProvider CreateProvider(IList parameters, string password = null)
+ public override IDbProvider CreateProvider(IList parameters, string password = null)
=> new MariaDBProvider(parameters, password);
}
}
diff --git a/QS.Launcher.Avalonia/DependencyInjection.cs b/QS.Launcher.Avalonia/DependencyInjection.cs
index d5dce86ef..30848bc44 100644
--- a/QS.Launcher.Avalonia/DependencyInjection.cs
+++ b/QS.Launcher.Avalonia/DependencyInjection.cs
@@ -1,6 +1,6 @@
using Avalonia.Controls;
using Microsoft.Extensions.DependencyInjection;
-using QS.Launcher.Services;
+using QS.Dialog;
using QS.Launcher.Views;
using QS.Launcher.Views.Pages;
using QS.Launcher.Views.Pages.DataBase;
@@ -14,6 +14,6 @@ public static IServiceCollection AddPages(this IServiceCollection services) {
.AddSingleton()
.AddTransient()
.AddTransient()
- .AddSingleton();
+ .AddSingleton();
}
}
diff --git a/QS.Launcher.Avalonia/Services/AvaloniaUiThreadInvoker.cs b/QS.Launcher.Avalonia/Services/AvaloniaUiThreadInvoker.cs
deleted file mode 100644
index 6ac1bc79e..000000000
--- a/QS.Launcher.Avalonia/Services/AvaloniaUiThreadInvoker.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System;
-using Avalonia.Threading;
-using QS.Launcher.Services;
-
-namespace QS.Launcher.Services {
- public class AvaloniaUiThreadInvoker : IUiThreadInvoker {
- public void Post(Action action) {
- if(action == null) return;
- Dispatcher.UIThread.Post(action);
- }
- }
-}
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml
index bb771c5f6..9ebc48b73 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml
@@ -16,7 +16,7 @@
-
+
{
if(e.Key == Key.Enter) {
TopLevel.GetTopLevel(this)?.FocusManager?.ClearFocus();
- if(DataContext is DataBasesVM vm)
- vm.ConnectCommand.Execute(null);
+ ViewModel.ConnectCommand.Execute(null);
}
};
}
@@ -56,7 +57,7 @@ public void Label_PointerPressed(object? sender, PointerPressedEventArgs e) {
}
private void Databases_OnDoubleTapped(object? sender, TappedEventArgs e) {
- if(databases.SelectedItem is not null && DataContext is DataBasesVM vm)
- vm.ConnectCommand.Execute(null);
+ if(databases.SelectedItem is not null)
+ ViewModel.ConnectCommand.Execute(null);
}
}
diff --git a/QS.Launcher/QS.Launcher.csproj b/QS.Launcher/QS.Launcher.csproj
index 3481e8522..271188d35 100644
--- a/QS.Launcher/QS.Launcher.csproj
+++ b/QS.Launcher/QS.Launcher.csproj
@@ -20,7 +20,6 @@
-
diff --git a/QS.Launcher/Services/IUiThreadInvoker.cs b/QS.Launcher/Services/IUiThreadInvoker.cs
deleted file mode 100644
index b7801d342..000000000
--- a/QS.Launcher/Services/IUiThreadInvoker.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using System;
-
-namespace QS.Launcher.Services {
- ///
- /// проксирование действия в UI-поток
- ///
- public interface IUiThreadInvoker {
- /// Запланировать действие в UI-потоке без его блокировки
- void Post(Action action);
- }
-}
diff --git a/QS.Launcher/Services/LauncherDbCreatorInteraction.cs b/QS.Launcher/Services/LauncherDbCreatorInteraction.cs
index e68e95b79..9d0a11469 100644
--- a/QS.Launcher/Services/LauncherDbCreatorInteraction.cs
+++ b/QS.Launcher/Services/LauncherDbCreatorInteraction.cs
@@ -1,53 +1,28 @@
using System;
-using System.Threading.Tasks;
using QS.DBScripts.Controllers;
using QS.Dialog;
namespace QS.Launcher.Services {
- ///
- /// проксирует вопросы и ошибки
- /// все вызовы диалогов уходят в UI-поток через IUiThreadInvoker,
- /// потому что creator дёргает их с фонового потока
- ///
public class LauncherDbCreatorInteraction : IDbCreatorInteraction {
private readonly IInteractiveQuestion question;
private readonly IInteractiveMessage message;
- private readonly IUiThreadInvoker uiThread;
public LauncherDbCreatorInteraction(
IInteractiveQuestion question,
- IInteractiveMessage message,
- IUiThreadInvoker uiThread)
+ IInteractiveMessage message)
{
this.question = question ?? throw new ArgumentNullException(nameof(question));
this.message = message ?? throw new ArgumentNullException(nameof(message));
- this.uiThread = uiThread ?? throw new ArgumentNullException(nameof(uiThread));
}
- public Task AskDropExistingDatabaseAsync(string dbName) {
- var tcs = new TaskCompletionSource();
- uiThread.Post(async () => {
- try {
- bool answer = await question.QuestionAsync(
- $"База с именем `{dbName}` уже существует на сервере. Удалить существующую базу перед созданием новой?",
- "Создание базы данных");
- tcs.TrySetResult(answer);
- }
- catch(Exception ex) { tcs.TrySetException(ex); }
- });
- return tcs.Task;
+ public bool AskDropExistingDatabase(string dbName) {
+ return question.Question(
+ $"База с именем `{dbName}` уже существует на сервере. Удалить существующую базу перед созданием новой?",
+ "Создание базы данных");
}
- public Task ReportErrorAsync(string text, string lastExecutedStatement) {
- var tcs = new TaskCompletionSource();
- uiThread.Post(() => {
- try {
- message.ShowMessage(ImportanceLevel.Error, text, "Ошибка создания базы");
- tcs.TrySetResult(true);
- }
- catch(Exception ex) { tcs.TrySetException(ex); }
- });
- return tcs.Task;
+ public void ReportError(string text, string lastExecutedStatement) {
+ message.ShowMessage(ImportanceLevel.Error, text, "Ошибка создания базы");
}
}
}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
index 5a59100a0..a679d57ed 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
@@ -7,14 +7,9 @@
using QS.DbManagement;
using QS.DBScripts.Controllers;
using QS.Dialog;
-using QS.Launcher.Services;
using ReactiveUI;
namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
- ///
- /// показывает прогресс создания базы, прогресс приходит из не-UI потока, поэтому все мутации
- /// reactive-свойств проксируются через IUiThreadInvoker
- ///
public class CreateDataBaseProgressVM : CarouselPageVM, IProgressBarDisplayable {
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
@@ -24,10 +19,10 @@ public class CreateDataBaseProgressVM : CarouselPageVM, IProgressBarDisplayable
private readonly IDbCreatorInteraction interaction;
private readonly IServiceProvider services;
- private readonly IUiThreadInvoker uiThread;
+ private readonly IGuiDispatcher guiDispatcher;
private readonly CancellationTokenSource cts;
- #region IProgressBarDisplayable backed properties
+ #region IProgressBarDisplayable поля
private double minValue;
private double maxValue = 1;
@@ -58,10 +53,7 @@ public bool IsStarted {
#endregion
- /// Поднимается, когда база успешно создана, на него должен быть подписан DataBasesVM
public event Action DatabaseCreated;
-
- /// Поднимается, когда создание завершилось отменой
public event Action DatabaseCreationFailed;
public ReactiveCommand StartCreationCommand { get; }
@@ -69,11 +61,11 @@ public bool IsStarted {
public CreateDataBaseProgressVM(
IDbCreatorInteraction interaction,
- IUiThreadInvoker uiThread,
+ IGuiDispatcher guiDispatcher,
IServiceProvider services)
{
this.interaction = interaction ?? throw new ArgumentNullException(nameof(interaction));
- this.uiThread = uiThread ?? throw new ArgumentNullException(nameof(uiThread));
+ this.guiDispatcher = guiDispatcher ?? throw new ArgumentNullException(nameof(guiDispatcher));
this.services = services ?? throw new ArgumentNullException(nameof(services));
cts = new CancellationTokenSource();
@@ -95,6 +87,9 @@ public void SetPipeline(
throw new ArgumentException("Пайплайн создания базы пуст.", nameof(phases));
}
+ ///
+ /// выносим всю синхронную цепочку фаз в пул, чтобы UI поток оставался свободным для перерисовки прогрессбара
+ ///
public async Task StartCreationAsync() {
try {
var args = new CreatorFactoryArgs {
@@ -105,18 +100,12 @@ public async Task StartCreationAsync() {
ServiceProvider = services
};
- for(int i = 0; i < phases.Count; i++) {
- cts.Token.ThrowIfCancellationRequested();
- var phase = phases[i];
- uiThread.Post(() => CurrentText = phase.Title);
-
- bool ok = await phase.Action(args);
- if(!ok) {
- DatabaseCreationFailed?.Invoke();
- return;
- }
- }
- DatabaseCreated?.Invoke();
+ bool success = await Task.Run(() => RunPipeline(args), cts.Token);
+
+ if(success)
+ DatabaseCreated?.Invoke();
+ else
+ DatabaseCreationFailed?.Invoke();
}
catch(OperationCanceledException) {
logger.Info("Создание базы отменено.");
@@ -124,14 +113,23 @@ public async Task StartCreationAsync() {
}
catch(Exception ex) {
logger.Error(ex, "Сбой в процессе создания базы.");
- await interaction.ReportErrorAsync(ex.Message, null);
+ interaction.ReportError(ex.Message, null);
DatabaseCreationFailed?.Invoke();
}
}
+ private bool RunPipeline(CreatorFactoryArgs args) {
+ foreach(var phase in phases) {
+ args.CancellationToken.ThrowIfCancellationRequested();
+ guiDispatcher.RunInGuiTread(() => CurrentText = phase.Title);
+ if(!phase.Action(args))
+ return false;
+ }
+ return true;
+ }
- #region IProgressBarDisplayable
+ #region IProgressBarDisplayable методы
public void Start(double maxValue = 1, double minValue = 0, string text = null, double startValue = 0) {
- uiThread.Post(() => {
+ guiDispatcher.RunInGuiTread(() => {
MaxValue = maxValue;
MinValue = minValue;
Value = startValue;
@@ -140,20 +138,20 @@ public void Start(double maxValue = 1, double minValue = 0, string text = null,
});
}
- public void Update(double curValue) => uiThread.Post(() => Value = curValue);
+ public void Update(double curValue) => guiDispatcher.RunInGuiTread(() => Value = curValue);
- public void UpdateMax(double maxValue) => uiThread.Post(() => MaxValue = maxValue);
+ public void UpdateMax(double maxValue) => guiDispatcher.RunInGuiTread(() => MaxValue = maxValue);
- public void Update(string curText) => uiThread.Post(() => CurrentText = curText);
+ public void Update(string curText) => guiDispatcher.RunInGuiTread(() => CurrentText = curText);
public void Add(double addValue = 1, string text = null) {
- uiThread.Post(() => {
+ guiDispatcher.RunInGuiTread(() => {
Value += addValue;
if(text != null) CurrentText = text;
});
}
- public void Close() => uiThread.Post(() => IsStarted = false);
+ public void Close() => guiDispatcher.RunInGuiTread(() => IsStarted = false);
#endregion
}
}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
index 9957dc2f4..02e73dc81 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
@@ -1,7 +1,6 @@
using System;
using System.Reactive;
using System.Reactive.Linq;
-using System.Threading.Tasks;
using QS.DbManagement;
using QS.DBScripts.Controllers;
using ReactiveUI;
@@ -55,12 +54,12 @@ private void GoToProgress() {
var pipeline = new[] {
new DbCreationPhase(
"Создание базы данных",
- (args) => Task.FromResult(args.Provider.CreateDatabase(DbName, DbTitle, services))),
+ args => args.Provider.CreateDatabase(DbName, DbTitle, services)),
new DbCreationPhase(
"Наполнение базы данных",
- async args => {
+ args => {
IDbCreatorModel creator = Connection.ConnectionType.CreateCreator(args);
- return await creator.RunCreationAsync(DbName, DbTitle);
+ return creator.RunCreation(DbName, DbTitle);
})
};
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
index 047307e6e..f288d3c2f 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
@@ -25,11 +25,23 @@ public IDbProvider Provider {
this.RaiseAndSetIfChanged(ref provider, value);
Databases = provider.GetUserDatabases(applicationInfo).AsList();
this.RaisePropertyChanged(nameof(Databases));
+ this.RaisePropertyChanged(nameof(CanCreateDatabase));
LoadLastSelectedDatabase();
}
}
+ ///
+ /// можно создать базу только если:
+ /// есть права пользователя на создание
+ /// тип подключения поддерживает создание в текущем окружении
+ /// задана фабрика и зарегистрирован скрипт создания
+ ///
+ public bool CanCreateDatabase =>
+ provider != null
+ && provider.CanCreateDatabase
+ && currentConnection?.ConnectionType?.SupportsDatabaseCreation(serviceProvider) == true;
+
public Connection CurrentConnection => currentConnection;
public void SetProvider(IDbProvider dbProvider, Connection connection, Action saveConnections) {
@@ -94,7 +106,7 @@ public DataBasesVM(
/// создаёт возвращает фокус на и обновляет список баз
///
private void OpenCreateDatabase() {
- if(provider == null || currentConnection == null)
+ if(!CanCreateDatabase)
return;
var settings = ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider);
@@ -147,6 +159,9 @@ public void Connect() {
SaveLastSelectedDatabase();
+ // Определяем, нужно ли закрывать лаунчер через Shutdown
+ // В standalone режиме учитываем галочку ShouldCloseLauncherAfterStart
+ // В in-process режиме НЕ делаем shutdown (возвращаем false)
var isStandalone = launcherOptions?.IsStandalone ?? false;
logger.Info($">>> Connect: IsStandalone={isStandalone}, ShouldCloseLauncherAfterStart={ShouldCloseLauncherAfterStart}");
diff --git a/QS.LibsTest.Core/Launcher/ConfiguratorTest.cs b/QS.LibsTest.Core/Launcher/ConfiguratorTest.cs
index 48c3b3b8c..99a042658 100644
--- a/QS.LibsTest.Core/Launcher/ConfiguratorTest.cs
+++ b/QS.LibsTest.Core/Launcher/ConfiguratorTest.cs
@@ -430,8 +430,10 @@ public TestConnectionType(string name) {
public override IDbProvider CreateProvider(IList parameters, string password = null)
=> Substitute.For();
- public override IDbCreatorModel CreatorFactory(CreatorFactoryArgs args)
- => Substitute.For();
+ public TestConnectionType WithStubCreator() {
+ CreatorFactory = args => Substitute.For();
+ return this;
+ }
}
}
}
diff --git a/QS.Project.Core/DBScripts/Controllers/IDbCreatorInteraction.cs b/QS.Project.Core/DBScripts/Controllers/IDbCreatorInteraction.cs
index 3a499361a..f2c6c61e8 100644
--- a/QS.Project.Core/DBScripts/Controllers/IDbCreatorInteraction.cs
+++ b/QS.Project.Core/DBScripts/Controllers/IDbCreatorInteraction.cs
@@ -1,5 +1,3 @@
-using System.Threading.Tasks;
-
namespace QS.DBScripts.Controllers
{
///
@@ -7,8 +5,8 @@ namespace QS.DBScripts.Controllers
///
public interface IDbCreatorInteraction
{
- Task AskDropExistingDatabaseAsync(string dbName);
+ bool AskDropExistingDatabase(string dbName);
- Task ReportErrorAsync(string text, string lastExecutedStatement);
+ void ReportError(string text, string lastExecutedStatement);
}
}
diff --git a/QS.Project.Core/DBScripts/Controllers/IDbCreatorModel.cs b/QS.Project.Core/DBScripts/Controllers/IDbCreatorModel.cs
index 625b9dd3c..c562fac51 100644
--- a/QS.Project.Core/DBScripts/Controllers/IDbCreatorModel.cs
+++ b/QS.Project.Core/DBScripts/Controllers/IDbCreatorModel.cs
@@ -1,12 +1,12 @@
-using System.Threading.Tasks;
-
namespace QS.DBScripts.Controllers
{
///
- /// Низкоуровневая модель создания БД: знает, как физически создать и наполнить базу
+ /// Низкоуровневая модель наполнения БД для конкретного движка
///
public interface IDbCreatorModel
{
- Task RunCreationAsync(string dbName, string dbTitle);
+ // Метод блокирует вызывающий поток на время работы с базой
+ // Вынесение в фоновый поток — ответственность вызывающего кода
+ bool RunCreation(string dbName, string dbTitle);
}
}
diff --git a/QS.Project.Core/Dialog/IInteractiveQuestion.cs b/QS.Project.Core/Dialog/IInteractiveQuestion.cs
index 49f9c698d..51384f120 100644
--- a/QS.Project.Core/Dialog/IInteractiveQuestion.cs
+++ b/QS.Project.Core/Dialog/IInteractiveQuestion.cs
@@ -20,22 +20,5 @@ public interface IInteractiveQuestion
/// Заголовок окна диалога
/// Вернет заголовок кнопки которую нажал пользователь. Если пользователь закроет диалог крестиком, вернется null.
string Question(string[] buttons, string message, string title = null);
-
- ///
- /// Отобразит диалог с вопросом пользователю и кнопками Да Нет.
- ///
- /// Сообщение диалога
- /// Заголовок окна диалога
- /// True - Да, False - Нет(или закрытие крестиком)
- Task QuestionAsync(string message, string title = null);
-
- ///
- /// Отобразит диалог с вопросом пользователю.
- ///
- /// Список заголовков для кнопок
- /// Сообщение диалога
- /// Заголовок окна диалога
- /// Вернет заголовок кнопки которую нажал пользователь. Если пользователь закроет диалог крестиком, вернется null.
- Task QuestionAsync(string[] buttons, string message, string title = null);
}
}
diff --git a/QS.Project.Gtk/Project.Services.GtkUI/GtkInteractiveService.cs b/QS.Project.Gtk/Project.Services.GtkUI/GtkInteractiveService.cs
index e44d53802..d086b26c7 100644
--- a/QS.Project.Gtk/Project.Services.GtkUI/GtkInteractiveService.cs
+++ b/QS.Project.Gtk/Project.Services.GtkUI/GtkInteractiveService.cs
@@ -26,13 +26,5 @@ public string Question(string[] buttons, string message, string title = null)
{
return interactiveQuestion.Question(buttons, message, title);
}
-
- public Task QuestionAsync(string message, string title = null) {
- throw new System.NotImplementedException();
- }
-
- public Task QuestionAsync(string[] buttons, string message, string title = null) {
- throw new System.NotImplementedException();
- }
}
}
diff --git a/QS.Project/Project.Services/Interactive/ConsoleInteractiveQuestion.cs b/QS.Project/Project.Services/Interactive/ConsoleInteractiveQuestion.cs
index 3ca6bc1f2..87d40cc74 100644
--- a/QS.Project/Project.Services/Interactive/ConsoleInteractiveQuestion.cs
+++ b/QS.Project/Project.Services/Interactive/ConsoleInteractiveQuestion.cs
@@ -37,13 +37,5 @@ public string Question(string[] buttons, string message, string title = null)
}
return null;
}
-
- public Task QuestionAsync(string message, string title = null) {
- throw new NotImplementedException();
- }
-
- public Task QuestionAsync(string[] buttons, string message, string title = null) {
- throw new NotImplementedException();
- }
}
}
diff --git a/QS.Project/Project.Services/Interactive/ConsoleInteractiveService.cs b/QS.Project/Project.Services/Interactive/ConsoleInteractiveService.cs
index 8f3b69424..879365700 100644
--- a/QS.Project/Project.Services/Interactive/ConsoleInteractiveService.cs
+++ b/QS.Project/Project.Services/Interactive/ConsoleInteractiveService.cs
@@ -23,18 +23,9 @@ public bool Question(string message, string title = null)
{
return interactiveQuestion.Question(message, title);
}
-
public string Question(string[] buttons, string message, string title = null)
{
return interactiveQuestion.Question(buttons, message, title);
}
-
- public Task QuestionAsync(string message, string title = null) {
- throw new System.NotImplementedException();
- }
-
- public Task QuestionAsync(string[] buttons, string message, string title = null) {
- throw new System.NotImplementedException();
- }
}
}
diff --git a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
index fb4a00696..f1b8c2df1 100644
--- a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
+++ b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
@@ -4,7 +4,6 @@
using System;
using System.Text.RegularExpressions;
using System.Threading;
-using System.Threading.Tasks;
namespace QS.DBScripts.Models
{
@@ -57,12 +56,6 @@ public MySqlDbCreateModel(
}
- public Task RunCreationAsync(string dbName, string dbTitle = null) {
- // Тяжёлая часть с MySqlScript.Execute синхронная,
- // поэтому уносим её на пул, чтобы не блокировать UI-поток
- return Task.Run(() => RunCreation(dbName, dbTitle), cancellationToken);
- }
-
public bool RunCreation(string dbName, string dbTitle = null) {
using(var connectionDB = new MySqlConnection(connectionString)) {
try {
@@ -78,7 +71,7 @@ public bool RunCreation(string dbName, string dbTitle = null) {
using(var rdr = cmd.ExecuteReader()) {
while(rdr.Read()) {
if(rdr[0].ToString() == dbName) {
- if(interaction.AskDropExistingDatabaseAsync(dbName).GetAwaiter().GetResult()) {
+ if(interaction.AskDropExistingDatabase(dbName)) {
needDropBase = true;
}
hasBase = true;
@@ -146,8 +139,6 @@ public bool RunCreation(string dbName, string dbTitle = null) {
}
catch(InvalidCastException ex) { //FIXME Временный для более адекватного обхода проблемы с отсутствием поддержки MariaDB 10.10. Удалить как починим работу с этой версией.
logger.Error(ex, "Ошибка подключения к серверу.");
- interaction.ReportErrorAsync("Работа с MariaDB 10.10 пока не поддерживается. Установите версию MariaDB 10.9.", lastExecutedStatement)
- .GetAwaiter().GetResult();
return false;
}
catch(MySqlException ex) {
@@ -159,7 +150,7 @@ public bool RunCreation(string dbName, string dbTitle = null) {
text = "Не удалось подключиться к серверу БД.";
else
text = ex.Message;
- interaction.ReportErrorAsync(text, lastExecutedStatement).GetAwaiter().GetResult();
+ interaction.ReportError(text, lastExecutedStatement);
return false;
}
finally {
From d20c4985455c5e2dd05e9296a561802c35f8379b Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Wed, 10 Jun 2026 20:58:38 +0300
Subject: [PATCH 014/135] Update MySqlDbCreateModel.cs
---
QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
index f1b8c2df1..c7e7f1ea6 100644
--- a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
+++ b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
@@ -139,6 +139,7 @@ public bool RunCreation(string dbName, string dbTitle = null) {
}
catch(InvalidCastException ex) { //FIXME Временный для более адекватного обхода проблемы с отсутствием поддержки MariaDB 10.10. Удалить как починим работу с этой версией.
logger.Error(ex, "Ошибка подключения к серверу.");
+ interaction.ReportError("Работа с MariaDB 10.10 пока не поддерживается. Установите версию MariaDB 10.9.", lastExecutedStatement);
return false;
}
catch(MySqlException ex) {
From 41ba7a77c6883a910015c19761ca7d76ed60c128 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Thu, 11 Jun 2026 11:53:27 +0300
Subject: [PATCH 015/135] =?UTF-8?q?=D0=B4=D0=BE=D1=80=D0=B0=D0=B1=D0=BE?=
=?UTF-8?q?=D1=82=D0=B0=D0=BB=20=D0=BB=D0=B5=D0=B3=D0=B0=D1=81=D0=B8=20Use?=
=?UTF-8?q?rCreateDbController?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
IDbCreateController нигде не используется кроме UserCreateDbController.cs и тестов, так что его можно удалить
---
QS.Cloud.Client/DataBase/QsCloudDbCreator.cs | 2 +-
.../MariaDb/MariaDbConnectionTypeBase.cs | 2 +-
.../DbScriptsConfigurationAdapter.cs | 24 -------
.../DBScripts/Models/MySqlDbCreateModel.cs | 12 ++--
QS.Updater.DB.Gtk/UpdaterDBAutofacModule.cs | 13 ----
.../Controllers/UserCreateDbController.cs | 64 +++++++------------
6 files changed, 30 insertions(+), 87 deletions(-)
delete mode 100644 QS.Updater.Core/DBScripts/DbScriptsConfigurationAdapter.cs
diff --git a/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs b/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
index a11307579..4550edba8 100644
--- a/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
+++ b/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
@@ -60,7 +60,7 @@ public bool RunCreation(string dbName, string dbTitle) {
var creator = new MySqlDbCreateModel(
session.Db.Server, session.Db.Port, session.Db.Login, session.Db.Password,
- configuration, progress, interaction, cancellationToken);
+ configuration.MakeCreationScript(), progress, interaction, cancellationToken);
creator.FillBaseGuid = false;
bool success = creator.RunCreation(session.Db.BaseName, dbTitle);
diff --git a/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs b/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs
index 47dd17c1e..8b561cd6b 100644
--- a/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs
+++ b/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs
@@ -24,7 +24,7 @@ public MariaDbConnectionTypeBase() {
var scripts = args.ServiceProvider.GetRequiredService();
return new MySqlDbCreateModel(
p.ConnectionStringBuilder.ConnectionString,
- scripts, args.Progress, args.Interaction, args.CancellationToken) { FillBaseGuid = false };
+ scripts.MakeCreationScript(), args.Progress, args.Interaction, args.CancellationToken) { FillBaseGuid = false };
};
}
diff --git a/QS.Updater.Core/DBScripts/DbScriptsConfigurationAdapter.cs b/QS.Updater.Core/DBScripts/DbScriptsConfigurationAdapter.cs
deleted file mode 100644
index 905a75275..000000000
--- a/QS.Updater.Core/DBScripts/DbScriptsConfigurationAdapter.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System;
-using QS.DBScripts.Models;
-using QS.Updater.DB;
-
-namespace QS.DBScripts
-{
- public class DbScriptsConfigurationAdapter : IDbScriptsConfiguration
- {
- private readonly CreationScript creationScript;
- private readonly UpdateConfiguration updateConfiguration;
-
- public DbScriptsConfigurationAdapter(CreationScript creationScript, UpdateConfiguration updateConfiguration = null)
- {
- this.creationScript = creationScript ?? throw new ArgumentNullException(nameof(creationScript));
- this.updateConfiguration = updateConfiguration;
- }
-
- public bool HasCreationScript() => creationScript != null;
-
- public CreationScript MakeCreationScript() => creationScript;
-
- public UpdateConfiguration MakeUpdateConfiguration() => updateConfiguration ?? new UpdateConfiguration();
- }
-}
diff --git a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
index c7e7f1ea6..85e818350 100644
--- a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
+++ b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
@@ -12,7 +12,7 @@ public class MySqlDbCreateModel : IDbCreatorModel
static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly string connectionString;
- private readonly CreationScript scripts;
+ private readonly CreationScript script;
private readonly IProgressBarDisplayable progress;
private readonly IDbCreatorInteraction interaction;
private readonly CancellationToken cancellationToken;
@@ -21,7 +21,7 @@ public class MySqlDbCreateModel : IDbCreatorModel
public MySqlDbCreateModel(
string connectionString,
- IDbScriptsConfiguration scripts,
+ CreationScript script,
IProgressBarDisplayable progress,
IDbCreatorInteraction interaction,
CancellationToken cancellationToken)
@@ -29,7 +29,7 @@ public MySqlDbCreateModel(
if(string.IsNullOrWhiteSpace(connectionString))
throw new ArgumentException("Connection string is required", nameof(connectionString));
this.connectionString = connectionString;
- this.scripts = scripts.MakeCreationScript() ?? throw new ArgumentNullException(nameof(scripts));
+ this.script = script ?? throw new ArgumentNullException(nameof(script));
this.progress = progress ?? throw new ArgumentNullException(nameof(progress));
this.interaction = interaction ?? throw new ArgumentNullException(nameof(interaction));
this.cancellationToken = cancellationToken;
@@ -37,7 +37,7 @@ public MySqlDbCreateModel(
public MySqlDbCreateModel(
string server, uint port, string login, string password,
- IDbScriptsConfiguration scripts,
+ CreationScript script,
IProgressBarDisplayable progress,
IDbCreatorInteraction interaction,
CancellationToken cancellationToken) {
@@ -49,7 +49,7 @@ public MySqlDbCreateModel(
Password = password,
AllowUserVariables = true
}.ConnectionString;
- this.scripts = scripts.MakeCreationScript() ?? throw new ArgumentNullException(nameof(scripts));
+ this.script = script ?? throw new ArgumentNullException(nameof(script));
this.progress = progress ?? throw new ArgumentNullException(nameof(progress));
this.interaction = interaction ?? throw new ArgumentNullException(nameof(interaction));
this.cancellationToken = cancellationToken;
@@ -84,7 +84,7 @@ public bool RunCreation(string dbName, string dbTitle = null) {
progress.Start(text: "Получаем скрипт создания базы");
- string sqlScript = scripts.GetSqlScript();
+ string sqlScript = script.GetSqlScript();
int predictedCount = Regex.Matches(sqlScript, ";").Count;
logger.Debug("Предполагаем наличие {0} команд в скрипте.", predictedCount);
diff --git a/QS.Updater.DB.Gtk/UpdaterDBAutofacModule.cs b/QS.Updater.DB.Gtk/UpdaterDBAutofacModule.cs
index 971241817..4f89c9b4f 100644
--- a/QS.Updater.DB.Gtk/UpdaterDBAutofacModule.cs
+++ b/QS.Updater.DB.Gtk/UpdaterDBAutofacModule.cs
@@ -1,7 +1,5 @@
using Autofac;
-using QS.DBScripts;
using QS.DBScripts.Controllers;
-using QS.DBScripts.Models;
using QS.Updater.DB;
using QS.Updater.DB.ViewModels;
using QS.ViewModels;
@@ -14,22 +12,11 @@ protected override void Load(ContainerBuilder builder)
{
builder.RegisterType().As();
#region Models
- builder.RegisterType().AsSelf();
builder.RegisterType().AsSelf();
#endregion
#region Desktop
builder.RegisterType().As();
#endregion
- builder.Register(c => {
- var creation = c.Resolve();
- UpdateConfiguration updates = null;
- if(c.IsRegistered())
- updates = c.Resolve();
- return new DbScriptsConfigurationAdapter(creation, updates);
- })
- .As()
- .SingleInstance()
- .PreserveExistingDefaults();
#region ViewModels
builder.RegisterAssemblyTypes(System.Reflection.Assembly.GetAssembly(typeof(UpdateProcessViewModel)))
.Where(t => t.IsAssignableTo() && t.Name.EndsWith("ViewModel"))
diff --git a/QS.Updater.DB/DBScripts/Controllers/UserCreateDbController.cs b/QS.Updater.DB/DBScripts/Controllers/UserCreateDbController.cs
index 531b8d3ce..81b9217d7 100644
--- a/QS.Updater.DB/DBScripts/Controllers/UserCreateDbController.cs
+++ b/QS.Updater.DB/DBScripts/Controllers/UserCreateDbController.cs
@@ -1,6 +1,5 @@
using System;
using System.Threading;
-using System.Threading.Tasks;
using QS.DBScripts.Models;
using QS.DBScripts.ViewModels;
using QS.Dialog;
@@ -16,18 +15,18 @@ public class UserCreateDbController : IDBCreator, IDbCreatorInteraction
private readonly INavigationManager navigation;
private readonly IInteractiveService interactive;
private readonly IGuiDispatcher guiDispatcher;
- private readonly IDbScriptsConfiguration scripts;
+ private readonly CreationScript creationScript;
public UserCreateDbController(
INavigationManager navigation,
IInteractiveService interactive,
IGuiDispatcher guiDispatcher,
- IDbScriptsConfiguration scripts)
+ CreationScript creationScript)
{
this.navigation = navigation ?? throw new ArgumentNullException(nameof(navigation));
this.interactive = interactive ?? throw new ArgumentNullException(nameof(interactive));
this.guiDispatcher = guiDispatcher ?? throw new ArgumentNullException(nameof(guiDispatcher));
- this.scripts = scripts ?? throw new ArgumentNullException(nameof(scripts));
+ this.creationScript = creationScript ?? throw new ArgumentNullException(nameof(creationScript));
}
public void RunCreation(string server, string dbname)
@@ -41,36 +40,29 @@ public void RunCreation(string server, string dbname)
};
}
- async void StartCreation(string server, string dbname, string login, string password)
+ void StartCreation(string server, string dbname, string login, string password)
{
- ParseServer(server, out string host, out uint port);
-
- bool success = false;
try {
+ ParseServer(server, out string host, out uint port);
+
var createModel = new MySqlDbCreateModel(
host, port, login, password,
- scripts,
+ creationScript,
Progress,
interaction: this,
cancellationToken: CancellationToken.None);
- success = await createModel.RunCreationAsync(dbname, dbTitle: null);
+ bool success = createModel.RunCreation(dbname);
+ if(success)
+ interactive.ShowMessage(ImportanceLevel.Info, "Создание базы успешно завершено.\nЗайдите в программу под администратором для добавления пользователей.");
}
catch(Exception ex) {
logger.Error(ex, "Ошибка создания базы.");
- guiDispatcher.RunInGuiTread(() => interactive.ShowMessage(ImportanceLevel.Error, ex.Message));
+ interactive.ShowMessage(ImportanceLevel.Error, ex.Message);
}
finally {
- guiDispatcher.RunInGuiTread(() => {
- if(progressPage != null)
- navigation.ForceClosePage(progressPage, CloseSource.FromParentPage);
- });
- }
-
- if(success) {
- guiDispatcher.RunInGuiTread(() =>
- interactive.ShowMessage(ImportanceLevel.Info,
- "Создание базы успешно завершено.\nЗайдите в программу под администратором для добавления пользователей."));
+ if(progressPage != null)
+ navigation.ForceClosePage(progressPage, CloseSource.FromParentPage);
}
}
@@ -85,33 +77,21 @@ private static void ParseServer(string server, out string host, out uint port) {
}
#region IDbCreatorInteraction
+ //Создание идет в GUI-потоке (как и раньше), прогресс сам прокачивает событийный цикл,
+ //поэтому диалоги показываем напрямую.
- public Task AskDropExistingDatabaseAsync(string dbName) {
- var tcs = new TaskCompletionSource();
- guiDispatcher.RunInGuiTread(() => {
- try {
- tcs.SetResult(interactive.Question(
- $"База с именем `{dbName}` уже существует на сервере. Удалить существующую базу перед созданием новой?"));
- }
- catch(Exception ex) { tcs.SetException(ex); }
- });
- return tcs.Task;
+ public bool AskDropExistingDatabase(string dbName)
+ {
+ return interactive.Question($"База с именем `{dbName}` уже существует на сервере. Удалить существующую базу перед созданием новой?");
}
- public Task ReportErrorAsync(string text, string lastExecutedStatement) {
- var tcs = new TaskCompletionSource();
- guiDispatcher.RunInGuiTread(() => {
- try {
- interactive.ShowMessage(ImportanceLevel.Error, text);
- tcs.SetResult(true);
- }
- catch(Exception ex) { tcs.SetException(ex); }
- });
- return tcs.Task;
+ public void ReportError(string text, string lastExecutedStatement)
+ {
+ interactive.ShowMessage(ImportanceLevel.Error, text);
}
#endregion
- #region Свойства процесса
+ #region Progress page
IPage progressPage;
public IProgressBarDisplayable Progress {
From c9c4b79caf73e92e8fb4a1a2a6fc472222494bb6 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Thu, 11 Jun 2026 13:23:59 +0300
Subject: [PATCH 016/135] =?UTF-8?q?=D0=B2=D1=8B=D0=BD=D0=B5=D1=81=20=D1=82?=
=?UTF-8?q?=D0=B5=D1=81=D1=82=D0=BE=D0=B2=D1=83=D1=8E=20=D0=BA=D0=BE=D0=BD?=
=?UTF-8?q?=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D1=8E=20=D1=81?=
=?UTF-8?q?=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B1=D0=B0=D0=B7?=
=?UTF-8?q?=D1=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../DataBase/QsCloudScriptsConfiguration.cs | 38 -
QS.Cloud.Client/Scripts/new_empty.sql | 779 ------------------
2 files changed, 817 deletions(-)
delete mode 100644 QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs
delete mode 100644 QS.Cloud.Client/Scripts/new_empty.sql
diff --git a/QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs b/QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs
deleted file mode 100644
index 163e42421..000000000
--- a/QS.Cloud.Client/DataBase/QsCloudScriptsConfiguration.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using QS.DBScripts;
-using QS.DBScripts.Models;
-using QS.Updater.DB;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
-using System.Text;
-
-namespace QS.Cloud.Client.DataBase {
- public class QsCloudScriptsConfiguration : IDbScriptsConfiguration {
- private string ResourceName = "QS.Cloud.Client.Scripts.new_empty.sql";
- public bool HasCreationScript() {
- return Assembly.GetAssembly(typeof(QsCloudScriptsConfiguration))
- .GetManifestResourceNames()
- .Contains(ResourceName);
- }
-
- public CreationScript MakeCreationScript() {
- return new CreationScript(
- Assembly.GetAssembly(typeof(QsCloudScriptsConfiguration)),
- ResourceName,
- new Version(1, 7)
- );
- }
-
- public UpdateConfiguration MakeUpdateConfiguration() {
- var configuration = new UpdateConfiguration();
-
- configuration.AddUpdate(
- new Version(1, 7),
- new Version(1, 7, 1),
- "QS.Cloud.Client.Scripts.1.7.sql");
-
- return configuration;
- }
- }
-}
diff --git a/QS.Cloud.Client/Scripts/new_empty.sql b/QS.Cloud.Client/Scripts/new_empty.sql
deleted file mode 100644
index b25952e11..000000000
--- a/QS.Cloud.Client/Scripts/new_empty.sql
+++ /dev/null
@@ -1,779 +0,0 @@
--- phpMyAdmin SQL Dump
--- version 5.0.4deb2~bpo10+1
--- https://www.phpmyadmin.net/
---
--- Host: demeter.srv.qsolution.ru
--- Generation Time: Apr 30, 2026 at 01:52 PM
--- Server version: 10.3.39-MariaDB-0+deb10u2
--- PHP Version: 7.3.31-1~deb10u7
-
-SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
-START TRANSACTION;
-SET time_zone = "+00:00";
-
-
-/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
-/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
-/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
-/*!40101 SET NAMES utf8mb4 */;
-
---
--- Database: `QSService`
---
-
--- --------------------------------------------------------
-
---
--- Table structure for table `accounts`
---
-
-CREATE TABLE `accounts` (
- `id` int(10) UNSIGNED NOT NULL,
- `login` varchar(20) NOT NULL,
- `client_id` int(10) UNSIGNED DEFAULT NULL,
- `customer` varchar(50) NOT NULL,
- `email` varchar(50) DEFAULT NULL,
- `paid_until` date DEFAULT NULL,
- `notify_by_days` int(11) DEFAULT NULL COMMENT 'Уведомить за Н дней до окончания',
- `bases_limit` int(10) UNSIGNED NOT NULL DEFAULT 1,
- `users_limit` int(10) UNSIGNED NOT NULL DEFAULT 3,
- `space_limit` int(10) UNSIGNED NOT NULL DEFAULT 500,
- `deactivated` tinyint(1) NOT NULL DEFAULT 0
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- --------------------------------------------------------
-
---
--- Table structure for table `api_tokens`
---
-
-CREATE TABLE `api_tokens` (
- `id` int(10) UNSIGNED NOT NULL,
- `base_id` int(10) UNSIGNED NOT NULL,
- `token` char(36) NOT NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- --------------------------------------------------------
-
---
--- Table structure for table `bases`
---
-
-CREATE TABLE `bases` (
- `id` int(10) UNSIGNED NOT NULL,
- `account_id` int(10) UNSIGNED NOT NULL,
- `server_id` int(10) UNSIGNED NOT NULL,
- `base_title` varchar(64) DEFAULT NULL COMMENT 'Русское название базы для пользователя',
- `base_name` varchar(45) NOT NULL,
- `product_id` int(10) UNSIGNED NOT NULL,
- `real_name` varchar(64) DEFAULT NULL,
- `base_guid` char(36) DEFAULT NULL,
- `wear_lk` tinyint(1) NOT NULL DEFAULT 0,
- `number_of_lk_client` int(11) DEFAULT 0,
- `claims_lk` tinyint(1) NOT NULL DEFAULT 0,
- `postomats` tinyint(1) NOT NULL DEFAULT 0,
- `catalog` tinyint(1) NOT NULL DEFAULT 0,
- `comments` text DEFAULT NULL,
- `ratings` tinyint(1) NOT NULL DEFAULT 0,
- `appointment_lk` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'включение предварительной записи',
- `washing_lk` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Включение отображения стирки в мобильном кабинете',
- `speccoin_lk` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Включение функциональности спецкойнов',
- `size_editing_lk` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'Редактирование размеров в мобилке',
- `size_editing_days_before` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Запрет изменения размеров за указанное количество дней до выдачи.',
- `postomat_email_notification` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Дублируют ли постоматы уведомления на Email.',
- `stock_availability_enable` tinyint(1) NOT NULL DEFAULT 0,
- `stock_availability_warehouse_id` int(10) UNSIGNED DEFAULT NULL COMMENT 'id склада по которому показывать наличие',
- `choice_nomenclature_lk` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Выбор номенклатур сотрудником'
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- --------------------------------------------------------
-
---
--- Table structure for table `bases_scripts`
---
-
-CREATE TABLE `bases_scripts` (
- `id` int(10) UNSIGNED NOT NULL,
- `product_id` int(10) UNSIGNED NOT NULL,
- `start_version` varchar(15) DEFAULT NULL,
- `end_version` varchar(15) NOT NULL,
- `script` mediumtext DEFAULT NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- --------------------------------------------------------
-
---
--- Table structure for table `base_access`
---
-
-CREATE TABLE `base_access` (
- `id` int(10) UNSIGNED NOT NULL,
- `user_id` int(10) UNSIGNED NOT NULL,
- `base_id` int(10) UNSIGNED NOT NULL,
- `admin` tinyint(1) NOT NULL DEFAULT 0,
- `read_only` tinyint(1) NOT NULL DEFAULT 0,
- `torpedo` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'База доступна в панели инструментов'
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- --------------------------------------------------------
-
---
--- Table structure for table `base_parameters`
---
-
-CREATE TABLE `base_parameters` (
- `name` varchar(20) NOT NULL,
- `str_value` varchar(100) DEFAULT NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
-
-
-INSERT INTO `base_parameters` (`name`, `str_value`) VALUES
-('ProductCode', '5'),
-('product_name', 'ClientManager'),
-('version', '1.7');
-
--- --------------------------------------------------------
-
---
--- Table structure for table `bug_reports`
---
-
-CREATE TABLE `bug_reports` (
- `id` int(10) UNSIGNED NOT NULL,
- `created` datetime DEFAULT NULL,
- `last_update` datetime DEFAULT NULL,
- `product_id` int(10) UNSIGNED NOT NULL,
- `edition` varchar(20) DEFAULT NULL,
- `version` varchar(16) NOT NULL,
- `fixed_in_version` varchar(16) DEFAULT NULL COMMENT 'Версия в которой баг пофикшен',
- `message` varchar(2000) DEFAULT NULL,
- `stack_trace` text DEFAULT NULL,
- `description` text DEFAULT NULL,
- `email` varchar(600) DEFAULT NULL,
- `count` int(10) UNSIGNED DEFAULT 1,
- `status` enum('New','InWork','NeedInfo','Rejected','Later','Known','Unreproducable','EndOfLife','Done') DEFAULT 'New',
- `comments` text DEFAULT NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- --------------------------------------------------------
-
---
--- Table structure for table `bug_reports_messages`
---
-
-CREATE TABLE `bug_reports_messages` (
- `id` int(10) UNSIGNED NOT NULL,
- `created` datetime NOT NULL,
- `bug_reports_id` int(10) UNSIGNED NOT NULL,
- `email` varchar(254) DEFAULT NULL,
- `user_name` varchar(60) DEFAULT NULL,
- `messages` text DEFAULT NULL,
- `db_name` varchar(60) DEFAULT NULL,
- `report_type` enum('User','Automatic','Known') NOT NULL DEFAULT 'User',
- `log_file` mediumtext DEFAULT NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- --------------------------------------------------------
-
---
--- Table structure for table `clients`
---
-
-CREATE TABLE `clients` (
- `id` int(10) UNSIGNED NOT NULL,
- `name` varchar(300) NOT NULL,
- `email` varchar(45) DEFAULT NULL,
- `email_notifications` varchar(200) DEFAULT NULL COMMENT 'Адреса для уведомлений',
- `city` varchar(45) DEFAULT NULL,
- `comments` text DEFAULT NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- --------------------------------------------------------
-
---
--- Table structure for table `cloud_users`
---
-
-CREATE TABLE `cloud_users` (
- `id` int(10) UNSIGNED NOT NULL,
- `login` varchar(20) NOT NULL,
- `name` varchar(80) DEFAULT NULL,
- `password` varchar(81) NOT NULL,
- `disabled` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Пользователь отключен',
- `is_account_admin` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Администратор учетной записи',
- `post` varchar(200) DEFAULT NULL COMMENT 'Должность',
- `phone` varchar(16) DEFAULT NULL COMMENT 'Телефон',
- `email` varchar(60) DEFAULT NULL,
- `account_id` int(10) UNSIGNED NOT NULL,
- `multi_ip` tinyint(1) NOT NULL DEFAULT 0,
- `client_id` int(10) UNSIGNED DEFAULT NULL,
- `comment` text DEFAULT NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- --------------------------------------------------------
-
---
--- Table structure for table `products`
---
-
-CREATE TABLE `products` (
- `id` int(10) UNSIGNED NOT NULL,
- `name` varchar(45) NOT NULL,
- `internal_name` varchar(45) NOT NULL,
- `not_support_ver_regexp` varchar(45) DEFAULT NULL,
- `telegram_notify` varchar(50) DEFAULT NULL COMMENT 'Отправлять уведомления о новых ошибках в чат'
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- --------------------------------------------------------
-
---
--- Table structure for table `product_editions`
---
-
-CREATE TABLE `product_editions` (
- `id` int(10) UNSIGNED NOT NULL,
- `product_id` int(10) UNSIGNED NOT NULL,
- `code_number` int(10) UNSIGNED DEFAULT NULL COMMENT 'Номер редакции, внутри продукта',
- `code_name` varchar(10) DEFAULT NULL COMMENT 'Кодовое имя редакции.',
- `name` varchar(100) DEFAULT NULL COMMENT 'Название редакции отображаемое для пользователя.'
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- --------------------------------------------------------
-
---
--- Table structure for table `product_versions`
---
-
-CREATE TABLE `product_versions` (
- `id` int(10) UNSIGNED NOT NULL,
- `product_id` int(10) UNSIGNED NOT NULL,
- `modification` varchar(25) DEFAULT NULL,
- `channel` enum('Current','Stable') NOT NULL DEFAULT 'Current',
- `disable` tinyint(1) NOT NULL DEFAULT 0,
- `version_major` int(10) UNSIGNED NOT NULL DEFAULT 0,
- `version_minor` int(10) UNSIGNED NOT NULL DEFAULT 0,
- `version_build` int(10) UNSIGNED NOT NULL DEFAULT 0,
- `version_revision` int(10) UNSIGNED NOT NULL DEFAULT 0,
- `date` date NOT NULL,
- `link_install` varchar(256) DEFAULT NULL,
- `link_news` varchar(256) DEFAULT NULL,
- `changes` text DEFAULT NULL,
- `db_update` enum('None','Required','BreakingChange') NOT NULL DEFAULT 'None',
- `comment` text DEFAULT NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- --------------------------------------------------------
-
---
--- Table structure for table `serial_numbers`
---
-
-CREATE TABLE `serial_numbers` (
- `id` int(10) UNSIGNED NOT NULL,
- `client_id` int(10) UNSIGNED NOT NULL,
- `number` varchar(50) NOT NULL,
- `recall` tinyint(1) NOT NULL DEFAULT 0,
- `notify_by_days` int(11) DEFAULT NULL COMMENT 'Уведомить за Н дней до окончания',
- `active_until` date DEFAULT NULL,
- `serial_expiry_date` date DEFAULT NULL COMMENT 'Дата окончания действия серийного номера',
- `instance` int(10) UNSIGNED NOT NULL DEFAULT 1,
- `comment` text DEFAULT NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- --------------------------------------------------------
-
---
--- Table structure for table `servers`
---
-
-CREATE TABLE `servers` (
- `id` int(10) UNSIGNED NOT NULL,
- `server_address` varchar(60) NOT NULL,
- `service_user` varchar(16) NOT NULL,
- `service_password` varchar(81) NOT NULL,
- `comment` text DEFAULT NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- --------------------------------------------------------
-
---
--- Table structure for table `sessions`
---
-
-CREATE TABLE `sessions` (
- `id` int(10) UNSIGNED NOT NULL,
- `session_id` varchar(36) NOT NULL,
- `user_id` int(10) UNSIGNED DEFAULT NULL,
- `account_id` int(10) UNSIGNED NOT NULL,
- `base_id` int(10) UNSIGNED NOT NULL,
- `start_time` datetime NOT NULL,
- `end_time` datetime NOT NULL,
- `is_closed` tinyint(1) NOT NULL DEFAULT 0,
- `login` varchar(40) DEFAULT NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- --------------------------------------------------------
-
---
--- Table structure for table `telemetry_statistics`
---
-
-CREATE TABLE `telemetry_statistics` (
- `id` int(10) UNSIGNED NOT NULL,
- `last_update` datetime NOT NULL,
- `ip_address` varchar(39) DEFAULT NULL,
- `product` varchar(20) NOT NULL,
- `edition` varchar(20) DEFAULT NULL,
- `version` varchar(20) NOT NULL,
- `os` varchar(100) DEFAULT NULL,
- `net_framework` varchar(100) DEFAULT NULL,
- `is_demo` tinyint(1) NOT NULL DEFAULT 0,
- `app_edition` int(10) UNSIGNED DEFAULT NULL COMMENT 'Редакция программы',
- `base_employees` int(10) UNSIGNED DEFAULT NULL COMMENT 'Количество сотрудников в базе',
- `counter` mediumtext NOT NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- --------------------------------------------------------
-
---
--- Table structure for table `update_info`
---
-
-CREATE TABLE `update_info` (
- `id` int(10) UNSIGNED NOT NULL,
- `product` varchar(25) NOT NULL,
- `edition` varchar(25) DEFAULT NULL,
- `serial_number` varchar(45) DEFAULT NULL,
- `start_version_major` int(10) UNSIGNED DEFAULT 0,
- `start_version_minor` int(10) UNSIGNED DEFAULT 0,
- `start_version_build` int(10) UNSIGNED DEFAULT 0,
- `start_version_revision` int(10) UNSIGNED DEFAULT 0,
- `new_version_major` int(10) UNSIGNED DEFAULT 0,
- `new_version_minor` int(10) UNSIGNED DEFAULT 0,
- `new_version_build` int(10) UNSIGNED DEFAULT 0,
- `new_version_revision` int(10) UNSIGNED DEFAULT 0,
- `link_install` varchar(256) NOT NULL,
- `link_news` varchar(256) DEFAULT NULL,
- `use_common` tinyint(1) DEFAULT 0,
- `update_description` text DEFAULT NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- --------------------------------------------------------
-
---
--- Table structure for table `update_statistics`
---
-
-CREATE TABLE `update_statistics` (
- `id` int(10) UNSIGNED NOT NULL,
- `product_id` int(10) UNSIGNED DEFAULT NULL,
- `edition` varchar(25) DEFAULT NULL,
- `serial_number` varchar(45) DEFAULT NULL,
- `client_version` varchar(16) NOT NULL,
- `new_version` varchar(16) DEFAULT NULL,
- `date` datetime NOT NULL DEFAULT utc_timestamp(),
- `client_ip` varchar(15) DEFAULT NULL,
- `channel` enum('Current','Stable') NOT NULL DEFAULT 'Current',
- `status` enum('NoUpdates','NeedUpdate','Expired','Recalled','LicenceNotFound') DEFAULT NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- --------------------------------------------------------
-
---
--- Table structure for table `users`
---
-
-CREATE TABLE `users` (
- `id` int(10) UNSIGNED NOT NULL,
- `name` varchar(45) NOT NULL,
- `login` varchar(45) NOT NULL,
- `deactivated` tinyint(1) NOT NULL DEFAULT 0,
- `email` varchar(60) DEFAULT NULL,
- `description` text DEFAULT NULL,
- `admin` tinyint(1) NOT NULL DEFAULT 0
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
---
--- Indexes for dumped tables
---
-
---
--- Indexes for table `accounts`
---
-ALTER TABLE `accounts`
- ADD PRIMARY KEY (`id`),
- ADD UNIQUE KEY `login_UNIQUE` (`login`),
- ADD KEY `fk_accounts_1_idx` (`client_id`);
-
---
--- Indexes for table `api_tokens`
---
-ALTER TABLE `api_tokens`
- ADD PRIMARY KEY (`id`),
- ADD UNIQUE KEY `token_UNIQUE` (`token`),
- ADD KEY `fk_api_tokens_1_idx` (`base_id`);
-
---
--- Indexes for table `bases`
---
-ALTER TABLE `bases`
- ADD PRIMARY KEY (`id`),
- ADD UNIQUE KEY `idx1_bases` (`base_name`,`account_id`),
- ADD UNIQUE KEY `base_guid_UNIQUE` (`base_guid`),
- ADD KEY `fk1_bases_idx` (`account_id`),
- ADD KEY `fk2_bases_idx` (`server_id`),
- ADD KEY `fk3_bases_idx` (`product_id`);
-
---
--- Indexes for table `bases_scripts`
---
-ALTER TABLE `bases_scripts`
- ADD PRIMARY KEY (`id`),
- ADD KEY `fk1_bases_scripts_idx` (`product_id`);
-
---
--- Indexes for table `base_access`
---
-ALTER TABLE `base_access`
- ADD PRIMARY KEY (`id`),
- ADD UNIQUE KEY `base_access_idx` (`user_id`,`base_id`),
- ADD KEY `fk1_base_access_idx` (`user_id`),
- ADD KEY `fk1_base_access_idx1` (`base_id`);
-
---
--- Indexes for table `base_parameters`
---
-ALTER TABLE `base_parameters`
- ADD PRIMARY KEY (`name`);
-
---
--- Indexes for table `bug_reports`
---
-ALTER TABLE `bug_reports`
- ADD PRIMARY KEY (`id`),
- ADD KEY `fk_bug_reports_1_idx` (`product_id`),
- ADD KEY `bug_reports_created` (`created`),
- ADD KEY `bug_reports_last_update` (`last_update`),
- ADD KEY `bug_reports_edition` (`edition`),
- ADD KEY `bug_reports_version` (`version`),
- ADD KEY `bug_reports_status` (`status`);
-
---
--- Indexes for table `bug_reports_messages`
---
-ALTER TABLE `bug_reports_messages`
- ADD PRIMARY KEY (`id`),
- ADD KEY `fk_bug_reports_messages_1_idx` (`bug_reports_id`);
-
---
--- Indexes for table `clients`
---
-ALTER TABLE `clients`
- ADD PRIMARY KEY (`id`);
-
---
--- Indexes for table `cloud_users`
---
-ALTER TABLE `cloud_users`
- ADD PRIMARY KEY (`id`),
- ADD UNIQUE KEY `idx1_users` (`login`,`account_id`),
- ADD KEY `fk1_users_idx` (`account_id`),
- ADD KEY `fk_cloud_users_1_idx` (`client_id`);
-
---
--- Indexes for table `products`
---
-ALTER TABLE `products`
- ADD PRIMARY KEY (`id`),
- ADD UNIQUE KEY `internal_name_UNIQUE` (`internal_name`);
-
---
--- Indexes for table `product_editions`
---
-ALTER TABLE `product_editions`
- ADD PRIMARY KEY (`id`),
- ADD KEY `fk_product_id_idx` (`product_id`);
-
---
--- Indexes for table `product_versions`
---
-ALTER TABLE `product_versions`
- ADD PRIMARY KEY (`id`),
- ADD KEY `_idx` (`product_id`);
-
---
--- Indexes for table `serial_numbers`
---
-ALTER TABLE `serial_numbers`
- ADD PRIMARY KEY (`id`),
- ADD UNIQUE KEY `number_UNIQUE` (`number`),
- ADD KEY `fk_serial_numbers_1_idx` (`client_id`);
-
---
--- Indexes for table `servers`
---
-ALTER TABLE `servers`
- ADD PRIMARY KEY (`id`);
-
---
--- Indexes for table `sessions`
---
-ALTER TABLE `sessions`
- ADD PRIMARY KEY (`id`),
- ADD UNIQUE KEY `session_id_UNIQUE` (`session_id`),
- ADD KEY `fk1_sessions_idx` (`account_id`),
- ADD KEY `fk2_sessions_idx` (`user_id`),
- ADD KEY `fk3_sessions_idx` (`base_id`),
- ADD KEY `end_time_idx` (`end_time`),
- ADD KEY `is_closed_idx` (`is_closed`);
-
---
--- Indexes for table `telemetry_statistics`
---
-ALTER TABLE `telemetry_statistics`
- ADD PRIMARY KEY (`id`),
- ADD KEY `index_telemetry_statistics_last_update` (`last_update`),
- ADD KEY `index_telemetry_statistics_ip` (`ip_address`),
- ADD KEY `index_telemetry_statistics_product` (`product`),
- ADD KEY `index_telemetry_statistics_edition` (`edition`),
- ADD KEY `index_telemetry_statistics_version` (`version`),
- ADD KEY `inxex_telemetry_statistics_os` (`os`);
-
---
--- Indexes for table `update_info`
---
-ALTER TABLE `update_info`
- ADD PRIMARY KEY (`id`);
-
---
--- Indexes for table `update_statistics`
---
-ALTER TABLE `update_statistics`
- ADD PRIMARY KEY (`id`),
- ADD UNIQUE KEY `id_UNIQUE` (`id`),
- ADD KEY `fk_update_statistics_1_idx` (`product_id`),
- ADD KEY `update_statistics_edition_idx` (`edition`),
- ADD KEY `update_statistics_serial_idx` (`serial_number`),
- ADD KEY `update_statistics_client_version_idx` (`client_version`),
- ADD KEY `update_statistics_ip_idx` (`client_ip`),
- ADD KEY `update_statistics_new_version_idx` (`new_version`),
- ADD KEY `update_statistics_date_idx` (`date`),
- ADD KEY `update_statistics_channel_idx` (`channel`);
-
---
--- Indexes for table `users`
---
-ALTER TABLE `users`
- ADD PRIMARY KEY (`id`);
-
---
--- AUTO_INCREMENT for dumped tables
---
-
---
--- AUTO_INCREMENT for table `accounts`
---
-ALTER TABLE `accounts`
- MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
-
---
--- AUTO_INCREMENT for table `api_tokens`
---
-ALTER TABLE `api_tokens`
- MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
-
---
--- AUTO_INCREMENT for table `bases`
---
-ALTER TABLE `bases`
- MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
-
---
--- AUTO_INCREMENT for table `bases_scripts`
---
-ALTER TABLE `bases_scripts`
- MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
-
---
--- AUTO_INCREMENT for table `base_access`
---
-ALTER TABLE `base_access`
- MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
-
---
--- AUTO_INCREMENT for table `bug_reports`
---
-ALTER TABLE `bug_reports`
- MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
-
---
--- AUTO_INCREMENT for table `bug_reports_messages`
---
-ALTER TABLE `bug_reports_messages`
- MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
-
---
--- AUTO_INCREMENT for table `clients`
---
-ALTER TABLE `clients`
- MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
-
---
--- AUTO_INCREMENT for table `cloud_users`
---
-ALTER TABLE `cloud_users`
- MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
-
---
--- AUTO_INCREMENT for table `products`
---
-ALTER TABLE `products`
- MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
-
---
--- AUTO_INCREMENT for table `product_editions`
---
-ALTER TABLE `product_editions`
- MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
-
---
--- AUTO_INCREMENT for table `product_versions`
---
-ALTER TABLE `product_versions`
- MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
-
---
--- AUTO_INCREMENT for table `serial_numbers`
---
-ALTER TABLE `serial_numbers`
- MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
-
---
--- AUTO_INCREMENT for table `servers`
---
-ALTER TABLE `servers`
- MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
-
---
--- AUTO_INCREMENT for table `sessions`
---
-ALTER TABLE `sessions`
- MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
-
---
--- AUTO_INCREMENT for table `telemetry_statistics`
---
-ALTER TABLE `telemetry_statistics`
- MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
-
---
--- AUTO_INCREMENT for table `update_info`
---
-ALTER TABLE `update_info`
- MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
-
---
--- AUTO_INCREMENT for table `update_statistics`
---
-ALTER TABLE `update_statistics`
- MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
-
---
--- AUTO_INCREMENT for table `users`
---
-ALTER TABLE `users`
- MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
-
---
--- Constraints for dumped tables
---
-
---
--- Constraints for table `accounts`
---
-ALTER TABLE `accounts`
- ADD CONSTRAINT `fk_accounts_1` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
-
---
--- Constraints for table `api_tokens`
---
-ALTER TABLE `api_tokens`
- ADD CONSTRAINT `fk_api_tokens_1` FOREIGN KEY (`base_id`) REFERENCES `bases` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
-
---
--- Constraints for table `bases`
---
-ALTER TABLE `bases`
- ADD CONSTRAINT `fk1_bases` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
- ADD CONSTRAINT `fk2_bases` FOREIGN KEY (`server_id`) REFERENCES `servers` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
- ADD CONSTRAINT `fk3_bases` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
-
---
--- Constraints for table `bases_scripts`
---
-ALTER TABLE `bases_scripts`
- ADD CONSTRAINT `fk1_bases_scripts` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
-
---
--- Constraints for table `base_access`
---
-ALTER TABLE `base_access`
- ADD CONSTRAINT `fk1_base_access` FOREIGN KEY (`user_id`) REFERENCES `cloud_users` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
- ADD CONSTRAINT `fk2_base_access` FOREIGN KEY (`base_id`) REFERENCES `bases` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
-
---
--- Constraints for table `bug_reports`
---
-ALTER TABLE `bug_reports`
- ADD CONSTRAINT `fk_bug_reports_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION;
-
---
--- Constraints for table `bug_reports_messages`
---
-ALTER TABLE `bug_reports_messages`
- ADD CONSTRAINT `fk_bug_reports_messages_1` FOREIGN KEY (`bug_reports_id`) REFERENCES `bug_reports` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-
---
--- Constraints for table `cloud_users`
---
-ALTER TABLE `cloud_users`
- ADD CONSTRAINT `fk1_users` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION,
- ADD CONSTRAINT `fk_cloud_users_1` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
-
---
--- Constraints for table `product_editions`
---
-ALTER TABLE `product_editions`
- ADD CONSTRAINT `fk_product_id` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
-
---
--- Constraints for table `product_versions`
---
-ALTER TABLE `product_versions`
- ADD CONSTRAINT `fk_product_versions_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-
---
--- Constraints for table `serial_numbers`
---
-ALTER TABLE `serial_numbers`
- ADD CONSTRAINT `fk_serial_numbers_1` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-
---
--- Constraints for table `sessions`
---
-ALTER TABLE `sessions`
- ADD CONSTRAINT `fk1_sessions` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
- ADD CONSTRAINT `fk2_sessions` FOREIGN KEY (`user_id`) REFERENCES `cloud_users` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
- ADD CONSTRAINT `fk3_sessions` FOREIGN KEY (`base_id`) REFERENCES `bases` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
-
---
--- Constraints for table `update_statistics`
---
-ALTER TABLE `update_statistics`
- ADD CONSTRAINT `fk_update_statistics_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
-COMMIT;
-
-/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
-/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
-/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
From 84669e9c6ee15aee0bdaf2d40a7ee629ae6571fa Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Thu, 11 Jun 2026 14:32:18 +0300
Subject: [PATCH 017/135] =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=BD=D1=83=D0=BB=20?=
=?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=80=D0=B8?=
=?UTF-8?q?=D0=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
QS.Project.Core/Dialog/IProgressBarDisplayable.cs | 3 +++
1 file changed, 3 insertions(+)
diff --git a/QS.Project.Core/Dialog/IProgressBarDisplayable.cs b/QS.Project.Core/Dialog/IProgressBarDisplayable.cs
index 8ed6f1770..f8041b878 100644
--- a/QS.Project.Core/Dialog/IProgressBarDisplayable.cs
+++ b/QS.Project.Core/Dialog/IProgressBarDisplayable.cs
@@ -1,6 +1,9 @@
using System;
namespace QS.Dialog
{
+ ///
+ /// Интерфейс позволяющий управлять прогресс баром не зависимо от графического тул кита.
+ ///
public interface IProgressBarDisplayable
{
void Start(double maxValue = 1, double minValue = 0, string text = null, double startValue = 0);
From 5f2ee3a66d2acdeaa7e6f4234bc4a6d768b76494 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Mon, 15 Jun 2026 15:44:31 +0300
Subject: [PATCH 018/135] =?UTF-8?q?=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?=
=?UTF-8?q?=D0=B8=D0=B5=20=D0=B8=20=D1=8D=D0=BA=D1=81=D0=BF=D0=BE=D1=80?=
=?UTF-8?q?=D1=82=20=D0=B1=D0=B0=D0=B7=D1=8B=20=D0=BD=D0=B0=D1=87=D0=B0?=
=?UTF-8?q?=D0=BB=D0=BE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
QS.DbManagement/MariaDb/MariaDBProvider.cs | 11 ++
.../MariaDb/MariaDbBackupService.cs | 70 +++++++++++
QS.DbManagement/QS.DbManagement.csproj | 1 +
.../QS.Launcher.Avalonia.csproj | 3 -
.../Views/Pages/BaseManagementView.axaml | 43 -------
.../Views/Pages/BaseManagementView.axaml.cs | 11 --
.../DataBase/CreateDataBaseProgressView.axaml | 2 +-
.../CreateDataBaseProgressView.axaml.cs | 2 +-
.../DataBase/CreateDataBaseSettingsView.axaml | 32 +++--
.../CreateDataBaseSettingsView.axaml.cs | 30 +++++
.../Views/Pages/DataBase/DataBasesView.axaml | 25 ++--
.../Pages/DataBase/DataBasesView.axaml.cs | 12 ++
QS.Launcher/DependencyInjection.cs | 3 +-
QS.Launcher/ViewModels/MainWindowVM.cs | 1 -
.../PageViewModels/BaseManagementVM.cs | 15 ---
.../DataBase/CreateDataBaseProgressVM.cs | 33 +++--
.../DataBase/CreateDataBaseSettingsVM.cs | 115 +++++++++++++++---
.../PageViewModels/DataBase/DataBasesVM.cs | 83 +++++++++++--
.../TypeViewModels/DatabaseViewModel.cs | 9 --
19 files changed, 358 insertions(+), 143 deletions(-)
create mode 100644 QS.DbManagement/MariaDb/MariaDbBackupService.cs
delete mode 100644 QS.Launcher.Avalonia/Views/Pages/BaseManagementView.axaml
delete mode 100644 QS.Launcher.Avalonia/Views/Pages/BaseManagementView.axaml.cs
delete mode 100644 QS.Launcher/ViewModels/PageViewModels/BaseManagementVM.cs
delete mode 100644 QS.Launcher/ViewModels/TypeViewModels/DatabaseViewModel.cs
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index 7565a4b0d..cec9469ba 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -1,11 +1,13 @@
using Dapper;
using MySqlConnector;
using QS.DbManagement.Responces;
+using QS.Dialog;
using QS.Project.Versioning;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
+using System.Threading;
namespace QS.DbManagement
{
@@ -188,6 +190,15 @@ public bool DropDatabase(string databaseName) {
return connection.Execute(sql) != 0;
}
+ ///
+ /// Резервное копирование базы в SQL-скрипт. Взаимодействие с базой идёт через провайдер,
+ /// а сам экспорт вынесен в .
+ /// Метод блокирующий - вызывать из фонового потока.
+ ///
+ public void BackupDatabase(string databaseName, string filePath, IProgressBarDisplayable progress, CancellationToken cancellation) {
+ new MariaDbBackupService().Backup(ConnectionStringBuilder, databaseName, filePath, progress, cancellation);
+ }
+
public void Dispose() {
connection?.Dispose();
}
diff --git a/QS.DbManagement/MariaDb/MariaDbBackupService.cs b/QS.DbManagement/MariaDb/MariaDbBackupService.cs
new file mode 100644
index 000000000..a8650fad5
--- /dev/null
+++ b/QS.DbManagement/MariaDb/MariaDbBackupService.cs
@@ -0,0 +1,70 @@
+using System;
+using System.IO;
+using System.Threading;
+using MySqlConnector;
+using QS.Dialog;
+
+namespace QS.DbManagement {
+ ///
+ /// Экспорт базы MariaDB/MySQL в SQL-скрипт через MySqlBackup.NET.
+ /// Вынесен отдельным сервисом, чтобы провайдер не держал логику бэкапа в себе.
+ ///
+ public class MariaDbBackupService {
+ private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
+
+ ///
+ /// Синхронно выгружает базу в файл .
+ /// Вызывать из фонового потока - метод блокирующий (как и MySqlBackup).
+ ///
+ public void Backup(
+ MySqlConnectionStringBuilder connectionSettings,
+ string databaseName,
+ string filePath,
+ IProgressBarDisplayable progress,
+ CancellationToken cancellation) {
+ if(connectionSettings == null)
+ throw new ArgumentNullException(nameof(connectionSettings));
+ if(string.IsNullOrWhiteSpace(databaseName))
+ throw new ArgumentException("Не указано имя базы для резервного копирования.", nameof(databaseName));
+ if(string.IsNullOrWhiteSpace(filePath))
+ throw new ArgumentException("Не указан путь к файлу резервной копии.", nameof(filePath));
+
+ // Отдельная строка подключения именно к выгружаемой базе - провайдер может быть подключён к другой.
+ var builder = new MySqlConnectionStringBuilder(connectionSettings.ConnectionString) {
+ Database = databaseName
+ };
+
+ var directory = Path.GetDirectoryName(filePath);
+ if(!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
+ Directory.CreateDirectory(directory);
+
+ logger.Info("Создаём резервную копию базы {0} в файл {1}", databaseName, filePath);
+
+ using(var connection = new MySqlConnection(builder.ConnectionString)) {
+ connection.Open();
+ using(var command = connection.CreateCommand())
+ using(var backup = new MySqlBackup(command)) {
+ bool started = false;
+ string currentTable = null;
+ backup.ExportProgressChanged += (sender, e) => {
+ if(cancellation.IsCancellationRequested) {
+ ((MySqlBackup)sender).StopAllProcess();
+ return;
+ }
+ if(!started) {
+ progress?.Start(maxValue: e.TotalRowsInAllTables, text: "Создание резервной копии");
+ started = true;
+ }
+ if(currentTable != e.CurrentTableName) {
+ currentTable = e.CurrentTableName;
+ progress?.Update($"Экспорт таблицы {currentTable}");
+ }
+ progress?.Update(e.CurrentRowIndexInAllTables);
+ };
+
+ backup.ExportToFile(filePath);
+ }
+ }
+ }
+ }
+}
diff --git a/QS.DbManagement/QS.DbManagement.csproj b/QS.DbManagement/QS.DbManagement.csproj
index f6c7779e5..bcbc1f23c 100644
--- a/QS.DbManagement/QS.DbManagement.csproj
+++ b/QS.DbManagement/QS.DbManagement.csproj
@@ -14,6 +14,7 @@
+
diff --git a/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj b/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj
index e8a347a5a..05c9a25da 100644
--- a/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj
+++ b/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj
@@ -47,9 +47,6 @@
LauncherApp.axaml
-
- BaseManagementView.axaml
-
LoginView.axaml
diff --git a/QS.Launcher.Avalonia/Views/Pages/BaseManagementView.axaml b/QS.Launcher.Avalonia/Views/Pages/BaseManagementView.axaml
deleted file mode 100644
index b779d92a1..000000000
--- a/QS.Launcher.Avalonia/Views/Pages/BaseManagementView.axaml
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/QS.Launcher.Avalonia/Views/Pages/BaseManagementView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/BaseManagementView.axaml.cs
deleted file mode 100644
index e6332d279..000000000
--- a/QS.Launcher.Avalonia/Views/Pages/BaseManagementView.axaml.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Avalonia.Controls;
-using QS.Launcher.ViewModels.PageViewModels;
-
-namespace QS.Launcher.Views.Pages;
-
-public partial class BaseManagementView : UserControl {
- public BaseManagementView(BaseManagementVM viewModel) {
- InitializeComponent();
- DataContext = viewModel;
- }
-}
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml
index ee33ff10a..03a75c017 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml
@@ -10,7 +10,7 @@
Loaded="OnLoaded"
mc:Ignorable="d">
-
+
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml.cs
index e2ffde338..e54aea154 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml.cs
@@ -16,6 +16,6 @@ private void OnLoaded(object? sender, RoutedEventArgs e) {
cogwheel.Classes.Add("rolled");
if(DataContext is CreateDataBaseProgressVM vm)
- vm.StartCreationCommand.Execute().Subscribe();
+ vm.StartCommand.Execute().Subscribe();
}
}
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml
index 0bd1897bd..f0bc3cf0f 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml
@@ -8,18 +8,34 @@
d:DesignWidth="450"
x:DataType="vm:CreateDataBaseSettingsVM"
mc:Ignorable="d">
-
-
+
-
-
+
+
-
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml.cs
index fc36e41c4..11974694e 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml.cs
@@ -1,4 +1,7 @@
+using System.IO;
using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Platform.Storage;
using QS.Launcher.ViewModels.PageViewModels.DataBase;
namespace QS.Launcher.Views.Pages.DataBase;
@@ -9,4 +12,31 @@ public CreateDataBaseSettingsView(CreateDataBaseSettingsVM settingsVM) {
DataContext = settingsVM;
}
+
+ private async void BrowseBackupFile_OnClick(object? sender, RoutedEventArgs e) {
+ if(DataContext is not CreateDataBaseSettingsVM vm)
+ return;
+
+ var topLevel = TopLevel.GetTopLevel(this);
+ if(topLevel == null)
+ return;
+
+ var options = new FilePickerSaveOptions {
+ Title = "Сохранить резервную копию",
+ DefaultExtension = "sql",
+ SuggestedFileName = Path.GetFileName(vm.BackupFilePath),
+ FileTypeChoices = new[] { new FilePickerFileType("SQL-скрипт") { Patterns = new[] { "*.sql" } } }
+ };
+
+ var directory = Path.GetDirectoryName(vm.BackupFilePath);
+ if(!string.IsNullOrEmpty(directory)) {
+ var folder = await topLevel.StorageProvider.TryGetFolderFromPathAsync(directory);
+ if(folder != null)
+ options.SuggestedStartLocation = folder;
+ }
+
+ var file = await topLevel.StorageProvider.SaveFilePickerAsync(options);
+ if(file != null)
+ vm.BackupFilePath = file.Path.LocalPath;
+ }
}
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml
index 9ebc48b73..46481e656 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml
@@ -4,9 +4,11 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:QS.Launcher.ViewModels.PageViewModels;assembly=QS.Launcher"
+ xmlns:vmdb="clr-namespace:QS.Launcher.ViewModels.PageViewModels.DataBase;assembly=QS.Launcher"
+ xmlns:dbm="clr-namespace:QS.DbManagement;assembly=QS.DbManagement"
d:DesignHeight="650"
d:DesignWidth="450"
- x:DataType="vm:DataBase.DataBasesVM"
+ x:DataType="vmdb:DataBasesVM"
mc:Ignorable="d">
@@ -22,10 +24,20 @@
SelectedItem="{Binding SelectedDatabase}"
DoubleTapped="Databases_OnDoubleTapped">
-
+
-
-
+
+
+
@@ -81,11 +93,6 @@
-
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml.cs
index 4598a6b2b..56c9019ee 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml.cs
@@ -2,6 +2,8 @@
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input;
+using Avalonia.Interactivity;
+using QS.DbManagement;
using QS.Launcher.ViewModels.PageViewModels.DataBase;
using System.Linq;
using System.Threading.Tasks;
@@ -60,4 +62,14 @@ private void Databases_OnDoubleTapped(object? sender, TappedEventArgs e) {
if(databases.SelectedItem is not null)
ViewModel.ConnectCommand.Execute(null);
}
+
+ private void BackupDatabase_OnClick(object? sender, RoutedEventArgs e) {
+ if((sender as Control)?.DataContext is DbInfo database)
+ ViewModel.BackupDatabaseCommand.Execute(database);
+ }
+
+ private void DeleteDatabase_OnClick(object? sender, RoutedEventArgs e) {
+ if((sender as Control)?.DataContext is DbInfo database)
+ ViewModel.DeleteDatabaseCommand.Execute(database);
+ }
}
diff --git a/QS.Launcher/DependencyInjection.cs b/QS.Launcher/DependencyInjection.cs
index af7b67634..07bbf750b 100644
--- a/QS.Launcher/DependencyInjection.cs
+++ b/QS.Launcher/DependencyInjection.cs
@@ -16,8 +16,7 @@ public static IServiceCollection AddLauncherViewModels(this IServiceCollection s
.AddSingleton()
.AddSingleton()
.AddSingleton()
- .AddSingleton()
- // Wizard-страницы создания БД
+ // Wizard-страницы создания БД и операций с базой
.AddSingleton()
.AddSingleton()
.AddSingleton();
diff --git a/QS.Launcher/ViewModels/MainWindowVM.cs b/QS.Launcher/ViewModels/MainWindowVM.cs
index 6039da0c0..5061791fd 100644
--- a/QS.Launcher/ViewModels/MainWindowVM.cs
+++ b/QS.Launcher/ViewModels/MainWindowVM.cs
@@ -29,7 +29,6 @@ public int PagesCount {
public MainWindowVM(
DataBasesVM dataBasesVM,
LoginVM loginVM,
- BaseManagementVM baseManagementVM,
UserManagementVM userManagementVM,
IServiceProvider provider)
{
diff --git a/QS.Launcher/ViewModels/PageViewModels/BaseManagementVM.cs b/QS.Launcher/ViewModels/PageViewModels/BaseManagementVM.cs
deleted file mode 100644
index 035ef2690..000000000
--- a/QS.Launcher/ViewModels/PageViewModels/BaseManagementVM.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using QS.Launcher.ViewModels.TypeViewModels;
-using System.Collections.Generic;
-
-namespace QS.Launcher.ViewModels.PageViewModels {
- public class BaseManagementVM : CarouselPageVM {
-
- public List Databases { get; set; }
-
- public DatabaseViewModel SelectedDatabase { get; set; }
-
- public BaseManagementVM() {
-
- }
- }
-}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
index a679d57ed..65192bd5b 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
@@ -10,6 +10,10 @@
using ReactiveUI;
namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
+ ///
+ /// Универсальная страница прогресса: последовательно выполняет пайплайн фаз
+ /// (создание базы, наполнение, резервное копирование и т.п.) в одном фоновом потоке.
+ ///
public class CreateDataBaseProgressVM : CarouselPageVM, IProgressBarDisplayable {
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
@@ -17,6 +21,13 @@ public class CreateDataBaseProgressVM : CarouselPageVM, IProgressBarDisplayable
public Connection Connection { get; private set; }
private IReadOnlyList phases = Array.Empty();
+ private string operationTitle = "Создание базы данных";
+ /// Заголовок страницы - задаётся настройками под конкретную операцию.
+ public string OperationTitle {
+ get => operationTitle;
+ set => this.RaiseAndSetIfChanged(ref operationTitle, value);
+ }
+
private readonly IDbCreatorInteraction interaction;
private readonly IServiceProvider services;
private readonly IGuiDispatcher guiDispatcher;
@@ -53,10 +64,10 @@ public bool IsStarted {
#endregion
- public event Action DatabaseCreated;
- public event Action DatabaseCreationFailed;
+ public event Action OperationCompleted;
+ public event Action OperationFailed;
- public ReactiveCommand StartCreationCommand { get; }
+ public ReactiveCommand StartCommand { get; }
public ReactiveCommand CancelCommand { get; }
public CreateDataBaseProgressVM(
@@ -69,7 +80,7 @@ public CreateDataBaseProgressVM(
this.services = services ?? throw new ArgumentNullException(nameof(services));
cts = new CancellationTokenSource();
- StartCreationCommand = ReactiveCommand.CreateFromTask(StartCreationAsync);
+ StartCommand = ReactiveCommand.CreateFromTask(RunAsync);
CancelCommand = ReactiveCommand.Create(() => {
cts.Cancel();
PopToRootCommand?.Execute(null);
@@ -90,7 +101,7 @@ public void SetPipeline(
///
/// выносим всю синхронную цепочку фаз в пул, чтобы UI поток оставался свободным для перерисовки прогрессбара
///
- public async Task StartCreationAsync() {
+ public async Task RunAsync() {
try {
var args = new CreatorFactoryArgs {
Provider = Provider,
@@ -103,18 +114,18 @@ public async Task StartCreationAsync() {
bool success = await Task.Run(() => RunPipeline(args), cts.Token);
if(success)
- DatabaseCreated?.Invoke();
+ OperationCompleted?.Invoke();
else
- DatabaseCreationFailed?.Invoke();
+ OperationFailed?.Invoke();
}
catch(OperationCanceledException) {
- logger.Info("Создание базы отменено.");
- DatabaseCreationFailed?.Invoke();
+ logger.Info("Операция с базой отменена.");
+ OperationFailed?.Invoke();
}
catch(Exception ex) {
- logger.Error(ex, "Сбой в процессе создания базы.");
+ logger.Error(ex, "Сбой в процессе выполнения операции с базой.");
interaction.ReportError(ex.Message, null);
- DatabaseCreationFailed?.Invoke();
+ OperationFailed?.Invoke();
}
}
private bool RunPipeline(CreatorFactoryArgs args) {
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
index 02e73dc81..aa7e69a62 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
@@ -1,16 +1,44 @@
using System;
+using System.IO;
using System.Reactive;
using System.Reactive.Linq;
+using Microsoft.Extensions.DependencyInjection;
using QS.DbManagement;
using QS.DBScripts.Controllers;
using ReactiveUI;
namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
+ public enum DbWizardOperation {
+ Create,
+ Backup
+ }
+
+ ///
+ /// Универсальная страница настроек операции с базой: ввод параметров создания базы
+ /// либо выбор файла для резервной копии. Конкретную операцию строит .
+ ///
public class CreateDataBaseSettingsVM : CarouselPageVM {
public IDbProvider Provider { get; private set; }
public Connection Connection { get; private set; }
private readonly IServiceProvider services;
+ private DbInfo backupTarget;
+
+ private DbWizardOperation operation = DbWizardOperation.Create;
+ public DbWizardOperation Operation {
+ get => operation;
+ private set {
+ this.RaiseAndSetIfChanged(ref operation, value);
+ this.RaisePropertyChanged(nameof(IsCreateMode));
+ this.RaisePropertyChanged(nameof(IsBackupMode));
+ }
+ }
+
+ public bool IsCreateMode => Operation == DbWizardOperation.Create;
+ public bool IsBackupMode => Operation == DbWizardOperation.Backup;
+
+ #region Создание
+
private string dbTitle;
public string DbTitle {
get => dbTitle;
@@ -23,7 +51,25 @@ public string DbName {
set => this.RaiseAndSetIfChanged(ref dbName, value);
}
- public ReactiveCommand CreateDataBaseCommand { get; }
+ #endregion
+
+ #region Резервная копия
+
+ private string backupTargetTitle;
+ public string BackupTargetTitle {
+ get => backupTargetTitle;
+ private set => this.RaiseAndSetIfChanged(ref backupTargetTitle, value);
+ }
+
+ private string backupFilePath;
+ public string BackupFilePath {
+ get => backupFilePath;
+ set => this.RaiseAndSetIfChanged(ref backupFilePath, value);
+ }
+
+ #endregion
+
+ public ReactiveCommand ProceedCommand { get; }
public ReactiveCommand CancelCommand { get; }
///
@@ -35,33 +81,66 @@ public string DbName {
public CreateDataBaseSettingsVM(IServiceProvider services) {
this.services = services ?? throw new ArgumentNullException(nameof(services));
- var canCreate = this.WhenAnyValue(x => x.DbName, x => x.DbTitle,
- (name, title) => !string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(title));
+ var canProceed = this.WhenAnyValue(
+ x => x.Operation, x => x.DbName, x => x.DbTitle, x => x.BackupFilePath,
+ (op, name, title, path) => op == DbWizardOperation.Create
+ ? !string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(title)
+ : !string.IsNullOrWhiteSpace(path));
- CreateDataBaseCommand = ReactiveCommand.Create(GoToProgress, canCreate);
+ ProceedCommand = ReactiveCommand.Create(GoToProgress, canProceed);
CancelCommand = ReactiveCommand.Create(() => PopPageCommand?.Execute(null));
}
public void SetDbSettings(IDbProvider provider, Connection connection) {
Provider = provider ?? throw new ArgumentNullException(nameof(provider));
Connection = connection ?? throw new ArgumentNullException(nameof(connection));
+ Operation = DbWizardOperation.Create;
+ }
+
+ public void SetBackupSettings(IDbProvider provider, Connection connection, DbInfo database) {
+ Provider = provider ?? throw new ArgumentNullException(nameof(provider));
+ Connection = connection ?? throw new ArgumentNullException(nameof(connection));
+ backupTarget = database ?? throw new ArgumentNullException(nameof(database));
+ Operation = DbWizardOperation.Backup;
+
+ BackupTargetTitle = database.Title;
+ BackupFilePath = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
+ "Резервные копии",
+ string.Format("{0}-{1:yyMMdd-HHmm}.sql", database.BaseName, DateTime.Now));
}
private void GoToProgress() {
- var progress = Microsoft.Extensions.DependencyInjection.ActivatorUtilities
- .GetServiceOrCreateInstance(services);
-
- var pipeline = new[] {
- new DbCreationPhase(
- "Создание базы данных",
- args => args.Provider.CreateDatabase(DbName, DbTitle, services)),
- new DbCreationPhase(
- "Наполнение базы данных",
- args => {
- IDbCreatorModel creator = Connection.ConnectionType.CreateCreator(args);
- return creator.RunCreation(DbName, DbTitle);
- })
- };
+ var progress = ActivatorUtilities.GetServiceOrCreateInstance(services);
+
+ DbCreationPhase[] pipeline;
+ if(Operation == DbWizardOperation.Backup) {
+ progress.OperationTitle = "Создание резервной копии базы данных";
+ pipeline = new[] {
+ new DbCreationPhase(
+ "Создание резервной копии базы данных",
+ args => {
+ ((MariaDBProvider)args.Provider).BackupDatabase(
+ backupTarget.BaseName, BackupFilePath, args.Progress, args.CancellationToken);
+ args.CancellationToken.ThrowIfCancellationRequested();
+ return true;
+ })
+ };
+ }
+ else {
+ progress.OperationTitle = "Создание базы данных";
+ pipeline = new[] {
+ new DbCreationPhase(
+ "Создание базы данных",
+ args => args.Provider.CreateDatabase(DbName, DbTitle, services)),
+ new DbCreationPhase(
+ "Наполнение базы данных",
+ args => {
+ IDbCreatorModel creator = Connection.ConnectionType.CreateCreator(args);
+ return creator.RunCreation(DbName, DbTitle);
+ })
+ };
+ }
progress.SetPipeline(Provider, Connection, pipeline);
ProgressPageRequested?.Invoke(progress);
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
index f288d3c2f..ef3b2baa7 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
@@ -26,6 +26,7 @@ public IDbProvider Provider {
Databases = provider.GetUserDatabases(applicationInfo).AsList();
this.RaisePropertyChanged(nameof(Databases));
this.RaisePropertyChanged(nameof(CanCreateDatabase));
+ this.RaisePropertyChanged(nameof(CanManageDatabases));
LoadLastSelectedDatabase();
}
@@ -42,6 +43,12 @@ public IDbProvider Provider {
&& provider.CanCreateDatabase
&& currentConnection?.ConnectionType?.SupportsDatabaseCreation(serviceProvider) == true;
+ ///
+ /// Управление базой (резервная копия, удаление) пока поддержано только для MariaDB-провайдера,
+ /// облако реализуем отдельно.
+ ///
+ public bool CanManageDatabases => provider is MariaDBProvider;
+
public Connection CurrentConnection => currentConnection;
public void SetProvider(IDbProvider dbProvider, Connection connection, Action saveConnections) {
@@ -72,10 +79,13 @@ public DbInfo SelectedDatabase {
public ICommand ConnectCommand { get; }
public ReactiveCommand OpenCreateDatabaseCommand { get; }
+ public ICommand BackupDatabaseCommand { get; }
+ public ICommand DeleteDatabaseCommand { get; }
public event Action StartLaunchProgram;
IInteractiveMessage interactiveMessage;
+ private readonly IInteractiveQuestion interactiveQuestion;
private readonly IServiceProvider serviceProvider;
private readonly IAppRunner appRunner;
@@ -85,12 +95,14 @@ public DataBasesVM(
IAppRunner appRunner,
IApplicationInfo applicationInfo,
IInteractiveMessage interactiveMessage,
+ IInteractiveQuestion interactiveQuestion,
LauncherOptions launcherOptions,
IServiceProvider serviceProvider)
{
this.appRunner = appRunner ?? throw new ArgumentNullException(nameof(appRunner));
this.applicationInfo = applicationInfo ?? throw new ArgumentNullException(nameof(applicationInfo));
this.interactiveMessage = interactiveMessage ?? throw new ArgumentNullException(nameof(interactiveMessage));
+ this.interactiveQuestion = interactiveQuestion ?? throw new ArgumentNullException(nameof(interactiveQuestion));
this.launcherOptions = launcherOptions;
this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
@@ -100,32 +112,81 @@ public DataBasesVM(
ConnectCommand = ReactiveCommand.Create(Connect, canExecuteConnection);
OpenCreateDatabaseCommand = ReactiveCommand.Create(OpenCreateDatabase);
+ BackupDatabaseCommand = ReactiveCommand.Create(OpenBackup);
+ DeleteDatabaseCommand = ReactiveCommand.CreateFromTask(DeleteDatabaseAsync);
}
///
- /// создаёт возвращает фокус на и обновляет список баз
+ /// Мастер настроек/прогресса един для всех операций с базой - резолвим один раз
+ /// и один раз подписываемся на завершение операции (без накопления подписок).
+ ///
+ private CreateDataBaseSettingsVM settingsWizard;
+ private CreateDataBaseSettingsVM SettingsWizard {
+ get {
+ if(settingsWizard == null) {
+ settingsWizard = ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider);
+ settingsWizard.ProgressPageRequested += progressVm => {
+ progressVm.OperationCompleted -= OnWizardOperationCompleted;
+ progressVm.OperationCompleted += OnWizardOperationCompleted;
+ };
+ }
+ return settingsWizard;
+ }
+ }
+
+ ///
+ /// открывает мастер создания базы, по завершении возвращает фокус на и обновляет список баз
///
private void OpenCreateDatabase() {
if(!CanCreateDatabase)
return;
- var settings = ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider);
- settings.SetDbSettings(Provider, CurrentConnection);
+ SettingsWizard.SetDbSettings(Provider, CurrentConnection);
+ PushPageCommand?.Execute(SettingsWizard);
+ }
- settings.ProgressPageRequested += progressVm => {
- progressVm.DatabaseCreated += OnDatabaseCreatedFromWizard;
- progressVm.DatabaseCreationFailed += () => {
- // пользователь сам решит вернуться или попробовать снова
- };
- };
+ ///
+ /// открывает мастер резервного копирования выбранной базы
+ ///
+ private void OpenBackup(DbInfo database) {
+ if(database == null || !CanManageDatabases)
+ return;
- PushPageCommand?.Execute(settings);
+ SettingsWizard.SetBackupSettings(Provider, CurrentConnection, database);
+ PushPageCommand?.Execute(SettingsWizard);
}
- private void OnDatabaseCreatedFromWizard() {
+ private void OnWizardOperationCompleted() {
// Закрываем все wizard-страницы и возвращаемся на DataBasesVM.
PopToRootCommand?.Execute(null);
RefreshDatabases();
+
+ if(settingsWizard?.Operation == DbWizardOperation.Backup)
+ interactiveMessage.ShowMessage(ImportanceLevel.Success,
+ $"Резервная копия базы данных сохранена:\n{settingsWizard.BackupFilePath}",
+ "Резервное копирование");
+ }
+
+ private async System.Threading.Tasks.Task DeleteDatabaseAsync(DbInfo database) {
+ if(database == null || !CanManageDatabases)
+ return;
+
+ // Question() кидает исключение на UI-потоке, поэтому диалог и удаление выполняем в фоне.
+ bool confirmed = await System.Threading.Tasks.Task.Run(() => interactiveQuestion.Question(
+ $"Безвозвратно удалить базу данных «{database.Title}»?", "Удаление базы данных"));
+ if(!confirmed)
+ return;
+
+ try {
+ await System.Threading.Tasks.Task.Run(() => provider.DropDatabase(database.BaseName));
+ RefreshDatabases();
+ interactiveMessage.ShowMessage(ImportanceLevel.Success,
+ $"База данных «{database.Title}» удалена.", "Удаление базы данных");
+ }
+ catch(Exception ex) {
+ logger.Error(ex, "Не удалось удалить базу {0}", database.BaseName);
+ interactiveMessage.ShowMessage(ImportanceLevel.Error, ex.Message, "Ошибка удаления базы данных");
+ }
}
public void RefreshDatabases() {
diff --git a/QS.Launcher/ViewModels/TypeViewModels/DatabaseViewModel.cs b/QS.Launcher/ViewModels/TypeViewModels/DatabaseViewModel.cs
deleted file mode 100644
index b9a9db3b4..000000000
--- a/QS.Launcher/ViewModels/TypeViewModels/DatabaseViewModel.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace QS.Launcher.ViewModels.TypeViewModels {
- public class DatabaseViewModel {
- public string Name { get; set; }
-
- public string ConnectionString { get; set; }
-
- public string Size { get; set; }
- }
-}
From fb66aef468f4f58a4e31cd4cb7446f75cdf065cf Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Mon, 15 Jun 2026 15:54:56 +0300
Subject: [PATCH 019/135] =?UTF-8?q?=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=20?=
=?UTF-8?q?=D0=B1=D0=B0=D0=B7=D1=8B=20=D0=B8=D0=B7=20sql=20=D1=81=D0=BA?=
=?UTF-8?q?=D1=80=D0=B8=D0=BF=D1=82=D0=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
QS.DbManagement/MariaDb/MariaDBProvider.cs | 9 +++
.../MariaDb/MariaDbImportService.cs | 64 +++++++++++++++++++
.../DataBase/CreateDataBaseSettingsView.axaml | 6 ++
.../CreateDataBaseSettingsView.axaml.cs | 19 ++++++
.../DataBase/CreateDataBaseSettingsVM.cs | 43 +++++++++++--
.../PageViewModels/DataBase/DataBasesVM.cs | 10 +--
6 files changed, 140 insertions(+), 11 deletions(-)
create mode 100644 QS.DbManagement/MariaDb/MariaDbImportService.cs
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index cec9469ba..924990007 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -199,6 +199,15 @@ public void BackupDatabase(string databaseName, string filePath, IProgressBarDis
new MariaDbBackupService().Backup(ConnectionStringBuilder, databaseName, filePath, progress, cancellation);
}
+ ///
+ /// Импорт SQL-дампа в уже созданную базу. Взаимодействие с базой идёт через провайдер,
+ /// а сам импорт вынесен в .
+ /// Метод блокирующий - вызывать из фонового потока.
+ ///
+ public void ImportDatabase(string databaseName, string filePath, IProgressBarDisplayable progress, CancellationToken cancellation) {
+ new MariaDbImportService().Import(ConnectionStringBuilder, databaseName, filePath, progress, cancellation);
+ }
+
public void Dispose() {
connection?.Dispose();
}
diff --git a/QS.DbManagement/MariaDb/MariaDbImportService.cs b/QS.DbManagement/MariaDb/MariaDbImportService.cs
new file mode 100644
index 000000000..e7643a2c1
--- /dev/null
+++ b/QS.DbManagement/MariaDb/MariaDbImportService.cs
@@ -0,0 +1,64 @@
+using System;
+using System.IO;
+using System.Threading;
+using MySqlConnector;
+using QS.Dialog;
+
+namespace QS.DbManagement {
+ ///
+ /// Импорт SQL-дампа в существующую базу MariaDB/MySQL через MySqlBackup.NET.
+ /// Симметричен . Вынесен отдельным сервисом,
+ /// чтобы провайдер не держал логику работы с дампом в себе.
+ ///
+ public class MariaDbImportService {
+ private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
+
+ ///
+ /// Синхронно заливает дамп в базу .
+ /// База должна уже существовать. Вызывать из фонового потока - метод блокирующий.
+ ///
+ public void Import(
+ MySqlConnectionStringBuilder connectionSettings,
+ string databaseName,
+ string filePath,
+ IProgressBarDisplayable progress,
+ CancellationToken cancellation) {
+ if(connectionSettings == null)
+ throw new ArgumentNullException(nameof(connectionSettings));
+ if(string.IsNullOrWhiteSpace(databaseName))
+ throw new ArgumentException("Не указано имя базы для импорта дампа.", nameof(databaseName));
+ if(string.IsNullOrWhiteSpace(filePath))
+ throw new ArgumentException("Не указан путь к файлу дампа.", nameof(filePath));
+ if(!File.Exists(filePath))
+ throw new FileNotFoundException("Файл дампа не найден.", filePath);
+
+ // Отдельная строка подключения именно к целевой базе - провайдер может смотреть в другую.
+ var builder = new MySqlConnectionStringBuilder(connectionSettings.ConnectionString) {
+ Database = databaseName
+ };
+
+ logger.Info("Импортируем дамп {0} в базу {1}", filePath, databaseName);
+
+ using(var connection = new MySqlConnection(builder.ConnectionString)) {
+ connection.Open();
+ using(var command = connection.CreateCommand())
+ using(var backup = new MySqlBackup(command)) {
+ bool started = false;
+ backup.ImportProgressChanged += (sender, e) => {
+ if(cancellation.IsCancellationRequested) {
+ ((MySqlBackup)sender).StopAllProcess();
+ return;
+ }
+ if(!started) {
+ progress?.Start(maxValue: e.TotalBytes, text: "Импорт дампа в базу данных");
+ started = true;
+ }
+ progress?.Update(e.CurrentBytes);
+ };
+
+ backup.ImportFromFile(filePath);
+ }
+ }
+ }
+ }
+}
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml
index f0bc3cf0f..dbb08c025 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml
@@ -18,6 +18,12 @@
+
+
+
+
+
+
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml.cs
index 11974694e..a3c3d11df 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml.cs
@@ -39,4 +39,23 @@ private async void BrowseBackupFile_OnClick(object? sender, RoutedEventArgs e) {
if(file != null)
vm.BackupFilePath = file.Path.LocalPath;
}
+
+ private async void BrowseImportFile_OnClick(object? sender, RoutedEventArgs e) {
+ if(DataContext is not CreateDataBaseSettingsVM vm)
+ return;
+
+ var topLevel = TopLevel.GetTopLevel(this);
+ if(topLevel == null)
+ return;
+
+ var options = new FilePickerOpenOptions {
+ Title = "Выбрать дамп базы данных",
+ AllowMultiple = false,
+ FileTypeFilter = new[] { new FilePickerFileType("SQL-скрипт") { Patterns = new[] { "*.sql" } } }
+ };
+
+ var files = await topLevel.StorageProvider.OpenFilePickerAsync(options);
+ if(files.Count > 0)
+ vm.ImportDumpFilePath = files[0].Path.LocalPath;
+ }
}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
index aa7e69a62..51146f7e3 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
@@ -15,7 +15,8 @@ public enum DbWizardOperation {
///
/// Универсальная страница настроек операции с базой: ввод параметров создания базы
- /// либо выбор файла для резервной копии. Конкретную операцию строит .
+ /// (с необязательным импортом дампа для MariaDB) либо выбор файла для резервной копии.
+ /// Конкретную операцию (состав пайплайна фаз) строит .
///
public class CreateDataBaseSettingsVM : CarouselPageVM {
public IDbProvider Provider { get; private set; }
@@ -31,12 +32,18 @@ private set {
this.RaiseAndSetIfChanged(ref operation, value);
this.RaisePropertyChanged(nameof(IsCreateMode));
this.RaisePropertyChanged(nameof(IsBackupMode));
+ this.RaisePropertyChanged(nameof(CanImportDump));
}
}
public bool IsCreateMode => Operation == DbWizardOperation.Create;
public bool IsBackupMode => Operation == DbWizardOperation.Backup;
+ ///
+ /// Импорт дампа при создании пока поддержан только для MariaDB (не через облако).
+ ///
+ public bool CanImportDump => Operation == DbWizardOperation.Create && Provider is MariaDBProvider;
+
#region Создание
private string dbTitle;
@@ -51,6 +58,13 @@ public string DbName {
set => this.RaiseAndSetIfChanged(ref dbName, value);
}
+ /// Необязательный путь к SQL-дампу, заливаемому в созданную базу (только MariaDB).
+ private string importDumpFilePath;
+ public string ImportDumpFilePath {
+ get => importDumpFilePath;
+ set => this.RaiseAndSetIfChanged(ref importDumpFilePath, value);
+ }
+
#endregion
#region Резервная копия
@@ -94,6 +108,7 @@ public CreateDataBaseSettingsVM(IServiceProvider services) {
public void SetDbSettings(IDbProvider provider, Connection connection) {
Provider = provider ?? throw new ArgumentNullException(nameof(provider));
Connection = connection ?? throw new ArgumentNullException(nameof(connection));
+ ImportDumpFilePath = null;
Operation = DbWizardOperation.Create;
}
@@ -129,17 +144,33 @@ private void GoToProgress() {
}
else {
progress.OperationTitle = "Создание базы данных";
- pipeline = new[] {
+ var phases = new System.Collections.Generic.List {
new DbCreationPhase(
"Создание базы данных",
- args => args.Provider.CreateDatabase(DbName, DbTitle, services)),
- new DbCreationPhase(
+ args => args.Provider.CreateDatabase(DbName, DbTitle, services))
+ };
+
+ if(!string.IsNullOrWhiteSpace(ImportDumpFilePath) && Provider is MariaDBProvider) {
+ // Залить выбранный дамп вместо стандартного наполнения скриптом.
+ phases.Add(new DbCreationPhase(
+ "Импорт дампа в базу данных",
+ args => {
+ ((MariaDBProvider)args.Provider).ImportDatabase(
+ DbName, ImportDumpFilePath, args.Progress, args.CancellationToken);
+ args.CancellationToken.ThrowIfCancellationRequested();
+ return true;
+ }));
+ }
+ else if(Connection.ConnectionType.SupportsDatabaseCreation(services)) {
+ phases.Add(new DbCreationPhase(
"Наполнение базы данных",
args => {
IDbCreatorModel creator = Connection.ConnectionType.CreateCreator(args);
return creator.RunCreation(DbName, DbTitle);
- })
- };
+ }));
+ }
+
+ pipeline = phases.ToArray();
}
progress.SetPipeline(Provider, Connection, pipeline);
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
index ef3b2baa7..414c62204 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
@@ -33,15 +33,15 @@ public IDbProvider Provider {
}
///
- /// можно создать базу только если:
- /// есть права пользователя на создание
- /// тип подключения поддерживает создание в текущем окружении
- /// задана фабрика и зарегистрирован скрипт создания
+ /// можно создать базу если есть права пользователя на создание И
+ /// либо тип подключения поддерживает создание скриптом (есть фабрика + скрипт),
+ /// либо это MariaDB (наполнение возможно импортом дампа в мастере)
///
public bool CanCreateDatabase =>
provider != null
&& provider.CanCreateDatabase
- && currentConnection?.ConnectionType?.SupportsDatabaseCreation(serviceProvider) == true;
+ && (currentConnection?.ConnectionType?.SupportsDatabaseCreation(serviceProvider) == true
+ || provider is MariaDBProvider);
///
/// Управление базой (резервная копия, удаление) пока поддержано только для MariaDB-провайдера,
From 4b6d9c87d656253eec1d5d4de343d9c7c5fb64b1 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Mon, 15 Jun 2026 19:50:01 +0300
Subject: [PATCH 020/135] =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20?=
=?UTF-8?q?=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D0=B0=20=D1=8D=D0=BA=D1=81?=
=?UTF-8?q?=D0=BF=D0=BE=D1=80=D1=82=D0=B0=20=D0=B8=20=D1=83=D0=B4=D0=B0?=
=?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=20=D1=81=D0=B5=D1=80?=
=?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Clients/DataBaseManagementCloudClient.cs | 6 ++++
QS.Cloud.Client/DataBase/QSCloudProvider.cs | 33 +++++++++++++++++--
.../DataBase/QsCloudConnectionTypeBase.cs | 3 +-
QS.Cloud.Client/DataBase/QsCloudDbCreator.cs | 33 +++++++++++++++----
.../Protos/DataBaseManagement.proto | 10 ++++++
QS.DbManagement/ConnectionTypeBase.cs | 6 ++++
QS.DbManagement/IDbProvider.cs | 14 +++++---
QS.DbManagement/MariaDb/MariaDBProvider.cs | 8 ++---
.../DataBase/CreateDataBaseSettingsVM.cs | 18 +++++++---
.../PageViewModels/DataBase/DataBasesVM.cs | 8 ++---
10 files changed, 113 insertions(+), 26 deletions(-)
diff --git a/QS.Cloud.Client/Clients/DataBaseManagementCloudClient.cs b/QS.Cloud.Client/Clients/DataBaseManagementCloudClient.cs
index 9579fd835..7c0c73034 100644
--- a/QS.Cloud.Client/Clients/DataBaseManagementCloudClient.cs
+++ b/QS.Cloud.Client/Clients/DataBaseManagementCloudClient.cs
@@ -17,5 +17,11 @@ public CreateDataBaseResponse CreateDataBase(string dbName, string dbTitle, IApp
var request = new CreateDataBaseRequest { Name = dbName, Title = dbTitle, ProductId = applicationInfo.ProductCode };
return client.CreateDataBase(request, headers);
}
+
+ public DropDataBaseResponse DropDataBase(int baseId) {
+ var client = new DataBaseManagement.DataBaseManagementClient(Channel);
+ var request = new DropDataBaseRequest { BaseId = baseId };
+ return client.DropDataBase(request, headers);
+ }
}
}
diff --git a/QS.Cloud.Client/DataBase/QSCloudProvider.cs b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
index c8c2c9738..d0e86b891 100644
--- a/QS.Cloud.Client/DataBase/QSCloudProvider.cs
+++ b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
@@ -3,6 +3,7 @@
using QS.Cloud.Core;
using QS.DbManagement.Responces;
using QS.DbManagement;
+using QS.Dialog;
using QS.Project.Versioning;
using System.Collections.Generic;
using System.Linq;
@@ -69,9 +70,37 @@ public void Dispose()
loginClient.Dispose();
}
- public bool DropDatabase(string databaseName)
+ public bool DropDatabase(DbInfo database)
{
- throw new NotImplementedException();
+ // Удаление - лёгкая операция с бухгалтерией реестра, делаем на сервере унарным gRPC.
+ var response = dbClient.DropDataBase(database.BaseId);
+ return response.Success;
+ }
+
+ public void BackupDatabase(DbInfo database, string filePath, IProgressBarDisplayable progress, CancellationToken cancellation)
+ {
+ // Бэкап тяжёлый - берём временное подключение к базе через сессию и гоним экспорт локально,
+ // тем же сервисом, что и MariaDB (по аналогии с наполнением при создании).
+ var session = loginClient.StartSession(database.BaseId);
+ if(!session.Success)
+ throw new InvalidOperationException("Не удалось открыть сессию к облачной базе: " + session.Description);
+
+ var sessionLife = new AliveCloudClient(new SessionInfoProvider(session.SessionId));
+ sessionLife.KeepAlive();
+ try {
+ var builder = new MySqlConnectionStringBuilder {
+ Server = session.Db.Server,
+ Port = session.Db.Port,
+ UserID = session.Db.Login,
+ Password = session.Db.Password,
+ Database = session.Db.BaseName,
+ AllowUserVariables = true
+ };
+ new MariaDbBackupService().Backup(builder, session.Db.BaseName, filePath, progress, cancellation);
+ }
+ finally {
+ sessionLife.Dispose();
+ }
}
public List GetUserDatabases(IApplicationInfo applicationInfo) {
diff --git a/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs b/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
index cb206c682..3f41ee176 100644
--- a/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
+++ b/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
@@ -30,7 +30,8 @@ public QsCloudConnectionTypeBase() {
scripts,
args.Progress,
args.Interaction,
- args.CancellationToken);
+ args.CancellationToken,
+ args.ImportDumpFilePath);
};
}
diff --git a/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs b/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
index 4550edba8..ebd3094ca 100644
--- a/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
+++ b/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
@@ -1,4 +1,6 @@
+using MySqlConnector;
using QS.Cloud.Core;
+using QS.DbManagement;
using QS.DBScripts;
using QS.DBScripts.Controllers;
using QS.DBScripts.Models;
@@ -17,6 +19,7 @@ public class QsCloudDbCreator : IDbCreatorModel
private readonly IDbCreatorInteraction interaction;
private readonly IDbScriptsConfiguration configuration;
private readonly CancellationToken cancellationToken;
+ private readonly string importDumpFilePath;
private LoginManagementCloudClient loginClient;
@@ -26,7 +29,8 @@ public QsCloudDbCreator(
IDbScriptsConfiguration configuration,
IProgressBarDisplayable progress,
IDbCreatorInteraction interaction,
- CancellationToken cancellationToken)
+ CancellationToken cancellationToken,
+ string importDumpFilePath = null)
{
this.baseId = baseId;
loginClient = new LoginManagementCloudClient(authInfo);
@@ -34,6 +38,7 @@ public QsCloudDbCreator(
this.progress = progress ?? throw new ArgumentNullException(nameof(progress));
this.interaction = interaction ?? throw new ArgumentNullException(nameof(interaction));
this.cancellationToken = cancellationToken;
+ this.importDumpFilePath = importDumpFilePath;
}
public bool RunCreation(string dbName, string dbTitle) {
@@ -58,11 +63,27 @@ public bool RunCreation(string dbName, string dbTitle) {
};
sessionLife.KeepAlive();
- var creator = new MySqlDbCreateModel(
- session.Db.Server, session.Db.Port, session.Db.Login, session.Db.Password,
- configuration.MakeCreationScript(), progress, interaction, cancellationToken);
- creator.FillBaseGuid = false;
- bool success = creator.RunCreation(session.Db.BaseName, dbTitle);
+ bool success;
+ if(!string.IsNullOrWhiteSpace(importDumpFilePath)) {
+ // Наполнение импортом выбранного дампа вместо встроенного скрипта.
+ var builder = new MySqlConnectionStringBuilder {
+ Server = session.Db.Server,
+ Port = session.Db.Port,
+ UserID = session.Db.Login,
+ Password = session.Db.Password,
+ Database = session.Db.BaseName,
+ AllowUserVariables = true
+ };
+ new MariaDbImportService().Import(builder, session.Db.BaseName, importDumpFilePath, progress, cancellationToken);
+ success = true;
+ }
+ else {
+ var creator = new MySqlDbCreateModel(
+ session.Db.Server, session.Db.Port, session.Db.Login, session.Db.Password,
+ configuration.MakeCreationScript(), progress, interaction, cancellationToken);
+ creator.FillBaseGuid = false;
+ success = creator.RunCreation(session.Db.BaseName, dbTitle);
+ }
sessionLife.Dispose();
return success;
diff --git a/QS.Cloud.Client/Protos/DataBaseManagement.proto b/QS.Cloud.Client/Protos/DataBaseManagement.proto
index bb5c03f91..9158fbec5 100644
--- a/QS.Cloud.Client/Protos/DataBaseManagement.proto
+++ b/QS.Cloud.Client/Protos/DataBaseManagement.proto
@@ -5,6 +5,8 @@ package QS.Cloud.Core;
service DataBaseManagement{
// Создать пустую базу
rpc CreateDataBase (CreateDataBaseRequest) returns (CreateDataBaseResponse);
+ // Удалить базу: дроп реальной базы на сервере + чистка реестра (bases/base_access)
+ rpc DropDataBase (DropDataBaseRequest) returns (DropDataBaseResponse);
}
message CreateDataBaseRequest{
@@ -17,3 +19,11 @@ message CreateDataBaseResponse{
int32 base_id = 1;
string base_guid = 2;
}
+
+message DropDataBaseRequest{
+ int32 base_id = 1;
+}
+
+message DropDataBaseResponse{
+ bool success = 1;
+}
diff --git a/QS.DbManagement/ConnectionTypeBase.cs b/QS.DbManagement/ConnectionTypeBase.cs
index 82ea666c5..6bdcee32b 100644
--- a/QS.DbManagement/ConnectionTypeBase.cs
+++ b/QS.DbManagement/ConnectionTypeBase.cs
@@ -49,5 +49,11 @@ public class CreatorFactoryArgs {
public IDbCreatorInteraction Interaction { get; set; }
public System.Threading.CancellationToken CancellationToken { get; set; }
public IServiceProvider ServiceProvider { get; set; }
+
+ ///
+ /// Необязательный путь к SQL-дампу. Если задан - движок создания наполняет базу
+ /// импортом дампа вместо встроенного скрипта (поддерживает облачный creator).
+ ///
+ public string ImportDumpFilePath { get; set; }
}
}
diff --git a/QS.DbManagement/IDbProvider.cs b/QS.DbManagement/IDbProvider.cs
index c97d2f142..a72202cbe 100644
--- a/QS.DbManagement/IDbProvider.cs
+++ b/QS.DbManagement/IDbProvider.cs
@@ -1,6 +1,8 @@
using QS.DbManagement.Responces;
+using QS.Dialog;
using QS.Project.Versioning;
using System.Collections.Generic;
+using System.Threading;
using System;
namespace QS.DbManagement
@@ -8,13 +10,17 @@ namespace QS.DbManagement
public interface IDbProvider : IDisposable
{
string UserName { get; }
-
+
bool ChangePassword(string username, string oldPassword, string newPassword);
bool CreateDatabase(string databaseName, string title, IServiceProvider services = null);
-
- bool DropDatabase(string databaseName);
-
+
+ // DbInfo, а не имя: облаку нужен BaseId (у облачного DbInfo нет BaseName), MariaDB берёт BaseName.
+ bool DropDatabase(DbInfo database);
+
+ // Резервная копия базы в SQL-скрипт. MariaDB - напрямую, облако - по временной сессии.
+ void BackupDatabase(DbInfo database, string filePath, IProgressBarDisplayable progress, CancellationToken cancellation);
+
bool AddUser(string username, string password);
LoginToServerResponse LoginToServer();
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index 924990007..5b6c4b6c3 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -185,8 +185,8 @@ public bool CreateDatabase(string databaseName, string title, IServiceProvider s
return connection.Execute(sql) != 0;
}
- public bool DropDatabase(string databaseName) {
- string sql = $"DROP DATABASE IF EXISTS `{databaseName}`";
+ public bool DropDatabase(DbInfo database) {
+ string sql = $"DROP DATABASE IF EXISTS `{database.BaseName}`";
return connection.Execute(sql) != 0;
}
@@ -195,8 +195,8 @@ public bool DropDatabase(string databaseName) {
/// а сам экспорт вынесен в .
/// Метод блокирующий - вызывать из фонового потока.
///
- public void BackupDatabase(string databaseName, string filePath, IProgressBarDisplayable progress, CancellationToken cancellation) {
- new MariaDbBackupService().Backup(ConnectionStringBuilder, databaseName, filePath, progress, cancellation);
+ public void BackupDatabase(DbInfo database, string filePath, IProgressBarDisplayable progress, CancellationToken cancellation) {
+ new MariaDbBackupService().Backup(ConnectionStringBuilder, database.BaseName, filePath, progress, cancellation);
}
///
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
index 51146f7e3..02f3834ed 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
@@ -40,9 +40,12 @@ private set {
public bool IsBackupMode => Operation == DbWizardOperation.Backup;
///
- /// Импорт дампа при создании пока поддержан только для MariaDB (не через облако).
+ /// Импорт дампа при создании: MariaDB напрямую, облако - через свой creator
+ /// (доступен, когда тип подключения поддерживает создание, т.е. есть creator).
///
- public bool CanImportDump => Operation == DbWizardOperation.Create && Provider is MariaDBProvider;
+ public bool CanImportDump => Operation == DbWizardOperation.Create
+ && (Provider is MariaDBProvider
+ || Connection?.ConnectionType?.SupportsDatabaseCreation(services) == true);
#region Создание
@@ -119,10 +122,12 @@ public void SetBackupSettings(IDbProvider provider, Connection connection, DbInf
Operation = DbWizardOperation.Backup;
BackupTargetTitle = database.Title;
+ // У облачного DbInfo нет BaseName - подставляем Title в имя файла.
+ var fileBaseName = string.IsNullOrEmpty(database.BaseName) ? database.Title : database.BaseName;
BackupFilePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
"Резервные копии",
- string.Format("{0}-{1:yyMMdd-HHmm}.sql", database.BaseName, DateTime.Now));
+ string.Format("{0}-{1:yyMMdd-HHmm}.sql", fileBaseName, DateTime.Now));
}
private void GoToProgress() {
@@ -135,8 +140,9 @@ private void GoToProgress() {
new DbCreationPhase(
"Создание резервной копии базы данных",
args => {
- ((MariaDBProvider)args.Provider).BackupDatabase(
- backupTarget.BaseName, BackupFilePath, args.Progress, args.CancellationToken);
+ // BackupDatabase теперь на IDbProvider - работает и для MariaDB, и для облака.
+ args.Provider.BackupDatabase(
+ backupTarget, BackupFilePath, args.Progress, args.CancellationToken);
args.CancellationToken.ThrowIfCancellationRequested();
return true;
})
@@ -165,6 +171,8 @@ private void GoToProgress() {
phases.Add(new DbCreationPhase(
"Наполнение базы данных",
args => {
+ // Передаём дамп в creator: облачный creator зальёт его вместо скрипта.
+ args.ImportDumpFilePath = ImportDumpFilePath;
IDbCreatorModel creator = Connection.ConnectionType.CreateCreator(args);
return creator.RunCreation(DbName, DbTitle);
}));
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
index 414c62204..23b5524ea 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
@@ -44,10 +44,10 @@ public IDbProvider Provider {
|| provider is MariaDBProvider);
///
- /// Управление базой (резервная копия, удаление) пока поддержано только для MariaDB-провайдера,
- /// облако реализуем отдельно.
+ /// Управление базой (резервная копия, удаление) идёт через IDbProvider -
+ /// поддержано и для MariaDB, и для облака. Видно при активном подключении.
///
- public bool CanManageDatabases => provider is MariaDBProvider;
+ public bool CanManageDatabases => provider != null;
public Connection CurrentConnection => currentConnection;
@@ -178,7 +178,7 @@ private async System.Threading.Tasks.Task DeleteDatabaseAsync(DbInfo database) {
return;
try {
- await System.Threading.Tasks.Task.Run(() => provider.DropDatabase(database.BaseName));
+ await System.Threading.Tasks.Task.Run(() => provider.DropDatabase(database));
RefreshDatabases();
interactiveMessage.ShowMessage(ImportanceLevel.Success,
$"База данных «{database.Title}» удалена.", "Удаление базы данных");
From 2119b28d41a319d1857000aeae22725d5cd5008a Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Wed, 17 Jun 2026 20:29:14 +0300
Subject: [PATCH 021/135] =?UTF-8?q?=D0=B2=D1=8B=D0=BD=D0=B5=D1=81=D0=B5?=
=?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BE=D0=B1=D1=89=D0=B5=D0=B9=20=D0=BB?=
=?UTF-8?q?=D0=BE=D0=B3=D0=B8=D0=BA=D0=B8=20=D0=B2=20=D0=BE=D0=B4=D0=B8?=
=?UTF-8?q?=D0=BD=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
для импорта экспорта и открытия сессии для наполнения базы
---
QS.Cloud.Client/DataBase/CloudDbSession.cs | 47 +++++++++
QS.Cloud.Client/DataBase/QSCloudProvider.cs | 23 +----
QS.Cloud.Client/DataBase/QsCloudDbCreator.cs | 51 +++-------
QS.DbManagement/MariaDb/MariaDBProvider.cs | 8 +-
.../MariaDb/MariaDbBackupService.cs | 70 -------------
QS.DbManagement/MariaDb/MariaDbDumpService.cs | 97 +++++++++++++++++++
.../MariaDb/MariaDbImportService.cs | 64 ------------
7 files changed, 167 insertions(+), 193 deletions(-)
create mode 100644 QS.Cloud.Client/DataBase/CloudDbSession.cs
delete mode 100644 QS.DbManagement/MariaDb/MariaDbBackupService.cs
create mode 100644 QS.DbManagement/MariaDb/MariaDbDumpService.cs
delete mode 100644 QS.DbManagement/MariaDb/MariaDbImportService.cs
diff --git a/QS.Cloud.Client/DataBase/CloudDbSession.cs b/QS.Cloud.Client/DataBase/CloudDbSession.cs
new file mode 100644
index 000000000..2e1d2acd8
--- /dev/null
+++ b/QS.Cloud.Client/DataBase/CloudDbSession.cs
@@ -0,0 +1,47 @@
+using System;
+using MySqlConnector;
+using QS.Cloud.Client.Clients;
+using QS.Cloud.Core;
+
+namespace QS.Cloud.Client.DataBase {
+ public sealed class CloudDbSession : IDisposable {
+ private readonly AliveCloudClient sessionLife;
+
+ public bool Success { get; }
+ public string Description { get; }
+ public bool IsAdmin { get; }
+ public BaseConnection Db { get; }
+ public MySqlConnectionStringBuilder ConnectionStringBuilder { get; }
+
+ private CloudDbSession(StartSessionResponse session, AliveCloudClient sessionLife, MySqlConnectionStringBuilder connectionStringBuilder) {
+ Success = session.Success;
+ Description = session.Description;
+ IsAdmin = session.IsAdmin;
+ Db = session.Db;
+ this.sessionLife = sessionLife;
+ ConnectionStringBuilder = connectionStringBuilder;
+ }
+
+ public static CloudDbSession Open(LoginManagementCloudClient loginClient, int baseId) {
+ var session = loginClient.StartSession(baseId);
+ if(!session.Success)
+ return new CloudDbSession(session, null, null);
+
+ var sessionLife = new AliveCloudClient(new SessionInfoProvider(session.SessionId));
+ sessionLife.KeepAlive();
+
+ var builder = new MySqlConnectionStringBuilder {
+ Server = session.Db.Server,
+ Port = session.Db.Port,
+ UserID = session.Db.Login,
+ Password = session.Db.Password,
+ Database = session.Db.BaseName,
+ AllowUserVariables = true
+ };
+
+ return new CloudDbSession(session, sessionLife, builder);
+ }
+
+ public void Dispose() => sessionLife?.Dispose();
+ }
+}
diff --git a/QS.Cloud.Client/DataBase/QSCloudProvider.cs b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
index d0e86b891..cf876ce98 100644
--- a/QS.Cloud.Client/DataBase/QSCloudProvider.cs
+++ b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
@@ -81,25 +81,10 @@ public void BackupDatabase(DbInfo database, string filePath, IProgressBarDisplay
{
// Бэкап тяжёлый - берём временное подключение к базе через сессию и гоним экспорт локально,
// тем же сервисом, что и MariaDB (по аналогии с наполнением при создании).
- var session = loginClient.StartSession(database.BaseId);
- if(!session.Success)
- throw new InvalidOperationException("Не удалось открыть сессию к облачной базе: " + session.Description);
-
- var sessionLife = new AliveCloudClient(new SessionInfoProvider(session.SessionId));
- sessionLife.KeepAlive();
- try {
- var builder = new MySqlConnectionStringBuilder {
- Server = session.Db.Server,
- Port = session.Db.Port,
- UserID = session.Db.Login,
- Password = session.Db.Password,
- Database = session.Db.BaseName,
- AllowUserVariables = true
- };
- new MariaDbBackupService().Backup(builder, session.Db.BaseName, filePath, progress, cancellation);
- }
- finally {
- sessionLife.Dispose();
+ using(var session = CloudDbSession.Open(loginClient, database.BaseId)) {
+ if(!session.Success)
+ throw new InvalidOperationException("Не удалось открыть сессию к облачной базе: " + session.Description);
+ new MariaDbDumpService().Export(session.ConnectionStringBuilder, session.Db.BaseName, filePath, progress, cancellation);
}
}
diff --git a/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs b/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
index ebd3094ca..7948b29db 100644
--- a/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
+++ b/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
@@ -1,4 +1,3 @@
-using MySqlConnector;
using QS.Cloud.Core;
using QS.DbManagement;
using QS.DBScripts;
@@ -45,48 +44,28 @@ public bool RunCreation(string dbName, string dbTitle) {
try {
cancellationToken.ThrowIfCancellationRequested();
- StartSessionResponse session = loginClient.StartSession(baseId);
+ using(var session = CloudDbSession.Open(loginClient, baseId)) {
+ if(!session.Success) {
+ interaction.ReportError("Ошибка в создании сессии", "Запрос в облако");
+ return false;
+ }
+ if(!session.IsAdmin) {
+ interaction.ReportError("Вы не имеете прав Администратора", "Запрос в облако");
+ return false;
+ }
- if(!session.Success) {
- interaction.ReportError("Ошибка в создании сессии", "Запрос в облако");
- return false;
- }
- if(!session.IsAdmin) {
- interaction.ReportError("Вы не имеете прав Администратора", "Запрос в облако");
- return false;
- }
-
- var infoProvider = new SessionInfoProvider(sessionId: session.SessionId);
- var sessionLife = new AliveCloudClient(infoProvider);
- sessionLife.NewMessage += (mes) => {
- progress.Update("Сессия: " + mes + " в статусе " + sessionLife.LastStatus.ToString());
- };
- sessionLife.KeepAlive();
+ if(!string.IsNullOrWhiteSpace(importDumpFilePath)) {
+ // Наполнение импортом выбранного дампа вместо встроенного скрипта.
+ new MariaDbDumpService().Import(session.ConnectionStringBuilder, session.Db.BaseName, importDumpFilePath, progress, cancellationToken);
+ return true;
+ }
- bool success;
- if(!string.IsNullOrWhiteSpace(importDumpFilePath)) {
- // Наполнение импортом выбранного дампа вместо встроенного скрипта.
- var builder = new MySqlConnectionStringBuilder {
- Server = session.Db.Server,
- Port = session.Db.Port,
- UserID = session.Db.Login,
- Password = session.Db.Password,
- Database = session.Db.BaseName,
- AllowUserVariables = true
- };
- new MariaDbImportService().Import(builder, session.Db.BaseName, importDumpFilePath, progress, cancellationToken);
- success = true;
- }
- else {
var creator = new MySqlDbCreateModel(
session.Db.Server, session.Db.Port, session.Db.Login, session.Db.Password,
configuration.MakeCreationScript(), progress, interaction, cancellationToken);
creator.FillBaseGuid = false;
- success = creator.RunCreation(session.Db.BaseName, dbTitle);
+ return creator.RunCreation(session.Db.BaseName, dbTitle);
}
-
- sessionLife.Dispose();
- return success;
}
catch(OperationCanceledException) {
logger.Info("Создание базы в облаке отменено пользователем.");
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index 5b6c4b6c3..6abc855ce 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -192,20 +192,20 @@ public bool DropDatabase(DbInfo database) {
///
/// Резервное копирование базы в SQL-скрипт. Взаимодействие с базой идёт через провайдер,
- /// а сам экспорт вынесен в .
+ /// а сам экспорт вынесен в .
/// Метод блокирующий - вызывать из фонового потока.
///
public void BackupDatabase(DbInfo database, string filePath, IProgressBarDisplayable progress, CancellationToken cancellation) {
- new MariaDbBackupService().Backup(ConnectionStringBuilder, database.BaseName, filePath, progress, cancellation);
+ new MariaDbDumpService().Export(ConnectionStringBuilder, database.BaseName, filePath, progress, cancellation);
}
///
/// Импорт SQL-дампа в уже созданную базу. Взаимодействие с базой идёт через провайдер,
- /// а сам импорт вынесен в .
+ /// а сам импорт вынесен в .
/// Метод блокирующий - вызывать из фонового потока.
///
public void ImportDatabase(string databaseName, string filePath, IProgressBarDisplayable progress, CancellationToken cancellation) {
- new MariaDbImportService().Import(ConnectionStringBuilder, databaseName, filePath, progress, cancellation);
+ new MariaDbDumpService().Import(ConnectionStringBuilder, databaseName, filePath, progress, cancellation);
}
public void Dispose() {
diff --git a/QS.DbManagement/MariaDb/MariaDbBackupService.cs b/QS.DbManagement/MariaDb/MariaDbBackupService.cs
deleted file mode 100644
index a8650fad5..000000000
--- a/QS.DbManagement/MariaDb/MariaDbBackupService.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-using System;
-using System.IO;
-using System.Threading;
-using MySqlConnector;
-using QS.Dialog;
-
-namespace QS.DbManagement {
- ///
- /// Экспорт базы MariaDB/MySQL в SQL-скрипт через MySqlBackup.NET.
- /// Вынесен отдельным сервисом, чтобы провайдер не держал логику бэкапа в себе.
- ///
- public class MariaDbBackupService {
- private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
-
- ///
- /// Синхронно выгружает базу в файл .
- /// Вызывать из фонового потока - метод блокирующий (как и MySqlBackup).
- ///
- public void Backup(
- MySqlConnectionStringBuilder connectionSettings,
- string databaseName,
- string filePath,
- IProgressBarDisplayable progress,
- CancellationToken cancellation) {
- if(connectionSettings == null)
- throw new ArgumentNullException(nameof(connectionSettings));
- if(string.IsNullOrWhiteSpace(databaseName))
- throw new ArgumentException("Не указано имя базы для резервного копирования.", nameof(databaseName));
- if(string.IsNullOrWhiteSpace(filePath))
- throw new ArgumentException("Не указан путь к файлу резервной копии.", nameof(filePath));
-
- // Отдельная строка подключения именно к выгружаемой базе - провайдер может быть подключён к другой.
- var builder = new MySqlConnectionStringBuilder(connectionSettings.ConnectionString) {
- Database = databaseName
- };
-
- var directory = Path.GetDirectoryName(filePath);
- if(!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
- Directory.CreateDirectory(directory);
-
- logger.Info("Создаём резервную копию базы {0} в файл {1}", databaseName, filePath);
-
- using(var connection = new MySqlConnection(builder.ConnectionString)) {
- connection.Open();
- using(var command = connection.CreateCommand())
- using(var backup = new MySqlBackup(command)) {
- bool started = false;
- string currentTable = null;
- backup.ExportProgressChanged += (sender, e) => {
- if(cancellation.IsCancellationRequested) {
- ((MySqlBackup)sender).StopAllProcess();
- return;
- }
- if(!started) {
- progress?.Start(maxValue: e.TotalRowsInAllTables, text: "Создание резервной копии");
- started = true;
- }
- if(currentTable != e.CurrentTableName) {
- currentTable = e.CurrentTableName;
- progress?.Update($"Экспорт таблицы {currentTable}");
- }
- progress?.Update(e.CurrentRowIndexInAllTables);
- };
-
- backup.ExportToFile(filePath);
- }
- }
- }
- }
-}
diff --git a/QS.DbManagement/MariaDb/MariaDbDumpService.cs b/QS.DbManagement/MariaDb/MariaDbDumpService.cs
new file mode 100644
index 000000000..dd17e98e4
--- /dev/null
+++ b/QS.DbManagement/MariaDb/MariaDbDumpService.cs
@@ -0,0 +1,97 @@
+using System;
+using System.IO;
+using System.Threading;
+using MySqlConnector;
+using QS.Dialog;
+
+namespace QS.DbManagement {
+ public class MariaDbDumpService {
+ private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
+
+ public void Export(
+ MySqlConnectionStringBuilder connectionSettings,
+ string databaseName,
+ string filePath,
+ IProgressBarDisplayable progress,
+ CancellationToken cancellation) {
+ if(string.IsNullOrWhiteSpace(filePath))
+ throw new ArgumentException("Не указан путь к файлу резервной копии.", nameof(filePath));
+
+ var directory = Path.GetDirectoryName(filePath);
+ if(!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
+ Directory.CreateDirectory(directory);
+
+ logger.Info("Создаём резервную копию базы {0} в файл {1}", databaseName, filePath);
+
+ RunWithBackup(connectionSettings, databaseName, backup => {
+ bool started = false;
+ string currentTable = null;
+ backup.ExportProgressChanged += (sender, e) => {
+ if(cancellation.IsCancellationRequested) {
+ ((MySqlBackup)sender).StopAllProcess();
+ return;
+ }
+ if(!started) {
+ progress?.Start(maxValue: e.TotalRowsInAllTables, text: "Создание резервной копии");
+ started = true;
+ }
+ if(currentTable != e.CurrentTableName) {
+ currentTable = e.CurrentTableName;
+ progress?.Update($"Экспорт таблицы {currentTable}");
+ }
+ progress?.Update(e.CurrentRowIndexInAllTables);
+ };
+ backup.ExportToFile(filePath);
+ });
+ }
+
+ public void Import(
+ MySqlConnectionStringBuilder connectionSettings,
+ string databaseName,
+ string filePath,
+ IProgressBarDisplayable progress,
+ CancellationToken cancellation) {
+ if(string.IsNullOrWhiteSpace(filePath))
+ throw new ArgumentException("Не указан путь к файлу дампа.", nameof(filePath));
+ if(!File.Exists(filePath))
+ throw new FileNotFoundException("Файл дампа не найден.", filePath);
+
+ logger.Info("Импортируем дамп {0} в базу {1}", filePath, databaseName);
+
+ RunWithBackup(connectionSettings, databaseName, backup => {
+ bool started = false;
+ backup.ImportProgressChanged += (sender, e) => {
+ if(cancellation.IsCancellationRequested) {
+ ((MySqlBackup)sender).StopAllProcess();
+ return;
+ }
+ if(!started) {
+ progress?.Start(maxValue: e.TotalBytes, text: "Импорт дампа в базу данных");
+ started = true;
+ }
+ progress?.Update(e.CurrentBytes);
+ };
+ backup.ImportFromFile(filePath);
+ });
+ }
+
+ private void RunWithBackup(MySqlConnectionStringBuilder connectionSettings, string databaseName, Action action) {
+ if(connectionSettings == null)
+ throw new ArgumentNullException(nameof(connectionSettings));
+ if(string.IsNullOrWhiteSpace(databaseName))
+ throw new ArgumentException("Не указано имя базы.", nameof(databaseName));
+
+ var builder = new MySqlConnectionStringBuilder(connectionSettings.ConnectionString) {
+ Database = databaseName
+ };
+
+ using(var connection = new MySqlConnection(builder.ConnectionString)) {
+ connection.Open();
+ using(var command = connection.CreateCommand())
+ using(var backup = new MySqlBackup(command)) {
+ action(backup);
+ }
+ }
+ }
+ }
+}
diff --git a/QS.DbManagement/MariaDb/MariaDbImportService.cs b/QS.DbManagement/MariaDb/MariaDbImportService.cs
deleted file mode 100644
index e7643a2c1..000000000
--- a/QS.DbManagement/MariaDb/MariaDbImportService.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-using System;
-using System.IO;
-using System.Threading;
-using MySqlConnector;
-using QS.Dialog;
-
-namespace QS.DbManagement {
- ///
- /// Импорт SQL-дампа в существующую базу MariaDB/MySQL через MySqlBackup.NET.
- /// Симметричен . Вынесен отдельным сервисом,
- /// чтобы провайдер не держал логику работы с дампом в себе.
- ///
- public class MariaDbImportService {
- private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
-
- ///
- /// Синхронно заливает дамп в базу .
- /// База должна уже существовать. Вызывать из фонового потока - метод блокирующий.
- ///
- public void Import(
- MySqlConnectionStringBuilder connectionSettings,
- string databaseName,
- string filePath,
- IProgressBarDisplayable progress,
- CancellationToken cancellation) {
- if(connectionSettings == null)
- throw new ArgumentNullException(nameof(connectionSettings));
- if(string.IsNullOrWhiteSpace(databaseName))
- throw new ArgumentException("Не указано имя базы для импорта дампа.", nameof(databaseName));
- if(string.IsNullOrWhiteSpace(filePath))
- throw new ArgumentException("Не указан путь к файлу дампа.", nameof(filePath));
- if(!File.Exists(filePath))
- throw new FileNotFoundException("Файл дампа не найден.", filePath);
-
- // Отдельная строка подключения именно к целевой базе - провайдер может смотреть в другую.
- var builder = new MySqlConnectionStringBuilder(connectionSettings.ConnectionString) {
- Database = databaseName
- };
-
- logger.Info("Импортируем дамп {0} в базу {1}", filePath, databaseName);
-
- using(var connection = new MySqlConnection(builder.ConnectionString)) {
- connection.Open();
- using(var command = connection.CreateCommand())
- using(var backup = new MySqlBackup(command)) {
- bool started = false;
- backup.ImportProgressChanged += (sender, e) => {
- if(cancellation.IsCancellationRequested) {
- ((MySqlBackup)sender).StopAllProcess();
- return;
- }
- if(!started) {
- progress?.Start(maxValue: e.TotalBytes, text: "Импорт дампа в базу данных");
- started = true;
- }
- progress?.Update(e.CurrentBytes);
- };
-
- backup.ImportFromFile(filePath);
- }
- }
- }
- }
-}
From f68a2d552738f3d33abbf7c107d7857b7a6e493e Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Thu, 18 Jun 2026 12:24:59 +0300
Subject: [PATCH 022/135] =?UTF-8?q?=D0=BD=D0=B5=D0=B1=D0=BE=D0=BB=D1=8C?=
=?UTF-8?q?=D1=88=D0=B8=D0=B5=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20?=
=?UTF-8?q?=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=D0=BE=D0=B2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
QS.Cloud.Client/DataBase/CloudDbSession.cs | 8 +++++++-
QS.Cloud.Client/DataBase/QSCloudProvider.cs | 3 ---
.../Protos/DataBaseManagement.proto | 2 +-
QS.DbManagement/ConnectionTypeBase.cs | 4 ----
QS.DbManagement/IDbProvider.cs | 2 --
QS.DbManagement/MariaDb/MariaDBProvider.cs | 10 ++++------
QS.DbManagement/MariaDb/MariaDbDumpService.cs | 20 ++++++++++---------
7 files changed, 23 insertions(+), 26 deletions(-)
diff --git a/QS.Cloud.Client/DataBase/CloudDbSession.cs b/QS.Cloud.Client/DataBase/CloudDbSession.cs
index 2e1d2acd8..bffd21eae 100644
--- a/QS.Cloud.Client/DataBase/CloudDbSession.cs
+++ b/QS.Cloud.Client/DataBase/CloudDbSession.cs
@@ -4,6 +4,9 @@
using QS.Cloud.Core;
namespace QS.Cloud.Client.DataBase {
+ ///
+ /// Временное подключение к облачной базе
+ ///
public sealed class CloudDbSession : IDisposable {
private readonly AliveCloudClient sessionLife;
@@ -22,6 +25,10 @@ private CloudDbSession(StartSessionResponse session, AliveCloudClient sessionLif
ConnectionStringBuilder = connectionStringBuilder;
}
+ ///
+ /// Открывает сессию к базе. При успехе запускает keep-alive и собирает строку подключения;
+ /// при отказе возвращает сессию с = false
+ ///
public static CloudDbSession Open(LoginManagementCloudClient loginClient, int baseId) {
var session = loginClient.StartSession(baseId);
if(!session.Success)
@@ -38,7 +45,6 @@ public static CloudDbSession Open(LoginManagementCloudClient loginClient, int ba
Database = session.Db.BaseName,
AllowUserVariables = true
};
-
return new CloudDbSession(session, sessionLife, builder);
}
diff --git a/QS.Cloud.Client/DataBase/QSCloudProvider.cs b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
index cf876ce98..8da08913e 100644
--- a/QS.Cloud.Client/DataBase/QSCloudProvider.cs
+++ b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
@@ -72,15 +72,12 @@ public void Dispose()
public bool DropDatabase(DbInfo database)
{
- // Удаление - лёгкая операция с бухгалтерией реестра, делаем на сервере унарным gRPC.
var response = dbClient.DropDataBase(database.BaseId);
return response.Success;
}
public void BackupDatabase(DbInfo database, string filePath, IProgressBarDisplayable progress, CancellationToken cancellation)
{
- // Бэкап тяжёлый - берём временное подключение к базе через сессию и гоним экспорт локально,
- // тем же сервисом, что и MariaDB (по аналогии с наполнением при создании).
using(var session = CloudDbSession.Open(loginClient, database.BaseId)) {
if(!session.Success)
throw new InvalidOperationException("Не удалось открыть сессию к облачной базе: " + session.Description);
diff --git a/QS.Cloud.Client/Protos/DataBaseManagement.proto b/QS.Cloud.Client/Protos/DataBaseManagement.proto
index 9158fbec5..c0cbe0c64 100644
--- a/QS.Cloud.Client/Protos/DataBaseManagement.proto
+++ b/QS.Cloud.Client/Protos/DataBaseManagement.proto
@@ -5,7 +5,7 @@ package QS.Cloud.Core;
service DataBaseManagement{
// Создать пустую базу
rpc CreateDataBase (CreateDataBaseRequest) returns (CreateDataBaseResponse);
- // Удалить базу: дроп реальной базы на сервере + чистка реестра (bases/base_access)
+ // Удалить базу и очистить реестр
rpc DropDataBase (DropDataBaseRequest) returns (DropDataBaseResponse);
}
diff --git a/QS.DbManagement/ConnectionTypeBase.cs b/QS.DbManagement/ConnectionTypeBase.cs
index 6bdcee32b..4a96e350c 100644
--- a/QS.DbManagement/ConnectionTypeBase.cs
+++ b/QS.DbManagement/ConnectionTypeBase.cs
@@ -50,10 +50,6 @@ public class CreatorFactoryArgs {
public System.Threading.CancellationToken CancellationToken { get; set; }
public IServiceProvider ServiceProvider { get; set; }
- ///
- /// Необязательный путь к SQL-дампу. Если задан - движок создания наполняет базу
- /// импортом дампа вместо встроенного скрипта (поддерживает облачный creator).
- ///
public string ImportDumpFilePath { get; set; }
}
}
diff --git a/QS.DbManagement/IDbProvider.cs b/QS.DbManagement/IDbProvider.cs
index a72202cbe..3627c7823 100644
--- a/QS.DbManagement/IDbProvider.cs
+++ b/QS.DbManagement/IDbProvider.cs
@@ -15,10 +15,8 @@ public interface IDbProvider : IDisposable
bool CreateDatabase(string databaseName, string title, IServiceProvider services = null);
- // DbInfo, а не имя: облаку нужен BaseId (у облачного DbInfo нет BaseName), MariaDB берёт BaseName.
bool DropDatabase(DbInfo database);
- // Резервная копия базы в SQL-скрипт. MariaDB - напрямую, облако - по временной сессии.
void BackupDatabase(DbInfo database, string filePath, IProgressBarDisplayable progress, CancellationToken cancellation);
bool AddUser(string username, string password);
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index 6abc855ce..08d88e655 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -191,18 +191,16 @@ public bool DropDatabase(DbInfo database) {
}
///
- /// Резервное копирование базы в SQL-скрипт. Взаимодействие с базой идёт через провайдер,
- /// а сам экспорт вынесен в .
- /// Метод блокирующий - вызывать из фонового потока.
+ /// Резервное копирование базы в SQL-скрипт
+ /// Метод блокирующий - вызывать из фонового потока
///
public void BackupDatabase(DbInfo database, string filePath, IProgressBarDisplayable progress, CancellationToken cancellation) {
new MariaDbDumpService().Export(ConnectionStringBuilder, database.BaseName, filePath, progress, cancellation);
}
///
- /// Импорт SQL-дампа в уже созданную базу. Взаимодействие с базой идёт через провайдер,
- /// а сам импорт вынесен в .
- /// Метод блокирующий - вызывать из фонового потока.
+ /// Импорт SQL-дампа в уже созданную базу
+ /// Метод блокирующий - вызывать из фонового потока
///
public void ImportDatabase(string databaseName, string filePath, IProgressBarDisplayable progress, CancellationToken cancellation) {
new MariaDbDumpService().Import(ConnectionStringBuilder, databaseName, filePath, progress, cancellation);
diff --git a/QS.DbManagement/MariaDb/MariaDbDumpService.cs b/QS.DbManagement/MariaDb/MariaDbDumpService.cs
index dd17e98e4..d9381585d 100644
--- a/QS.DbManagement/MariaDb/MariaDbDumpService.cs
+++ b/QS.DbManagement/MariaDb/MariaDbDumpService.cs
@@ -6,14 +6,14 @@
namespace QS.DbManagement {
public class MariaDbDumpService {
- private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
-
+ /// Выгружает базу в файл
public void Export(
MySqlConnectionStringBuilder connectionSettings,
string databaseName,
string filePath,
IProgressBarDisplayable progress,
- CancellationToken cancellation) {
+ CancellationToken cancellation)
+ {
if(string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException("Не указан путь к файлу резервной копии.", nameof(filePath));
@@ -21,7 +21,7 @@ public void Export(
if(!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
Directory.CreateDirectory(directory);
- logger.Info("Создаём резервную копию базы {0} в файл {1}", databaseName, filePath);
+ progress?.Update($"Создаём резервную копию базы {databaseName} в файл {filePath}");
RunWithBackup(connectionSettings, databaseName, backup => {
bool started = false;
@@ -45,6 +45,7 @@ public void Export(
});
}
+ /// Заливает дамп в уже существующую базу
public void Import(
MySqlConnectionStringBuilder connectionSettings,
string databaseName,
@@ -56,7 +57,7 @@ public void Import(
if(!File.Exists(filePath))
throw new FileNotFoundException("Файл дампа не найден.", filePath);
- logger.Info("Импортируем дамп {0} в базу {1}", filePath, databaseName);
+ progress?.Update($"Импортируем дамп {filePath} в базу {databaseName}");
RunWithBackup(connectionSettings, databaseName, backup => {
bool started = false;
@@ -79,7 +80,7 @@ private void RunWithBackup(MySqlConnectionStringBuilder connectionSettings, stri
if(connectionSettings == null)
throw new ArgumentNullException(nameof(connectionSettings));
if(string.IsNullOrWhiteSpace(databaseName))
- throw new ArgumentException("Не указано имя базы.", nameof(databaseName));
+ throw new ArgumentException("Не указано имя базы", nameof(databaseName));
var builder = new MySqlConnectionStringBuilder(connectionSettings.ConnectionString) {
Database = databaseName
@@ -87,9 +88,10 @@ private void RunWithBackup(MySqlConnectionStringBuilder connectionSettings, stri
using(var connection = new MySqlConnection(builder.ConnectionString)) {
connection.Open();
- using(var command = connection.CreateCommand())
- using(var backup = new MySqlBackup(command)) {
- action(backup);
+ using(var command = connection.CreateCommand()) {
+ using(var backup = new MySqlBackup(command)) {
+ action(backup);
+ }
}
}
}
From 4474913b2b64fe6307a0049bb2f06aa24b268a3f Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Thu, 18 Jun 2026 19:16:19 +0300
Subject: [PATCH 023/135] =?UTF-8?q?=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82?=
=?UTF-8?q?=D0=BE=D1=80=D0=B8=D0=BD=D0=B3=20=D0=BA=D0=B0=D1=80=D1=83=D1=81?=
=?UTF-8?q?=D0=B5=D0=BB=D0=B8=20=D0=B8=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5?=
=?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BD=D0=B0=D1=81=D1=82=D1=80?=
=?UTF-8?q?=D0=BE=D0=B5=D0=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
QS.Launcher.Avalonia/DependencyInjection.cs | 5 +-
.../QS.Launcher.Avalonia.csproj | 7 +-
QS.Launcher.Avalonia/Views/MainWindow.axaml | 1 -
.../Views/MainWindow.axaml.cs | 46 ++++-
QS.Launcher.Avalonia/Views/PageViewLocator.cs | 36 ++++
.../Pages/DataBase/BackupDbSettingsView.axaml | 30 +++
...axaml.cs => BackupDbSettingsView.axaml.cs} | 27 +--
...sView.axaml => CreateDbSettingsView.axaml} | 22 +-
.../DataBase/CreateDbSettingsView.axaml.cs | 34 ++++
QS.Launcher/DependencyInjection.cs | 5 +-
.../DataBase/BackupDbSettingsVM.cs | 47 +++++
.../DataBase/CreateDataBaseSettingsVM.cs | 189 ------------------
.../DataBase/CreateDbSettingsVM.cs | 69 +++++++
.../PageViewModels/DataBase/DataBasesVM.cs | 67 +++----
.../DataBase/DbOperationSettingsVM.cs | 46 +++++
.../DataBase/IDbOperationSettings.cs | 18 ++
16 files changed, 361 insertions(+), 288 deletions(-)
create mode 100644 QS.Launcher.Avalonia/Views/PageViewLocator.cs
create mode 100644 QS.Launcher.Avalonia/Views/Pages/DataBase/BackupDbSettingsView.axaml
rename QS.Launcher.Avalonia/Views/Pages/DataBase/{CreateDataBaseSettingsView.axaml.cs => BackupDbSettingsView.axaml.cs} (57%)
rename QS.Launcher.Avalonia/Views/Pages/DataBase/{CreateDataBaseSettingsView.axaml => CreateDbSettingsView.axaml} (62%)
create mode 100644 QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDbSettingsView.axaml.cs
create mode 100644 QS.Launcher/ViewModels/PageViewModels/DataBase/BackupDbSettingsVM.cs
delete mode 100644 QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
create mode 100644 QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
create mode 100644 QS.Launcher/ViewModels/PageViewModels/DataBase/DbOperationSettingsVM.cs
create mode 100644 QS.Launcher/ViewModels/PageViewModels/DataBase/IDbOperationSettings.cs
diff --git a/QS.Launcher.Avalonia/DependencyInjection.cs b/QS.Launcher.Avalonia/DependencyInjection.cs
index 30848bc44..7e2762d6b 100644
--- a/QS.Launcher.Avalonia/DependencyInjection.cs
+++ b/QS.Launcher.Avalonia/DependencyInjection.cs
@@ -10,10 +10,7 @@ public static partial class DependencyInjection {
public static IServiceCollection AddPages(this IServiceCollection services) {
return services
.AddSingleton()
- .AddSingleton()
- .AddSingleton()
- .AddTransient()
- .AddTransient()
+ .AddSingleton()
.AddSingleton();
}
}
diff --git a/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj b/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj
index 05c9a25da..1f19cf1fc 100644
--- a/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj
+++ b/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj
@@ -53,8 +53,11 @@
DataBasesView.axaml
-
- CreateDataBaseSettingsView.axaml
+
+ CreateDbSettingsView.axaml
+
+
+ BackupDbSettingsView.axaml
CreateDataBaseProgressView.axaml
diff --git a/QS.Launcher.Avalonia/Views/MainWindow.axaml b/QS.Launcher.Avalonia/Views/MainWindow.axaml
index 761d4a59b..901767110 100644
--- a/QS.Launcher.Avalonia/Views/MainWindow.axaml
+++ b/QS.Launcher.Avalonia/Views/MainWindow.axaml
@@ -17,7 +17,6 @@
diff --git a/QS.Launcher.Avalonia/Views/MainWindow.axaml.cs b/QS.Launcher.Avalonia/Views/MainWindow.axaml.cs
index d377b5ad7..3a8ab6b3b 100644
--- a/QS.Launcher.Avalonia/Views/MainWindow.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/MainWindow.axaml.cs
@@ -1,23 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using QS.Launcher.ViewModels;
-using System.Collections.Generic;
+using QS.Launcher.ViewModels.PageViewModels;
namespace QS.Launcher.Views;
-public partial class MainWindow : Window {
- public MainWindow(MainWindowVM vm, IEnumerable pages, LauncherOptions options) {
+public partial class MainWindow : Window
+{
+ private readonly PageViewLocator viewLocator;
+
+ public MainWindow(MainWindowVM vm, PageViewLocator viewLocator, LauncherOptions options) {
InitializeComponent();
+ this.viewLocator = viewLocator ?? throw new ArgumentNullException(nameof(viewLocator));
+
Icon = new WindowIcon(new Bitmap(new System.IO.MemoryStream(options.LogoIcon)));
Title = options.AppTitle;
Closing += (_, _) => vm.SaveConnections();
- foreach(var page in pages)
- carousel.Items.Add(page);
- vm.PagesCount = carousel.ItemCount;
+ // корневые страницы:
+ foreach(var page in vm.Pages)
+ carousel.Items.Add(viewLocator.Resolve(page));
+
+ vm.Pages.CollectionChanged += OnPagesChanged;
DataContext = vm;
}
+
+ ///
+ /// Поддерживает carousel.Items в соответствии со стеком
+ ///
+ private void OnPagesChanged(object? sender, NotifyCollectionChangedEventArgs e) {
+ switch(e.Action) {
+ case NotifyCollectionChangedAction.Add:
+ for(int i = 0; i < e.NewItems!.Count; i++)
+ carousel.Items.Insert(e.NewStartingIndex + i,
+ viewLocator.Resolve((CarouselPageVM)e.NewItems[i]!));
+ break;
+
+ case NotifyCollectionChangedAction.Remove:
+ for(int i = 0; i < e.OldItems!.Count; i++)
+ carousel.Items.RemoveAt(e.OldStartingIndex);
+ break;
+
+ case NotifyCollectionChangedAction.Reset:
+ carousel.Items.Clear();
+ foreach(var page in (IEnumerable)sender!)
+ carousel.Items.Add(viewLocator.Resolve(page));
+ break;
+ }
+ }
}
diff --git a/QS.Launcher.Avalonia/Views/PageViewLocator.cs b/QS.Launcher.Avalonia/Views/PageViewLocator.cs
new file mode 100644
index 000000000..d3fd92878
--- /dev/null
+++ b/QS.Launcher.Avalonia/Views/PageViewLocator.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Controls;
+using QS.Launcher.ViewModels.PageViewModels;
+using QS.Launcher.ViewModels.PageViewModels.DataBase;
+using QS.Launcher.Views.Pages;
+using QS.Launcher.Views.Pages.DataBase;
+
+namespace QS.Launcher.Views;
+
+///
+/// Сопоставляет VM страницы её View,
+/// чтобы добавить новый тип страницы, достаточно зарегистрировать здесь пару (VM, View)
+///
+public class PageViewLocator {
+ private readonly Dictionary> factories;
+
+ public PageViewLocator() {
+ factories = new Dictionary> {
+ [typeof(LoginVM)] = vm => new LoginView((LoginVM)vm),
+ [typeof(DataBasesVM)] = vm => new DataBasesView((DataBasesVM)vm),
+ [typeof(UserManagementVM)] = vm => new UserManagementView((UserManagementVM)vm),
+ [typeof(CreateDbSettingsVM)] = vm => new CreateDbSettingsView((CreateDbSettingsVM)vm),
+ [typeof(BackupDbSettingsVM)] = vm => new BackupDbSettingsView((BackupDbSettingsVM)vm),
+ [typeof(CreateDataBaseProgressVM)] = vm => new CreateDataBaseProgressView((CreateDataBaseProgressVM)vm),
+ };
+ }
+
+ public UserControl Resolve(CarouselPageVM page) {
+ if(page == null)
+ throw new ArgumentNullException(nameof(page));
+ if(factories.TryGetValue(page.GetType(), out var factory))
+ return factory(page);
+ throw new InvalidOperationException($"Не зарегистрирован View для страницы {page.GetType().Name}.");
+ }
+}
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/BackupDbSettingsView.axaml b/QS.Launcher.Avalonia/Views/Pages/DataBase/BackupDbSettingsView.axaml
new file mode 100644
index 000000000..7e3b813aa
--- /dev/null
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/BackupDbSettingsView.axaml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/DataBase/BackupDbSettingsView.axaml.cs
similarity index 57%
rename from QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml.cs
rename to QS.Launcher.Avalonia/Views/Pages/DataBase/BackupDbSettingsView.axaml.cs
index a3c3d11df..8da9f5fee 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/BackupDbSettingsView.axaml.cs
@@ -6,15 +6,15 @@
namespace QS.Launcher.Views.Pages.DataBase;
-public partial class CreateDataBaseSettingsView : UserControl {
- public CreateDataBaseSettingsView(CreateDataBaseSettingsVM settingsVM) {
+public partial class BackupDbSettingsView : UserControl {
+ public BackupDbSettingsView(BackupDbSettingsVM viewModel) {
InitializeComponent();
- DataContext = settingsVM;
+ DataContext = viewModel;
}
private async void BrowseBackupFile_OnClick(object? sender, RoutedEventArgs e) {
- if(DataContext is not CreateDataBaseSettingsVM vm)
+ if(DataContext is not BackupDbSettingsVM vm)
return;
var topLevel = TopLevel.GetTopLevel(this);
@@ -39,23 +39,4 @@ private async void BrowseBackupFile_OnClick(object? sender, RoutedEventArgs e) {
if(file != null)
vm.BackupFilePath = file.Path.LocalPath;
}
-
- private async void BrowseImportFile_OnClick(object? sender, RoutedEventArgs e) {
- if(DataContext is not CreateDataBaseSettingsVM vm)
- return;
-
- var topLevel = TopLevel.GetTopLevel(this);
- if(topLevel == null)
- return;
-
- var options = new FilePickerOpenOptions {
- Title = "Выбрать дамп базы данных",
- AllowMultiple = false,
- FileTypeFilter = new[] { new FilePickerFileType("SQL-скрипт") { Patterns = new[] { "*.sql" } } }
- };
-
- var files = await topLevel.StorageProvider.OpenFilePickerAsync(options);
- if(files.Count > 0)
- vm.ImportDumpFilePath = files[0].Path.LocalPath;
- }
}
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDbSettingsView.axaml
similarity index 62%
rename from QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml
rename to QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDbSettingsView.axaml
index dbb08c025..d8171d30f 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseSettingsView.axaml
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDbSettingsView.axaml
@@ -1,4 +1,4 @@
-
+
-
-
-
+
@@ -26,19 +25,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDbSettingsView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDbSettingsView.axaml.cs
new file mode 100644
index 000000000..9432dceba
--- /dev/null
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDbSettingsView.axaml.cs
@@ -0,0 +1,34 @@
+using System.IO;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Platform.Storage;
+using QS.Launcher.ViewModels.PageViewModels.DataBase;
+
+namespace QS.Launcher.Views.Pages.DataBase;
+
+public partial class CreateDbSettingsView : UserControl {
+ public CreateDbSettingsView(CreateDbSettingsVM viewModel) {
+ InitializeComponent();
+
+ DataContext = viewModel;
+ }
+
+ private async void BrowseImportFile_OnClick(object? sender, RoutedEventArgs e) {
+ if(DataContext is not CreateDbSettingsVM vm)
+ return;
+
+ var topLevel = TopLevel.GetTopLevel(this);
+ if(topLevel == null)
+ return;
+
+ var options = new FilePickerOpenOptions {
+ Title = "Выбрать дамп базы данных",
+ AllowMultiple = false,
+ FileTypeFilter = new[] { new FilePickerFileType("SQL-скрипт") { Patterns = new[] { "*.sql" } } }
+ };
+
+ var files = await topLevel.StorageProvider.OpenFilePickerAsync(options);
+ if(files.Count > 0)
+ vm.ImportDumpFilePath = files[0].Path.LocalPath;
+ }
+}
diff --git a/QS.Launcher/DependencyInjection.cs b/QS.Launcher/DependencyInjection.cs
index 07bbf750b..834e50d6c 100644
--- a/QS.Launcher/DependencyInjection.cs
+++ b/QS.Launcher/DependencyInjection.cs
@@ -16,9 +16,8 @@ public static IServiceCollection AddLauncherViewModels(this IServiceCollection s
.AddSingleton()
.AddSingleton()
.AddSingleton()
- // Wizard-страницы создания БД и операций с базой
- .AddSingleton()
- .AddSingleton()
+ // Страница прогресса создаётся заново на каждую операцию с базой
+ .AddTransient()
.AddSingleton();
}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/BackupDbSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/BackupDbSettingsVM.cs
new file mode 100644
index 000000000..9377ed7e2
--- /dev/null
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/BackupDbSettingsVM.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reactive.Linq;
+using QS.DbManagement;
+using ReactiveUI;
+
+namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
+ public class BackupDbSettingsVM : DbOperationSettingsVM {
+ private readonly DbInfo database;
+
+ public BackupDbSettingsVM(DbInfo database, IDbProvider provider, Connection connection, IServiceProvider services)
+ : base(provider, connection, services) {
+ this.database = database ?? throw new ArgumentNullException(nameof(database));
+
+ BackupTargetTitle = string.IsNullOrEmpty(database.BaseName) ? database.Title : database.BaseName;
+ BackupFilePath = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
+ "Backups",
+ string.Format("{0}-{1:yyMMdd-HHmm}.sql", BackupTargetTitle, DateTime.Now));
+
+ SetValidity(this.WhenAnyValue(x => x.BackupFilePath, path => !string.IsNullOrWhiteSpace(path)));
+ }
+
+ public override string Title => "Создание резервной копии базы данных";
+
+ public string BackupTargetTitle { get; }
+
+ private string backupFilePath;
+ public string BackupFilePath {
+ get => backupFilePath;
+ set => this.RaiseAndSetIfChanged(ref backupFilePath, value);
+ }
+
+ public override IEnumerable BuildPipeline() {
+ return new[] {
+ new DbCreationPhase(
+ "Создание резервной копии базы данных",
+ args => {
+ args.Provider.BackupDatabase(database, BackupFilePath, args.Progress, args.CancellationToken);
+ args.CancellationToken.ThrowIfCancellationRequested();
+ return true;
+ })
+ };
+ }
+ }
+}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
deleted file mode 100644
index 02f3834ed..000000000
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseSettingsVM.cs
+++ /dev/null
@@ -1,189 +0,0 @@
-using System;
-using System.IO;
-using System.Reactive;
-using System.Reactive.Linq;
-using Microsoft.Extensions.DependencyInjection;
-using QS.DbManagement;
-using QS.DBScripts.Controllers;
-using ReactiveUI;
-
-namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
- public enum DbWizardOperation {
- Create,
- Backup
- }
-
- ///
- /// Универсальная страница настроек операции с базой: ввод параметров создания базы
- /// (с необязательным импортом дампа для MariaDB) либо выбор файла для резервной копии.
- /// Конкретную операцию (состав пайплайна фаз) строит .
- ///
- public class CreateDataBaseSettingsVM : CarouselPageVM {
- public IDbProvider Provider { get; private set; }
- public Connection Connection { get; private set; }
- private readonly IServiceProvider services;
-
- private DbInfo backupTarget;
-
- private DbWizardOperation operation = DbWizardOperation.Create;
- public DbWizardOperation Operation {
- get => operation;
- private set {
- this.RaiseAndSetIfChanged(ref operation, value);
- this.RaisePropertyChanged(nameof(IsCreateMode));
- this.RaisePropertyChanged(nameof(IsBackupMode));
- this.RaisePropertyChanged(nameof(CanImportDump));
- }
- }
-
- public bool IsCreateMode => Operation == DbWizardOperation.Create;
- public bool IsBackupMode => Operation == DbWizardOperation.Backup;
-
- ///
- /// Импорт дампа при создании: MariaDB напрямую, облако - через свой creator
- /// (доступен, когда тип подключения поддерживает создание, т.е. есть creator).
- ///
- public bool CanImportDump => Operation == DbWizardOperation.Create
- && (Provider is MariaDBProvider
- || Connection?.ConnectionType?.SupportsDatabaseCreation(services) == true);
-
- #region Создание
-
- private string dbTitle;
- public string DbTitle {
- get => dbTitle;
- set => this.RaiseAndSetIfChanged(ref dbTitle, value);
- }
-
- private string dbName;
- public string DbName {
- get => dbName;
- set => this.RaiseAndSetIfChanged(ref dbName, value);
- }
-
- /// Необязательный путь к SQL-дампу, заливаемому в созданную базу (только MariaDB).
- private string importDumpFilePath;
- public string ImportDumpFilePath {
- get => importDumpFilePath;
- set => this.RaiseAndSetIfChanged(ref importDumpFilePath, value);
- }
-
- #endregion
-
- #region Резервная копия
-
- private string backupTargetTitle;
- public string BackupTargetTitle {
- get => backupTargetTitle;
- private set => this.RaiseAndSetIfChanged(ref backupTargetTitle, value);
- }
-
- private string backupFilePath;
- public string BackupFilePath {
- get => backupFilePath;
- set => this.RaiseAndSetIfChanged(ref backupFilePath, value);
- }
-
- #endregion
-
- public ReactiveCommand ProceedCommand { get; }
- public ReactiveCommand CancelCommand { get; }
-
- ///
- /// Сообщает заинтересованным о том, что только что создана
- /// progress-VM и пора подписаться на её события
- ///
- public event Action ProgressPageRequested;
-
- public CreateDataBaseSettingsVM(IServiceProvider services) {
- this.services = services ?? throw new ArgumentNullException(nameof(services));
-
- var canProceed = this.WhenAnyValue(
- x => x.Operation, x => x.DbName, x => x.DbTitle, x => x.BackupFilePath,
- (op, name, title, path) => op == DbWizardOperation.Create
- ? !string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(title)
- : !string.IsNullOrWhiteSpace(path));
-
- ProceedCommand = ReactiveCommand.Create(GoToProgress, canProceed);
- CancelCommand = ReactiveCommand.Create(() => PopPageCommand?.Execute(null));
- }
-
- public void SetDbSettings(IDbProvider provider, Connection connection) {
- Provider = provider ?? throw new ArgumentNullException(nameof(provider));
- Connection = connection ?? throw new ArgumentNullException(nameof(connection));
- ImportDumpFilePath = null;
- Operation = DbWizardOperation.Create;
- }
-
- public void SetBackupSettings(IDbProvider provider, Connection connection, DbInfo database) {
- Provider = provider ?? throw new ArgumentNullException(nameof(provider));
- Connection = connection ?? throw new ArgumentNullException(nameof(connection));
- backupTarget = database ?? throw new ArgumentNullException(nameof(database));
- Operation = DbWizardOperation.Backup;
-
- BackupTargetTitle = database.Title;
- // У облачного DbInfo нет BaseName - подставляем Title в имя файла.
- var fileBaseName = string.IsNullOrEmpty(database.BaseName) ? database.Title : database.BaseName;
- BackupFilePath = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
- "Резервные копии",
- string.Format("{0}-{1:yyMMdd-HHmm}.sql", fileBaseName, DateTime.Now));
- }
-
- private void GoToProgress() {
- var progress = ActivatorUtilities.GetServiceOrCreateInstance(services);
-
- DbCreationPhase[] pipeline;
- if(Operation == DbWizardOperation.Backup) {
- progress.OperationTitle = "Создание резервной копии базы данных";
- pipeline = new[] {
- new DbCreationPhase(
- "Создание резервной копии базы данных",
- args => {
- // BackupDatabase теперь на IDbProvider - работает и для MariaDB, и для облака.
- args.Provider.BackupDatabase(
- backupTarget, BackupFilePath, args.Progress, args.CancellationToken);
- args.CancellationToken.ThrowIfCancellationRequested();
- return true;
- })
- };
- }
- else {
- progress.OperationTitle = "Создание базы данных";
- var phases = new System.Collections.Generic.List {
- new DbCreationPhase(
- "Создание базы данных",
- args => args.Provider.CreateDatabase(DbName, DbTitle, services))
- };
-
- if(!string.IsNullOrWhiteSpace(ImportDumpFilePath) && Provider is MariaDBProvider) {
- // Залить выбранный дамп вместо стандартного наполнения скриптом.
- phases.Add(new DbCreationPhase(
- "Импорт дампа в базу данных",
- args => {
- ((MariaDBProvider)args.Provider).ImportDatabase(
- DbName, ImportDumpFilePath, args.Progress, args.CancellationToken);
- args.CancellationToken.ThrowIfCancellationRequested();
- return true;
- }));
- }
- else if(Connection.ConnectionType.SupportsDatabaseCreation(services)) {
- phases.Add(new DbCreationPhase(
- "Наполнение базы данных",
- args => {
- // Передаём дамп в creator: облачный creator зальёт его вместо скрипта.
- args.ImportDumpFilePath = ImportDumpFilePath;
- IDbCreatorModel creator = Connection.ConnectionType.CreateCreator(args);
- return creator.RunCreation(DbName, DbTitle);
- }));
- }
-
- pipeline = phases.ToArray();
- }
-
- progress.SetPipeline(Provider, Connection, pipeline);
- ProgressPageRequested?.Invoke(progress);
- PushPageCommand?.Execute(progress);
- }
- }
-}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
new file mode 100644
index 000000000..c1557db68
--- /dev/null
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive.Linq;
+using QS.DbManagement;
+using QS.DBScripts.Controllers;
+using ReactiveUI;
+
+namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
+ public class CreateDbSettingsVM : DbOperationSettingsVM {
+ public CreateDbSettingsVM(IDbProvider provider, Connection connection, IServiceProvider services)
+ : base(provider, connection, services) {
+ SetValidity(this.WhenAnyValue(x => x.DbName, x => x.DbTitle,
+ (name, title) => !string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(title)));
+ }
+
+ public override string Title => "Создание базы данных";
+
+ private string dbTitle;
+ public string DbTitle {
+ get => dbTitle;
+ set => this.RaiseAndSetIfChanged(ref dbTitle, value);
+ }
+
+ private string dbName;
+ public string DbName {
+ get => dbName;
+ set => this.RaiseAndSetIfChanged(ref dbName, value);
+ }
+
+ private string importDumpFilePath;
+ public string ImportDumpFilePath {
+ get => importDumpFilePath;
+ set => this.RaiseAndSetIfChanged(ref importDumpFilePath, value);
+ }
+
+ public bool CanImportDump => Provider is MariaDBProvider
+ || Connection?.ConnectionType?.SupportsDatabaseCreation(Services) == true;
+
+ public override IEnumerable BuildPipeline() {
+ var phases = new List {
+ new DbCreationPhase(
+ "Создание базы данных",
+ args => args.Provider.CreateDatabase(DbName, DbTitle, Services))
+ };
+
+ if(!string.IsNullOrWhiteSpace(ImportDumpFilePath) && Provider is MariaDBProvider) {
+ phases.Add(new DbCreationPhase(
+ "Импорт дампа в базу данных",
+ args => {
+ ((MariaDBProvider)args.Provider).ImportDatabase(
+ DbName, ImportDumpFilePath, args.Progress, args.CancellationToken);
+ args.CancellationToken.ThrowIfCancellationRequested();
+ return true;
+ }));
+ }
+ else if(Connection.ConnectionType.SupportsDatabaseCreation(Services)) {
+ phases.Add(new DbCreationPhase(
+ "Наполнение базы данных",
+ args => {
+ args.ImportDumpFilePath = ImportDumpFilePath;
+ IDbCreatorModel creator = Connection.ConnectionType.CreateCreator(args);
+ return creator.RunCreation(DbName, DbTitle);
+ }));
+ }
+
+ return phases;
+ }
+ }
+}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
index 23b5524ea..a9b3bae36 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
@@ -3,9 +3,9 @@
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
+using System.Threading.Tasks;
using System.Windows.Input;
using DynamicData.Kernel;
-using Microsoft.Extensions.DependencyInjection;
using QS.DbManagement;
using QS.Dialog;
using QS.Launcher.AppRunner;
@@ -33,21 +33,20 @@ public IDbProvider Provider {
}
///
- /// можно создать базу если есть права пользователя на создание И
- /// либо тип подключения поддерживает создание скриптом (есть фабрика + скрипт),
- /// либо это MariaDB (наполнение возможно импортом дампа в мастере)
+ /// можно создать базу только если:
+ /// есть права пользователя на создание
+ /// тип подключения поддерживает создание в текущем окружении
+ /// задана фабрика и зарегистрирован скрипт создания
///
public bool CanCreateDatabase =>
provider != null
&& provider.CanCreateDatabase
- && (currentConnection?.ConnectionType?.SupportsDatabaseCreation(serviceProvider) == true
- || provider is MariaDBProvider);
+ && (currentConnection?.ConnectionType?.SupportsDatabaseCreation(serviceProvider) == true);
///
- /// Управление базой (резервная копия, удаление) идёт через IDbProvider -
- /// поддержано и для MariaDB, и для облака. Видно при активном подключении.
+ /// резервная копия, удаление
///
- public bool CanManageDatabases => provider != null;
+ public bool CanManageDatabases => provider != null;//может надо сделать чисто на удаление
public Connection CurrentConnection => currentConnection;
@@ -117,71 +116,55 @@ public DataBasesVM(
}
///
- /// Мастер настроек/прогресса един для всех операций с базой - резолвим один раз
- /// и один раз подписываемся на завершение операции (без накопления подписок).
- ///
- private CreateDataBaseSettingsVM settingsWizard;
- private CreateDataBaseSettingsVM SettingsWizard {
- get {
- if(settingsWizard == null) {
- settingsWizard = ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider);
- settingsWizard.ProgressPageRequested += progressVm => {
- progressVm.OperationCompleted -= OnWizardOperationCompleted;
- progressVm.OperationCompleted += OnWizardOperationCompleted;
- };
- }
- return settingsWizard;
- }
- }
-
- ///
- /// открывает мастер создания базы, по завершении возвращает фокус на и обновляет список баз
+ /// открывает страницу создания базы; по завершении возвращает фокус на и обновляет список баз
///
private void OpenCreateDatabase() {
if(!CanCreateDatabase)
return;
- SettingsWizard.SetDbSettings(Provider, CurrentConnection);
- PushPageCommand?.Execute(SettingsWizard);
+ var settings = new CreateDbSettingsVM(Provider, CurrentConnection, serviceProvider);
+ settings.OperationCompleted += () => OnOperationCompleted(settings);
+ PushPageCommand?.Execute(settings);
}
///
- /// открывает мастер резервного копирования выбранной базы
+ /// открывает страницу резервного копирования выбранной базы
///
private void OpenBackup(DbInfo database) {
if(database == null || !CanManageDatabases)
return;
- SettingsWizard.SetBackupSettings(Provider, CurrentConnection, database);
- PushPageCommand?.Execute(SettingsWizard);
+ var settings = new BackupDbSettingsVM(database, Provider, CurrentConnection, serviceProvider);
+ settings.OperationCompleted += () => OnOperationCompleted(settings);
+ PushPageCommand?.Execute(settings);
}
- private void OnWizardOperationCompleted() {
- // Закрываем все wizard-страницы и возвращаемся на DataBasesVM.
+ private void OnOperationCompleted(DbOperationSettingsVM operation) {
+ // Закрываем все нерутовые страницы и возвращаемся на DataBasesVM
PopToRootCommand?.Execute(null);
RefreshDatabases();
- if(settingsWizard?.Operation == DbWizardOperation.Backup)
+ if(operation is BackupDbSettingsVM backup)
interactiveMessage.ShowMessage(ImportanceLevel.Success,
- $"Резервная копия базы данных сохранена:\n{settingsWizard.BackupFilePath}",
+ $"Резервная копия базы данных сохранена:\n{backup.BackupFilePath}",
"Резервное копирование");
}
- private async System.Threading.Tasks.Task DeleteDatabaseAsync(DbInfo database) {
+ private async Task DeleteDatabaseAsync(DbInfo database) {
if(database == null || !CanManageDatabases)
return;
- // Question() кидает исключение на UI-потоке, поэтому диалог и удаление выполняем в фоне.
- bool confirmed = await System.Threading.Tasks.Task.Run(() => interactiveQuestion.Question(
+ // Question кидает исключение на UIпотоке, поэтому диалог и удаление выполняем в фоне
+ bool confirmed = await Task.Run(() => interactiveQuestion.Question(
$"Безвозвратно удалить базу данных «{database.Title}»?", "Удаление базы данных"));
if(!confirmed)
return;
try {
- await System.Threading.Tasks.Task.Run(() => provider.DropDatabase(database));
+ await Task.Run(() => provider.DropDatabase(database));
RefreshDatabases();
interactiveMessage.ShowMessage(ImportanceLevel.Success,
- $"База данных «{database.Title}» удалена.", "Удаление базы данных");
+ $"База данных {database.Title} удалена.", "Удаление базы данных");
}
catch(Exception ex) {
logger.Error(ex, "Не удалось удалить базу {0}", database.BaseName);
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/DbOperationSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/DbOperationSettingsVM.cs
new file mode 100644
index 000000000..8fd0873f5
--- /dev/null
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/DbOperationSettingsVM.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive;
+using System.Reactive.Linq;
+using Microsoft.Extensions.DependencyInjection;
+using QS.DbManagement;
+using ReactiveUI;
+
+namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
+ public abstract class DbOperationSettingsVM : CarouselPageVM, IDbOperationSettings {
+ protected IDbProvider Provider { get; }
+ protected Connection Connection { get; }
+ protected IServiceProvider Services { get; }
+
+ protected DbOperationSettingsVM(IDbProvider provider, Connection connection, IServiceProvider services) {
+ Provider = provider ?? throw new ArgumentNullException(nameof(provider));
+ Connection = connection ?? throw new ArgumentNullException(nameof(connection));
+ Services = services ?? throw new ArgumentNullException(nameof(services));
+
+ CancelCommand = ReactiveCommand.Create(() => PopPageCommand?.Execute(null));
+ }
+
+ public abstract string Title { get; }
+
+ public IObservable CanProceed { get; private set; }
+
+ public abstract IEnumerable BuildPipeline();
+
+ public ReactiveCommand ProceedCommand { get; private set; }
+ public ReactiveCommand CancelCommand { get; }
+ public event Action OperationCompleted;
+
+ protected void SetValidity(IObservable canProceed) {
+ CanProceed = canProceed ?? Observable.Return(true);
+ ProceedCommand = ReactiveCommand.Create(GoToProgress, CanProceed);
+ }
+
+ private void GoToProgress() {
+ var progress = ActivatorUtilities.GetServiceOrCreateInstance(Services);
+ progress.OperationTitle = Title;
+ progress.SetPipeline(Provider, Connection, BuildPipeline());
+ progress.OperationCompleted += () => OperationCompleted?.Invoke();
+ PushPageCommand?.Execute(progress);
+ }
+ }
+}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/IDbOperationSettings.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/IDbOperationSettings.cs
new file mode 100644
index 000000000..7caf32ce3
--- /dev/null
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/IDbOperationSettings.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+
+namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
+ ///
+ /// Одна операция мастера настроек базы
+ ///
+ public interface IDbOperationSettings {
+ /// Заголовок страницы
+ string Title { get; }
+
+ /// валидность ввода операции
+ IObservable CanProceed { get; }
+
+ /// состав фаз операции
+ IEnumerable BuildPipeline();
+ }
+}
From 64c0ae04fdbcc8f9d820390c154bd3c2dde0b6c8 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Fri, 19 Jun 2026 15:11:01 +0300
Subject: [PATCH 024/135] =?UTF-8?q?=D0=B4=D0=BE=D1=80=D0=B0=D0=B1=D0=BE?=
=?UTF-8?q?=D1=82=D0=BA=D0=B8=20=D0=BF=D0=BE=20=D1=82=D0=B5=D1=81=D1=82?=
=?UTF-8?q?=D0=B0=D0=BC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
QS.Cloud.Client/DataBase/QsCloudDbCreator.cs | 2 +-
QS.DbManagement/MariaDb/MariaDBProvider.cs | 8 ++++----
QS.DbManagement/MariaDb/MariaDbDumpService.cs | 17 ++++++++++++++++-
QS.Launcher/ViewModels/MainWindowVM.cs | 10 +++++++---
.../ViewModels/PageViewModels/CarouselPageVM.cs | 8 +++++---
.../DataBase/CreateDbSettingsVM.cs | 2 +-
.../PageViewModels/DataBase/DataBasesVM.cs | 2 +-
7 files changed, 35 insertions(+), 14 deletions(-)
diff --git a/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs b/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
index 7948b29db..b62528169 100644
--- a/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
+++ b/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
@@ -56,7 +56,7 @@ public bool RunCreation(string dbName, string dbTitle) {
if(!string.IsNullOrWhiteSpace(importDumpFilePath)) {
// Наполнение импортом выбранного дампа вместо встроенного скрипта.
- new MariaDbDumpService().Import(session.ConnectionStringBuilder, session.Db.BaseName, importDumpFilePath, progress, cancellationToken);
+ new MariaDbDumpService().Import(session.ConnectionStringBuilder, session.Db.BaseName, importDumpFilePath, progress, cancellationToken, dbTitle);
return true;
}
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index 08d88e655..4deabe854 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -191,7 +191,7 @@ public bool DropDatabase(DbInfo database) {
}
///
- /// Резервное копирование базы в SQL-скрипт
+ /// Резервное копирование базы в скрипт
/// Метод блокирующий - вызывать из фонового потока
///
public void BackupDatabase(DbInfo database, string filePath, IProgressBarDisplayable progress, CancellationToken cancellation) {
@@ -199,11 +199,11 @@ public void BackupDatabase(DbInfo database, string filePath, IProgressBarDisplay
}
///
- /// Импорт SQL-дампа в уже созданную базу
+ /// Импорт дампа в уже созданную базу
/// Метод блокирующий - вызывать из фонового потока
///
- public void ImportDatabase(string databaseName, string filePath, IProgressBarDisplayable progress, CancellationToken cancellation) {
- new MariaDbDumpService().Import(ConnectionStringBuilder, databaseName, filePath, progress, cancellation);
+ public void ImportDatabase(string databaseName, string filePath, IProgressBarDisplayable progress, CancellationToken cancellation, string dbTitle = null) {
+ new MariaDbDumpService().Import(ConnectionStringBuilder, databaseName, filePath, progress, cancellation, dbTitle);
}
public void Dispose() {
diff --git a/QS.DbManagement/MariaDb/MariaDbDumpService.cs b/QS.DbManagement/MariaDb/MariaDbDumpService.cs
index d9381585d..f30584977 100644
--- a/QS.DbManagement/MariaDb/MariaDbDumpService.cs
+++ b/QS.DbManagement/MariaDb/MariaDbDumpService.cs
@@ -51,7 +51,9 @@ public void Import(
string databaseName,
string filePath,
IProgressBarDisplayable progress,
- CancellationToken cancellation) {
+ CancellationToken cancellation,
+ string title = null)
+ {
if(string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException("Не указан путь к файлу дампа.", nameof(filePath));
if(!File.Exists(filePath))
@@ -73,6 +75,19 @@ public void Import(
progress?.Update(e.CurrentBytes);
};
backup.ImportFromFile(filePath);
+
+ if(!string.IsNullOrEmpty(title))
+ {
+ progress?.Update("Вставляем BaseTitle");
+ backup.Command.CommandText = @"INSERT INTO base_parameters (name, str_value)
+ VALUES ('BaseTitle', @title)
+ ON DUPLICATE KEY UPDATE
+ str_value = VALUES(str_value);";
+ backup.Command.Parameters.Clear();
+ backup.Command.Parameters.AddWithValue("@title", title);
+ backup.Command.ExecuteNonQuery();
+ progress?.Update("Новый BaseTitle вставлен");
+ }
});
}
diff --git a/QS.Launcher/ViewModels/MainWindowVM.cs b/QS.Launcher/ViewModels/MainWindowVM.cs
index 5061791fd..8e881b67d 100644
--- a/QS.Launcher/ViewModels/MainWindowVM.cs
+++ b/QS.Launcher/ViewModels/MainWindowVM.cs
@@ -49,6 +49,13 @@ private void WirePage(CarouselPageVM page) {
page.PushPageCommand = ReactiveCommand.Create(PushPage);
page.PopPageCommand = ReactiveCommand.Create(PopPage);
page.PopToRootCommand = ReactiveCommand.Create(PopToRoot);
+ page.PopToPageCommand = ReactiveCommand.Create(type => {
+ var method = GetType()
+ .GetMethod(nameof(PopToPage))
+ .MakeGenericMethod(type);
+
+ method.Invoke(this, null);
+ });
}
public void SaveConnections() {
@@ -91,9 +98,6 @@ public void PopToRoot() {
SelectedPageIndex = rootPagesCount - 1;
}
- ///
- /// Найти первую страницу указанного типа в стеке и переключиться на неё, сняв всё, что стоит выше
- ///
public void PopToPage() where TPage : CarouselPageVM {
int targetIdx = -1;
for(int i = 0; i < Pages.Count; i++) {
diff --git a/QS.Launcher/ViewModels/PageViewModels/CarouselPageVM.cs b/QS.Launcher/ViewModels/PageViewModels/CarouselPageVM.cs
index 1d865db8b..bc034ea10 100644
--- a/QS.Launcher/ViewModels/PageViewModels/CarouselPageVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/CarouselPageVM.cs
@@ -2,9 +2,6 @@
using QS.ViewModels;
namespace QS.Launcher.ViewModels.PageViewModels {
- ///
- /// NextPage/PreviousPage/ChangePage — кольцевая навигация по корневым страницам
- ///
public class CarouselPageVM : ViewModelBase {
public ICommand NextPageCommand { get; set; }
@@ -26,5 +23,10 @@ public class CarouselPageVM : ViewModelBase {
/// Закрыть все нерутовые страницы и вернуться к корневым вкладкам
///
public ICommand PopToRootCommand { get; set; }
+
+ ///
+ /// Найти первую страницу указанного типа в стеке и переключиться на неё, сняв всё, что стоит выше
+ ///
+ public ICommand PopToPageCommand { get; set; }
}
}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
index c1557db68..4e4c5b89a 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
@@ -48,7 +48,7 @@ public override IEnumerable BuildPipeline() {
"Импорт дампа в базу данных",
args => {
((MariaDBProvider)args.Provider).ImportDatabase(
- DbName, ImportDumpFilePath, args.Progress, args.CancellationToken);
+ DbName, ImportDumpFilePath, args.Progress, args.CancellationToken, DbTitle);
args.CancellationToken.ThrowIfCancellationRequested();
return true;
}));
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
index a9b3bae36..7748d4967 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
@@ -141,7 +141,7 @@ private void OpenBackup(DbInfo database) {
private void OnOperationCompleted(DbOperationSettingsVM operation) {
// Закрываем все нерутовые страницы и возвращаемся на DataBasesVM
- PopToRootCommand?.Execute(null);
+ PopToPageCommand?.Execute(GetType());
RefreshDatabases();
if(operation is BackupDbSettingsVM backup)
From 9ea6cdba2d14d547862b66f2ea7786542010e0f5 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Fri, 19 Jun 2026 15:54:57 +0300
Subject: [PATCH 025/135] =?UTF-8?q?=D0=BE=D1=87=D0=B8=D1=81=D1=82=D0=B8?=
=?UTF-8?q?=D0=BB=20=D0=BE=D1=82=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8?=
=?UTF-8?q?=D0=BC=D0=BE=D1=81=D1=82=D0=B5=D0=B9=20=D0=B8=D0=BC=D0=BF=D0=BE?=
=?UTF-8?q?=D1=80=D1=82=20=D0=B2=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE?=
=?UTF-8?q?=D0=B9=D0=BA=D0=B0=D1=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../DataBase/QsCloudConnectionTypeBase.cs | 3 ++
QS.DbManagement/ConnectionTypeBase.cs | 19 ++++++++--
QS.DbManagement/MariaDb/MariaDBProvider.cs | 8 -----
.../MariaDb/MariaDbConnectionTypeBase.cs | 6 ++++
QS.DbManagement/MariaDb/MariaDbImportModel.cs | 36 +++++++++++++++++++
.../DataBase/CreateDbSettingsVM.cs | 13 +++----
6 files changed, 67 insertions(+), 18 deletions(-)
create mode 100644 QS.DbManagement/MariaDb/MariaDbImportModel.cs
diff --git a/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs b/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
index 3f41ee176..451cd9d45 100644
--- a/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
+++ b/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
@@ -33,6 +33,9 @@ public QsCloudConnectionTypeBase() {
args.CancellationToken,
args.ImportDumpFilePath);
};
+
+ // QsCloudDbCreator сам импортирует дамп, когда задан ImportDumpFilePath
+ ImportFactory = CreatorFactory;
}
public override bool CanConnect(IEnumerable parameters) {
diff --git a/QS.DbManagement/ConnectionTypeBase.cs b/QS.DbManagement/ConnectionTypeBase.cs
index 4a96e350c..7042f34f7 100644
--- a/QS.DbManagement/ConnectionTypeBase.cs
+++ b/QS.DbManagement/ConnectionTypeBase.cs
@@ -21,6 +21,8 @@ public abstract class ConnectionTypeBase {
public Func CreatorFactory { get; set; }
+ public Func ImportFactory { get; set; }
+
///
/// Создание базы доступно, только если задана фабрика и приложение
/// зарегистрировало конфигурацию скриптов с реальным скриптом создания
@@ -30,13 +32,26 @@ public virtual bool SupportsDatabaseCreation(IServiceProvider services) {
&& services.GetService()?.HasCreationScript() == true;
}
+ ///
+ /// Импорт дампа доступен, если тип подключения умеет наполнять базу из файла
+ ///
+ public virtual bool SupportsDatabaseImport(IServiceProvider services) {
+ return ImportFactory != null;
+ }
+
public IDbCreatorModel CreateCreator(CreatorFactoryArgs args) {
if(CreatorFactory == null)
throw new InvalidOperationException(
- $"Для типа подключения '{ConnectionTypeName}' не настроена фабрика создания БД (CreatorFactory). "
- + "Заполните её в композиционном корне приложения.");
+ $"Для типа подключения '{ConnectionTypeName}' не настроена фабрика создания БД");
return CreatorFactory(args);
}
+
+ public IDbCreatorModel CreateImporter(CreatorFactoryArgs args) {
+ if(ImportFactory == null)
+ throw new InvalidOperationException(
+ $"Для типа подключения '{ConnectionTypeName}' не настроена фабрика импорта дампа");
+ return ImportFactory(args);
+ }
}
///
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index 4deabe854..464625b74 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -198,14 +198,6 @@ public void BackupDatabase(DbInfo database, string filePath, IProgressBarDisplay
new MariaDbDumpService().Export(ConnectionStringBuilder, database.BaseName, filePath, progress, cancellation);
}
- ///
- /// Импорт дампа в уже созданную базу
- /// Метод блокирующий - вызывать из фонового потока
- ///
- public void ImportDatabase(string databaseName, string filePath, IProgressBarDisplayable progress, CancellationToken cancellation, string dbTitle = null) {
- new MariaDbDumpService().Import(ConnectionStringBuilder, databaseName, filePath, progress, cancellation, dbTitle);
- }
-
public void Dispose() {
connection?.Dispose();
}
diff --git a/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs b/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs
index 8b561cd6b..b1590b200 100644
--- a/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs
+++ b/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs
@@ -27,6 +27,12 @@ public MariaDbConnectionTypeBase() {
scripts.MakeCreationScript(), args.Progress, args.Interaction, args.CancellationToken) { FillBaseGuid = false };
};
+ ImportFactory = args => {
+ var p = (MariaDBProvider)args.Provider;
+ return new MariaDbImportModel(
+ p.ConnectionStringBuilder, args.ImportDumpFilePath, args.Progress, args.CancellationToken);
+ };
+
}
public override bool CanConnect(IEnumerable parameters) {
diff --git a/QS.DbManagement/MariaDb/MariaDbImportModel.cs b/QS.DbManagement/MariaDb/MariaDbImportModel.cs
new file mode 100644
index 000000000..f13b91b92
--- /dev/null
+++ b/QS.DbManagement/MariaDb/MariaDbImportModel.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Threading;
+using MySqlConnector;
+using QS.DBScripts.Controllers;
+using QS.Dialog;
+
+namespace QS.DbManagement {
+ ///
+ /// Наполнение MariaDB базы пользовательским дампом.
+ /// Метод блокирует вызывающий поток — выносить в фон ответственность вызывающего кода.
+ ///
+ public class MariaDbImportModel : IDbCreatorModel {
+ private readonly MySqlConnectionStringBuilder connectionStringBuilder;
+ private readonly string dumpFilePath;
+ private readonly IProgressBarDisplayable progress;
+ private readonly CancellationToken cancellation;
+
+ public MariaDbImportModel(
+ MySqlConnectionStringBuilder connectionStringBuilder,
+ string dumpFilePath,
+ IProgressBarDisplayable progress,
+ CancellationToken cancellation) {
+ this.connectionStringBuilder = connectionStringBuilder
+ ?? throw new ArgumentNullException(nameof(connectionStringBuilder));
+ this.dumpFilePath = dumpFilePath;
+ this.progress = progress;
+ this.cancellation = cancellation;
+ }
+
+ public bool RunCreation(string dbName, string dbTitle) {
+ new MariaDbDumpService().Import(connectionStringBuilder, dbName, dumpFilePath, progress, cancellation, dbTitle);
+ cancellation.ThrowIfCancellationRequested();
+ return true;
+ }
+ }
+}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
index 4e4c5b89a..cd8ef4845 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
@@ -33,8 +33,7 @@ public string ImportDumpFilePath {
set => this.RaiseAndSetIfChanged(ref importDumpFilePath, value);
}
- public bool CanImportDump => Provider is MariaDBProvider
- || Connection?.ConnectionType?.SupportsDatabaseCreation(Services) == true;
+ public bool CanImportDump => Connection?.ConnectionType?.SupportsDatabaseImport(Services) == true;
public override IEnumerable BuildPipeline() {
var phases = new List {
@@ -43,21 +42,19 @@ public override IEnumerable BuildPipeline() {
args => args.Provider.CreateDatabase(DbName, DbTitle, Services))
};
- if(!string.IsNullOrWhiteSpace(ImportDumpFilePath) && Provider is MariaDBProvider) {
+ if(!string.IsNullOrWhiteSpace(ImportDumpFilePath)
+ && Connection.ConnectionType.SupportsDatabaseImport(Services)) {
phases.Add(new DbCreationPhase(
"Импорт дампа в базу данных",
args => {
- ((MariaDBProvider)args.Provider).ImportDatabase(
- DbName, ImportDumpFilePath, args.Progress, args.CancellationToken, DbTitle);
- args.CancellationToken.ThrowIfCancellationRequested();
- return true;
+ args.ImportDumpFilePath = ImportDumpFilePath;
+ return Connection.ConnectionType.CreateImporter(args).RunCreation(DbName, DbTitle);
}));
}
else if(Connection.ConnectionType.SupportsDatabaseCreation(Services)) {
phases.Add(new DbCreationPhase(
"Наполнение базы данных",
args => {
- args.ImportDumpFilePath = ImportDumpFilePath;
IDbCreatorModel creator = Connection.ConnectionType.CreateCreator(args);
return creator.RunCreation(DbName, DbTitle);
}));
From 4dc5f97dc56cde963191f480a8a258d5b918db41 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Fri, 19 Jun 2026 18:00:45 +0300
Subject: [PATCH 026/135] =?UTF-8?q?=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=B0?=
=?UTF-8?q?=D1=86=D0=B8=D1=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
QS.Cloud.Client/DataBase/QSCloudProvider.cs | 4 +-
QS.DbManagement/ConnectionTypeBase.cs | 35 +++++++++++--
QS.DbManagement/IDbProvider.cs | 1 +
QS.DbManagement/MariaDb/MariaDBProvider.cs | 8 ++-
QS.DbManagement/MariaDb/MariaDbDumpService.cs | 5 +-
QS.DbManagement/ProviderResponces.cs | 1 -
QS.DbManagement/SqlDumpFileValidator.cs | 51 +++++++++++++++++++
.../Views/Pages/DataBase/DataBasesView.axaml | 6 ++-
.../DataBase/CreateDbSettingsVM.cs | 2 +-
.../PageViewModels/DataBase/DataBasesVM.cs | 28 +++++-----
10 files changed, 112 insertions(+), 29 deletions(-)
create mode 100644 QS.DbManagement/SqlDumpFileValidator.cs
diff --git a/QS.Cloud.Client/DataBase/QSCloudProvider.cs b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
index 8da08913e..254a1829b 100644
--- a/QS.Cloud.Client/DataBase/QSCloudProvider.cs
+++ b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
@@ -32,7 +32,8 @@ public class QSCloudProvider : IDbProvider {
#endregion
public string UserName { get; private set; }
- public bool CanCreateDatabase => dbClient.CanConnect;
+ public bool CanCreateDatabase => dbClient.CanConnect && IsAdmin;
+ public bool CanDropDatabase => CanCreateDatabase;
private LoginManagementCloudClient loginClient;
private DataBaseManagementCloudClient dbClient;
@@ -133,6 +134,7 @@ public LoginToServerResponse LoginToServer() {
try {
cloudResponce = loginClient.Start(Assembly.GetExecutingAssembly().GetName().Version.ToString());
+ IsAdmin = cloudResponce.YouAccountAdmin;
resp = new LoginToServerResponse {
Success = true,
IsAdmin = cloudResponce.YouAccountAdmin,
diff --git a/QS.DbManagement/ConnectionTypeBase.cs b/QS.DbManagement/ConnectionTypeBase.cs
index 7042f34f7..bee4254dc 100644
--- a/QS.DbManagement/ConnectionTypeBase.cs
+++ b/QS.DbManagement/ConnectionTypeBase.cs
@@ -24,8 +24,10 @@ public abstract class ConnectionTypeBase {
public Func ImportFactory { get; set; }
///
- /// Создание базы доступно, только если задана фабрика и приложение
- /// зарегистрировало конфигурацию скриптов с реальным скриптом создания
+ /// умеет создавать базу, если
+ /// задана фабрика,
+ /// в конфигурации есть скрипт создания.
+ /// НЕ учитывает права пользователя
///
public virtual bool SupportsDatabaseCreation(IServiceProvider services) {
return CreatorFactory != null
@@ -33,12 +35,39 @@ public virtual bool SupportsDatabaseCreation(IServiceProvider services) {
}
///
- /// Импорт дампа доступен, если тип подключения умеет наполнять базу из файла
+ /// умеет наполнять базу дампом, если задана фабрика
+ ///
+ /// НЕ учитывает права пользователя
///
public virtual bool SupportsDatabaseImport(IServiceProvider services) {
return ImportFactory != null;
}
+ #region Права пользователя по управлению базой
+
+ public bool CanCreateDatabase(IDbProvider provider, IServiceProvider services) {
+ return provider != null
+ && provider.CanCreateDatabase
+ && SupportsDatabaseCreation(services);
+ }
+
+ public bool CanImportDatabase(IDbProvider provider, IServiceProvider services) {
+ return provider != null
+ && provider.CanCreateDatabase
+ && SupportsDatabaseImport(services);
+ }
+
+ public bool CanBackupDatabase(IDbProvider provider) {
+ return provider != null
+ && provider.CanDropDatabase;
+ }
+
+ public bool CanDropDatabase(IDbProvider provider) {
+ return provider != null
+ && provider.CanDropDatabase;
+ }
+ #endregion
+
public IDbCreatorModel CreateCreator(CreatorFactoryArgs args) {
if(CreatorFactory == null)
throw new InvalidOperationException(
diff --git a/QS.DbManagement/IDbProvider.cs b/QS.DbManagement/IDbProvider.cs
index 3627c7823..b74c0fa55 100644
--- a/QS.DbManagement/IDbProvider.cs
+++ b/QS.DbManagement/IDbProvider.cs
@@ -31,5 +31,6 @@ public interface IDbProvider : IDisposable
bool IsAdmin { get; }
bool CanCreateDatabase { get; }
+ bool CanDropDatabase { get; }
}
}
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index 464625b74..f09aa393a 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -28,6 +28,7 @@ public class MariaDBProvider : IDbProvider {
public bool IsAdmin { get; private set; }
public bool CanCreateDatabase { get; private set; }
+ public bool CanDropDatabase { get; private set; }
///
/// Переданный в тайтл созданой базы,
@@ -88,11 +89,14 @@ public LoginToServerResponse LoginToServer() {
g.IndexOf("ALL PRIVILEGES", StringComparison.OrdinalIgnoreCase) >= 0
|| g.IndexOf("CREATE", StringComparison.OrdinalIgnoreCase) >= 0);
+ CanDropDatabase = IsAdmin || grants.Any(g =>
+ g.IndexOf("ALL PRIVILEGES", StringComparison.OrdinalIgnoreCase) >= 0
+ || g.IndexOf("DROP", StringComparison.OrdinalIgnoreCase) >= 0);
+
return new LoginToServerResponse {
Success = true,
IsAdmin = IsAdmin,
- NeedToUpdateLauncher = false,
- CanCreateDatabase = CanCreateDatabase
+ NeedToUpdateLauncher = false
};
}
catch(MySqlException ex) {
diff --git a/QS.DbManagement/MariaDb/MariaDbDumpService.cs b/QS.DbManagement/MariaDb/MariaDbDumpService.cs
index f30584977..7a521fe62 100644
--- a/QS.DbManagement/MariaDb/MariaDbDumpService.cs
+++ b/QS.DbManagement/MariaDb/MariaDbDumpService.cs
@@ -54,10 +54,7 @@ public void Import(
CancellationToken cancellation,
string title = null)
{
- if(string.IsNullOrWhiteSpace(filePath))
- throw new ArgumentException("Не указан путь к файлу дампа.", nameof(filePath));
- if(!File.Exists(filePath))
- throw new FileNotFoundException("Файл дампа не найден.", filePath);
+ SqlDumpFileValidator.EnsureLooksLikeSqlDump(filePath);
progress?.Update($"Импортируем дамп {filePath} в базу {databaseName}");
diff --git a/QS.DbManagement/ProviderResponces.cs b/QS.DbManagement/ProviderResponces.cs
index b9e6dfa62..b08cf0a57 100644
--- a/QS.DbManagement/ProviderResponces.cs
+++ b/QS.DbManagement/ProviderResponces.cs
@@ -9,7 +9,6 @@ public class Response {
public class LoginToServerResponse : Response
{
- public bool CanCreateDatabase { get; set; }
public bool IsAdmin { get; set; }
public bool NeedToUpdateLauncher { get; set; }
}
diff --git a/QS.DbManagement/SqlDumpFileValidator.cs b/QS.DbManagement/SqlDumpFileValidator.cs
new file mode 100644
index 000000000..44e1c34f8
--- /dev/null
+++ b/QS.DbManagement/SqlDumpFileValidator.cs
@@ -0,0 +1,51 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace QS.DbManagement {
+ public static class SqlDumpFileValidator {
+ private const int InspectBytes = 8 * 1024;
+
+ private static readonly string[] SqlStartTokens = {
+ "--", "/*", "#", "CREATE", "INSERT", "REPLACE", "DROP", "ALTER",
+ "SET ", "USE ", "LOCK", "DELIMITER", "START TRANSACTION"
+ };
+
+ ///
+ /// Бросает исключение, если файл отсутствует, пуст, бинарный или не начинается с SQL синтаксиса
+ ///
+ public static void EnsureLooksLikeSqlDump(string filePath) {
+ if(string.IsNullOrWhiteSpace(filePath))
+ throw new ArgumentException("Не указан путь к файлу дампа.", nameof(filePath));
+ if(!File.Exists(filePath))
+ throw new FileNotFoundException("Файл дампа не найден.", filePath);
+ if(new FileInfo(filePath).Length == 0)
+ throw new InvalidDataException("Файл дампа пуст.");
+
+ string head = ReadHead(filePath, InspectBytes);
+
+ // Бинарный файл (картинка/архив/документ) почти всегда содержит нулевые байты.
+ if(head.IndexOf('\0') >= 0)
+ throw new InvalidDataException(
+ "Файл не похож на SQL-дамп: это бинарный файл, а не текстовый SQL-скрипт.");
+
+ string trimmed = head.TrimStart('', ' ', '\t', '\r', '\n');
+ bool looksLikeSql = SqlStartTokens.Any(
+ token => trimmed.StartsWith(token, StringComparison.OrdinalIgnoreCase));
+ if(!looksLikeSql)
+ throw new InvalidDataException(
+ "Файл не похож на SQL-дамп: в начале файла нет SQL-инструкций. "
+ + "Выберите корректный файл дампа (.sql).");
+ }
+
+ private static string ReadHead(string filePath, int maxBytes) {
+ using(var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) {
+ int toRead = (int)Math.Min(maxBytes, stream.Length);
+ var buffer = new byte[toRead];
+ int read = stream.Read(buffer, 0, toRead);
+ return Encoding.UTF8.GetString(buffer, 0, read);
+ }
+ }
+ }
+}
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml
index 46481e656..9ad3f5eea 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml
@@ -31,8 +31,10 @@
IsVisible="{Binding $parent[ListBox].((vmdb:DataBasesVM)DataContext).CanManageDatabases}">
-
-
+
+
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
index cd8ef4845..3c0845db7 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
@@ -33,7 +33,7 @@ public string ImportDumpFilePath {
set => this.RaiseAndSetIfChanged(ref importDumpFilePath, value);
}
- public bool CanImportDump => Connection?.ConnectionType?.SupportsDatabaseImport(Services) == true;
+ public bool CanImportDump => Connection?.ConnectionType?.CanImportDatabase(Provider, Services) == true;
public override IEnumerable BuildPipeline() {
var phases = new List {
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
index 7748d4967..ab64918e6 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
@@ -26,27 +26,25 @@ public IDbProvider Provider {
Databases = provider.GetUserDatabases(applicationInfo).AsList();
this.RaisePropertyChanged(nameof(Databases));
this.RaisePropertyChanged(nameof(CanCreateDatabase));
+ this.RaisePropertyChanged(nameof(CanDropDatabase));
+ this.RaisePropertyChanged(nameof(CanBackupDatabase));
this.RaisePropertyChanged(nameof(CanManageDatabases));
LoadLastSelectedDatabase();
}
}
- ///
- /// можно создать базу только если:
- /// есть права пользователя на создание
- /// тип подключения поддерживает создание в текущем окружении
- /// задана фабрика и зарегистрирован скрипт создания
- ///
public bool CanCreateDatabase =>
- provider != null
- && provider.CanCreateDatabase
- && (currentConnection?.ConnectionType?.SupportsDatabaseCreation(serviceProvider) == true);
+ currentConnection?.ConnectionType?.CanCreateDatabase(provider, serviceProvider) == true;
- ///
- /// резервная копия, удаление
- ///
- public bool CanManageDatabases => provider != null;//может надо сделать чисто на удаление
+ public bool CanDropDatabase =>
+ currentConnection?.ConnectionType?.CanDropDatabase(provider) == true;
+
+ public bool CanBackupDatabase =>
+ currentConnection?.ConnectionType?.CanBackupDatabase(provider) == true;
+
+ public bool CanManageDatabases =>
+ CanDropDatabase || CanBackupDatabase;
public Connection CurrentConnection => currentConnection;
@@ -131,7 +129,7 @@ private void OpenCreateDatabase() {
/// открывает страницу резервного копирования выбранной базы
///
private void OpenBackup(DbInfo database) {
- if(database == null || !CanManageDatabases)
+ if(database == null || !CanBackupDatabase)
return;
var settings = new BackupDbSettingsVM(database, Provider, CurrentConnection, serviceProvider);
@@ -151,7 +149,7 @@ private void OnOperationCompleted(DbOperationSettingsVM operation) {
}
private async Task DeleteDatabaseAsync(DbInfo database) {
- if(database == null || !CanManageDatabases)
+ if(database == null || !CanDropDatabase)
return;
// Question кидает исключение на UIпотоке, поэтому диалог и удаление выполняем в фоне
From 96e4bfbd675a3c088a94591d879485a48a2b4b8a Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Sun, 28 Jun 2026 00:37:22 +0300
Subject: [PATCH 027/135] =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D1=80=D0=B0?=
=?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0?=
=?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20=D0=BF?=
=?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=B9=D0=B4=D0=B5=D1=80=20=D0=BF=D0=BE?=
=?UTF-8?q?=D0=BB=D0=BD=D0=BE=D1=81=D1=82=D1=8C=D1=8E?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
и стратегия на заполнение
---
QS.Cloud.Client/DataBase/QSCloudProvider.cs | 45 +++++++---
.../DataBase/QsCloudConnectionTypeBase.cs | 22 +----
QS.Cloud.Client/DataBase/QsCloudDbCreator.cs | 85 ------------------
QS.DbManagement/Connection.cs | 1 +
QS.DbManagement/ConnectionTypeBase.cs | 86 +------------------
QS.DbManagement/DbCapabilities.cs | 41 +++++++++
QS.DbManagement/DumpDbFillStrategy.cs | 23 +++++
.../{ => Entities}/ConnectionParameter.cs | 2 +-
.../ConnectionParameterValue.cs | 2 +-
.../{ => Entities}/DbCreationPhase.cs | 7 +-
QS.DbManagement/Entities/DbCreationRequest.cs | 22 +++++
QS.DbManagement/{ => Entities}/DbInfo.cs | 2 +-
QS.DbManagement/Entities/DbPhaseArgs.cs | 18 ++++
.../{ => Entities}/ProviderResponces.cs | 2 +-
QS.DbManagement/IDbFillStrategy.cs | 19 ++++
QS.DbManagement/IDbProvider.cs | 7 +-
QS.DbManagement/MariaDb/MariaDBProvider.cs | 27 +++---
.../MariaDb/MariaDbConnectionTypeBase.cs | 21 +----
QS.DbManagement/ScriptDbFillStrategy.cs | 23 +++++
.../Pages/DataBase/DataBasesView.axaml.cs | 2 +-
QS.Launcher/AppRunner/IAppRunner.cs | 2 +-
QS.Launcher/AppRunner/InProcessRunner.cs | 2 +-
QS.Launcher/AppRunner/NewProcessRunner.cs | 2 +-
QS.Launcher/DependencyInjection.cs | 4 +-
.../DataBase/BackupDbSettingsVM.cs | 1 +
.../DataBase/CreateDataBaseProgressVM.cs | 5 +-
.../DataBase/CreateDbSettingsVM.cs | 52 ++++++-----
.../PageViewModels/DataBase/DataBasesVM.cs | 15 ++--
.../DataBase/DbOperationSettingsVM.cs | 3 +-
.../DataBase/IDbOperationSettings.cs | 18 ----
QS.LibsTest.Core/Launcher/ConfiguratorTest.cs | 6 --
31 files changed, 258 insertions(+), 309 deletions(-)
delete mode 100644 QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
create mode 100644 QS.DbManagement/DbCapabilities.cs
create mode 100644 QS.DbManagement/DumpDbFillStrategy.cs
rename QS.DbManagement/{ => Entities}/ConnectionParameter.cs (85%)
rename QS.DbManagement/{ => Entities}/ConnectionParameterValue.cs (94%)
rename QS.DbManagement/{ => Entities}/DbCreationPhase.cs (60%)
create mode 100644 QS.DbManagement/Entities/DbCreationRequest.cs
rename QS.DbManagement/{ => Entities}/DbInfo.cs (82%)
create mode 100644 QS.DbManagement/Entities/DbPhaseArgs.cs
rename QS.DbManagement/{ => Entities}/ProviderResponces.cs (95%)
create mode 100644 QS.DbManagement/IDbFillStrategy.cs
create mode 100644 QS.DbManagement/ScriptDbFillStrategy.cs
delete mode 100644 QS.Launcher/ViewModels/PageViewModels/DataBase/IDbOperationSettings.cs
diff --git a/QS.Cloud.Client/DataBase/QSCloudProvider.cs b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
index 254a1829b..1f5efa498 100644
--- a/QS.Cloud.Client/DataBase/QSCloudProvider.cs
+++ b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
@@ -1,7 +1,6 @@
using Grpc.Core;
using MySqlConnector;
using QS.Cloud.Core;
-using QS.DbManagement.Responces;
using QS.DbManagement;
using QS.Dialog;
using QS.Project.Versioning;
@@ -13,15 +12,12 @@
using QS.Cloud.Client.Clients;
using QS.DBScripts.Controllers;
using System.Threading.Tasks;
-using Microsoft.Extensions.DependencyInjection;
+using QS.DbManagement.Entities;
namespace QS.Cloud.Client.DataBase
{
public class QSCloudProvider : IDbProvider {
- public int BaseId { get; private set; }
- public BasicAuthInfoProvider AuthInfo { get; private set; }
-
public bool IsConnected { get; private set; }
public bool IsAdmin { get; protected set; }
@@ -42,10 +38,10 @@ public class QSCloudProvider : IDbProvider {
public QSCloudProvider(IList parameters, string password = null) {
Account = parameters.First(p => p.Name == "Account").Value;
UserName = parameters.First(p => p.Name == "Login").Value;
- AuthInfo = new BasicAuthInfoProvider($@"{Account}\{UserName}", password);
-
- loginClient = new LoginManagementCloudClient(AuthInfo);
- dbClient = new DataBaseManagementCloudClient(AuthInfo);
+ var authInfo = new BasicAuthInfoProvider($@"{Account}\{UserName}", password);
+
+ loginClient = new LoginManagementCloudClient(authInfo);
+ dbClient = new DataBaseManagementCloudClient(authInfo);
}
public bool AddUser(string username, string password)
@@ -58,12 +54,33 @@ public bool ChangePassword(string username, string oldPassword, string newPasswo
throw new NotImplementedException();
}
- public bool CreateDatabase(string databaseName, string title, IServiceProvider services)
+ public bool CreateDatabase(DbCreationRequest request)
{
- IApplicationInfo applicationInfo = services.GetService();
- CreateDataBaseResponse response = dbClient.CreateDataBase(databaseName, title, applicationInfo);
- BaseId = response.BaseId;
- return true;
+ if(request == null)
+ throw new ArgumentNullException(nameof(request));
+
+ // 1. Создаём базу в облаке (gRPC). BaseId нужен только локально — открыть сессию.
+ var response = dbClient.CreateDataBase(request.DbName, request.DbTitle, request.ApplicationInfo);
+
+ // 2. Открываем временную сессию к созданной базе и наполняем её тем же механизмом, что и MariaDB.
+ using(var session = CloudDbSession.Open(loginClient, response.BaseId)) {
+ if(!session.Success) {
+ request.Interaction.ReportError("Не удалось открыть сессию к созданной базе: " + session.Description, "Создание базы в облаке");
+ return false;
+ }
+ if(!session.IsAdmin) {
+ request.Interaction.ReportError("Вы не имеете прав администратора для наполнения базы.", "Создание базы в облаке");
+ return false;
+ }
+
+ var filler = request.FillStrategy.CreateFiller(new DbFillResources {
+ ConnectionString = session.ConnectionStringBuilder.ConnectionString,
+ Progress = request.Progress,
+ Interaction = request.Interaction,
+ CancellationToken = request.CancellationToken,
+ });
+ return filler.RunCreation(session.Db.BaseName, request.DbTitle);
+ }
}
public void Dispose()
diff --git a/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs b/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
index 451cd9d45..3e52d689c 100644
--- a/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
+++ b/QS.Cloud.Client/DataBase/QsCloudConnectionTypeBase.cs
@@ -1,10 +1,6 @@
-using Microsoft.Extensions.DependencyInjection;
using QS.DbManagement;
-using QS.DBScripts;
-using QS.DBScripts.Controllers;
-using QS.DBScripts.Models;
+using QS.DbManagement.Entities;
using QS.Utilities.Extensions;
-using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
@@ -20,22 +16,6 @@ public QsCloudConnectionTypeBase() {
Parameters.Add(new ConnectionParameter("Login","Пользователь"));
IconBytes = Assembly.GetExecutingAssembly().GetResourceByteArray("QS.Cloud.Client.Icons.qscloud.ico");
-
- CreatorFactory = args => {
- var provider = (QSCloudProvider)args.Provider;
- var scripts = args.ServiceProvider.GetRequiredService();
- return new QsCloudDbCreator(
- provider.BaseId,
- provider.AuthInfo,
- scripts,
- args.Progress,
- args.Interaction,
- args.CancellationToken,
- args.ImportDumpFilePath);
- };
-
- // QsCloudDbCreator сам импортирует дамп, когда задан ImportDumpFilePath
- ImportFactory = CreatorFactory;
}
public override bool CanConnect(IEnumerable parameters) {
diff --git a/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs b/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
deleted file mode 100644
index b62528169..000000000
--- a/QS.Cloud.Client/DataBase/QsCloudDbCreator.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using QS.Cloud.Core;
-using QS.DbManagement;
-using QS.DBScripts;
-using QS.DBScripts.Controllers;
-using QS.DBScripts.Models;
-using QS.Dialog;
-using System;
-using System.Threading;
-
-namespace QS.Cloud.Client.DataBase
-{
- public class QsCloudDbCreator : IDbCreatorModel
- {
- static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
-
- private readonly int baseId;
- private readonly IProgressBarDisplayable progress;
- private readonly IDbCreatorInteraction interaction;
- private readonly IDbScriptsConfiguration configuration;
- private readonly CancellationToken cancellationToken;
- private readonly string importDumpFilePath;
-
- private LoginManagementCloudClient loginClient;
-
- public QsCloudDbCreator(
- int baseId,
- BasicAuthInfoProvider authInfo,
- IDbScriptsConfiguration configuration,
- IProgressBarDisplayable progress,
- IDbCreatorInteraction interaction,
- CancellationToken cancellationToken,
- string importDumpFilePath = null)
- {
- this.baseId = baseId;
- loginClient = new LoginManagementCloudClient(authInfo);
- this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
- this.progress = progress ?? throw new ArgumentNullException(nameof(progress));
- this.interaction = interaction ?? throw new ArgumentNullException(nameof(interaction));
- this.cancellationToken = cancellationToken;
- this.importDumpFilePath = importDumpFilePath;
- }
-
- public bool RunCreation(string dbName, string dbTitle) {
- try {
- cancellationToken.ThrowIfCancellationRequested();
-
- using(var session = CloudDbSession.Open(loginClient, baseId)) {
- if(!session.Success) {
- interaction.ReportError("Ошибка в создании сессии", "Запрос в облако");
- return false;
- }
- if(!session.IsAdmin) {
- interaction.ReportError("Вы не имеете прав Администратора", "Запрос в облако");
- return false;
- }
-
- if(!string.IsNullOrWhiteSpace(importDumpFilePath)) {
- // Наполнение импортом выбранного дампа вместо встроенного скрипта.
- new MariaDbDumpService().Import(session.ConnectionStringBuilder, session.Db.BaseName, importDumpFilePath, progress, cancellationToken, dbTitle);
- return true;
- }
-
- var creator = new MySqlDbCreateModel(
- session.Db.Server, session.Db.Port, session.Db.Login, session.Db.Password,
- configuration.MakeCreationScript(), progress, interaction, cancellationToken);
- creator.FillBaseGuid = false;
- return creator.RunCreation(session.Db.BaseName, dbTitle);
- }
- }
- catch(OperationCanceledException) {
- logger.Info("Создание базы в облаке отменено пользователем.");
- return false;
- }
- catch(Exception ex) {
- logger.Error(ex, "Ошибка при создании базы в облаке.");
- interaction.ReportError(ex.Message, null);
- throw;
- }
- finally {
- if(progress.IsStarted)
- progress.Close();
- }
- }
- }
-}
diff --git a/QS.DbManagement/Connection.cs b/QS.DbManagement/Connection.cs
index 581921fea..2b5d1b83d 100644
--- a/QS.DbManagement/Connection.cs
+++ b/QS.DbManagement/Connection.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using QS.DbManagement.Entities;
using ReactiveUI;
namespace QS.DbManagement {
diff --git a/QS.DbManagement/ConnectionTypeBase.cs b/QS.DbManagement/ConnectionTypeBase.cs
index bee4254dc..c11e3c920 100644
--- a/QS.DbManagement/ConnectionTypeBase.cs
+++ b/QS.DbManagement/ConnectionTypeBase.cs
@@ -1,12 +1,11 @@
-using Microsoft.Extensions.DependencyInjection;
-using QS.DBScripts;
-using QS.DBScripts.Controllers;
-using QS.Dialog;
-using System;
+using QS.DbManagement.Entities;
using System.Collections.Generic;
namespace QS.DbManagement {
+ ///
+ /// метаданные, проверка параметров и создание провайдера
+ ///
public abstract class ConnectionTypeBase {
public string Title { get; protected set; }
public string ConnectionTypeName { get; protected set; }
@@ -18,82 +17,5 @@ public abstract class ConnectionTypeBase {
public abstract bool CanConnect(IEnumerable parameters);
public abstract IDbProvider CreateProvider(IList parameters, string password = null);
-
- public Func CreatorFactory { get; set; }
-
- public Func ImportFactory { get; set; }
-
- ///
- /// умеет создавать базу, если
- /// задана фабрика,
- /// в конфигурации есть скрипт создания.
- /// НЕ учитывает права пользователя
- ///
- public virtual bool SupportsDatabaseCreation(IServiceProvider services) {
- return CreatorFactory != null
- && services.GetService()?.HasCreationScript() == true;
- }
-
- ///
- /// умеет наполнять базу дампом, если задана фабрика
- ///
- /// НЕ учитывает права пользователя
- ///
- public virtual bool SupportsDatabaseImport(IServiceProvider services) {
- return ImportFactory != null;
- }
-
- #region Права пользователя по управлению базой
-
- public bool CanCreateDatabase(IDbProvider provider, IServiceProvider services) {
- return provider != null
- && provider.CanCreateDatabase
- && SupportsDatabaseCreation(services);
- }
-
- public bool CanImportDatabase(IDbProvider provider, IServiceProvider services) {
- return provider != null
- && provider.CanCreateDatabase
- && SupportsDatabaseImport(services);
- }
-
- public bool CanBackupDatabase(IDbProvider provider) {
- return provider != null
- && provider.CanDropDatabase;
- }
-
- public bool CanDropDatabase(IDbProvider provider) {
- return provider != null
- && provider.CanDropDatabase;
- }
- #endregion
-
- public IDbCreatorModel CreateCreator(CreatorFactoryArgs args) {
- if(CreatorFactory == null)
- throw new InvalidOperationException(
- $"Для типа подключения '{ConnectionTypeName}' не настроена фабрика создания БД");
- return CreatorFactory(args);
- }
-
- public IDbCreatorModel CreateImporter(CreatorFactoryArgs args) {
- if(ImportFactory == null)
- throw new InvalidOperationException(
- $"Для типа подключения '{ConnectionTypeName}' не настроена фабрика импорта дампа");
- return ImportFactory(args);
- }
- }
-
- ///
- /// interaction — канал диалогов с пользователем
- /// serviceProvider — для получения дополнительных зависимостей
- ///
- public class CreatorFactoryArgs {
- public IDbProvider Provider { get; set; }
- public IProgressBarDisplayable Progress { get; set; }
- public IDbCreatorInteraction Interaction { get; set; }
- public System.Threading.CancellationToken CancellationToken { get; set; }
- public IServiceProvider ServiceProvider { get; set; }
-
- public string ImportDumpFilePath { get; set; }
}
}
diff --git a/QS.DbManagement/DbCapabilities.cs b/QS.DbManagement/DbCapabilities.cs
new file mode 100644
index 000000000..74abefa9d
--- /dev/null
+++ b/QS.DbManagement/DbCapabilities.cs
@@ -0,0 +1,41 @@
+using QS.DBScripts;
+
+namespace QS.DbManagement {
+ ///
+ /// комбинирует права пользователя с конфигурацией приложения
+ ///
+ public class DbCapabilities {
+ private readonly IDbScriptsConfiguration scripts;
+
+ public DbCapabilities(IDbScriptsConfiguration scripts) {
+ this.scripts = scripts;
+ }
+
+ ///
+ /// Создание из встроенного скрипта, если
+ /// сервер разрешает и зарегистрирован скрипт создания
+ ///
+ public bool CanCreate(IDbProvider provider) {
+ return provider?.CanCreateDatabase == true
+ && scripts?.HasCreationScript() == true;
+ }
+
+ ///
+ /// Наполнение дампом, если
+ /// есть права на создание
+ ///
+ public bool CanImport(IDbProvider provider) {
+ return provider?.CanCreateDatabase == true;
+ }
+
+ /// для любой подключённой базы
+ public bool CanBackup(IDbProvider provider) {
+ return provider != null;
+ }
+
+ /// по праву провайдера
+ public bool CanDrop(IDbProvider provider) {
+ return provider?.CanDropDatabase == true;
+ }
+ }
+}
diff --git a/QS.DbManagement/DumpDbFillStrategy.cs b/QS.DbManagement/DumpDbFillStrategy.cs
new file mode 100644
index 000000000..8b0b1ca3e
--- /dev/null
+++ b/QS.DbManagement/DumpDbFillStrategy.cs
@@ -0,0 +1,23 @@
+using System;
+using MySqlConnector;
+using QS.DBScripts.Controllers;
+
+namespace QS.DbManagement {
+ public class DumpDbFillStrategy : IDbFillStrategy {
+ private readonly string dumpFilePath;
+
+ public DumpDbFillStrategy(string dumpFilePath) {
+ if(string.IsNullOrWhiteSpace(dumpFilePath))
+ throw new ArgumentException("Не задан путь к дампу", nameof(dumpFilePath));
+ this.dumpFilePath = dumpFilePath;
+ }
+
+ public IDbCreatorModel CreateFiller(DbFillResources resources) {
+ return new MariaDbImportModel(
+ new MySqlConnectionStringBuilder(resources.ConnectionString),
+ dumpFilePath,
+ resources.Progress,
+ resources.CancellationToken);
+ }
+ }
+}
diff --git a/QS.DbManagement/ConnectionParameter.cs b/QS.DbManagement/Entities/ConnectionParameter.cs
similarity index 85%
rename from QS.DbManagement/ConnectionParameter.cs
rename to QS.DbManagement/Entities/ConnectionParameter.cs
index ddbaaa231..4498b2f8d 100644
--- a/QS.DbManagement/ConnectionParameter.cs
+++ b/QS.DbManagement/Entities/ConnectionParameter.cs
@@ -1,4 +1,4 @@
-namespace QS.DbManagement
+namespace QS.DbManagement.Entities
{
public class ConnectionParameter
{
diff --git a/QS.DbManagement/ConnectionParameterValue.cs b/QS.DbManagement/Entities/ConnectionParameterValue.cs
similarity index 94%
rename from QS.DbManagement/ConnectionParameterValue.cs
rename to QS.DbManagement/Entities/ConnectionParameterValue.cs
index 18a24b8f6..11bf825f3 100644
--- a/QS.DbManagement/ConnectionParameterValue.cs
+++ b/QS.DbManagement/Entities/ConnectionParameterValue.cs
@@ -1,7 +1,7 @@
using System;
using ReactiveUI;
-namespace QS.DbManagement {
+namespace QS.DbManagement.Entities {
public class ConnectionParameterValue : ReactiveObject {
private readonly ConnectionParameter parameter;
diff --git a/QS.DbManagement/DbCreationPhase.cs b/QS.DbManagement/Entities/DbCreationPhase.cs
similarity index 60%
rename from QS.DbManagement/DbCreationPhase.cs
rename to QS.DbManagement/Entities/DbCreationPhase.cs
index 0c70e4528..ede8a5f98 100644
--- a/QS.DbManagement/DbCreationPhase.cs
+++ b/QS.DbManagement/Entities/DbCreationPhase.cs
@@ -1,15 +1,14 @@
using System;
-using QS.DbManagement;
-namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
+namespace QS.DbManagement.Entities {
///
/// Один шаг пайплайна создания базы
///
public sealed class DbCreationPhase {
public string Title { get; }
- public Func Action { get; }
+ public Func Action { get; }
- public DbCreationPhase(string title, Func action) {
+ public DbCreationPhase(string title, Func action) {
Title = title ?? throw new ArgumentNullException(nameof(title));
Action = action ?? throw new ArgumentNullException(nameof(action));
}
diff --git a/QS.DbManagement/Entities/DbCreationRequest.cs b/QS.DbManagement/Entities/DbCreationRequest.cs
new file mode 100644
index 000000000..0fbe6bbe6
--- /dev/null
+++ b/QS.DbManagement/Entities/DbCreationRequest.cs
@@ -0,0 +1,22 @@
+using System.Threading;
+using QS.DBScripts.Controllers;
+using QS.Dialog;
+using QS.Project.Versioning;
+
+namespace QS.DbManagement.Entities {
+ ///
+ /// запрос на создание базы
+ ///
+ public sealed class DbCreationRequest {
+ public string DbName { get; set; }
+ public string DbTitle { get; set; }
+
+ /// Чем наполнять созданную базу
+ public IDbFillStrategy FillStrategy { get; set; }
+ public IApplicationInfo ApplicationInfo { get; set; }
+
+ public IProgressBarDisplayable Progress { get; set; }
+ public IDbCreatorInteraction Interaction { get; set; }
+ public CancellationToken CancellationToken { get; set; }
+ }
+}
diff --git a/QS.DbManagement/DbInfo.cs b/QS.DbManagement/Entities/DbInfo.cs
similarity index 82%
rename from QS.DbManagement/DbInfo.cs
rename to QS.DbManagement/Entities/DbInfo.cs
index 3fd58f347..d9e54c7ad 100644
--- a/QS.DbManagement/DbInfo.cs
+++ b/QS.DbManagement/Entities/DbInfo.cs
@@ -1,4 +1,4 @@
-namespace QS.DbManagement {
+namespace QS.DbManagement.Entities {
public class DbInfo {
public string Title { get; set; }
public string BaseName { get; set; }
diff --git a/QS.DbManagement/Entities/DbPhaseArgs.cs b/QS.DbManagement/Entities/DbPhaseArgs.cs
new file mode 100644
index 000000000..6b41c1f24
--- /dev/null
+++ b/QS.DbManagement/Entities/DbPhaseArgs.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Threading;
+using QS.DBScripts.Controllers;
+using QS.Dialog;
+
+namespace QS.DbManagement.Entities {
+ ///
+ /// Контекст выполнения одной фазы операции с базой (создание+наполнение, бэкап и т.п.).
+ /// Раннер прогресса заполняет его и передаёт в каждую фазу пайплайна.
+ ///
+ public class DbPhaseArgs {
+ public IDbProvider Provider { get; set; }
+ public IProgressBarDisplayable Progress { get; set; }
+ public IDbCreatorInteraction Interaction { get; set; }
+ public CancellationToken CancellationToken { get; set; }
+ public IServiceProvider ServiceProvider { get; set; }
+ }
+}
diff --git a/QS.DbManagement/ProviderResponces.cs b/QS.DbManagement/Entities/ProviderResponces.cs
similarity index 95%
rename from QS.DbManagement/ProviderResponces.cs
rename to QS.DbManagement/Entities/ProviderResponces.cs
index b08cf0a57..34c7275bd 100644
--- a/QS.DbManagement/ProviderResponces.cs
+++ b/QS.DbManagement/Entities/ProviderResponces.cs
@@ -1,6 +1,6 @@
using System.Collections.Generic;
-namespace QS.DbManagement.Responces
+namespace QS.DbManagement.Entities
{
public class Response {
public bool Success { get; set; }
diff --git a/QS.DbManagement/IDbFillStrategy.cs b/QS.DbManagement/IDbFillStrategy.cs
new file mode 100644
index 000000000..d873031e1
--- /dev/null
+++ b/QS.DbManagement/IDbFillStrategy.cs
@@ -0,0 +1,19 @@
+using System.Threading;
+using QS.DBScripts.Controllers;
+using QS.Dialog;
+
+namespace QS.DbManagement {
+ ///
+ /// Ресурсы наполнения, известные только в момент операции
+ ///
+ public sealed class DbFillResources {
+ public string ConnectionString { get; set; }
+ public IProgressBarDisplayable Progress { get; set; }
+ public IDbCreatorInteraction Interaction { get; set; }
+ public CancellationToken CancellationToken { get; set; }
+ }
+
+ public interface IDbFillStrategy {
+ IDbCreatorModel CreateFiller(DbFillResources resources);
+ }
+}
diff --git a/QS.DbManagement/IDbProvider.cs b/QS.DbManagement/IDbProvider.cs
index b74c0fa55..34b40c7f1 100644
--- a/QS.DbManagement/IDbProvider.cs
+++ b/QS.DbManagement/IDbProvider.cs
@@ -1,9 +1,9 @@
-using QS.DbManagement.Responces;
using QS.Dialog;
using QS.Project.Versioning;
using System.Collections.Generic;
using System.Threading;
using System;
+using QS.DbManagement.Entities;
namespace QS.DbManagement
{
@@ -13,7 +13,10 @@ public interface IDbProvider : IDisposable
bool ChangePassword(string username, string oldPassword, string newPassword);
- bool CreateDatabase(string databaseName, string title, IServiceProvider services = null);
+ ///
+ /// Создаёт базу и сразу наполняет её
+ ///
+ bool CreateDatabase(DbCreationRequest request);
bool DropDatabase(DbInfo database);
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index f09aa393a..de26833a2 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -1,6 +1,6 @@
using Dapper;
using MySqlConnector;
-using QS.DbManagement.Responces;
+using QS.DbManagement.Entities;
using QS.Dialog;
using QS.Project.Versioning;
using System;
@@ -30,12 +30,6 @@ public class MariaDBProvider : IDbProvider {
public bool CanCreateDatabase { get; private set; }
public bool CanDropDatabase { get; private set; }
- ///
- /// Переданный в тайтл созданой базы,
- /// нужен потом при применения скрипта с наполнением базы
- ///
- public string CreatedTitle { get; private set; }
-
#region Параметры подключения
public string Server { get; }
public string UserName { get; }
@@ -183,10 +177,21 @@ public bool ChangePassword(string username, string oldPassword, string newPasswo
return connection.Execute(sql) != 0;
}
- public bool CreateDatabase(string databaseName, string title, IServiceProvider services = null) {
- CreatedTitle = title;
- string sql = $"CREATE DATABASE IF NOT EXISTS `{databaseName}`";
- return connection.Execute(sql) != 0;
+ public bool CreateDatabase(DbCreationRequest request) {
+ if(request == null)
+ throw new ArgumentNullException(nameof(request));
+ connection.Execute($"CREATE DATABASE IF NOT EXISTS `{request.DbName}`");
+
+ var fillBuilder = new MySqlConnectionStringBuilder(ConnectionStringBuilder.ConnectionString) {
+ Database = request.DbName
+ };
+ var filler = request.FillStrategy.CreateFiller(new DbFillResources {
+ ConnectionString = fillBuilder.ConnectionString,
+ Progress = request.Progress,
+ Interaction = request.Interaction,
+ CancellationToken = request.CancellationToken,
+ });
+ return filler.RunCreation(request.DbName, request.DbTitle);
}
public bool DropDatabase(DbInfo database) {
diff --git a/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs b/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs
index b1590b200..490bd6866 100644
--- a/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs
+++ b/QS.DbManagement/MariaDb/MariaDbConnectionTypeBase.cs
@@ -1,8 +1,5 @@
-using Microsoft.Extensions.DependencyInjection;
-using QS.DBScripts;
-using QS.DBScripts.Models;
+using QS.DbManagement.Entities;
using QS.Utilities.Extensions;
-using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
@@ -17,22 +14,6 @@ public MariaDbConnectionTypeBase() {
Parameters.Add(new ConnectionParameter("Server", "Адрес сервера"));
Parameters.Add(new ConnectionParameter("Login", "Пользователь"));
IconBytes = Assembly.GetExecutingAssembly().GetResourceByteArray("QS.DbManagement.Assets.mariadb.ico");
-
-
- CreatorFactory = args => {
- var p = (MariaDBProvider)args.Provider;
- var scripts = args.ServiceProvider.GetRequiredService();
- return new MySqlDbCreateModel(
- p.ConnectionStringBuilder.ConnectionString,
- scripts.MakeCreationScript(), args.Progress, args.Interaction, args.CancellationToken) { FillBaseGuid = false };
- };
-
- ImportFactory = args => {
- var p = (MariaDBProvider)args.Provider;
- return new MariaDbImportModel(
- p.ConnectionStringBuilder, args.ImportDumpFilePath, args.Progress, args.CancellationToken);
- };
-
}
public override bool CanConnect(IEnumerable parameters) {
diff --git a/QS.DbManagement/ScriptDbFillStrategy.cs b/QS.DbManagement/ScriptDbFillStrategy.cs
new file mode 100644
index 000000000..cfac8ae77
--- /dev/null
+++ b/QS.DbManagement/ScriptDbFillStrategy.cs
@@ -0,0 +1,23 @@
+using System;
+using QS.DBScripts;
+using QS.DBScripts.Controllers;
+using QS.DBScripts.Models;
+
+namespace QS.DbManagement {
+ public class ScriptDbFillStrategy : IDbFillStrategy {
+ private readonly IDbScriptsConfiguration scripts;
+
+ public ScriptDbFillStrategy(IDbScriptsConfiguration scripts) {
+ this.scripts = scripts ?? throw new ArgumentNullException(nameof(scripts));
+ }
+
+ public IDbCreatorModel CreateFiller(DbFillResources resources) {
+ return new MySqlDbCreateModel(
+ resources.ConnectionString,
+ scripts.MakeCreationScript(),
+ resources.Progress,
+ resources.Interaction,
+ resources.CancellationToken) { FillBaseGuid = false };
+ }
+ }
+}
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml.cs
index 56c9019ee..c64f67821 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml.cs
@@ -3,7 +3,7 @@
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input;
using Avalonia.Interactivity;
-using QS.DbManagement;
+using QS.DbManagement.Entities;
using QS.Launcher.ViewModels.PageViewModels.DataBase;
using System.Linq;
using System.Threading.Tasks;
diff --git a/QS.Launcher/AppRunner/IAppRunner.cs b/QS.Launcher/AppRunner/IAppRunner.cs
index dfeb1f013..a1b3f9976 100644
--- a/QS.Launcher/AppRunner/IAppRunner.cs
+++ b/QS.Launcher/AppRunner/IAppRunner.cs
@@ -1,4 +1,4 @@
-using QS.DbManagement.Responces;
+using QS.DbManagement.Entities;
namespace QS.Launcher.AppRunner {
public interface IAppRunner {
diff --git a/QS.Launcher/AppRunner/InProcessRunner.cs b/QS.Launcher/AppRunner/InProcessRunner.cs
index 956006e06..cfb0f1739 100644
--- a/QS.Launcher/AppRunner/InProcessRunner.cs
+++ b/QS.Launcher/AppRunner/InProcessRunner.cs
@@ -1,5 +1,5 @@
using System;
-using QS.DbManagement.Responces;
+using QS.DbManagement.Entities;
namespace QS.Launcher.AppRunner {
public class InProcessRunner : IAppRunner {
diff --git a/QS.Launcher/AppRunner/NewProcessRunner.cs b/QS.Launcher/AppRunner/NewProcessRunner.cs
index f1640e9e0..07476aca3 100644
--- a/QS.Launcher/AppRunner/NewProcessRunner.cs
+++ b/QS.Launcher/AppRunner/NewProcessRunner.cs
@@ -1,7 +1,7 @@
using System;
using System.Diagnostics;
using System.IO;
-using QS.DbManagement.Responces;
+using QS.DbManagement.Entities;
namespace QS.Launcher.AppRunner {
public class NewProcessRunner : IAppRunner {
diff --git a/QS.Launcher/DependencyInjection.cs b/QS.Launcher/DependencyInjection.cs
index 834e50d6c..1febcf1af 100644
--- a/QS.Launcher/DependencyInjection.cs
+++ b/QS.Launcher/DependencyInjection.cs
@@ -18,7 +18,9 @@ public static IServiceCollection AddLauncherViewModels(this IServiceCollection s
.AddSingleton()
// Страница прогресса создаётся заново на каждую операцию с базой
.AddTransient()
- .AddSingleton();
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton();
}
public static IServiceCollection AddLauncherOptions(this IServiceCollection services, LauncherOptions launcherOptions) {
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/BackupDbSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/BackupDbSettingsVM.cs
index 9377ed7e2..e5b2f5b6d 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/BackupDbSettingsVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/BackupDbSettingsVM.cs
@@ -3,6 +3,7 @@
using System.IO;
using System.Reactive.Linq;
using QS.DbManagement;
+using QS.DbManagement.Entities;
using ReactiveUI;
namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
index 65192bd5b..c2444617d 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
@@ -5,6 +5,7 @@
using System.Threading;
using System.Threading.Tasks;
using QS.DbManagement;
+using QS.DbManagement.Entities;
using QS.DBScripts.Controllers;
using QS.Dialog;
using ReactiveUI;
@@ -103,7 +104,7 @@ public void SetPipeline(
///
public async Task RunAsync() {
try {
- var args = new CreatorFactoryArgs {
+ var args = new DbPhaseArgs {
Provider = Provider,
Progress = this,
Interaction = interaction,
@@ -128,7 +129,7 @@ public async Task RunAsync() {
OperationFailed?.Invoke();
}
}
- private bool RunPipeline(CreatorFactoryArgs args) {
+ private bool RunPipeline(DbPhaseArgs args) {
foreach(var phase in phases) {
args.CancellationToken.ThrowIfCancellationRequested();
guiDispatcher.RunInGuiTread(() => CurrentText = phase.Title);
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
index 3c0845db7..d8f155369 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
@@ -1,8 +1,9 @@
using System;
using System.Collections.Generic;
-using System.Reactive.Linq;
+using Microsoft.Extensions.DependencyInjection;
using QS.DbManagement;
-using QS.DBScripts.Controllers;
+using QS.DbManagement.Entities;
+using QS.Project.Versioning;
using ReactiveUI;
namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
@@ -11,6 +12,8 @@ public CreateDbSettingsVM(IDbProvider provider, Connection connection, IServiceP
: base(provider, connection, services) {
SetValidity(this.WhenAnyValue(x => x.DbName, x => x.DbTitle,
(name, title) => !string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(title)));
+
+ CanImportDump = services.GetRequiredService().CanImport(provider);
}
public override string Title => "Создание базы данных";
@@ -33,34 +36,29 @@ public string ImportDumpFilePath {
set => this.RaiseAndSetIfChanged(ref importDumpFilePath, value);
}
- public bool CanImportDump => Connection?.ConnectionType?.CanImportDatabase(Provider, Services) == true;
+ public bool CanImportDump;
public override IEnumerable BuildPipeline() {
- var phases = new List {
- new DbCreationPhase(
- "Создание базы данных",
- args => args.Provider.CreateDatabase(DbName, DbTitle, Services))
- };
+ return new[] {
+ new DbCreationPhase("Создание базы данных", args => {
+ IDbFillStrategy fillStrategy;
+ if(string.IsNullOrWhiteSpace(ImportDumpFilePath))
+ fillStrategy = args.ServiceProvider.GetRequiredService();
+ else
+ fillStrategy = new DumpDbFillStrategy(ImportDumpFilePath);
- if(!string.IsNullOrWhiteSpace(ImportDumpFilePath)
- && Connection.ConnectionType.SupportsDatabaseImport(Services)) {
- phases.Add(new DbCreationPhase(
- "Импорт дампа в базу данных",
- args => {
- args.ImportDumpFilePath = ImportDumpFilePath;
- return Connection.ConnectionType.CreateImporter(args).RunCreation(DbName, DbTitle);
- }));
- }
- else if(Connection.ConnectionType.SupportsDatabaseCreation(Services)) {
- phases.Add(new DbCreationPhase(
- "Наполнение базы данных",
- args => {
- IDbCreatorModel creator = Connection.ConnectionType.CreateCreator(args);
- return creator.RunCreation(DbName, DbTitle);
- }));
- }
-
- return phases;
+ var request = new DbCreationRequest {
+ DbName = DbName,
+ DbTitle = DbTitle,
+ FillStrategy = fillStrategy,
+ ApplicationInfo = args.ServiceProvider.GetService(),
+ Progress = args.Progress,
+ Interaction = args.Interaction,
+ CancellationToken = args.CancellationToken,
+ };
+ return args.Provider.CreateDatabase(request);
+ })
+ };
}
}
}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
index ab64918e6..9f4c5a5d4 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
@@ -7,6 +7,7 @@
using System.Windows.Input;
using DynamicData.Kernel;
using QS.DbManagement;
+using QS.DbManagement.Entities;
using QS.Dialog;
using QS.Launcher.AppRunner;
using QS.Project.Versioning;
@@ -34,14 +35,11 @@ public IDbProvider Provider {
}
}
- public bool CanCreateDatabase =>
- currentConnection?.ConnectionType?.CanCreateDatabase(provider, serviceProvider) == true;
+ public bool CanCreateDatabase => capabilities.CanCreate(provider);
- public bool CanDropDatabase =>
- currentConnection?.ConnectionType?.CanDropDatabase(provider) == true;
+ public bool CanDropDatabase => capabilities.CanDrop(provider);
- public bool CanBackupDatabase =>
- currentConnection?.ConnectionType?.CanBackupDatabase(provider) == true;
+ public bool CanBackupDatabase => capabilities.CanBackup(provider);
public bool CanManageDatabases =>
CanDropDatabase || CanBackupDatabase;
@@ -87,6 +85,7 @@ public DbInfo SelectedDatabase {
private readonly IAppRunner appRunner;
private readonly IApplicationInfo applicationInfo;
+ private readonly DbCapabilities capabilities;
public DataBasesVM(
IAppRunner appRunner,
@@ -94,7 +93,8 @@ public DataBasesVM(
IInteractiveMessage interactiveMessage,
IInteractiveQuestion interactiveQuestion,
LauncherOptions launcherOptions,
- IServiceProvider serviceProvider)
+ IServiceProvider serviceProvider,
+ DbCapabilities capabilities)
{
this.appRunner = appRunner ?? throw new ArgumentNullException(nameof(appRunner));
this.applicationInfo = applicationInfo ?? throw new ArgumentNullException(nameof(applicationInfo));
@@ -102,6 +102,7 @@ public DataBasesVM(
this.interactiveQuestion = interactiveQuestion ?? throw new ArgumentNullException(nameof(interactiveQuestion));
this.launcherOptions = launcherOptions;
this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
+ this.capabilities = capabilities ?? throw new ArgumentNullException(nameof(capabilities));
IObservable canExecuteConnection = this
.WhenAnyValue(x => x.SelectedDatabase)
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/DbOperationSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/DbOperationSettingsVM.cs
index 8fd0873f5..489da3126 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/DbOperationSettingsVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/DbOperationSettingsVM.cs
@@ -4,10 +4,11 @@
using System.Reactive.Linq;
using Microsoft.Extensions.DependencyInjection;
using QS.DbManagement;
+using QS.DbManagement.Entities;
using ReactiveUI;
namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
- public abstract class DbOperationSettingsVM : CarouselPageVM, IDbOperationSettings {
+ public abstract class DbOperationSettingsVM : CarouselPageVM {
protected IDbProvider Provider { get; }
protected Connection Connection { get; }
protected IServiceProvider Services { get; }
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/IDbOperationSettings.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/IDbOperationSettings.cs
deleted file mode 100644
index 7caf32ce3..000000000
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/IDbOperationSettings.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
- ///
- /// Одна операция мастера настроек базы
- ///
- public interface IDbOperationSettings {
- /// Заголовок страницы
- string Title { get; }
-
- /// валидность ввода операции
- IObservable CanProceed { get; }
-
- /// состав фаз операции
- IEnumerable BuildPipeline();
- }
-}
diff --git a/QS.LibsTest.Core/Launcher/ConfiguratorTest.cs b/QS.LibsTest.Core/Launcher/ConfiguratorTest.cs
index 99a042658..b68265cf1 100644
--- a/QS.LibsTest.Core/Launcher/ConfiguratorTest.cs
+++ b/QS.LibsTest.Core/Launcher/ConfiguratorTest.cs
@@ -6,7 +6,6 @@
using NSubstitute;
using NUnit.Framework;
using QS.DbManagement;
-using QS.DBScripts.Controllers;
using QS.Dialog;
using QS.Launcher;
@@ -429,11 +428,6 @@ public TestConnectionType(string name) {
public override IDbProvider CreateProvider(IList parameters, string password = null)
=> Substitute.For();
-
- public TestConnectionType WithStubCreator() {
- CreatorFactory = args => Substitute.For();
- return this;
- }
}
}
}
From 6222efb04460b68dd593ea4b4cae01c87a028f60 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Mon, 29 Jun 2026 10:19:13 +0300
Subject: [PATCH 028/135] =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20=D0=BF?=
=?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B4=D0=B0=D1=87=D1=83=20DI,=20=D1=80=D0=B0?=
=?UTF-8?q?=D0=B7=D0=B4=D0=B5=D0=BB=D0=B8=D0=BB=20=D0=B8=D0=BC=D0=BF=D0=BE?=
=?UTF-8?q?=D1=80=D1=82=20=D0=B8=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD?=
=?UTF-8?q?=D0=B8=D0=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
QS.Cloud.Client/DataBase/QSCloudProvider.cs | 1 -
QS.DbManagement/DbFillStrategyFactory.cs | 19 ++++++
QS.DbManagement/Entities/DbPhaseArgs.cs | 5 +-
QS.DbManagement/IDbFillStrategy.cs | 4 --
QS.DbManagement/IDbFillStrategyFactory.cs | 7 +++
QS.DbManagement/MariaDb/MariaDBProvider.cs | 1 -
QS.DbManagement/ScriptDbFillStrategy.cs | 6 +-
QS.DbManagement/SqlDumpFileValidator.cs | 13 ++--
.../QS.Launcher.Avalonia.csproj | 3 +
QS.Launcher.Avalonia/Views/PageViewLocator.cs | 1 +
.../Pages/DataBase/CreateDbSettingsView.axaml | 6 --
.../DataBase/CreateDbSettingsView.axaml.cs | 22 -------
.../Views/Pages/DataBase/DataBasesView.axaml | 24 ++++----
.../Pages/DataBase/ImportDbSettingsView.axaml | 33 ++++++++++
.../DataBase/ImportDbSettingsView.axaml.cs | 33 ++++++++++
QS.Launcher/DependencyInjection.cs | 2 +-
.../DataBase/CreateDataBaseProgressVM.cs | 4 +-
.../DataBase/CreateDbSettingsVM.cs | 21 ++-----
.../PageViewModels/DataBase/DataBasesVM.cs | 17 ++++++
.../DataBase/ImportDbSettingsVM.cs | 60 +++++++++++++++++++
20 files changed, 204 insertions(+), 78 deletions(-)
create mode 100644 QS.DbManagement/DbFillStrategyFactory.cs
create mode 100644 QS.DbManagement/IDbFillStrategyFactory.cs
create mode 100644 QS.Launcher.Avalonia/Views/Pages/DataBase/ImportDbSettingsView.axaml
create mode 100644 QS.Launcher.Avalonia/Views/Pages/DataBase/ImportDbSettingsView.axaml.cs
create mode 100644 QS.Launcher/ViewModels/PageViewModels/DataBase/ImportDbSettingsVM.cs
diff --git a/QS.Cloud.Client/DataBase/QSCloudProvider.cs b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
index 1f5efa498..619646259 100644
--- a/QS.Cloud.Client/DataBase/QSCloudProvider.cs
+++ b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
@@ -76,7 +76,6 @@ public bool CreateDatabase(DbCreationRequest request)
var filler = request.FillStrategy.CreateFiller(new DbFillResources {
ConnectionString = session.ConnectionStringBuilder.ConnectionString,
Progress = request.Progress,
- Interaction = request.Interaction,
CancellationToken = request.CancellationToken,
});
return filler.RunCreation(session.Db.BaseName, request.DbTitle);
diff --git a/QS.DbManagement/DbFillStrategyFactory.cs b/QS.DbManagement/DbFillStrategyFactory.cs
new file mode 100644
index 000000000..075d928b9
--- /dev/null
+++ b/QS.DbManagement/DbFillStrategyFactory.cs
@@ -0,0 +1,19 @@
+using System;
+using QS.DBScripts;
+using QS.DBScripts.Controllers;
+
+namespace QS.DbManagement {
+ public class DbFillStrategyFactory : IDbFillStrategyFactory {
+ private readonly IDbScriptsConfiguration scripts;
+ private readonly IDbCreatorInteraction interaction;
+
+ public DbFillStrategyFactory(IDbScriptsConfiguration scripts, IDbCreatorInteraction interaction) {
+ this.scripts = scripts ?? throw new ArgumentNullException(nameof(scripts));
+ this.interaction = interaction ?? throw new ArgumentNullException(nameof(interaction));
+ }
+
+ public IDbFillStrategy ForScript() => new ScriptDbFillStrategy(scripts, interaction);
+
+ public IDbFillStrategy ForDump(string dumpFilePath) => new DumpDbFillStrategy(dumpFilePath);
+ }
+}
diff --git a/QS.DbManagement/Entities/DbPhaseArgs.cs b/QS.DbManagement/Entities/DbPhaseArgs.cs
index 6b41c1f24..50d454d24 100644
--- a/QS.DbManagement/Entities/DbPhaseArgs.cs
+++ b/QS.DbManagement/Entities/DbPhaseArgs.cs
@@ -1,17 +1,14 @@
using System;
using System.Threading;
-using QS.DBScripts.Controllers;
using QS.Dialog;
namespace QS.DbManagement.Entities {
///
- /// Контекст выполнения одной фазы операции с базой (создание+наполнение, бэкап и т.п.).
- /// Раннер прогресса заполняет его и передаёт в каждую фазу пайплайна.
+ /// Контекст выполнения одной фазы операциии
///
public class DbPhaseArgs {
public IDbProvider Provider { get; set; }
public IProgressBarDisplayable Progress { get; set; }
- public IDbCreatorInteraction Interaction { get; set; }
public CancellationToken CancellationToken { get; set; }
public IServiceProvider ServiceProvider { get; set; }
}
diff --git a/QS.DbManagement/IDbFillStrategy.cs b/QS.DbManagement/IDbFillStrategy.cs
index d873031e1..587db21ab 100644
--- a/QS.DbManagement/IDbFillStrategy.cs
+++ b/QS.DbManagement/IDbFillStrategy.cs
@@ -3,13 +3,9 @@
using QS.Dialog;
namespace QS.DbManagement {
- ///
- /// Ресурсы наполнения, известные только в момент операции
- ///
public sealed class DbFillResources {
public string ConnectionString { get; set; }
public IProgressBarDisplayable Progress { get; set; }
- public IDbCreatorInteraction Interaction { get; set; }
public CancellationToken CancellationToken { get; set; }
}
diff --git a/QS.DbManagement/IDbFillStrategyFactory.cs b/QS.DbManagement/IDbFillStrategyFactory.cs
new file mode 100644
index 000000000..04cdd9cc6
--- /dev/null
+++ b/QS.DbManagement/IDbFillStrategyFactory.cs
@@ -0,0 +1,7 @@
+namespace QS.DbManagement {
+ public interface IDbFillStrategyFactory {
+ IDbFillStrategy ForScript();
+
+ IDbFillStrategy ForDump(string dumpFilePath);
+ }
+}
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index de26833a2..f22993204 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -188,7 +188,6 @@ public bool CreateDatabase(DbCreationRequest request) {
var filler = request.FillStrategy.CreateFiller(new DbFillResources {
ConnectionString = fillBuilder.ConnectionString,
Progress = request.Progress,
- Interaction = request.Interaction,
CancellationToken = request.CancellationToken,
});
return filler.RunCreation(request.DbName, request.DbTitle);
diff --git a/QS.DbManagement/ScriptDbFillStrategy.cs b/QS.DbManagement/ScriptDbFillStrategy.cs
index cfac8ae77..f064a5acc 100644
--- a/QS.DbManagement/ScriptDbFillStrategy.cs
+++ b/QS.DbManagement/ScriptDbFillStrategy.cs
@@ -6,9 +6,11 @@
namespace QS.DbManagement {
public class ScriptDbFillStrategy : IDbFillStrategy {
private readonly IDbScriptsConfiguration scripts;
+ private readonly IDbCreatorInteraction interaction;
- public ScriptDbFillStrategy(IDbScriptsConfiguration scripts) {
+ public ScriptDbFillStrategy(IDbScriptsConfiguration scripts, IDbCreatorInteraction interaction) {
this.scripts = scripts ?? throw new ArgumentNullException(nameof(scripts));
+ this.interaction = interaction ?? throw new ArgumentNullException(nameof(interaction));
}
public IDbCreatorModel CreateFiller(DbFillResources resources) {
@@ -16,7 +18,7 @@ public IDbCreatorModel CreateFiller(DbFillResources resources) {
resources.ConnectionString,
scripts.MakeCreationScript(),
resources.Progress,
- resources.Interaction,
+ interaction,
resources.CancellationToken) { FillBaseGuid = false };
}
}
diff --git a/QS.DbManagement/SqlDumpFileValidator.cs b/QS.DbManagement/SqlDumpFileValidator.cs
index 44e1c34f8..111f5f17c 100644
--- a/QS.DbManagement/SqlDumpFileValidator.cs
+++ b/QS.DbManagement/SqlDumpFileValidator.cs
@@ -17,26 +17,25 @@ public static class SqlDumpFileValidator {
///
public static void EnsureLooksLikeSqlDump(string filePath) {
if(string.IsNullOrWhiteSpace(filePath))
- throw new ArgumentException("Не указан путь к файлу дампа.", nameof(filePath));
+ throw new ArgumentException("Не указан путь к файлу дампа", nameof(filePath));
if(!File.Exists(filePath))
- throw new FileNotFoundException("Файл дампа не найден.", filePath);
+ throw new FileNotFoundException("Файл дампа не найден", filePath);
if(new FileInfo(filePath).Length == 0)
- throw new InvalidDataException("Файл дампа пуст.");
+ throw new InvalidDataException("Файл дампа пуст");
string head = ReadHead(filePath, InspectBytes);
- // Бинарный файл (картинка/архив/документ) почти всегда содержит нулевые байты.
+ // Бинарный файл почти всегда содержит нулевые байты
if(head.IndexOf('\0') >= 0)
throw new InvalidDataException(
- "Файл не похож на SQL-дамп: это бинарный файл, а не текстовый SQL-скрипт.");
+ "Файл не похож на SQL-дамп: это бинарный файл, а не текстовый SQL-скрипт");
string trimmed = head.TrimStart('', ' ', '\t', '\r', '\n');
bool looksLikeSql = SqlStartTokens.Any(
token => trimmed.StartsWith(token, StringComparison.OrdinalIgnoreCase));
if(!looksLikeSql)
throw new InvalidDataException(
- "Файл не похож на SQL-дамп: в начале файла нет SQL-инструкций. "
- + "Выберите корректный файл дампа (.sql).");
+ "Файл не похож на SQL-дамп: в начале файла нет SQL-инструкций");
}
private static string ReadHead(string filePath, int maxBytes) {
diff --git a/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj b/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj
index 1f19cf1fc..d48ffc8b0 100644
--- a/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj
+++ b/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj
@@ -56,6 +56,9 @@
CreateDbSettingsView.axaml
+
+ ImportDbSettingsView.axaml
+
BackupDbSettingsView.axaml
diff --git a/QS.Launcher.Avalonia/Views/PageViewLocator.cs b/QS.Launcher.Avalonia/Views/PageViewLocator.cs
index d3fd92878..2cb9894ec 100644
--- a/QS.Launcher.Avalonia/Views/PageViewLocator.cs
+++ b/QS.Launcher.Avalonia/Views/PageViewLocator.cs
@@ -21,6 +21,7 @@ public PageViewLocator() {
[typeof(DataBasesVM)] = vm => new DataBasesView((DataBasesVM)vm),
[typeof(UserManagementVM)] = vm => new UserManagementView((UserManagementVM)vm),
[typeof(CreateDbSettingsVM)] = vm => new CreateDbSettingsView((CreateDbSettingsVM)vm),
+ [typeof(ImportDbSettingsVM)] = vm => new ImportDbSettingsView((ImportDbSettingsVM)vm),
[typeof(BackupDbSettingsVM)] = vm => new BackupDbSettingsView((BackupDbSettingsVM)vm),
[typeof(CreateDataBaseProgressVM)] = vm => new CreateDataBaseProgressView((CreateDataBaseProgressVM)vm),
};
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDbSettingsView.axaml b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDbSettingsView.axaml
index d8171d30f..b0e76ee67 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDbSettingsView.axaml
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDbSettingsView.axaml
@@ -17,12 +17,6 @@
-
-
-
-
-
-
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDbSettingsView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDbSettingsView.axaml.cs
index 9432dceba..31d8aec4d 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDbSettingsView.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDbSettingsView.axaml.cs
@@ -1,7 +1,4 @@
-using System.IO;
using Avalonia.Controls;
-using Avalonia.Interactivity;
-using Avalonia.Platform.Storage;
using QS.Launcher.ViewModels.PageViewModels.DataBase;
namespace QS.Launcher.Views.Pages.DataBase;
@@ -12,23 +9,4 @@ public CreateDbSettingsView(CreateDbSettingsVM viewModel) {
DataContext = viewModel;
}
-
- private async void BrowseImportFile_OnClick(object? sender, RoutedEventArgs e) {
- if(DataContext is not CreateDbSettingsVM vm)
- return;
-
- var topLevel = TopLevel.GetTopLevel(this);
- if(topLevel == null)
- return;
-
- var options = new FilePickerOpenOptions {
- Title = "Выбрать дамп базы данных",
- AllowMultiple = false,
- FileTypeFilter = new[] { new FilePickerFileType("SQL-скрипт") { Patterns = new[] { "*.sql" } } }
- };
-
- var files = await topLevel.StorageProvider.OpenFilePickerAsync(options);
- if(files.Count > 0)
- vm.ImportDumpFilePath = files[0].Path.LocalPath;
- }
}
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml
index 9ad3f5eea..3c0dc1b1a 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml
@@ -6,6 +6,7 @@
xmlns:vm="clr-namespace:QS.Launcher.ViewModels.PageViewModels;assembly=QS.Launcher"
xmlns:vmdb="clr-namespace:QS.Launcher.ViewModels.PageViewModels.DataBase;assembly=QS.Launcher"
xmlns:dbm="clr-namespace:QS.DbManagement;assembly=QS.DbManagement"
+ xmlns:dbme="clr-namespace:QS.DbManagement.Entities;assembly=QS.DbManagement"
d:DesignHeight="650"
d:DesignWidth="450"
x:DataType="vmdb:DataBasesVM"
@@ -17,14 +18,13 @@
-
-
+
-
+
-
-
+
+
-
+
-
+
+
+
+
-
+
@@ -76,7 +80,7 @@
-
+
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/ImportDbSettingsView.axaml b/QS.Launcher.Avalonia/Views/Pages/DataBase/ImportDbSettingsView.axaml
new file mode 100644
index 000000000..025421928
--- /dev/null
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/ImportDbSettingsView.axaml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/ImportDbSettingsView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/DataBase/ImportDbSettingsView.axaml.cs
new file mode 100644
index 000000000..0d665ac69
--- /dev/null
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/ImportDbSettingsView.axaml.cs
@@ -0,0 +1,33 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Platform.Storage;
+using QS.Launcher.ViewModels.PageViewModels.DataBase;
+
+namespace QS.Launcher.Views.Pages.DataBase;
+
+public partial class ImportDbSettingsView : UserControl {
+ public ImportDbSettingsView(ImportDbSettingsVM viewModel) {
+ InitializeComponent();
+
+ DataContext = viewModel;
+ }
+
+ private async void BrowseImportFile_OnClick(object? sender, RoutedEventArgs e) {
+ if(DataContext is not ImportDbSettingsVM vm)
+ return;
+
+ var topLevel = TopLevel.GetTopLevel(this);
+ if(topLevel == null)
+ return;
+
+ var options = new FilePickerOpenOptions {
+ Title = "Выбрать дамп базы данных",
+ AllowMultiple = false,
+ FileTypeFilter = new[] { new FilePickerFileType("SQL-скрипт") { Patterns = new[] { "*.sql" } } }
+ };
+
+ var files = await topLevel.StorageProvider.OpenFilePickerAsync(options);
+ if(files.Count > 0)
+ vm.ImportDumpFilePath = files[0].Path.LocalPath;
+ }
+}
diff --git a/QS.Launcher/DependencyInjection.cs b/QS.Launcher/DependencyInjection.cs
index 1febcf1af..e92712b69 100644
--- a/QS.Launcher/DependencyInjection.cs
+++ b/QS.Launcher/DependencyInjection.cs
@@ -19,7 +19,7 @@ public static IServiceCollection AddLauncherViewModels(this IServiceCollection s
// Страница прогресса создаётся заново на каждую операцию с базой
.AddTransient()
.AddSingleton()
- .AddSingleton()
+ .AddSingleton()
.AddSingleton();
}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
index c2444617d..6fe1a5241 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDataBaseProgressVM.cs
@@ -12,8 +12,7 @@
namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
///
- /// Универсальная страница прогресса: последовательно выполняет пайплайн фаз
- /// (создание базы, наполнение, резервное копирование и т.п.) в одном фоновом потоке.
+ /// последовательно выполняет пайплайн фаз в одном фоновом потоке
///
public class CreateDataBaseProgressVM : CarouselPageVM, IProgressBarDisplayable {
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
@@ -107,7 +106,6 @@ public async Task RunAsync() {
var args = new DbPhaseArgs {
Provider = Provider,
Progress = this,
- Interaction = interaction,
CancellationToken = cts.Token,
ServiceProvider = services
};
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
index d8f155369..2a0d6a139 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
@@ -3,6 +3,7 @@
using Microsoft.Extensions.DependencyInjection;
using QS.DbManagement;
using QS.DbManagement.Entities;
+using QS.DBScripts.Controllers;
using QS.Project.Versioning;
using ReactiveUI;
@@ -12,8 +13,6 @@ public CreateDbSettingsVM(IDbProvider provider, Connection connection, IServiceP
: base(provider, connection, services) {
SetValidity(this.WhenAnyValue(x => x.DbName, x => x.DbTitle,
(name, title) => !string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(title)));
-
- CanImportDump = services.GetRequiredService().CanImport(provider);
}
public override string Title => "Создание базы данных";
@@ -30,30 +29,18 @@ public string DbName {
set => this.RaiseAndSetIfChanged(ref dbName, value);
}
- private string importDumpFilePath;
- public string ImportDumpFilePath {
- get => importDumpFilePath;
- set => this.RaiseAndSetIfChanged(ref importDumpFilePath, value);
- }
-
- public bool CanImportDump;
-
public override IEnumerable BuildPipeline() {
return new[] {
new DbCreationPhase("Создание базы данных", args => {
- IDbFillStrategy fillStrategy;
- if(string.IsNullOrWhiteSpace(ImportDumpFilePath))
- fillStrategy = args.ServiceProvider.GetRequiredService();
- else
- fillStrategy = new DumpDbFillStrategy(ImportDumpFilePath);
+ var strategy = args.ServiceProvider.GetRequiredService().ForScript();
var request = new DbCreationRequest {
DbName = DbName,
DbTitle = DbTitle,
- FillStrategy = fillStrategy,
+ FillStrategy = strategy,
ApplicationInfo = args.ServiceProvider.GetService(),
Progress = args.Progress,
- Interaction = args.Interaction,
+ Interaction = args.ServiceProvider.GetRequiredService(),
CancellationToken = args.CancellationToken,
};
return args.Provider.CreateDatabase(request);
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
index 9f4c5a5d4..d1c197eaf 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
@@ -27,6 +27,7 @@ public IDbProvider Provider {
Databases = provider.GetUserDatabases(applicationInfo).AsList();
this.RaisePropertyChanged(nameof(Databases));
this.RaisePropertyChanged(nameof(CanCreateDatabase));
+ this.RaisePropertyChanged(nameof(CanImportDatabase));
this.RaisePropertyChanged(nameof(CanDropDatabase));
this.RaisePropertyChanged(nameof(CanBackupDatabase));
this.RaisePropertyChanged(nameof(CanManageDatabases));
@@ -37,6 +38,8 @@ public IDbProvider Provider {
public bool CanCreateDatabase => capabilities.CanCreate(provider);
+ public bool CanImportDatabase => capabilities.CanImport(provider);
+
public bool CanDropDatabase => capabilities.CanDrop(provider);
public bool CanBackupDatabase => capabilities.CanBackup(provider);
@@ -74,6 +77,7 @@ public DbInfo SelectedDatabase {
public ICommand ConnectCommand { get; }
public ReactiveCommand OpenCreateDatabaseCommand { get; }
+ public ReactiveCommand OpenImportDatabaseCommand { get; }
public ICommand BackupDatabaseCommand { get; }
public ICommand DeleteDatabaseCommand { get; }
@@ -110,6 +114,7 @@ public DataBasesVM(
ConnectCommand = ReactiveCommand.Create(Connect, canExecuteConnection);
OpenCreateDatabaseCommand = ReactiveCommand.Create(OpenCreateDatabase);
+ OpenImportDatabaseCommand = ReactiveCommand.Create(OpenImportDatabase);
BackupDatabaseCommand = ReactiveCommand.Create(OpenBackup);
DeleteDatabaseCommand = ReactiveCommand.CreateFromTask(DeleteDatabaseAsync);
}
@@ -126,6 +131,18 @@ private void OpenCreateDatabase() {
PushPageCommand?.Execute(settings);
}
+ ///
+ /// открывает страницу импорта базы из дампа; по завершении возвращает фокус на и обновляет список баз
+ ///
+ private void OpenImportDatabase() {
+ if(!CanImportDatabase)
+ return;
+
+ var settings = new ImportDbSettingsVM(Provider, CurrentConnection, serviceProvider);
+ settings.OperationCompleted += () => OnOperationCompleted(settings);
+ PushPageCommand?.Execute(settings);
+ }
+
///
/// открывает страницу резервного копирования выбранной базы
///
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/ImportDbSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/ImportDbSettingsVM.cs
new file mode 100644
index 000000000..913ddef84
--- /dev/null
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/ImportDbSettingsVM.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Extensions.DependencyInjection;
+using QS.DbManagement;
+using QS.DbManagement.Entities;
+using QS.DBScripts.Controllers;
+using QS.Project.Versioning;
+using ReactiveUI;
+
+namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
+ public class ImportDbSettingsVM : DbOperationSettingsVM {
+ public ImportDbSettingsVM(IDbProvider provider, Connection connection, IServiceProvider services)
+ : base(provider, connection, services) {
+ SetValidity(this.WhenAnyValue(x => x.DbName, x => x.DbTitle, x => x.ImportDumpFilePath,
+ (name, title, dump) => !string.IsNullOrWhiteSpace(name)
+ && !string.IsNullOrWhiteSpace(title)
+ && !string.IsNullOrWhiteSpace(dump)));
+ }
+
+ public override string Title => "Импорт базы данных из дампа";
+
+ private string dbTitle;
+ public string DbTitle {
+ get => dbTitle;
+ set => this.RaiseAndSetIfChanged(ref dbTitle, value);
+ }
+
+ private string dbName;
+ public string DbName {
+ get => dbName;
+ set => this.RaiseAndSetIfChanged(ref dbName, value);
+ }
+
+ private string importDumpFilePath;
+ public string ImportDumpFilePath {
+ get => importDumpFilePath;
+ set => this.RaiseAndSetIfChanged(ref importDumpFilePath, value);
+ }
+
+ public override IEnumerable BuildPipeline() {
+ // Наполнение из дампа. Конкретную стратегию строит фабрика по пути.
+ return new[] {
+ new DbCreationPhase("Импорт базы данных из дампа", args => {
+ var strategy = args.ServiceProvider.GetRequiredService().ForDump(ImportDumpFilePath);
+
+ var request = new DbCreationRequest {
+ DbName = DbName,
+ DbTitle = DbTitle,
+ FillStrategy = strategy,
+ ApplicationInfo = args.ServiceProvider.GetService(),
+ Progress = args.Progress,
+ Interaction = args.ServiceProvider.GetRequiredService(),
+ CancellationToken = args.CancellationToken,
+ };
+ return args.Provider.CreateDatabase(request);
+ })
+ };
+ }
+ }
+}
From ef6488534b4c489bcf747fd72a4b716675411397 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Mon, 29 Jun 2026 13:35:58 +0300
Subject: [PATCH 029/135] =?UTF-8?q?=D1=80=D0=B5=D1=84=D0=B0=D1=82=D0=BE?=
=?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3=20=D0=BD=D0=B0=20=D0=BD=D0=BE=D0=B2?=
=?UTF-8?q?=D1=8B=D0=B9=20=D0=B4=D0=B2=D0=B8=D0=B6=D0=BE=D0=BA=20=D0=BC?=
=?UTF-8?q?=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0?=
=?UTF-8?q?=D0=BD=D0=B8=D1=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
QS.Cloud.Client/DataBase/QSCloudProvider.cs | 25 +++++++----------
QS.DbManagement/Creation/DbCreationFactory.cs | 20 +++++++++++++
QS.DbManagement/Creation/DbDumpResources.cs | 15 ++++++++++
.../Creation/DbResourcesCreationMap.cs | 23 +++++++++++++++
.../MariaDbImportModel.cs | 28 ++++++++++---------
QS.DbManagement/DbFillStrategyFactory.cs | 19 -------------
QS.DbManagement/DumpDbFillStrategy.cs | 23 ---------------
QS.DbManagement/Entities/DbCreationRequest.cs | 11 ++++----
QS.DbManagement/IDbFillStrategy.cs | 15 ----------
QS.DbManagement/IDbFillStrategyFactory.cs | 7 -----
QS.DbManagement/IDbProvider.cs | 3 +-
QS.DbManagement/MariaDb/MariaDBProvider.cs | 14 ++++------
QS.DbManagement/ScriptDbFillStrategy.cs | 25 -----------------
QS.Launcher/DependencyInjection.cs | 16 ++++++++++-
.../DataBase/CreateDbSettingsVM.cs | 22 ++++++++++-----
.../DataBase/ImportDbSettingsVM.cs | 20 ++++++++-----
.../Controllers/DbCreationResources.cs | 8 ++++++
.../Models/MySqlCreationResources.cs | 16 +++++++++++
.../DBScripts/Models/MySqlDbCreateModel.cs | 18 +++++-------
19 files changed, 171 insertions(+), 157 deletions(-)
create mode 100644 QS.DbManagement/Creation/DbCreationFactory.cs
create mode 100644 QS.DbManagement/Creation/DbDumpResources.cs
create mode 100644 QS.DbManagement/Creation/DbResourcesCreationMap.cs
rename QS.DbManagement/{MariaDb => Creation}/MariaDbImportModel.cs (58%)
delete mode 100644 QS.DbManagement/DbFillStrategyFactory.cs
delete mode 100644 QS.DbManagement/DumpDbFillStrategy.cs
delete mode 100644 QS.DbManagement/IDbFillStrategy.cs
delete mode 100644 QS.DbManagement/IDbFillStrategyFactory.cs
delete mode 100644 QS.DbManagement/ScriptDbFillStrategy.cs
create mode 100644 QS.Project.Core/DBScripts/Controllers/DbCreationResources.cs
create mode 100644 QS.Updater.Core/DBScripts/Models/MySqlCreationResources.cs
diff --git a/QS.Cloud.Client/DataBase/QSCloudProvider.cs b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
index 619646259..02e2cb054 100644
--- a/QS.Cloud.Client/DataBase/QSCloudProvider.cs
+++ b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
@@ -1,18 +1,19 @@
+using FluentNHibernate.Cfg.Db;
using Grpc.Core;
using MySqlConnector;
+using QS.Cloud.Client.Clients;
using QS.Cloud.Core;
using QS.DbManagement;
+using QS.DbManagement.Entities;
+using QS.DBScripts.Controllers;
using QS.Dialog;
using QS.Project.Versioning;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
-using System;
using System.Threading;
-using QS.Cloud.Client.Clients;
-using QS.DBScripts.Controllers;
using System.Threading.Tasks;
-using QS.DbManagement.Entities;
namespace QS.Cloud.Client.DataBase
{
@@ -54,31 +55,25 @@ public bool ChangePassword(string username, string oldPassword, string newPasswo
throw new NotImplementedException();
}
- public bool CreateDatabase(DbCreationRequest request)
- {
+ public bool CreateDatabase(DbCreationRequest request) where CreationArgs : DbCreationResources {
if(request == null)
throw new ArgumentNullException(nameof(request));
- // 1. Создаём базу в облаке (gRPC). BaseId нужен только локально — открыть сессию.
var response = dbClient.CreateDataBase(request.DbName, request.DbTitle, request.ApplicationInfo);
- // 2. Открываем временную сессию к созданной базе и наполняем её тем же механизмом, что и MariaDB.
using(var session = CloudDbSession.Open(loginClient, response.BaseId)) {
if(!session.Success) {
request.Interaction.ReportError("Не удалось открыть сессию к созданной базе: " + session.Description, "Создание базы в облаке");
return false;
}
if(!session.IsAdmin) {
- request.Interaction.ReportError("Вы не имеете прав администратора для наполнения базы.", "Создание базы в облаке");
+ request.Interaction.ReportError("Вы не имеете прав администратора для наполнения базы", "Создание базы в облаке");
return false;
}
- var filler = request.FillStrategy.CreateFiller(new DbFillResources {
- ConnectionString = session.ConnectionStringBuilder.ConnectionString,
- Progress = request.Progress,
- CancellationToken = request.CancellationToken,
- });
- return filler.RunCreation(session.Db.BaseName, request.DbTitle);
+ request.CreationResources.ConnectionString = session.ConnectionStringBuilder.ConnectionString;
+ var creationModel = request.CreationFactory.Create(request.CreationResources);
+ return creationModel.RunCreation(session.Db.BaseName, request.DbTitle);
}
}
diff --git a/QS.DbManagement/Creation/DbCreationFactory.cs b/QS.DbManagement/Creation/DbCreationFactory.cs
new file mode 100644
index 000000000..04382b249
--- /dev/null
+++ b/QS.DbManagement/Creation/DbCreationFactory.cs
@@ -0,0 +1,20 @@
+using MySqlConnector;
+using QS.DBScripts.Controllers;
+using System;
+using System.Collections.Generic;
+
+namespace QS.DbManagement.Creation {
+ public class DbCreationFactory
+ {
+ private readonly DbResourcesCreationMap _map;
+
+ public DbCreationFactory(DbResourcesCreationMap map) {
+ _map = map;
+ }
+
+ public IDbCreatorModel Create(Arg resources) where Arg : DbCreationResources
+ {
+ return (IDbCreatorModel)_map.Resolve(resources);
+ }
+ }
+}
diff --git a/QS.DbManagement/Creation/DbDumpResources.cs b/QS.DbManagement/Creation/DbDumpResources.cs
new file mode 100644
index 000000000..e08ffc69d
--- /dev/null
+++ b/QS.DbManagement/Creation/DbDumpResources.cs
@@ -0,0 +1,15 @@
+using QS.DBScripts.Controllers;
+using QS.DBScripts.Models;
+using QS.Dialog;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace QS.DbManagement.Creation {
+ public class DbDumpResources : DbCreationResources {
+ public string DumpFilePath { get; set; }
+ public IProgressBarDisplayable Progress { get; set; }
+ public IDbCreatorInteraction Interactions { get; set; }
+ public CreationScript Script { get; set; }
+ }
+}
diff --git a/QS.DbManagement/Creation/DbResourcesCreationMap.cs b/QS.DbManagement/Creation/DbResourcesCreationMap.cs
new file mode 100644
index 000000000..184f36613
--- /dev/null
+++ b/QS.DbManagement/Creation/DbResourcesCreationMap.cs
@@ -0,0 +1,23 @@
+using QS.DBScripts.Controllers;
+using System;
+using System.Collections.Generic;
+
+namespace QS.DbManagement.Creation {
+ public class DbResourcesCreationMap
+ {
+ private Dictionary> _map = new Dictionary>();
+
+ public void Register(Type resource, Type creator)
+ {
+ if(!typeof(DbCreationResources).IsAssignableFrom(resource))
+ throw new ArgumentException($"{resource} не наследует DbCreationResources", nameof(resource));
+
+ if(!typeof(IDbCreatorModel).IsAssignableFrom(creator))
+ throw new ArgumentException($"{creator} не реализует IDbCreatorModel", nameof(creator));
+
+ _map[resource] = arg => Activator.CreateInstance(creator, arg);
+ }
+
+ public object Resolve(DbCreationResources arg) => _map[arg.GetType()](arg);
+ }
+}
diff --git a/QS.DbManagement/MariaDb/MariaDbImportModel.cs b/QS.DbManagement/Creation/MariaDbImportModel.cs
similarity index 58%
rename from QS.DbManagement/MariaDb/MariaDbImportModel.cs
rename to QS.DbManagement/Creation/MariaDbImportModel.cs
index f13b91b92..2ffcb7621 100644
--- a/QS.DbManagement/MariaDb/MariaDbImportModel.cs
+++ b/QS.DbManagement/Creation/MariaDbImportModel.cs
@@ -1,33 +1,35 @@
-using System;
-using System.Threading;
using MySqlConnector;
using QS.DBScripts.Controllers;
using QS.Dialog;
+using System;
+using System.Resources;
+using System.Threading;
-namespace QS.DbManagement {
+namespace QS.DbManagement.Creation {
///
/// Наполнение MariaDB базы пользовательским дампом.
/// Метод блокирует вызывающий поток — выносить в фон ответственность вызывающего кода.
///
public class MariaDbImportModel : IDbCreatorModel {
- private readonly MySqlConnectionStringBuilder connectionStringBuilder;
+ private readonly string connectionString;
private readonly string dumpFilePath;
private readonly IProgressBarDisplayable progress;
private readonly CancellationToken cancellation;
public MariaDbImportModel(
- MySqlConnectionStringBuilder connectionStringBuilder,
- string dumpFilePath,
- IProgressBarDisplayable progress,
- CancellationToken cancellation) {
- this.connectionStringBuilder = connectionStringBuilder
- ?? throw new ArgumentNullException(nameof(connectionStringBuilder));
- this.dumpFilePath = dumpFilePath;
- this.progress = progress;
- this.cancellation = cancellation;
+ DbDumpResources resources) {
+ this.connectionString = resources.ConnectionString
+ ?? throw new ArgumentNullException(nameof(connectionString));
+ this.dumpFilePath = resources.DumpFilePath;
+ this.progress = resources.Progress;
+ this.cancellation = resources.CancellationToken;
}
public bool RunCreation(string dbName, string dbTitle) {
+ if(string.IsNullOrWhiteSpace(dumpFilePath))
+ throw new ArgumentException("Не задан путь к дампу", nameof(dumpFilePath));
+
+ var connectionStringBuilder = new MySqlConnectionStringBuilder(connectionString);
new MariaDbDumpService().Import(connectionStringBuilder, dbName, dumpFilePath, progress, cancellation, dbTitle);
cancellation.ThrowIfCancellationRequested();
return true;
diff --git a/QS.DbManagement/DbFillStrategyFactory.cs b/QS.DbManagement/DbFillStrategyFactory.cs
deleted file mode 100644
index 075d928b9..000000000
--- a/QS.DbManagement/DbFillStrategyFactory.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System;
-using QS.DBScripts;
-using QS.DBScripts.Controllers;
-
-namespace QS.DbManagement {
- public class DbFillStrategyFactory : IDbFillStrategyFactory {
- private readonly IDbScriptsConfiguration scripts;
- private readonly IDbCreatorInteraction interaction;
-
- public DbFillStrategyFactory(IDbScriptsConfiguration scripts, IDbCreatorInteraction interaction) {
- this.scripts = scripts ?? throw new ArgumentNullException(nameof(scripts));
- this.interaction = interaction ?? throw new ArgumentNullException(nameof(interaction));
- }
-
- public IDbFillStrategy ForScript() => new ScriptDbFillStrategy(scripts, interaction);
-
- public IDbFillStrategy ForDump(string dumpFilePath) => new DumpDbFillStrategy(dumpFilePath);
- }
-}
diff --git a/QS.DbManagement/DumpDbFillStrategy.cs b/QS.DbManagement/DumpDbFillStrategy.cs
deleted file mode 100644
index 8b0b1ca3e..000000000
--- a/QS.DbManagement/DumpDbFillStrategy.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System;
-using MySqlConnector;
-using QS.DBScripts.Controllers;
-
-namespace QS.DbManagement {
- public class DumpDbFillStrategy : IDbFillStrategy {
- private readonly string dumpFilePath;
-
- public DumpDbFillStrategy(string dumpFilePath) {
- if(string.IsNullOrWhiteSpace(dumpFilePath))
- throw new ArgumentException("Не задан путь к дампу", nameof(dumpFilePath));
- this.dumpFilePath = dumpFilePath;
- }
-
- public IDbCreatorModel CreateFiller(DbFillResources resources) {
- return new MariaDbImportModel(
- new MySqlConnectionStringBuilder(resources.ConnectionString),
- dumpFilePath,
- resources.Progress,
- resources.CancellationToken);
- }
- }
-}
diff --git a/QS.DbManagement/Entities/DbCreationRequest.cs b/QS.DbManagement/Entities/DbCreationRequest.cs
index 0fbe6bbe6..1be7a183c 100644
--- a/QS.DbManagement/Entities/DbCreationRequest.cs
+++ b/QS.DbManagement/Entities/DbCreationRequest.cs
@@ -1,4 +1,5 @@
using System.Threading;
+using QS.DbManagement.Creation;
using QS.DBScripts.Controllers;
using QS.Dialog;
using QS.Project.Versioning;
@@ -7,16 +8,16 @@ namespace QS.DbManagement.Entities {
///
/// запрос на создание базы
///
- public sealed class DbCreationRequest {
+ public sealed class DbCreationRequest where CreationArgs : DbCreationResources {
public string DbName { get; set; }
public string DbTitle { get; set; }
+ public IDbCreatorInteraction Interaction { get; set; }
+
/// Чем наполнять созданную базу
- public IDbFillStrategy FillStrategy { get; set; }
+ public DbCreationFactory CreationFactory { get; set; }
public IApplicationInfo ApplicationInfo { get; set; }
- public IProgressBarDisplayable Progress { get; set; }
- public IDbCreatorInteraction Interaction { get; set; }
- public CancellationToken CancellationToken { get; set; }
+ public CreationArgs CreationResources { get; set; }
}
}
diff --git a/QS.DbManagement/IDbFillStrategy.cs b/QS.DbManagement/IDbFillStrategy.cs
deleted file mode 100644
index 587db21ab..000000000
--- a/QS.DbManagement/IDbFillStrategy.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using System.Threading;
-using QS.DBScripts.Controllers;
-using QS.Dialog;
-
-namespace QS.DbManagement {
- public sealed class DbFillResources {
- public string ConnectionString { get; set; }
- public IProgressBarDisplayable Progress { get; set; }
- public CancellationToken CancellationToken { get; set; }
- }
-
- public interface IDbFillStrategy {
- IDbCreatorModel CreateFiller(DbFillResources resources);
- }
-}
diff --git a/QS.DbManagement/IDbFillStrategyFactory.cs b/QS.DbManagement/IDbFillStrategyFactory.cs
deleted file mode 100644
index 04cdd9cc6..000000000
--- a/QS.DbManagement/IDbFillStrategyFactory.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace QS.DbManagement {
- public interface IDbFillStrategyFactory {
- IDbFillStrategy ForScript();
-
- IDbFillStrategy ForDump(string dumpFilePath);
- }
-}
diff --git a/QS.DbManagement/IDbProvider.cs b/QS.DbManagement/IDbProvider.cs
index 34b40c7f1..9c0a52ab9 100644
--- a/QS.DbManagement/IDbProvider.cs
+++ b/QS.DbManagement/IDbProvider.cs
@@ -4,6 +4,7 @@
using System.Threading;
using System;
using QS.DbManagement.Entities;
+using QS.DBScripts.Controllers;
namespace QS.DbManagement
{
@@ -16,7 +17,7 @@ public interface IDbProvider : IDisposable
///
/// Создаёт базу и сразу наполняет её
///
- bool CreateDatabase(DbCreationRequest request);
+ bool CreateDatabase(DbCreationRequest request) where CreationArgs : DbCreationResources;
bool DropDatabase(DbInfo database);
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index f22993204..4a544451f 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -1,6 +1,7 @@
using Dapper;
using MySqlConnector;
using QS.DbManagement.Entities;
+using QS.DBScripts.Controllers;
using QS.Dialog;
using QS.Project.Versioning;
using System;
@@ -177,20 +178,17 @@ public bool ChangePassword(string username, string oldPassword, string newPasswo
return connection.Execute(sql) != 0;
}
- public bool CreateDatabase(DbCreationRequest request) {
+ public bool CreateDatabase(DbCreationRequest request) where CreationArgs : DbCreationResources {
if(request == null)
throw new ArgumentNullException(nameof(request));
connection.Execute($"CREATE DATABASE IF NOT EXISTS `{request.DbName}`");
- var fillBuilder = new MySqlConnectionStringBuilder(ConnectionStringBuilder.ConnectionString) {
+ var connectionStringBuilder = new MySqlConnectionStringBuilder(ConnectionStringBuilder.ConnectionString) {
Database = request.DbName
};
- var filler = request.FillStrategy.CreateFiller(new DbFillResources {
- ConnectionString = fillBuilder.ConnectionString,
- Progress = request.Progress,
- CancellationToken = request.CancellationToken,
- });
- return filler.RunCreation(request.DbName, request.DbTitle);
+ request.CreationResources.ConnectionString = connectionStringBuilder.ConnectionString;
+ var creationModel = request.CreationFactory.Create(request.CreationResources);
+ return creationModel.RunCreation(request.DbName, request.DbTitle);
}
public bool DropDatabase(DbInfo database) {
diff --git a/QS.DbManagement/ScriptDbFillStrategy.cs b/QS.DbManagement/ScriptDbFillStrategy.cs
deleted file mode 100644
index f064a5acc..000000000
--- a/QS.DbManagement/ScriptDbFillStrategy.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using System;
-using QS.DBScripts;
-using QS.DBScripts.Controllers;
-using QS.DBScripts.Models;
-
-namespace QS.DbManagement {
- public class ScriptDbFillStrategy : IDbFillStrategy {
- private readonly IDbScriptsConfiguration scripts;
- private readonly IDbCreatorInteraction interaction;
-
- public ScriptDbFillStrategy(IDbScriptsConfiguration scripts, IDbCreatorInteraction interaction) {
- this.scripts = scripts ?? throw new ArgumentNullException(nameof(scripts));
- this.interaction = interaction ?? throw new ArgumentNullException(nameof(interaction));
- }
-
- public IDbCreatorModel CreateFiller(DbFillResources resources) {
- return new MySqlDbCreateModel(
- resources.ConnectionString,
- scripts.MakeCreationScript(),
- resources.Progress,
- interaction,
- resources.CancellationToken) { FillBaseGuid = false };
- }
- }
-}
diff --git a/QS.Launcher/DependencyInjection.cs b/QS.Launcher/DependencyInjection.cs
index e92712b69..37d04ad7e 100644
--- a/QS.Launcher/DependencyInjection.cs
+++ b/QS.Launcher/DependencyInjection.cs
@@ -7,9 +7,24 @@
using QS.Launcher.ViewModels;
using QS.Launcher.ViewModels.PageViewModels;
using QS.Launcher.ViewModels.PageViewModels.DataBase;
+using QS.DbManagement.Creation;
+using System;
+using System.Collections.Generic;
namespace QS.Launcher {
public static partial class DependencyInjection {
+ public static IServiceCollection AddLauncherDataBaseCreation(this IServiceCollection services, List<(Type res,Type creator)> resourceCratorMap)
+ {
+ var map = new DbResourcesCreationMap();
+ foreach(var resourceCrator in resourceCratorMap) {
+ map.Register(resourceCrator.res, resourceCrator.creator);
+ }
+
+ return services
+ .AddSingleton(map)
+ .AddSingleton();
+ }
+
public static IServiceCollection AddLauncherViewModels(this IServiceCollection services) {
return services
.AddSingleton()
@@ -19,7 +34,6 @@ public static IServiceCollection AddLauncherViewModels(this IServiceCollection s
// Страница прогресса создаётся заново на каждую операцию с базой
.AddTransient()
.AddSingleton()
- .AddSingleton()
.AddSingleton();
}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
index 2a0d6a139..6dc531a72 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
@@ -1,11 +1,15 @@
-using System;
-using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using QS.DbManagement;
+using QS.DbManagement.Creation;
using QS.DbManagement.Entities;
+using QS.DBScripts;
using QS.DBScripts.Controllers;
using QS.Project.Versioning;
using ReactiveUI;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Threading;
namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
public class CreateDbSettingsVM : DbOperationSettingsVM {
@@ -32,16 +36,20 @@ public string DbName {
public override IEnumerable BuildPipeline() {
return new[] {
new DbCreationPhase("Создание базы данных", args => {
- var strategy = args.ServiceProvider.GetRequiredService().ForScript();
+ var factory = args.ServiceProvider.GetRequiredService();
- var request = new DbCreationRequest {
+ var request = new DbCreationRequest {
DbName = DbName,
DbTitle = DbTitle,
- FillStrategy = strategy,
+ CreationFactory = factory,
ApplicationInfo = args.ServiceProvider.GetService(),
- Progress = args.Progress,
Interaction = args.ServiceProvider.GetRequiredService(),
- CancellationToken = args.CancellationToken,
+ //заполнение строки подключения оставляем провайдеру
+ CreationResources =new MySqlCreationResources{
+ Progress = args.Progress,
+ Interactions = args.ServiceProvider.GetRequiredService(),
+ Script = args.ServiceProvider.GetRequiredService().MakeCreationScript(),
+ CancellationToken = args.CancellationToken }
};
return args.Provider.CreateDatabase(request);
})
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/ImportDbSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/ImportDbSettingsVM.cs
index 913ddef84..7c6851125 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/ImportDbSettingsVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/ImportDbSettingsVM.cs
@@ -1,11 +1,13 @@
-using System;
-using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using QS.DbManagement;
+using QS.DbManagement.Creation;
using QS.DbManagement.Entities;
+using QS.DBScripts;
using QS.DBScripts.Controllers;
using QS.Project.Versioning;
using ReactiveUI;
+using System;
+using System.Collections.Generic;
namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
public class ImportDbSettingsVM : DbOperationSettingsVM {
@@ -41,16 +43,20 @@ public override IEnumerable BuildPipeline() {
// Наполнение из дампа. Конкретную стратегию строит фабрика по пути.
return new[] {
new DbCreationPhase("Импорт базы данных из дампа", args => {
- var strategy = args.ServiceProvider.GetRequiredService().ForDump(ImportDumpFilePath);
+ var factory = args.ServiceProvider.GetRequiredService();
- var request = new DbCreationRequest {
+ var request = new DbCreationRequest {
DbName = DbName,
DbTitle = DbTitle,
- FillStrategy = strategy,
+ CreationFactory = factory,
ApplicationInfo = args.ServiceProvider.GetService(),
- Progress = args.Progress,
Interaction = args.ServiceProvider.GetRequiredService(),
- CancellationToken = args.CancellationToken,
+ CreationResources =new DbDumpResources{
+ Progress = args.Progress,
+ DumpFilePath = ImportDumpFilePath,
+ Interactions = args.ServiceProvider.GetRequiredService(),
+ Script = args.ServiceProvider.GetRequiredService().MakeCreationScript(),
+ CancellationToken = args.CancellationToken }
};
return args.Provider.CreateDatabase(request);
})
diff --git a/QS.Project.Core/DBScripts/Controllers/DbCreationResources.cs b/QS.Project.Core/DBScripts/Controllers/DbCreationResources.cs
new file mode 100644
index 000000000..2159edc14
--- /dev/null
+++ b/QS.Project.Core/DBScripts/Controllers/DbCreationResources.cs
@@ -0,0 +1,8 @@
+using System.Threading;
+
+namespace QS.DBScripts.Controllers {
+ public abstract class DbCreationResources {
+ public string ConnectionString { get; set; }
+ public CancellationToken CancellationToken { get; set; }
+ }
+}
diff --git a/QS.Updater.Core/DBScripts/Models/MySqlCreationResources.cs b/QS.Updater.Core/DBScripts/Models/MySqlCreationResources.cs
new file mode 100644
index 000000000..99672441d
--- /dev/null
+++ b/QS.Updater.Core/DBScripts/Models/MySqlCreationResources.cs
@@ -0,0 +1,16 @@
+using QS.DBScripts.Controllers;
+using QS.DBScripts.Models;
+using QS.Dialog;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading;
+
+namespace QS.DbManagement.Creation {
+
+ public class MySqlCreationResources : DbCreationResources {
+ public IProgressBarDisplayable Progress { get; set; }
+ public IDbCreatorInteraction Interactions { get; set; }
+ public CreationScript Script { get; set; }
+ }
+}
diff --git a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
index 85e818350..db8c327e3 100644
--- a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
+++ b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
@@ -1,4 +1,5 @@
using MySqlConnector;
+using QS.DbManagement.Creation;
using QS.DBScripts.Controllers;
using QS.Dialog;
using System;
@@ -19,20 +20,15 @@ public class MySqlDbCreateModel : IDbCreatorModel
public bool FillBaseGuid { get; set; } = true;
- public MySqlDbCreateModel(
- string connectionString,
- CreationScript script,
- IProgressBarDisplayable progress,
- IDbCreatorInteraction interaction,
- CancellationToken cancellationToken)
+ public MySqlDbCreateModel(MySqlCreationResources resources)
{
if(string.IsNullOrWhiteSpace(connectionString))
throw new ArgumentException("Connection string is required", nameof(connectionString));
- this.connectionString = connectionString;
- this.script = script ?? throw new ArgumentNullException(nameof(script));
- this.progress = progress ?? throw new ArgumentNullException(nameof(progress));
- this.interaction = interaction ?? throw new ArgumentNullException(nameof(interaction));
- this.cancellationToken = cancellationToken;
+ this.connectionString = resources.ConnectionString;
+ this.script = resources.Script ?? throw new ArgumentNullException(nameof(script));
+ this.progress = resources.Progress ?? throw new ArgumentNullException(nameof(progress));
+ this.interaction = resources.Interactions ?? throw new ArgumentNullException(nameof(interaction));
+ this.cancellationToken = resources.CancellationToken;
}
public MySqlDbCreateModel(
From ce01f9346bcabfb796a829234f53c37f18a14484 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Mon, 29 Jun 2026 13:57:54 +0300
Subject: [PATCH 030/135] =?UTF-8?q?=D0=BF=D0=BE=D1=87=D0=B8=D1=81=D1=82?=
=?UTF-8?q?=D0=B8=D0=BB=20=D0=BF=D1=80=D0=BE=D1=88=D0=BB=D1=8B=D0=B9=20?=
=?UTF-8?q?=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE=D1=80=D0=B8=D0=BD?=
=?UTF-8?q?=D0=B3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
QS.Cloud.Client/DataBase/QSCloudProvider.cs | 2 +-
QS.DbManagement/Creation/DbCreationFactory.cs | 14 +++++++++-----
QS.DbManagement/Creation/DbDumpResources.cs | 6 ------
QS.DbManagement/Creation/DbResourcesCreationMap.cs | 9 ++++++++-
QS.DbManagement/Entities/DbCreationRequest.cs | 6 ++----
QS.DbManagement/IDbProvider.cs | 3 +--
QS.DbManagement/MariaDb/MariaDBProvider.cs | 2 +-
.../PageViewModels/DataBase/CreateDbSettingsVM.cs | 9 ++++-----
.../PageViewModels/DataBase/ImportDbSettingsVM.cs | 10 ++++------
.../DBScripts/Models/MySqlCreationResources.cs | 8 +-------
.../DBScripts/Models/MySqlDbCreateModel.cs | 13 +++++++------
11 files changed, 38 insertions(+), 44 deletions(-)
diff --git a/QS.Cloud.Client/DataBase/QSCloudProvider.cs b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
index 02e2cb054..37157262e 100644
--- a/QS.Cloud.Client/DataBase/QSCloudProvider.cs
+++ b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
@@ -55,7 +55,7 @@ public bool ChangePassword(string username, string oldPassword, string newPasswo
throw new NotImplementedException();
}
- public bool CreateDatabase(DbCreationRequest request) where CreationArgs : DbCreationResources {
+ public bool CreateDatabase(DbCreationRequest request) {
if(request == null)
throw new ArgumentNullException(nameof(request));
diff --git a/QS.DbManagement/Creation/DbCreationFactory.cs b/QS.DbManagement/Creation/DbCreationFactory.cs
index 04382b249..8e983566c 100644
--- a/QS.DbManagement/Creation/DbCreationFactory.cs
+++ b/QS.DbManagement/Creation/DbCreationFactory.cs
@@ -1,7 +1,6 @@
-using MySqlConnector;
using QS.DBScripts.Controllers;
using System;
-using System.Collections.Generic;
+using System.Reflection;
namespace QS.DbManagement.Creation {
public class DbCreationFactory
@@ -9,12 +8,17 @@ public class DbCreationFactory
private readonly DbResourcesCreationMap _map;
public DbCreationFactory(DbResourcesCreationMap map) {
- _map = map;
+ _map = map ?? throw new ArgumentNullException(nameof(map));
}
- public IDbCreatorModel Create(Arg resources) where Arg : DbCreationResources
+ public IDbCreatorModel Create(DbCreationResources resources)
{
- return (IDbCreatorModel)_map.Resolve(resources);
+ try {
+ return (IDbCreatorModel)_map.Resolve(resources);
+ }
+ catch(TargetInvocationException ex) when(ex.InnerException != null) {
+ throw ex.InnerException;
+ }
}
}
}
diff --git a/QS.DbManagement/Creation/DbDumpResources.cs b/QS.DbManagement/Creation/DbDumpResources.cs
index e08ffc69d..9ae1104f6 100644
--- a/QS.DbManagement/Creation/DbDumpResources.cs
+++ b/QS.DbManagement/Creation/DbDumpResources.cs
@@ -1,15 +1,9 @@
using QS.DBScripts.Controllers;
-using QS.DBScripts.Models;
using QS.Dialog;
-using System;
-using System.Collections.Generic;
-using System.Text;
namespace QS.DbManagement.Creation {
public class DbDumpResources : DbCreationResources {
public string DumpFilePath { get; set; }
public IProgressBarDisplayable Progress { get; set; }
- public IDbCreatorInteraction Interactions { get; set; }
- public CreationScript Script { get; set; }
}
}
diff --git a/QS.DbManagement/Creation/DbResourcesCreationMap.cs b/QS.DbManagement/Creation/DbResourcesCreationMap.cs
index 184f36613..41cce5cde 100644
--- a/QS.DbManagement/Creation/DbResourcesCreationMap.cs
+++ b/QS.DbManagement/Creation/DbResourcesCreationMap.cs
@@ -18,6 +18,13 @@ public void Register(Type resource, Type creator)
_map[resource] = arg => Activator.CreateInstance(creator, arg);
}
- public object Resolve(DbCreationResources arg) => _map[arg.GetType()](arg);
+ public object Resolve(DbCreationResources arg)
+ {
+ if(arg == null)
+ throw new ArgumentNullException(nameof(arg));
+ if(!_map.TryGetValue(arg.GetType(), out var creator))
+ throw new InvalidOperationException($"Нет зарегистрированного создателя для ресурса {arg.GetType().Name}");
+ return creator(arg);
+ }
}
}
diff --git a/QS.DbManagement/Entities/DbCreationRequest.cs b/QS.DbManagement/Entities/DbCreationRequest.cs
index 1be7a183c..3820a3f97 100644
--- a/QS.DbManagement/Entities/DbCreationRequest.cs
+++ b/QS.DbManagement/Entities/DbCreationRequest.cs
@@ -1,14 +1,12 @@
-using System.Threading;
using QS.DbManagement.Creation;
using QS.DBScripts.Controllers;
-using QS.Dialog;
using QS.Project.Versioning;
namespace QS.DbManagement.Entities {
///
/// запрос на создание базы
///
- public sealed class DbCreationRequest where CreationArgs : DbCreationResources {
+ public sealed class DbCreationRequest {
public string DbName { get; set; }
public string DbTitle { get; set; }
@@ -18,6 +16,6 @@ public sealed class DbCreationRequest where CreationArgs : DbCreat
public DbCreationFactory CreationFactory { get; set; }
public IApplicationInfo ApplicationInfo { get; set; }
- public CreationArgs CreationResources { get; set; }
+ public DbCreationResources CreationResources { get; set; }
}
}
diff --git a/QS.DbManagement/IDbProvider.cs b/QS.DbManagement/IDbProvider.cs
index 9c0a52ab9..34b40c7f1 100644
--- a/QS.DbManagement/IDbProvider.cs
+++ b/QS.DbManagement/IDbProvider.cs
@@ -4,7 +4,6 @@
using System.Threading;
using System;
using QS.DbManagement.Entities;
-using QS.DBScripts.Controllers;
namespace QS.DbManagement
{
@@ -17,7 +16,7 @@ public interface IDbProvider : IDisposable
///
/// Создаёт базу и сразу наполняет её
///
- bool CreateDatabase(DbCreationRequest request) where CreationArgs : DbCreationResources;
+ bool CreateDatabase(DbCreationRequest request);
bool DropDatabase(DbInfo database);
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index 4a544451f..39141d85d 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -178,7 +178,7 @@ public bool ChangePassword(string username, string oldPassword, string newPasswo
return connection.Execute(sql) != 0;
}
- public bool CreateDatabase(DbCreationRequest request) where CreationArgs : DbCreationResources {
+ public bool CreateDatabase(DbCreationRequest request) {
if(request == null)
throw new ArgumentNullException(nameof(request));
connection.Execute($"CREATE DATABASE IF NOT EXISTS `{request.DbName}`");
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
index 6dc531a72..488be4fa8 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
@@ -4,12 +4,11 @@
using QS.DbManagement.Entities;
using QS.DBScripts;
using QS.DBScripts.Controllers;
+using QS.DBScripts.Models;
using QS.Project.Versioning;
using ReactiveUI;
using System;
using System.Collections.Generic;
-using System.Diagnostics;
-using System.Threading;
namespace QS.Launcher.ViewModels.PageViewModels.DataBase {
public class CreateDbSettingsVM : DbOperationSettingsVM {
@@ -38,14 +37,14 @@ public override IEnumerable BuildPipeline() {
new DbCreationPhase("Создание базы данных", args => {
var factory = args.ServiceProvider.GetRequiredService();
- var request = new DbCreationRequest {
+ var request = new DbCreationRequest {
DbName = DbName,
DbTitle = DbTitle,
CreationFactory = factory,
ApplicationInfo = args.ServiceProvider.GetService(),
Interaction = args.ServiceProvider.GetRequiredService(),
- //заполнение строки подключения оставляем провайдеру
- CreationResources =new MySqlCreationResources{
+ // строку подключения заполнит провайдер
+ CreationResources = new MySqlCreationResources {
Progress = args.Progress,
Interactions = args.ServiceProvider.GetRequiredService(),
Script = args.ServiceProvider.GetRequiredService().MakeCreationScript(),
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/ImportDbSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/ImportDbSettingsVM.cs
index 7c6851125..b1b036550 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/ImportDbSettingsVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/ImportDbSettingsVM.cs
@@ -2,7 +2,6 @@
using QS.DbManagement;
using QS.DbManagement.Creation;
using QS.DbManagement.Entities;
-using QS.DBScripts;
using QS.DBScripts.Controllers;
using QS.Project.Versioning;
using ReactiveUI;
@@ -40,22 +39,21 @@ public string ImportDumpFilePath {
}
public override IEnumerable BuildPipeline() {
- // Наполнение из дампа. Конкретную стратегию строит фабрика по пути.
+ // Наполнение из дампа.
return new[] {
new DbCreationPhase("Импорт базы данных из дампа", args => {
var factory = args.ServiceProvider.GetRequiredService();
- var request = new DbCreationRequest {
+ var request = new DbCreationRequest {
DbName = DbName,
DbTitle = DbTitle,
CreationFactory = factory,
ApplicationInfo = args.ServiceProvider.GetService(),
Interaction = args.ServiceProvider.GetRequiredService(),
- CreationResources =new DbDumpResources{
+ // строку подключения заполнит провайдер
+ CreationResources = new DbDumpResources {
Progress = args.Progress,
DumpFilePath = ImportDumpFilePath,
- Interactions = args.ServiceProvider.GetRequiredService(),
- Script = args.ServiceProvider.GetRequiredService().MakeCreationScript(),
CancellationToken = args.CancellationToken }
};
return args.Provider.CreateDatabase(request);
diff --git a/QS.Updater.Core/DBScripts/Models/MySqlCreationResources.cs b/QS.Updater.Core/DBScripts/Models/MySqlCreationResources.cs
index 99672441d..4c36e3c2b 100644
--- a/QS.Updater.Core/DBScripts/Models/MySqlCreationResources.cs
+++ b/QS.Updater.Core/DBScripts/Models/MySqlCreationResources.cs
@@ -1,13 +1,7 @@
using QS.DBScripts.Controllers;
-using QS.DBScripts.Models;
using QS.Dialog;
-using System;
-using System.Collections.Generic;
-using System.Text;
-using System.Threading;
-
-namespace QS.DbManagement.Creation {
+namespace QS.DBScripts.Models {
public class MySqlCreationResources : DbCreationResources {
public IProgressBarDisplayable Progress { get; set; }
public IDbCreatorInteraction Interactions { get; set; }
diff --git a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
index db8c327e3..e133b76f1 100644
--- a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
+++ b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
@@ -1,5 +1,4 @@
using MySqlConnector;
-using QS.DbManagement.Creation;
using QS.DBScripts.Controllers;
using QS.Dialog;
using System;
@@ -22,12 +21,14 @@ public class MySqlDbCreateModel : IDbCreatorModel
public MySqlDbCreateModel(MySqlCreationResources resources)
{
- if(string.IsNullOrWhiteSpace(connectionString))
- throw new ArgumentException("Connection string is required", nameof(connectionString));
+ if(resources == null)
+ throw new ArgumentNullException(nameof(resources));
+ if(string.IsNullOrWhiteSpace(resources.ConnectionString))
+ throw new ArgumentException("Connection string is required", nameof(resources));
this.connectionString = resources.ConnectionString;
- this.script = resources.Script ?? throw new ArgumentNullException(nameof(script));
- this.progress = resources.Progress ?? throw new ArgumentNullException(nameof(progress));
- this.interaction = resources.Interactions ?? throw new ArgumentNullException(nameof(interaction));
+ this.script = resources.Script ?? throw new ArgumentNullException(nameof(resources.Script));
+ this.progress = resources.Progress ?? throw new ArgumentNullException(nameof(resources.Progress));
+ this.interaction = resources.Interactions ?? throw new ArgumentNullException(nameof(resources.Interactions));
this.cancellationToken = resources.CancellationToken;
}
From de674ca952a3098b4acea0d222a673694a3c7d69 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Sun, 5 Jul 2026 18:10:52 +0300
Subject: [PATCH 031/135] =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20=D0=B2?=
=?UTF-8?q?=D1=81=D0=B5=20=D1=84=D0=B0=D0=B1=D1=80=D0=B8=D0=BA=D0=B8=20?=
=?UTF-8?q?=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D0=B5=D0=B9=20=D0=BD=D0=B0=D0=BF?=
=?UTF-8?q?=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
QS.Cloud.Client/DataBase/QSCloudProvider.cs | 38 ++++++++++---
QS.DbManagement/Creation/DbCreationFactory.cs | 24 ---------
QS.DbManagement/Creation/DbDumpResources.cs | 9 ----
.../Creation/DbResourcesCreationMap.cs | 30 -----------
.../Creation/MariaDbImportModel.cs | 38 -------------
QS.DbManagement/Entities/DbCreationRequest.cs | 14 ++---
QS.DbManagement/Entities/DbImportRequest.cs | 25 +++++++++
QS.DbManagement/IDbDumpService.cs | 10 ++++
QS.DbManagement/IDbProvider.cs | 9 +++-
QS.DbManagement/MariaDb/MariaDBProvider.cs | 28 +++++++---
QS.DbManagement/MariaDb/MariaDbDumpService.cs | 18 +++----
QS.Launcher/DependencyInjection.cs | 24 +++------
.../DataBase/BackupDbSettingsVM.cs | 5 +-
.../DataBase/CreateDbSettingsVM.cs | 17 ++----
.../DataBase/ImportDbSettingsVM.cs | 20 +++----
.../Controllers/DbCreationResources.cs | 8 ---
.../DBScripts/Controllers/IDbCreatorModel.cs | 5 +-
.../Models/MySqlCreationResources.cs | 10 ----
.../DBScripts/Models/MySqlDbCreateModel.cs | 53 ++++++++-----------
.../Controllers/UserCreateDbController.cs | 18 ++++---
20 files changed, 168 insertions(+), 235 deletions(-)
delete mode 100644 QS.DbManagement/Creation/DbCreationFactory.cs
delete mode 100644 QS.DbManagement/Creation/DbDumpResources.cs
delete mode 100644 QS.DbManagement/Creation/DbResourcesCreationMap.cs
delete mode 100644 QS.DbManagement/Creation/MariaDbImportModel.cs
create mode 100644 QS.DbManagement/Entities/DbImportRequest.cs
create mode 100644 QS.DbManagement/IDbDumpService.cs
delete mode 100644 QS.Project.Core/DBScripts/Controllers/DbCreationResources.cs
delete mode 100644 QS.Updater.Core/DBScripts/Models/MySqlCreationResources.cs
diff --git a/QS.Cloud.Client/DataBase/QSCloudProvider.cs b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
index 37157262e..ce1828d25 100644
--- a/QS.Cloud.Client/DataBase/QSCloudProvider.cs
+++ b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
@@ -1,11 +1,9 @@
-using FluentNHibernate.Cfg.Db;
using Grpc.Core;
using MySqlConnector;
using QS.Cloud.Client.Clients;
using QS.Cloud.Core;
using QS.DbManagement;
using QS.DbManagement.Entities;
-using QS.DBScripts.Controllers;
using QS.Dialog;
using QS.Project.Versioning;
using System;
@@ -35,7 +33,6 @@ public class QSCloudProvider : IDbProvider {
private LoginManagementCloudClient loginClient;
private DataBaseManagementCloudClient dbClient;
-
public QSCloudProvider(IList parameters, string password = null) {
Account = parameters.First(p => p.Name == "Account").Value;
UserName = parameters.First(p => p.Name == "Login").Value;
@@ -71,9 +68,34 @@ public bool CreateDatabase(DbCreationRequest request) {
return false;
}
- request.CreationResources.ConnectionString = session.ConnectionStringBuilder.ConnectionString;
- var creationModel = request.CreationFactory.Create(request.CreationResources);
- return creationModel.RunCreation(session.Db.BaseName, request.DbTitle);
+ return request.CreationModel.RunCreation(
+ session.ConnectionStringBuilder.ConnectionString,
+ session.Db.BaseName, request.DbTitle,
+ request.Progress, request.CancellationToken);
+ }
+ }
+
+ public bool ImportDatabase(DbImportRequest request) {
+ if(request == null)
+ throw new ArgumentNullException(nameof(request));
+
+ var response = dbClient.CreateDataBase(request.DbName, request.DbTitle, request.ApplicationInfo);
+
+ using(var session = CloudDbSession.Open(loginClient, response.BaseId)) {
+ if(!session.Success) {
+ request.Interaction.ReportError("Не удалось открыть сессию к созданной базе: " + session.Description, "Импорт базы в облако");
+ return false;
+ }
+ if(!session.IsAdmin) {
+ request.Interaction.ReportError("Вы не имеете прав администратора для наполнения базы", "Импорт базы в облако");
+ return false;
+ }
+
+ request.DumpService.Import(
+ session.ConnectionStringBuilder.ConnectionString, session.Db.BaseName, request.DumpFilePath,
+ request.Progress, request.CancellationToken, request.DbTitle);
+ request.CancellationToken.ThrowIfCancellationRequested();
+ return true;
}
}
@@ -88,12 +110,12 @@ public bool DropDatabase(DbInfo database)
return response.Success;
}
- public void BackupDatabase(DbInfo database, string filePath, IProgressBarDisplayable progress, CancellationToken cancellation)
+ public void BackupDatabase(DbInfo database, string filePath, IDbDumpService dumpService, IProgressBarDisplayable progress, CancellationToken cancellation)
{
using(var session = CloudDbSession.Open(loginClient, database.BaseId)) {
if(!session.Success)
throw new InvalidOperationException("Не удалось открыть сессию к облачной базе: " + session.Description);
- new MariaDbDumpService().Export(session.ConnectionStringBuilder, session.Db.BaseName, filePath, progress, cancellation);
+ dumpService.Export(session.ConnectionStringBuilder.ConnectionString, session.Db.BaseName, filePath, progress, cancellation);
}
}
diff --git a/QS.DbManagement/Creation/DbCreationFactory.cs b/QS.DbManagement/Creation/DbCreationFactory.cs
deleted file mode 100644
index 8e983566c..000000000
--- a/QS.DbManagement/Creation/DbCreationFactory.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using QS.DBScripts.Controllers;
-using System;
-using System.Reflection;
-
-namespace QS.DbManagement.Creation {
- public class DbCreationFactory
- {
- private readonly DbResourcesCreationMap _map;
-
- public DbCreationFactory(DbResourcesCreationMap map) {
- _map = map ?? throw new ArgumentNullException(nameof(map));
- }
-
- public IDbCreatorModel Create(DbCreationResources resources)
- {
- try {
- return (IDbCreatorModel)_map.Resolve(resources);
- }
- catch(TargetInvocationException ex) when(ex.InnerException != null) {
- throw ex.InnerException;
- }
- }
- }
-}
diff --git a/QS.DbManagement/Creation/DbDumpResources.cs b/QS.DbManagement/Creation/DbDumpResources.cs
deleted file mode 100644
index 9ae1104f6..000000000
--- a/QS.DbManagement/Creation/DbDumpResources.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using QS.DBScripts.Controllers;
-using QS.Dialog;
-
-namespace QS.DbManagement.Creation {
- public class DbDumpResources : DbCreationResources {
- public string DumpFilePath { get; set; }
- public IProgressBarDisplayable Progress { get; set; }
- }
-}
diff --git a/QS.DbManagement/Creation/DbResourcesCreationMap.cs b/QS.DbManagement/Creation/DbResourcesCreationMap.cs
deleted file mode 100644
index 41cce5cde..000000000
--- a/QS.DbManagement/Creation/DbResourcesCreationMap.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using QS.DBScripts.Controllers;
-using System;
-using System.Collections.Generic;
-
-namespace QS.DbManagement.Creation {
- public class DbResourcesCreationMap
- {
- private Dictionary> _map = new Dictionary>();
-
- public void Register(Type resource, Type creator)
- {
- if(!typeof(DbCreationResources).IsAssignableFrom(resource))
- throw new ArgumentException($"{resource} не наследует DbCreationResources", nameof(resource));
-
- if(!typeof(IDbCreatorModel).IsAssignableFrom(creator))
- throw new ArgumentException($"{creator} не реализует IDbCreatorModel", nameof(creator));
-
- _map[resource] = arg => Activator.CreateInstance(creator, arg);
- }
-
- public object Resolve(DbCreationResources arg)
- {
- if(arg == null)
- throw new ArgumentNullException(nameof(arg));
- if(!_map.TryGetValue(arg.GetType(), out var creator))
- throw new InvalidOperationException($"Нет зарегистрированного создателя для ресурса {arg.GetType().Name}");
- return creator(arg);
- }
- }
-}
diff --git a/QS.DbManagement/Creation/MariaDbImportModel.cs b/QS.DbManagement/Creation/MariaDbImportModel.cs
deleted file mode 100644
index 2ffcb7621..000000000
--- a/QS.DbManagement/Creation/MariaDbImportModel.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using MySqlConnector;
-using QS.DBScripts.Controllers;
-using QS.Dialog;
-using System;
-using System.Resources;
-using System.Threading;
-
-namespace QS.DbManagement.Creation {
- ///
- /// Наполнение MariaDB базы пользовательским дампом.
- /// Метод блокирует вызывающий поток — выносить в фон ответственность вызывающего кода.
- ///
- public class MariaDbImportModel : IDbCreatorModel {
- private readonly string connectionString;
- private readonly string dumpFilePath;
- private readonly IProgressBarDisplayable progress;
- private readonly CancellationToken cancellation;
-
- public MariaDbImportModel(
- DbDumpResources resources) {
- this.connectionString = resources.ConnectionString
- ?? throw new ArgumentNullException(nameof(connectionString));
- this.dumpFilePath = resources.DumpFilePath;
- this.progress = resources.Progress;
- this.cancellation = resources.CancellationToken;
- }
-
- public bool RunCreation(string dbName, string dbTitle) {
- if(string.IsNullOrWhiteSpace(dumpFilePath))
- throw new ArgumentException("Не задан путь к дампу", nameof(dumpFilePath));
-
- var connectionStringBuilder = new MySqlConnectionStringBuilder(connectionString);
- new MariaDbDumpService().Import(connectionStringBuilder, dbName, dumpFilePath, progress, cancellation, dbTitle);
- cancellation.ThrowIfCancellationRequested();
- return true;
- }
- }
-}
diff --git a/QS.DbManagement/Entities/DbCreationRequest.cs b/QS.DbManagement/Entities/DbCreationRequest.cs
index 3820a3f97..90af814a1 100644
--- a/QS.DbManagement/Entities/DbCreationRequest.cs
+++ b/QS.DbManagement/Entities/DbCreationRequest.cs
@@ -1,21 +1,21 @@
-using QS.DbManagement.Creation;
using QS.DBScripts.Controllers;
+using QS.Dialog;
using QS.Project.Versioning;
+using System.Threading;
namespace QS.DbManagement.Entities {
///
- /// запрос на создание базы
+ /// запрос на создание базы с наполнением из скрипта
///
public sealed class DbCreationRequest {
public string DbName { get; set; }
public string DbTitle { get; set; }
- public IDbCreatorInteraction Interaction { get; set; }
+ public IDbCreatorModel CreationModel { get; set; }
- /// Чем наполнять созданную базу
- public DbCreationFactory CreationFactory { get; set; }
+ public IProgressBarDisplayable Progress { get; set; }
+ public IDbCreatorInteraction Interaction { get; set; }
public IApplicationInfo ApplicationInfo { get; set; }
-
- public DbCreationResources CreationResources { get; set; }
+ public CancellationToken CancellationToken { get; set; }
}
}
diff --git a/QS.DbManagement/Entities/DbImportRequest.cs b/QS.DbManagement/Entities/DbImportRequest.cs
new file mode 100644
index 000000000..2bc14919d
--- /dev/null
+++ b/QS.DbManagement/Entities/DbImportRequest.cs
@@ -0,0 +1,25 @@
+using QS.DBScripts.Controllers;
+using QS.Dialog;
+using QS.Project.Versioning;
+using System.Threading;
+
+namespace QS.DbManagement.Entities {
+ ///
+ /// запрос на создание базы с наполнением из пользовательского дампа
+ ///
+ public sealed class DbImportRequest {
+ public string DbName { get; set; }
+ public string DbTitle { get; set; }
+
+ /// Путь к дампу, которым наполняется база
+ public string DumpFilePath { get; set; }
+
+ /// Сервис заливки дампа из DI; строку подключения ему выдаст провайдер
+ public IDbDumpService DumpService { get; set; }
+
+ public IProgressBarDisplayable Progress { get; set; }
+ public IDbCreatorInteraction Interaction { get; set; }
+ public IApplicationInfo ApplicationInfo { get; set; }
+ public CancellationToken CancellationToken { get; set; }
+ }
+}
diff --git a/QS.DbManagement/IDbDumpService.cs b/QS.DbManagement/IDbDumpService.cs
new file mode 100644
index 000000000..88ceef3c2
--- /dev/null
+++ b/QS.DbManagement/IDbDumpService.cs
@@ -0,0 +1,10 @@
+using QS.Dialog;
+using System.Threading;
+
+namespace QS.DbManagement {
+ public interface IDbDumpService {
+ void Export(string connectionString, string databaseName, string filePath, IProgressBarDisplayable progress, CancellationToken cancellation);
+
+ void Import(string connectionString, string databaseName, string filePath, IProgressBarDisplayable progress, CancellationToken cancellation, string title = null);
+ }
+}
diff --git a/QS.DbManagement/IDbProvider.cs b/QS.DbManagement/IDbProvider.cs
index 34b40c7f1..07453558c 100644
--- a/QS.DbManagement/IDbProvider.cs
+++ b/QS.DbManagement/IDbProvider.cs
@@ -14,13 +14,18 @@ public interface IDbProvider : IDisposable
bool ChangePassword(string username, string oldPassword, string newPassword);
///
- /// Создаёт базу и сразу наполняет её
+ /// Создаёт базу и наполняет её скриптом создания
///
bool CreateDatabase(DbCreationRequest request);
+ ///
+ /// Создаёт базу и наполняет её пользовательским дампом
+ ///
+ bool ImportDatabase(DbImportRequest request);
+
bool DropDatabase(DbInfo database);
- void BackupDatabase(DbInfo database, string filePath, IProgressBarDisplayable progress, CancellationToken cancellation);
+ void BackupDatabase(DbInfo database, string filePath, IDbDumpService dumpService, IProgressBarDisplayable progress, CancellationToken cancellation);
bool AddUser(string username, string password);
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index 39141d85d..d04790ab1 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -1,7 +1,6 @@
using Dapper;
using MySqlConnector;
using QS.DbManagement.Entities;
-using QS.DBScripts.Controllers;
using QS.Dialog;
using QS.Project.Versioning;
using System;
@@ -186,9 +185,26 @@ public bool CreateDatabase(DbCreationRequest request) {
var connectionStringBuilder = new MySqlConnectionStringBuilder(ConnectionStringBuilder.ConnectionString) {
Database = request.DbName
};
- request.CreationResources.ConnectionString = connectionStringBuilder.ConnectionString;
- var creationModel = request.CreationFactory.Create(request.CreationResources);
- return creationModel.RunCreation(request.DbName, request.DbTitle);
+ return request.CreationModel.RunCreation(
+ connectionStringBuilder.ConnectionString,
+ request.DbName, request.DbTitle,
+ request.Progress, request.CancellationToken);
+ }
+
+ ///
+ /// Создание базы с наполнением из пользовательского дампа
+ /// Метод блокирующий - вызывать из фонового потока
+ ///
+ public bool ImportDatabase(DbImportRequest request) {
+ if(request == null)
+ throw new ArgumentNullException(nameof(request));
+ connection.Execute($"CREATE DATABASE IF NOT EXISTS `{request.DbName}`");
+
+ request.DumpService.Import(
+ ConnectionStringBuilder.ConnectionString, request.DbName, request.DumpFilePath,
+ request.Progress, request.CancellationToken, request.DbTitle);
+ request.CancellationToken.ThrowIfCancellationRequested();
+ return true;
}
public bool DropDatabase(DbInfo database) {
@@ -200,8 +216,8 @@ public bool DropDatabase(DbInfo database) {
/// Резервное копирование базы в скрипт
/// Метод блокирующий - вызывать из фонового потока
///
- public void BackupDatabase(DbInfo database, string filePath, IProgressBarDisplayable progress, CancellationToken cancellation) {
- new MariaDbDumpService().Export(ConnectionStringBuilder, database.BaseName, filePath, progress, cancellation);
+ public void BackupDatabase(DbInfo database, string filePath, IDbDumpService dumpService, IProgressBarDisplayable progress, CancellationToken cancellation) {
+ dumpService.Export(ConnectionStringBuilder.ConnectionString, database.BaseName, filePath, progress, cancellation);
}
public void Dispose() {
diff --git a/QS.DbManagement/MariaDb/MariaDbDumpService.cs b/QS.DbManagement/MariaDb/MariaDbDumpService.cs
index 7a521fe62..70c42153a 100644
--- a/QS.DbManagement/MariaDb/MariaDbDumpService.cs
+++ b/QS.DbManagement/MariaDb/MariaDbDumpService.cs
@@ -5,10 +5,10 @@
using QS.Dialog;
namespace QS.DbManagement {
- public class MariaDbDumpService {
+ public class MariaDbDumpService : IDbDumpService {
/// Выгружает базу в файл
public void Export(
- MySqlConnectionStringBuilder connectionSettings,
+ string connectionString,
string databaseName,
string filePath,
IProgressBarDisplayable progress,
@@ -23,7 +23,7 @@ public void Export(
progress?.Update($"Создаём резервную копию базы {databaseName} в файл {filePath}");
- RunWithBackup(connectionSettings, databaseName, backup => {
+ RunWithBackup(connectionString, databaseName, backup => {
bool started = false;
string currentTable = null;
backup.ExportProgressChanged += (sender, e) => {
@@ -47,7 +47,7 @@ public void Export(
/// Заливает дамп в уже существующую базу
public void Import(
- MySqlConnectionStringBuilder connectionSettings,
+ string connectionString,
string databaseName,
string filePath,
IProgressBarDisplayable progress,
@@ -58,7 +58,7 @@ public void Import(
progress?.Update($"Импортируем дамп {filePath} в базу {databaseName}");
- RunWithBackup(connectionSettings, databaseName, backup => {
+ RunWithBackup(connectionString, databaseName, backup => {
bool started = false;
backup.ImportProgressChanged += (sender, e) => {
if(cancellation.IsCancellationRequested) {
@@ -88,13 +88,13 @@ ON DUPLICATE KEY UPDATE
});
}
- private void RunWithBackup(MySqlConnectionStringBuilder connectionSettings, string databaseName, Action action) {
- if(connectionSettings == null)
- throw new ArgumentNullException(nameof(connectionSettings));
+ private void RunWithBackup(string connectionString, string databaseName, Action action) {
+ if(string.IsNullOrWhiteSpace(connectionString))
+ throw new ArgumentException("Не указана строка подключения", nameof(connectionString));
if(string.IsNullOrWhiteSpace(databaseName))
throw new ArgumentException("Не указано имя базы", nameof(databaseName));
- var builder = new MySqlConnectionStringBuilder(connectionSettings.ConnectionString) {
+ var builder = new MySqlConnectionStringBuilder(connectionString) {
Database = databaseName
};
diff --git a/QS.Launcher/DependencyInjection.cs b/QS.Launcher/DependencyInjection.cs
index 37d04ad7e..1208b7fe4 100644
--- a/QS.Launcher/DependencyInjection.cs
+++ b/QS.Launcher/DependencyInjection.cs
@@ -7,24 +7,9 @@
using QS.Launcher.ViewModels;
using QS.Launcher.ViewModels.PageViewModels;
using QS.Launcher.ViewModels.PageViewModels.DataBase;
-using QS.DbManagement.Creation;
-using System;
-using System.Collections.Generic;
namespace QS.Launcher {
public static partial class DependencyInjection {
- public static IServiceCollection AddLauncherDataBaseCreation(this IServiceCollection services, List<(Type res,Type creator)> resourceCratorMap)
- {
- var map = new DbResourcesCreationMap();
- foreach(var resourceCrator in resourceCratorMap) {
- map.Register(resourceCrator.res, resourceCrator.creator);
- }
-
- return services
- .AddSingleton(map)
- .AddSingleton();
- }
-
public static IServiceCollection AddLauncherViewModels(this IServiceCollection services) {
return services
.AddSingleton()
@@ -46,9 +31,12 @@ public static IServiceCollection AddLauncherDependencies(this IServiceCollection
.AddSingleton();
}
- public static IServiceCollection AddConnectionType(this IServiceCollection services, ConnectionTypeBase connectionType) {
- services.AddSingleton(connectionType);
- return services;
+ ///
+ /// Тип подключения создаётся контейнером, чтобы получить свои зависимости через DI
+ ///
+ public static IServiceCollection AddConnectionType(this IServiceCollection services)
+ where TConnectionType : ConnectionTypeBase {
+ return services.AddSingleton();
}
#region AppRunner
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/BackupDbSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/BackupDbSettingsVM.cs
index e5b2f5b6d..f2ab08917 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/BackupDbSettingsVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/BackupDbSettingsVM.cs
@@ -1,3 +1,4 @@
+using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.IO;
@@ -38,7 +39,9 @@ public override IEnumerable BuildPipeline() {
new DbCreationPhase(
"Создание резервной копии базы данных",
args => {
- args.Provider.BackupDatabase(database, BackupFilePath, args.Progress, args.CancellationToken);
+ args.Provider.BackupDatabase(database, BackupFilePath,
+ args.ServiceProvider.GetRequiredService(),
+ args.Progress, args.CancellationToken);
args.CancellationToken.ThrowIfCancellationRequested();
return true;
})
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
index 488be4fa8..1d77e72d5 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/CreateDbSettingsVM.cs
@@ -1,10 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using QS.DbManagement;
-using QS.DbManagement.Creation;
using QS.DbManagement.Entities;
-using QS.DBScripts;
using QS.DBScripts.Controllers;
-using QS.DBScripts.Models;
using QS.Project.Versioning;
using ReactiveUI;
using System;
@@ -35,20 +32,14 @@ public string DbName {
public override IEnumerable BuildPipeline() {
return new[] {
new DbCreationPhase("Создание базы данных", args => {
- var factory = args.ServiceProvider.GetRequiredService();
-
var request = new DbCreationRequest {
DbName = DbName,
DbTitle = DbTitle,
- CreationFactory = factory,
- ApplicationInfo = args.ServiceProvider.GetService(),
+ CreationModel = args.ServiceProvider.GetRequiredService(),
+ Progress = args.Progress,
Interaction = args.ServiceProvider.GetRequiredService(),
- // строку подключения заполнит провайдер
- CreationResources = new MySqlCreationResources {
- Progress = args.Progress,
- Interactions = args.ServiceProvider.GetRequiredService(),
- Script = args.ServiceProvider.GetRequiredService().MakeCreationScript(),
- CancellationToken = args.CancellationToken }
+ ApplicationInfo = args.ServiceProvider.GetService(),
+ CancellationToken = args.CancellationToken
};
return args.Provider.CreateDatabase(request);
})
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/ImportDbSettingsVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/ImportDbSettingsVM.cs
index b1b036550..d7b023061 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/ImportDbSettingsVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/ImportDbSettingsVM.cs
@@ -1,6 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using QS.DbManagement;
-using QS.DbManagement.Creation;
using QS.DbManagement.Entities;
using QS.DBScripts.Controllers;
using QS.Project.Versioning;
@@ -39,24 +38,19 @@ public string ImportDumpFilePath {
}
public override IEnumerable BuildPipeline() {
- // Наполнение из дампа.
return new[] {
new DbCreationPhase("Импорт базы данных из дампа", args => {
- var factory = args.ServiceProvider.GetRequiredService();
-
- var request = new DbCreationRequest {
+ var request = new DbImportRequest {
DbName = DbName,
DbTitle = DbTitle,
- CreationFactory = factory,
- ApplicationInfo = args.ServiceProvider.GetService(),
+ DumpFilePath = ImportDumpFilePath,
+ DumpService = args.ServiceProvider.GetRequiredService(),
+ Progress = args.Progress,
Interaction = args.ServiceProvider.GetRequiredService(),
- // строку подключения заполнит провайдер
- CreationResources = new DbDumpResources {
- Progress = args.Progress,
- DumpFilePath = ImportDumpFilePath,
- CancellationToken = args.CancellationToken }
+ ApplicationInfo = args.ServiceProvider.GetService(),
+ CancellationToken = args.CancellationToken
};
- return args.Provider.CreateDatabase(request);
+ return args.Provider.ImportDatabase(request);
})
};
}
diff --git a/QS.Project.Core/DBScripts/Controllers/DbCreationResources.cs b/QS.Project.Core/DBScripts/Controllers/DbCreationResources.cs
deleted file mode 100644
index 2159edc14..000000000
--- a/QS.Project.Core/DBScripts/Controllers/DbCreationResources.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using System.Threading;
-
-namespace QS.DBScripts.Controllers {
- public abstract class DbCreationResources {
- public string ConnectionString { get; set; }
- public CancellationToken CancellationToken { get; set; }
- }
-}
diff --git a/QS.Project.Core/DBScripts/Controllers/IDbCreatorModel.cs b/QS.Project.Core/DBScripts/Controllers/IDbCreatorModel.cs
index c562fac51..9c0bf4bfa 100644
--- a/QS.Project.Core/DBScripts/Controllers/IDbCreatorModel.cs
+++ b/QS.Project.Core/DBScripts/Controllers/IDbCreatorModel.cs
@@ -1,3 +1,6 @@
+using QS.Dialog;
+using System.Threading;
+
namespace QS.DBScripts.Controllers
{
///
@@ -7,6 +10,6 @@ public interface IDbCreatorModel
{
// Метод блокирует вызывающий поток на время работы с базой
// Вынесение в фоновый поток — ответственность вызывающего кода
- bool RunCreation(string dbName, string dbTitle);
+ bool RunCreation(string connectionString, string dbName, string dbTitle, IProgressBarDisplayable progress, CancellationToken cancellationToken);
}
}
diff --git a/QS.Updater.Core/DBScripts/Models/MySqlCreationResources.cs b/QS.Updater.Core/DBScripts/Models/MySqlCreationResources.cs
deleted file mode 100644
index 4c36e3c2b..000000000
--- a/QS.Updater.Core/DBScripts/Models/MySqlCreationResources.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using QS.DBScripts.Controllers;
-using QS.Dialog;
-
-namespace QS.DBScripts.Models {
- public class MySqlCreationResources : DbCreationResources {
- public IProgressBarDisplayable Progress { get; set; }
- public IDbCreatorInteraction Interactions { get; set; }
- public CreationScript Script { get; set; }
- }
-}
diff --git a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
index e133b76f1..9bb443870 100644
--- a/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
+++ b/QS.Updater.Core/DBScripts/Models/MySqlDbCreateModel.cs
@@ -11,49 +11,40 @@ public class MySqlDbCreateModel : IDbCreatorModel
{
static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
- private readonly string connectionString;
- private readonly CreationScript script;
- private readonly IProgressBarDisplayable progress;
+ private string connectionString;
+ private CreationScript script;
+ private IProgressBarDisplayable progress;
private readonly IDbCreatorInteraction interaction;
- private readonly CancellationToken cancellationToken;
+ private CancellationToken cancellationToken;
+ private readonly IDbScriptsConfiguration scriptsConfiguration;
public bool FillBaseGuid { get; set; } = true;
- public MySqlDbCreateModel(MySqlCreationResources resources)
+ public MySqlDbCreateModel(IDbScriptsConfiguration scriptsConfiguration, IDbCreatorInteraction interaction)
{
- if(resources == null)
- throw new ArgumentNullException(nameof(resources));
- if(string.IsNullOrWhiteSpace(resources.ConnectionString))
- throw new ArgumentException("Connection string is required", nameof(resources));
- this.connectionString = resources.ConnectionString;
- this.script = resources.Script ?? throw new ArgumentNullException(nameof(resources.Script));
- this.progress = resources.Progress ?? throw new ArgumentNullException(nameof(resources.Progress));
- this.interaction = resources.Interactions ?? throw new ArgumentNullException(nameof(resources.Interactions));
- this.cancellationToken = resources.CancellationToken;
+ this.scriptsConfiguration = scriptsConfiguration ?? throw new ArgumentNullException(nameof(scriptsConfiguration));
+ this.interaction = interaction ?? throw new ArgumentNullException(nameof(interaction));
}
- public MySqlDbCreateModel(
- string server, uint port, string login, string password,
- CreationScript script,
- IProgressBarDisplayable progress,
- IDbCreatorInteraction interaction,
- CancellationToken cancellationToken) {
-
- this.connectionString = new MySqlConnectionStringBuilder {
- Server = server,
- Port = port,
- UserID = login,
- Password = password,
- AllowUserVariables = true
- }.ConnectionString;
+ public MySqlDbCreateModel(CreationScript script, IDbCreatorInteraction interaction)
+ {
this.script = script ?? throw new ArgumentNullException(nameof(script));
- this.progress = progress ?? throw new ArgumentNullException(nameof(progress));
this.interaction = interaction ?? throw new ArgumentNullException(nameof(interaction));
- this.cancellationToken = cancellationToken;
}
+ public bool RunCreation(string connectionString, string dbName, string dbTitle, IProgressBarDisplayable progress, CancellationToken cancellationToken)
+ {
+ if(string.IsNullOrWhiteSpace(connectionString))
+ throw new ArgumentException("Connection string is required", nameof(connectionString));
+ this.connectionString = connectionString;
+ this.progress = progress ?? throw new ArgumentNullException(nameof(progress));
+ this.cancellationToken = cancellationToken;
+ if(script == null)
+ script = scriptsConfiguration.MakeCreationScript();
+ return RunCreationCore(dbName, dbTitle);
+ }
- public bool RunCreation(string dbName, string dbTitle = null) {
+ private bool RunCreationCore(string dbName, string dbTitle) {
using(var connectionDB = new MySqlConnection(connectionString)) {
try {
logger.Info("Connecting to MySQL...");
diff --git a/QS.Updater.DB/DBScripts/Controllers/UserCreateDbController.cs b/QS.Updater.DB/DBScripts/Controllers/UserCreateDbController.cs
index 81b9217d7..7bece6f58 100644
--- a/QS.Updater.DB/DBScripts/Controllers/UserCreateDbController.cs
+++ b/QS.Updater.DB/DBScripts/Controllers/UserCreateDbController.cs
@@ -1,3 +1,4 @@
+using MySqlConnector;
using System;
using System.Threading;
using QS.DBScripts.Models;
@@ -45,14 +46,17 @@ void StartCreation(string server, string dbname, string login, string password)
try {
ParseServer(server, out string host, out uint port);
- var createModel = new MySqlDbCreateModel(
- host, port, login, password,
- creationScript,
- Progress,
- interaction: this,
- cancellationToken: CancellationToken.None);
+ var connectionString = new MySqlConnectionStringBuilder {
+ Server = host,
+ Port = port,
+ UserID = login,
+ Password = password,
+ AllowUserVariables = true
+ }.ConnectionString;
- bool success = createModel.RunCreation(dbname);
+ var createModel = new MySqlDbCreateModel(creationScript, interaction: this);
+
+ bool success = createModel.RunCreation(connectionString, dbname, dbTitle: null, Progress, CancellationToken.None);
if(success)
interactive.ShowMessage(ImportanceLevel.Info, "Создание базы успешно завершено.\nЗайдите в программу под администратором для добавления пользователей.");
}
From 9aa3b9eb5f7913d57d6e6b2115bd3071fc537d31 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Mon, 6 Jul 2026 16:39:34 +0300
Subject: [PATCH 032/135] =?UTF-8?q?=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20?=
=?UTF-8?q?=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20?=
=?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5?=
=?UTF-8?q?=D0=BB=D0=B5=D0=BC=20=D0=B2=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B0?=
=?UTF-8?q?=D0=B9=D0=B4=D0=B5=D1=80=D0=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Clients/UserManagementCloudClient.cs | 78 ++--
QS.Cloud.Client/DataBase/QSCloudProvider.cs | 123 +++++-
QS.Cloud.Client/Protos/UserManagement.proto | 51 ++-
QS.DbManagement/DbCapabilities.cs | 8 +
QS.DbManagement/Entities/DbUserBaseAccess.cs | 15 +
QS.DbManagement/Entities/DbUserInfo.cs | 36 ++
QS.DbManagement/IDbProvider.cs | 24 +-
QS.DbManagement/MariaDb/MariaDBProvider.cs | 346 +++++++++++++++--
.../Views/Pages/DataBase/DataBasesView.axaml | 16 +-
.../Views/Pages/UserManagementView.axaml | 102 ++++-
QS.Launcher/ViewModels/MainWindowVM.cs | 1 -
.../PageViewModels/BaseAccessRowVM.cs | 80 ++++
.../PageViewModels/DataBase/DataBasesVM.cs | 16 +
.../PageViewModels/UserManagementVM.cs | 357 ++++++++++++++++++
14 files changed, 1170 insertions(+), 83 deletions(-)
create mode 100644 QS.DbManagement/Entities/DbUserBaseAccess.cs
create mode 100644 QS.DbManagement/Entities/DbUserInfo.cs
create mode 100644 QS.Launcher/ViewModels/PageViewModels/BaseAccessRowVM.cs
diff --git a/QS.Cloud.Client/Clients/UserManagementCloudClient.cs b/QS.Cloud.Client/Clients/UserManagementCloudClient.cs
index 77ed41744..4a6ced284 100644
--- a/QS.Cloud.Client/Clients/UserManagementCloudClient.cs
+++ b/QS.Cloud.Client/Clients/UserManagementCloudClient.cs
@@ -1,62 +1,80 @@
+using System.Collections.Generic;
+using System.Linq;
using QS.Cloud.Core;
namespace QS.Cloud.Client
{
- public class UserManagementCloudClient : CloudClientBySession
+ public class UserManagementCloudClient : CloudClientByBasicAuth
{
- public UserManagementCloudClient(ISessionInfoProvider sessionInfoProvider)
- : base(sessionInfoProvider, "core.cloud.qsolution.ru", 443) { }
+ public UserManagementCloudClient(IBasicAuthInfoProvider basicAuthInfoProvider)
+ : base(basicAuthInfoProvider, "core.cloud.qsolution.ru", 443) { }
-
- public CreateUserResponse CreateUser(string login, string userName, string email, string password)
+ public List GetUsers()
{
var client = new UserManagement.UserManagementClient(Channel);
+ var response = client.GetUsers(new GetUsersRequest(), headers);
+ return response.Users.ToList();
+ }
- var request = new CreateUserRequest
- {
- Login = login, Name = userName, Email = email, Password = password
+ public CreateUserResponse CreateUser(UserInfo user, string password)
+ {
+ var client = new UserManagement.UserManagementClient(Channel);
+ var request = new CreateUserRequest {
+ Login = user.Login,
+ Name = user.Name ?? "",
+ Email = user.Email ?? "",
+ Password = password ?? "",
+ Phone = user.Phone ?? "",
+ Post = user.Post ?? "",
+ Comment = user.Comment ?? "",
+ IsAccountAdmin = user.IsAccountAdmin
};
+ return client.CreateUser(request, headers);
+ }
- var response = client.CreateUser(request, headers);
-
- return response;
+ public UpdateUserResponse UpdateUser(UserInfo user, string newPassword)
+ {
+ var client = new UserManagement.UserManagementClient(Channel);
+ var request = new UpdateUserRequest {
+ Login = user.Login,
+ Name = user.Name ?? "",
+ Email = user.Email ?? "",
+ Phone = user.Phone ?? "",
+ Post = user.Post ?? "",
+ Comment = user.Comment ?? "",
+ Disabled = user.Disabled,
+ IsAccountAdmin = user.IsAccountAdmin,
+ NewPassword = newPassword ?? ""
+ };
+ return client.UpdateUser(request, headers);
}
public DeleteUserResponse DeleteUser(string login)
{
var client = new UserManagement.UserManagementClient(Channel);
-
var request = new DeleteUserRequest { User = login };
- var response = client.DeleteUser(request, headers);
-
- return response;
+ return client.DeleteUser(request, headers);
}
- // strange, but protobuf has the same signature
- public UpdateUserResponse UpdateUser()
+ public List GetUserBaseAccess(string login, uint productId)
{
var client = new UserManagement.UserManagementClient(Channel);
-
- var request = new UpdateUserRequest();
- var response = client.UpdateUser(request, headers);
-
- return response;
+ var request = new GetUserBaseAccessRequest { User = login, ProductId = productId };
+ var response = client.GetUserBaseAccess(request, headers);
+ return response.Bases.ToList();
}
- public bool ChangeBaseAccess(string user, int baseId, bool grant, bool admin)
+ public bool ChangeBaseAccess(string user, int baseId, bool grant, bool admin, bool readOnly)
{
var client = new UserManagement.UserManagementClient(Channel);
-
- var request = new ChangeBaseAccessRequest
- {
+ var request = new ChangeBaseAccessRequest {
User = user,
BaseId = baseId,
Grant = grant,
- Admin = admin
+ Admin = admin,
+ ReadOnly = readOnly
};
-
- var response = client.ChangeBaseAccess(request);
-
+ var response = client.ChangeBaseAccess(request, headers);
return response.Success;
}
}
diff --git a/QS.Cloud.Client/DataBase/QSCloudProvider.cs b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
index ce1828d25..89297dbfb 100644
--- a/QS.Cloud.Client/DataBase/QSCloudProvider.cs
+++ b/QS.Cloud.Client/DataBase/QSCloudProvider.cs
@@ -32,6 +32,7 @@ public class QSCloudProvider : IDbProvider {
private LoginManagementCloudClient loginClient;
private DataBaseManagementCloudClient dbClient;
+ private UserManagementCloudClient userClient;
public QSCloudProvider(IList parameters, string password = null) {
Account = parameters.First(p => p.Name == "Account").Value;
@@ -40,17 +41,123 @@ public QSCloudProvider(IList parameters, string passwo
loginClient = new LoginManagementCloudClient(authInfo);
dbClient = new DataBaseManagementCloudClient(authInfo);
+ userClient = new UserManagementCloudClient(authInfo);
}
- public bool AddUser(string username, string password)
- {
- throw new NotImplementedException();
+ #region Управление пользователями
+
+ public bool CanManageUsers => IsAdmin;
+
+ public DbUserFields SupportedUserFields =>
+ DbUserFields.Name | DbUserFields.Email | DbUserFields.Phone | DbUserFields.Post
+ | DbUserFields.Comment | DbUserFields.AdminFlag | DbUserFields.Disabling | DbUserFields.BaseReadOnly;
+
+ public bool ChangeOwnPassword(string newPassword) {
+ try {
+ return loginClient.ChangePassword(newPassword).Success;
+ }
+ catch(RpcException ex) {
+ throw CloudError(ex);
+ }
}
-
- public bool ChangePassword(string username, string oldPassword, string newPassword)
- {
- throw new NotImplementedException();
+
+ public List GetUsers() {
+ try {
+ return userClient.GetUsers().Select(u => new DbUserInfo {
+ Login = u.Login,
+ Name = u.Name,
+ Email = u.Email,
+ Phone = u.Phone,
+ Post = u.Post,
+ Comment = u.Comment,
+ Disabled = u.Disabled,
+ IsAdmin = u.IsAccountAdmin,
+ IsCurrentUser = string.Equals(u.Login, UserName, StringComparison.OrdinalIgnoreCase)
+ }).ToList();
+ }
+ catch(RpcException ex) {
+ throw CloudError(ex);
+ }
+ }
+
+ public bool CreateUser(DbUserInfo user, string password) {
+ try {
+ var response = userClient.CreateUser(ToCloudUser(user), password);
+ if(!response.Success)
+ throw new InvalidOperationException(response.Message);
+ return true;
+ }
+ catch(RpcException ex) {
+ throw CloudError(ex);
+ }
+ }
+
+ public bool UpdateUser(DbUserInfo user, string newPassword = null) {
+ try {
+ var response = userClient.UpdateUser(ToCloudUser(user), newPassword);
+ if(!response.Success)
+ throw new InvalidOperationException(response.Message);
+ return true;
+ }
+ catch(RpcException ex) {
+ throw CloudError(ex);
+ }
+ }
+
+ public bool DeleteUser(string login) {
+ try {
+ var response = userClient.DeleteUser(login);
+ if(!response.Success)
+ throw new InvalidOperationException(response.Message);
+ return true;
+ }
+ catch(RpcException ex) {
+ throw CloudError(ex);
+ }
}
+
+ public List GetUserBaseAccess(string login, IApplicationInfo applicationInfo) {
+ try {
+ return userClient.GetUserBaseAccess(login, applicationInfo.ProductCode).Select(b => new DbUserBaseAccess {
+ BaseId = b.BaseId,
+ Title = b.BaseTitle,
+ HasAccess = b.HasAccess,
+ IsAdmin = b.Admin,
+ ReadOnly = b.ReadOnly
+ }).ToList();
+ }
+ catch(RpcException ex) {
+ throw CloudError(ex);
+ }
+ }
+
+ public bool SetUserBaseAccess(string login, DbUserBaseAccess access) {
+ try {
+ bool ok = userClient.ChangeBaseAccess(login, access.BaseId, access.HasAccess, access.IsAdmin, access.ReadOnly);
+ if(!ok)
+ throw new InvalidOperationException("Не удалось изменить доступ к базе");
+ return true;
+ }
+ catch(RpcException ex) {
+ throw CloudError(ex);
+ }
+ }
+
+ private static QS.Cloud.Core.UserInfo ToCloudUser(DbUserInfo user) => new QS.Cloud.Core.UserInfo {
+ Login = user.Login ?? "",
+ Name = user.Name ?? "",
+ Email = user.Email ?? "",
+ Phone = user.Phone ?? "",
+ Post = user.Post ?? "",
+ Comment = user.Comment ?? "",
+ Disabled = user.Disabled,
+ IsAccountAdmin = user.IsAdmin
+ };
+
+ private static Exception CloudError(RpcException ex) =>
+ new InvalidOperationException(string.IsNullOrEmpty(ex.Status.Detail) ? ex.Message : ex.Status.Detail);
+
+ #endregion
public bool CreateDatabase(DbCreationRequest request) {
if(request == null)
@@ -102,6 +209,8 @@ public bool ImportDatabase(DbImportRequest request) {
public void Dispose()
{
loginClient.Dispose();
+ dbClient.Dispose();
+ userClient.Dispose();
}
public bool DropDatabase(DbInfo database)
diff --git a/QS.Cloud.Client/Protos/UserManagement.proto b/QS.Cloud.Client/Protos/UserManagement.proto
index 3d002dcaf..8b4722657 100644
--- a/QS.Cloud.Client/Protos/UserManagement.proto
+++ b/QS.Cloud.Client/Protos/UserManagement.proto
@@ -2,23 +2,56 @@ syntax = "proto3";
package QS.Cloud.Core;
service UserManagement{
+ rpc GetUsers (GetUsersRequest) returns (GetUsersResponse);
rpc CreateUser (CreateUserRequest) returns (CreateUserResponse);
rpc UpdateUser (UpdateUserRequest) returns (UpdateUserResponse);
rpc DeleteUser (DeleteUserRequest) returns (DeleteUserResponse);
+ rpc GetUserBaseAccess (GetUserBaseAccessRequest) returns (GetUserBaseAccessResponse);
rpc ChangeBaseAccess (ChangeBaseAccessRequest) returns (ChangeBaseAccessResponse);
}
+message UserInfo{
+ string login = 1;
+ string name = 2;
+ string email = 3;
+ string phone = 4;
+ string post = 5;
+ string comment = 6;
+ bool disabled = 7;
+ bool is_account_admin = 8;
+}
+
+message GetUsersRequest{
+}
+message GetUsersResponse{
+ repeated UserInfo users = 1;
+}
+
message CreateUserRequest{
string login = 1;
string name = 2;
string email = 3;
string password = 4;
+ string phone = 5;
+ string post = 6;
+ string comment = 7;
+ bool is_account_admin = 8;
}
message CreateUserResponse{
bool success = 1;
string message = 2;
}
+
message UpdateUserRequest{
+ string login = 1;
+ string name = 2;
+ string email = 3;
+ string phone = 4;
+ string post = 5;
+ string comment = 6;
+ bool disabled = 7;
+ bool is_account_admin = 8;
+ string new_password = 9;
}
message UpdateUserResponse{
bool success = 1;
@@ -28,18 +61,32 @@ message UpdateUserResponse{
message DeleteUserRequest{
string user = 1;
}
-
message DeleteUserResponse {
bool success = 1;
string message = 2;
}
+message GetUserBaseAccessRequest{
+ string user = 1;
+ uint32 product_id = 2;
+}
+message BaseAccessInfo{
+ int32 base_id = 1;
+ string base_title = 2;
+ bool has_access = 3;
+ bool admin = 4;
+ bool read_only = 5;
+}
+message GetUserBaseAccessResponse{
+ repeated BaseAccessInfo bases = 1;
+}
+
message ChangeBaseAccessRequest{
string user = 1;
int32 base_id = 2;
bool grant = 3;
bool admin = 4;
-
+ bool read_only = 5;
}
message ChangeBaseAccessResponse {
bool success = 1;
diff --git a/QS.DbManagement/DbCapabilities.cs b/QS.DbManagement/DbCapabilities.cs
index 74abefa9d..418cf1b23 100644
--- a/QS.DbManagement/DbCapabilities.cs
+++ b/QS.DbManagement/DbCapabilities.cs
@@ -37,5 +37,13 @@ public bool CanBackup(IDbProvider provider) {
public bool CanDrop(IDbProvider provider) {
return provider?.CanDropDatabase == true;
}
+
+ public bool CanChangeOwnPassword(IDbProvider provider) {
+ return provider != null;
+ }
+
+ public bool CanManageUsers(IDbProvider provider) {
+ return provider?.CanManageUsers == true;
+ }
}
}
diff --git a/QS.DbManagement/Entities/DbUserBaseAccess.cs b/QS.DbManagement/Entities/DbUserBaseAccess.cs
new file mode 100644
index 000000000..e113b4c1c
--- /dev/null
+++ b/QS.DbManagement/Entities/DbUserBaseAccess.cs
@@ -0,0 +1,15 @@
+namespace QS.DbManagement.Entities {
+ public class DbUserBaseAccess {
+ public int BaseId { get; set; }
+
+ public string BaseName { get; set; }
+
+ public string Title { get; set; }
+
+ public bool HasAccess { get; set; }
+
+ public bool IsAdmin { get; set; }
+
+ public bool ReadOnly { get; set; }
+ }
+}
diff --git a/QS.DbManagement/Entities/DbUserInfo.cs b/QS.DbManagement/Entities/DbUserInfo.cs
new file mode 100644
index 000000000..fa47dfd1c
--- /dev/null
+++ b/QS.DbManagement/Entities/DbUserInfo.cs
@@ -0,0 +1,36 @@
+using System;
+
+namespace QS.DbManagement.Entities {
+ public class DbUserInfo {
+ public string Login { get; set; }
+ public string Name { get; set; }
+ public string Email { get; set; }
+ public string Phone { get; set; }
+ public string Post { get; set; }
+ public string Comment { get; set; }
+
+ /// не может входить
+ public bool Disabled { get; set; }
+
+ /// может управлять другими пользователями
+ public bool IsAdmin { get; set; }
+ /// текущий пользователь подключения
+ public bool IsCurrentUser { get; set; }
+ }
+
+ ///
+ ///
+ [Flags]
+ public enum DbUserFields {
+ None = 0,
+ Name = 1,
+ Email = 2,
+ Phone = 4,
+ Post = 8,
+ Comment = 16,
+ AdminFlag = 32,
+ /// Возможность отключать пользователя
+ Disabling = 64,
+ BaseReadOnly = 128
+ }
+}
diff --git a/QS.DbManagement/IDbProvider.cs b/QS.DbManagement/IDbProvider.cs
index 07453558c..5357e9940 100644
--- a/QS.DbManagement/IDbProvider.cs
+++ b/QS.DbManagement/IDbProvider.cs
@@ -11,7 +11,27 @@ public interface IDbProvider : IDisposable
{
string UserName { get; }
- bool ChangePassword(string username, string oldPassword, string newPassword);
+ #region Управление пользователями
+
+ bool ChangeOwnPassword(string newPassword);
+
+ bool CanManageUsers { get; }
+
+ DbUserFields SupportedUserFields { get; }
+
+ List GetUsers();
+
+ bool CreateUser(DbUserInfo user, string password);
+
+ bool UpdateUser(DbUserInfo user, string newPassword = null);
+
+ bool DeleteUser(string login);
+
+ List GetUserBaseAccess(string login, IApplicationInfo applicationInfo);
+
+ bool SetUserBaseAccess(string login, DbUserBaseAccess access);
+
+ #endregion
///
/// Создаёт базу и наполняет её скриптом создания
@@ -27,8 +47,6 @@ public interface IDbProvider : IDisposable
void BackupDatabase(DbInfo database, string filePath, IDbDumpService dumpService, IProgressBarDisplayable progress, CancellationToken cancellation);
- bool AddUser(string username, string password);
-
LoginToServerResponse LoginToServer();
List GetUserDatabases(IApplicationInfo applicationInfo);
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index d04790ab1..e9a49d14c 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -7,6 +7,8 @@
using System.Collections.Generic;
using System.Data;
using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
using System.Threading;
namespace QS.DbManagement
@@ -69,23 +71,18 @@ public MariaDBProvider(IList parameters, string passwo
public LoginToServerResponse LoginToServer() {
try {
- if(connection.State != ConnectionState.Open)
- connection.Open();
+ EnsureOpen();
var grants = connection.Query("SHOW GRANTS FOR CURRENT_USER").ToList();
- IsAdmin = grants.Any(g =>
- g.IndexOf("ALL PRIVILEGES ON *.*", StringComparison.OrdinalIgnoreCase) >= 0
- || g.IndexOf("GRANT OPTION", StringComparison.OrdinalIgnoreCase) >= 0
- || g.IndexOf("SUPER", StringComparison.OrdinalIgnoreCase) >= 0);
+ IsAdmin = HasGlobalAdminGrant(grants);
- CanCreateDatabase = IsAdmin || grants.Any(g =>
- g.IndexOf("ALL PRIVILEGES", StringComparison.OrdinalIgnoreCase) >= 0
- || g.IndexOf("CREATE", StringComparison.OrdinalIgnoreCase) >= 0);
+ var privileges = new HashSet(grants
+ .Where(g => GrantScope(g) != null)
+ .SelectMany(GrantPrivileges));
- CanDropDatabase = IsAdmin || grants.Any(g =>
- g.IndexOf("ALL PRIVILEGES", StringComparison.OrdinalIgnoreCase) >= 0
- || g.IndexOf("DROP", StringComparison.OrdinalIgnoreCase) >= 0);
+ CanCreateDatabase = IsAdmin || privileges.Contains("ALL PRIVILEGES") || privileges.Contains("CREATE");
+ CanDropDatabase = IsAdmin || privileges.Contains("ALL PRIVILEGES") || privileges.Contains("DROP");
return new LoginToServerResponse {
Success = true,
@@ -105,8 +102,7 @@ public LoginToServerResponse LoginToServer() {
public List GetUserDatabases(IApplicationInfo applicationInfo) {
var result = new List();
- if(connection.State != ConnectionState.Open)
- connection.Open();
+ EnsureOpen();
var databases = connection.Query("SHOW DATABASES").ToList();
byte expectedProductCode = applicationInfo.ProductCode;
@@ -167,17 +163,309 @@ public LoginToDatabaseResponse LoginToDatabase(DbInfo dbInfo) {
}
}
- public bool AddUser(string username, string password) {
- string sql = $"CREATE USER IF NOT EXISTS '{username}' IDENTIFIED BY '{password}'";
- return connection.Execute(sql) != 0;
+ #region Управление пользователями
+
+ public DbUserFields SupportedUserFields =>
+ DbUserFields.BaseReadOnly
+ | (CanManageUsers && SupportsAccountLock ? DbUserFields.Disabling : DbUserFields.None);
+
+ public bool CanManageUsers => IsAdmin;
+
+ private static readonly string[] SystemUsers = { "root", "mariadb.sys", "mysql", "PUBLIC" };
+
+ private readonly Dictionary> userHosts = new Dictionary>(StringComparer.Ordinal);
+
+ private bool? supportsAccountLock;
+ private bool SupportsAccountLock {
+ get {
+ if(supportsAccountLock == null) {
+ EnsureOpen();
+ supportsAccountLock = connection.ExecuteScalar(
+ "SELECT COUNT(*) FROM information_schema.COLUMNS " +
+ "WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user' AND COLUMN_NAME = 'account_locked'") > 0;
+ }
+ return supportsAccountLock.Value;
+ }
}
- public bool ChangePassword(string username, string oldPassword, string newPassword) {
- string sql = $"ALTER USER '{username}'@'%' IDENTIFIED BY '{newPassword}'";
- return connection.Execute(sql) != 0;
+ public bool ChangeOwnPassword(string newPassword)
+ {
+ if(string.IsNullOrEmpty(newPassword))
+ throw new ArgumentException("Пароль не может быть пустым", nameof(newPassword));
+ EnsureOpen();
+
+ connection.Execute($"ALTER USER CURRENT_USER() IDENTIFIED BY '{EscapeString(newPassword)}'");
+ return true;
+ }
+
+ public List GetUsers()
+ {
+ EnsureOpen();
+
+ string lockedColumn = SupportsAccountLock ? "account_locked" : "NULL";
+ var rows = connection.Query(
+ $"SELECT User AS Login, Host, {lockedColumn} AS AccountLocked FROM mysql.user ORDER BY User, Host").ToList();
+
+ userHosts.Clear();
+ var result = new List();
+ foreach(var userRows in rows
+ .Where(r => !string.IsNullOrEmpty(r.Login)
+ && !r.Login.StartsWith("mysql.", StringComparison.OrdinalIgnoreCase)
+ && !SystemUsers.Contains(r.Login, StringComparer.OrdinalIgnoreCase))
+ .GroupBy(r => r.Login, StringComparer.Ordinal)) {
+
+ userHosts[userRows.Key] = userRows.Select(r => string.IsNullOrEmpty(r.Host) ? "%" : r.Host).ToList();
+ result.Add(new DbUserInfo {
+ Login = userRows.Key,
+ // отключён, только если заблокированы все хосты логина
+ Disabled = userRows.All(r => string.Equals(r.AccountLocked, "Y", StringComparison.OrdinalIgnoreCase)),
+ IsCurrentUser = string.Equals(userRows.Key, UserName, StringComparison.OrdinalIgnoreCase)
+ });
+ }
+ return result;
+ }
+
+ public bool CreateUser(DbUserInfo user, string password)
+ {
+ ValidateLogin(user?.Login);
+ if(string.IsNullOrEmpty(password))
+ throw new ArgumentException("Пароль не может быть пустым", nameof(password));
+ EnsureOpen();
+
+ string lockOption = user.Disabled && SupportsAccountLock ? " ACCOUNT LOCK" : string.Empty;
+ connection.Execute($"CREATE USER '{EscapeString(user.Login)}'@'%' IDENTIFIED BY '{EscapeString(password)}'{lockOption}");
+ userHosts[user.Login] = new List { "%" };
+ return true;
+ }
+
+ public bool UpdateUser(DbUserInfo user, string newPassword = null)
+ {
+ ValidateLogin(user?.Login);
+ EnsureOpen();
+
+ var options = new List();
+ if(!string.IsNullOrEmpty(newPassword))
+ options.Add($"IDENTIFIED BY '{EscapeString(newPassword)}'");
+ if(SupportsAccountLock)
+ options.Add(user.Disabled ? "ACCOUNT LOCK" : "ACCOUNT UNLOCK");
+ if(options.Count == 0)
+ return true;
+
+ foreach(var host in HostsOf(user.Login))
+ connection.Execute($"ALTER USER '{EscapeString(user.Login)}'@'{EscapeString(host)}' {string.Join(" ", options)}");
+ return true;
+ }
+
+ public bool DeleteUser(string login) {
+ ValidateLogin(login);
+ EnsureOpen();
+
+ foreach(var host in HostsOf(login))
+ connection.Execute($"DROP USER IF EXISTS '{EscapeString(login)}'@'{EscapeString(host)}'");
+ userHosts.Remove(login);
+ return true;
+ }
+
+ public List GetUserBaseAccess(string login, IApplicationInfo applicationInfo) {
+ EnsureOpen();
+
+ var databases = GetUserDatabases(applicationInfo);
+ // объединяем гранты всех хостов логина
+ var grants = new List();
+ foreach(var host in HostsOf(login)) {
+ try {
+ grants.AddRange(connection.Query($"SHOW GRANTS FOR '{EscapeString(login)}'@'{EscapeString(host)}'"));
+ }
+ catch(MySqlException ex) {
+ logger.Debug(ex, "Не удалось получить гранты пользователя {0}@{1}", login, host);
+ }
+ }
+
+ bool globalAdmin = HasGlobalAdminGrant(grants);
+
+ return databases.Select(db => {
+ var access = new DbUserBaseAccess { BaseName = db.BaseName, Title = db.Title };
+ if(globalAdmin) {
+ access.HasAccess = true;
+ access.IsAdmin = true;
+ return access;
+ }
+
+ var privileges = grants
+ .Where(g => {
+ var scope = GrantScope(g);
+ if(scope == null)
+ return false;
+ // шаблонные гранты вида `prefix\_%` не разворачиваем - учитываются только *.* и точное имя базы
+ return scope == "*" || string.Equals(UnescapeGrantPattern(scope), db.BaseName, StringComparison.OrdinalIgnoreCase);
+ })
+ .SelectMany(GrantPrivileges)
+ .Where(p => p != "USAGE")
+ .ToList();
+
+ if(privileges.Count == 0)
+ return access;
+
+ access.HasAccess = true;
+ if(privileges.Contains("ALL PRIVILEGES"))
+ access.IsAdmin = true;
+ else if(privileges.All(p => p == "SELECT" || p == "LOCK TABLES" || p == "SHOW VIEW"))
+ access.ReadOnly = true;
+ return access;
+ }).ToList();
+ }
+
+ public bool SetUserBaseAccess(string login, DbUserBaseAccess access) {
+ ValidateLogin(login);
+ if(string.IsNullOrWhiteSpace(access?.BaseName))
+ throw new ArgumentException("Не указано имя базы", nameof(access));
+ EnsureOpen();
+
+ var grantsByHost = new Dictionary>();
+ foreach(var host in HostsOf(login)) {
+ try {
+ grantsByHost[host] = connection.Query($"SHOW GRANTS FOR '{EscapeString(login)}'@'{EscapeString(host)}'").ToList();
+ }
+ catch(MySqlException ex) {
+ logger.Debug(ex, "Не удалось получить гранты {0}@{1}", login, host);
+ }
+ }
+ if(grantsByHost.Count == 0)
+ throw new InvalidOperationException($"Пользователь {login} не найден на сервере.");
+
+ if(HasGlobalAdminGrant(grantsByHost.Values.SelectMany(g => g)))
+ throw new InvalidOperationException(
+ $"У пользователя {login} глобальные права на весь сервер, доступ к отдельным базам для него не настраивается.");
+
+ foreach(var hostGrants in grantsByHost) {
+ string user = $"'{EscapeString(login)}'@'{EscapeString(hostGrants.Key)}'";
+
+ // отзываем прежние права ровно по тем шаблонам, по которым они были выданы,
+ foreach(var grant in hostGrants.Value) {
+ string scope = GrantScope(grant);
+ if(scope == null || scope == "*"
+ || !string.Equals(UnescapeGrantPattern(scope), access.BaseName, StringComparison.OrdinalIgnoreCase))
+ continue;
+ string pattern = $"`{EscapeIdentifier(scope)}`.*";
+ if(GrantPrivileges(grant).Any(p => p != "USAGE"))
+ connection.Execute($"REVOKE ALL PRIVILEGES ON {pattern} FROM {user}");
+ // ALL PRIVILEGES не включает право раздачи грантов - его отзываем отдельно
+ if(grant.IndexOf("WITH GRANT OPTION", StringComparison.OrdinalIgnoreCase) >= 0)
+ connection.Execute($"REVOKE GRANT OPTION ON {pattern} FROM {user}");
+ }
+
+ if(access.HasAccess) {
+ string privileges;
+ if(access.IsAdmin)
+ privileges = "ALL PRIVILEGES";
+ else if(access.ReadOnly)
+ privileges = "SELECT, LOCK TABLES, SHOW VIEW";
+ else
+ privileges = "SELECT, INSERT, UPDATE, DELETE, EXECUTE, CREATE TEMPORARY TABLES, LOCK TABLES, SHOW VIEW";
+ connection.Execute($"GRANT {privileges} ON `{EscapeGrantPattern(access.BaseName)}`.* TO {user}");
+ }
+ }
+ return true;
+ }
+
+ private class MySqlUserRow {
+ public string Login { get; set; }
+ public string Host { get; set; }
+ public string AccountLocked { get; set; }
+ }
+
+ private IReadOnlyList HostsOf(string login) =>
+ userHosts.TryGetValue(login, out var hosts) && hosts.Count > 0
+ ? (IReadOnlyList)hosts
+ : new[] { "%" };
+
+ private static void ValidateLogin(string login) {
+ if(string.IsNullOrWhiteSpace(login))
+ throw new ArgumentException("Логин пользователя не может быть пустым");
+ if(login.Length > 80) // ограничение MariaDB, в MySQL строже (32) - это проверит сам сервер
+ throw new ArgumentException("Логин пользователя длиннее 80 символов");
}
- public bool CreateDatabase(DbCreationRequest request) {
+ private static bool HasGlobalAdminGrant(IEnumerable grants) =>
+ grants.Any(g => {
+ if(GrantScope(g) != "*")
+ return false;
+ var privileges = GrantPrivileges(g).ToList();
+ return privileges.Contains("ALL PRIVILEGES")
+ || privileges.Contains("SUPER")
+ || privileges.Contains("CREATE USER");
+ });
+
+ private static string EscapeString(string value) =>
+ value == null ? string.Empty : value.Replace("\\", "\\\\").Replace("'", "\\'");
+
+ private static string EscapeIdentifier(string value) =>
+ value == null ? string.Empty : value.Replace("`", "``");
+
+ private static string EscapeGrantPattern(string dbName) =>
+ EscapeIdentifier(dbName).Replace("_", "\\_").Replace("%", "\\%");
+
+ private static string UnescapeGrantPattern(string pattern) =>
+ pattern.Replace("\\_", "_").Replace("\\%", "%");
+
+ private static string GrantScope(string grant) {
+ int onIdx = grant.IndexOf(" ON ", StringComparison.OrdinalIgnoreCase);
+ if(onIdx < 0)
+ return null;
+ string rest = grant.Substring(onIdx + 4).TrimStart(); //4 = " ON "
+
+ string scope;
+ int pos;
+ if(rest.StartsWith("`", StringComparison.Ordinal)) {
+ var name = new StringBuilder();
+ pos = 1;
+ while(pos < rest.Length) {
+ if(rest[pos] == '`') {
+ if(pos + 1 < rest.Length && rest[pos + 1] == '`') {
+ name.Append('`');
+ pos += 2;
+ continue;
+ }
+ pos++;
+ break;
+ }
+ name.Append(rest[pos]);
+ pos++;
+ }
+ scope = name.ToString();
+ }
+ else {
+ pos = rest.IndexOf('.');
+ if(pos < 0)
+ return null;
+ scope = rest.Substring(0, pos).Trim();
+ }
+
+ if(pos + 1 >= rest.Length || rest[pos] != '.' || rest[pos + 1] != '*')
+ return null;
+ return scope;
+ }
+
+ private static IEnumerable GrantPrivileges(string grant) {
+ int grantIdx = grant.IndexOf("GRANT ", StringComparison.OrdinalIgnoreCase);
+ int onIdx = grant.IndexOf(" ON ", StringComparison.OrdinalIgnoreCase);
+ if(grantIdx < 0 || onIdx < 0 || onIdx <= grantIdx)
+ return Enumerable.Empty();
+ int start = grantIdx + 6; //6 = "GRANT "
+ string privsPart = grant.Substring(start, onIdx - start);
+ // списки колонок "SELECT (col1, col2)" выкидываем - запятые внутри скобок не разделители привилегий
+ privsPart = Regex.Replace(privsPart, @"\([^)]*\)", string.Empty);
+ return privsPart.Split(',')
+ .Select(p => p.Trim().ToUpperInvariant())
+ .Where(p => p.Length > 0);
+ }
+
+ #endregion
+
+ public bool CreateDatabase(DbCreationRequest request)
+ {
+ EnsureOpen();
+
if(request == null)
throw new ArgumentNullException(nameof(request));
connection.Execute($"CREATE DATABASE IF NOT EXISTS `{request.DbName}`");
@@ -195,7 +483,10 @@ public bool CreateDatabase(DbCreationRequest request) {
/// Создание базы с наполнением из пользовательского дампа
/// Метод блокирующий - вызывать из фонового потока
///
- public bool ImportDatabase(DbImportRequest request) {
+ public bool ImportDatabase(DbImportRequest request)
+ {
+ EnsureOpen();
+
if(request == null)
throw new ArgumentNullException(nameof(request));
connection.Execute($"CREATE DATABASE IF NOT EXISTS `{request.DbName}`");
@@ -207,7 +498,10 @@ public bool ImportDatabase(DbImportRequest request) {
return true;
}
- public bool DropDatabase(DbInfo database) {
+ public bool DropDatabase(DbInfo database)
+ {
+ EnsureOpen();
+
string sql = $"DROP DATABASE IF EXISTS `{database.BaseName}`";
return connection.Execute(sql) != 0;
}
@@ -220,6 +514,12 @@ public void BackupDatabase(DbInfo database, string filePath, IDbDumpService dump
dumpService.Export(ConnectionStringBuilder.ConnectionString, database.BaseName, filePath, progress, cancellation);
}
+
+ private void EnsureOpen() {
+ if(connection.State != ConnectionState.Open)
+ connection.Open();
+ }
+
public void Dispose() {
connection?.Dispose();
}
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml
index 3c0dc1b1a..6f9ff3f53 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml
@@ -48,20 +48,22 @@
-
+
-
+
-
+
-
+
+
-
+
@@ -80,7 +82,7 @@
-
+
@@ -99,7 +101,7 @@
-
+
-
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml.cs
index e54aea154..466510404 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml.cs
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/CreateDataBaseProgressView.axaml.cs
@@ -13,8 +13,6 @@ public CreateDataBaseProgressView(CreateDataBaseProgressVM progressVM) {
}
private void OnLoaded(object? sender, RoutedEventArgs e) {
- cogwheel.Classes.Add("rolled");
-
if(DataContext is CreateDataBaseProgressVM vm)
vm.StartCommand.Execute().Subscribe();
}
From f8c8db027609821311be31c83a955f6d3ee9713e Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Thu, 9 Jul 2026 17:25:51 +0300
Subject: [PATCH 040/135] =?UTF-8?q?=D0=BE=D0=B3=D1=80=D0=B0=D0=BD=D0=B8?=
=?UTF-8?q?=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=82=D0=BE=D1=87=D0=B5=D1=87?=
=?UTF-8?q?=D0=BD=D0=BE=D0=B9=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9?=
=?UTF-8?q?=D0=BA=D0=B8=20=D0=B4=D0=BE=D1=81=D1=82=D1=83=D0=BF=D0=B0=20?=
=?UTF-8?q?=D0=BA=20=D0=B1=D0=B0=D0=B7=D0=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
QS.DbManagement/Entities/DbUserBaseAccess.cs | 1 +
QS.DbManagement/MariaDb/MariaDBProvider.cs | 3 +++
.../Views/Pages/UserManagementView.axaml | 10 +++++++---
.../ViewModels/PageViewModels/BaseAccessRowVM.cs | 3 +++
.../ViewModels/PageViewModels/UserManagementVM.cs | 9 +++++++--
5 files changed, 21 insertions(+), 5 deletions(-)
diff --git a/QS.DbManagement/Entities/DbUserBaseAccess.cs b/QS.DbManagement/Entities/DbUserBaseAccess.cs
index a980d6810..ff7674f16 100644
--- a/QS.DbManagement/Entities/DbUserBaseAccess.cs
+++ b/QS.DbManagement/Entities/DbUserBaseAccess.cs
@@ -13,5 +13,6 @@ public class DbUserBaseAccess {
public bool IsAdmin { get; set; }
public bool ReadOnly { get; set; }
+ public bool CanEdit { get; set; } = true;
}
}
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index 253a02d73..550eef27f 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -329,8 +329,11 @@ public List GetUserBaseAccess(string login, IApplicationInfo a
return databases.Select(db => {
var access = new DbUserBaseAccess { BaseName = db.BaseName, Title = db.Title };
if(globalAdmin) {
+ // доступ следует из грантов на *.* - аддитивная модель прав не позволяет
+ // сузить его точечным REVOKE, поэтому строки не редактируются
access.HasAccess = true;
access.IsAdmin = true;
+ access.CanEdit = false;
return access;
}
diff --git a/QS.Launcher.Avalonia/Views/Pages/UserManagementView.axaml b/QS.Launcher.Avalonia/Views/Pages/UserManagementView.axaml
index fa62ad7ee..c8e8c9adb 100644
--- a/QS.Launcher.Avalonia/Views/Pages/UserManagementView.axaml
+++ b/QS.Launcher.Avalonia/Views/Pages/UserManagementView.axaml
@@ -108,16 +108,20 @@
+ IsChecked="{Binding HasAccess}" IsEnabled="{Binding CanEdit}" />
+ IsChecked="{Binding IsAdmin}" IsEnabled="{Binding CanEdit}" />
+ IsChecked="{Binding ReadOnly}" IsVisible="{Binding ShowReadOnly}" IsEnabled="{Binding CanEdit}" />
+
+
x.SelectedUser).Select(u => u != null);
- var canSaveAccess = this.WhenAnyValue(x => x.SelectedUser, x => x.CanManageBaseAccess,
- (user, canManage) => user != null && canManage);
+ var canSaveAccess = this.WhenAnyValue(x => x.SelectedUser, x => x.CanManageBaseAccess, x => x.BaseAccessLocked,
+ (user, canManage, locked) => user != null && canManage && !locked);
NewUserCommand = ReactiveCommand.Create(StartNewUser);
DeleteUserCommand = ReactiveCommand.CreateFromTask(DeleteUserAsync, hasSelectedUser);
SaveAccessCommand = ReactiveCommand.CreateFromTask(SaveAccessAsync, canSaveAccess);
@@ -175,6 +175,7 @@ public void RefreshUsers() {
private void OnSelectedUserChanged() {
this.RaisePropertyChanged(nameof(HasSelectedUser));
BaseAccesses.Clear();
+ this.RaisePropertyChanged(nameof(BaseAccessLocked));
if(SelectedUser == null) {
IsNewUser = false;
return;
@@ -341,6 +342,9 @@ private void ClearEditBuffer() {
public ObservableCollection BaseAccesses { get; }
+ // Доступ пользователя следует из глобальных прав на сервер и точечно не настраивается
+ public bool BaseAccessLocked => BaseAccesses.Count > 0 && BaseAccesses.All(r => !r.CanEdit);
+
private void LoadBaseAccess(string login) {
if(applicationInfo == null)
return;
@@ -353,6 +357,7 @@ private void LoadBaseAccess(string login) {
logger.Error(ex, "Не удалось получить доступы пользователя {0}", login);
interactiveMessage.ShowMessage(ImportanceLevel.Error, ex.Message, "Доступ к базам");
}
+ this.RaisePropertyChanged(nameof(BaseAccessLocked));
}
private async Task SaveAccessAsync() {
From d2fae6d0402a9c1d531f01714146d26709b886e8 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Sat, 11 Jul 2026 11:37:04 +0300
Subject: [PATCH 041/135] =?UTF-8?q?=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE?=
=?UTF-8?q?=D1=82=D1=87=D0=B8=D0=BA=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA?=
=?UTF-8?q?=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20AvaloniaInteractiveMessage?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
QS.Launcher.Avalonia/LauncherApp.axaml.cs | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/QS.Launcher.Avalonia/LauncherApp.axaml.cs b/QS.Launcher.Avalonia/LauncherApp.axaml.cs
index 1a7d531c5..56247cc13 100644
--- a/QS.Launcher.Avalonia/LauncherApp.axaml.cs
+++ b/QS.Launcher.Avalonia/LauncherApp.axaml.cs
@@ -1,13 +1,18 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
+using QS.Dialog;
using QS.Launcher.Views;
+using ReactiveUI;
using System;
+using System.Reactive;
namespace QS.Launcher;
public partial class LauncherApp() : Application
{
+ private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
+
public Func MainWindowGetter { get; set; }
public override void Initialize() {
@@ -15,6 +20,11 @@ public override void Initialize() {
}
public override void OnFrameworkInitializationCompleted() {
+ RxApp.DefaultExceptionHandler = Observer.Create(ex => {
+ logger.Error(ex, "Необработанная ошибка в ReactiveUI-команде.");
+ new AvaloniaInteractiveMessage().ShowMessage(ImportanceLevel.Error, ex.Message, "Непредвиденная ошибка");
+ });
+
if (MainWindowGetter is null)
throw new ArgumentNullException(nameof(MainWindowGetter));
From 5fb30c4f64adf8388bc755c12e70821775a196a4 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Sat, 11 Jul 2026 12:47:01 +0300
Subject: [PATCH 042/135] =?UTF-8?q?=D1=84=D0=BB=D0=B0=D0=B3=20=D0=B3=D0=BB?=
=?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=B0?=
=?UTF-8?q?=D0=B4=D0=BC=D0=B8=D0=BD=D0=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
QS.DbManagement/MariaDb/MariaDBProvider.cs | 45 +++++++++++++++++-----
1 file changed, 36 insertions(+), 9 deletions(-)
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index 550eef27f..2af9314ee 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -221,10 +221,13 @@ public void BackupDatabase(DbInfo database, string filePath, IDbDumpService dump
public DbUserFields SupportedUserFields =>
DbUserFields.BaseReadOnly
- | (CanManageUsers && SupportsAccountLock ? DbUserFields.Disabling : DbUserFields.None);
+ | (CanManageUsers && SupportsAccountLock ? DbUserFields.Disabling : DbUserFields.None)
+ | (SupportsAdminFlag ? DbUserFields.AdminFlag : DbUserFields.None);
public bool CanManageUsers => IsAdmin;
+ private bool SupportsAdminFlag => CanManageUsers && CanManageBaseAccess;
+
private static readonly string[] SystemUsers = { "root", "mariadb.sys", "mysql", "PUBLIC" };
private readonly Dictionary> userHosts = new Dictionary>(StringComparer.Ordinal);
@@ -255,8 +258,11 @@ public List GetUsers() {
EnsureOpen();
string lockedColumn = SupportsAccountLock ? "account_locked" : "NULL";
+
var rows = connection.Query(
- $"SELECT User AS Login, Host, {lockedColumn} AS AccountLocked FROM mysql.user ORDER BY User, Host").ToList();
+ $"SELECT User AS Login, Host, {lockedColumn} AS AccountLocked, " +
+ "Super_priv AS SuperPriv, Create_user_priv AS CreateUserPriv " +
+ "FROM mysql.user ORDER BY User, Host").ToList();
userHosts.Clear();
var result = new List();
@@ -271,6 +277,9 @@ public List GetUsers() {
Login = userRows.Key,
// отключён, только если заблокированы все хосты логина
Disabled = userRows.All(r => string.Equals(r.AccountLocked, "Y", StringComparison.OrdinalIgnoreCase)),
+ // админ, если хоть на одном хосте есть SUPER или CREATE USER
+ IsAdmin = userRows.Any(r => string.Equals(r.SuperPriv, "Y", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(r.CreateUserPriv, "Y", StringComparison.OrdinalIgnoreCase)),
IsCurrentUser = string.Equals(userRows.Key, UserName, StringComparison.OrdinalIgnoreCase)
});
}
@@ -284,7 +293,14 @@ public bool CreateUser(DbUserInfo user, string password) {
EnsureOpen();
string lockOption = user.Disabled && SupportsAccountLock ? " ACCOUNT LOCK" : string.Empty;
- connection.Execute($"CREATE USER '{EscapeString(user.Login)}'@'%' IDENTIFIED BY '{EscapeString(password)}'{lockOption}");
+ string account = $"'{EscapeString(user.Login)}'@'%'";
+ var statements = new List {
+ $"CREATE USER {account} IDENTIFIED BY '{EscapeString(password)}'{lockOption}"
+ };
+ if(SupportsAdminFlag && user.IsAdmin)
+ statements.Add($"GRANT ALL PRIVILEGES ON *.* TO {account} WITH GRANT OPTION");
+
+ connection.Execute(string.Join(";", statements));
userHosts[user.Login] = new List { "%" };
return true;
}
@@ -298,13 +314,22 @@ public bool UpdateUser(DbUserInfo user, string newPassword = null) {
options.Add($"IDENTIFIED BY '{EscapeString(newPassword)}'");
if(SupportsAccountLock)
options.Add(user.Disabled ? "ACCOUNT LOCK" : "ACCOUNT UNLOCK");
- if(options.Count == 0)
- return true;
-
- // одним батчем по всем хостам логина - меньше сетевых обращений (роллбека тут всё равно нет: DDL по учёткам самокоммитится)
string suffix = string.Join(" ", options);
- connection.Execute(string.Join(";", HostsOf(user.Login)
- .Select(host => $"ALTER USER '{EscapeString(user.Login)}'@'{EscapeString(host)}' {suffix}")));
+
+ // одним батчем по всем хостам логина
+ var statements = new List();
+ foreach(var host in HostsOf(user.Login)) {
+ string account = $"'{EscapeString(user.Login)}'@'{EscapeString(host)}'";
+ if(options.Count > 0)
+ statements.Add($"ALTER USER {account} {suffix}");
+ if(SupportsAdminFlag)
+ statements.Add(user.IsAdmin
+ ? $"GRANT ALL PRIVILEGES ON *.* TO {account} WITH GRANT OPTION"
+ : $"REVOKE ALL PRIVILEGES, GRANT OPTION ON *.* FROM {account}");
+ }
+ if(statements.Count == 0)
+ return true;
+ connection.Execute(string.Join(";", statements));
return true;
}
@@ -432,6 +457,8 @@ private class MySqlUserRow {
public string Login { get; set; }
public string Host { get; set; }
public string AccountLocked { get; set; }
+ public string SuperPriv { get; set; }
+ public string CreateUserPriv { get; set; }
}
private IReadOnlyList HostsOf(string login) =>
From f3ee47e0604d7ca8aa866dbff292f28ad9b92693 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Sat, 11 Jul 2026 14:25:43 +0300
Subject: [PATCH 043/135] =?UTF-8?q?=D0=B3=D0=BB=D0=BE=D0=B1=D0=B0=D0=BB?=
=?UTF-8?q?=D1=8C=D0=BD=D0=BE=D0=B5=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?=
=?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B2=20users=20=D0=B8=D0=BD=D1=84?=
=?UTF-8?q?=D0=BE=D1=80=D0=BC=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BE=20=D0=BF?=
=?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB?=
=?UTF-8?q?=D0=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
QS.DbManagement/Entities/DbUserBaseAccess.cs | 2 +
QS.DbManagement/MariaDb/MariaDBProvider.cs | 50 ++++++++++++++++++-
.../PageViewModels/UserManagementVM.cs | 19 ++++++-
3 files changed, 68 insertions(+), 3 deletions(-)
diff --git a/QS.DbManagement/Entities/DbUserBaseAccess.cs b/QS.DbManagement/Entities/DbUserBaseAccess.cs
index ff7674f16..5c6d21c63 100644
--- a/QS.DbManagement/Entities/DbUserBaseAccess.cs
+++ b/QS.DbManagement/Entities/DbUserBaseAccess.cs
@@ -14,5 +14,7 @@ public class DbUserBaseAccess {
public bool ReadOnly { get; set; }
public bool CanEdit { get; set; } = true;
+ public string Name { get; set; }
+ public string Email { get; set; }
}
}
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index 2af9314ee..075c658cb 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -221,6 +221,7 @@ public void BackupDatabase(DbInfo database, string filePath, IDbDumpService dump
public DbUserFields SupportedUserFields =>
DbUserFields.BaseReadOnly
+ | DbUserFields.Name | DbUserFields.Email
| (CanManageUsers && SupportsAccountLock ? DbUserFields.Disabling : DbUserFields.None)
| (SupportsAdminFlag ? DbUserFields.AdminFlag : DbUserFields.None);
@@ -351,7 +352,7 @@ public List GetUserBaseAccess(string login, IApplicationInfo a
bool globalAdmin = HasGlobalAdminGrant(grants);
- return databases.Select(db => {
+ var result = databases.Select(db => {
var access = new DbUserBaseAccess { BaseName = db.BaseName, Title = db.Title };
if(globalAdmin) {
// доступ следует из грантов на *.* - аддитивная модель прав не позволяет
@@ -384,6 +385,25 @@ public List GetUserBaseAccess(string login, IApplicationInfo a
access.ReadOnly = true;
return access;
}).ToList();
+
+ foreach(var access in result.Where(a => a.HasAccess))
+ FillUsersProfile(access, login);
+ return result;
+ }
+
+ private void FillUsersProfile(DbUserBaseAccess access, string login) {
+ try {
+ var row = connection.QueryFirstOrDefault(
+ $"SELECT name AS Name, email AS Email FROM `{EscapeIdentifier(access.BaseName)}`.users WHERE login = @login",
+ new { login });
+ if(row != null) {
+ access.Name = row.Name;
+ access.Email = row.Email;
+ }
+ }
+ catch(MySqlException ex) {
+ logger.Debug(ex, "Не удалось прочитать users в базе {0}", access.BaseName);
+ }
}
public bool SetUserBaseAccess(string login, DbUserBaseAccess access, IApplicationInfo applicationInfo) {
@@ -433,9 +453,37 @@ public bool SetUserBaseAccess(string login, DbUserBaseAccess access, IApplicatio
if(statements.Count > 0)
connection.Execute(string.Join(";", statements));
+
+ SyncUsersTable(login, access);
return true;
}
+ // если таблицы может не быть, тогда молча пропускаем; запись идемпотентна
+ private void SyncUsersTable(string login, DbUserBaseAccess access) {
+ bool tableExists = connection.ExecuteScalar(
+ "SELECT COUNT(*) > 0 FROM information_schema.tables WHERE table_schema = @db AND table_name = 'users'",
+ new { db = access.BaseName });
+ if(!tableExists)
+ return;
+
+ string table = $"`{EscapeIdentifier(access.BaseName)}`.users";
+
+ if(!access.HasAccess) {
+ connection.Execute($"UPDATE {table} SET deactivated = TRUE WHERE login = @login", new { login });
+ return;
+ }
+
+ var p = new { login, name = access.Name, email = access.Email, admin = access.IsAdmin };
+ var existingId = connection.QueryFirstOrDefault($"SELECT id FROM {table} WHERE login = @login", new { login });
+ if(existingId != null)
+ // пустые поля формы не затирают уже заполненное приложением значение (COALESCE/NULLIF)
+ connection.Execute($"UPDATE {table} SET name = COALESCE(NULLIF(@name, ''), name), " +
+ "email = COALESCE(NULLIF(@email, ''), email), admin = @admin, deactivated = FALSE WHERE login = @login", p);
+ else
+ connection.Execute($"INSERT INTO {table} (name, login, email, admin, deactivated) " +
+ "VALUES (COALESCE(NULLIF(@name, ''), @login), @login, NULLIF(@email, ''), @admin, FALSE)", p);
+ }
+
private Dictionary> ReadGrantsByHost(string login) {
var hosts = HostsOf(login).ToList();
var result = new Dictionary>(StringComparer.Ordinal);
diff --git a/QS.Launcher/ViewModels/PageViewModels/UserManagementVM.cs b/QS.Launcher/ViewModels/PageViewModels/UserManagementVM.cs
index 05e8056bf..9627fdea8 100644
--- a/QS.Launcher/ViewModels/PageViewModels/UserManagementVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/UserManagementVM.cs
@@ -352,6 +352,14 @@ private void LoadBaseAccess(string login) {
var rows = provider.GetUserBaseAccess(login, applicationInfo);
foreach(var row in rows)
BaseAccesses.Add(new BaseAccessRowVM(row, ShowReadOnly));
+
+ var profile = rows.FirstOrDefault(r => !string.IsNullOrEmpty(r.Name) || !string.IsNullOrEmpty(r.Email));
+ if(profile != null) {
+ if(string.IsNullOrEmpty(EditName))
+ EditName = profile.Name;
+ if(string.IsNullOrEmpty(EditEmail))
+ EditEmail = profile.Email;
+ }
}
catch(Exception ex) {
logger.Error(ex, "Не удалось получить доступы пользователя {0}", login);
@@ -369,9 +377,16 @@ private async Task SaveAccessAsync() {
if(changedRows.Count == 0)
return;
try {
+ string name = EditName;
+ string email = EditEmail;
await Task.Run(() => {
- foreach(var row in changedRows)
- provider.SetUserBaseAccess(user.Login, row.ToAccess(), applicationInfo);
+ foreach(var row in changedRows) {
+ var access = row.ToAccess();
+ // профиль пишется в таблицу users каждой базы, куда выдаём доступ
+ access.Name = name;
+ access.Email = email;
+ provider.SetUserBaseAccess(user.Login, access, applicationInfo);
+ }
});
foreach(var row in changedRows)
row.AcceptChanges();
From ef7c8b9d694a7a5ed1aa9cdcfc9e77c8a9e882d0 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Sun, 12 Jul 2026 20:07:51 +0300
Subject: [PATCH 044/135] =?UTF-8?q?=D0=BF=D1=80=D0=B8=D0=BA=D0=BB=D0=B0?=
=?UTF-8?q?=D0=B4=D0=BD=D1=8B=D0=B5=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B0=20?=
=?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D1=81=D0=B2=D0=BE=D0=B1=D0=BE=D0=B4=D0=BD?=
=?UTF-8?q?=D0=BE=D0=B3=D0=BE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
QS.DbManagement/Entities/DbUserBaseAccess.cs | 4 ++
QS.DbManagement/Entities/DbUserInfo.cs | 3 +-
QS.DbManagement/MariaDb/MariaDBProvider.cs | 37 ++++++++++++---
.../Views/Pages/UserManagementView.axaml | 32 ++++++++-----
.../PageViewModels/BaseAccessRowVM.cs | 45 +++++++++++++++++--
.../PageViewModels/UserManagementVM.cs | 4 +-
6 files changed, 104 insertions(+), 21 deletions(-)
diff --git a/QS.DbManagement/Entities/DbUserBaseAccess.cs b/QS.DbManagement/Entities/DbUserBaseAccess.cs
index 5c6d21c63..c5f653a72 100644
--- a/QS.DbManagement/Entities/DbUserBaseAccess.cs
+++ b/QS.DbManagement/Entities/DbUserBaseAccess.cs
@@ -16,5 +16,9 @@ public class DbUserBaseAccess {
public bool CanEdit { get; set; } = true;
public string Name { get; set; }
public string Email { get; set; }
+
+ public bool CanDelete { get; set; } = true;
+ public bool CanAccountingSettings { get; set; } = true;
+ public bool CanChangeDocumentDate { get; set; } = true;
}
}
diff --git a/QS.DbManagement/Entities/DbUserInfo.cs b/QS.DbManagement/Entities/DbUserInfo.cs
index 28767d3b6..4ce0f34f8 100644
--- a/QS.DbManagement/Entities/DbUserInfo.cs
+++ b/QS.DbManagement/Entities/DbUserInfo.cs
@@ -32,6 +32,7 @@ public enum DbUserFields {
AdminFlag = 32,
/// Возможность отключать пользователя
Disabling = 64,
- BaseReadOnly = 128
+ BaseReadOnly = 128,
+ BaseAppPermissions = 256
}
}
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index 075c658cb..8e37e0006 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -222,6 +222,7 @@ public void BackupDatabase(DbInfo database, string filePath, IDbDumpService dump
public DbUserFields SupportedUserFields =>
DbUserFields.BaseReadOnly
| DbUserFields.Name | DbUserFields.Email
+ | DbUserFields.BaseAppPermissions
| (CanManageUsers && SupportsAccountLock ? DbUserFields.Disabling : DbUserFields.None)
| (SupportsAdminFlag ? DbUserFields.AdminFlag : DbUserFields.None);
@@ -393,12 +394,17 @@ public List GetUserBaseAccess(string login, IApplicationInfo a
private void FillUsersProfile(DbUserBaseAccess access, string login) {
try {
- var row = connection.QueryFirstOrDefault(
- $"SELECT name AS Name, email AS Email FROM `{EscapeIdentifier(access.BaseName)}`.users WHERE login = @login",
+ var row = connection.QueryFirstOrDefault(
+ "SELECT name AS Name, email AS Email, can_delete AS CanDelete, " +
+ "can_accounting_settings AS CanAccountingSettings, can_change_document_date AS CanChangeDocumentDate " +
+ $"FROM `{EscapeIdentifier(access.BaseName)}`.users WHERE login = @login",
new { login });
if(row != null) {
access.Name = row.Name;
access.Email = row.Email;
+ access.CanDelete = row.CanDelete;
+ access.CanAccountingSettings = row.CanAccountingSettings;
+ access.CanChangeDocumentDate = row.CanChangeDocumentDate;
}
}
catch(MySqlException ex) {
@@ -406,6 +412,14 @@ private void FillUsersProfile(DbUserBaseAccess access, string login) {
}
}
+ private class UsersTableRow {
+ public string Name { get; set; }
+ public string Email { get; set; }
+ public bool CanDelete { get; set; }
+ public bool CanAccountingSettings { get; set; }
+ public bool CanChangeDocumentDate { get; set; }
+ }
+
public bool SetUserBaseAccess(string login, DbUserBaseAccess access, IApplicationInfo applicationInfo) {
ValidateLogin(login);
if(string.IsNullOrWhiteSpace(access?.BaseName))
@@ -473,15 +487,26 @@ private void SyncUsersTable(string login, DbUserBaseAccess access) {
return;
}
- var p = new { login, name = access.Name, email = access.Email, admin = access.IsAdmin };
+ var p = new {
+ login,
+ name = access.Name,
+ email = access.Email,
+ admin = access.IsAdmin,
+ canDelete = access.CanDelete,
+ canAccounting = access.CanAccountingSettings,
+ canDocDate = access.CanChangeDocumentDate
+ };
var existingId = connection.QueryFirstOrDefault($"SELECT id FROM {table} WHERE login = @login", new { login });
if(existingId != null)
// пустые поля формы не затирают уже заполненное приложением значение (COALESCE/NULLIF)
connection.Execute($"UPDATE {table} SET name = COALESCE(NULLIF(@name, ''), name), " +
- "email = COALESCE(NULLIF(@email, ''), email), admin = @admin, deactivated = FALSE WHERE login = @login", p);
+ "email = COALESCE(NULLIF(@email, ''), email), admin = @admin, " +
+ "can_delete = @canDelete, can_accounting_settings = @canAccounting, can_change_document_date = @canDocDate, " +
+ "deactivated = FALSE WHERE login = @login", p);
else
- connection.Execute($"INSERT INTO {table} (name, login, email, admin, deactivated) " +
- "VALUES (COALESCE(NULLIF(@name, ''), @login), @login, NULLIF(@email, ''), @admin, FALSE)", p);
+ connection.Execute($"INSERT INTO {table} " +
+ "(name, login, email, admin, can_delete, can_accounting_settings, can_change_document_date, deactivated) " +
+ "VALUES (COALESCE(NULLIF(@name, ''), @login), @login, NULLIF(@email, ''), @admin, @canDelete, @canAccounting, @canDocDate, FALSE)", p);
}
private Dictionary> ReadGrantsByHost(string login) {
diff --git a/QS.Launcher.Avalonia/Views/Pages/UserManagementView.axaml b/QS.Launcher.Avalonia/Views/Pages/UserManagementView.axaml
index c8e8c9adb..60b012bf5 100644
--- a/QS.Launcher.Avalonia/Views/Pages/UserManagementView.axaml
+++ b/QS.Launcher.Avalonia/Views/Pages/UserManagementView.axaml
@@ -104,16 +104,28 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/QS.Launcher/ViewModels/PageViewModels/BaseAccessRowVM.cs b/QS.Launcher/ViewModels/PageViewModels/BaseAccessRowVM.cs
index 6d93f4369..34e283146 100644
--- a/QS.Launcher/ViewModels/PageViewModels/BaseAccessRowVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/BaseAccessRowVM.cs
@@ -9,37 +9,55 @@ public class BaseAccessRowVM : ReactiveObject {
public bool ShowReadOnly { get; }
+ public bool ShowAppPermissions { get; }
+
public bool CanEdit { get; }
- public BaseAccessRowVM(DbUserBaseAccess access, bool showReadOnly) {
+ public BaseAccessRowVM(DbUserBaseAccess access, bool showReadOnly, bool showAppPermissions) {
BaseId = access.BaseId;
BaseName = access.BaseName;
Title = access.Title;
ShowReadOnly = showReadOnly;
+ ShowAppPermissions = showAppPermissions;
CanEdit = access.CanEdit;
hasAccess = originalHasAccess = access.HasAccess;
isAdmin = originalIsAdmin = access.IsAdmin;
readOnly = originalReadOnly = access.ReadOnly;
+ canDelete = originalCanDelete = access.CanDelete;
+ canAccountingSettings = originalCanAccountingSettings = access.CanAccountingSettings;
+ canChangeDocumentDate = originalCanChangeDocumentDate = access.CanChangeDocumentDate;
}
private bool originalHasAccess;
private bool originalIsAdmin;
private bool originalReadOnly;
+ private bool originalCanDelete;
+ private bool originalCanAccountingSettings;
+ private bool originalCanChangeDocumentDate;
public bool IsDirty =>
- HasAccess != originalHasAccess || IsAdmin != originalIsAdmin || ReadOnly != originalReadOnly;
+ HasAccess != originalHasAccess || IsAdmin != originalIsAdmin || ReadOnly != originalReadOnly
+ || CanDelete != originalCanDelete
+ || CanAccountingSettings != originalCanAccountingSettings
+ || CanChangeDocumentDate != originalCanChangeDocumentDate;
public void AcceptChanges() {
originalHasAccess = HasAccess;
originalIsAdmin = IsAdmin;
originalReadOnly = ReadOnly;
+ originalCanDelete = CanDelete;
+ originalCanAccountingSettings = CanAccountingSettings;
+ originalCanChangeDocumentDate = CanChangeDocumentDate;
}
+ public bool AppPermissionsVisible => ShowAppPermissions && HasAccess;
+
private bool hasAccess;
public bool HasAccess {
get => hasAccess;
set {
this.RaiseAndSetIfChanged(ref hasAccess, value);
+ this.RaisePropertyChanged(nameof(AppPermissionsVisible));
if(!value) {
IsAdmin = false;
ReadOnly = false;
@@ -71,13 +89,34 @@ public bool ReadOnly {
}
}
+ private bool canDelete;
+ public bool CanDelete {
+ get => canDelete;
+ set => this.RaiseAndSetIfChanged(ref canDelete, value);
+ }
+
+ private bool canAccountingSettings;
+ public bool CanAccountingSettings {
+ get => canAccountingSettings;
+ set => this.RaiseAndSetIfChanged(ref canAccountingSettings, value);
+ }
+
+ private bool canChangeDocumentDate;
+ public bool CanChangeDocumentDate {
+ get => canChangeDocumentDate;
+ set => this.RaiseAndSetIfChanged(ref canChangeDocumentDate, value);
+ }
+
public DbUserBaseAccess ToAccess() => new DbUserBaseAccess {
BaseId = BaseId,
BaseName = BaseName,
Title = Title,
HasAccess = HasAccess,
IsAdmin = IsAdmin,
- ReadOnly = ReadOnly
+ ReadOnly = ReadOnly,
+ CanDelete = CanDelete,
+ CanAccountingSettings = CanAccountingSettings,
+ CanChangeDocumentDate = CanChangeDocumentDate
};
}
}
diff --git a/QS.Launcher/ViewModels/PageViewModels/UserManagementVM.cs b/QS.Launcher/ViewModels/PageViewModels/UserManagementVM.cs
index 9627fdea8..11b08ebec 100644
--- a/QS.Launcher/ViewModels/PageViewModels/UserManagementVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/UserManagementVM.cs
@@ -75,6 +75,7 @@ public void SetProvider(IDbUserManager userManager) {
this.RaisePropertyChanged(nameof(ShowAdminFlag));
this.RaisePropertyChanged(nameof(ShowDisabling));
this.RaisePropertyChanged(nameof(ShowReadOnly));
+ this.RaisePropertyChanged(nameof(ShowAppPermissions));
RefreshUsers();
}
@@ -130,6 +131,7 @@ private async Task ChangeOwnPasswordAsync() {
public bool ShowAdminFlag => (provider?.SupportedUserFields.HasFlag(DbUserFields.AdminFlag)) == true;
public bool ShowDisabling => (provider?.SupportedUserFields.HasFlag(DbUserFields.Disabling)) == true;
public bool ShowReadOnly => (provider?.SupportedUserFields.HasFlag(DbUserFields.BaseReadOnly)) == true;
+ public bool ShowAppPermissions => (provider?.SupportedUserFields.HasFlag(DbUserFields.BaseAppPermissions)) == true;
public ObservableCollection Users { get; }
@@ -351,7 +353,7 @@ private void LoadBaseAccess(string login) {
try {
var rows = provider.GetUserBaseAccess(login, applicationInfo);
foreach(var row in rows)
- BaseAccesses.Add(new BaseAccessRowVM(row, ShowReadOnly));
+ BaseAccesses.Add(new BaseAccessRowVM(row, ShowReadOnly, ShowAppPermissions));
var profile = rows.FirstOrDefault(r => !string.IsNullOrEmpty(r.Name) || !string.IsNullOrEmpty(r.Email));
if(profile != null) {
From 5fbce5a52c2206a4a744de83c28af9fdecb75217 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Mon, 13 Jul 2026 17:28:41 +0300
Subject: [PATCH 045/135] =?UTF-8?q?=D0=BD=D0=B5=D0=B1=D0=BE=D0=BB=D1=8C?=
=?UTF-8?q?=D1=88=D0=B8=D0=B5=20=D0=BF=D0=BE=D0=BF=D1=80=D0=B0=D0=B2=D0=BA?=
=?UTF-8?q?=D0=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
QS.DbManagement/MariaDb/MariaDBProvider.cs | 11 +++++------
.../ViewModels/PageViewModels/UserManagementVM.cs | 3 +--
2 files changed, 6 insertions(+), 8 deletions(-)
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index 8e37e0006..a6f14e450 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -217,6 +217,8 @@ public void BackupDatabase(DbInfo database, string filePath, IDbDumpService dump
dumpService.Export(ConnectionStringBuilder.ConnectionString, database.BaseName, filePath, progress, cancellation);
}
+ #endregion
+
#region Управление пользователями
public DbUserFields SupportedUserFields =>
@@ -264,7 +266,7 @@ public List GetUsers() {
var rows = connection.Query(
$"SELECT User AS Login, Host, {lockedColumn} AS AccountLocked, " +
"Super_priv AS SuperPriv, Create_user_priv AS CreateUserPriv " +
- "FROM mysql.user ORDER BY User, Host").ToList();
+ "FROM mysql.user").ToList();
userHosts.Clear();
var result = new List();
@@ -277,10 +279,8 @@ public List GetUsers() {
userHosts[userRows.Key] = userRows.Select(r => string.IsNullOrEmpty(r.Host) ? "%" : r.Host).ToList();
result.Add(new DbUserInfo {
Login = userRows.Key,
- // отключён, только если заблокированы все хосты логина
Disabled = userRows.All(r => string.Equals(r.AccountLocked, "Y", StringComparison.OrdinalIgnoreCase)),
- // админ, если хоть на одном хосте есть SUPER или CREATE USER
- IsAdmin = userRows.Any(r => string.Equals(r.SuperPriv, "Y", StringComparison.OrdinalIgnoreCase)
+ IsAdmin = userRows.All(r => string.Equals(r.SuperPriv, "Y", StringComparison.OrdinalIgnoreCase)
|| string.Equals(r.CreateUserPriv, "Y", StringComparison.OrdinalIgnoreCase)),
IsCurrentUser = string.Equals(userRows.Key, UserName, StringComparison.OrdinalIgnoreCase)
});
@@ -369,7 +369,7 @@ public List GetUserBaseAccess(string login, IApplicationInfo a
var scope = GrantScope(g);
if(scope == null)
return false;
- // шаблонные гранты вида не разворачиваем
+ // шаблонные гранты не разворачиваем
return scope == "*" || string.Equals(UnescapeGrantPattern(scope), db.BaseName, StringComparison.OrdinalIgnoreCase);
})
.SelectMany(GrantPrivileges)
@@ -634,6 +634,5 @@ private void EnsureOpen() {
public void Dispose() {
connection?.Dispose();
}
- #endregion
}
}
diff --git a/QS.Launcher/ViewModels/PageViewModels/UserManagementVM.cs b/QS.Launcher/ViewModels/PageViewModels/UserManagementVM.cs
index 11b08ebec..e27052f67 100644
--- a/QS.Launcher/ViewModels/PageViewModels/UserManagementVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/UserManagementVM.cs
@@ -165,8 +165,7 @@ public void RefreshUsers() {
return;
try {
- foreach(var user in provider.GetUsers())
- Users.Add(user);
+ Users.AddRange(provider.GetUsers());
}
catch(Exception ex) {
logger.Error(ex, "Не удалось получить список пользователей");
From 317b48a3ae9749bc452fbd28bb0e91c372d57722 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Mon, 13 Jul 2026 18:28:31 +0300
Subject: [PATCH 046/135] =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?=
=?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE?=
=?UTF-8?q?=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D1=91=D0=BD=D1=8B=D1=85=20?=
=?UTF-8?q?=D0=BF=D0=BE=D0=BB=D0=B5=D0=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
QS.DbManagement/Entities/DbUserInfo.cs | 7 +-
QS.DbManagement/MariaDb/MariaDBProvider.cs | 69 ++++++++++---------
.../PageViewModels/UserManagementVM.cs | 53 +++++++++++---
3 files changed, 85 insertions(+), 44 deletions(-)
diff --git a/QS.DbManagement/Entities/DbUserInfo.cs b/QS.DbManagement/Entities/DbUserInfo.cs
index 4ce0f34f8..9dfd30430 100644
--- a/QS.DbManagement/Entities/DbUserInfo.cs
+++ b/QS.DbManagement/Entities/DbUserInfo.cs
@@ -16,11 +16,11 @@ public class DbUserInfo {
public bool IsAdmin { get; set; }
/// текущий пользователь подключения
public bool IsCurrentUser { get; set; }
+
+ /// затронутые поля изменениями с вьюхи
+ public DbUserFields DirtyFields { get; set; } = DbUserFields.None;
}
- ///
- /// Какие поля и виды доступа поддерживает конкретный провайдер
- ///
[Flags]
public enum DbUserFields {
None = 0,
@@ -30,7 +30,6 @@ public enum DbUserFields {
Post = 8,
Comment = 16,
AdminFlag = 32,
- /// Возможность отключать пользователя
Disabling = 64,
BaseReadOnly = 128,
BaseAppPermissions = 256
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index a6f14e450..de73115cb 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -314,7 +314,7 @@ public bool UpdateUser(DbUserInfo user, string newPassword = null) {
var options = new List();
if(!string.IsNullOrEmpty(newPassword))
options.Add($"IDENTIFIED BY '{EscapeString(newPassword)}'");
- if(SupportsAccountLock)
+ if(SupportsAccountLock && user.DirtyFields.HasFlag(DbUserFields.Disabling))
options.Add(user.Disabled ? "ACCOUNT LOCK" : "ACCOUNT UNLOCK");
string suffix = string.Join(" ", options);
@@ -324,7 +324,7 @@ public bool UpdateUser(DbUserInfo user, string newPassword = null) {
string account = $"'{EscapeString(user.Login)}'@'{EscapeString(host)}'";
if(options.Count > 0)
statements.Add($"ALTER USER {account} {suffix}");
- if(SupportsAdminFlag)
+ if(SupportsAdminFlag && user.DirtyFields.HasFlag(DbUserFields.AdminFlag))
statements.Add(user.IsAdmin
? $"GRANT ALL PRIVILEGES ON *.* TO {account} WITH GRANT OPTION"
: $"REVOKE ALL PRIVILEGES, GRANT OPTION ON *.* FROM {account}");
@@ -474,39 +474,46 @@ public bool SetUserBaseAccess(string login, DbUserBaseAccess access, IApplicatio
// если таблицы может не быть, тогда молча пропускаем; запись идемпотентна
private void SyncUsersTable(string login, DbUserBaseAccess access) {
- bool tableExists = connection.ExecuteScalar(
- "SELECT COUNT(*) > 0 FROM information_schema.tables WHERE table_schema = @db AND table_name = 'users'",
- new { db = access.BaseName });
- if(!tableExists)
- return;
+ try {
+ bool tableExists = connection.ExecuteScalar(
+ "SELECT COUNT(*) > 0 FROM information_schema.tables WHERE table_schema = @db AND table_name = 'users'",
+ new { db = access.BaseName });
+ if(!tableExists)
+ return;
+
+ string table = $"`{EscapeIdentifier(access.BaseName)}`.users";
- string table = $"`{EscapeIdentifier(access.BaseName)}`.users";
+ if(!access.HasAccess) {
+ connection.Execute($"UPDATE {table} SET deactivated = TRUE WHERE login = @login", new { login });
+ return;
+ }
- if(!access.HasAccess) {
- connection.Execute($"UPDATE {table} SET deactivated = TRUE WHERE login = @login", new { login });
- return;
+ var p = new {
+ login,
+ name = access.Name,
+ email = access.Email,
+ admin = access.IsAdmin,
+ canDelete = access.CanDelete,
+ canAccounting = access.CanAccountingSettings,
+ canDocDate = access.CanChangeDocumentDate
+ };
+ var existingId = connection.QueryFirstOrDefault($"SELECT id FROM {table} WHERE login = @login", new { login });
+ if(existingId != null)
+ // пустые поля формы не затирают уже заполненное приложением значение (COALESCE/NULLIF)
+ connection.Execute($"UPDATE {table} SET name = COALESCE(NULLIF(@name, ''), name), " +
+ "email = COALESCE(NULLIF(@email, ''), email), admin = @admin, " +
+ "can_delete = @canDelete, can_accounting_settings = @canAccounting, can_change_document_date = @canDocDate, " +
+ "deactivated = FALSE WHERE login = @login", p);
+ else
+ connection.Execute($"INSERT INTO {table} " +
+ "(name, login, email, admin, can_delete, can_accounting_settings, can_change_document_date, deactivated) " +
+ "VALUES (COALESCE(NULLIF(@name, ''), @login), @login, NULLIF(@email, ''), @admin, @canDelete, @canAccounting, @canDocDate, FALSE)", p);
}
+ catch(MySqlException ex) {
+ logger.Debug(ex, "Не удалось синхронизировать users в базе {0} для пользователя {1}", access.BaseName, login);
+ }
+ }
- var p = new {
- login,
- name = access.Name,
- email = access.Email,
- admin = access.IsAdmin,
- canDelete = access.CanDelete,
- canAccounting = access.CanAccountingSettings,
- canDocDate = access.CanChangeDocumentDate
- };
- var existingId = connection.QueryFirstOrDefault($"SELECT id FROM {table} WHERE login = @login", new { login });
- if(existingId != null)
- // пустые поля формы не затирают уже заполненное приложением значение (COALESCE/NULLIF)
- connection.Execute($"UPDATE {table} SET name = COALESCE(NULLIF(@name, ''), name), " +
- "email = COALESCE(NULLIF(@email, ''), email), admin = @admin, " +
- "can_delete = @canDelete, can_accounting_settings = @canAccounting, can_change_document_date = @canDocDate, " +
- "deactivated = FALSE WHERE login = @login", p);
- else
- connection.Execute($"INSERT INTO {table} " +
- "(name, login, email, admin, can_delete, can_accounting_settings, can_change_document_date, deactivated) " +
- "VALUES (COALESCE(NULLIF(@name, ''), @login), @login, NULLIF(@email, ''), @admin, @canDelete, @canAccounting, @canDocDate, FALSE)", p);
}
private Dictionary> ReadGrantsByHost(string login) {
diff --git a/QS.Launcher/ViewModels/PageViewModels/UserManagementVM.cs b/QS.Launcher/ViewModels/PageViewModels/UserManagementVM.cs
index e27052f67..8e2f2d839 100644
--- a/QS.Launcher/ViewModels/PageViewModels/UserManagementVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/UserManagementVM.cs
@@ -3,7 +3,9 @@
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
+using System.Reflection;
using System.Threading.Tasks;
+using DynamicData;
using QS.DbManagement;
using QS.DbManagement.Entities;
using QS.Dialog;
@@ -138,7 +140,10 @@ private async Task ChangeOwnPasswordAsync() {
private DbUserInfo selectedUser;
public DbUserInfo SelectedUser {
get => selectedUser;
- set => this.RaiseAndSetIfChanged(ref selectedUser, value);
+ set
+ {
+ this.RaiseAndSetIfChanged(ref selectedUser, value);
+ }
}
public bool HasSelectedUser => SelectedUser != null;
@@ -203,6 +208,8 @@ private async Task SaveUserAsync() {
Disabled = EditDisabled,
IsAdmin = EditIsAdmin
};
+ user.DirtyFields = editedDirtyFields;
+
bool creating = IsNewUser;
string password = EditNewPassword;
@@ -253,6 +260,8 @@ private async Task DeleteUserAsync() {
#region Редактируемые поля пользователя
+ DbUserFields editedDirtyFields = DbUserFields.None;
+
private bool isNewUser;
public bool IsNewUser {
get => isNewUser;
@@ -262,49 +271,73 @@ public bool IsNewUser {
private string editLogin;
public string EditLogin {
get => editLogin;
- set => this.RaiseAndSetIfChanged(ref editLogin, value);
+ set
+ {
+ this.RaiseAndSetIfChanged(ref editLogin, value);
+ }
}
private string editName;
public string EditName {
get => editName;
- set => this.RaiseAndSetIfChanged(ref editName, value);
+ set {
+ editedDirtyFields |= DbUserFields.Name;
+ this.RaiseAndSetIfChanged(ref editName, value);
+ }
}
private string editEmail;
public string EditEmail {
get => editEmail;
- set => this.RaiseAndSetIfChanged(ref editEmail, value);
+ set {
+ editedDirtyFields |= DbUserFields.Email;
+ this.RaiseAndSetIfChanged(ref editEmail, value);
+ }
}
private string editPhone;
public string EditPhone {
get => editPhone;
- set => this.RaiseAndSetIfChanged(ref editPhone, value);
+ set {
+ editedDirtyFields |= DbUserFields.Phone;
+ this.RaiseAndSetIfChanged(ref editPhone, value);
+ }
}
private string editPost;
public string EditPost {
get => editPost;
- set => this.RaiseAndSetIfChanged(ref editPost, value);
+ set {
+ editedDirtyFields |= DbUserFields.Post;
+ this.RaiseAndSetIfChanged(ref editPost, value);
+ }
}
private string editComment;
public string EditComment {
get => editComment;
- set => this.RaiseAndSetIfChanged(ref editComment, value);
+ set {
+ editedDirtyFields |= DbUserFields.Comment;
+ this.RaiseAndSetIfChanged(ref editComment, value);
+ }
}
private bool editDisabled;
public bool EditDisabled {
get => editDisabled;
- set => this.RaiseAndSetIfChanged(ref editDisabled, value);
+ set {
+ editedDirtyFields |= DbUserFields.Disabling;
+ this.RaiseAndSetIfChanged(ref editDisabled, value);
+ }
}
private bool editIsAdmin;
public bool EditIsAdmin {
get => editIsAdmin;
- set => this.RaiseAndSetIfChanged(ref editIsAdmin, value);
+ set {
+ editedDirtyFields |= DbUserFields.AdminFlag;
+ this.RaiseAndSetIfChanged(ref editIsAdmin, value);
+ }
}
private string editNewPassword;
@@ -323,6 +356,8 @@ private void LoadEditBuffer(DbUserInfo user) {
EditDisabled = user.Disabled;
EditIsAdmin = user.IsAdmin;
EditNewPassword = null;
+
+ editedDirtyFields = DbUserFields.None;
}
private void ClearEditBuffer() {
From 8a5683e65c3447f19dac23b8b024c64212ff76f0 Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Mon, 13 Jul 2026 18:29:07 +0300
Subject: [PATCH 047/135] =?UTF-8?q?=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE?=
=?UTF-8?q?=D0=BD=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B3=D0=BB=D0=BE?=
=?UTF-8?q?=D0=B1=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D1=85=20=D0=B8=D0=B7=D0=BC?=
=?UTF-8?q?=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=BF=D0=BE=D0=BB=D0=B5?=
=?UTF-8?q?=D0=B9=20=D0=B2=20users?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
QS.DbManagement/MariaDb/MariaDBProvider.cs | 29 ++++++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/QS.DbManagement/MariaDb/MariaDBProvider.cs b/QS.DbManagement/MariaDb/MariaDBProvider.cs
index de73115cb..f29ca9ca6 100644
--- a/QS.DbManagement/MariaDb/MariaDBProvider.cs
+++ b/QS.DbManagement/MariaDb/MariaDBProvider.cs
@@ -329,6 +329,7 @@ public bool UpdateUser(DbUserInfo user, string newPassword = null) {
? $"GRANT ALL PRIVILEGES ON *.* TO {account} WITH GRANT OPTION"
: $"REVOKE ALL PRIVILEGES, GRANT OPTION ON *.* FROM {account}");
}
+ SyncUsersTables(user);
if(statements.Count == 0)
return true;
connection.Execute(string.Join(";", statements));
@@ -514,6 +515,32 @@ private void SyncUsersTable(string login, DbUserBaseAccess access) {
}
}
+ private void SyncUsersTables(DbUserInfo user) {
+ try {
+ List changeStatement = new List();
+ if(user.DirtyFields.HasFlag(DbUserFields.Name)) {
+ changeStatement.Add("name = COALESCE(NULLIF(@name, ''), name)");
+ }
+ if(user.DirtyFields.HasFlag(DbUserFields.Email)) {
+ changeStatement.Add("email = COALESCE(NULLIF(@email, ''), email)");
+ }
+ if(changeStatement.Count == 0)
+ return;
+
+ IEnumerable tables = DbsOf(user.Login).Select(x => x + ".users");
+ StringBuilder statement = new StringBuilder();
+ foreach(var table in tables) {
+ var existingId = connection.QueryFirstOrDefault($"SELECT id FROM {table} WHERE login = @login", new { user.Login });
+ if(existingId != null)
+ statement.Append($"UPDATE {table} SET " + string.Join(" , ", changeStatement) +
+ " WHERE login = @login;");
+ }
+ if(statement.Length > 0)
+ connection.Execute(statement.ToString(), new { login = user.Login, name = user.Name, email = user.Email });
+ }
+ catch(MySqlException ex) {
+ logger.Debug(ex, "Не удалось синхронизировать users для пользователя {0}", user.Login);
+ }
}
private Dictionary> ReadGrantsByHost(string login) {
@@ -545,6 +572,8 @@ private IReadOnlyList HostsOf(string login) =>
userHosts.TryGetValue(login, out var hosts) && hosts.Count > 0
? (IReadOnlyList)hosts
: new[] { "%" };
+ private IEnumerable DbsOf(string login) =>
+ connection.Query("SELECT table_schema FROM information_schema.tables WHERE table_name = 'users'");
private static void ValidateLogin(string login) {
if(string.IsNullOrWhiteSpace(login))
From afa01912caf1b50776935bc306f0011cb7ac547a Mon Sep 17 00:00:00 2001
From: J0shlerB0y <86416124+J0shlerB0y@users.noreply.github.com>
Date: Mon, 13 Jul 2026 21:40:36 +0300
Subject: [PATCH 048/135] =?UTF-8?q?=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB?=
=?UTF-8?q?=D0=B8=D0=BB=20=D0=B0=20=D1=82=D1=80=D0=B8=20=D1=81=D1=82=D1=80?=
=?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=86=D1=8B=20=D1=83=D0=BF=D1=80=D0=B0=D0=B2?=
=?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=D0=BC=20=D0=BF=D0=BE=D0=BB=D1=8C?=
=?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F=D0=BC=D0=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../QS.Launcher.Avalonia.csproj | 6 +
QS.Launcher.Avalonia/Views/PageViewLocator.cs | 2 +
.../Views/Pages/ChangePasswordView.axaml | 24 +++
.../Views/Pages/ChangePasswordView.axaml.cs | 12 ++
.../Views/Pages/DataBase/DataBasesView.axaml | 8 +-
.../Views/Pages/UserManagementView.axaml | 40 +---
.../Views/Pages/UsersView.axaml | 51 +++++
.../Views/Pages/UsersView.axaml.cs | 12 ++
QS.Launcher/DependencyInjection.cs | 2 +
.../PageViewModels/ChangePasswordVM.cs | 63 ++++++
.../PageViewModels/DataBase/DataBasesVM.cs | 21 +-
.../PageViewModels/UserManagementVM.cs | 193 ++++++------------
.../ViewModels/PageViewModels/UsersVM.cs | 125 ++++++++++++
13 files changed, 384 insertions(+), 175 deletions(-)
create mode 100644 QS.Launcher.Avalonia/Views/Pages/ChangePasswordView.axaml
create mode 100644 QS.Launcher.Avalonia/Views/Pages/ChangePasswordView.axaml.cs
create mode 100644 QS.Launcher.Avalonia/Views/Pages/UsersView.axaml
create mode 100644 QS.Launcher.Avalonia/Views/Pages/UsersView.axaml.cs
create mode 100644 QS.Launcher/ViewModels/PageViewModels/ChangePasswordVM.cs
create mode 100644 QS.Launcher/ViewModels/PageViewModels/UsersVM.cs
diff --git a/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj b/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj
index d48ffc8b0..876045fff 100644
--- a/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj
+++ b/QS.Launcher.Avalonia/QS.Launcher.Avalonia.csproj
@@ -65,6 +65,12 @@
CreateDataBaseProgressView.axaml
+
+ ChangePasswordView.axaml
+
+
+ UsersView.axaml
+
UserManagementView.axaml
diff --git a/QS.Launcher.Avalonia/Views/PageViewLocator.cs b/QS.Launcher.Avalonia/Views/PageViewLocator.cs
index 2cb9894ec..49b350733 100644
--- a/QS.Launcher.Avalonia/Views/PageViewLocator.cs
+++ b/QS.Launcher.Avalonia/Views/PageViewLocator.cs
@@ -19,6 +19,8 @@ public PageViewLocator() {
factories = new Dictionary> {
[typeof(LoginVM)] = vm => new LoginView((LoginVM)vm),
[typeof(DataBasesVM)] = vm => new DataBasesView((DataBasesVM)vm),
+ [typeof(UsersVM)] = vm => new UsersView((UsersVM)vm),
+ [typeof(ChangePasswordVM)] = vm => new ChangePasswordView((ChangePasswordVM)vm),
[typeof(UserManagementVM)] = vm => new UserManagementView((UserManagementVM)vm),
[typeof(CreateDbSettingsVM)] = vm => new CreateDbSettingsView((CreateDbSettingsVM)vm),
[typeof(ImportDbSettingsVM)] = vm => new ImportDbSettingsView((ImportDbSettingsVM)vm),
diff --git a/QS.Launcher.Avalonia/Views/Pages/ChangePasswordView.axaml b/QS.Launcher.Avalonia/Views/Pages/ChangePasswordView.axaml
new file mode 100644
index 000000000..37dea3778
--- /dev/null
+++ b/QS.Launcher.Avalonia/Views/Pages/ChangePasswordView.axaml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/QS.Launcher.Avalonia/Views/Pages/ChangePasswordView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/ChangePasswordView.axaml.cs
new file mode 100644
index 000000000..680403c67
--- /dev/null
+++ b/QS.Launcher.Avalonia/Views/Pages/ChangePasswordView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+using QS.Launcher.ViewModels.PageViewModels;
+
+namespace QS.Launcher.Views.Pages;
+
+public partial class ChangePasswordView : UserControl {
+ public ChangePasswordView(ChangePasswordVM viewModel) {
+ InitializeComponent();
+
+ DataContext = viewModel;
+ }
+}
diff --git a/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml
index 6f9ff3f53..5b4481f6e 100644
--- a/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml
+++ b/QS.Launcher.Avalonia/Views/Pages/DataBase/DataBasesView.axaml
@@ -52,12 +52,14 @@
+
+
-
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -88,8 +53,6 @@
-
-
@@ -134,11 +97,12 @@
Text="У пользователя глобальные права на весь сервер — доступ к отдельным базам не настраивается"
IsVisible="{Binding BaseAccessLocked}" />
-
+
+
diff --git a/QS.Launcher.Avalonia/Views/Pages/UsersView.axaml b/QS.Launcher.Avalonia/Views/Pages/UsersView.axaml
new file mode 100644
index 000000000..7ea047037
--- /dev/null
+++ b/QS.Launcher.Avalonia/Views/Pages/UsersView.axaml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/QS.Launcher.Avalonia/Views/Pages/UsersView.axaml.cs b/QS.Launcher.Avalonia/Views/Pages/UsersView.axaml.cs
new file mode 100644
index 000000000..04f9c8ce1
--- /dev/null
+++ b/QS.Launcher.Avalonia/Views/Pages/UsersView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+using QS.Launcher.ViewModels.PageViewModels;
+
+namespace QS.Launcher.Views.Pages;
+
+public partial class UsersView : UserControl {
+ public UsersView(UsersVM viewModel) {
+ InitializeComponent();
+
+ DataContext = viewModel;
+ }
+}
diff --git a/QS.Launcher/DependencyInjection.cs b/QS.Launcher/DependencyInjection.cs
index 1208b7fe4..07fa8feb4 100644
--- a/QS.Launcher/DependencyInjection.cs
+++ b/QS.Launcher/DependencyInjection.cs
@@ -16,6 +16,8 @@ public static IServiceCollection AddLauncherViewModels(this IServiceCollection s
.AddSingleton()
.AddSingleton()
.AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
// Страница прогресса создаётся заново на каждую операцию с базой
.AddTransient()
.AddSingleton()
diff --git a/QS.Launcher/ViewModels/PageViewModels/ChangePasswordVM.cs b/QS.Launcher/ViewModels/PageViewModels/ChangePasswordVM.cs
new file mode 100644
index 000000000..ad3b3f4c0
--- /dev/null
+++ b/QS.Launcher/ViewModels/PageViewModels/ChangePasswordVM.cs
@@ -0,0 +1,63 @@
+using QS.DbManagement;
+using QS.DbManagement.Entities;
+using QS.Dialog;
+using ReactiveUI;
+using System;
+using System.Reactive;
+using System.Threading.Tasks;
+
+namespace QS.Launcher.ViewModels.PageViewModels {
+ public class ChangePasswordVM : CarouselPageVM {
+ private IDbUserManager provider;
+ private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
+ private readonly IInteractiveMessage interactiveMessage;
+
+ public ChangePasswordVM(IInteractiveMessage interactiveMessage) {
+ this.interactiveMessage = interactiveMessage ?? throw new ArgumentNullException(nameof(interactiveMessage));
+
+ var canChangeOwnPassword = this.WhenAnyValue(x => x.OwnNewPassword, x => x.OwnConfirmPassword,
+ (pass, confirm) => !string.IsNullOrEmpty(pass) && pass == confirm);
+ ChangeOwnPasswordCommand = ReactiveCommand.CreateFromTask(ChangeOwnPasswordAsync, canChangeOwnPassword);
+ BackCommand = ReactiveCommand.Create(() => PopPageCommand?.Execute(null));
+ }
+
+ private string ownNewPassword;
+ public string OwnNewPassword {
+ get => ownNewPassword;
+ set => this.RaiseAndSetIfChanged(ref ownNewPassword, value);
+ }
+
+ private string ownConfirmPassword;
+ public string OwnConfirmPassword {
+ get => ownConfirmPassword;
+ set => this.RaiseAndSetIfChanged(ref ownConfirmPassword, value);
+ }
+
+ public ReactiveCommand ChangeOwnPasswordCommand { get; }
+ public ReactiveCommand BackCommand { get; }
+ public void SetProvider(IDbUserManager userManager) {
+ provider = userManager ?? throw new ArgumentNullException(nameof(userManager));
+ }
+
+ private async Task ChangeOwnPasswordAsync() {
+ try {
+ string newPassword = OwnNewPassword;
+ bool ok = await Task.Run(() => provider.ChangeOwnPassword(newPassword));
+ if(ok) {
+ OwnNewPassword = null;
+ OwnConfirmPassword = null;
+ interactiveMessage.ShowMessage(ImportanceLevel.Success, "Пароль изменён.", "Смена пароля");
+ }
+ else
+ interactiveMessage.ShowMessage(ImportanceLevel.Error, "Не удалось изменить пароль.", "Смена пароля");
+ }
+ catch(Exception ex) {
+ logger.Error(ex, "Не удалось сменить собственный пароль");
+ interactiveMessage.ShowMessage(ImportanceLevel.Error, ex.Message, "Смена пароля");
+ return;
+ }
+
+ PopPageCommand?.Execute(null);
+ }
+ }
+}
diff --git a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
index a31126c5f..5dc3b9932 100644
--- a/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/DataBase/DataBasesVM.cs
@@ -33,6 +33,7 @@ public IDbProvider Provider {
this.RaisePropertyChanged(nameof(CanDropDatabase));
this.RaisePropertyChanged(nameof(CanBackupDatabase));
this.RaisePropertyChanged(nameof(CanManageDatabases));
+ this.RaisePropertyChanged(nameof(CanOpenChangePassword));
this.RaisePropertyChanged(nameof(CanOpenUserManagement));
LoadLastSelectedDatabase();
@@ -50,7 +51,8 @@ public IDbProvider Provider {
public bool CanManageDatabases =>
CanDropDatabase || CanBackupDatabase;
- public bool CanOpenUserManagement => capabilities.CanChangeOwnPassword(provider);
+ public bool CanOpenChangePassword => capabilities.CanChangeOwnPassword(provider);
+ public bool CanOpenUserManagement => capabilities.CanManageUsers(provider);
public Connection CurrentConnection => currentConnection;
@@ -86,6 +88,7 @@ public DbInfo SelectedDatabase {
public ICommand BackupDatabaseCommand { get; }
public ICommand DeleteDatabaseCommand { get; }
public ReactiveCommand OpenUserManagementCommand { get; }
+ public ReactiveCommand OpenChangePasswordCommand { get; }
public event Action StartLaunchProgram;
@@ -123,14 +126,24 @@ public DataBasesVM(
OpenImportDatabaseCommand = ReactiveCommand.Create(OpenImportDatabase);
BackupDatabaseCommand = ReactiveCommand.Create(OpenBackup);
DeleteDatabaseCommand = ReactiveCommand.CreateFromTask(DeleteDatabaseAsync);
- OpenUserManagementCommand = ReactiveCommand.Create(OpenUserManagement);
+ OpenUserManagementCommand = ReactiveCommand.Create(OpenUsers);
+ OpenChangePasswordCommand = ReactiveCommand.Create(ChangePassword);
}
- private void OpenUserManagement() {
+ private void ChangePassword() {
if(provider == null)
return;
- var vm = serviceProvider.GetRequiredService();
+ var vm = serviceProvider.GetRequiredService();
+ vm.SetProvider(provider);
+ PushPageCommand?.Execute(vm);
+ }
+
+ private void OpenUsers() {
+ if(provider == null)
+ return;
+
+ var vm = serviceProvider.GetRequiredService();
vm.SetProvider(provider);
PushPageCommand?.Execute(vm);
}
diff --git a/QS.Launcher/ViewModels/PageViewModels/UserManagementVM.cs b/QS.Launcher/ViewModels/PageViewModels/UserManagementVM.cs
index 8e2f2d839..5ae91f374 100644
--- a/QS.Launcher/ViewModels/PageViewModels/UserManagementVM.cs
+++ b/QS.Launcher/ViewModels/PageViewModels/UserManagementVM.cs
@@ -1,16 +1,16 @@
-using System;
-using System.Collections.ObjectModel;
-using System.Linq;
-using System.Reactive;
-using System.Reactive.Linq;
-using System.Reflection;
-using System.Threading.Tasks;
using DynamicData;
using QS.DbManagement;
using QS.DbManagement.Entities;
using QS.Dialog;
using QS.Project.Versioning;
using ReactiveUI;
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Reactive;
+using System.Reactive.Linq;
+using System.Security.Policy;
+using System.Threading.Tasks;
namespace QS.Launcher.ViewModels.PageViewModels {
public class UserManagementVM : CarouselPageVM {
@@ -20,6 +20,8 @@ public class UserManagementVM : CarouselPageVM {
private readonly IInteractiveQuestion interactiveQuestion;
private readonly IApplicationInfo applicationInfo;
+ private readonly string messageTitle = "Управление пользователями";
+
private IDbUserManager provider;
public UserManagementVM(
@@ -30,25 +32,13 @@ public UserManagementVM(
this.interactiveQuestion = interactiveQuestion ?? throw new ArgumentNullException(nameof(interactiveQuestion));
this.applicationInfo = applicationInfo;
- Users = new ObservableCollection();
BaseAccesses = new ObservableCollection();
- var canChangeOwnPassword = this.WhenAnyValue(x => x.OwnNewPassword, x => x.OwnConfirmPassword,
- (pass, confirm) => !string.IsNullOrEmpty(pass) && pass == confirm);
- ChangeOwnPasswordCommand = ReactiveCommand.CreateFromTask(ChangeOwnPasswordAsync, canChangeOwnPassword);
-
var canSaveUser = this.WhenAnyValue(x => x.EditLogin, x => x.EditNewPassword, x => x.IsNewUser,
(login, pass, isNew) => !string.IsNullOrWhiteSpace(login) && (!isNew || !string.IsNullOrEmpty(pass)));
- SaveUserCommand = ReactiveCommand.CreateFromTask(SaveUserAsync, canSaveUser);
+ SaveCommand = ReactiveCommand.CreateFromTask(Save, canSaveUser);
- var hasSelectedUser = this.WhenAnyValue(x => x.SelectedUser).Select(u => u != null);
- var canSaveAccess = this.WhenAnyValue(x => x.SelectedUser, x => x.CanManageBaseAccess, x => x.BaseAccessLocked,
- (user, canManage, locked) => user != null && canManage && !locked);
- NewUserCommand = ReactiveCommand.Create(StartNewUser);
- DeleteUserCommand = ReactiveCommand.CreateFromTask(DeleteUserAsync, hasSelectedUser);
- SaveAccessCommand = ReactiveCommand.CreateFromTask(SaveAccessAsync, canSaveAccess);
- RefreshUsersCommand = ReactiveCommand.Create(RefreshUsers);
- BackCommand = ReactiveCommand.Create(() => PopPageCommand?.Execute(null));
+ BackCommand = ReactiveCommand.CreateFromTask(GoBack);
this.WhenAnyValue(x => x.SelectedUser)
.Subscribe(_ => OnSelectedUserChanged());
@@ -60,13 +50,9 @@ public UserManagementVM(
});
}
- /// Задаёт провайдера подключения и обновляет состояние страницы.
- public void SetProvider(IDbUserManager userManager) {
+ public void SetContext(IDbUserManager userManager, DbUserInfo user, bool isCreating) {
provider = userManager ?? throw new ArgumentNullException(nameof(userManager));
- OwnNewPassword = null;
- OwnConfirmPassword = null;
-
this.RaisePropertyChanged(nameof(CanManageUsers));
this.RaisePropertyChanged(nameof(CanManageBaseAccess));
this.RaisePropertyChanged(nameof(ShowName));
@@ -79,45 +65,11 @@ public void SetProvider(IDbUserManager userManager) {
this.RaisePropertyChanged(nameof(ShowReadOnly));
this.RaisePropertyChanged(nameof(ShowAppPermissions));
- RefreshUsers();
- }
-
- #region Смена своего пароля
-
- private string ownNewPassword;
- public string OwnNewPassword {
- get => ownNewPassword;
- set => this.RaiseAndSetIfChanged(ref ownNewPassword, value);
- }
-
- private string ownConfirmPassword;
- public string OwnConfirmPassword {
- get => ownConfirmPassword;
- set => this.RaiseAndSetIfChanged(ref ownConfirmPassword, value);
- }
-
- public ReactiveCommand ChangeOwnPasswordCommand { get; }
-
- private async Task ChangeOwnPasswordAsync() {
- try {
- string newPassword = OwnNewPassword;
- bool ok = await Task.Run(() => provider.ChangeOwnPassword(newPassword));
- if(ok) {
- OwnNewPassword = null;
- OwnConfirmPassword = null;
- interactiveMessage.ShowMessage(ImportanceLevel.Success, "Пароль изменён.", "Смена пароля");
- }
- else
- interactiveMessage.ShowMessage(ImportanceLevel.Error, "Не удалось изменить пароль.", "Смена пароля");
- }
- catch(Exception ex) {
- logger.Error(ex, "Не удалось сменить собственный пароль");
- interactiveMessage.ShowMessage(ImportanceLevel.Error, ex.Message, "Смена пароля");
- }
+ SelectedUser = user;
+ IsNewUser = isCreating;
+ ClearEditBuffer();
}
- #endregion
-
#region Управление пользователями
public bool CanManageUsers => provider?.CanManageUsers == true;
@@ -135,8 +87,6 @@ private async Task ChangeOwnPasswordAsync() {
public bool ShowReadOnly => (provider?.SupportedUserFields.HasFlag(DbUserFields.BaseReadOnly)) == true;
public bool ShowAppPermissions => (provider?.SupportedUserFields.HasFlag(DbUserFields.BaseAppPermissions)) == true;
- public ObservableCollection Users { get; }
-
private DbUserInfo selectedUser;
public DbUserInfo SelectedUser {
get => selectedUser;
@@ -156,28 +106,9 @@ public DbUserInfo SelectedUser {
? "задайте пароль нового пользователя"
: "оставьте пустым, чтобы не менять";
- public ReactiveCommand NewUserCommand { get; }
- public ReactiveCommand SaveUserCommand { get; }
- public ReactiveCommand DeleteUserCommand { get; }
- public ReactiveCommand SaveAccessCommand { get; }
- public ReactiveCommand RefreshUsersCommand { get; }
+ public ReactiveCommand SaveCommand { get; }
public ReactiveCommand BackCommand { get; }
- public void RefreshUsers() {
- Users.Clear();
- SelectedUser = null;
- if(!CanManageUsers)
- return;
-
- try {
- Users.AddRange(provider.GetUsers());
- }
- catch(Exception ex) {
- logger.Error(ex, "Не удалось получить список пользователей");
- interactiveMessage.ShowMessage(ImportanceLevel.Error, ex.Message, "Управление пользователями");
- }
- }
-
private void OnSelectedUserChanged() {
this.RaisePropertyChanged(nameof(HasSelectedUser));
BaseAccesses.Clear();
@@ -189,16 +120,11 @@ private void OnSelectedUserChanged() {
LoadEditBuffer(SelectedUser);
IsNewUser = false;
LoadBaseAccess(SelectedUser.Login);
- }
-
- private void StartNewUser() {
- SelectedUser = null;
ClearEditBuffer();
- IsNewUser = true;
}
- private async Task SaveUserAsync() {
- var user = new DbUserInfo {
+ private async Task