diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 382d37e..3887b80 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -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
diff --git a/.github/workflows/nuget-publish.yml b/.github/workflows/nuget-publish.yml
index 9927492..c9609af 100644
--- a/.github/workflows/nuget-publish.yml
+++ b/.github/workflows/nuget-publish.yml
@@ -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
diff --git a/ConsoleApp1/ConsoleApp1.csproj b/ConsoleApp1/ConsoleApp1.csproj
index 19a98fd..38c045f 100644
--- a/ConsoleApp1/ConsoleApp1.csproj
+++ b/ConsoleApp1/ConsoleApp1.csproj
@@ -2,7 +2,7 @@
Exe
- net9.0
+ net10.0
enable
enable
@@ -11,6 +11,10 @@
+
+
+
+
PreserveNewest
diff --git a/ConsoleApp1/Program.cs b/ConsoleApp1/Program.cs
index 7924af5..6aade9c 100644
--- a/ConsoleApp1/Program.cs
+++ b/ConsoleApp1/Program.cs
@@ -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();
+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 RunEndpointAsync(ICollection results, string name, Func> 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 results, string name, Func action)
+ => RunEndpointAsync