Files
parr_api/PARR.Core/Services/HelpDocument/Implementations/GitLabHelpProvider.cs

201 lines
9.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.Extensions.Logging;
using PARR.Core.Services.HelpDocument.Interfaces;
using PARR.Domain.DTOs.HelpDocument;
using PARR.Domain.Settings;
using System.Web;
namespace PARR.Core.Services.HelpDocument.Implementations
{
/// <summary>
/// Получение справочной документации из GitLab
/// </summary>
internal class GitLabHelpProvider : IHelpDocumentProvider
{
private readonly HttpClient _httpClient;
private readonly HelpDocumentSettings _helpDocumentSettings;
private readonly ILogger<GitLabHelpProvider> _logger;
public GitLabHelpProvider(HttpClient httpClient, HelpDocumentSettings helpDocumentSettings, ILogger<GitLabHelpProvider> logger)
{
_httpClient = httpClient;
_helpDocumentSettings = helpDocumentSettings;
_logger = logger;
// Базовая настройка http
_httpClient = httpClient;
if (_helpDocumentSettings.GitLabSettings == null)
{
_logger.LogCritical("Инициализация провайдера справки провалена: не задана секция GitLabSettings");
throw new ArgumentNullException(nameof(_helpDocumentSettings.GitLabSettings), "Не заданы настройки подключения к GitLab");
}
_httpClient.BaseAddress = new Uri(_helpDocumentSettings.GitLabSettings.BaseUrl);
_httpClient.DefaultRequestHeaders.Add("PRIVATE-TOKEN", _helpDocumentSettings.GitLabSettings.Token);
}
public async Task<HelpDocumentResult?> GetDocumentAsync(string helpId)
{
//1. Формируем путь к файлу
var filePath = $"{helpId}.md";
// 2. GitLab API требует, чтобы относительный путь к файлу в URL был закодирован (экранирован)
var urlEncodedPath = HttpUtility.UrlEncode(filePath);
// 3. Собираем эндпоинт для получения RAW контента файла
string relativeRequestUrl = $"projects/{_helpDocumentSettings.GitLabSettings!.ProjectId}/repository/files/{urlEncodedPath}/raw?ref={_helpDocumentSettings.GitLabSettings.DefaultBranch}";
try
{
// Создаем URI строго с флагом UriKind.Relative. Это заставит HttpClient отправить %2F в сыром виде!
var relativeUri = new Uri(relativeRequestUrl, UriKind.Relative);
_logger.LogDebug("Отправка HTTP GET запроса к GitLab API. Относительный URI: {RelativeUri}", relativeRequestUrl);
var response = await _httpClient.GetAsync(relativeUri);
// Если файла нет (404) или токен не подошел (401/403)
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning("Документ справки не найден или недоступен в GitLab. HelpId: {HelpId}, StatusCode: {StatusCode}",
helpId, response.StatusCode);
return null;
}
_logger.LogDebug("Файл справки успешно получен из GitLab. Размер ответа: {ContentLength} байт",
response.Content.Headers.ContentLength);
// Получаем весь текст файла (и шапку, и контент)
string rawFileContent = await response.Content.ReadAsStringAsync();
// 4. Парсим файл
var result = ParseMarkdownFile(helpId, rawFileContent);
_logger.LogDebug("Документ справки успешно загружен и распарсен. HelpId: {HelpId}, Title: '{Title}'",
helpId, result.Title);
var editUrl = GenerateEditUrl(helpId);
return result with
{
EditUrl = editUrl
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка при запросе справки {HelpId} из GitLab", helpId);
return null;
}
}
private string? GenerateEditUrl(string helpId)
{
if (_helpDocumentSettings.GitLabSettings == null)
return null;
var repoPath = _helpDocumentSettings.GitLabSettings.RepositoryPath;
var branch = _helpDocumentSettings.GitLabSettings.DefaultBranch ?? "main";
return $"{repoPath}/-/edit/{branch}/{helpId}.md";
}
private HelpDocumentResult ParseMarkdownFile(string helpId, string rawContent)
{
_logger.LogDebug("Начало парсинга структуры Markdown для HelpId: {HelpId}", helpId);
if (string.IsNullOrWhiteSpace(rawContent))
{
_logger.LogWarning("Получен пустой контент для HelpId: {HelpId}", helpId);
return new HelpDocumentResult { HelpId = helpId };
}
// Стандартное разделение строк с поддержкой разных типов окончаний (Windows/Linux)
var lines = rawContent.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
var title = string.Empty;
string? prevId = null;
string? nextId = null;
var contentStartIndex = 0;
var isInsideFrontMatter = false;
var frontMatterClosed = false;
for (int i = 0; i < lines.Length; i++)
{
var line = lines[i].Trim();
// Ищем маркеры начала и конца метаданных "---"
if (line == "---")
{
if (!isInsideFrontMatter && !frontMatterClosed)
{
isInsideFrontMatter = true;
_logger.LogDebug("Обнаружено начало блока метаданных (Front Matter) на строке {LineIndex}", i);
continue;
}
else if (isInsideFrontMatter)
{
isInsideFrontMatter = false;
frontMatterClosed = true;
contentStartIndex = i + 1; // Контент начинается строго со следующей строки
_logger.LogDebug("Обнаружен конец блока метаданных на строке {LineIndex}", i);
break;
}
}
// Читаем свойства внутри блока метаданных
if (isInsideFrontMatter)
{
if (line.StartsWith("title:", StringComparison.OrdinalIgnoreCase))
{
title = line.Substring("title:".Length).Trim();
_logger.LogDebug("В метаданных найден Title: '{Title}'", title);
}
// Парсим Предыдущий шаг
else if (line.StartsWith("prev_id:", StringComparison.OrdinalIgnoreCase))
{
prevId = line.Substring("prev_id:".Length).Trim();
_logger.LogDebug("В метаданных найден PrevId: '{PrevId}'", prevId);
}
// Парсим Следующий шаг
else if (line.StartsWith("next_id:", StringComparison.OrdinalIgnoreCase))
{
nextId = line.Substring("next_id:".Length).Trim();
_logger.LogDebug("В метаданных найден NextId: '{NextId}'", nextId);
}
// Тут в будущем можно парсить другие поля, например: icon, min_role и т.д.
}
}
string cleanContent;
if (frontMatterClosed)
{
// Собираем обратно чистый контент, исключая строки с шапкой
cleanContent = string.Join(Environment.NewLine, lines.Skip(contentStartIndex));
}
else
{
// Если шапки "---" вообще не было в файле, берем весь текст как есть
_logger.LogDebug("Блок метаданных '---' не найден в файле. Весь текст перенесен в Content.");
cleanContent = rawContent;
title = Path.GetFileNameWithoutExtension(helpId); // Дефолтный заголовок из имени файла
}
return new HelpDocumentResult
{
HelpId = helpId,
Title = title,
Content = cleanContent,
PrevId = prevId,
NextId = nextId
};
}
}
}