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:
parent
ad82306b52
commit
e35aa8d878
172
sdk/csharp/Gitea.SDK/ChunkedUpload.cs
Normal file
172
sdk/csharp/Gitea.SDK/ChunkedUpload.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
84
sdk/csharp/Gitea.SDK/Exceptions.cs
Normal file
84
sdk/csharp/Gitea.SDK/Exceptions.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
sdk/csharp/Gitea.SDK/Gitea.SDK.csproj
Normal file
26
sdk/csharp/Gitea.SDK/Gitea.SDK.csproj
Normal 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>
|
||||||
268
sdk/csharp/Gitea.SDK/GiteaClient.cs
Normal file
268
sdk/csharp/Gitea.SDK/GiteaClient.cs
Normal 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; }
|
||||||
|
}
|
||||||
161
sdk/csharp/Gitea.SDK/Models.cs
Normal file
161
sdk/csharp/Gitea.SDK/Models.cs
Normal 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
165
sdk/csharp/README.md
Normal 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
182
sdk/java/README.md
Normal 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
111
sdk/java/pom.xml
Normal 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>
|
||||||
195
sdk/java/src/main/java/io/gitea/sdk/ChunkedUpload.java
Normal file
195
sdk/java/src/main/java/io/gitea/sdk/ChunkedUpload.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
297
sdk/java/src/main/java/io/gitea/sdk/GiteaClient.java
Normal file
297
sdk/java/src/main/java/io/gitea/sdk/GiteaClient.java
Normal 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+
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
66
sdk/java/src/main/java/io/gitea/sdk/models/Attachment.java
Normal file
66
sdk/java/src/main/java/io/gitea/sdk/models/Attachment.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
124
sdk/java/src/main/java/io/gitea/sdk/models/Progress.java
Normal file
124
sdk/java/src/main/java/io/gitea/sdk/models/Progress.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
86
sdk/java/src/main/java/io/gitea/sdk/models/Release.java
Normal file
86
sdk/java/src/main/java/io/gitea/sdk/models/Release.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
123
sdk/java/src/main/java/io/gitea/sdk/models/Repository.java
Normal file
123
sdk/java/src/main/java/io/gitea/sdk/models/Repository.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
55
sdk/java/src/main/java/io/gitea/sdk/models/UploadResult.java
Normal file
55
sdk/java/src/main/java/io/gitea/sdk/models/UploadResult.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
64
sdk/java/src/main/java/io/gitea/sdk/models/User.java
Normal file
64
sdk/java/src/main/java/io/gitea/sdk/models/User.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user