Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
dotnet-version: |
10.0.x
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: OpenAPI coverage audit
run: python tools/list_openapi_coverage.py
- name: Restore dependencies
run: dotnet restore
- name: Build
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/nuget-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ jobs:
- name: 🔄 Checkout code
uses: actions/checkout@v4

- name: ⚙️ Setup .NET 9
- name: ⚙️ Setup .NET 10
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
dotnet-version: '10.0.x'

- name: 📦 Restore dependencies
run: dotnet restore
Expand Down
6 changes: 5 additions & 1 deletion ConsoleApp1/ConsoleApp1.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand All @@ -11,6 +11,10 @@
<ProjectReference Include="..\DriveClient\kDriveClient.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.10" />
</ItemGroup>

<ItemGroup>
<None Update="example.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
Expand Down
181 changes: 163 additions & 18 deletions ConsoleApp1/Program.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,174 @@
using kDriveClient.kDriveClient;
using kDriveClient.kDriveClient;
using kDriveClient.Models;
using kDriveClient.Models.Domain;
using kDriveClient.Models.Exceptions;
using System.Diagnostics;
using System.Globalization;

var client = new KDriveClient("toekn", 0000);
var file = new KDriveFile
// Local E2E sample. Credentials come from environment / user-secrets — never hardcode tokens.
var token = Environment.GetEnvironmentVariable("KDRIVE_TOKEN")
?? throw new InvalidOperationException("Set KDRIVE_TOKEN environment variable (or user-secrets) before running.");
var driveId = long.Parse(Environment.GetEnvironmentVariable("KDRIVE_DRIVE_ID") ?? "0", CultureInfo.InvariantCulture);
var testRootParentDirectoryId = long.Parse(Environment.GetEnvironmentVariable("KDRIVE_PARENT_DIR_ID") ?? "5", CultureInfo.InvariantCulture);
var accountIdEnv = Environment.GetEnvironmentVariable("KDRIVE_ACCOUNT_ID");
long? accountId = long.TryParse(accountIdEnv, out var aid) ? aid : null;
var testRootFolderName = "kDriveClient-E2E-Test";
var localInputFilePath = Path.Combine(AppContext.BaseDirectory, "example.txt");
var localTestDirectory = Path.Combine(AppContext.BaseDirectory, "test");
var cleanupRemoteAtEnd = true;

if (driveId <= 0 || testRootParentDirectoryId <= 0)
{
Console.Error.WriteLine("Set KDRIVE_DRIVE_ID and optionally KDRIVE_PARENT_DIR_ID / KDRIVE_ACCOUNT_ID.");
return;
}

if (!File.Exists(localInputFilePath))
{
Console.Error.WriteLine($"Input file not found: {localInputFilePath}");
return;
}

Directory.CreateDirectory(localTestDirectory);

var uploadOptions = new KDriveUploadOptions
{
Name = "example.txt",
DirectoryPath = "/Private/test",
Content = File.OpenRead("example.txt"),
CreatedAt = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
LastModifiedAt = (int)new FileInfo("example.txt").LastWriteTimeUtc.Subtract(DateTime.UnixEpoch).TotalSeconds
UseAutoChunkSize = false,
ChunkSize = 1024 * 1024 * 8,
Parallelism = 4
};

var response = await client.UploadAsync(file);
Console.WriteLine($"Uploaded file ID: {response.Id}");
var client = await KDriveClient.CreateAsync(token, driveId, uploadOptions);
var results = new List<EndpointCheck>();
long? uploadedFileId = null;
long? runFolderId = null;
long? testRootFolderId = null;
var localDownloadedPath = Path.Combine(localTestDirectory, "downloaded.txt");

try
{
if (accountId is > 0)
{
await RunEndpointAsync(results, "BootstrapAsync", async () =>
{
var ctx = await client.BootstrapAsync(accountId.Value, preferredDriveId: driveId);
Console.WriteLine($"Bootstrap selected drive {ctx.DriveId} ({ctx.Drives.Count} drives).");
return ctx;
});
}

var testRoot = await client.GetOrCreateFolderAsync(testRootParentDirectoryId, testRootFolderName);
testRootFolderId = testRoot.Id;

var runFolder = await client.GetOrCreateFolderAsync(testRoot.Id, $"run-{DateTime.UtcNow:yyyyMMdd-HHmmss}");
runFolderId = runFolder.Id;

using var uploadFile = new KDriveFile(localInputFilePath, "/");
uploadFile.DirectoryId = runFolderId.Value.ToString(CultureInfo.InvariantCulture);

await RunEndpointNoResultAsync(results, "UploadAsync", async () =>
{
var uploaded = await client.UploadAsync(uploadFile);
uploadedFileId = uploaded.Id;
Console.WriteLine($"Uploaded file id: {uploaded.Id}");
});

var stream = await client.DownloadFileAsync(response.Id);
if (uploadedFileId is null || runFolderId is null)
throw new InvalidOperationException("Setup failed: uploaded file id or run folder id is missing.");

Console.WriteLine($"File downloaded");
var listQuery = new KDriveListQuery { Limit = 50, Includes = KDriveItemIncludes.Path };

using var fs = File.Create("downloaded.txt");
await stream.CopyToAsync(fs);
await RunEndpointNoResultAsync(results, "DownloadFileAsync", async () =>
{
await client.DownloadFileAsync(uploadedFileId.Value, localDownloadedPath);
});

Console.WriteLine($"File saved to {fs.Name}");
await RunEndpointAsync(results, "GetItemAsync", () => client.GetItemAsync(uploadedFileId.Value, "path"));
await RunEndpointAsync(results, "GetItemsAsync", () => client.GetItemsAsync(runFolderId.Value, listQuery));
await RunEndpointAsync(results, "GetRecentAsync", () => client.GetRecentAsync(listQuery));
await RunEndpointAsync(results, "GetFavoritesAsync", () => client.GetFavoritesAsync(listQuery));
await RunEndpointAsync(results, "SearchItemsAsync", () => client.SearchItemsAsync(new KDriveSearchQuery { Query = "example", Limit = 20 }));
await RunEndpointAsync(results, "GetTrashItemsAsync", () => client.GetTrashItemsAsync(listQuery));

using var outFile = File.Create("downloaded_direct.txt");
await client.DownloadFileAsync(response.Id, outFile);
await RunEndpointNoResultAsync(results, "EnumerateItemsAsync", async () =>
{
var count = 0;
await foreach (var _ in client.EnumerateItemsAsync(runFolderId.Value, listQuery, pageSize: 50))
count++;
Console.WriteLine($"Enumerated {count} items.");
});

var copy = await RunEndpointAsync(results, "CopyItemAsync", () =>
client.CopyItemAsync(uploadedFileId.Value, runFolderId.Value, "copy-e2e.txt", conflict: "rename"));
if (copy?.Item?.Id is long copyId)
await RunEndpointAsync(results, "TrashItemAsync", () => client.TrashItemAsync(copyId));
}
finally
{
PrintSummary(results);

if (cleanupRemoteAtEnd)
{
if (uploadedFileId is not null)
await SafeTrashAsync(client, uploadedFileId.Value, "uploaded file");
if (runFolderId is not null)
await SafeTrashAsync(client, runFolderId.Value, "run folder");
}
}

static async Task<T?> RunEndpointAsync<T>(ICollection<EndpointCheck> results, string name, Func<Task<T>> action)
{
var sw = Stopwatch.StartNew();
try
{
var value = await action();
sw.Stop();
results.Add(new EndpointCheck(name, true, $"OK ({sw.ElapsedMilliseconds} ms)"));
Console.WriteLine($"[OK] {name} ({sw.ElapsedMilliseconds} ms)");
return value;
}
catch (Exception ex)
{
sw.Stop();
var detail = BuildExceptionDetail(ex);
results.Add(new EndpointCheck(name, false, detail));
Console.WriteLine($"[ERR] {name} => {detail}");
return default;
}
}

static Task RunEndpointNoResultAsync(ICollection<EndpointCheck> results, string name, Func<Task> action)
=> RunEndpointAsync<object?>(results, name, async () => { await action(); return null; });

static void PrintSummary(IEnumerable<EndpointCheck> results)
{
var entries = results.ToList();
var ok = entries.Count(x => x.Success);
Console.WriteLine();
Console.WriteLine($"========== Endpoint Test Summary ==========");
Console.WriteLine($"Total: {entries.Count} | Success: {ok} | Failed: {entries.Count - ok}");
foreach (var item in entries.Where(x => !x.Success))
Console.WriteLine($" - {item.Name}: {item.Detail}");
}

static string BuildExceptionDetail(Exception ex)
{
if (ex is KDriveApiException apiEx)
return $"{apiEx.Error.Error.Code}: {apiEx.Error.Error.Description}";
return $"{ex.GetType().Name}: {ex.Message}";
}

static async Task SafeTrashAsync(KDriveClient client, long itemId, string label)
{
try
{
await client.TrashItemAsync(itemId);
Console.WriteLine($"Cleanup: trashed {label} (id: {itemId}).");
}
catch (Exception ex)
{
Console.WriteLine($"Cleanup warning for {label}: {BuildExceptionDetail(ex)}");
}
}

Console.WriteLine($"File downloaded and saved to {fs.Name}");
readonly record struct EndpointCheck(string Name, bool Success, string Detail);
44 changes: 37 additions & 7 deletions DriveClient.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36212.18
# Visual Studio Version 18
VisualStudioVersion = 18.5.11716.220
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "kDriveClient", "DriveClient\kDriveClient.csproj", "{084DDB93-889E-4836-9540-9B62114F273C}"
EndProject
Expand All @@ -12,26 +12,56 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Éléments de solution", "
README.md = README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "ConsoleApp1\ConsoleApp1.csproj", "{4D48AA16-E910-4A1F-B3A0-ACEF4A14E780}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "ConsoleApp1\ConsoleApp1.csproj", "{8C7C8C7D-E8F8-4003-9222-67F4561A1BD9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DriveClient", "DriveClient", "{0FD03D6A-3714-32FA-703E-1549276A6354}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{084DDB93-889E-4836-9540-9B62114F273C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{084DDB93-889E-4836-9540-9B62114F273C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{084DDB93-889E-4836-9540-9B62114F273C}.Debug|x64.ActiveCfg = Debug|Any CPU
{084DDB93-889E-4836-9540-9B62114F273C}.Debug|x64.Build.0 = Debug|Any CPU
{084DDB93-889E-4836-9540-9B62114F273C}.Debug|x86.ActiveCfg = Debug|Any CPU
{084DDB93-889E-4836-9540-9B62114F273C}.Debug|x86.Build.0 = Debug|Any CPU
{084DDB93-889E-4836-9540-9B62114F273C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{084DDB93-889E-4836-9540-9B62114F273C}.Release|Any CPU.Build.0 = Release|Any CPU
{084DDB93-889E-4836-9540-9B62114F273C}.Release|x64.ActiveCfg = Release|Any CPU
{084DDB93-889E-4836-9540-9B62114F273C}.Release|x64.Build.0 = Release|Any CPU
{084DDB93-889E-4836-9540-9B62114F273C}.Release|x86.ActiveCfg = Release|Any CPU
{084DDB93-889E-4836-9540-9B62114F273C}.Release|x86.Build.0 = Release|Any CPU
{7CAAEB7C-4DEF-47B2-8928-EA1453D9C286}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7CAAEB7C-4DEF-47B2-8928-EA1453D9C286}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7CAAEB7C-4DEF-47B2-8928-EA1453D9C286}.Debug|x64.ActiveCfg = Debug|Any CPU
{7CAAEB7C-4DEF-47B2-8928-EA1453D9C286}.Debug|x64.Build.0 = Debug|Any CPU
{7CAAEB7C-4DEF-47B2-8928-EA1453D9C286}.Debug|x86.ActiveCfg = Debug|Any CPU
{7CAAEB7C-4DEF-47B2-8928-EA1453D9C286}.Debug|x86.Build.0 = Debug|Any CPU
{7CAAEB7C-4DEF-47B2-8928-EA1453D9C286}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7CAAEB7C-4DEF-47B2-8928-EA1453D9C286}.Release|Any CPU.Build.0 = Release|Any CPU
{4D48AA16-E910-4A1F-B3A0-ACEF4A14E780}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4D48AA16-E910-4A1F-B3A0-ACEF4A14E780}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4D48AA16-E910-4A1F-B3A0-ACEF4A14E780}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4D48AA16-E910-4A1F-B3A0-ACEF4A14E780}.Release|Any CPU.Build.0 = Release|Any CPU
{7CAAEB7C-4DEF-47B2-8928-EA1453D9C286}.Release|x64.ActiveCfg = Release|Any CPU
{7CAAEB7C-4DEF-47B2-8928-EA1453D9C286}.Release|x64.Build.0 = Release|Any CPU
{7CAAEB7C-4DEF-47B2-8928-EA1453D9C286}.Release|x86.ActiveCfg = Release|Any CPU
{7CAAEB7C-4DEF-47B2-8928-EA1453D9C286}.Release|x86.Build.0 = Release|Any CPU
{8C7C8C7D-E8F8-4003-9222-67F4561A1BD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8C7C8C7D-E8F8-4003-9222-67F4561A1BD9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8C7C8C7D-E8F8-4003-9222-67F4561A1BD9}.Debug|x64.ActiveCfg = Debug|Any CPU
{8C7C8C7D-E8F8-4003-9222-67F4561A1BD9}.Debug|x64.Build.0 = Debug|Any CPU
{8C7C8C7D-E8F8-4003-9222-67F4561A1BD9}.Debug|x86.ActiveCfg = Debug|Any CPU
{8C7C8C7D-E8F8-4003-9222-67F4561A1BD9}.Debug|x86.Build.0 = Debug|Any CPU
{8C7C8C7D-E8F8-4003-9222-67F4561A1BD9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8C7C8C7D-E8F8-4003-9222-67F4561A1BD9}.Release|Any CPU.Build.0 = Release|Any CPU
{8C7C8C7D-E8F8-4003-9222-67F4561A1BD9}.Release|x64.ActiveCfg = Release|Any CPU
{8C7C8C7D-E8F8-4003-9222-67F4561A1BD9}.Release|x64.Build.0 = Release|Any CPU
{8C7C8C7D-E8F8-4003-9222-67F4561A1BD9}.Release|x86.ActiveCfg = Release|Any CPU
{8C7C8C7D-E8F8-4003-9222-67F4561A1BD9}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
41 changes: 41 additions & 0 deletions DriveClient/Extensions/KDriveClientOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using kDriveClient.Models;

namespace kDriveClient.Extensions
{
/// <summary>
/// Options for registering <see cref="kDriveClient.kDriveClient.KDriveClient"/> in DI.

Check warning on line 6 in DriveClient/Extensions/KDriveClientOptions.cs

View workflow job for this annotation

GitHub Actions / build

XML comment has cref attribute 'KDriveClient' that could not be resolved
/// </summary>
public sealed class KDriveClientOptions
{
/// <summary>
/// Bearer API token.
/// </summary>
public string Token { get; set; } = string.Empty;

/// <summary>
/// Target drive id.
/// </summary>
public long DriveId { get; set; }

/// <summary>
/// Optional Infomaniak account id (useful for <c>BootstrapAsync</c> / multi-drive flows).
/// </summary>
public long? AccountId { get; set; }

/// <summary>
/// API base address (must be absolute; defaults to Infomaniak).
/// </summary>
public Uri BaseAddress { get; set; } = new("https://api.infomaniak.com");

/// <summary>
/// Upload strategy options.
/// </summary>
public KDriveUploadOptions Upload { get; set; } = new();

/// <summary>
/// Named <see cref="HttpClient"/> registration name.
/// When using a custom name, ensure that named client is registered (default registration uses <c>kDrive</c>).
/// </summary>
public string HttpClientName { get; set; } = "kDrive";
}
}
Loading
Loading