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>
269 lines
9.3 KiB
C#
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; }
|
|
}
|