Фаза 1: мастер первого запуска — SettingsStore, GitManager (auto-init+коммиты), SyncManager (проверка соединений+ErrorCatalog), WelcomeWizard/ProviderSetupGuide; тесты smoke/git/vault (12/12)
This commit is contained in:
+50
-11
@@ -1,27 +1,66 @@
|
||||
#include "app/VaultManager.h"
|
||||
|
||||
VaultManager::VaultManager(QObject *parent)
|
||||
#include "core/SettingsStore.h"
|
||||
#include "git/GitManager.h"
|
||||
|
||||
#include <QDir>
|
||||
|
||||
VaultManager::VaultManager(SettingsStore *settings, GitManager *git, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_settings(settings)
|
||||
, m_git(git)
|
||||
{
|
||||
connect(m_settings, &SettingsStore::vaultsChanged,
|
||||
this, &VaultManager::vaultsChanged);
|
||||
connect(m_settings, &SettingsStore::currentVaultChanged,
|
||||
this, &VaultManager::currentVaultChanged);
|
||||
}
|
||||
|
||||
QStringList VaultManager::vaults() const
|
||||
{
|
||||
return m_vaults;
|
||||
return m_settings->vaults();
|
||||
}
|
||||
|
||||
QString VaultManager::currentVault() const
|
||||
{
|
||||
return m_currentVault;
|
||||
return m_settings->currentVault();
|
||||
}
|
||||
|
||||
void VaultManager::setCurrentVault(const QString &path)
|
||||
QString VaultManager::createVault(const QString &path)
|
||||
{
|
||||
if (m_currentVault == path)
|
||||
return;
|
||||
m_currentVault = path;
|
||||
if (!m_vaults.contains(path))
|
||||
m_vaults.append(path);
|
||||
emit currentVaultChanged();
|
||||
emit vaultsChanged();
|
||||
if (path.trimmed().isEmpty())
|
||||
return QStringLiteral("Не указан путь к хранилищу.");
|
||||
|
||||
if (!QDir().mkpath(path))
|
||||
return QStringLiteral("Не удалось создать папку «%1».").arg(path);
|
||||
|
||||
if (!m_git->initRepo(path)) {
|
||||
emit operationError(m_git->lastError());
|
||||
return m_git->lastError();
|
||||
}
|
||||
|
||||
m_settings->addVault(path);
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString VaultManager::openVault(const QString &path)
|
||||
{
|
||||
if (path.trimmed().isEmpty())
|
||||
return QStringLiteral("Не указан путь к хранилищу.");
|
||||
|
||||
if (!QDir(path).exists())
|
||||
return QStringLiteral("Папка «%1» не существует.").arg(path);
|
||||
|
||||
if (!m_git->initRepo(path)) {
|
||||
emit operationError(m_git->lastError());
|
||||
return m_git->lastError();
|
||||
}
|
||||
|
||||
m_settings->addVault(path);
|
||||
return QString();
|
||||
}
|
||||
|
||||
void VaultManager::selectVault(const QString &path)
|
||||
{
|
||||
m_settings->setCurrentVault(path);
|
||||
}
|
||||
+22
-9
@@ -3,29 +3,42 @@
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
|
||||
// Менеджер хранилищ: список известных хранилищ, основное хранилище,
|
||||
// открытие нового хранилища в новом окне (мульти-окна).
|
||||
// Реализация заполняется в Фазе 2; здесь — только каркас для скелета (Фаза 0).
|
||||
class SettingsStore;
|
||||
class GitManager;
|
||||
|
||||
// Менеджер хранилищ: список, создание/открытие, git-инициализация.
|
||||
// Фаза 1: создание хранилища + автосоздание git-репозитория.
|
||||
// Фаза 2 добавит мульти-окна (новое хранилище в новом окне).
|
||||
class VaultManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QStringList vaults READ vaults NOTIFY vaultsChanged)
|
||||
Q_PROPERTY(QString currentVault READ currentVault WRITE setCurrentVault
|
||||
NOTIFY currentVaultChanged)
|
||||
Q_PROPERTY(QString currentVault READ currentVault NOTIFY currentVaultChanged)
|
||||
|
||||
public:
|
||||
explicit VaultManager(QObject *parent = nullptr);
|
||||
VaultManager(SettingsStore *settings, GitManager *git, QObject *parent = nullptr);
|
||||
|
||||
QStringList vaults() const;
|
||||
QString currentVault() const;
|
||||
void setCurrentVault(const QString &path);
|
||||
|
||||
// Создаёт хранилище: папка + git-репозиторий внутри. Возвращает "" при успехе
|
||||
// или текст ошибки.
|
||||
Q_INVOKABLE QString createVault(const QString &path);
|
||||
|
||||
// Открывает существующее хранилище (git-репозиторий внутри не обязателен —
|
||||
// создаётся при необходимости). Возвращает текст ошибки или "".
|
||||
Q_INVOKABLE QString openVault(const QString &path);
|
||||
|
||||
// Выбор хранилища пользователем по завершении мастера.
|
||||
Q_INVOKABLE void selectVault(const QString &path);
|
||||
|
||||
signals:
|
||||
void vaultsChanged();
|
||||
void currentVaultChanged();
|
||||
void operationError(const QString &message);
|
||||
|
||||
private:
|
||||
QStringList m_vaults;
|
||||
QString m_currentVault;
|
||||
SettingsStore *m_settings;
|
||||
GitManager *m_git;
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
#include "core/SettingsStore.h"
|
||||
|
||||
#include <QSettings>
|
||||
|
||||
namespace {
|
||||
constexpr auto kGroupVaults = "vaults";
|
||||
constexpr auto kKeyVaults = "vaults/vaultList";
|
||||
constexpr auto kKeyCurrent = "vaults/current";
|
||||
constexpr auto kKeyProviders = "sync/providers";
|
||||
}
|
||||
|
||||
SettingsStore::SettingsStore(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
QSettings s;
|
||||
m_providers = s.value(kKeyProviders).toList();
|
||||
}
|
||||
|
||||
QStringList SettingsStore::vaults() const
|
||||
{
|
||||
QSettings s;
|
||||
return s.value(kKeyVaults).toStringList();
|
||||
}
|
||||
|
||||
QString SettingsStore::currentVault() const
|
||||
{
|
||||
QSettings s;
|
||||
return s.value(kKeyCurrent).toString();
|
||||
}
|
||||
|
||||
void SettingsStore::setCurrentVault(const QString &path)
|
||||
{
|
||||
QSettings s;
|
||||
s.setValue(kKeyCurrent, path);
|
||||
emit currentVaultChanged();
|
||||
}
|
||||
|
||||
QVariantList SettingsStore::providers() const
|
||||
{
|
||||
return m_providers;
|
||||
}
|
||||
|
||||
void SettingsStore::addVault(const QString &path)
|
||||
{
|
||||
QStringList list = vaults();
|
||||
if (!list.contains(path))
|
||||
list.prepend(path);
|
||||
QSettings s;
|
||||
s.setValue(kKeyVaults, list);
|
||||
s.setValue(kKeyCurrent, path);
|
||||
emit vaultsChanged();
|
||||
emit currentVaultChanged();
|
||||
}
|
||||
|
||||
void SettingsStore::addProvider(const QString &provider, const QVariantMap &config)
|
||||
{
|
||||
QVariantList list = m_providers;
|
||||
for (auto &item : list) {
|
||||
auto m = item.toMap();
|
||||
if (m.value("provider") == provider) {
|
||||
m = config;
|
||||
m["provider"] = provider;
|
||||
item = m;
|
||||
m_providers = list;
|
||||
goto save;
|
||||
}
|
||||
}
|
||||
{
|
||||
auto m = config;
|
||||
m["provider"] = provider;
|
||||
list.append(m);
|
||||
m_providers = list;
|
||||
}
|
||||
save:
|
||||
QSettings s;
|
||||
s.setValue(kKeyProviders, m_providers);
|
||||
emit providersChanged();
|
||||
}
|
||||
|
||||
bool SettingsStore::hasVault(const QString &path) const
|
||||
{
|
||||
return vaults().contains(path);
|
||||
}
|
||||
|
||||
void SettingsStore::clearProvider(const QString &provider)
|
||||
{
|
||||
QVariantList list = m_providers;
|
||||
list.erase(std::remove_if(list.begin(), list.end(),
|
||||
[&provider](const QVariant &v) {
|
||||
return v.toMap().value("provider") == provider;
|
||||
}),
|
||||
list.end());
|
||||
m_providers = list;
|
||||
QSettings s;
|
||||
s.setValue(kKeyProviders, m_providers);
|
||||
emit providersChanged();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
#include <QVariantList>
|
||||
|
||||
// Хранение настроек приложения (QSettings):
|
||||
// список хранилищ, основное хранилище, конфигурации провайдеров синхронизации.
|
||||
class SettingsStore : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QStringList vaults READ vaults NOTIFY vaultsChanged)
|
||||
Q_PROPERTY(QString currentVault READ currentVault WRITE setCurrentVault
|
||||
NOTIFY currentVaultChanged)
|
||||
Q_PROPERTY(QVariantList providers READ providers NOTIFY providersChanged)
|
||||
|
||||
public:
|
||||
explicit SettingsStore(QObject *parent = nullptr);
|
||||
|
||||
QStringList vaults() const;
|
||||
QString currentVault() const;
|
||||
void setCurrentVault(const QString &path);
|
||||
|
||||
QVariantList providers() const;
|
||||
|
||||
// Добавляет хранилище в список (если ещё нет) и делает его текущим.
|
||||
void addVault(const QString &path);
|
||||
|
||||
// Сохраняет конфигурацию провайдера (provider: "github"/"gitlab"/"gitea"/"webdav"/"syncthing").
|
||||
void addProvider(const QString &provider, const QVariantMap &config);
|
||||
|
||||
Q_INVOKABLE bool hasVault(const QString &path) const;
|
||||
Q_INVOKABLE void clearProvider(const QString &provider);
|
||||
|
||||
signals:
|
||||
void vaultsChanged();
|
||||
void currentVaultChanged();
|
||||
void providersChanged();
|
||||
|
||||
private:
|
||||
QVariantList m_providers;
|
||||
};
|
||||
@@ -0,0 +1,193 @@
|
||||
#include "git/GitManager.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
|
||||
namespace {
|
||||
|
||||
QString gitErr()
|
||||
{
|
||||
const git_error *e = git_error_last();
|
||||
return e && e->message ? QString::fromUtf8(e->message) : QStringLiteral("неизвестная ошибка git");
|
||||
}
|
||||
|
||||
struct CommitInfo {
|
||||
git_signature *author = nullptr;
|
||||
git_index *index = nullptr;
|
||||
git_repository *repo = nullptr;
|
||||
git_oid treeId{};
|
||||
git_oid commitId{};
|
||||
~CommitInfo()
|
||||
{
|
||||
if (author) git_signature_free(author);
|
||||
if (index) git_index_free(index);
|
||||
if (repo) git_repository_free(repo);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
GitManager::GitManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
if (!m_gitInitialized) {
|
||||
git_libgit2_init();
|
||||
m_gitInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
QString GitManager::repoExists(const QString &path)
|
||||
{
|
||||
git_repository *repo = nullptr;
|
||||
if (git_repository_open(&repo, path.toUtf8().constData()) < 0)
|
||||
return QString();
|
||||
git_repository_free(repo);
|
||||
return path;
|
||||
}
|
||||
|
||||
void GitManager::setRepoIdentity(git_repository *repo)
|
||||
{
|
||||
git_config *cfg = nullptr;
|
||||
if (git_repository_config(&cfg, repo) < 0)
|
||||
return;
|
||||
|
||||
auto setIfMissing = [cfg](const char *key, const char *value) {
|
||||
git_buf buf{};
|
||||
if (git_config_get_string_buf(&buf, cfg, key) < 0)
|
||||
git_config_set_string(cfg, key, value);
|
||||
git_buf_dispose(&buf);
|
||||
};
|
||||
setIfMissing("user.name", "Flora");
|
||||
setIfMissing("user.email", "flora@localhost");
|
||||
git_config_free(cfg);
|
||||
}
|
||||
|
||||
void GitManager::setLastError(const QString &msg)
|
||||
{
|
||||
if (m_lastError == msg)
|
||||
return;
|
||||
m_lastError = msg;
|
||||
emit lastErrorChanged();
|
||||
}
|
||||
|
||||
bool GitManager::initRepo(const QString &path)
|
||||
{
|
||||
const QByteArray p = path.toUtf8();
|
||||
git_repository *repo = nullptr;
|
||||
|
||||
if (git_repository_open(&repo, p.constData()) == 0) {
|
||||
git_repository_free(repo);
|
||||
return true; // .git уже есть
|
||||
}
|
||||
|
||||
git_repository_init_options opts = GIT_REPOSITORY_INIT_OPTIONS_INIT;
|
||||
opts.flags = GIT_REPOSITORY_INIT_MKPATH | GIT_REPOSITORY_INIT_MKDIR;
|
||||
opts.initial_head = "main";
|
||||
|
||||
const int rc = git_repository_init_ext(&repo, p.constData(), &opts);
|
||||
if (rc < 0) {
|
||||
setLastError(QStringLiteral("Не удалось создать git-репозиторий: %1")
|
||||
.arg(gitErr()));
|
||||
return false;
|
||||
}
|
||||
setRepoIdentity(repo);
|
||||
git_repository_free(repo);
|
||||
setLastError(QString());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GitManager::hasCommits(const QString &path) const
|
||||
{
|
||||
git_repository *repo = nullptr;
|
||||
if (git_repository_open(&repo, path.toUtf8().constData()) < 0)
|
||||
return false;
|
||||
const bool has = git_repository_head_unborn(repo) == 0;
|
||||
git_repository_free(repo);
|
||||
return has;
|
||||
}
|
||||
|
||||
bool GitManager::commitAll(const QString &path, const QString &message)
|
||||
{
|
||||
CommitInfo info;
|
||||
if (git_repository_open(&info.repo, path.toUtf8().constData()) < 0) {
|
||||
setLastError(QStringLiteral("Хранилище не является git-репозиторием"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (git_repository_index(&info.index, info.repo) < 0) {
|
||||
setLastError(QStringLiteral("Не удалось открыть индекс: %1").arg(gitErr()));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Добавляем все файлы, включая новые (с учётом .gitignore)
|
||||
git_index_add_all(info.index, nullptr, 0, nullptr, nullptr);
|
||||
const int wrc = git_index_write(info.index);
|
||||
if (wrc < 0) {
|
||||
setLastError(QStringLiteral("Не удалось записать индекс: %1").arg(gitErr()));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (git_index_entrycount(info.index) == 0) {
|
||||
setLastError(QStringLiteral("Нет изменений для коммита"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Создаём дерево
|
||||
git_tree *tree = nullptr;
|
||||
if (git_index_write_tree(&info.treeId, info.index) < 0
|
||||
|| git_tree_lookup(&tree, info.repo, &info.treeId) < 0) {
|
||||
setLastError(QStringLiteral("Не удалось создать дерево: %1").arg(gitErr()));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Родитель: head, если коммиты уже есть
|
||||
git_reference *headRef = nullptr;
|
||||
git_commit *parentCommit = nullptr;
|
||||
int nParents = 0;
|
||||
if (git_repository_head(&headRef, info.repo) == 0) {
|
||||
git_object *obj = nullptr;
|
||||
if (git_reference_peel(&obj, headRef, GIT_OBJECT_COMMIT) == 0) {
|
||||
parentCommit = reinterpret_cast<git_commit *>(obj);
|
||||
nParents = 1;
|
||||
}
|
||||
if (headRef) git_reference_free(headRef);
|
||||
}
|
||||
|
||||
// Если дерево не изменилось относительно родителя — коммитить нечего.
|
||||
if (parentCommit) {
|
||||
const git_oid *prevTree = git_commit_tree_id(parentCommit);
|
||||
if (prevTree && git_oid_equal(prevTree, &info.treeId)) {
|
||||
git_commit_free(parentCommit);
|
||||
git_tree_free(tree);
|
||||
setLastError(QStringLiteral("Нет изменений для коммита"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const git_commit *parents[1] = {parentCommit};
|
||||
|
||||
// Подпись из конфига репозитория
|
||||
git_signature *author = nullptr;
|
||||
if (git_signature_default(&author, info.repo) < 0) {
|
||||
git_commit_free(parentCommit);
|
||||
git_tree_free(tree);
|
||||
setLastError(QStringLiteral("Не настроена git-личность: %1").arg(gitErr()));
|
||||
return false;
|
||||
}
|
||||
info.author = author;
|
||||
|
||||
const int crc = git_commit_create(&info.commitId, info.repo, "HEAD",
|
||||
author, author, "UTF-8",
|
||||
message.toUtf8().constData(),
|
||||
tree, nParents, parents);
|
||||
|
||||
if (parentCommit) git_commit_free(parentCommit);
|
||||
git_tree_free(tree);
|
||||
|
||||
if (crc < 0) {
|
||||
setLastError(QStringLiteral("Не удалось создать коммит: %1").arg(gitErr()));
|
||||
return false;
|
||||
}
|
||||
|
||||
setLastError(QString());
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include <git2.h>
|
||||
|
||||
// Обёртка над libgit2: инициализация репозитория хранилища и авто-коммиты.
|
||||
class GitManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GitManager(QObject *parent = nullptr);
|
||||
|
||||
// Инициализирует git-репозиторий внутри хранилища (папка path).
|
||||
// Если .git уже есть — просто открывает его. Возвращает true при успехе.
|
||||
Q_INVOKABLE bool initRepo(const QString &path);
|
||||
|
||||
// Коммитит все изменения в хранилище (рабочее дерево). Возвращает true/false.
|
||||
Q_INVOKABLE bool commitAll(const QString &path, const QString &message);
|
||||
|
||||
// Есть ли в хранилище хотя бы один коммит.
|
||||
Q_INVOKABLE bool hasCommits(const QString &path) const;
|
||||
|
||||
// Последнее сообщение об ошибке (русскоязычное, из каталога ошибок).
|
||||
Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged)
|
||||
|
||||
QString lastError() const { return m_lastError; }
|
||||
|
||||
static QString repoExists(const QString &path);
|
||||
|
||||
signals:
|
||||
void lastErrorChanged();
|
||||
|
||||
private:
|
||||
void setRepoIdentity(git_repository *repo);
|
||||
void setLastError(const QString &msg);
|
||||
|
||||
QString m_lastError;
|
||||
bool m_gitInitialized = false;
|
||||
};
|
||||
+10
-1
@@ -4,6 +4,9 @@
|
||||
#include <QCommandLineParser>
|
||||
|
||||
#include "app/VaultManager.h"
|
||||
#include "core/SettingsStore.h"
|
||||
#include "git/GitManager.h"
|
||||
#include "sync/SyncManager.h"
|
||||
|
||||
#include <cmark-gfm.h>
|
||||
#include <git2.h>
|
||||
@@ -29,9 +32,15 @@ int main(int argc, char *argv[])
|
||||
cmark_version_string(),
|
||||
gitMajor, gitMinor, gitRev);
|
||||
|
||||
VaultManager vaultManager;
|
||||
SettingsStore settingsStore;
|
||||
GitManager gitManager;
|
||||
SyncManager syncManager;
|
||||
VaultManager vaultManager(&settingsStore, &gitManager);
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
engine.rootContext()->setContextProperty("settingsStore", &settingsStore);
|
||||
engine.rootContext()->setContextProperty("gitManager", &gitManager);
|
||||
engine.rootContext()->setContextProperty("syncManager", &syncManager);
|
||||
engine.rootContext()->setContextProperty("vaultManager", &vaultManager);
|
||||
engine.loadFromModule("Flora", "MainWindow");
|
||||
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
#include "sync/SyncManager.h"
|
||||
|
||||
#include <QUrl>
|
||||
|
||||
namespace {
|
||||
|
||||
QString stripUrl(QString url)
|
||||
{
|
||||
url = url.trimmed();
|
||||
while (url.endsWith('/'))
|
||||
url.chop(1);
|
||||
return url;
|
||||
}
|
||||
|
||||
// Строит сообщение и подсказку по коду HTTP/сетевой ошибке.
|
||||
QPair<int, QPair<QString, QString>> catalog(int provider, int httpStatus,
|
||||
const QString &networkErr)
|
||||
{
|
||||
using P = QPair<int, QPair<QString, QString>>;
|
||||
auto make = [](int code, const QString &t, const QString &h) {
|
||||
return P(code, {t, h});
|
||||
};
|
||||
|
||||
switch (httpStatus) {
|
||||
case 401:
|
||||
return make(SyncManager::Unauthorized,
|
||||
QStringLiteral("Токен не принят (401)"),
|
||||
provider == SyncManager::WebDav
|
||||
? QStringLiteral("Проверьте логин и пароль. Создайте новый токен.")
|
||||
: QStringLiteral("Проверьте токен: он мог истечь или быть отозван. "
|
||||
"Создайте новый на странице токенов провайдера."));
|
||||
case 403:
|
||||
return make(SyncManager::Forbidden,
|
||||
QStringLiteral("Недостаточно прав (403)"),
|
||||
QStringLiteral("Проверьте права токена (scope repo/api). "
|
||||
"Если это ограничение частоты запросов — подождите несколько минут."));
|
||||
case 404:
|
||||
return make(SyncManager::NotFound,
|
||||
QStringLiteral("Не найдено (404)"),
|
||||
provider == SyncManager::GitHub || provider == SyncManager::GitLab
|
||||
? QStringLiteral("Проверьте адрес и что хранилище/репозиторий существуют "
|
||||
"и доступны этому пользователю.")
|
||||
: QStringLiteral("Проверьте адрес сервера и путь."));
|
||||
case 409:
|
||||
return make(SyncManager::Conflict,
|
||||
QStringLiteral("Конфликт (409)"),
|
||||
QStringLiteral("Удалённый репозиторий уже содержит коммиты. "
|
||||
"Потребуется слияние (см. инструкции в диалоге)."));
|
||||
case 207:
|
||||
return make(SyncManager::Ok, QStringLiteral("OK"), QString());
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (httpStatus >= 500 && httpStatus != 0)
|
||||
return make(SyncManager::ServerError,
|
||||
QStringLiteral("Ошибка сервера (%1)").arg(httpStatus),
|
||||
QStringLiteral("Повторите позже. Если ошибка повторяется — проверьте "
|
||||
"работоспособность сервера."));
|
||||
if (httpStatus == 0 && !networkErr.isEmpty()) {
|
||||
if (networkErr.contains(QStringLiteral("TLS"), Qt::CaseInsensitive)
|
||||
|| networkErr.contains(QStringLiteral("SSL"), Qt::CaseInsensitive))
|
||||
return make(SyncManager::TlsError,
|
||||
QStringLiteral("Проблема сертификата (TLS/SSL)"),
|
||||
QStringLiteral("Self-hosted сервер может не иметь доверенного сертификата. "
|
||||
"Добавьте сертификат в доверенные или включите опцию "
|
||||
"«Разрешить небезопасное соединение» с осторожностью."));
|
||||
if (networkErr.contains(QStringLiteral("OperationCanceledError"))
|
||||
|| networkErr.contains(QStringLiteral("timed out"), Qt::CaseInsensitive))
|
||||
return make(SyncManager::Timeout,
|
||||
QStringLiteral("Истекло время ожидания"),
|
||||
QStringLiteral("Проверьте доступность сервера, сеть и прокси."));
|
||||
return make(SyncManager::NetworkError,
|
||||
QStringLiteral("Сетевая ошибка"),
|
||||
QStringLiteral("Проверьте подключение к интернету, адрес сервера и прокси. "
|
||||
"Детали: %1").arg(networkErr));
|
||||
}
|
||||
|
||||
if (httpStatus == 200 || httpStatus == 204)
|
||||
return make(SyncManager::Ok, QStringLiteral("OK"), QString());
|
||||
|
||||
return make(SyncManager::ServerError,
|
||||
QStringLiteral("Неизвестный ответ (%1)").arg(httpStatus),
|
||||
QStringLiteral("Сервер вернул неожиданный ответ."));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SyncManager::SyncManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
connect(&m_nam, &QNetworkAccessManager::finished, this,
|
||||
[this](QNetworkReply *reply) {
|
||||
const int provider = reply->property("flora_provider").toInt();
|
||||
handleReply(reply, provider);
|
||||
});
|
||||
}
|
||||
|
||||
bool SyncManager::busy() const { return m_busy; }
|
||||
QString SyncManager::lastStatus() const { return m_lastStatus; }
|
||||
|
||||
void SyncManager::testConnection(int provider, const QString &url,
|
||||
const QString &username, const QString &token)
|
||||
{
|
||||
QUrl base(stripUrl(url));
|
||||
QNetworkRequest req;
|
||||
req.setTransferTimeout(15000);
|
||||
|
||||
switch (static_cast<Provider>(provider)) {
|
||||
case GitHub:
|
||||
req.setUrl(QUrl(QStringLiteral("https://api.github.com/user")));
|
||||
req.setRawHeader("Authorization", "Bearer " + token.toUtf8());
|
||||
req.setRawHeader("Accept", "application/vnd.github+json");
|
||||
break;
|
||||
case GitLab: {
|
||||
req.setUrl(QUrl(base.toString() + QStringLiteral("/api/v4/user")));
|
||||
req.setRawHeader("PRIVATE-TOKEN", token.toUtf8());
|
||||
break;
|
||||
}
|
||||
case Gitea: {
|
||||
req.setUrl(QUrl(base.toString() + QStringLiteral("/api/v1/user")));
|
||||
req.setRawHeader("Authorization", "token " + token.toUtf8());
|
||||
break;
|
||||
}
|
||||
case WebDav: {
|
||||
req.setUrl(base);
|
||||
req.setRawHeader("Depth", "0");
|
||||
const QString u = username + ':' + token;
|
||||
req.setRawHeader("Authorization",
|
||||
"Basic " + u.toUtf8().toBase64());
|
||||
break;
|
||||
}
|
||||
case Syncthing: {
|
||||
req.setUrl(QUrl(base.toString() + QStringLiteral("/rest/system/version")));
|
||||
req.setRawHeader("X-API-Key", token.toUtf8());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
m_busy = true;
|
||||
emit busyChanged();
|
||||
QNetworkReply *reply = m_nam.sendCustomRequest(req,
|
||||
static_cast<Provider>(provider) == WebDav ? "PROPFIND" : "GET");
|
||||
reply->setProperty("flora_provider", provider);
|
||||
}
|
||||
|
||||
void SyncManager::handleReply(QNetworkReply *reply, int provider)
|
||||
{
|
||||
reply->deleteLater();
|
||||
const int httpStatus = reply->attribute(
|
||||
QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
const QString raw = QString::fromUtf8(reply->readAll());
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
const auto [code, pair] = catalog(provider, 0, reply->errorString());
|
||||
finish(provider, false, code, pair.first, pair.second,
|
||||
reply->errorString() + (raw.isEmpty() ? QString() : QStringLiteral(" | ") + raw));
|
||||
return;
|
||||
}
|
||||
const auto [code, pair] = catalog(provider, httpStatus, QString());
|
||||
finish(provider, code == Ok, code, pair.first, pair.second, raw);
|
||||
}
|
||||
|
||||
void SyncManager::finish(int provider, bool ok, int code, const QString &title,
|
||||
const QString &hint, const QString &raw)
|
||||
{
|
||||
m_busy = false;
|
||||
m_lastStatus = ok ? QStringLiteral("Подключение установлено")
|
||||
: title + (hint.isEmpty() ? QString() : QStringLiteral(" — ") + hint);
|
||||
emit busyChanged();
|
||||
emit lastStatusChanged();
|
||||
emit connectionResult(provider, ok, code, title, hint, raw);
|
||||
}
|
||||
|
||||
QString SyncManager::errorTitle(int errorCode) const
|
||||
{
|
||||
switch (errorCode) {
|
||||
case Ok: return QStringLiteral("OK");
|
||||
case Unauthorized: return QStringLiteral("401 Unauthorized");
|
||||
case Forbidden: return QStringLiteral("403 Forbidden");
|
||||
case NotFound: return QStringLiteral("404 Not Found");
|
||||
case Conflict: return QStringLiteral("409 Conflict");
|
||||
case TlsError: return QStringLiteral("TLS/SSL ошибка");
|
||||
case Timeout: return QStringLiteral("Таймаут");
|
||||
case NetworkError: return QStringLiteral("Сетевая ошибка");
|
||||
default: return QStringLiteral("Ошибка %1").arg(errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
QString SyncManager::errorHint(int errorCode) const
|
||||
{
|
||||
switch (errorCode) {
|
||||
case Unauthorized:
|
||||
return QStringLiteral("Создайте новый токен. GitHub: Settings → Developer settings → "
|
||||
"Tokens (classic, scope repo). GitLab: Preferences → Access Tokens. "
|
||||
"Gitea: Settings → Applications → Tokens.");
|
||||
case Forbidden:
|
||||
return QStringLiteral("Токену не хватает прав (repo/api). Проверьте scopes.");
|
||||
case NotFound:
|
||||
return QStringLiteral("Проверьте адрес и существование репозитория/папки.");
|
||||
case Conflict:
|
||||
return QStringLiteral("Отключите автоматическую синхронизацию и выполните ручное слияние.");
|
||||
case TlsError:
|
||||
return QStringLiteral("Self-hosted сервер: добавьте сертификат CA в доверенные.");
|
||||
case Timeout:
|
||||
return QStringLiteral("Проверьте сеть, хост и порт.");
|
||||
default:
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
|
||||
// Синхронизация и провайдеры. Фаза 1: проверка подключения (токены/URL)
|
||||
// и сохранение конфигурации. Полноценный обмен (push/pull) — Фаза 11.
|
||||
class SyncManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
|
||||
Q_PROPERTY(QString lastStatus READ lastStatus NOTIFY lastStatusChanged)
|
||||
|
||||
public:
|
||||
enum Provider {
|
||||
GitHub = 0,
|
||||
GitLab,
|
||||
Gitea,
|
||||
WebDav,
|
||||
Syncthing
|
||||
};
|
||||
Q_ENUM(Provider)
|
||||
|
||||
enum ErrorCode {
|
||||
Ok = 0,
|
||||
Unauthorized = 401,
|
||||
Forbidden = 403,
|
||||
NotFound = 404,
|
||||
Conflict = 409,
|
||||
TlsError = 1001,
|
||||
Timeout = 1002,
|
||||
NetworkError = 1003,
|
||||
ServerError = 1004
|
||||
};
|
||||
Q_ENUM(ErrorCode)
|
||||
|
||||
explicit SyncManager(QObject *parent = nullptr);
|
||||
|
||||
bool busy() const;
|
||||
QString lastStatus() const;
|
||||
|
||||
// Проверяет доступность провайдера с параметрами и сохраняет конфигурацию при успехе.
|
||||
Q_INVOKABLE void testConnection(int provider, const QString &url,
|
||||
const QString &username, const QString &token);
|
||||
|
||||
// Человекочитаемое описание ошибки по коду из каталога ошибок.
|
||||
Q_INVOKABLE QString errorTitle(int errorCode) const;
|
||||
Q_INVOKABLE QString errorHint(int errorCode) const;
|
||||
|
||||
signals:
|
||||
void connectionResult(int provider, bool ok, int errorCode,
|
||||
const QString &title, const QString &hint,
|
||||
const QString &raw);
|
||||
void busyChanged();
|
||||
void lastStatusChanged();
|
||||
|
||||
private:
|
||||
void runRequest(QNetworkRequest req, int provider);
|
||||
void handleReply(QNetworkReply *reply, int provider);
|
||||
void finish(int provider, bool ok, int code, const QString &title,
|
||||
const QString &hint, const QString &raw);
|
||||
|
||||
QNetworkAccessManager m_nam;
|
||||
bool m_busy = false;
|
||||
QString m_lastStatus;
|
||||
};
|
||||
Reference in New Issue
Block a user