Files
gitea/sdk/csharp/Gitea.SDK/GiteaClient.cs
logikonline e35aa8d878 sdk: add C# and Java SDK libraries with chunked upload support
Both SDKs provide:
- Full API client for users, repositories, and releases
- Chunked upload with parallel workers
- Progress tracking with speed/ETA
- SHA256 checksum verification
- Comprehensive exception handling

C# SDK (.NET 8.0):
- Modern record types for models
- Async/await pattern throughout
- System.Text.Json serialization

Java SDK (Java 17):
- Standard Maven project
- Jackson for JSON
- HttpClient for HTTP
- ExecutorService for parallel uploads

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 12:19:17 -05:00

269 lines
9.3 KiB
C#

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Gitea.SDK;
/// <summary>
/// Client for the Gitea API.
/// </summary>
public class GiteaClient : IDisposable
{
private readonly HttpClient _httpClient;
private readonly string _baseUrl;
private readonly JsonSerializerOptions _jsonOptions;
private bool _disposed;
/// <summary>
/// Creates a new Gitea API client.
/// </summary>
/// <param name="baseUrl">Base URL of the Gitea instance</param>
/// <param name="token">API token for authentication</param>
/// <param name="httpClient">Optional HttpClient instance</param>
public GiteaClient(string baseUrl, string? token = null, HttpClient? httpClient = null)
{
_baseUrl = baseUrl.TrimEnd('/');
_httpClient = httpClient ?? new HttpClient();
_httpClient.DefaultRequestHeaders.UserAgent.Add(
new ProductInfoHeaderValue("Gitea.SDK", "1.0.0"));
if (!string.IsNullOrEmpty(token))
{
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("token", token);
}
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
}
#region User Methods
/// <summary>
/// Gets the currently authenticated user.
/// </summary>
public async Task<User> GetCurrentUserAsync(CancellationToken cancellationToken = default)
{
return await GetAsync<User>("/api/v1/user", cancellationToken);
}
/// <summary>
/// Gets a user by username.
/// </summary>
public async Task<User> GetUserAsync(string username, CancellationToken cancellationToken = default)
{
return await GetAsync<User>($"/api/v1/users/{Uri.EscapeDataString(username)}", cancellationToken);
}
#endregion
#region Repository Methods
/// <summary>
/// Gets a repository by owner and name.
/// </summary>
public async Task<Repository> GetRepositoryAsync(string owner, string repo, CancellationToken cancellationToken = default)
{
return await GetAsync<Repository>(
$"/api/v1/repos/{Uri.EscapeDataString(owner)}/{Uri.EscapeDataString(repo)}",
cancellationToken);
}
/// <summary>
/// Lists repositories for a user.
/// </summary>
public async Task<List<Repository>> ListUserRepositoriesAsync(string username, CancellationToken cancellationToken = default)
{
return await GetAsync<List<Repository>>(
$"/api/v1/users/{Uri.EscapeDataString(username)}/repos",
cancellationToken);
}
#endregion
#region Release Methods
/// <summary>
/// Gets a release by ID.
/// </summary>
public async Task<Release> GetReleaseAsync(string owner, string repo, long releaseId, CancellationToken cancellationToken = default)
{
return await GetAsync<Release>(
$"/api/v1/repos/{Uri.EscapeDataString(owner)}/{Uri.EscapeDataString(repo)}/releases/{releaseId}",
cancellationToken);
}
/// <summary>
/// Gets a release by tag name.
/// </summary>
public async Task<Release> GetReleaseByTagAsync(string owner, string repo, string tag, CancellationToken cancellationToken = default)
{
return await GetAsync<Release>(
$"/api/v1/repos/{Uri.EscapeDataString(owner)}/{Uri.EscapeDataString(repo)}/releases/tags/{Uri.EscapeDataString(tag)}",
cancellationToken);
}
/// <summary>
/// Lists all releases for a repository.
/// </summary>
public async Task<List<Release>> ListReleasesAsync(string owner, string repo, CancellationToken cancellationToken = default)
{
return await GetAsync<List<Release>>(
$"/api/v1/repos/{Uri.EscapeDataString(owner)}/{Uri.EscapeDataString(repo)}/releases",
cancellationToken);
}
#endregion
#region Upload Methods
/// <summary>
/// Uploads a release asset using chunked upload.
/// </summary>
public async Task<UploadResult> UploadReleaseAssetAsync(
string owner,
string repo,
long releaseId,
Stream fileStream,
string filename,
ChunkedUploadOptions? options = null,
CancellationToken cancellationToken = default)
{
var upload = new ChunkedUpload(this, owner, repo, releaseId, options ?? new ChunkedUploadOptions());
return await upload.UploadAsync(fileStream, filename, cancellationToken);
}
/// <summary>
/// Creates a chunked upload handler.
/// </summary>
public ChunkedUpload CreateChunkedUpload(
string owner,
string repo,
long releaseId,
ChunkedUploadOptions? options = null)
{
return new ChunkedUpload(this, owner, repo, releaseId, options ?? new ChunkedUploadOptions());
}
/// <summary>
/// Gets the status of an upload session.
/// </summary>
public async Task<UploadSession> GetUploadSessionAsync(string sessionId, CancellationToken cancellationToken = default)
{
return await GetAsync<UploadSession>($"/api/v1/repos/uploads/{sessionId}", cancellationToken);
}
/// <summary>
/// Cancels an upload session.
/// </summary>
public async Task CancelUploadAsync(string sessionId, CancellationToken cancellationToken = default)
{
await DeleteAsync($"/api/v1/repos/uploads/{sessionId}", cancellationToken);
}
#endregion
#region Internal HTTP Methods
internal async Task<T> GetAsync<T>(string path, CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync(_baseUrl + path, cancellationToken);
await EnsureSuccessAsync(response, cancellationToken);
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, cancellationToken)
?? throw new GiteaException("Empty response");
}
internal async Task<T> PostAsync<T>(string path, object? body, CancellationToken cancellationToken)
{
var response = await _httpClient.PostAsJsonAsync(_baseUrl + path, body, _jsonOptions, cancellationToken);
await EnsureSuccessAsync(response, cancellationToken);
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, cancellationToken)
?? throw new GiteaException("Empty response");
}
internal async Task PostAsync(string path, object? body, CancellationToken cancellationToken)
{
var response = await _httpClient.PostAsJsonAsync(_baseUrl + path, body, _jsonOptions, cancellationToken);
await EnsureSuccessAsync(response, cancellationToken);
}
internal async Task<T> PutBinaryAsync<T>(string path, byte[] data, CancellationToken cancellationToken)
{
var content = new ByteArrayContent(data);
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
var response = await _httpClient.PutAsync(_baseUrl + path, content, cancellationToken);
await EnsureSuccessAsync(response, cancellationToken);
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, cancellationToken)
?? throw new GiteaException("Empty response");
}
internal async Task PutBinaryAsync(string path, byte[] data, CancellationToken cancellationToken)
{
var content = new ByteArrayContent(data);
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
var response = await _httpClient.PutAsync(_baseUrl + path, content, cancellationToken);
await EnsureSuccessAsync(response, cancellationToken);
}
internal async Task DeleteAsync(string path, CancellationToken cancellationToken)
{
var response = await _httpClient.DeleteAsync(_baseUrl + path, cancellationToken);
await EnsureSuccessAsync(response, cancellationToken);
}
private async Task EnsureSuccessAsync(HttpResponseMessage response, CancellationToken cancellationToken)
{
if (response.IsSuccessStatusCode)
return;
var content = await response.Content.ReadAsStringAsync(cancellationToken);
switch ((int)response.StatusCode)
{
case 401:
throw new AuthenticationException("Authentication failed");
case 404:
throw new NotFoundException("Resource not found");
default:
try
{
var error = JsonSerializer.Deserialize<ApiErrorResponse>(content, _jsonOptions);
throw new ApiException(
error?.Message ?? content,
error?.Code,
(int)response.StatusCode);
}
catch (JsonException)
{
throw new ApiException(content, null, (int)response.StatusCode);
}
}
}
#endregion
public void Dispose()
{
if (!_disposed)
{
_httpClient.Dispose();
_disposed = true;
}
GC.SuppressFinalize(this);
}
}
internal class ApiErrorResponse
{
public string? Code { get; set; }
public string? Message { get; set; }
public int? Status { get; set; }
}