dbkeeper 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +240 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +166 -0
- package/dist/config/loadConfig.d.ts +6 -0
- package/dist/config/loadConfig.js +74 -0
- package/dist/core/logger.d.ts +14 -0
- package/dist/core/logger.js +28 -0
- package/dist/core/retention.d.ts +24 -0
- package/dist/core/retention.js +33 -0
- package/dist/core/runBackup.d.ts +20 -0
- package/dist/core/runBackup.js +137 -0
- package/dist/database/mongo.d.ts +10 -0
- package/dist/database/mongo.js +102 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +24 -0
- package/dist/notify/mail.d.ts +11 -0
- package/dist/notify/mail.js +89 -0
- package/dist/storage/gdrive.d.ts +11 -0
- package/dist/storage/gdrive.js +132 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +2 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Faysal Sarker
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="./assets/DB-Keeper.png" alt="DBKeeper - Automatic Database Backup" width="100%">
|
|
4
|
+
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div align="center">
|
|
8
|
+
|
|
9
|
+
<pre>
|
|
10
|
+
██████╗ ██████╗ ██╗ ██╗███████╗███████╗██████╗ ███████╗██████╗
|
|
11
|
+
██╔══██╗██╔══██╗██║ ██╔╝██╔════╝██╔════╝██╔══██╗██╔════╝██╔══██╗
|
|
12
|
+
██║ ██║██████╔╝█████╔╝ █████╗ █████╗ ██████╔╝█████╗ ██████╔╝
|
|
13
|
+
██║ ██║██╔══██╗██╔═██╗ ██╔══╝ ██╔══╝ ██╔═══╝ ██╔══╝ ██╔══██╗
|
|
14
|
+
██████╔╝██████╔╝██║ ██╗███████╗███████╗██║ ███████╗██║ ██║
|
|
15
|
+
╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚══════╝╚═╝ ╚═╝
|
|
16
|
+
</pre>
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
**Reliable, self-hosted database backups for MongoDB and beyond.**
|
|
21
|
+
|
|
22
|
+
[](https://www.npmjs.com/package/dbkeeper)
|
|
23
|
+
[](./LICENSE)
|
|
24
|
+
[](https://nodejs.org)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## Why this exists
|
|
32
|
+
|
|
33
|
+
Most backup solutions are either cloud-locked SaaS products with recurring fees, or bare shell scripts with no retry logic, no rotation safety, and no notifications. dbkeeper is a self-hosted CLI tool that handles rolling backup retention, safe rotation (never deletes an old backup before a new one is verified uploaded), and email notifications — all in one composable package. No subscription. No vendor lock-in. You own your data pipeline.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- **MongoDB backup** via `mongodump` — produces a compressed `.tar.gz` archive
|
|
40
|
+
- **Google Drive storage** — uploads via service account credentials, no OAuth flow required
|
|
41
|
+
- **Rolling retention** — keep the N most recent backups; older ones are deleted automatically
|
|
42
|
+
- **Safe rotation** — old backups are only deleted *after* the new upload is confirmed with a remote ID
|
|
43
|
+
- **Email notifications** — configurable success and/or failure alerts via any SMTP server
|
|
44
|
+
- **TypeScript-first** — full type declarations included; works equally well from plain JavaScript
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Global (recommended for CLI use)
|
|
52
|
+
npm install -g dbkeeper
|
|
53
|
+
|
|
54
|
+
# Local dev dependency (for programmatic use or CI)
|
|
55
|
+
npm install --save-dev dbkeeper
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
**1. Create a `backup.config.js` in your project root:**
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
// backup.config.js
|
|
66
|
+
require('dotenv').config();
|
|
67
|
+
|
|
68
|
+
module.exports = {
|
|
69
|
+
database: {
|
|
70
|
+
type: 'mongo',
|
|
71
|
+
uri: process.env.MONGO_URI,
|
|
72
|
+
},
|
|
73
|
+
storage: {
|
|
74
|
+
type: 'gdrive',
|
|
75
|
+
credentialsPath: process.env.GDRIVE_CREDENTIALS_PATH ?? './gdrive-credentials.json',
|
|
76
|
+
folderId: process.env.GDRIVE_FOLDER_ID,
|
|
77
|
+
},
|
|
78
|
+
retention: {
|
|
79
|
+
keep: 24, // keep the 24 most recent backups
|
|
80
|
+
},
|
|
81
|
+
notifications: {
|
|
82
|
+
success: false,
|
|
83
|
+
failure: true,
|
|
84
|
+
to: process.env.NOTIFY_EMAIL,
|
|
85
|
+
smtp: {
|
|
86
|
+
host: process.env.SMTP_HOST,
|
|
87
|
+
port: 587,
|
|
88
|
+
auth: {
|
|
89
|
+
user: process.env.SMTP_USER,
|
|
90
|
+
pass: process.env.SMTP_PASS,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**2. Run a backup:**
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
dbkeeper run
|
|
101
|
+
# or with a custom config path:
|
|
102
|
+
dbkeeper run --config ./config/backup.config.js
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**3. Check the last result:**
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
dbkeeper status
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Configuration Reference
|
|
114
|
+
|
|
115
|
+
All configuration is provided via a `backup.config.js` (or any `.js` file) that exports a plain object.
|
|
116
|
+
|
|
117
|
+
### `database`
|
|
118
|
+
|
|
119
|
+
| Field | Type | Required | Description |
|
|
120
|
+
|--------|----------|----------|--------------------------------------------------|
|
|
121
|
+
| `type` | `string` | ✅ | Database type. Currently only `"mongo"`. |
|
|
122
|
+
| `uri` | `string` | ✅ | MongoDB connection URI (supports Atlas SRV URIs). |
|
|
123
|
+
|
|
124
|
+
### `storage`
|
|
125
|
+
|
|
126
|
+
| Field | Type | Required | Description |
|
|
127
|
+
|---------------------|----------|----------|-------------------------------------------------------------------|
|
|
128
|
+
| `type` | `string` | ✅ | Storage backend. Currently only `"gdrive"`. |
|
|
129
|
+
| `credentialsPath` | `string` | ✅ | Path to the Google Drive service account JSON key file. |
|
|
130
|
+
| `folderId` | `string` | ✅ | ID of the Drive folder where backups will be stored. |
|
|
131
|
+
|
|
132
|
+
### `retention`
|
|
133
|
+
|
|
134
|
+
| Field | Type | Required | Description |
|
|
135
|
+
|--------|--------------------|----------|-------------------------------------------------------------------------------|
|
|
136
|
+
| `keep` | `number \| "unlimited"` | ✅ | Number of recent backups to keep. Use `"unlimited"` to disable deletion. |
|
|
137
|
+
|
|
138
|
+
### `notifications`
|
|
139
|
+
|
|
140
|
+
| Field | Type | Required | Description |
|
|
141
|
+
|-----------|-----------|----------|-----------------------------------------------------------------|
|
|
142
|
+
| `success` | `boolean` | ✅ | Send an email when a backup succeeds. |
|
|
143
|
+
| `failure` | `boolean` | ✅ | Send an email when a backup fails. |
|
|
144
|
+
| `to` | `string` | ✅ | Recipient email address. |
|
|
145
|
+
| `smtp` | `object` | ✴️ * | SMTP connection config (required if `success` or `failure` is `true`). |
|
|
146
|
+
|
|
147
|
+
**`smtp` sub-fields:**
|
|
148
|
+
|
|
149
|
+
| Field | Type | Default | Description |
|
|
150
|
+
|----------|-----------|---------|---------------------------------------|
|
|
151
|
+
| `host` | `string` | — | SMTP server hostname. |
|
|
152
|
+
| `port` | `number` | `587` | SMTP port. |
|
|
153
|
+
| `secure` | `boolean` | `false` | Use TLS (`true` for port 465). |
|
|
154
|
+
| `auth` | `object` | — | `{ user: string, pass: string }` |
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Google Drive Setup
|
|
159
|
+
|
|
160
|
+
dbkeeper authenticates with Google Drive via a **service account** — there's no OAuth browser flow.
|
|
161
|
+
|
|
162
|
+
1. **Enable the Drive API** — Go to [Google Cloud Console](https://console.cloud.google.com) → APIs & Services → Enable APIs → search for "Google Drive API" and enable it.
|
|
163
|
+
|
|
164
|
+
2. **Create a service account** — Go to IAM & Admin → Service Accounts → Create Service Account. Give it a name (e.g. `dbkeeper-backup`), skip the optional role assignment.
|
|
165
|
+
|
|
166
|
+
3. **Download the JSON key** — Inside your new service account, go to Keys → Add Key → Create new key → JSON. Save the file somewhere secure (e.g. `./gdrive-credentials.json`). **Do not commit this file to version control.**
|
|
167
|
+
|
|
168
|
+
4. **Share the target folder** — In Google Drive, right-click your backup folder → Share → paste in the service account's email address (e.g. `dbkeeper-backup@your-project.iam.gserviceaccount.com`) and give it **Editor** access.
|
|
169
|
+
|
|
170
|
+
5. **Get the folder ID** — Open the folder in Drive. The URL will look like `https://drive.google.com/drive/folders/1AbCdEfGhIjKlMnOpQrStUvWxYz`. The folder ID is the last path segment.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## How Retention Works
|
|
175
|
+
|
|
176
|
+
After each successful upload, dbkeeper lists all backup files in your Drive folder and applies retention. Given `keep: N`:
|
|
177
|
+
|
|
178
|
+
- It sorts all backups by creation date (oldest first).
|
|
179
|
+
- It keeps the N most recent files.
|
|
180
|
+
- It deletes everything older.
|
|
181
|
+
- **Deletion only happens after the new upload has been verified** — dbkeeper confirms the remote file ID is non-empty before touching any existing backups.
|
|
182
|
+
|
|
183
|
+
**Example** with `keep: 3`, running daily:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
Before run: backup-day1.tar.gz backup-day2.tar.gz backup-day3.tar.gz
|
|
187
|
+
After run: backup-day2.tar.gz backup-day3.tar.gz backup-day4.tar.gz
|
|
188
|
+
(day1 deleted, day4 uploaded — always 3 in storage)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Set `keep: "unlimited"` to accumulate all backups indefinitely.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Programmatic API
|
|
196
|
+
|
|
197
|
+
dbkeeper also exports its internals for use in custom scripts or other tooling:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
import { runBackup, mongoAdapter, gdriveAdapter, loadConfig } from 'dbkeeper';
|
|
201
|
+
|
|
202
|
+
const config = loadConfig('./backup.config.js');
|
|
203
|
+
const result = await runBackup(config, mongoAdapter, gdriveAdapter);
|
|
204
|
+
|
|
205
|
+
if (result.ok) {
|
|
206
|
+
console.log(`Backed up to Drive: ${result.remoteId}`);
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
You can also bring your own adapters by implementing the `DatabaseAdapter` or `StorageAdapter` interfaces:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
import { DatabaseAdapter, StorageAdapter } from 'dbkeeper';
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Roadmap
|
|
219
|
+
|
|
220
|
+
The following are **not yet implemented**. This list is here so you know exactly what dbkeeper does and doesn't do today:
|
|
221
|
+
|
|
222
|
+
- [ ] PostgreSQL support
|
|
223
|
+
- [ ] MySQL / MariaDB support
|
|
224
|
+
- [ ] Amazon S3 storage backend
|
|
225
|
+
- [ ] Encryption at rest (GPG or AES)
|
|
226
|
+
- [ ] Restore verification (test-restore and checksum validation)
|
|
227
|
+
- [ ] Web dashboard / status UI
|
|
228
|
+
- [ ] Cron scheduling built into the CLI (currently relies on system cron or a process manager)
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Contributing
|
|
233
|
+
|
|
234
|
+
dbkeeper is open source and welcomes contributions. If you hit a bug, have a feature request, or want to add a new database or storage adapter, open an issue or a pull request on [GitHub](https://github.com/<your-github-username>/dbkeeper). The codebase is intentionally small and straightforward — adapters are just two-method interfaces, so adding support for a new backend is a well-defined, self-contained task.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
MIT © Faysal Sarker — see [LICENSE](./LICENSE)
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dbkeeper CLI entry point.
|
|
4
|
+
*
|
|
5
|
+
* Thin wrapper — all real logic lives in src/core/runBackup.ts.
|
|
6
|
+
* Supported commands (v1):
|
|
7
|
+
* dbkeeper run Execute one backup immediately
|
|
8
|
+
* dbkeeper status Show the last backup result from the status file
|
|
9
|
+
*/
|
|
10
|
+
import 'dotenv/config';
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* dbkeeper CLI entry point.
|
|
5
|
+
*
|
|
6
|
+
* Thin wrapper — all real logic lives in src/core/runBackup.ts.
|
|
7
|
+
* Supported commands (v1):
|
|
8
|
+
* dbkeeper run Execute one backup immediately
|
|
9
|
+
* dbkeeper status Show the last backup result from the status file
|
|
10
|
+
*/
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
require("dotenv/config");
|
|
48
|
+
const loadConfig_1 = require("./config/loadConfig");
|
|
49
|
+
const runBackup_1 = require("./core/runBackup");
|
|
50
|
+
const mongo_1 = require("./database/mongo");
|
|
51
|
+
const gdrive_1 = require("./storage/gdrive");
|
|
52
|
+
const logger_1 = require("./core/logger");
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Status file — persisted to cwd so it survives process restarts
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
const STATUS_FILE = path.resolve(process.cwd(), '.dbkeeper-status.json');
|
|
57
|
+
function saveStatus(result) {
|
|
58
|
+
try {
|
|
59
|
+
fs.writeFileSync(STATUS_FILE, JSON.stringify(result, null, 2), 'utf8');
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Non-fatal
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function readStatus() {
|
|
66
|
+
try {
|
|
67
|
+
if (!fs.existsSync(STATUS_FILE))
|
|
68
|
+
return null;
|
|
69
|
+
return JSON.parse(fs.readFileSync(STATUS_FILE, 'utf8'));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Command: run
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
async function cmdRun(configPath) {
|
|
79
|
+
const config = (0, loadConfig_1.loadConfig)(configPath);
|
|
80
|
+
// Select adapters based on config types
|
|
81
|
+
// (v1 only supports mongo + gdrive — add more cases here for future adapters)
|
|
82
|
+
if (config.database.type !== 'mongo') {
|
|
83
|
+
logger_1.logger.error(`Unsupported database type: "${config.database.type}". Only "mongo" is supported in v1.`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
if (config.storage.type !== 'gdrive') {
|
|
87
|
+
logger_1.logger.error(`Unsupported storage type: "${config.storage.type}". Only "gdrive" is supported in v1.`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
const result = await (0, runBackup_1.runBackup)(config, mongo_1.mongoAdapter, gdrive_1.gdriveAdapter);
|
|
91
|
+
saveStatus(result);
|
|
92
|
+
if (!result.ok) {
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Command: status
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
function cmdStatus() {
|
|
100
|
+
const status = readStatus();
|
|
101
|
+
if (!status) {
|
|
102
|
+
console.log('No backup has been run yet. Execute `dbkeeper run` first.');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (status.ok) {
|
|
106
|
+
console.log(`Last backup: SUCCESS`);
|
|
107
|
+
console.log(` Timestamp: ${new Date(status.timestamp).toISOString()}`);
|
|
108
|
+
console.log(` File: ${path.basename(status.filePath)}`);
|
|
109
|
+
console.log(` Size: ${formatBytes(status.sizeBytes)}`);
|
|
110
|
+
console.log(` Remote ID: ${status.remoteId}`);
|
|
111
|
+
console.log(` Retained: ${status.remainingCount} backup(s) in storage`);
|
|
112
|
+
console.log(` Deleted: ${status.deletedCount} old backup(s)`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
console.log(`Last backup: FAILED`);
|
|
116
|
+
console.log(` Timestamp: ${new Date(status.timestamp).toISOString()}`);
|
|
117
|
+
console.log(` Error: ${status.error?.message ?? 'unknown'}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// CLI routing
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
function printHelp() {
|
|
124
|
+
console.log(`
|
|
125
|
+
dbkeeper — MongoDB backup tool
|
|
126
|
+
|
|
127
|
+
Usage:
|
|
128
|
+
dbkeeper run [--config <path>] Run a backup immediately
|
|
129
|
+
dbkeeper status Show the last backup result
|
|
130
|
+
|
|
131
|
+
Options:
|
|
132
|
+
--config <path> Path to backup.config.js (default: ./backup.config.js)
|
|
133
|
+
`);
|
|
134
|
+
}
|
|
135
|
+
async function main() {
|
|
136
|
+
const args = process.argv.slice(2);
|
|
137
|
+
const command = args[0];
|
|
138
|
+
if (!command || command === '--help' || command === '-h') {
|
|
139
|
+
printHelp();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const configFlagIndex = args.indexOf('--config');
|
|
143
|
+
const configPath = configFlagIndex !== -1 ? args[configFlagIndex + 1] : undefined;
|
|
144
|
+
switch (command) {
|
|
145
|
+
case 'run':
|
|
146
|
+
await cmdRun(configPath);
|
|
147
|
+
break;
|
|
148
|
+
case 'status':
|
|
149
|
+
cmdStatus();
|
|
150
|
+
break;
|
|
151
|
+
default:
|
|
152
|
+
console.error(`Unknown command: "${command}". Run \`dbkeeper --help\` for usage.`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function formatBytes(bytes) {
|
|
157
|
+
if (bytes < 1024)
|
|
158
|
+
return `${bytes} B`;
|
|
159
|
+
if (bytes < 1024 * 1024)
|
|
160
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
161
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
162
|
+
}
|
|
163
|
+
main().catch((err) => {
|
|
164
|
+
logger_1.logger.error(`Unhandled error: ${err.message}`);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.loadConfig = loadConfig;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
/**
|
|
40
|
+
* Loads `backup.config.js` from the consuming project's root (cwd).
|
|
41
|
+
* Throws a clear error if the file is missing or invalid.
|
|
42
|
+
*/
|
|
43
|
+
function loadConfig(configPath) {
|
|
44
|
+
const resolvedPath = configPath
|
|
45
|
+
? path.resolve(configPath)
|
|
46
|
+
: path.resolve(process.cwd(), 'backup.config.js');
|
|
47
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
48
|
+
throw new Error(`dbkeeper: config file not found at "${resolvedPath}".\n` +
|
|
49
|
+
`Create a backup.config.js in your project root, or pass --config <path>.`);
|
|
50
|
+
}
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
52
|
+
const raw = require(resolvedPath);
|
|
53
|
+
if (!raw || typeof raw !== 'object') {
|
|
54
|
+
throw new Error(`dbkeeper: "${resolvedPath}" must export a plain object. Got: ${typeof raw}`);
|
|
55
|
+
}
|
|
56
|
+
const config = raw;
|
|
57
|
+
// Basic structural validation — keeps error messages actionable
|
|
58
|
+
if (!config.database?.type) {
|
|
59
|
+
throw new Error('dbkeeper: config.database.type is required (e.g. "mongo")');
|
|
60
|
+
}
|
|
61
|
+
if (!config.database?.uri) {
|
|
62
|
+
throw new Error('dbkeeper: config.database.uri is required');
|
|
63
|
+
}
|
|
64
|
+
if (!config.storage?.type) {
|
|
65
|
+
throw new Error('dbkeeper: config.storage.type is required (e.g. "gdrive")');
|
|
66
|
+
}
|
|
67
|
+
if (config.retention?.keep === undefined) {
|
|
68
|
+
throw new Error('dbkeeper: config.retention.keep is required (use a number or "unlimited")');
|
|
69
|
+
}
|
|
70
|
+
if (!config.notifications?.to) {
|
|
71
|
+
throw new Error('dbkeeper: config.notifications.to (recipient email) is required');
|
|
72
|
+
}
|
|
73
|
+
return config;
|
|
74
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight structured logger.
|
|
3
|
+
*
|
|
4
|
+
* Prefixes every line with a timestamp and log level.
|
|
5
|
+
* Replace this with a structured logger (pino, winston, etc.) if needed
|
|
6
|
+
* without touching any other module — all internal code imports from here.
|
|
7
|
+
*/
|
|
8
|
+
export type LogLevel = 'info' | 'warn' | 'error' | 'debug';
|
|
9
|
+
export declare const logger: {
|
|
10
|
+
info: (msg: string) => void;
|
|
11
|
+
warn: (msg: string) => void;
|
|
12
|
+
error: (msg: string) => void;
|
|
13
|
+
debug: (msg: string) => void;
|
|
14
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Lightweight structured logger.
|
|
4
|
+
*
|
|
5
|
+
* Prefixes every line with a timestamp and log level.
|
|
6
|
+
* Replace this with a structured logger (pino, winston, etc.) if needed
|
|
7
|
+
* without touching any other module — all internal code imports from here.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.logger = void 0;
|
|
11
|
+
function stamp() {
|
|
12
|
+
return new Date().toISOString();
|
|
13
|
+
}
|
|
14
|
+
function write(level, message) {
|
|
15
|
+
const line = `[${stamp()}] [${level.toUpperCase()}] ${message}`;
|
|
16
|
+
if (level === 'error') {
|
|
17
|
+
process.stderr.write(line + '\n');
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
process.stdout.write(line + '\n');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.logger = {
|
|
24
|
+
info: (msg) => write('info', msg),
|
|
25
|
+
warn: (msg) => write('warn', msg),
|
|
26
|
+
error: (msg) => write('error', msg),
|
|
27
|
+
debug: (msg) => write('debug', msg),
|
|
28
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure rolling-retention logic.
|
|
3
|
+
*
|
|
4
|
+
* No I/O, no network calls — fully unit-testable.
|
|
5
|
+
* The caller is responsible for actually deleting the returned IDs.
|
|
6
|
+
*/
|
|
7
|
+
export interface RemoteFile {
|
|
8
|
+
remoteId: string;
|
|
9
|
+
createdAt: Date;
|
|
10
|
+
}
|
|
11
|
+
export interface RetentionResult {
|
|
12
|
+
/** Files that should be deleted to satisfy the keep limit. */
|
|
13
|
+
toDelete: RemoteFile[];
|
|
14
|
+
/** How many files will remain after deletion. */
|
|
15
|
+
remainingCount: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Given the current list of remote backup files (oldest-first) and the keep
|
|
19
|
+
* limit, return which files need to be deleted and how many will remain.
|
|
20
|
+
*
|
|
21
|
+
* @param existing All existing backups, **including** the one just uploaded.
|
|
22
|
+
* @param keep Maximum number of backups to retain, or "unlimited".
|
|
23
|
+
*/
|
|
24
|
+
export declare function applyRetention(existing: RemoteFile[], keep: number | 'unlimited'): RetentionResult;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pure rolling-retention logic.
|
|
4
|
+
*
|
|
5
|
+
* No I/O, no network calls — fully unit-testable.
|
|
6
|
+
* The caller is responsible for actually deleting the returned IDs.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.applyRetention = applyRetention;
|
|
10
|
+
/**
|
|
11
|
+
* Given the current list of remote backup files (oldest-first) and the keep
|
|
12
|
+
* limit, return which files need to be deleted and how many will remain.
|
|
13
|
+
*
|
|
14
|
+
* @param existing All existing backups, **including** the one just uploaded.
|
|
15
|
+
* @param keep Maximum number of backups to retain, or "unlimited".
|
|
16
|
+
*/
|
|
17
|
+
function applyRetention(existing, keep) {
|
|
18
|
+
if (keep === 'unlimited') {
|
|
19
|
+
return { toDelete: [], remainingCount: existing.length };
|
|
20
|
+
}
|
|
21
|
+
if (typeof keep !== 'number' || keep < 1) {
|
|
22
|
+
throw new Error(`retention.keep must be a positive integer or "unlimited". Got: ${keep}`);
|
|
23
|
+
}
|
|
24
|
+
if (existing.length <= keep) {
|
|
25
|
+
return { toDelete: [], remainingCount: existing.length };
|
|
26
|
+
}
|
|
27
|
+
// Sort by date ascending so the oldest entries are at the front
|
|
28
|
+
const sorted = [...existing].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
29
|
+
const deleteCount = sorted.length - keep;
|
|
30
|
+
const toDelete = sorted.slice(0, deleteCount);
|
|
31
|
+
const remainingCount = sorted.length - deleteCount;
|
|
32
|
+
return { toDelete, remainingCount };
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BackupConfig, DatabaseAdapter, StorageAdapter, BackupResult } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Core backup orchestrator.
|
|
4
|
+
*
|
|
5
|
+
* Accepts adapters by interface — never imports mongo.ts or gdrive.ts directly.
|
|
6
|
+
* This is the key extension point: swap in any DatabaseAdapter / StorageAdapter
|
|
7
|
+
* to support new databases or storage backends without changing this file.
|
|
8
|
+
*
|
|
9
|
+
* Safe rotation order (hard requirement):
|
|
10
|
+
* 1. dump → create local backup file
|
|
11
|
+
* 2. upload → push to remote storage
|
|
12
|
+
* 3. verify → confirm the remote file ID exists
|
|
13
|
+
* 4. retain → delete old backups (ONLY after upload is confirmed)
|
|
14
|
+
* 5. notify → send success / failure email
|
|
15
|
+
*
|
|
16
|
+
* If any step before retention fails, we stop immediately:
|
|
17
|
+
* - Nothing is deleted from remote storage
|
|
18
|
+
* - A failure email is sent (if configured)
|
|
19
|
+
*/
|
|
20
|
+
export declare function runBackup(config: BackupConfig, dbAdapter: DatabaseAdapter, storageAdapter: StorageAdapter): Promise<BackupResult>;
|