feat: implement JSON backup export functionality with improved download handling
All checks were successful
Publish Container / publish (push) Successful in 3m25s
All checks were successful
Publish Container / publish (push) Successful in 3m25s
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
e8bbae0496
commit
bf333c4a00
7 changed files with 96 additions and 5 deletions
|
|
@ -76,7 +76,7 @@ else
|
|||
<p class="text-muted mb-3">Export the full database as JSON or restore a previously exported JSON backup. Restore replaces the current database only when the backup format version and database schema version are supported.</p>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center mb-4">
|
||||
<a class="btn btn-outline-secondary" href="/api/database-backup/export">Export JSON backup</a>
|
||||
<button class="btn btn-outline-secondary" type="button" @onclick="ExportAsync">Export JSON backup</button>
|
||||
<span class="small text-muted">Current database schema version: @DatabaseSchemaVersion</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -193,4 +193,20 @@ else
|
|||
isRestoring = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExportAsync()
|
||||
{
|
||||
backupStatusMessage = null;
|
||||
backupErrorMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
await JS.InvokeVoidAsync("workTrackerDownloads.downloadFromUrl", "/api/database-backup/export");
|
||||
backupStatusMessage = $"Backup export started at {DateTime.Now:t}.";
|
||||
}
|
||||
catch (JSException)
|
||||
{
|
||||
backupErrorMessage = "Unable to start the backup download.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
23
Program.cs
23
Program.cs
|
|
@ -1,5 +1,6 @@
|
|||
using System.Globalization;
|
||||
using System.Security.Claims;
|
||||
using NLog;
|
||||
using NLog.Web;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
|
@ -31,12 +32,22 @@ if (!runningInContainer)
|
|||
builder.WebHost.UseStaticWebAssets();
|
||||
}
|
||||
|
||||
var configuredStorageOptions = builder.Configuration.GetSection(CouchbaseLiteOptions.SectionName).Get<CouchbaseLiteOptions>() ?? new CouchbaseLiteOptions();
|
||||
var resolvedDatabaseDirectory = ResolveStorageDirectory(configuredStorageOptions.Directory, builder.Environment.ContentRootPath);
|
||||
var resolvedLogDirectory = Path.Combine(resolvedDatabaseDirectory, "logs");
|
||||
GlobalDiagnosticsContext.Set("worktrackerLogPath", Path.Combine(resolvedLogDirectory, "worktracker.log"));
|
||||
GlobalDiagnosticsContext.Set("worktrackerArchiveDirectory", Path.Combine(resolvedLogDirectory, "archive"));
|
||||
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Host.UseNLog();
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
builder.Services.Configure<Microsoft.AspNetCore.Components.Server.CircuitOptions>(options =>
|
||||
{
|
||||
options.DetailedErrors = builder.Configuration.GetValue("DetailedErrors", builder.Environment.IsDevelopment());
|
||||
});
|
||||
|
||||
builder.Services.Configure<AppAuthOptions>(builder.Configuration.GetSection(AppAuthOptions.SectionName));
|
||||
var appAuthOptions = builder.Configuration.GetSection(AppAuthOptions.SectionName).Get<AppAuthOptions>() ?? new AppAuthOptions();
|
||||
|
|
@ -354,3 +365,15 @@ app.MapRazorComponents<App>()
|
|||
.AddInteractiveServerRenderMode();
|
||||
|
||||
app.Run();
|
||||
|
||||
static string ResolveStorageDirectory(string configuredDirectory, string contentRootPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(configuredDirectory))
|
||||
{
|
||||
return Path.Combine(contentRootPath, "App_Data", "couchbase");
|
||||
}
|
||||
|
||||
return Path.IsPathRooted(configuredDirectory)
|
||||
? configuredDirectory
|
||||
: Path.GetFullPath(Path.Combine(contentRootPath, configuredDirectory));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,10 @@ Docker persistence:
|
|||
|
||||
- The app now uses an embedded Couchbase Lite database stored under `/data/couchbase` inside the container.
|
||||
- The compose file mounts that path from `${WORKTRACKER_DATA_PATH:-./.docker-data/couchbase}` on the host.
|
||||
- NLog also writes `logs/worktracker.log` under that same persisted Couchbase Lite directory, so logs survive container replacement.
|
||||
- Archived logs are rotated by size, compressed, and old archives are pruned automatically.
|
||||
- The same log events are still written to container stdout, so `docker logs` remains the primary production inspection path.
|
||||
- Keep `DetailedErrors=false` in production. That setting is for client-visible Blazor circuit details; production diagnostics should come from `docker logs` and `worktracker.log`, not the browser console.
|
||||
- Set `WORKTRACKER_DATA_PATH` before `docker compose up` if you want to move the database elsewhere.
|
||||
|
||||
Development in VS Code without Docker:
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@
|
|||
"CouchbaseLite": {
|
||||
"Directory": "App_Data/couchbase-dev"
|
||||
},
|
||||
"DetailedErrors": true,
|
||||
"UseHttpsRedirection": false,
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
"Default": "Debug",
|
||||
"Microsoft.AspNetCore": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,13 @@
|
|||
"DefaultUsername": "Admin",
|
||||
"DefaultUserId": "ADMIN"
|
||||
},
|
||||
"DetailedErrors": false,
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
"WorkTracker": "Debug",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.AspNetCore.Components.Server.Circuits": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
|
|
|
|||
13
nlog.config
13
nlog.config
|
|
@ -10,9 +10,20 @@
|
|||
layout="${longdate}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" />
|
||||
<target xsi:type="Debugger" name="debug"
|
||||
layout="${longdate}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" />
|
||||
<target xsi:type="File" name="file"
|
||||
fileName="${gdc:item=worktrackerLogPath}"
|
||||
createDirs="true"
|
||||
keepFileOpen="false"
|
||||
concurrentWrites="true"
|
||||
archiveFileName="${gdc:item=worktrackerArchiveDirectory}/worktracker.{#}.log"
|
||||
archiveNumbering="Rolling"
|
||||
archiveAboveSize="10485760"
|
||||
enableArchiveFileCompression="true"
|
||||
maxArchiveFiles="14"
|
||||
layout="${longdate}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" />
|
||||
</targets>
|
||||
|
||||
<rules>
|
||||
<logger name="*" minlevel="Debug" writeTo="console,debug" />
|
||||
<logger name="*" minlevel="Debug" writeTo="console,debug,file" />
|
||||
</rules>
|
||||
</nlog>
|
||||
|
|
|
|||
|
|
@ -86,6 +86,39 @@ window.workTrackerPreferences = {
|
|||
}
|
||||
};
|
||||
|
||||
window.workTrackerDownloads = {
|
||||
async downloadFromUrl(url) {
|
||||
const response = await fetch(url, {
|
||||
credentials: "same-origin"
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Download failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const contentDisposition = response.headers.get("content-disposition") || "";
|
||||
const fileNameMatch = /filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i.exec(contentDisposition);
|
||||
const fileName = decodeURIComponent(fileNameMatch?.[1] || fileNameMatch?.[2] || "worktracker-backup.json");
|
||||
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
|
||||
try {
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = objectUrl;
|
||||
anchor.download = fileName;
|
||||
anchor.rel = "noopener";
|
||||
anchor.style.display = "none";
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
}
|
||||
finally {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.workTrackerDateInput = (() => {
|
||||
const listeners = new WeakMap();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue