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>
This commit is contained in:
David H. Friedel Jr. 2026-01-09 12:19:17 -05:00
parent ad82306b52
commit e35aa8d878
25 changed files with 2521 additions and 0 deletions

View File

@ -0,0 +1,172 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
using System.Diagnostics;
using System.Security.Cryptography;
namespace Gitea.SDK;
/// <summary>
/// Handles chunked file uploads with parallel workers.
/// </summary>
public class ChunkedUpload
{
private readonly GiteaClient _client;
private readonly string _owner;
private readonly string _repo;
private readonly long _releaseId;
private readonly ChunkedUploadOptions _options;
private UploadSession? _session;
internal ChunkedUpload(
GiteaClient client,
string owner,
string repo,
long releaseId,
ChunkedUploadOptions options)
{
_client = client;
_owner = owner;
_repo = repo;
_releaseId = releaseId;
_options = options;
}
/// <summary>
/// Gets the current upload session.
/// </summary>
public UploadSession? Session => _session;
/// <summary>
/// Uploads a file using chunked upload.
/// </summary>
public async Task<UploadResult> UploadAsync(
Stream fileStream,
string filename,
CancellationToken cancellationToken = default)
{
var fileSize = fileStream.Length;
var chunkSize = _options.ChunkSize;
var totalChunks = (int)Math.Ceiling((double)fileSize / chunkSize);
// Calculate checksum if requested
string? checksum = null;
if (_options.VerifyChecksum)
{
checksum = await ComputeChecksumAsync(fileStream, cancellationToken);
fileStream.Position = 0;
}
// Create upload session
_session = await CreateSessionAsync(filename, fileSize, chunkSize, totalChunks, checksum, cancellationToken);
// Track progress
var stopwatch = Stopwatch.StartNew();
var chunksCompleted = 0;
var bytesCompleted = 0L;
var progressLock = new object();
// Upload chunks in parallel
var semaphore = new SemaphoreSlim(_options.Parallel);
var tasks = new List<Task>();
var chunks = new List<(int Index, byte[] Data)>();
// Read all chunks
for (var i = 0; i < totalChunks; i++)
{
var offset = i * chunkSize;
var size = (int)Math.Min(chunkSize, fileSize - offset);
var buffer = new byte[size];
fileStream.Position = offset;
await fileStream.ReadAsync(buffer.AsMemory(0, size), cancellationToken);
chunks.Add((i, buffer));
}
// Upload chunks
foreach (var (index, data) in chunks)
{
await semaphore.WaitAsync(cancellationToken);
var task = Task.Run(async () =>
{
try
{
await UploadChunkAsync(_session.Id, index, data, cancellationToken);
lock (progressLock)
{
chunksCompleted++;
bytesCompleted += data.Length;
var elapsed = stopwatch.Elapsed.TotalSeconds;
var speed = elapsed > 0 ? bytesCompleted / elapsed : 0;
var remaining = fileSize - bytesCompleted;
var eta = speed > 0 ? TimeSpan.FromSeconds(remaining / speed) : TimeSpan.Zero;
_options.OnProgress?.Invoke(new Progress
{
BytesDone = bytesCompleted,
BytesTotal = fileSize,
ChunksDone = chunksCompleted,
ChunksTotal = totalChunks,
Percent = (double)bytesCompleted / fileSize * 100,
Speed = speed,
Eta = eta
});
}
}
finally
{
semaphore.Release();
}
}, cancellationToken);
tasks.Add(task);
}
await Task.WhenAll(tasks);
// Complete the upload
return await CompleteUploadAsync(_session.Id, cancellationToken);
}
private async Task<string> ComputeChecksumAsync(Stream stream, CancellationToken cancellationToken)
{
using var sha256 = SHA256.Create();
var hash = await sha256.ComputeHashAsync(stream, cancellationToken);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private async Task<UploadSession> CreateSessionAsync(
string filename,
long fileSize,
long chunkSize,
int totalChunks,
string? checksum,
CancellationToken cancellationToken)
{
var path = $"/api/v1/repos/{Uri.EscapeDataString(_owner)}/{Uri.EscapeDataString(_repo)}/releases/{_releaseId}/assets/upload";
var body = new
{
filename,
file_size = fileSize,
chunk_size = chunkSize,
total_chunks = totalChunks,
checksum
};
return await _client.PostAsync<UploadSession>(path, body, cancellationToken);
}
private async Task UploadChunkAsync(string sessionId, int chunkIndex, byte[] data, CancellationToken cancellationToken)
{
var path = $"/api/v1/repos/uploads/{Uri.EscapeDataString(sessionId)}/chunks/{chunkIndex}";
await _client.PutBinaryAsync(path, data, cancellationToken);
}
private async Task<UploadResult> CompleteUploadAsync(string sessionId, CancellationToken cancellationToken)
{
var path = $"/api/v1/repos/uploads/{Uri.EscapeDataString(sessionId)}/complete";
return await _client.PostAsync<UploadResult>(path, null, cancellationToken);
}
}

View File

@ -0,0 +1,84 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
namespace Gitea.SDK;
/// <summary>
/// Base exception for all Gitea SDK errors.
/// </summary>
public class GiteaException : Exception
{
public GiteaException(string message) : base(message) { }
public GiteaException(string message, Exception inner) : base(message, inner) { }
}
/// <summary>
/// Thrown when authentication fails (401).
/// </summary>
public class AuthenticationException : GiteaException
{
public AuthenticationException(string message) : base(message) { }
}
/// <summary>
/// Thrown when a resource is not found (404).
/// </summary>
public class NotFoundException : GiteaException
{
public NotFoundException(string message) : base(message) { }
}
/// <summary>
/// Thrown when the API returns an error response.
/// </summary>
public class ApiException : GiteaException
{
public string? Code { get; }
public int StatusCode { get; }
public ApiException(string message, string? code, int statusCode)
: base(message)
{
Code = code;
StatusCode = statusCode;
}
}
/// <summary>
/// Thrown when an upload operation fails.
/// </summary>
public class UploadException : GiteaException
{
public string? SessionId { get; }
public int? ChunkNumber { get; }
public UploadException(string message, string? sessionId = null, int? chunkNumber = null)
: base(message)
{
SessionId = sessionId;
ChunkNumber = chunkNumber;
}
public UploadException(string message, Exception inner, string? sessionId = null, int? chunkNumber = null)
: base(message, inner)
{
SessionId = sessionId;
ChunkNumber = chunkNumber;
}
}
/// <summary>
/// Thrown when checksum verification fails.
/// </summary>
public class ChecksumException : UploadException
{
public string? Expected { get; }
public string? Actual { get; }
public ChecksumException(string message, string? expected = null, string? actual = null, string? sessionId = null)
: base(message, sessionId)
{
Expected = expected;
Actual = actual;
}
}

View File

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<!-- Package metadata -->
<PackageId>Gitea.SDK</PackageId>
<Version>1.0.0</Version>
<Authors>The Gitea Authors</Authors>
<Company>Gitea</Company>
<Description>Official .NET SDK for the Gitea API with chunked upload support for large files</Description>
<PackageTags>gitea;git;api;sdk;client;upload;chunked</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://docs.gitea.io/sdk/csharp</PackageProjectUrl>
<RepositoryUrl>https://github.com/go-gitea/gitea.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<!-- Build settings -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,268 @@
// 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; }
}

View File

@ -0,0 +1,161 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
using System.Text.Json.Serialization;
namespace Gitea.SDK;
/// <summary>
/// Represents a Gitea user.
/// </summary>
public record User
{
public long Id { get; init; }
public string Login { get; init; } = string.Empty;
public string FullName { get; init; } = string.Empty;
public string Email { get; init; } = string.Empty;
public string AvatarUrl { get; init; } = string.Empty;
public bool IsAdmin { get; init; }
}
/// <summary>
/// Represents a Gitea repository.
/// </summary>
public record Repository
{
public long Id { get; init; }
public string Name { get; init; } = string.Empty;
public string FullName { get; init; } = string.Empty;
public User? Owner { get; init; }
public string Description { get; init; } = string.Empty;
public bool Private { get; init; }
public bool Fork { get; init; }
public string DefaultBranch { get; init; } = "main";
public int StarsCount { get; init; }
public int ForksCount { get; init; }
public string CloneUrl { get; init; } = string.Empty;
public string HtmlUrl { get; init; } = string.Empty;
}
/// <summary>
/// Represents a release attachment/asset.
/// </summary>
public record Attachment
{
public long Id { get; init; }
public string Name { get; init; } = string.Empty;
public long Size { get; init; }
public long DownloadCount { get; init; }
public string BrowserDownloadUrl { get; init; } = string.Empty;
public DateTime CreatedAt { get; init; }
}
/// <summary>
/// Represents a Gitea release.
/// </summary>
public record Release
{
public long Id { get; init; }
public string TagName { get; init; } = string.Empty;
public string Name { get; init; } = string.Empty;
public string Body { get; init; } = string.Empty;
public bool Draft { get; init; }
public bool Prerelease { get; init; }
public DateTime PublishedAt { get; init; }
public List<Attachment> Assets { get; init; } = new();
}
/// <summary>
/// Represents a chunked upload session.
/// </summary>
public record UploadSession
{
public string Id { get; init; } = string.Empty;
public string FileName { get; init; } = string.Empty;
public long FileSize { get; init; }
public long ChunkSize { get; init; }
public long TotalChunks { get; init; }
public long ChunksReceived { get; init; }
public string Status { get; init; } = "pending";
public DateTime ExpiresAt { get; init; }
public string? Checksum { get; init; }
}
/// <summary>
/// Represents the result of a completed upload.
/// </summary>
public record UploadResult
{
public long Id { get; init; }
public string Name { get; init; } = string.Empty;
public long Size { get; init; }
public string BrowserDownloadUrl { get; init; } = string.Empty;
public bool ChecksumVerified { get; init; }
}
/// <summary>
/// Represents upload progress.
/// </summary>
public record Progress
{
/// <summary>Bytes uploaded so far.</summary>
public long BytesDone { get; init; }
/// <summary>Total bytes to upload.</summary>
public long BytesTotal { get; init; }
/// <summary>Number of chunks uploaded.</summary>
public long ChunksDone { get; init; }
/// <summary>Total number of chunks.</summary>
public long ChunksTotal { get; init; }
/// <summary>Percentage complete (0-100).</summary>
public double Percent { get; init; }
/// <summary>Upload speed in bytes per second.</summary>
public double Speed { get; init; }
/// <summary>Estimated time remaining.</summary>
public TimeSpan Eta { get; init; }
/// <summary>
/// Formats the speed as a human-readable string.
/// </summary>
public string SpeedFormatted => FormatBytes((long)Speed) + "/s";
/// <summary>
/// Formats the ETA as a human-readable string.
/// </summary>
public string EtaFormatted => Eta.TotalSeconds < 60
? $"{(int)Eta.TotalSeconds}s"
: Eta.TotalMinutes < 60
? $"{(int)Eta.TotalMinutes}m{Eta.Seconds}s"
: $"{(int)Eta.TotalHours}h{Eta.Minutes}m";
private static string FormatBytes(long bytes)
{
if (bytes < 1024) return $"{bytes} B";
if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB";
if (bytes < 1024 * 1024 * 1024) return $"{bytes / 1024.0 / 1024.0:F1} MB";
return $"{bytes / 1024.0 / 1024.0 / 1024.0:F1} GB";
}
}
/// <summary>
/// Options for chunked uploads.
/// </summary>
public class ChunkedUploadOptions
{
/// <summary>Size of each chunk in bytes (default: 10MB).</summary>
public long ChunkSize { get; set; } = 10 * 1024 * 1024;
/// <summary>Number of parallel upload workers (default: 4).</summary>
public int Parallel { get; set; } = 4;
/// <summary>Whether to verify file checksum (default: true).</summary>
public bool VerifyChecksum { get; set; } = true;
/// <summary>Progress callback.</summary>
public Action<Progress>? OnProgress { get; set; }
}

165
sdk/csharp/README.md Normal file
View File

@ -0,0 +1,165 @@
# Gitea .NET SDK
Official .NET SDK for the Gitea API with chunked upload support for large files.
## Installation
```bash
dotnet add package Gitea.SDK
```
## Quick Start
```csharp
using Gitea.SDK;
// Create client
var client = new GiteaClient("https://gitea.example.com", "your_token");
// Get current user
var user = await client.GetCurrentUserAsync();
Console.WriteLine($"Logged in as {user.Login}");
// Get a repository
var repo = await client.GetRepositoryAsync("owner", "repo");
Console.WriteLine($"Repository: {repo.FullName}");
```
## Chunked Upload
Upload large files with progress tracking:
```csharp
using Gitea.SDK;
var client = new GiteaClient("https://gitea.example.com", "your_token");
// Upload a release asset with progress
await using var fileStream = File.OpenRead("large-file.tar.gz");
var result = await client.UploadReleaseAssetAsync(
owner: "myorg",
repo: "myrepo",
releaseId: 123,
fileStream: fileStream,
filename: "large-file.tar.gz",
options: new ChunkedUploadOptions
{
ChunkSize = 50 * 1024 * 1024, // 50MB chunks
Parallel = 4,
VerifyChecksum = true,
OnProgress = p =>
{
Console.WriteLine($"Progress: {p.Percent:F1}%");
Console.WriteLine($"Speed: {p.SpeedFormatted}");
Console.WriteLine($"ETA: {p.EtaFormatted}");
}
});
Console.WriteLine($"Uploaded: {result.BrowserDownloadUrl}");
```
## Using ChunkedUpload Directly
For more control over the upload process:
```csharp
using Gitea.SDK;
var client = new GiteaClient("https://gitea.example.com", "your_token");
var upload = client.CreateChunkedUpload("owner", "repo", 123, new ChunkedUploadOptions
{
ChunkSize = 50 * 1024 * 1024,
Parallel = 4,
OnProgress = p => Console.WriteLine($"{p.Percent:F1}%")
});
try
{
await using var stream = File.OpenRead("file.tar.gz");
var result = await upload.UploadAsync(stream, "file.tar.gz");
Console.WriteLine($"Success: {result.BrowserDownloadUrl}");
}
catch (UploadException ex)
{
// Upload failed, can retry later
Console.WriteLine($"Resume with session: {upload.Session?.Id}");
}
```
## API Reference
### GiteaClient
#### Constructor
```csharp
var client = new GiteaClient(
baseUrl: "https://gitea.example.com",
token: "your_api_token",
httpClient: null // Optional custom HttpClient
);
```
#### User Methods
- `GetCurrentUserAsync()` - Get authenticated user
- `GetUserAsync(username)` - Get user by username
#### Repository Methods
- `GetRepositoryAsync(owner, repo)` - Get repository
- `ListUserRepositoriesAsync(username)` - List user's repositories
#### Release Methods
- `GetReleaseAsync(owner, repo, releaseId)` - Get release by ID
- `GetReleaseByTagAsync(owner, repo, tag)` - Get release by tag
- `ListReleasesAsync(owner, repo)` - List all releases
#### Upload Methods
- `UploadReleaseAssetAsync(...)` - Upload release asset with chunked upload
- `CreateChunkedUpload(owner, repo, releaseId, options)` - Create upload handler
- `GetUploadSessionAsync(sessionId)` - Get upload session status
- `CancelUploadAsync(sessionId)` - Cancel upload session
## Error Handling
```csharp
using Gitea.SDK;
var client = new GiteaClient("https://gitea.example.com", "your_token");
try
{
var repo = await client.GetRepositoryAsync("owner", "nonexistent");
}
catch (NotFoundException ex)
{
Console.WriteLine($"Repository not found: {ex.Message}");
}
catch (AuthenticationException ex)
{
Console.WriteLine($"Authentication failed: {ex.Message}");
}
catch (UploadException ex)
{
Console.WriteLine($"Upload failed: {ex.Message}");
Console.WriteLine($"Session: {ex.SessionId}, Chunk: {ex.ChunkNumber}");
}
catch (ApiException ex)
{
Console.WriteLine($"API error [{ex.Code}]: {ex.Message}");
}
```
## Requirements
- .NET 8.0 or later
- System.Text.Json for JSON serialization
## License
MIT License - See LICENSE file for details.

182
sdk/java/README.md Normal file
View File

@ -0,0 +1,182 @@
# Gitea Java SDK
Official Java SDK for the Gitea API with chunked upload support for large files.
## Requirements
- Java 17 or later
- Maven or Gradle
## Installation
### Maven
```xml
<dependency>
<groupId>io.gitea</groupId>
<artifactId>gitea-sdk</artifactId>
<version>1.0.0</version>
</dependency>
```
### Gradle
```groovy
implementation 'io.gitea:gitea-sdk:1.0.0'
```
## Quick Start
```java
import io.gitea.sdk.GiteaClient;
import io.gitea.sdk.models.*;
// Create client
var client = new GiteaClient("https://gitea.example.com", "your_token");
// Get current user
var user = client.getCurrentUser();
System.out.println("Logged in as " + user.getLogin());
// Get a repository
var repo = client.getRepository("owner", "repo");
System.out.println("Repository: " + repo.getFullName());
```
## Chunked Upload
Upload large files with progress tracking:
```java
import io.gitea.sdk.GiteaClient;
import io.gitea.sdk.models.*;
import java.io.FileInputStream;
var client = new GiteaClient("https://gitea.example.com", "your_token");
// Upload a release asset with progress
try (var fileStream = new FileInputStream("large-file.tar.gz")) {
var fileSize = new java.io.File("large-file.tar.gz").length();
var result = client.uploadReleaseAsset(
"myorg",
"myrepo",
123L,
fileStream,
"large-file.tar.gz",
fileSize,
new ChunkedUploadOptions()
.setChunkSize(50 * 1024 * 1024) // 50MB chunks
.setParallel(4)
.setVerifyChecksum(true)
.setOnProgress(p -> {
System.out.printf("Progress: %.1f%%%n", p.getPercent());
System.out.println("Speed: " + p.getSpeedFormatted());
System.out.println("ETA: " + p.getEtaFormatted());
})
);
System.out.println("Uploaded: " + result.getBrowserDownloadUrl());
}
```
## Using ChunkedUpload Directly
For more control over the upload process:
```java
import io.gitea.sdk.GiteaClient;
import io.gitea.sdk.ChunkedUpload;
import io.gitea.sdk.models.*;
var client = new GiteaClient("https://gitea.example.com", "your_token");
var upload = client.createChunkedUpload("owner", "repo", 123L,
new ChunkedUploadOptions()
.setChunkSize(50 * 1024 * 1024)
.setParallel(4)
.setOnProgress(p -> System.out.printf("%.1f%%%n", p.getPercent()))
);
try (var stream = new FileInputStream("file.tar.gz")) {
var fileSize = new java.io.File("file.tar.gz").length();
var result = upload.upload(stream, "file.tar.gz", fileSize);
System.out.println("Success: " + result.getBrowserDownloadUrl());
} catch (UploadException e) {
// Upload failed, can retry later
System.out.println("Resume with session: " + upload.getSession().getId());
}
```
## API Reference
### GiteaClient
#### Constructor
```java
// With token
var client = new GiteaClient("https://gitea.example.com", "your_api_token");
// With custom HttpClient
var httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(60))
.build();
var client = new GiteaClient("https://gitea.example.com", "your_api_token", httpClient);
```
#### User Methods
- `getCurrentUser()` - Get authenticated user
- `getUser(username)` - Get user by username
#### Repository Methods
- `getRepository(owner, repo)` - Get repository
- `listUserRepositories(username)` - List user's repositories
#### Release Methods
- `getRelease(owner, repo, releaseId)` - Get release by ID
- `getReleaseByTag(owner, repo, tag)` - Get release by tag
- `listReleases(owner, repo)` - List all releases
#### Upload Methods
- `uploadReleaseAsset(...)` - Upload release asset with chunked upload
- `createChunkedUpload(owner, repo, releaseId, options)` - Create upload handler
- `getUploadSession(sessionId)` - Get upload session status
- `cancelUpload(sessionId)` - Cancel upload session
## Error Handling
```java
import io.gitea.sdk.GiteaClient;
import io.gitea.sdk.exceptions.*;
var client = new GiteaClient("https://gitea.example.com", "your_token");
try {
var repo = client.getRepository("owner", "nonexistent");
} catch (NotFoundException e) {
System.out.println("Repository not found: " + e.getMessage());
} catch (AuthenticationException e) {
System.out.println("Authentication failed: " + e.getMessage());
} catch (UploadException e) {
System.out.println("Upload failed: " + e.getMessage());
System.out.println("Session: " + e.getSessionId() + ", Chunk: " + e.getChunkNumber());
} catch (ApiException e) {
System.out.println("API error [" + e.getCode() + "]: " + e.getMessage());
} catch (GiteaException e) {
System.out.println("Error: " + e.getMessage());
}
```
## Thread Safety
The `GiteaClient` is thread-safe and can be shared across multiple threads. Each `ChunkedUpload` instance should be used by a single thread at a time.
## License
MIT License - See LICENSE file for details.

111
sdk/java/pom.xml Normal file
View File

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.gitea</groupId>
<artifactId>gitea-sdk</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>Gitea SDK</name>
<description>Official Java SDK for the Gitea API with chunked upload support for large files</description>
<url>https://docs.gitea.io/sdk/java</url>
<licenses>
<license>
<name>MIT License</name>
<url>https://opensource.org/licenses/MIT</url>
</license>
</licenses>
<developers>
<developer>
<name>The Gitea Authors</name>
<organization>Gitea</organization>
<organizationUrl>https://gitea.io</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/go-gitea/gitea.git</connection>
<developerConnection>scm:git:ssh://github.com:go-gitea/gitea.git</developerConnection>
<url>https://github.com/go-gitea/gitea/tree/main/sdk/java</url>
</scm>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jackson.version>2.16.0</jackson.version>
</properties>
<dependencies>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.8.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.6.2</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,195 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package io.gitea.sdk;
import io.gitea.sdk.exceptions.GiteaException;
import io.gitea.sdk.exceptions.UploadException;
import io.gitea.sdk.models.*;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
/**
* Handles chunked file uploads with parallel workers.
*/
public class ChunkedUpload {
private final GiteaClient client;
private final String owner;
private final String repo;
private final long releaseId;
private final ChunkedUploadOptions options;
private UploadSession session;
ChunkedUpload(
GiteaClient client,
String owner,
String repo,
long releaseId,
ChunkedUploadOptions options) {
this.client = client;
this.owner = owner;
this.repo = repo;
this.releaseId = releaseId;
this.options = options != null ? options : ChunkedUploadOptions.defaults();
}
/**
* Gets the current upload session.
*/
public UploadSession getSession() {
return session;
}
/**
* Uploads a file using chunked upload.
*/
public UploadResult upload(InputStream fileStream, String filename, long fileSize) throws GiteaException {
long chunkSize = options.getChunkSize();
int totalChunks = (int) Math.ceil((double) fileSize / chunkSize);
// Read all data and optionally compute checksum
byte[] fileData;
String checksum = null;
try {
if (options.isVerifyChecksum()) {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
DigestInputStream dis = new DigestInputStream(fileStream, digest);
fileData = dis.readAllBytes();
checksum = bytesToHex(digest.digest());
} else {
fileData = fileStream.readAllBytes();
}
} catch (IOException | NoSuchAlgorithmException e) {
throw new UploadException("Failed to read file", e, null, null);
}
// Create upload session
session = createSession(filename, fileSize, chunkSize, totalChunks, checksum);
// Prepare chunks
List<ChunkData> chunks = new ArrayList<>();
for (int i = 0; i < totalChunks; i++) {
long offset = i * chunkSize;
int size = (int) Math.min(chunkSize, fileSize - offset);
byte[] data = new byte[size];
System.arraycopy(fileData, (int) offset, data, 0, size);
chunks.add(new ChunkData(i, data));
}
// Track progress
long startTime = System.nanoTime();
AtomicInteger chunksCompleted = new AtomicInteger(0);
AtomicLong bytesCompleted = new AtomicLong(0);
// Upload chunks in parallel
ExecutorService executor = Executors.newFixedThreadPool(options.getParallel());
List<Future<Void>> futures = new ArrayList<>();
for (ChunkData chunk : chunks) {
futures.add(executor.submit(() -> {
uploadChunk(session.getId(), chunk.index, chunk.data);
int done = chunksCompleted.incrementAndGet();
long bytes = bytesCompleted.addAndGet(chunk.data.length);
if (options.getOnProgress() != null) {
double elapsed = (System.nanoTime() - startTime) / 1_000_000_000.0;
double speed = elapsed > 0 ? bytes / elapsed : 0;
long remaining = fileSize - bytes;
Duration eta = speed > 0 ? Duration.ofSeconds((long) (remaining / speed)) : Duration.ZERO;
options.getOnProgress().accept(new Progress(
bytes,
fileSize,
done,
totalChunks,
(double) bytes / fileSize * 100,
speed,
eta
));
}
return null;
}));
}
// Wait for all uploads
try {
for (Future<Void> future : futures) {
future.get();
}
} catch (InterruptedException | ExecutionException e) {
throw new UploadException("Upload failed", e, session.getId(), null);
} finally {
executor.shutdown();
}
// Complete the upload
return completeUpload(session.getId());
}
private UploadSession createSession(
String filename,
long fileSize,
long chunkSize,
int totalChunks,
String checksum) throws GiteaException {
String path = "/api/v1/repos/" + encode(owner) + "/" + encode(repo) + "/releases/" + releaseId + "/assets/upload";
Map<String, Object> body = new HashMap<>();
body.put("filename", filename);
body.put("file_size", fileSize);
body.put("chunk_size", chunkSize);
body.put("total_chunks", totalChunks);
if (checksum != null) {
body.put("checksum", checksum);
}
return client.post(path, body, UploadSession.class);
}
private void uploadChunk(String sessionId, int chunkIndex, byte[] data) throws GiteaException {
String path = "/api/v1/repos/uploads/" + encode(sessionId) + "/chunks/" + chunkIndex;
client.putBinary(path, data);
}
private UploadResult completeUpload(String sessionId) throws GiteaException {
String path = "/api/v1/repos/uploads/" + encode(sessionId) + "/complete";
return client.post(path, null, UploadResult.class);
}
private static String encode(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
private static class ChunkData {
final int index;
final byte[] data;
ChunkData(int index, byte[] data) {
this.index = index;
this.data = data;
}
}
}

View File

@ -0,0 +1,297 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package io.gitea.sdk;
import io.gitea.sdk.exceptions.*;
import io.gitea.sdk.models.*;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
/**
* Client for the Gitea API.
*/
public class GiteaClient implements AutoCloseable {
private final HttpClient httpClient;
private final String baseUrl;
private final String token;
private final ObjectMapper objectMapper;
/**
* Creates a new Gitea API client.
*
* @param baseUrl Base URL of the Gitea instance
* @param token API token for authentication
*/
public GiteaClient(String baseUrl, String token) {
this(baseUrl, token, null);
}
/**
* Creates a new Gitea API client with custom HttpClient.
*
* @param baseUrl Base URL of the Gitea instance
* @param token API token for authentication
* @param httpClient Custom HttpClient (optional)
*/
public GiteaClient(String baseUrl, String token, HttpClient httpClient) {
this.baseUrl = baseUrl.replaceAll("/+$", "");
this.token = token;
this.httpClient = httpClient != null ? httpClient : HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(30))
.build();
this.objectMapper = new ObjectMapper()
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
// User Methods
/**
* Gets the currently authenticated user.
*/
public User getCurrentUser() throws GiteaException {
return get("/api/v1/user", User.class);
}
/**
* Gets a user by username.
*/
public User getUser(String username) throws GiteaException {
return get("/api/v1/users/" + encode(username), User.class);
}
// Repository Methods
/**
* Gets a repository by owner and name.
*/
public Repository getRepository(String owner, String repo) throws GiteaException {
return get("/api/v1/repos/" + encode(owner) + "/" + encode(repo), Repository.class);
}
/**
* Lists repositories for a user.
*/
public List<Repository> listUserRepositories(String username) throws GiteaException {
return getList("/api/v1/users/" + encode(username) + "/repos", Repository.class);
}
// Release Methods
/**
* Gets a release by ID.
*/
public Release getRelease(String owner, String repo, long releaseId) throws GiteaException {
return get("/api/v1/repos/" + encode(owner) + "/" + encode(repo) + "/releases/" + releaseId, Release.class);
}
/**
* Gets a release by tag name.
*/
public Release getReleaseByTag(String owner, String repo, String tag) throws GiteaException {
return get("/api/v1/repos/" + encode(owner) + "/" + encode(repo) + "/releases/tags/" + encode(tag), Release.class);
}
/**
* Lists all releases for a repository.
*/
public List<Release> listReleases(String owner, String repo) throws GiteaException {
return getList("/api/v1/repos/" + encode(owner) + "/" + encode(repo) + "/releases", Release.class);
}
// Upload Methods
/**
* Uploads a release asset using chunked upload.
*/
public UploadResult uploadReleaseAsset(
String owner,
String repo,
long releaseId,
InputStream fileStream,
String filename,
long fileSize,
ChunkedUploadOptions options) throws GiteaException {
var upload = new ChunkedUpload(this, owner, repo, releaseId, options);
return upload.upload(fileStream, filename, fileSize);
}
/**
* Creates a chunked upload handler.
*/
public ChunkedUpload createChunkedUpload(
String owner,
String repo,
long releaseId,
ChunkedUploadOptions options) {
return new ChunkedUpload(this, owner, repo, releaseId, options);
}
/**
* Gets the status of an upload session.
*/
public UploadSession getUploadSession(String sessionId) throws GiteaException {
return get("/api/v1/repos/uploads/" + encode(sessionId), UploadSession.class);
}
/**
* Cancels an upload session.
*/
public void cancelUpload(String sessionId) throws GiteaException {
delete("/api/v1/repos/uploads/" + encode(sessionId));
}
// Internal HTTP Methods
<T> T get(String path, Class<T> type) throws GiteaException {
var request = buildRequest(path)
.GET()
.build();
return execute(request, type);
}
<T> List<T> getList(String path, Class<T> elementType) throws GiteaException {
var request = buildRequest(path)
.GET()
.build();
return executeList(request, elementType);
}
<T> T post(String path, Object body, Class<T> type) throws GiteaException {
try {
var json = body != null ? objectMapper.writeValueAsString(body) : "{}";
var request = buildRequest(path)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
return execute(request, type);
} catch (IOException e) {
throw new GiteaException("Failed to serialize request body", e);
}
}
void post(String path, Object body) throws GiteaException {
try {
var json = body != null ? objectMapper.writeValueAsString(body) : "{}";
var request = buildRequest(path)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
executeVoid(request);
} catch (IOException e) {
throw new GiteaException("Failed to serialize request body", e);
}
}
<T> T putBinary(String path, byte[] data, Class<T> type) throws GiteaException {
var request = buildRequest(path)
.header("Content-Type", "application/octet-stream")
.PUT(HttpRequest.BodyPublishers.ofByteArray(data))
.build();
return execute(request, type);
}
void putBinary(String path, byte[] data) throws GiteaException {
var request = buildRequest(path)
.header("Content-Type", "application/octet-stream")
.PUT(HttpRequest.BodyPublishers.ofByteArray(data))
.build();
executeVoid(request);
}
void delete(String path) throws GiteaException {
var request = buildRequest(path)
.DELETE()
.build();
executeVoid(request);
}
private HttpRequest.Builder buildRequest(String path) {
var builder = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + path))
.header("User-Agent", "Gitea.SDK/1.0.0");
if (token != null && !token.isEmpty()) {
builder.header("Authorization", "token " + token);
}
return builder;
}
private <T> T execute(HttpRequest request, Class<T> type) throws GiteaException {
try {
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
ensureSuccess(response);
return objectMapper.readValue(response.body(), type);
} catch (IOException | InterruptedException e) {
throw new GiteaException("Request failed", e);
}
}
private <T> List<T> executeList(HttpRequest request, Class<T> elementType) throws GiteaException {
try {
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
ensureSuccess(response);
var listType = objectMapper.getTypeFactory().constructCollectionType(List.class, elementType);
return objectMapper.readValue(response.body(), listType);
} catch (IOException | InterruptedException e) {
throw new GiteaException("Request failed", e);
}
}
private void executeVoid(HttpRequest request) throws GiteaException {
try {
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
ensureSuccess(response);
} catch (IOException | InterruptedException e) {
throw new GiteaException("Request failed", e);
}
}
private void ensureSuccess(HttpResponse<String> response) throws GiteaException {
if (response.statusCode() >= 200 && response.statusCode() < 300) {
return;
}
switch (response.statusCode()) {
case 401:
throw new AuthenticationException("Authentication failed");
case 404:
throw new NotFoundException("Resource not found");
default:
try {
var error = objectMapper.readValue(response.body(), ApiErrorResponse.class);
throw new ApiException(
error.getMessage() != null ? error.getMessage() : response.body(),
error.getCode(),
response.statusCode());
} catch (IOException e) {
throw new ApiException(response.body(), null, response.statusCode());
}
}
}
private static String encode(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}
@Override
public void close() {
// HttpClient doesn't need explicit cleanup in Java 11+
}
}

View File

@ -0,0 +1,26 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package io.gitea.sdk.exceptions;
/**
* Thrown when the API returns an error response.
*/
public class ApiException extends GiteaException {
private final String code;
private final int statusCode;
public ApiException(String message, String code, int statusCode) {
super(message);
this.code = code;
this.statusCode = statusCode;
}
public String getCode() {
return code;
}
public int getStatusCode() {
return statusCode;
}
}

View File

@ -0,0 +1,13 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package io.gitea.sdk.exceptions;
/**
* Thrown when authentication fails (401).
*/
public class AuthenticationException extends GiteaException {
public AuthenticationException(String message) {
super(message);
}
}

View File

@ -0,0 +1,26 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package io.gitea.sdk.exceptions;
/**
* Thrown when checksum verification fails.
*/
public class ChecksumException extends UploadException {
private final String expected;
private final String actual;
public ChecksumException(String message, String expected, String actual, String sessionId) {
super(message, sessionId, null);
this.expected = expected;
this.actual = actual;
}
public String getExpected() {
return expected;
}
public String getActual() {
return actual;
}
}

View File

@ -0,0 +1,17 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package io.gitea.sdk.exceptions;
/**
* Base exception for all Gitea SDK errors.
*/
public class GiteaException extends Exception {
public GiteaException(String message) {
super(message);
}
public GiteaException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,13 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package io.gitea.sdk.exceptions;
/**
* Thrown when a resource is not found (404).
*/
public class NotFoundException extends GiteaException {
public NotFoundException(String message) {
super(message);
}
}

View File

@ -0,0 +1,36 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package io.gitea.sdk.exceptions;
/**
* Thrown when an upload operation fails.
*/
public class UploadException extends GiteaException {
private final String sessionId;
private final Integer chunkNumber;
public UploadException(String message) {
this(message, null, null);
}
public UploadException(String message, String sessionId, Integer chunkNumber) {
super(message);
this.sessionId = sessionId;
this.chunkNumber = chunkNumber;
}
public UploadException(String message, Throwable cause, String sessionId, Integer chunkNumber) {
super(message, cause);
this.sessionId = sessionId;
this.chunkNumber = chunkNumber;
}
public String getSessionId() {
return sessionId;
}
public Integer getChunkNumber() {
return chunkNumber;
}
}

View File

@ -0,0 +1,37 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package io.gitea.sdk.models;
/**
* Internal class for parsing API error responses.
*/
public class ApiErrorResponse {
private String code;
private String message;
private Integer status;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
}

View File

@ -0,0 +1,66 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package io.gitea.sdk.models;
import java.time.Instant;
/**
* Represents a release attachment/asset.
*/
public class Attachment {
private long id;
private String name;
private long size;
private long downloadCount;
private String browserDownloadUrl;
private Instant createdAt;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getSize() {
return size;
}
public void setSize(long size) {
this.size = size;
}
public long getDownloadCount() {
return downloadCount;
}
public void setDownloadCount(long downloadCount) {
this.downloadCount = downloadCount;
}
public String getBrowserDownloadUrl() {
return browserDownloadUrl;
}
public void setBrowserDownloadUrl(String browserDownloadUrl) {
this.browserDownloadUrl = browserDownloadUrl;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}

View File

@ -0,0 +1,81 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package io.gitea.sdk.models;
import java.util.function.Consumer;
/**
* Options for chunked uploads.
*/
public class ChunkedUploadOptions {
private long chunkSize = 10 * 1024 * 1024; // 10MB default
private int parallel = 4;
private boolean verifyChecksum = true;
private Consumer<Progress> onProgress;
public ChunkedUploadOptions() {
}
/**
* Size of each chunk in bytes (default: 10MB).
*/
public long getChunkSize() {
return chunkSize;
}
public ChunkedUploadOptions setChunkSize(long chunkSize) {
this.chunkSize = chunkSize;
return this;
}
/**
* Number of parallel upload workers (default: 4).
*/
public int getParallel() {
return parallel;
}
public ChunkedUploadOptions setParallel(int parallel) {
this.parallel = parallel;
return this;
}
/**
* Whether to verify file checksum (default: true).
*/
public boolean isVerifyChecksum() {
return verifyChecksum;
}
public ChunkedUploadOptions setVerifyChecksum(boolean verifyChecksum) {
this.verifyChecksum = verifyChecksum;
return this;
}
/**
* Progress callback.
*/
public Consumer<Progress> getOnProgress() {
return onProgress;
}
public ChunkedUploadOptions setOnProgress(Consumer<Progress> onProgress) {
this.onProgress = onProgress;
return this;
}
/**
* Creates options with default settings.
*/
public static ChunkedUploadOptions defaults() {
return new ChunkedUploadOptions();
}
/**
* Creates options with specified chunk size.
*/
public static ChunkedUploadOptions withChunkSize(long chunkSize) {
return new ChunkedUploadOptions().setChunkSize(chunkSize);
}
}

View File

@ -0,0 +1,124 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package io.gitea.sdk.models;
import java.time.Duration;
/**
* Represents upload progress.
*/
public class Progress {
private long bytesDone;
private long bytesTotal;
private long chunksDone;
private long chunksTotal;
private double percent;
private double speed;
private Duration eta;
public Progress() {
}
public Progress(long bytesDone, long bytesTotal, long chunksDone, long chunksTotal, double percent, double speed, Duration eta) {
this.bytesDone = bytesDone;
this.bytesTotal = bytesTotal;
this.chunksDone = chunksDone;
this.chunksTotal = chunksTotal;
this.percent = percent;
this.speed = speed;
this.eta = eta;
}
public long getBytesDone() {
return bytesDone;
}
public void setBytesDone(long bytesDone) {
this.bytesDone = bytesDone;
}
public long getBytesTotal() {
return bytesTotal;
}
public void setBytesTotal(long bytesTotal) {
this.bytesTotal = bytesTotal;
}
public long getChunksDone() {
return chunksDone;
}
public void setChunksDone(long chunksDone) {
this.chunksDone = chunksDone;
}
public long getChunksTotal() {
return chunksTotal;
}
public void setChunksTotal(long chunksTotal) {
this.chunksTotal = chunksTotal;
}
public double getPercent() {
return percent;
}
public void setPercent(double percent) {
this.percent = percent;
}
public double getSpeed() {
return speed;
}
public void setSpeed(double speed) {
this.speed = speed;
}
public Duration getEta() {
return eta;
}
public void setEta(Duration eta) {
this.eta = eta;
}
/**
* Formats the speed as a human-readable string.
*/
public String getSpeedFormatted() {
return formatBytes((long) speed) + "/s";
}
/**
* Formats the ETA as a human-readable string.
*/
public String getEtaFormatted() {
if (eta == null) {
return "unknown";
}
long seconds = eta.getSeconds();
if (seconds < 60) {
return seconds + "s";
} else if (seconds < 3600) {
return (seconds / 60) + "m" + (seconds % 60) + "s";
} else {
return (seconds / 3600) + "h" + ((seconds % 3600) / 60) + "m";
}
}
private static String formatBytes(long bytes) {
if (bytes < 1024) {
return bytes + " B";
} else if (bytes < 1024 * 1024) {
return String.format("%.1f KB", bytes / 1024.0);
} else if (bytes < 1024 * 1024 * 1024) {
return String.format("%.1f MB", bytes / 1024.0 / 1024.0);
} else {
return String.format("%.1f GB", bytes / 1024.0 / 1024.0 / 1024.0);
}
}
}

View File

@ -0,0 +1,86 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package io.gitea.sdk.models;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
/**
* Represents a Gitea release.
*/
public class Release {
private long id;
private String tagName;
private String name;
private String body;
private boolean draft;
private boolean prerelease;
private Instant publishedAt;
private List<Attachment> assets = new ArrayList<>();
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getTagName() {
return tagName;
}
public void setTagName(String tagName) {
this.tagName = tagName;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public boolean isDraft() {
return draft;
}
public void setDraft(boolean draft) {
this.draft = draft;
}
public boolean isPrerelease() {
return prerelease;
}
public void setPrerelease(boolean prerelease) {
this.prerelease = prerelease;
}
public Instant getPublishedAt() {
return publishedAt;
}
public void setPublishedAt(Instant publishedAt) {
this.publishedAt = publishedAt;
}
public List<Attachment> getAssets() {
return assets;
}
public void setAssets(List<Attachment> assets) {
this.assets = assets;
}
}

View File

@ -0,0 +1,123 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package io.gitea.sdk.models;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Represents a Gitea repository.
*/
public class Repository {
private long id;
private String name;
private String fullName;
private User owner;
private String description;
@JsonProperty("private")
private boolean isPrivate;
private boolean fork;
private String defaultBranch;
private int starsCount;
private int forksCount;
private String cloneUrl;
private String htmlUrl;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public User getOwner() {
return owner;
}
public void setOwner(User owner) {
this.owner = owner;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public boolean isPrivate() {
return isPrivate;
}
public void setPrivate(boolean isPrivate) {
this.isPrivate = isPrivate;
}
public boolean isFork() {
return fork;
}
public void setFork(boolean fork) {
this.fork = fork;
}
public String getDefaultBranch() {
return defaultBranch;
}
public void setDefaultBranch(String defaultBranch) {
this.defaultBranch = defaultBranch;
}
public int getStarsCount() {
return starsCount;
}
public void setStarsCount(int starsCount) {
this.starsCount = starsCount;
}
public int getForksCount() {
return forksCount;
}
public void setForksCount(int forksCount) {
this.forksCount = forksCount;
}
public String getCloneUrl() {
return cloneUrl;
}
public void setCloneUrl(String cloneUrl) {
this.cloneUrl = cloneUrl;
}
public String getHtmlUrl() {
return htmlUrl;
}
public void setHtmlUrl(String htmlUrl) {
this.htmlUrl = htmlUrl;
}
}

View File

@ -0,0 +1,55 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package io.gitea.sdk.models;
/**
* Represents the result of a completed upload.
*/
public class UploadResult {
private long id;
private String name;
private long size;
private String browserDownloadUrl;
private boolean checksumVerified;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getSize() {
return size;
}
public void setSize(long size) {
this.size = size;
}
public String getBrowserDownloadUrl() {
return browserDownloadUrl;
}
public void setBrowserDownloadUrl(String browserDownloadUrl) {
this.browserDownloadUrl = browserDownloadUrl;
}
public boolean isChecksumVerified() {
return checksumVerified;
}
public void setChecksumVerified(boolean checksumVerified) {
this.checksumVerified = checksumVerified;
}
}

View File

@ -0,0 +1,93 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package io.gitea.sdk.models;
import java.time.Instant;
/**
* Represents a chunked upload session.
*/
public class UploadSession {
private String id;
private String fileName;
private long fileSize;
private long chunkSize;
private long totalChunks;
private long chunksReceived;
private String status = "pending";
private Instant expiresAt;
private String checksum;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public long getFileSize() {
return fileSize;
}
public void setFileSize(long fileSize) {
this.fileSize = fileSize;
}
public long getChunkSize() {
return chunkSize;
}
public void setChunkSize(long chunkSize) {
this.chunkSize = chunkSize;
}
public long getTotalChunks() {
return totalChunks;
}
public void setTotalChunks(long totalChunks) {
this.totalChunks = totalChunks;
}
public long getChunksReceived() {
return chunksReceived;
}
public void setChunksReceived(long chunksReceived) {
this.chunksReceived = chunksReceived;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Instant getExpiresAt() {
return expiresAt;
}
public void setExpiresAt(Instant expiresAt) {
this.expiresAt = expiresAt;
}
public String getChecksum() {
return checksum;
}
public void setChecksum(String checksum) {
this.checksum = checksum;
}
}

View File

@ -0,0 +1,64 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package io.gitea.sdk.models;
/**
* Represents a Gitea user.
*/
public class User {
private long id;
private String login;
private String fullName;
private String email;
private String avatarUrl;
private boolean isAdmin;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
public boolean isAdmin() {
return isAdmin;
}
public void setAdmin(boolean admin) {
isAdmin = admin;
}
}