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.
@@ -0,0 +1,137 @@
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.runBackup = runBackup;
37
+ const fs = __importStar(require("fs"));
38
+ const retention_1 = require("./retention");
39
+ const mail_1 = require("../notify/mail");
40
+ const logger_1 = require("./logger");
41
+ /**
42
+ * Core backup orchestrator.
43
+ *
44
+ * Accepts adapters by interface — never imports mongo.ts or gdrive.ts directly.
45
+ * This is the key extension point: swap in any DatabaseAdapter / StorageAdapter
46
+ * to support new databases or storage backends without changing this file.
47
+ *
48
+ * Safe rotation order (hard requirement):
49
+ * 1. dump → create local backup file
50
+ * 2. upload → push to remote storage
51
+ * 3. verify → confirm the remote file ID exists
52
+ * 4. retain → delete old backups (ONLY after upload is confirmed)
53
+ * 5. notify → send success / failure email
54
+ *
55
+ * If any step before retention fails, we stop immediately:
56
+ * - Nothing is deleted from remote storage
57
+ * - A failure email is sent (if configured)
58
+ */
59
+ async function runBackup(config, dbAdapter, storageAdapter) {
60
+ const timestamp = new Date();
61
+ let localFilePath = null;
62
+ try {
63
+ // -----------------------------------------------------------------------
64
+ // Step 1: Dump database
65
+ // -----------------------------------------------------------------------
66
+ logger_1.logger.info('=== dbkeeper: starting backup ===');
67
+ const dumpResult = await dbAdapter.dump(config.database);
68
+ localFilePath = dumpResult.filePath;
69
+ logger_1.logger.info(`Dump complete: ${localFilePath} (${dumpResult.sizeBytes} bytes)`);
70
+ // -----------------------------------------------------------------------
71
+ // Step 2: Upload to storage
72
+ // -----------------------------------------------------------------------
73
+ logger_1.logger.info('Uploading backup…');
74
+ const uploadResult = await storageAdapter.upload(localFilePath, config.storage);
75
+ logger_1.logger.info(`Upload complete. Remote ID: ${uploadResult.remoteId}`);
76
+ // -----------------------------------------------------------------------
77
+ // Step 3: Verify the upload (confirm the remote ID is non-empty)
78
+ // -----------------------------------------------------------------------
79
+ if (!uploadResult.remoteId || uploadResult.remoteId.trim() === '') {
80
+ throw new Error('Upload verification failed: storage adapter returned an empty remote ID.');
81
+ }
82
+ logger_1.logger.info('Upload verified ✓');
83
+ // -----------------------------------------------------------------------
84
+ // Step 4: Apply retention (ONLY now that upload is confirmed safe)
85
+ // -----------------------------------------------------------------------
86
+ const existingFiles = await storageAdapter.list(config.storage);
87
+ const { toDelete, remainingCount } = (0, retention_1.applyRetention)(existingFiles, config.retention.keep);
88
+ let deletedCount = 0;
89
+ for (const file of toDelete) {
90
+ logger_1.logger.info(`Retention: deleting old backup ${file.remoteId} (created ${file.createdAt.toISOString()})`);
91
+ await storageAdapter.delete(file.remoteId, config.storage);
92
+ deletedCount++;
93
+ }
94
+ if (deletedCount > 0) {
95
+ logger_1.logger.info(`Retention: deleted ${deletedCount} old backup(s). ${remainingCount} remain.`);
96
+ }
97
+ else {
98
+ logger_1.logger.info(`Retention: no deletions needed. ${remainingCount} backup(s) in storage.`);
99
+ }
100
+ // -----------------------------------------------------------------------
101
+ // Step 5: Notify success
102
+ // -----------------------------------------------------------------------
103
+ const success = {
104
+ ok: true,
105
+ filePath: localFilePath,
106
+ sizeBytes: dumpResult.sizeBytes,
107
+ remoteId: uploadResult.remoteId,
108
+ deletedCount,
109
+ remainingCount,
110
+ timestamp,
111
+ };
112
+ await (0, mail_1.sendSuccessEmail)(config, success);
113
+ logger_1.logger.info('=== dbkeeper: backup completed successfully ===');
114
+ return success;
115
+ }
116
+ catch (err) {
117
+ const error = err instanceof Error ? err : new Error(String(err));
118
+ logger_1.logger.error(`=== dbkeeper: backup FAILED — ${error.message} ===`);
119
+ const failure = { ok: false, error, timestamp };
120
+ await (0, mail_1.sendFailureEmail)(config, failure);
121
+ return failure;
122
+ }
123
+ finally {
124
+ // Always clean up the local temp archive, regardless of success/failure
125
+ if (localFilePath && fs.existsSync(localFilePath)) {
126
+ try {
127
+ // Remove the parent temp directory (which contains only the archive)
128
+ const tmpDir = require('path').dirname(localFilePath);
129
+ fs.rmSync(tmpDir, { recursive: true, force: true });
130
+ logger_1.logger.info(`Cleaned up local temp dir: ${tmpDir}`);
131
+ }
132
+ catch {
133
+ // Non-fatal: temp file cleanup failure should not mask a real error
134
+ }
135
+ }
136
+ }
137
+ }
@@ -0,0 +1,10 @@
1
+ import { DatabaseAdapter } from '../types';
2
+ /**
3
+ * MongoDB database adapter.
4
+ *
5
+ * Uses `mongodump` (must be on PATH) to create a BSON dump of the database,
6
+ * then compresses it into a `.tar.gz` archive using the `archiver` package.
7
+ *
8
+ * Implements {@link DatabaseAdapter}.
9
+ */
10
+ export declare const mongoAdapter: DatabaseAdapter;
@@ -0,0 +1,102 @@
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.mongoAdapter = void 0;
37
+ const os = __importStar(require("os"));
38
+ const path = __importStar(require("path"));
39
+ const fs = __importStar(require("fs"));
40
+ const child_process_1 = require("child_process");
41
+ const archiver_1 = require("archiver");
42
+ const logger_1 = require("../core/logger");
43
+ /**
44
+ * MongoDB database adapter.
45
+ *
46
+ * Uses `mongodump` (must be on PATH) to create a BSON dump of the database,
47
+ * then compresses it into a `.tar.gz` archive using the `archiver` package.
48
+ *
49
+ * Implements {@link DatabaseAdapter}.
50
+ */
51
+ exports.mongoAdapter = {
52
+ async dump(config) {
53
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
54
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dbkeeper-'));
55
+ const dumpDir = path.join(tmpDir, 'dump');
56
+ const archivePath = path.join(tmpDir, `backup-mongo-${timestamp}.tar.gz`);
57
+ logger_1.logger.info(`[mongo] Starting mongodump → ${dumpDir}`);
58
+ // -----------------------------------------------------------------------
59
+ // 1. Run mongodump
60
+ // -----------------------------------------------------------------------
61
+ const result = (0, child_process_1.spawnSync)('mongodump', ['--uri', config.uri, '--out', dumpDir], { stdio: 'pipe', encoding: 'utf8' });
62
+ if (result.error) {
63
+ throw new Error(`[mongo] Failed to spawn mongodump. Is mongodump installed and on PATH?\n` +
64
+ result.error.message);
65
+ }
66
+ if (result.status !== 0) {
67
+ const stderr = result.stderr ?? '';
68
+ throw new Error(`[mongo] mongodump exited with code ${result.status}:\n${stderr}`);
69
+ }
70
+ logger_1.logger.info('[mongo] mongodump completed. Compressing…');
71
+ // -----------------------------------------------------------------------
72
+ // 2. Compress the dump directory into a tar.gz
73
+ // -----------------------------------------------------------------------
74
+ await compressDirectory(dumpDir, archivePath);
75
+ const { size } = fs.statSync(archivePath);
76
+ logger_1.logger.info(`[mongo] Archive ready: ${archivePath} (${formatBytes(size)})`);
77
+ // Clean up the raw dump dir — we only need the archive going forward
78
+ fs.rmSync(dumpDir, { recursive: true, force: true });
79
+ return { filePath: archivePath, sizeBytes: size };
80
+ },
81
+ };
82
+ // ---------------------------------------------------------------------------
83
+ // Helpers
84
+ // ---------------------------------------------------------------------------
85
+ function compressDirectory(sourceDir, destPath) {
86
+ return new Promise((resolve, reject) => {
87
+ const output = fs.createWriteStream(destPath);
88
+ const archive = new archiver_1.TarArchive({ gzip: true, gzipOptions: { level: 9 } });
89
+ output.on('close', resolve);
90
+ archive.on('error', reject);
91
+ archive.pipe(output);
92
+ archive.directory(sourceDir, false); // false = don't add parent dir prefix
93
+ archive.finalize();
94
+ });
95
+ }
96
+ function formatBytes(bytes) {
97
+ if (bytes < 1024)
98
+ return `${bytes} B`;
99
+ if (bytes < 1024 * 1024)
100
+ return `${(bytes / 1024).toFixed(1)} KB`;
101
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
102
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * dbkeeper — public programmatic API
3
+ *
4
+ * Usage (TypeScript):
5
+ * import { runBackup, mongoAdapter, gdriveAdapter } from 'dbkeeper';
6
+ *
7
+ * Usage (CommonJS):
8
+ * const { runBackup, mongoAdapter, gdriveAdapter } = require('dbkeeper');
9
+ */
10
+ export { runBackup } from './core/runBackup';
11
+ export { mongoAdapter } from './database/mongo';
12
+ export { gdriveAdapter } from './storage/gdrive';
13
+ export { loadConfig } from './config/loadConfig';
14
+ export { applyRetention } from './core/retention';
15
+ export { logger } from './core/logger';
16
+ export type { BackupConfig, BackupResult, BackupSuccess, BackupFailure, DatabaseAdapter, StorageAdapter, DatabaseConfig, StorageConfig, MongoDatabaseConfig, GDriveStorageConfig, RetentionConfig, NotificationsConfig, } from './types';
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ /**
3
+ * dbkeeper — public programmatic API
4
+ *
5
+ * Usage (TypeScript):
6
+ * import { runBackup, mongoAdapter, gdriveAdapter } from 'dbkeeper';
7
+ *
8
+ * Usage (CommonJS):
9
+ * const { runBackup, mongoAdapter, gdriveAdapter } = require('dbkeeper');
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.logger = exports.applyRetention = exports.loadConfig = exports.gdriveAdapter = exports.mongoAdapter = exports.runBackup = void 0;
13
+ var runBackup_1 = require("./core/runBackup");
14
+ Object.defineProperty(exports, "runBackup", { enumerable: true, get: function () { return runBackup_1.runBackup; } });
15
+ var mongo_1 = require("./database/mongo");
16
+ Object.defineProperty(exports, "mongoAdapter", { enumerable: true, get: function () { return mongo_1.mongoAdapter; } });
17
+ var gdrive_1 = require("./storage/gdrive");
18
+ Object.defineProperty(exports, "gdriveAdapter", { enumerable: true, get: function () { return gdrive_1.gdriveAdapter; } });
19
+ var loadConfig_1 = require("./config/loadConfig");
20
+ Object.defineProperty(exports, "loadConfig", { enumerable: true, get: function () { return loadConfig_1.loadConfig; } });
21
+ var retention_1 = require("./core/retention");
22
+ Object.defineProperty(exports, "applyRetention", { enumerable: true, get: function () { return retention_1.applyRetention; } });
23
+ var logger_1 = require("./core/logger");
24
+ Object.defineProperty(exports, "logger", { enumerable: true, get: function () { return logger_1.logger; } });
@@ -0,0 +1,11 @@
1
+ import { BackupConfig, BackupSuccess, BackupFailure } from '../types';
2
+ /**
3
+ * Sends a success email if `config.notifications.success` is true.
4
+ * Never throws — email failures are logged but do not fail the backup run.
5
+ */
6
+ export declare function sendSuccessEmail(config: BackupConfig, result: BackupSuccess): Promise<void>;
7
+ /**
8
+ * Sends a failure email if `config.notifications.failure` is true.
9
+ * Never throws — email failures are logged but do not mask the original error.
10
+ */
11
+ export declare function sendFailureEmail(config: BackupConfig, result: BackupFailure): Promise<void>;
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.sendSuccessEmail = sendSuccessEmail;
7
+ exports.sendFailureEmail = sendFailureEmail;
8
+ const nodemailer_1 = __importDefault(require("nodemailer"));
9
+ const logger_1 = require("../core/logger");
10
+ /**
11
+ * Sends a success email if `config.notifications.success` is true.
12
+ * Never throws — email failures are logged but do not fail the backup run.
13
+ */
14
+ async function sendSuccessEmail(config, result) {
15
+ if (!config.notifications.success)
16
+ return;
17
+ const subject = `✅ dbkeeper — backup succeeded (${result.timestamp.toISOString()})`;
18
+ const text = [
19
+ `Backup completed successfully.`,
20
+ ``,
21
+ ` Database: ${config.database.type}`,
22
+ ` File: ${result.filePath.split(/[\\/]/).pop()}`,
23
+ ` Size: ${formatBytes(result.sizeBytes)}`,
24
+ ` Storage: ${config.storage.type}`,
25
+ ` Remote ID: ${result.remoteId}`,
26
+ ` Backups kept: ${result.remainingCount}`,
27
+ ` Backups deleted: ${result.deletedCount}`,
28
+ ` Timestamp: ${result.timestamp.toISOString()}`,
29
+ ].join('\n');
30
+ await sendMail(config, subject, text);
31
+ }
32
+ /**
33
+ * Sends a failure email if `config.notifications.failure` is true.
34
+ * Never throws — email failures are logged but do not mask the original error.
35
+ */
36
+ async function sendFailureEmail(config, result) {
37
+ if (!config.notifications.failure)
38
+ return;
39
+ const subject = `❌ dbkeeper — backup FAILED (${result.timestamp.toISOString()})`;
40
+ const text = [
41
+ `Backup failed.`,
42
+ ``,
43
+ ` Error: ${result.error.message}`,
44
+ ` Timestamp: ${result.timestamp.toISOString()}`,
45
+ ``,
46
+ `Stack trace:`,
47
+ result.error.stack ?? '(no stack available)',
48
+ ].join('\n');
49
+ await sendMail(config, subject, text);
50
+ }
51
+ // ---------------------------------------------------------------------------
52
+ // Internal helpers
53
+ // ---------------------------------------------------------------------------
54
+ async function sendMail(config, subject, text) {
55
+ try {
56
+ const smtp = config.notifications.smtp;
57
+ // If no SMTP block is provided, fall back to a JSON-logger so
58
+ // self-hosted setups without a mail server don't crash.
59
+ if (!smtp) {
60
+ logger_1.logger.warn('[mail] No SMTP config provided — printing email to console instead.\n' +
61
+ `Subject: ${subject}\n\n${text}`);
62
+ return;
63
+ }
64
+ const transporter = nodemailer_1.default.createTransport({
65
+ host: smtp.host,
66
+ port: smtp.port ?? 587,
67
+ secure: smtp.secure ?? false,
68
+ auth: smtp.auth,
69
+ });
70
+ await transporter.sendMail({
71
+ from: smtp.auth?.user ?? 'dbkeeper@noreply.local',
72
+ to: config.notifications.to,
73
+ subject,
74
+ text,
75
+ });
76
+ logger_1.logger.info(`[mail] Email sent to ${config.notifications.to}: "${subject}"`);
77
+ }
78
+ catch (err) {
79
+ // Non-fatal: log the email send error, but never propagate it
80
+ logger_1.logger.error(`[mail] Failed to send email: ${err.message}`);
81
+ }
82
+ }
83
+ function formatBytes(bytes) {
84
+ if (bytes < 1024)
85
+ return `${bytes} B`;
86
+ if (bytes < 1024 * 1024)
87
+ return `${(bytes / 1024).toFixed(1)} KB`;
88
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
89
+ }
@@ -0,0 +1,11 @@
1
+ import { StorageAdapter } from '../types';
2
+ /**
3
+ * Google Drive storage adapter.
4
+ *
5
+ * Uses a service-account JSON key (path supplied in config) to authenticate
6
+ * with the Drive v3 API. All backup files are scoped to a single Drive
7
+ * folder supplied via `config.folderId`.
8
+ *
9
+ * Implements {@link StorageAdapter}.
10
+ */
11
+ export declare const gdriveAdapter: StorageAdapter;
@@ -0,0 +1,132 @@
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.gdriveAdapter = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const googleapis_1 = require("googleapis");
40
+ const logger_1 = require("../core/logger");
41
+ /**
42
+ * Google Drive storage adapter.
43
+ *
44
+ * Uses a service-account JSON key (path supplied in config) to authenticate
45
+ * with the Drive v3 API. All backup files are scoped to a single Drive
46
+ * folder supplied via `config.folderId`.
47
+ *
48
+ * Implements {@link StorageAdapter}.
49
+ */
50
+ exports.gdriveAdapter = {
51
+ // -------------------------------------------------------------------------
52
+ // upload
53
+ // -------------------------------------------------------------------------
54
+ async upload(filePath, config) {
55
+ const drive = await buildDriveClient(config.credentialsPath);
56
+ const fileName = path.basename(filePath);
57
+ const fileSize = fs.statSync(filePath).size;
58
+ logger_1.logger.info(`[gdrive] Uploading "${fileName}" (${formatBytes(fileSize)}) to folder ${config.folderId}`);
59
+ const response = await drive.files.create({
60
+ requestBody: {
61
+ name: fileName,
62
+ parents: [config.folderId],
63
+ },
64
+ media: {
65
+ mimeType: 'application/gzip',
66
+ body: fs.createReadStream(filePath),
67
+ },
68
+ fields: 'id, name, size',
69
+ });
70
+ const remoteId = response.data.id;
71
+ if (!remoteId) {
72
+ throw new Error('[gdrive] Upload succeeded but Drive returned no file ID');
73
+ }
74
+ logger_1.logger.info(`[gdrive] Upload complete. Remote ID: ${remoteId}`);
75
+ return { remoteId };
76
+ },
77
+ // -------------------------------------------------------------------------
78
+ // list
79
+ // -------------------------------------------------------------------------
80
+ async list(config) {
81
+ const drive = await buildDriveClient(config.credentialsPath);
82
+ logger_1.logger.info(`[gdrive] Listing backup files in folder ${config.folderId}`);
83
+ // Drive API paginates at 1000; for typical backup retention (≤100s of files)
84
+ // a single page is always sufficient. Add pagination if you ever use keep > 900.
85
+ const response = await drive.files.list({
86
+ q: `'${config.folderId}' in parents and trashed = false`,
87
+ fields: 'files(id, name, createdTime)',
88
+ orderBy: 'createdTime asc',
89
+ pageSize: 1000,
90
+ });
91
+ const files = response.data.files ?? [];
92
+ return files
93
+ .filter((f) => f.id && f.createdTime)
94
+ .map((f) => ({
95
+ remoteId: f.id,
96
+ createdAt: new Date(f.createdTime),
97
+ }));
98
+ },
99
+ // -------------------------------------------------------------------------
100
+ // delete
101
+ // -------------------------------------------------------------------------
102
+ async delete(remoteId, config) {
103
+ const drive = await buildDriveClient(config.credentialsPath);
104
+ logger_1.logger.info(`[gdrive] Deleting remote file: ${remoteId}`);
105
+ await drive.files.delete({ fileId: remoteId });
106
+ logger_1.logger.info(`[gdrive] Deleted: ${remoteId}`);
107
+ },
108
+ };
109
+ // ---------------------------------------------------------------------------
110
+ // Internal helpers
111
+ // ---------------------------------------------------------------------------
112
+ async function buildDriveClient(credentialsPath) {
113
+ const resolvedPath = path.resolve(credentialsPath);
114
+ if (!fs.existsSync(resolvedPath)) {
115
+ throw new Error(`[gdrive] Service account key not found at "${resolvedPath}".\n` +
116
+ `Set credentialsPath in your backup.config.js to the correct path.`);
117
+ }
118
+ const raw = fs.readFileSync(resolvedPath, 'utf8');
119
+ const credentials = JSON.parse(raw);
120
+ const auth = new googleapis_1.google.auth.GoogleAuth({
121
+ credentials,
122
+ scopes: ['https://www.googleapis.com/auth/drive'],
123
+ });
124
+ return googleapis_1.google.drive({ version: 'v3', auth });
125
+ }
126
+ function formatBytes(bytes) {
127
+ if (bytes < 1024)
128
+ return `${bytes} B`;
129
+ if (bytes < 1024 * 1024)
130
+ return `${(bytes / 1024).toFixed(1)} KB`;
131
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
132
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Adapter contract for database engines.
3
+ * Implement this interface to add a new database type (e.g. Postgres, MySQL).
4
+ */
5
+ export interface DatabaseAdapter {
6
+ dump(config: DatabaseConfig): Promise<{
7
+ filePath: string;
8
+ sizeBytes: number;
9
+ }>;
10
+ }
11
+ /**
12
+ * Adapter contract for storage backends.
13
+ * Implement this interface to add a new storage provider (e.g. S3, Dropbox).
14
+ */
15
+ export interface StorageAdapter {
16
+ upload(filePath: string, config: StorageConfig): Promise<{
17
+ remoteId: string;
18
+ }>;
19
+ list(config: StorageConfig): Promise<{
20
+ remoteId: string;
21
+ createdAt: Date;
22
+ }[]>;
23
+ delete(remoteId: string, config: StorageConfig): Promise<void>;
24
+ }
25
+ export interface MongoDatabaseConfig {
26
+ type: 'mongo';
27
+ uri: string;
28
+ }
29
+ export type DatabaseConfig = MongoDatabaseConfig;
30
+ export interface GDriveStorageConfig {
31
+ type: 'gdrive';
32
+ credentialsPath: string;
33
+ folderId: string;
34
+ }
35
+ export type StorageConfig = GDriveStorageConfig;
36
+ export interface RetentionConfig {
37
+ /** Number of most-recent backups to keep, or "unlimited" to never delete. */
38
+ keep: number | 'unlimited';
39
+ }
40
+ export interface NotificationsConfig {
41
+ /** Send an email when a backup succeeds. */
42
+ success: boolean;
43
+ /** Send an email when a backup fails. */
44
+ failure: boolean;
45
+ /** Recipient email address. */
46
+ to: string;
47
+ /** Nodemailer SMTP transport options — see https://nodemailer.com/smtp/ */
48
+ smtp?: {
49
+ host: string;
50
+ port?: number;
51
+ secure?: boolean;
52
+ auth?: {
53
+ user: string;
54
+ pass: string;
55
+ };
56
+ };
57
+ }
58
+ export interface BackupConfig {
59
+ database: DatabaseConfig;
60
+ storage: StorageConfig;
61
+ retention: RetentionConfig;
62
+ notifications: NotificationsConfig;
63
+ }
64
+ export interface BackupSuccess {
65
+ ok: true;
66
+ filePath: string;
67
+ sizeBytes: number;
68
+ remoteId: string;
69
+ deletedCount: number;
70
+ remainingCount: number;
71
+ timestamp: Date;
72
+ }
73
+ export interface BackupFailure {
74
+ ok: false;
75
+ error: Error;
76
+ timestamp: Date;
77
+ }
78
+ export type BackupResult = BackupSuccess | BackupFailure;
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });