dbdock 1.1.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 +596 -0
- package/dist/alerts/alert-templates.d.ts +3 -0
- package/dist/alerts/alert-templates.js +95 -0
- package/dist/alerts/alert-templates.js.map +1 -0
- package/dist/alerts/alert.module.d.ts +2 -0
- package/dist/alerts/alert.module.js +23 -0
- package/dist/alerts/alert.module.js.map +1 -0
- package/dist/alerts/alert.service.d.ts +23 -0
- package/dist/alerts/alert.service.js +210 -0
- package/dist/alerts/alert.service.js.map +1 -0
- package/dist/alerts/alert.types.d.ts +24 -0
- package/dist/alerts/alert.types.js +11 -0
- package/dist/alerts/alert.types.js.map +1 -0
- package/dist/app.module.d.ts +2 -0
- package/dist/app.module.js +34 -0
- package/dist/app.module.js.map +1 -0
- package/dist/backup/backup.module.d.ts +2 -0
- package/dist/backup/backup.module.js +26 -0
- package/dist/backup/backup.module.js.map +1 -0
- package/dist/backup/backup.service.d.ts +23 -0
- package/dist/backup/backup.service.js +303 -0
- package/dist/backup/backup.service.js.map +1 -0
- package/dist/backup/backup.types.d.ts +42 -0
- package/dist/backup/backup.types.js +16 -0
- package/dist/backup/backup.types.js.map +1 -0
- package/dist/backup/compression.service.d.ts +6 -0
- package/dist/backup/compression.service.js +30 -0
- package/dist/backup/compression.service.js.map +1 -0
- package/dist/cli/commands/backup.d.ts +8 -0
- package/dist/cli/commands/backup.js +198 -0
- package/dist/cli/commands/backup.js.map +1 -0
- package/dist/cli/commands/cleanup.d.ts +6 -0
- package/dist/cli/commands/cleanup.js +160 -0
- package/dist/cli/commands/cleanup.js.map +1 -0
- package/dist/cli/commands/delete.d.ts +6 -0
- package/dist/cli/commands/delete.js +252 -0
- package/dist/cli/commands/delete.js.map +1 -0
- package/dist/cli/commands/init.d.ts +1 -0
- package/dist/cli/commands/init.js +534 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/list.d.ts +8 -0
- package/dist/cli/commands/list.js +288 -0
- package/dist/cli/commands/list.js.map +1 -0
- package/dist/cli/commands/restore.d.ts +1 -0
- package/dist/cli/commands/restore.js +637 -0
- package/dist/cli/commands/restore.js.map +1 -0
- package/dist/cli/commands/schedule.d.ts +1 -0
- package/dist/cli/commands/schedule.js +197 -0
- package/dist/cli/commands/schedule.js.map +1 -0
- package/dist/cli/commands/start.d.ts +7 -0
- package/dist/cli/commands/start.js +267 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/commands/status.d.ts +1 -0
- package/dist/cli/commands/status.js +46 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/test.d.ts +1 -0
- package/dist/cli/commands/test.js +212 -0
- package/dist/cli/commands/test.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +78 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/utils/config.d.ts +80 -0
- package/dist/cli/utils/config.js +29 -0
- package/dist/cli/utils/config.js.map +1 -0
- package/dist/cli/utils/logger.d.ts +7 -0
- package/dist/cli/utils/logger.js +15 -0
- package/dist/cli/utils/logger.js.map +1 -0
- package/dist/cli/utils/progress.d.ts +21 -0
- package/dist/cli/utils/progress.js +130 -0
- package/dist/cli/utils/progress.js.map +1 -0
- package/dist/cli/utils/retention.d.ts +26 -0
- package/dist/cli/utils/retention.js +118 -0
- package/dist/cli/utils/retention.js.map +1 -0
- package/dist/config/config.module.d.ts +2 -0
- package/dist/config/config.module.js +29 -0
- package/dist/config/config.module.js.map +1 -0
- package/dist/config/config.schema.d.ts +56 -0
- package/dist/config/config.schema.js +219 -0
- package/dist/config/config.schema.js.map +1 -0
- package/dist/config/config.service.d.ts +13 -0
- package/dist/config/config.service.js +160 -0
- package/dist/config/config.service.js.map +1 -0
- package/dist/crypto/crypto.module.d.ts +2 -0
- package/dist/crypto/crypto.module.js +21 -0
- package/dist/crypto/crypto.module.js.map +1 -0
- package/dist/crypto/crypto.service.d.ts +22 -0
- package/dist/crypto/crypto.service.js +187 -0
- package/dist/crypto/crypto.service.js.map +1 -0
- package/dist/dbdock.d.ts +10 -0
- package/dist/dbdock.js +36 -0
- package/dist/dbdock.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +77 -0
- package/dist/index.js.map +1 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +10 -0
- package/dist/main.js.map +1 -0
- package/dist/scheduler/schedule-manager.d.ts +22 -0
- package/dist/scheduler/schedule-manager.js +126 -0
- package/dist/scheduler/schedule-manager.js.map +1 -0
- package/dist/scheduler/scheduler.module.d.ts +2 -0
- package/dist/scheduler/scheduler.module.js +25 -0
- package/dist/scheduler/scheduler.module.js.map +1 -0
- package/dist/scheduler/scheduler.service.d.ts +28 -0
- package/dist/scheduler/scheduler.service.js +171 -0
- package/dist/scheduler/scheduler.service.js.map +1 -0
- package/dist/standalone/backup-standalone.d.ts +14 -0
- package/dist/standalone/backup-standalone.js +364 -0
- package/dist/standalone/backup-standalone.js.map +1 -0
- package/dist/storage/adapters/cloudinary.adapter.d.ts +23 -0
- package/dist/storage/adapters/cloudinary.adapter.js +215 -0
- package/dist/storage/adapters/cloudinary.adapter.js.map +1 -0
- package/dist/storage/adapters/local.adapter.d.ts +20 -0
- package/dist/storage/adapters/local.adapter.js +214 -0
- package/dist/storage/adapters/local.adapter.js.map +1 -0
- package/dist/storage/adapters/r2.adapter.d.ts +10 -0
- package/dist/storage/adapters/r2.adapter.js +33 -0
- package/dist/storage/adapters/r2.adapter.js.map +1 -0
- package/dist/storage/adapters/s3.adapter.d.ts +26 -0
- package/dist/storage/adapters/s3.adapter.js +199 -0
- package/dist/storage/adapters/s3.adapter.js.map +1 -0
- package/dist/storage/storage.interface.d.ts +38 -0
- package/dist/storage/storage.interface.js +3 -0
- package/dist/storage/storage.interface.js.map +1 -0
- package/dist/storage/storage.module.d.ts +2 -0
- package/dist/storage/storage.module.js +21 -0
- package/dist/storage/storage.module.js.map +1 -0
- package/dist/storage/storage.service.d.ts +10 -0
- package/dist/storage/storage.service.js +89 -0
- package/dist/storage/storage.service.js.map +1 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +41 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/stream.pipe.d.ts +17 -0
- package/dist/utils/stream.pipe.js +54 -0
- package/dist/utils/stream.pipe.js.map +1 -0
- package/dist/wal/postgres-config.helper.d.ts +5 -0
- package/dist/wal/postgres-config.helper.js +117 -0
- package/dist/wal/postgres-config.helper.js.map +1 -0
- package/dist/wal/retention.service.d.ts +23 -0
- package/dist/wal/retention.service.js +158 -0
- package/dist/wal/retention.service.js.map +1 -0
- package/dist/wal/retention.types.d.ts +20 -0
- package/dist/wal/retention.types.js +3 -0
- package/dist/wal/retention.types.js.map +1 -0
- package/dist/wal/wal-archiver.service.d.ts +28 -0
- package/dist/wal/wal-archiver.service.js +263 -0
- package/dist/wal/wal-archiver.service.js.map +1 -0
- package/dist/wal/wal.module.d.ts +2 -0
- package/dist/wal/wal.module.js +26 -0
- package/dist/wal/wal.module.js.map +1 -0
- package/dist/wal/wal.types.d.ts +27 -0
- package/dist/wal/wal.types.js +11 -0
- package/dist/wal/wal.types.js.map +1 -0
- package/package.json +155 -0
|
@@ -0,0 +1,637 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.restoreCommand = restoreCommand;
|
|
40
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
41
|
+
const ora_1 = __importDefault(require("ora"));
|
|
42
|
+
const config_1 = require("../utils/config");
|
|
43
|
+
const logger_1 = require("../utils/logger");
|
|
44
|
+
const local_adapter_1 = require("../../storage/adapters/local.adapter");
|
|
45
|
+
const s3_adapter_1 = require("../../storage/adapters/s3.adapter");
|
|
46
|
+
const r2_adapter_1 = require("../../storage/adapters/r2.adapter");
|
|
47
|
+
const cloudinary_adapter_1 = require("../../storage/adapters/cloudinary.adapter");
|
|
48
|
+
const child_process_1 = require("child_process");
|
|
49
|
+
const zlib_1 = require("zlib");
|
|
50
|
+
const crypto_1 = require("crypto");
|
|
51
|
+
const os_1 = require("os");
|
|
52
|
+
const path_1 = require("path");
|
|
53
|
+
const fs_1 = require("fs");
|
|
54
|
+
const common_1 = require("@nestjs/common");
|
|
55
|
+
const progress_1 = require("../utils/progress");
|
|
56
|
+
common_1.Logger.overrideLogger(false);
|
|
57
|
+
async function restoreCommand() {
|
|
58
|
+
const spinner = (0, ora_1.default)('Loading configuration...').start();
|
|
59
|
+
try {
|
|
60
|
+
const config = (0, config_1.loadConfig)();
|
|
61
|
+
spinner.succeed('Configuration loaded');
|
|
62
|
+
let adapter;
|
|
63
|
+
switch (config.storage.provider) {
|
|
64
|
+
case 'local':
|
|
65
|
+
adapter = new local_adapter_1.LocalStorageAdapter(config.storage.local?.path || './backups');
|
|
66
|
+
break;
|
|
67
|
+
case 's3':
|
|
68
|
+
if (!config.storage.s3?.accessKeyId ||
|
|
69
|
+
!config.storage.s3?.secretAccessKey) {
|
|
70
|
+
spinner.fail('S3 credentials are required');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
adapter = new s3_adapter_1.S3StorageAdapter({
|
|
74
|
+
endpoint: config.storage.s3.endpoint,
|
|
75
|
+
bucket: config.storage.s3.bucket || '',
|
|
76
|
+
accessKeyId: config.storage.s3.accessKeyId,
|
|
77
|
+
secretAccessKey: config.storage.s3.secretAccessKey,
|
|
78
|
+
});
|
|
79
|
+
break;
|
|
80
|
+
case 'r2': {
|
|
81
|
+
if (!config.storage.s3?.accessKeyId ||
|
|
82
|
+
!config.storage.s3?.secretAccessKey) {
|
|
83
|
+
spinner.fail('R2 credentials are required');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
if (!config.storage.s3?.endpoint) {
|
|
87
|
+
spinner.fail('R2 endpoint is required');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
const accountId = config.storage.s3.endpoint.match(/https:\/\/([^.]+)/)?.[1];
|
|
91
|
+
if (!accountId) {
|
|
92
|
+
spinner.fail('Invalid R2 endpoint format');
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
adapter = new r2_adapter_1.R2StorageAdapter({
|
|
96
|
+
accountId,
|
|
97
|
+
bucket: config.storage.s3.bucket || '',
|
|
98
|
+
accessKeyId: config.storage.s3.accessKeyId,
|
|
99
|
+
secretAccessKey: config.storage.s3.secretAccessKey,
|
|
100
|
+
});
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case 'cloudinary':
|
|
104
|
+
if (!config.storage.cloudinary?.cloudName ||
|
|
105
|
+
!config.storage.cloudinary?.apiKey ||
|
|
106
|
+
!config.storage.cloudinary?.apiSecret) {
|
|
107
|
+
spinner.fail('Cloudinary credentials are required');
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
adapter = new cloudinary_adapter_1.CloudinaryStorageAdapter({
|
|
111
|
+
cloudName: config.storage.cloudinary.cloudName,
|
|
112
|
+
apiKey: config.storage.cloudinary.apiKey,
|
|
113
|
+
apiSecret: config.storage.cloudinary.apiSecret,
|
|
114
|
+
folder: 'dbdock_backups',
|
|
115
|
+
});
|
|
116
|
+
break;
|
|
117
|
+
default:
|
|
118
|
+
spinner.fail(`Unknown storage provider: ${config.storage.provider}`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
spinner.start('Loading backups...');
|
|
122
|
+
let objects;
|
|
123
|
+
try {
|
|
124
|
+
let prefix;
|
|
125
|
+
if (config.storage.provider === 'local') {
|
|
126
|
+
prefix = 'backup-';
|
|
127
|
+
}
|
|
128
|
+
else if (config.storage.provider === 'cloudinary') {
|
|
129
|
+
prefix = 'backup-';
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
prefix = 'dbdock_backups/backup-';
|
|
133
|
+
}
|
|
134
|
+
objects = await adapter.listObjects({ prefix });
|
|
135
|
+
objects = objects
|
|
136
|
+
.filter((obj) => {
|
|
137
|
+
const key = obj.key.toLowerCase();
|
|
138
|
+
return (key.includes('backup-') &&
|
|
139
|
+
(key.endsWith('.sql') ||
|
|
140
|
+
key.endsWith('.dump') ||
|
|
141
|
+
key.endsWith('.tar') ||
|
|
142
|
+
key.endsWith('.dir') ||
|
|
143
|
+
config.storage.provider === 'cloudinary'));
|
|
144
|
+
})
|
|
145
|
+
.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
spinner.fail('Failed to list backups');
|
|
149
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
150
|
+
logger_1.logger.error(`\n${errorMessage}\n`);
|
|
151
|
+
if (config.storage.provider === 's3') {
|
|
152
|
+
logger_1.logger.info('Common S3 issues:');
|
|
153
|
+
logger_1.logger.log(' • Verify AWS credentials are correct');
|
|
154
|
+
logger_1.logger.log(' • Ensure IAM user has s3:ListBucket permission');
|
|
155
|
+
logger_1.logger.log(' • Check bucket name and region are correct');
|
|
156
|
+
logger_1.logger.log(' • Verify bucket exists and is accessible');
|
|
157
|
+
}
|
|
158
|
+
else if (config.storage.provider === 'r2') {
|
|
159
|
+
logger_1.logger.info('Common R2 issues:');
|
|
160
|
+
logger_1.logger.log(' • Verify R2 API token is correct');
|
|
161
|
+
logger_1.logger.log(' • Ensure endpoint URL is correct');
|
|
162
|
+
logger_1.logger.log(' • Check bucket name is correct');
|
|
163
|
+
logger_1.logger.log(' • Verify bucket exists and is accessible');
|
|
164
|
+
}
|
|
165
|
+
else if (config.storage.provider === 'cloudinary') {
|
|
166
|
+
logger_1.logger.info('Common Cloudinary issues:');
|
|
167
|
+
logger_1.logger.log(' • Verify cloud name, API key, and secret are correct');
|
|
168
|
+
logger_1.logger.log(' • Check your Cloudinary account is active');
|
|
169
|
+
logger_1.logger.log(' • Ensure API credentials have media library access');
|
|
170
|
+
}
|
|
171
|
+
else if (config.storage.provider === 'local') {
|
|
172
|
+
const localPath = config.storage.local?.path || './backups';
|
|
173
|
+
logger_1.logger.info('Common local storage issues:');
|
|
174
|
+
logger_1.logger.log(` • Verify directory exists: ${localPath}`);
|
|
175
|
+
logger_1.logger.log(' • Check you have read permissions');
|
|
176
|
+
logger_1.logger.log(' • Ensure path is correct in dbdock.config.json');
|
|
177
|
+
}
|
|
178
|
+
logger_1.logger.info('\nTo test your configuration, run:');
|
|
179
|
+
logger_1.logger.log(' npx dbdock test');
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
spinner.succeed(`Found ${objects.length} backup(s)`);
|
|
183
|
+
if (objects.length === 0) {
|
|
184
|
+
logger_1.logger.error('\nNo backups found');
|
|
185
|
+
if (config.storage.provider === 'local') {
|
|
186
|
+
const localPath = config.storage.local?.path || './backups';
|
|
187
|
+
logger_1.logger.info('\nPlease verify:');
|
|
188
|
+
logger_1.logger.log(` • Backup files exist in: ${localPath}`);
|
|
189
|
+
logger_1.logger.log(` • Files are named: backup-*.dump or backup-*.sql`);
|
|
190
|
+
logger_1.logger.log(` • You have read permissions on the directory`);
|
|
191
|
+
}
|
|
192
|
+
else if (config.storage.provider === 's3') {
|
|
193
|
+
const bucket = config.storage.s3?.bucket || '';
|
|
194
|
+
logger_1.logger.info('\nPlease verify:');
|
|
195
|
+
logger_1.logger.log(` • Backups exist in S3 bucket: ${bucket}`);
|
|
196
|
+
logger_1.logger.log(` • Files are in folder: dbdock_backups/`);
|
|
197
|
+
logger_1.logger.log(` • Files are named: backup-*.dump or backup-*.sql`);
|
|
198
|
+
logger_1.logger.log(` • Your AWS credentials have s3:ListBucket permission`);
|
|
199
|
+
}
|
|
200
|
+
else if (config.storage.provider === 'r2') {
|
|
201
|
+
const bucket = config.storage.s3?.bucket || '';
|
|
202
|
+
logger_1.logger.info('\nPlease verify:');
|
|
203
|
+
logger_1.logger.log(` • Backups exist in R2 bucket: ${bucket}`);
|
|
204
|
+
logger_1.logger.log(` • Files are in folder: dbdock_backups/`);
|
|
205
|
+
logger_1.logger.log(` • Files are named: backup-*.dump or backup-*.sql`);
|
|
206
|
+
logger_1.logger.log(` • Your R2 credentials have read permissions`);
|
|
207
|
+
}
|
|
208
|
+
else if (config.storage.provider === 'cloudinary') {
|
|
209
|
+
const cloudName = config.storage.cloudinary?.cloudName || '';
|
|
210
|
+
logger_1.logger.info('\nPlease verify:');
|
|
211
|
+
logger_1.logger.log(` • Backups exist in Cloudinary cloud: ${cloudName}`);
|
|
212
|
+
logger_1.logger.log(` • Files are in folder: dbdock_backups`);
|
|
213
|
+
logger_1.logger.log(` • Files are named: backup-*.dump or backup-*.sql`);
|
|
214
|
+
logger_1.logger.log(` • Your API credentials are correct`);
|
|
215
|
+
logger_1.logger.log(` • Check: https://console.cloudinary.com/console/${cloudName}/media_library/folders/dbdock_backups`);
|
|
216
|
+
}
|
|
217
|
+
logger_1.logger.info('\nTo create a backup, run:');
|
|
218
|
+
logger_1.logger.log(' npx dbdock backup');
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
spinner.start('Analyzing current database...');
|
|
222
|
+
const currentDbStats = await getCurrentDatabaseStats(config);
|
|
223
|
+
spinner.succeed('Database analysis complete');
|
|
224
|
+
logger_1.logger.info('\nCurrent Database Statistics:');
|
|
225
|
+
logger_1.logger.log(` Database: ${currentDbStats.name}`);
|
|
226
|
+
logger_1.logger.log(` Tables: ${currentDbStats.tables}`);
|
|
227
|
+
logger_1.logger.log(` Total Size: ${currentDbStats.size}`);
|
|
228
|
+
logger_1.logger.log(` Estimated Rows: ${currentDbStats.rows}\n`);
|
|
229
|
+
let selectedBackup;
|
|
230
|
+
if (objects.length > 20) {
|
|
231
|
+
logger_1.logger.info(`Found ${objects.length} backups. Let's filter them to find the right one.\n`);
|
|
232
|
+
const { filterOption } = (await inquirer_1.default.prompt([
|
|
233
|
+
{
|
|
234
|
+
type: 'list',
|
|
235
|
+
name: 'filterOption',
|
|
236
|
+
message: 'How would you like to find your backup?',
|
|
237
|
+
choices: [
|
|
238
|
+
{ name: 'Show most recent backups (last 10)', value: 'recent' },
|
|
239
|
+
{ name: 'Filter by date range', value: 'date' },
|
|
240
|
+
{ name: 'Search by keyword/ID', value: 'search' },
|
|
241
|
+
{
|
|
242
|
+
name: 'Show all backups (not recommended for many backups)',
|
|
243
|
+
value: 'all',
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
]));
|
|
248
|
+
let filteredObjects = objects;
|
|
249
|
+
if (filterOption === 'recent') {
|
|
250
|
+
filteredObjects = objects.slice(0, 10);
|
|
251
|
+
logger_1.logger.info(`\nShowing the 10 most recent backups:\n`);
|
|
252
|
+
}
|
|
253
|
+
else if (filterOption === 'date') {
|
|
254
|
+
const { dateFilter } = (await inquirer_1.default.prompt([
|
|
255
|
+
{
|
|
256
|
+
type: 'list',
|
|
257
|
+
name: 'dateFilter',
|
|
258
|
+
message: 'Select time range:',
|
|
259
|
+
choices: [
|
|
260
|
+
{ name: 'Last 24 hours', value: 1 },
|
|
261
|
+
{ name: 'Last 7 days', value: 7 },
|
|
262
|
+
{ name: 'Last 30 days', value: 30 },
|
|
263
|
+
{ name: 'Last 90 days', value: 90 },
|
|
264
|
+
{ name: 'Custom date range', value: 'custom' },
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
]));
|
|
268
|
+
if (dateFilter === 'custom') {
|
|
269
|
+
const { startDate } = (await inquirer_1.default.prompt([
|
|
270
|
+
{
|
|
271
|
+
type: 'input',
|
|
272
|
+
name: 'startDate',
|
|
273
|
+
message: 'Enter start date (YYYY-MM-DD):',
|
|
274
|
+
validate: (input) => {
|
|
275
|
+
const date = new Date(input);
|
|
276
|
+
return !isNaN(date.getTime()) || 'Please enter a valid date';
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
]));
|
|
280
|
+
const cutoffDate = new Date(startDate);
|
|
281
|
+
filteredObjects = objects.filter((obj) => obj.lastModified >= cutoffDate);
|
|
282
|
+
}
|
|
283
|
+
else if (typeof dateFilter === 'number') {
|
|
284
|
+
const cutoffDate = new Date();
|
|
285
|
+
cutoffDate.setDate(cutoffDate.getDate() - dateFilter);
|
|
286
|
+
filteredObjects = objects.filter((obj) => obj.lastModified >= cutoffDate);
|
|
287
|
+
}
|
|
288
|
+
logger_1.logger.info(`\nFound ${filteredObjects.length} backup(s) in this time range:\n`);
|
|
289
|
+
}
|
|
290
|
+
else if (filterOption === 'search') {
|
|
291
|
+
const { searchTerm } = (await inquirer_1.default.prompt([
|
|
292
|
+
{
|
|
293
|
+
type: 'input',
|
|
294
|
+
name: 'searchTerm',
|
|
295
|
+
message: 'Enter search term (backup ID, date, etc.):',
|
|
296
|
+
},
|
|
297
|
+
]));
|
|
298
|
+
filteredObjects = objects.filter((obj) => obj.key.toLowerCase().includes(searchTerm.toLowerCase()));
|
|
299
|
+
logger_1.logger.info(`\nFound ${filteredObjects.length} backup(s) matching "${searchTerm}":\n`);
|
|
300
|
+
}
|
|
301
|
+
if (filteredObjects.length === 0) {
|
|
302
|
+
logger_1.logger.error('No backups found matching your criteria');
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
const { selected } = (await inquirer_1.default.prompt([
|
|
306
|
+
{
|
|
307
|
+
type: 'list',
|
|
308
|
+
name: 'selected',
|
|
309
|
+
message: `Select backup to restore (${filteredObjects.length} shown):`,
|
|
310
|
+
pageSize: 15,
|
|
311
|
+
choices: filteredObjects.map((obj) => ({
|
|
312
|
+
name: `${obj.key.replace('dbdock_backups/', '')} (${(obj.size / 1024 / 1024).toFixed(2)} MB) - ${obj.lastModified.toLocaleString()} - ${getTimeAgo(obj.lastModified)}`,
|
|
313
|
+
value: obj.key,
|
|
314
|
+
})),
|
|
315
|
+
},
|
|
316
|
+
]));
|
|
317
|
+
selectedBackup = selected;
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
const { selected } = (await inquirer_1.default.prompt([
|
|
321
|
+
{
|
|
322
|
+
type: 'list',
|
|
323
|
+
name: 'selected',
|
|
324
|
+
message: 'Select backup to restore:',
|
|
325
|
+
pageSize: 15,
|
|
326
|
+
choices: objects.map((obj) => ({
|
|
327
|
+
name: `${obj.key.replace('dbdock_backups/', '')} (${(obj.size / 1024 / 1024).toFixed(2)} MB) - ${obj.lastModified.toLocaleString()} - ${getTimeAgo(obj.lastModified)}`,
|
|
328
|
+
value: obj.key,
|
|
329
|
+
})),
|
|
330
|
+
},
|
|
331
|
+
]));
|
|
332
|
+
selectedBackup = selected;
|
|
333
|
+
}
|
|
334
|
+
const selectedBackupObj = objects.find((obj) => obj.key === selectedBackup);
|
|
335
|
+
if (selectedBackupObj) {
|
|
336
|
+
logger_1.logger.info('\nSelected Backup Details:');
|
|
337
|
+
logger_1.logger.log(` Backup: ${selectedBackup}`);
|
|
338
|
+
logger_1.logger.log(` Size: ${(selectedBackupObj.size / 1024 / 1024).toFixed(2)} MB`);
|
|
339
|
+
logger_1.logger.log(` Created: ${selectedBackupObj.lastModified.toLocaleString()}`);
|
|
340
|
+
logger_1.logger.log(` Age: ${getTimeAgo(selectedBackupObj.lastModified)}\n`);
|
|
341
|
+
}
|
|
342
|
+
const { confirm } = (await inquirer_1.default.prompt([
|
|
343
|
+
{
|
|
344
|
+
type: 'confirm',
|
|
345
|
+
name: 'confirm',
|
|
346
|
+
message: 'This will overwrite the current database. Continue?',
|
|
347
|
+
default: false,
|
|
348
|
+
},
|
|
349
|
+
]));
|
|
350
|
+
if (!confirm) {
|
|
351
|
+
logger_1.logger.warn('Restore cancelled');
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const restoreSteps = new progress_1.MultiStepProgress([
|
|
355
|
+
'Downloading backup',
|
|
356
|
+
'Decrypting data',
|
|
357
|
+
'Decompressing data',
|
|
358
|
+
'Restoring to database',
|
|
359
|
+
]);
|
|
360
|
+
restoreSteps.start();
|
|
361
|
+
const dbConfig = config.database;
|
|
362
|
+
const pgRestoreArgs = [
|
|
363
|
+
'-h',
|
|
364
|
+
dbConfig.host || 'localhost',
|
|
365
|
+
'-p',
|
|
366
|
+
String(dbConfig.port || 5432),
|
|
367
|
+
'-U',
|
|
368
|
+
dbConfig.username || 'postgres',
|
|
369
|
+
'-d',
|
|
370
|
+
dbConfig.database || 'postgres',
|
|
371
|
+
'-F',
|
|
372
|
+
'c',
|
|
373
|
+
'--clean',
|
|
374
|
+
'--if-exists',
|
|
375
|
+
'--no-owner',
|
|
376
|
+
'--no-acl',
|
|
377
|
+
'--no-password',
|
|
378
|
+
];
|
|
379
|
+
const env = {
|
|
380
|
+
...process.env,
|
|
381
|
+
PGPASSWORD: dbConfig.password,
|
|
382
|
+
};
|
|
383
|
+
let stream;
|
|
384
|
+
let tempFilePath = null;
|
|
385
|
+
try {
|
|
386
|
+
if (config.storage.provider === 'local') {
|
|
387
|
+
const localAdapter = adapter;
|
|
388
|
+
stream = await localAdapter.downloadStream({ key: selectedBackup });
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
tempFilePath = (0, path_1.join)((0, os_1.tmpdir)(), `dbdock-restore-${Date.now()}.sql`);
|
|
392
|
+
const downloadStream = await adapter.downloadStream({
|
|
393
|
+
key: selectedBackup,
|
|
394
|
+
});
|
|
395
|
+
const tempWriteStream = (0, fs_1.createWriteStream)(tempFilePath);
|
|
396
|
+
let downloadedBytes = 0;
|
|
397
|
+
downloadStream.on('data', (chunk) => {
|
|
398
|
+
downloadedBytes += chunk.length;
|
|
399
|
+
const mb = (downloadedBytes / 1024 / 1024).toFixed(2);
|
|
400
|
+
restoreSteps.nextStep(`${mb} MB downloaded`);
|
|
401
|
+
});
|
|
402
|
+
await new Promise((resolve, reject) => {
|
|
403
|
+
downloadStream.pipe(tempWriteStream);
|
|
404
|
+
downloadStream.on('error', reject);
|
|
405
|
+
tempWriteStream.on('error', reject);
|
|
406
|
+
tempWriteStream.on('finish', resolve);
|
|
407
|
+
});
|
|
408
|
+
const { createReadStream } = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
409
|
+
stream = createReadStream(tempFilePath);
|
|
410
|
+
}
|
|
411
|
+
restoreSteps.nextStep();
|
|
412
|
+
}
|
|
413
|
+
catch (err) {
|
|
414
|
+
restoreSteps.fail(err instanceof Error ? err.message : String(err));
|
|
415
|
+
if (tempFilePath && (0, fs_1.existsSync)(tempFilePath)) {
|
|
416
|
+
(0, fs_1.unlinkSync)(tempFilePath);
|
|
417
|
+
}
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
stream.on('error', (err) => {
|
|
421
|
+
restoreSteps.fail(`Failed to read backup file: ${err.message}`);
|
|
422
|
+
if (tempFilePath && (0, fs_1.existsSync)(tempFilePath)) {
|
|
423
|
+
(0, fs_1.unlinkSync)(tempFilePath);
|
|
424
|
+
}
|
|
425
|
+
process.exit(1);
|
|
426
|
+
});
|
|
427
|
+
if (config.backup?.encryption?.enabled && config.backup.encryption.key) {
|
|
428
|
+
const keyBuffer = Buffer.from(config.backup.encryption.key, 'hex');
|
|
429
|
+
if (keyBuffer.length !== 32) {
|
|
430
|
+
restoreSteps.fail(`Invalid encryption key length: ${keyBuffer.length} bytes (expected 32 bytes)`);
|
|
431
|
+
logger_1.logger.error(`\nYour key: "${config.backup.encryption.key}" (${config.backup.encryption.key.length} characters)\n\n` +
|
|
432
|
+
`Please fix:\n` +
|
|
433
|
+
` • Encryption key must be exactly 64 hexadecimal characters (32 bytes)\n` +
|
|
434
|
+
` • Generate a valid key: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"\n` +
|
|
435
|
+
` • Update the "backup.encryption.key" in your dbdock.config.json`);
|
|
436
|
+
if (tempFilePath && (0, fs_1.existsSync)(tempFilePath)) {
|
|
437
|
+
(0, fs_1.unlinkSync)(tempFilePath);
|
|
438
|
+
}
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
const iv = Buffer.alloc(16);
|
|
442
|
+
const decipher = (0, crypto_1.createDecipheriv)('aes-256-cbc', keyBuffer, iv);
|
|
443
|
+
stream = stream.pipe(decipher);
|
|
444
|
+
restoreSteps.nextStep();
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
restoreSteps.nextStep();
|
|
448
|
+
}
|
|
449
|
+
if (config.backup?.compression?.enabled) {
|
|
450
|
+
const decompressStream = (0, zlib_1.createBrotliDecompress)();
|
|
451
|
+
stream = stream.pipe(decompressStream);
|
|
452
|
+
restoreSteps.nextStep();
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
restoreSteps.nextStep();
|
|
456
|
+
}
|
|
457
|
+
const pgRestoreProcess = (0, child_process_1.spawn)('pg_restore', pgRestoreArgs, { env });
|
|
458
|
+
stream.pipe(pgRestoreProcess.stdin);
|
|
459
|
+
const ignoredPatterns = [
|
|
460
|
+
'NOTICE',
|
|
461
|
+
'WARNING',
|
|
462
|
+
'transaction_timeout',
|
|
463
|
+
'errors ignored on restore',
|
|
464
|
+
'unrecognized configuration parameter',
|
|
465
|
+
'already exists',
|
|
466
|
+
'does not exist',
|
|
467
|
+
'no privileges could be revoked',
|
|
468
|
+
'no privileges were granted',
|
|
469
|
+
'role .* does not exist',
|
|
470
|
+
'extension .* already exists',
|
|
471
|
+
'schema .* already exists',
|
|
472
|
+
'procedural language .* already exists',
|
|
473
|
+
];
|
|
474
|
+
const shouldIgnoreError = (message) => {
|
|
475
|
+
return ignoredPatterns.some((pattern) => message.toLowerCase().includes(pattern.toLowerCase()));
|
|
476
|
+
};
|
|
477
|
+
await new Promise((resolve, reject) => {
|
|
478
|
+
let errorOutput = '';
|
|
479
|
+
let hasWarnings = false;
|
|
480
|
+
pgRestoreProcess.on('close', (code) => {
|
|
481
|
+
if (tempFilePath && (0, fs_1.existsSync)(tempFilePath)) {
|
|
482
|
+
(0, fs_1.unlinkSync)(tempFilePath);
|
|
483
|
+
}
|
|
484
|
+
if (code === 0 || (code === 1 && !errorOutput && hasWarnings)) {
|
|
485
|
+
resolve();
|
|
486
|
+
}
|
|
487
|
+
else if (code === 1 && errorOutput) {
|
|
488
|
+
const friendlyError = parsePgRestoreError(errorOutput);
|
|
489
|
+
reject(new Error(friendlyError));
|
|
490
|
+
}
|
|
491
|
+
else if (code !== 0) {
|
|
492
|
+
reject(new Error(`pg_restore exited with code ${code}${errorOutput ? ': ' + errorOutput : ''}`));
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
pgRestoreProcess.on('error', (err) => {
|
|
496
|
+
if (tempFilePath && (0, fs_1.existsSync)(tempFilePath)) {
|
|
497
|
+
(0, fs_1.unlinkSync)(tempFilePath);
|
|
498
|
+
}
|
|
499
|
+
reject(new Error(`Failed to execute pg_restore: ${err.message}`));
|
|
500
|
+
});
|
|
501
|
+
pgRestoreProcess.stderr.on('data', (data) => {
|
|
502
|
+
const message = data.toString();
|
|
503
|
+
if (shouldIgnoreError(message)) {
|
|
504
|
+
hasWarnings = true;
|
|
505
|
+
}
|
|
506
|
+
else if (message.trim()) {
|
|
507
|
+
errorOutput += message;
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
restoreSteps.complete();
|
|
512
|
+
logger_1.logger.success('Restore completed successfully');
|
|
513
|
+
}
|
|
514
|
+
catch (error) {
|
|
515
|
+
logger_1.logger.error('\n✖ Restore failed');
|
|
516
|
+
logger_1.logger.error(error instanceof Error ? error.message : String(error));
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
async function getCurrentDatabaseStats(config) {
|
|
521
|
+
const dbConfig = config.database;
|
|
522
|
+
const queries = [
|
|
523
|
+
`SELECT count(*) as table_count FROM information_schema.tables WHERE table_schema = 'public'`,
|
|
524
|
+
`SELECT pg_size_pretty(pg_database_size('${dbConfig.database}')) as size`,
|
|
525
|
+
`SELECT sum(n_live_tup) as total_rows FROM pg_stat_user_tables`,
|
|
526
|
+
];
|
|
527
|
+
const psqlArgs = [
|
|
528
|
+
'-h',
|
|
529
|
+
dbConfig.host || 'localhost',
|
|
530
|
+
'-p',
|
|
531
|
+
String(dbConfig.port || 5432),
|
|
532
|
+
'-U',
|
|
533
|
+
dbConfig.username || 'postgres',
|
|
534
|
+
'-d',
|
|
535
|
+
dbConfig.database || 'postgres',
|
|
536
|
+
'-t',
|
|
537
|
+
'--no-password',
|
|
538
|
+
];
|
|
539
|
+
const env = {
|
|
540
|
+
...process.env,
|
|
541
|
+
PGPASSWORD: dbConfig.password,
|
|
542
|
+
};
|
|
543
|
+
const results = await Promise.all(queries.map((query) => new Promise((resolve, reject) => {
|
|
544
|
+
const psqlProcess = (0, child_process_1.spawn)('psql', [...psqlArgs, '-c', query], {
|
|
545
|
+
env,
|
|
546
|
+
});
|
|
547
|
+
let output = '';
|
|
548
|
+
let errorOutput = '';
|
|
549
|
+
psqlProcess.stdout.on('data', (data) => {
|
|
550
|
+
output += data.toString();
|
|
551
|
+
});
|
|
552
|
+
psqlProcess.stderr.on('data', (data) => {
|
|
553
|
+
errorOutput += data.toString();
|
|
554
|
+
});
|
|
555
|
+
psqlProcess.on('close', (code) => {
|
|
556
|
+
if (code === 0) {
|
|
557
|
+
resolve(output.trim());
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
reject(new Error(errorOutput || `Query failed with code ${code}`));
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
psqlProcess.on('error', reject);
|
|
564
|
+
})));
|
|
565
|
+
return {
|
|
566
|
+
name: dbConfig.database || 'postgres',
|
|
567
|
+
tables: parseInt(results[0]) || 0,
|
|
568
|
+
size: results[1] || 'Unknown',
|
|
569
|
+
rows: results[2] ? parseInt(results[2]).toLocaleString() : '0',
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function parsePgRestoreError(errorOutput) {
|
|
573
|
+
const lowerError = errorOutput.toLowerCase();
|
|
574
|
+
if (lowerError.includes('authentication failed')) {
|
|
575
|
+
return ('Database authentication failed\n\n' +
|
|
576
|
+
'Please verify:\n' +
|
|
577
|
+
' • Database password is correct in dbdock.config.json\n' +
|
|
578
|
+
' • Database user has necessary permissions\n' +
|
|
579
|
+
' • Database host and port are accessible');
|
|
580
|
+
}
|
|
581
|
+
if (lowerError.includes('connection refused') || lowerError.includes('could not connect')) {
|
|
582
|
+
return ('Failed to connect to database\n\n' +
|
|
583
|
+
'Please verify:\n' +
|
|
584
|
+
' • Database server is running\n' +
|
|
585
|
+
' • Host and port are correct in dbdock.config.json\n' +
|
|
586
|
+
' • Firewall allows connection to database port\n' +
|
|
587
|
+
' • Database accepts connections from your IP');
|
|
588
|
+
}
|
|
589
|
+
if (lowerError.includes('permission denied')) {
|
|
590
|
+
return ('Database permission denied\n\n' +
|
|
591
|
+
'Please verify:\n' +
|
|
592
|
+
' • Database user has CREATE/DROP privileges\n' +
|
|
593
|
+
' • User has permission to restore to this database\n' +
|
|
594
|
+
' • Try using a superuser account for restore');
|
|
595
|
+
}
|
|
596
|
+
if (lowerError.includes('database') && lowerError.includes('does not exist')) {
|
|
597
|
+
return ('Target database does not exist\n\n' +
|
|
598
|
+
'Please:\n' +
|
|
599
|
+
' • Create the database first, or\n' +
|
|
600
|
+
' • Update database name in dbdock.config.json');
|
|
601
|
+
}
|
|
602
|
+
if (lowerError.includes('disk full') || lowerError.includes('no space left')) {
|
|
603
|
+
return ('Insufficient disk space\n\n' +
|
|
604
|
+
'Please:\n' +
|
|
605
|
+
' • Free up disk space on database server\n' +
|
|
606
|
+
' • Check available storage before restoring');
|
|
607
|
+
}
|
|
608
|
+
if (lowerError.includes('corrupted') || lowerError.includes('invalid backup')) {
|
|
609
|
+
return ('Backup file appears to be corrupted\n\n' +
|
|
610
|
+
'Please:\n' +
|
|
611
|
+
' • Try a different backup file\n' +
|
|
612
|
+
' • Verify backup was created successfully\n' +
|
|
613
|
+
' • Check encryption key matches if encryption is enabled');
|
|
614
|
+
}
|
|
615
|
+
return `Database restore error:\n\n${errorOutput.trim()}\n\nIf you need help, please check the documentation or report this issue.`;
|
|
616
|
+
}
|
|
617
|
+
function getTimeAgo(date) {
|
|
618
|
+
const now = new Date();
|
|
619
|
+
const diff = now.getTime() - date.getTime();
|
|
620
|
+
const seconds = Math.floor(diff / 1000);
|
|
621
|
+
const minutes = Math.floor(seconds / 60);
|
|
622
|
+
const hours = Math.floor(minutes / 60);
|
|
623
|
+
const days = Math.floor(hours / 24);
|
|
624
|
+
if (days > 0) {
|
|
625
|
+
return `${days} day${days > 1 ? 's' : ''} ago`;
|
|
626
|
+
}
|
|
627
|
+
else if (hours > 0) {
|
|
628
|
+
return `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
|
629
|
+
}
|
|
630
|
+
else if (minutes > 0) {
|
|
631
|
+
return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
return 'Just now';
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
//# sourceMappingURL=restore.js.map
|