Skip to content

feat: implement DatabaseBackupService with list & download API and settings UI#159

Merged
lcomplete merged 1 commit into
mainfrom
feat/database-backup-service
May 24, 2026
Merged

feat: implement DatabaseBackupService with list & download API and settings UI#159
lcomplete merged 1 commit into
mainfrom
feat/database-backup-service

Conversation

@lcomplete
Copy link
Copy Markdown
Owner

Summary

Builds on the database backup configuration foundation to deliver the full backup feature end-to-end.


Server

DatabaseBackupService (new)

  • backupDatabase() — Uses SQLite's VACUUM INTO statement to create a point-in-time snapshot to the configured backup directory. Skips silently when backup is disabled or no path is configured.
  • listBackups() — Returns metadata (file name, creation time, size) for all existing backup files in the directory.
  • resolveBackupPath(fileName) — Validates file name against path-traversal attacks (../, absolute separators) and checks the resolved path stays inside the backup directory before returning it.
  • cleanExcessBackups() — Deletes the oldest backups once the backupKeepCount limit is exceeded.

DatabaseBackupInfo DTO (new)

Lombok @Data carrying fileName, createdAt (Instant), sizeBytes.

SettingController additions

  • GET /api/setting/general/database-backups — lists all available backup files.
  • GET /api/setting/general/database-backups/download?fileName=… — streams the backup file as an attachment with Content-Disposition header.

DatabaseBackupTask refactor

Simplified scheduling logic; delegates all backup work to DatabaseBackupService.

GlobalSetting / GlobalSettingService

Added backupKeepCount field and service method to expose the keep-count default.

AppConstants

Added DEFAULT_BACKUP_KEEP_COUNT = 5.


Client

databaseBackup.ts (new)

Typed axios helpers:

  • fetchDatabaseBackups() — calls the list endpoint and returns DatabaseBackupInfo[].
  • getDatabaseBackupDownloadUrl(fileName) — builds the download URL with encoded file name.

GeneralSetting component

Added a Database Backup panel in the settings modal showing:

  • Enable/disable toggle
  • Backup path input
  • Keep-count input
  • List of existing backups with creation date, size, and a download link per file.

i18n

Added EN/ZH-CN translation keys for all new backup UI strings.

Misc client improvements

  • Search.tsx: quick-search suggestion chips; selected page ID persisted in URL (?p=).
  • Twitter.tsx, SubHeader.tsx, PageFilters.tsx, SearchBox.tsx: minor fixes surfaced during development.

Tests

File Coverage
DatabaseBackupServiceTest (new, 151 lines) backupDatabase, listBackups, resolveBackupPath incl. path-traversal rejection
DatabaseBackupTaskScheduleTest (new, 106 lines) Scheduled task trigger conditions
GlobalSettingServiceTest Extended for new settings fields
DatabaseBackupTaskTest Removed — superseded by the above

Security notes

  • Backup file download validates the file name against path-traversal patterns before resolving the path.
  • The resolved absolute path is checked to remain inside the configured backup directory.

@augmentcode
Copy link
Copy Markdown

augmentcode Bot commented May 24, 2026

🤖 Augment PR Summary

Summary: Adds end-to-end SQLite database backup support, including server-side backup creation/listing/downloading and a settings UI to configure and access backups.

Changes:

  • Introduced DatabaseBackupService to create point-in-time backups (SQLite VACUUM INTO), list backup metadata, validate download paths, and enforce a keep-count limit.
  • Extended GlobalSetting/GlobalSettingService with enableDatabaseBackup, backupKeepCount, and backupTime defaults/validation.
  • Added settings endpoints to list backups and download a selected backup as an attachment.
  • Refactored DatabaseBackupTask to poll every minute and trigger backup at the configured time via the new service.
  • Client: added typed API helpers and a “Database Backup” panel with enable/path/time/keep-count inputs plus a dialog to list/download backups.
  • Client: enhanced Search UX by preserving selected page ID in the URL and adding date filtering with preserved query params.
  • Added/updated JUnit tests for backup service behavior and task scheduling logic.

🤖 Was this summary useful? React with 👍 or 👎

Copy link
Copy Markdown

@augmentcode augmentcode Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 4 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Path backupPath;
try {
backupPath = databaseBackupService.resolveBackupPath(fileName);
} catch (IllegalStateException e) {
Copy link
Copy Markdown

@augmentcode augmentcode Bot May 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

app/server/huntly-server/src/main/java/com/huntly/server/controller/SettingController.java:237 — resolveBackupPath() can throw IllegalArgumentException for invalid fileName, but this handler only catches IllegalStateException, so invalid input may surface as a 500 instead of a controlled 4xx response.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(resource.contentLength())
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + backupPath.getFileName() + "\"")
Copy link
Copy Markdown

@augmentcode augmentcode Bot May 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

app/server/huntly-server/src/main/java/com/huntly/server/controller/SettingController.java:248 — The Content-Disposition header is built from a raw filename string; if a backup file name contains quotes/control characters this can break the header or enable header-injection style issues.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

return;
private boolean shouldBackup(LocalDateTime now, LocalTime backupTime) {
LocalTime currentTime = now.toLocalTime();
if (!currentTime.equals(backupTime)) {
Copy link
Copy Markdown

@augmentcode augmentcode Bot May 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

app/server/huntly-server/src/main/java/com/huntly/server/task/DatabaseBackupTask.java:62 — shouldBackup() requires currentTime.equals(backupTime), so if the app is down or the scheduler runs late (past that minute) the daily backup will be skipped until the next day.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.


public static final Integer DEFAULT_COLD_DATA_KEEP_DAYS = 60;

public static final Integer DEFAULT_BACKUP_KEEP_COUNT = 3;
Copy link
Copy Markdown

@augmentcode augmentcode Bot May 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

app/server/huntly-server/src/main/java/com/huntly/server/domain/constant/AppConstants.java:20 — PR description mentions a default keep-count of 5, but DEFAULT_BACKUP_KEEP_COUNT is set to 3 here; consider aligning the docs/expectations with the implemented default.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

@lcomplete lcomplete merged commit d9702bf into main May 24, 2026
@lcomplete lcomplete deleted the feat/database-backup-service branch May 24, 2026 12:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant