-
Notifications
You must be signed in to change notification settings - Fork 2
Update/v1.1.0 #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update/v1.1.0 #11
Conversation
- Introduce admin authentication and dashboard UI in Blazor app - Implement backup management: create, list, download, restore, delete - Add BackupService and file token support to PocketBaseSharp SDK - Update models, services, and extension methods for admin support - Add JS interop for file downloads; update layout and docs - Change default PocketBase URL to localhost for dev use
Added GitHub Actions workflow for automatic NuGet publishing on master branch. Updated PocketBaseSharp.csproj with new package metadata, set version to 1.1.0, changed target framework to net10.0, and included icon.png as the package icon. Added icon.png binary file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request adds backup management capabilities to the PocketBaseSharp SDK and introduces an admin dashboard in the demo application. The changes include new backup service methods, admin authentication updates, and DevOps automation.
Changes:
- Added comprehensive backup management API (create, download, restore, delete backups) with both sync and async methods
- Updated admin authentication to use the
_superuserscollection endpoint and modified the admin model structure - Introduced GitHub Actions workflow for automated NuGet package publishing
Reviewed changes
Copilot reviewed 30 out of 32 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| PocketBaseSharp/Services/BackupService.cs | Implements complete backup management functionality with detailed XML documentation |
| PocketBaseSharp/Services/AdminService.cs | Updates admin authentication to use _superusers collection endpoint |
| PocketBaseSharp/PocketBase.cs | Adds 204 No Content response handling, file token API, and enhanced stream download support |
| PocketBaseSharp/Models/* | Updates admin and auth models, adds FileTokenResponse, updates BackupModel with DateTimeConverter |
| PocketBaseSharp/Extensions/PocketBaseExtensions.cs | Adds GetCurrentAdmin/GetCurrentAdminAsync extension methods |
| Example/Pages/Admin/* | New admin dashboard with login, profile, and backup management UI components |
| Example/Services/FileDownloadInterop.cs | JavaScript interop service for browser file downloads |
| Example/Options/PocketBaseOptions.cs | Changes default BaseUrl to localhost for local development |
| .github/workflows/release.yaml | Automates NuGet package publishing on master branch pushes |
| README.md | Documents new backup management features and admin dashboard usage |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| [JsonPropertyName("username")] | ||
| public string? Username { get; set; } | ||
|
|
||
| public string? UserName { get; set; } |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The property is named "UserName" (with capital 'N') in the interface but the JSON attribute still uses "username" (lowercase). While this will work for deserialization, the inconsistency between the property name and the interface declaration could cause confusion. Consider whether the interface should use "Username" to match the JSON property name pattern, or if "UserName" is the intended standard.
| /// Service for administrating PocketBase superusers (admins). | ||
| /// Handles authentication with the _superusers collection endpoint. | ||
| /// </summary> | ||
| public class AdminService : BaseAuthService<RecordAuthModel<AdminModel>> |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The AdminService now uses RecordAuthModel as the generic type parameter and points to the "_superusers" collection endpoint. However, AdminModel now inherits from BaseAuthModel instead of BaseModel. This is a breaking change that could affect existing code that depends on the previous AdminAuthModel type. The PR description doesn't explicitly call out this breaking change, which could cause issues for consumers of the library.
| <Description>Multiplatform C# SDK for PocketBase</Description> | ||
| <PackageTags>pocketbase;blazor;webassembly;dotnet;sdk;api;realtime;backend;database;authentication</PackageTags> | ||
| <Copyright>Copyright (c) 2025 PSCourtney</Copyright> | ||
| <PackageReleaseNotes>Version 1.1.0: Added backup management features (create, download, restore, delete backups). Compatible with PocketBase v0.35.1 and .NET 10.</PackageReleaseNotes> |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The package version is being set to "1.1.0" but the PackageReleaseNotes mention compatibility with "PocketBase v0.35.1". However, the README.md at line 24 mentions "Pocketbase v0.28.4", and the actual CHANGELOG.md shows v0.35.1 is newer. This inconsistency in version references across documentation should be resolved to avoid confusion about which PocketBase version is actually supported.
| <PackageReleaseNotes>Version 1.1.0: Added backup management features (create, download, restore, delete backups). Compatible with PocketBase v0.35.1 and .NET 10.</PackageReleaseNotes> | |
| <PackageReleaseNotes>Version 1.1.0: Added backup management features (create, download, restore, delete backups).</PackageReleaseNotes> |
| public const string Position = "PocketBase"; | ||
|
|
||
| public string BaseUrl { get; set; } = "https://sdk-todo-example.pockethost.io/"; | ||
| public string BaseUrl { get; set; } = "http://127.0.0.1:8090/"; |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The BaseUrl change from a public hosted instance to localhost (127.0.0.1:8090) will break the demo for users who don't have PocketBase running locally. While this might be intentional for local development, it should be documented in the PR description as a breaking change for the example application, or the value should be configurable via appsettings.json as mentioned in the README at line 65.
| public string BaseUrl { get; set; } = "http://127.0.0.1:8090/"; | |
| public string BaseUrl { get; set; } = string.Empty; |
| public async Task<Result<IEnumerable<BackupModel>>> GetFullListAsync() | ||
| { | ||
| var b = await _pocketBase.SendAsync<IEnumerable<BackupModel>>("/api/backups", HttpMethod.Get); | ||
| return b; | ||
| var body = await _pocketBase.SendAsync<IEnumerable<BackupModel>>(BasePath(), HttpMethod.Get); | ||
| return body; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Triggers a new database backup. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Endpoint: POST /api/backups | ||
| /// Auth: Admin/Superuser required (via AuthStore) | ||
| /// Note: PocketBase returns 204 No Content on success, so the BackupModel in the result will be null. | ||
| /// Use GetFullListAsync() after creation to retrieve the backup details. | ||
| /// </remarks> | ||
| /// <param name="basename"> | ||
| /// Optional custom filename for the backup (e.g., "daily-backup.zip"). | ||
| /// If null, PocketBase will auto-generate a timestamp-based name. | ||
| /// </param> | ||
| /// <param name="body">The request body to send to the API. Default is null.</param> | ||
| /// <param name="query">The query parameters to send to the API. Default is null.</param> | ||
| /// <param name="headers">The headers to send to the API. Default is null.</param> | ||
| /// <param name="cancellationToken">Cancellation token. Default is default.</param> | ||
| /// <returns>Result containing null BackupModel (API returns 204 No Content on success).</returns> | ||
| public async Task<Result<BackupModel>> CreateAsync(string? basename = null,IDictionary<string, object>? body = null,IDictionary<string, object?>? query = null,IDictionary<string, string>? headers = null,CancellationToken cancellationToken = default) | ||
| { | ||
| body ??= new Dictionary<string, object>(); | ||
| if (!string.IsNullOrWhiteSpace(basename)) | ||
| { | ||
| body["basename"] = basename; | ||
| } | ||
| var url = BasePath(); | ||
| return await _pocketBase.SendAsync<BackupModel>(url, HttpMethod.Post, headers: headers, query: query, body: body, files: null, cancellationToken: cancellationToken); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Triggers a new database backup (synchronous version). | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Endpoint: POST /api/backups | ||
| /// Auth: Admin/Superuser required (via AuthStore) | ||
| /// Note: PocketBase returns 204 No Content on success, so the BackupModel in the result will be null. | ||
| /// Use GetFullListAsync() after creation to retrieve the backup details. | ||
| /// </remarks> | ||
| /// <param name="basename"> | ||
| /// Optional custom filename for the backup (e.g., "daily-backup.zip"). | ||
| /// If null, PocketBase will auto-generate a timestamp-based name. | ||
| /// </param> | ||
| /// <param name="body">The request body to send to the API. Default is null.</param> | ||
| /// <param name="query">The query parameters to send to the API. Default is null.</param> | ||
| /// <param name="headers">The headers to send to the API. Default is null.</param> | ||
| /// <param name="cancellationToken">Cancellation token. Default is default.</param> | ||
| /// <returns>Result containing null BackupModel (API returns 204 No Content on success).</returns> | ||
| public Result<BackupModel> Create(string? basename = null,IDictionary<string, object>? body = null,IDictionary<string, object?>? query = null,IDictionary<string, string>? headers = null,CancellationToken cancellationToken = default) | ||
| { | ||
| body ??= new Dictionary<string, object>(); | ||
| if (!string.IsNullOrWhiteSpace(basename)) | ||
| { | ||
| body["basename"] = basename; | ||
| } | ||
| var url = BasePath(); | ||
| return _pocketBase.Send<BackupModel>(url, HttpMethod.Post, headers: headers, query: query, body: body, files: null, cancellationToken: cancellationToken); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Downloads a backup file as a stream. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Endpoint: GET /api/backups/{key} | ||
| /// Auth: Admin/Superuser required (via AuthStore) | ||
| /// Requires: A valid file token obtained via GetFileTokenAsync() | ||
| /// </remarks> | ||
| /// <param name="key">The backup key identifier.</param> | ||
| /// <param name="query">The query parameters to send to the API. Default is null.</param> | ||
| /// <param name="cancellationToken">Cancellation token. Default is default.</param> | ||
| /// <returns>Result containing a readable Stream of the backup file.</returns> | ||
| public async Task<Result<Stream>> DownloadAsync(string key,IDictionary<string, object?>? query = null,CancellationToken cancellationToken = default) | ||
| { | ||
| query ??= new Dictionary<string, object?>(); | ||
|
|
||
| var tokenResult = await _pocketBase.GetFileTokenAsync(cancellationToken); | ||
| if (!tokenResult.IsSuccess) | ||
| { | ||
| return Result.Fail<Stream>(tokenResult.Errors); | ||
| } | ||
|
|
||
| query["token"] = tokenResult.Value; | ||
|
|
||
| var url = $"{BasePath()}/{UrlEncode(key)}"; | ||
| return await _pocketBase.GetStreamAsync(url, query, cancellationToken); | ||
| } | ||
|
|
||
|
|
||
| /// <summary> | ||
| /// Restores the database from a backup. | ||
| /// WARNING: DESTRUCTIVE OPERATION - Completely replaces current database. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Endpoint: POST /api/backups/{key}/restore | ||
| /// Auth: Admin/Superuser required (via AuthStore) | ||
| /// Note: PocketBase will temporarily stop the server, restore, and restart. | ||
| /// </remarks> | ||
| /// <param name="key">The backup key identifier.</param> | ||
| /// <param name="body">The request body to send to the API. Default is null.</param> | ||
| /// <param name="query">The query parameters to send to the API. Default is null.</param> | ||
| /// <param name="headers">The headers to send to the API. Default is null.</param> | ||
| /// <param name="cancellationToken">Cancellation token. Default is default.</param> | ||
| /// <returns>Result indicating success/failure of restore operation.</returns> | ||
| public async Task<Result> RestoreAsync(string key,IDictionary<string, object>? body = null,IDictionary<string, object?>? query = null,IDictionary<string, string>? headers = null,CancellationToken cancellationToken = default) | ||
| { | ||
| body ??= new Dictionary<string, object>(); | ||
| var url = $"{BasePath()}/{UrlEncode(key)}/restore"; | ||
| return await _pocketBase.SendAsync(url, HttpMethod.Post, headers: headers, query: query, body: body, files: null, cancellationToken: cancellationToken); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Restores the database from a backup (synchronous version). | ||
| /// WARNING: DESTRUCTIVE OPERATION - Completely replaces current database. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Endpoint: POST /api/backups/{key}/restore | ||
| /// Auth: Admin/Superuser required (via AuthStore) | ||
| /// Note: PocketBase will temporarily stop the server, restore, and restart. | ||
| /// </remarks> | ||
| /// <param name="key">The backup key identifier.</param> | ||
| /// <param name="body">The request body to send to the API. Default is null.</param> | ||
| /// <param name="query">The query parameters to send to the API. Default is null.</param> | ||
| /// <param name="headers">The headers to send to the API. Default is null.</param> | ||
| /// <param name="cancellationToken">Cancellation token. Default is default.</param> | ||
| /// <returns>Result indicating success/failure of restore operation.</returns> | ||
| public Result Restore(string key,IDictionary<string, object>? body = null,IDictionary<string, object?>? query = null,IDictionary<string, string>? headers = null,CancellationToken cancellationToken = default) | ||
| { | ||
| body ??= new Dictionary<string, object>(); | ||
| var url = $"{BasePath()}/{UrlEncode(key)}/restore"; | ||
| return _pocketBase.Send(url, HttpMethod.Post, headers: headers, query: query, body: body, files: null, cancellationToken: cancellationToken); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Deletes a backup file. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Endpoint: DELETE /api/backups/{key} | ||
| /// Auth: Admin/Superuser required (via AuthStore) | ||
| /// </remarks> | ||
| /// <param name="key">The backup key identifier.</param> | ||
| /// <param name="body">The request body to send to the API. Default is null.</param> | ||
| /// <param name="query">The query parameters to send to the API. Default is null.</param> | ||
| /// <param name="headers">The headers to send to the API. Default is null.</param> | ||
| /// <param name="cancellationToken">Cancellation token. Default is default.</param> | ||
| /// <returns>Result indicating success/failure of delete operation.</returns> | ||
| public async Task<Result> DeleteAsync(string key,IDictionary<string, object>? body = null,IDictionary<string, object?>? query = null,IDictionary<string, string>? headers = null,CancellationToken cancellationToken = default) | ||
| { | ||
| var url = $"{BasePath()}/{UrlEncode(key)}"; | ||
| return await _pocketBase.SendAsync(url, HttpMethod.Delete, headers: headers, query: query, body: body, files: null, cancellationToken: cancellationToken); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Deletes a backup file (synchronous version). | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Endpoint: DELETE /api/backups/{key} | ||
| /// Auth: Admin/Superuser required (via AuthStore) | ||
| /// </remarks> | ||
| /// <param name="key">The backup key identifier.</param> | ||
| /// <param name="body">The request body to send to the API. Default is null.</param> | ||
| /// <param name="query">The query parameters to send to the API. Default is null.</param> | ||
| /// <param name="headers">The headers to send to the API. Default is null.</param> | ||
| /// <param name="cancellationToken">Cancellation token. Default is default.</param> | ||
| /// <returns>Result indicating success/failure of delete operation.</returns> | ||
| public Result Delete(string key,IDictionary<string, object>? body = null,IDictionary<string, object?>? query = null,IDictionary<string, string>? headers = null,CancellationToken cancellationToken = default) | ||
| { | ||
| var url = $"{BasePath()}/{UrlEncode(key)}"; | ||
| return _pocketBase.Send(url, HttpMethod.Delete, headers: headers, query: query, body: body, files: null, cancellationToken: cancellationToken); | ||
| } |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The BackupService introduces significant new functionality (create, download, restore, delete backups) but lacks test coverage. Since the repository has existing tests for other services, test coverage should be added for the new backup management methods to ensure they work correctly and handle edge cases properly.
| if (result.IsSuccess && !string.IsNullOrWhiteSpace(result.Value?.Token)) | ||
| { | ||
| return Result.Ok(result.Value.Token); | ||
| } | ||
| return Result.Fail("Failed to obtain file token"); |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The GetFileTokenAsync method lacks error handling for cases where the API response is successful but the token is empty or null. The current implementation returns a generic error message "Failed to obtain file token" which doesn't provide details about what went wrong. Consider adding more specific error information from the result object if available.
| if (result.IsSuccess && !string.IsNullOrWhiteSpace(result.Value?.Token)) | |
| { | |
| return Result.Ok(result.Value.Token); | |
| } | |
| return Result.Fail("Failed to obtain file token"); | |
| if (result.IsFailed) | |
| { | |
| // Preserve detailed errors from the underlying request. | |
| return Result.Fail(result.Errors); | |
| } | |
| if (!string.IsNullOrWhiteSpace(result.Value?.Token)) | |
| { | |
| return Result.Ok(result.Value.Token); | |
| } | |
| // Successful response but the token is missing or empty. | |
| return Result.Fail("Failed to obtain file token: the token was null or empty in the response."); |
| if (result.IsSuccess) | ||
| { | ||
| using (var memoryStream = new MemoryStream()) | ||
| { | ||
| await result.Value.CopyToAsync(memoryStream); | ||
| var bytes = memoryStream.ToArray(); | ||
|
|
||
| await _fileDownloadInterop!.DownloadFileAsync(bytes, key); | ||
| Snackbar.Add($"Backup downloaded: {key} ({bytes.Length} bytes)", Severity.Success); | ||
| } |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Loading the entire backup file into memory via memoryStream.ToArray() could cause memory issues for large database backups. Consider streaming the download directly to the browser or implementing chunked downloads for better memory efficiency, especially in WASM environments where memory is limited.
| if (result.IsSuccess) | |
| { | |
| using (var memoryStream = new MemoryStream()) | |
| { | |
| await result.Value.CopyToAsync(memoryStream); | |
| var bytes = memoryStream.ToArray(); | |
| await _fileDownloadInterop!.DownloadFileAsync(bytes, key); | |
| Snackbar.Add($"Backup downloaded: {key} ({bytes.Length} bytes)", Severity.Success); | |
| } | |
| if (result.IsSuccess && result.Value is not null) | |
| { | |
| await using var streamRef = new DotNetStreamReference(result.Value); | |
| await JSRuntime.InvokeVoidAsync("downloadFileFromStream", key, streamRef); | |
| Snackbar.Add($"Backup downloaded: {key}", Severity.Success); |
| try | ||
| { | ||
| //Warning: This does not work on pocketbase windows hosted. See: https://github.com/pocketbase/pocketbase/issues/3411 | ||
| //It retrns a 204 but the logs will show restore is not supported on Windows |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment mentions "retrns" which is a typo. It should be "returns".
| //It retrns a 204 but the logs will show restore is not supported on Windows | |
| //It returns a 204 but the logs will show restore is not supported on Windows |
| var result = await PocketBase.Backup.DownloadAsync(key); | ||
| if (result.IsSuccess) | ||
| { | ||
| using (var memoryStream = new MemoryStream()) | ||
| { | ||
| await result.Value.CopyToAsync(memoryStream); | ||
| var bytes = memoryStream.ToArray(); | ||
|
|
||
| await _fileDownloadInterop!.DownloadFileAsync(bytes, key); | ||
| Snackbar.Add($"Backup downloaded: {key} ({bytes.Length} bytes)", Severity.Success); | ||
| } |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Stream returned by DownloadAsync is not being disposed in the DownloadBackupAsync method. After copying to memoryStream, the original stream should be disposed to prevent resource leaks. Consider wrapping result.Value in a using statement.
Admin Dashboard and Authentication:
AuthorizeView, including components for admin profile and login handling. [1] [2] [3] [4] [5] [6]AdminNotLoggedIn.razor.cs.Backup Management Features:
BackupManagementcomponent and code-behind for listing, creating, downloading, restoring, and deleting database backups, with UI feedback and confirmation dialogs. [1] [2] [3]DevOps and Configuration:
release.yaml) to automate publishing of NuGet packages on push tomaster.BaseUrlinPocketBaseOptionsto use a local PocketBase instance for easier development and testing.Bug Fixes and Minor Improvements: