Files
mindforge/Mindforge.API/Services/GiteaService.cs
2026-03-26 19:36:25 -03:00

166 lines
6.2 KiB
C#

using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Configuration;
using Mindforge.API.Exceptions;
using Mindforge.API.Models;
using Mindforge.API.Services.Interfaces;
namespace Mindforge.API.Services
{
public class GiteaService : IGiteaService
{
private readonly HttpClient _httpClient;
private readonly string _baseUrl;
private readonly string _owner;
private readonly string _repo;
private readonly string _token;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
public GiteaService(HttpClient httpClient, IConfiguration configuration)
{
_httpClient = httpClient;
var repoUrl = configuration["GITEA_REPO_URL"];
if (string.IsNullOrEmpty(repoUrl))
throw new InvalidOperationException("GITEA_REPO_URL is not set in configuration.");
_token = configuration["GITEA_ACCESS_TOKEN"]
?? throw new InvalidOperationException("GITEA_ACCESS_TOKEN is not set in configuration.");
// Parse: https://host/owner/repo or https://host/owner/repo.git
var normalizedUrl = repoUrl.TrimEnd('/').TrimEnd(".git".ToCharArray());
var uri = new Uri(normalizedUrl);
_baseUrl = $"{uri.Scheme}://{uri.Host}{(uri.IsDefaultPort ? "" : $":{uri.Port}")}";
var parts = uri.AbsolutePath.Trim('/').Split('/');
_owner = parts[0];
_repo = parts[1];
}
public string GetRepositoryName() => _repo;
public async Task<List<FileTreeNode>> GetFileTreeAsync()
{
// Get the master branch to obtain the latest commit SHA
var branchJson = await GetApiAsync($"/api/v1/repos/{_owner}/{_repo}/branches/master");
var branch = JsonSerializer.Deserialize<GiteaBranch>(branchJson, JsonOptions)
?? throw new InvalidOperationException("Failed to parse branch response from Gitea.");
var treeSha = branch.Commit.Id;
// Fetch the full recursive tree
var treeJson = await GetApiAsync($"/api/v1/repos/{_owner}/{_repo}/git/trees/{treeSha}?recursive=true");
var treeResponse = JsonSerializer.Deserialize<GiteaTreeResponse>(treeJson, JsonOptions)
?? throw new InvalidOperationException("Failed to parse tree response from Gitea.");
return BuildTree(treeResponse.Tree);
}
public async Task<string> GetFileContentAsync(string path)
{
var request = new HttpRequestMessage(HttpMethod.Get,
$"{_baseUrl}/api/v1/repos/{_owner}/{_repo}/raw/{path}?ref=master");
request.Headers.Add("Authorization", $"token {_token}");
var response = await _httpClient.SendAsync(request);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
throw new UserException($"File not found in repository: {path}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
private async Task<string> GetApiAsync(string endpoint)
{
var request = new HttpRequestMessage(HttpMethod.Get, $"{_baseUrl}{endpoint}");
request.Headers.Add("Authorization", $"token {_token}");
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
private static List<FileTreeNode> BuildTree(List<GiteaTreeItem> items)
{
var root = new List<FileTreeNode>();
var folderMap = new Dictionary<string, FileTreeNode>();
// Only include .md files and their parent folders
var mdFiles = items.Where(i => i.Type == "blob" && i.Path.EndsWith(".md")).ToList();
var neededFolders = new HashSet<string>();
foreach (var file in mdFiles)
{
var dir = System.IO.Path.GetDirectoryName(file.Path)?.Replace('\\', '/');
while (!string.IsNullOrEmpty(dir))
{
neededFolders.Add(dir);
dir = System.IO.Path.GetDirectoryName(dir)?.Replace('\\', '/');
}
}
// Create folder nodes
foreach (var item in items.Where(i => i.Type == "tree" && neededFolders.Contains(i.Path)).OrderBy(i => i.Path))
{
var node = new FileTreeNode
{
Name = System.IO.Path.GetFileName(item.Path),
Path = item.Path,
Type = "folder",
Children = []
};
folderMap[item.Path] = node;
}
// Place file and folder nodes into parent
var allNodes = mdFiles.Select(i => new FileTreeNode
{
Name = System.IO.Path.GetFileName(i.Path),
Path = i.Path,
Type = "file"
}).Concat(folderMap.Values).OrderBy(n => n.Path);
foreach (var node in allNodes)
{
var parentPath = System.IO.Path.GetDirectoryName(node.Path)?.Replace('\\', '/');
if (string.IsNullOrEmpty(parentPath))
root.Add(node);
else if (folderMap.TryGetValue(parentPath, out var parent))
parent.Children!.Add(node);
}
return root;
}
// ---- Gitea API DTOs ----
private class GiteaBranch
{
[JsonPropertyName("commit")]
public GiteaCommit Commit { get; set; } = new();
}
private class GiteaCommit
{
[JsonPropertyName("id")]
public string Id { get; set; } = "";
}
private class GiteaTreeResponse
{
[JsonPropertyName("tree")]
public List<GiteaTreeItem> Tree { get; set; } = [];
}
private class GiteaTreeItem
{
[JsonPropertyName("path")]
public string Path { get; set; } = "";
[JsonPropertyName("type")]
public string Type { get; set; } = ""; // "blob" | "tree"
}
}
}