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
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ All business logic lives here. Server Actions must delegate to Services.
```
job-service.ts → CRUD for backup jobs
backup-service.ts → Triggers runJob()
retention-service.ts → GVS (Grandfather-Father-Son) algorithm
retention-service.ts → GFS (Grandfather-Father-Son) algorithm
restore-service.ts → Restore orchestration
```

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Whether you're running a single MySQL database or managing multiple PostgreSQL,
### ⏰ Scheduling & Retention

- **Cron-based Scheduling** - Flexible job scheduling with a visual Schedule Picker (Simple Mode + Cron Mode)
- **GVS Retention Policies** - Grandfather-Father-Son rotation with per-destination retention settings
- **GFS Retention Policies** - Grandfather-Father-Son rotation with per-destination retention settings
- **Automated Config Backups** - Self-backup of the entire DBackup configuration to any storage adapter

### 👥 Access Control & Security
Expand Down
2 changes: 1 addition & 1 deletion api-docs/openapi.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: DBackup API
version: 2.4.0
version: 2.4.1
description: |
REST API for DBackup - a self-hosted database backup automation platform with encryption, compression, and smart retention.

Expand Down
30 changes: 30 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@

All notable changes to DBackup are documented here.

## v2.4.1 - Multiple Bug Fixes across MSSQL, SMB, Retention, and Storage Adapters
*Released: May 27, 2026*

### 🐛 Bug Fixes

- **mssql**: Fixed backup abort and missing remote cleanup when SSH-transferring more than ~10 databases due to SSH channel exhaustion - the SFTP session is now cached and reused instead of opening a new channel per operation.
- **mssql**: Fixed "Arithmetic overflow" crash in Database Explorer and Restore for databases larger than ~2 GB.
- **smb**: Fixed `.connection-test-*` probe files not being deleted when `sendFile` throws after the remote file was already created.
- **retention**: Fixed SMART/GFS tier overlap that incorrectly mapped multiple tiers to the same backup, causing over-aggressive deletion. ([#101](https://github.com/Skyfay/DBackup/issues/101))
- **storage**: Fixed file descriptor leak causing deleted `.tar` temp files to hold disk blocks until container restart, affecting S3, SFTP, FTP, OneDrive, and tar-utils streams. ([#100](https://github.com/Skyfay/DBackup/issues/100))

### 🎨 Improvements

- **retention**: Corrected the retention algorithm abbreviation from "GVS" to "GFS" across UI, documentation, and the built-in retention template.
- **history**: Retention execution logs now include the applied retention template name.

### 🧪 Tests

- **smb**: Added unit tests for `finally`-block cleanup when `sendFile` throws and for cleanup retry when the delete itself fails.
- **retention**: Added regression tests for GFS non-overlapping tier selection and template-name visibility in retention history. ([#101](https://github.com/Skyfay/DBackup/issues/101))
- **ftp/sftp**: Fixed broken upload unit tests by adding missing `destroy: vi.fn()` to the `createReadStream` mock - the adapter calls `fileStream.destroy()` in the `finally` block, which threw a TypeError without this mock method.

### 🐳 Docker

- **Image**: `skyfay/dbackup:v2.4.1`
- **Also tagged as**: `latest`, `v2`
- **CI Image**: `skyfay/dbackup:ci`
- **Platforms**: linux/amd64, linux/arm64


## v2.4.0 - Database Explorer Browser, Drill-down Data Viewer, and Bug Fixes
*Released: May 25, 2026*

Expand Down
12 changes: 6 additions & 6 deletions docs/developer-guide/advanced/retention.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Retention System

The Retention System automatically manages backup storage by implementing smart rotation policies based on the Grandfather-Father-Son (GVS) algorithm.
The Retention System automatically manages backup storage by implementing smart rotation policies based on the Grandfather-Father-Son (GFS) algorithm.

## Overview

Expand All @@ -10,11 +10,11 @@ DBackup supports three retention modes:
| :--- | :--- |
| **None** | Keep all backups (no deletion) |
| **Simple** | Keep the last N backups |
| **Smart (GVS)** | Grandfather-Father-Son strategy |
| **Smart (GFS)** | Grandfather-Father-Son strategy |

## Grandfather-Father-Son (GVS)
## Grandfather-Father-Son (GFS)

The GVS algorithm keeps backups at decreasing frequencies as they age:
The GFS algorithm keeps backups at decreasing frequencies as they age:

```
Today ←──── Daily ────→ Weekly ────→ Monthly ────→ Yearly
Expand Down Expand Up @@ -108,7 +108,7 @@ export const RetentionService = {
keep = sorted.slice(0, config.simple!.keepCount);
break;
case "SMART":
keep = this.applyGVS(sorted, config.smart!);
keep = this.applyGFS(sorted, config.smart!);
break;
}

Expand All @@ -122,7 +122,7 @@ export const RetentionService = {
};
},

applyGVS(files: FileInfo[], config: SmartConfig): FileInfo[] {
applyGFS(files: FileInfo[], config: SmartConfig): FileInfo[] {
const keep = new Set<string>();

// Daily buckets
Expand Down
6 changes: 3 additions & 3 deletions docs/developer-guide/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ src/services/
├── job-service.ts # Job CRUD
├── backup-service.ts # Backup triggering
├── restore-service.ts # Restore orchestration
├── retention-service.ts # GVS algorithm
├── retention-service.ts # GFS algorithm
├── encryption-service.ts # Key management
├── user-service.ts # User management
└── oidc-provider-service.ts
Expand Down Expand Up @@ -205,7 +205,7 @@ Executes backups through discrete steps:
│ Completion │◀──│ Retention │◀────────┘
│ │ │ │
│ • Cleanup │ │ • Apply │
│ • Notify │ │ GVS
│ • Notify │ │ GFS
│ • Finalize │ │ • Delete │
└────────────┘ └────────────┘
```
Expand Down Expand Up @@ -286,7 +286,7 @@ Runner Pipeline:
│ └── Cleanup, notify, update status
└── stepRetention()
└── Apply GVS, delete old backups
└── Apply GFS, delete old backups
```

## Security Architecture
Expand Down
2 changes: 1 addition & 1 deletion docs/developer-guide/core/services.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ src/services/
├── job-service.ts # CRUD for backup jobs
├── backup-service.ts # Trigger backups
├── restore-service.ts # Restore orchestration
├── retention-service.ts # GVS algorithm
├── retention-service.ts # GFS algorithm
├── encryption-service.ts # Encryption profiles
├── integrity-service.ts # SHA-256 checksum verification
├── notification-log-service.ts # Notification log recording & queries
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ features:
details: Each backup job can target multiple storage destinations simultaneously for redundancy or off-site copies.
- icon: 📅
title: Scheduling & Retention
details: Cron-based job scheduling with GVS (Grandfather-Father-Son) retention policies for automatic rotation.
details: Cron-based job scheduling with GFS (Grandfather-Father-Son) retention policies for automatic rotation.
- icon: 🔔
title: Notifications
details: 9+ notification adapters including Discord, Slack, Teams, Telegram, Gotify, ntfy, Webhook, SMS, and Email (SMTP).
Expand Down
2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dbackup-docs",
"version": "2.4.0",
"version": "2.4.1",
"private": true,
"scripts": {
"dev": "vitepress dev",
Expand Down
2 changes: 1 addition & 1 deletion docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ For a full list of completed features, see the [Changelog](/changelog).
- ✅ GZIP and Brotli compression
- ✅ S3, SFTP, Local, WebDAV, SMB, FTP/FTPS storage adapters
- ✅ Discord and Email notifications
- ✅ Cron-based scheduling with GVS retention
- ✅ Cron-based scheduling with GFS retention
- ✅ RBAC permission system
- ✅ SSO/OIDC authentication (Authentik, PocketID, Generic)
- ✅ TOTP and Passkey 2FA
Expand Down
2 changes: 1 addition & 1 deletion docs/user-guide/destinations/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ The `.meta.json` file stores compression, encryption metadata (IV, auth tag, pro
Destinations work with retention policies to automatically clean up old backups:

- **Simple**: Keep last N backups
- **Smart (GVS)**: Grandfather-Father-Son rotation
- **Smart (GFS)**: Grandfather-Father-Son rotation

See [Retention Policies](/user-guide/jobs/retention) for details.
- [FTP / FTPS](/user-guide/destinations/ftp)
Expand Down
2 changes: 1 addition & 1 deletion docs/user-guide/first-steps.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ In the **General** tab, use the Schedule picker:

In the **Destinations** tab, expand a destination row and configure its retention policy:
- **Simple**: Keep last N backups
- **Smart (GVS)**: Grandfather-Father-Son rotation
- **Smart (GFS)**: Grandfather-Father-Son rotation

## Step 4: Run Your First Backup

Expand Down
2 changes: 1 addition & 1 deletion docs/user-guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ DBackup is a self-hosted web application for automating database backups. It sup
- **Multi-Destination Jobs**: A single job can upload to multiple storage destinations simultaneously
- **Backup Encryption**: AES-256-GCM encryption with an Encryption Vault, key rotation, and offline Recovery Kits
- **Compression**: Built-in GZIP and Brotli compression
- **Scheduling & Retention**: Cron-based scheduling with GVS (Grandfather-Father-Son) retention policies
- **Scheduling & Retention**: Cron-based scheduling with GFS (Grandfather-Father-Son) retention policies
- **Notifications**: 9+ adapters including Discord, Slack, Teams, Telegram, Email, and more
- **Storage Monitoring**: Per-destination alerts for usage spikes, storage limit warnings, and missing backups
- **Restore**: Browse backup history, verify checksums, and restore directly to a database including database remapping
Expand Down
6 changes: 3 additions & 3 deletions docs/user-guide/jobs/retention.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Retention policies prevent unlimited storage growth by automatically deleting ol
| :--- | :--- | :--- |
| **None** | Keep all backups | Manual management |
| **Simple** | Keep last N backups | Fixed rotation |
| **Smart (GVS)** | Grandfather-Father-Son | Long-term archival |
| **Smart (GFS)** | Grandfather-Father-Son | Long-term archival |

## Per-Destination Retention

Expand Down Expand Up @@ -55,7 +55,7 @@ With `Keep Count: 5`:
- Frequent backups with short retention
- Simple rotation needs

## Smart Retention (GVS)
## Smart Retention (GFS)

Grandfather-Father-Son is an intelligent retention strategy that keeps:
- Recent backups (daily)
Expand Down Expand Up @@ -259,7 +259,7 @@ With compression (70% reduction):

### Wrong Backups Deleted

The GVS algorithm keeps the **oldest** backup in each time bucket. This is intentional:
The GFS algorithm keeps the **oldest** backup in each time bucket. This is intentional:
- Weekly: Keeps backup from start of week
- Monthly: Keeps backup from start of month

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dbackup",
"version": "2.4.0",
"version": "2.4.1",
"private": true,
"scripts": {
"dev": "next dev",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- Fix: Rename built-in retention template from "Smart GVS" to "Smart GFS" (correct Grandfather-Father-Son abbreviation)
-- Guard: skip if the target name already exists (user may have created a policy with that name)
-- to avoid a unique-constraint failure on the "name" column.
UPDATE "RetentionPolicy"
SET "name" = 'Smart GFS (7/4/12/2)',
"description" = 'Grandfather-Father-Son: keep 7 daily, 4 weekly, 12 monthly, 2 yearly backups.',
"updatedAt" = CURRENT_TIMESTAMP
WHERE "id" = 'retention-gvs-default'
AND "name" = 'Smart GVS (7/4/12/2)'
AND NOT EXISTS (
SELECT 1 FROM "RetentionPolicy" WHERE "name" = 'Smart GFS (7/4/12/2)'
);
2 changes: 1 addition & 1 deletion public/openapi.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: DBackup API
version: 2.4.0
version: 2.4.1
description: |
REST API for DBackup - a self-hosted database backup automation platform with encryption, compression, and smart retention.

Expand Down
4 changes: 2 additions & 2 deletions src/components/settings/templates/retention-policy-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export function RetentionPolicyList() {
return `Simple - keep ${parsed.simple?.keepCount ?? "?"} backups`;
if (parsed.mode === "SMART") {
const s = parsed.smart;
return `Smart GVS (${s?.daily ?? 0}/${s?.weekly ?? 0}/${s?.monthly ?? 0}/${s?.yearly ?? 0})`;
return `Smart GFS (${s?.daily ?? 0}/${s?.weekly ?? 0}/${s?.monthly ?? 0}/${s?.yearly ?? 0})`;
}
} catch {
// ignore
Expand Down Expand Up @@ -358,7 +358,7 @@ export function RetentionPolicyDialog({
id="rp-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g. Smart GVS Production"
placeholder="e.g. Smart GFS Production"
/>
</div>
<div className="space-y-1.5">
Expand Down
31 changes: 26 additions & 5 deletions src/lib/adapters/database/common/tar-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ export async function createMultiDbTar(
// Stream file contents to tar entry
const fileStream = createReadStream(file.path);
await new Promise<void>((resolve, reject) => {
fileStream.on("error", reject);
fileStream.on("error", (err) => {
fileStream.destroy();
reject(err);
});
fileStream.on("end", () => {
entry.end();
resolve();
Expand Down Expand Up @@ -179,13 +182,19 @@ export async function extractMultiDbTar(
files: extractedFiles,
});
});
const readStream = createReadStream(sourcePath);
/* v8 ignore start */
extractor.on("error", (err) => {
readStream.destroy();
reject(err);
});
readStream.on("error", (err) => {
extractor.destroy(err);
reject(err);
});
/* v8 ignore end */

createReadStream(sourcePath).pipe(extractor);
readStream.pipe(extractor);
});
}

Expand Down Expand Up @@ -261,13 +270,19 @@ export async function extractSelectedDatabases(
});
});

const readStream = createReadStream(sourcePath);
/* v8 ignore start */
extractor.on("error", (err) => {
readStream.destroy();
reject(err);
});
readStream.on("error", (err) => {
extractor.destroy(err);
reject(err);
});
/* v8 ignore end */

createReadStream(sourcePath).pipe(extractor);
readStream.pipe(extractor);
});
}

Expand Down Expand Up @@ -320,6 +335,7 @@ export async function readTarManifest(filePath: string): Promise<TarManifest | n
return new Promise((resolve) => {
const extractor = extract();
let manifestFound = false;
const readStream = createReadStream(filePath);

extractor.on("entry", (header, stream, next) => {
if (header.name === MANIFEST_FILENAME) {
Expand Down Expand Up @@ -355,14 +371,19 @@ export async function readTarManifest(filePath: string): Promise<TarManifest | n
});

extractor.on("error", () => {
readStream.destroy();
resolve(null);
});

extractor.on("close", () => {
// Handle early close from destroy()
readStream.destroy();
});

createReadStream(filePath).pipe(extractor);
readStream.on("error", () => {
extractor.destroy();
resolve(null);
});
readStream.pipe(extractor);
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/lib/adapters/database/mssql/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export async function getTables(config: MSSQLConfig, database: string): Promise<
t.TABLE_NAME AS name,
t.TABLE_TYPE AS table_type,
COALESCE(SUM(p.rows), 0) AS row_count,
COALESCE(SUM(a.total_pages) * 8 * 1024, 0) AS size_bytes
COALESCE(SUM(CAST(a.total_pages AS BIGINT)) * 8 * 1024, 0) AS size_bytes
FROM [${dbId}].INFORMATION_SCHEMA.TABLES t
LEFT JOIN [${dbId}].sys.tables st ON st.name = t.TABLE_NAME
LEFT JOIN [${dbId}].sys.indexes i ON i.object_id = st.object_id AND i.type <= 1
Expand Down
2 changes: 1 addition & 1 deletion src/lib/adapters/database/mssql/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export async function getDatabasesWithStats(config: MSSQLConfig): Promise<Databa
SELECT
d.name,
d.state_desc,
SUM(mf.size) * 8 * 1024 AS size_bytes
SUM(CAST(mf.size AS BIGINT)) * 8 * 1024 AS size_bytes
FROM sys.databases d
LEFT JOIN sys.master_files mf ON d.database_id = mf.database_id
WHERE d.database_id > 4
Expand Down
Loading
Loading