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