s3db.js 11.2.2 → 11.2.4
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/dist/s3db.cjs.js +1650 -136
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +1644 -137
- package/dist/s3db.es.js.map +1 -1
- package/package.json +1 -1
- package/src/behaviors/enforce-limits.js +28 -4
- package/src/behaviors/index.js +6 -1
- package/src/client.class.js +11 -1
- package/src/concerns/partition-queue.js +7 -1
- package/src/concerns/plugin-storage.js +75 -13
- package/src/database.class.js +22 -4
- package/src/errors.js +414 -24
- package/src/partition-drivers/base-partition-driver.js +12 -2
- package/src/partition-drivers/index.js +7 -1
- package/src/partition-drivers/memory-partition-driver.js +20 -5
- package/src/partition-drivers/sqs-partition-driver.js +6 -1
- package/src/plugins/audit.errors.js +46 -0
- package/src/plugins/backup/base-backup-driver.class.js +36 -6
- package/src/plugins/backup/filesystem-backup-driver.class.js +55 -7
- package/src/plugins/backup/index.js +40 -9
- package/src/plugins/backup/multi-backup-driver.class.js +69 -9
- package/src/plugins/backup/s3-backup-driver.class.js +48 -6
- package/src/plugins/backup.errors.js +45 -0
- package/src/plugins/cache/cache.class.js +8 -1
- package/src/plugins/cache/memory-cache.class.js +216 -33
- package/src/plugins/cache.errors.js +47 -0
- package/src/plugins/cache.plugin.js +94 -3
- package/src/plugins/eventual-consistency/analytics.js +145 -0
- package/src/plugins/eventual-consistency/index.js +203 -1
- package/src/plugins/fulltext.errors.js +46 -0
- package/src/plugins/fulltext.plugin.js +15 -3
- package/src/plugins/metrics.errors.js +46 -0
- package/src/plugins/queue-consumer.plugin.js +31 -4
- package/src/plugins/queue.errors.js +46 -0
- package/src/plugins/replicator.errors.js +46 -0
- package/src/plugins/replicator.plugin.js +40 -5
- package/src/plugins/replicators/base-replicator.class.js +19 -3
- package/src/plugins/replicators/index.js +9 -3
- package/src/plugins/replicators/s3db-replicator.class.js +38 -8
- package/src/plugins/scheduler.errors.js +46 -0
- package/src/plugins/scheduler.plugin.js +79 -19
- package/src/plugins/state-machine.errors.js +47 -0
- package/src/plugins/state-machine.plugin.js +86 -17
- package/src/resource.class.js +8 -1
- package/src/stream/index.js +6 -1
- package/src/stream/resource-reader.class.js +6 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { BackupError } from '../backup.errors.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* BaseBackupDriver - Abstract base class for backup drivers
|
|
3
5
|
*
|
|
@@ -38,7 +40,12 @@ export default class BaseBackupDriver {
|
|
|
38
40
|
* @returns {Object} Upload result with destination info
|
|
39
41
|
*/
|
|
40
42
|
async upload(filePath, backupId, manifest) {
|
|
41
|
-
throw new
|
|
43
|
+
throw new BackupError('upload() method must be implemented by subclass', {
|
|
44
|
+
operation: 'upload',
|
|
45
|
+
driver: this.constructor.name,
|
|
46
|
+
backupId,
|
|
47
|
+
suggestion: 'Extend BaseBackupDriver and implement the upload() method'
|
|
48
|
+
});
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
/**
|
|
@@ -49,7 +56,12 @@ export default class BaseBackupDriver {
|
|
|
49
56
|
* @returns {string} Path to downloaded file
|
|
50
57
|
*/
|
|
51
58
|
async download(backupId, targetPath, metadata) {
|
|
52
|
-
throw new
|
|
59
|
+
throw new BackupError('download() method must be implemented by subclass', {
|
|
60
|
+
operation: 'download',
|
|
61
|
+
driver: this.constructor.name,
|
|
62
|
+
backupId,
|
|
63
|
+
suggestion: 'Extend BaseBackupDriver and implement the download() method'
|
|
64
|
+
});
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
/**
|
|
@@ -58,7 +70,12 @@ export default class BaseBackupDriver {
|
|
|
58
70
|
* @param {Object} metadata - Backup metadata
|
|
59
71
|
*/
|
|
60
72
|
async delete(backupId, metadata) {
|
|
61
|
-
throw new
|
|
73
|
+
throw new BackupError('delete() method must be implemented by subclass', {
|
|
74
|
+
operation: 'delete',
|
|
75
|
+
driver: this.constructor.name,
|
|
76
|
+
backupId,
|
|
77
|
+
suggestion: 'Extend BaseBackupDriver and implement the delete() method'
|
|
78
|
+
});
|
|
62
79
|
}
|
|
63
80
|
|
|
64
81
|
/**
|
|
@@ -67,7 +84,11 @@ export default class BaseBackupDriver {
|
|
|
67
84
|
* @returns {Array} List of backup metadata
|
|
68
85
|
*/
|
|
69
86
|
async list(options = {}) {
|
|
70
|
-
throw new
|
|
87
|
+
throw new BackupError('list() method must be implemented by subclass', {
|
|
88
|
+
operation: 'list',
|
|
89
|
+
driver: this.constructor.name,
|
|
90
|
+
suggestion: 'Extend BaseBackupDriver and implement the list() method'
|
|
91
|
+
});
|
|
71
92
|
}
|
|
72
93
|
|
|
73
94
|
/**
|
|
@@ -78,7 +99,12 @@ export default class BaseBackupDriver {
|
|
|
78
99
|
* @returns {boolean} True if backup is valid
|
|
79
100
|
*/
|
|
80
101
|
async verify(backupId, expectedChecksum, metadata) {
|
|
81
|
-
throw new
|
|
102
|
+
throw new BackupError('verify() method must be implemented by subclass', {
|
|
103
|
+
operation: 'verify',
|
|
104
|
+
driver: this.constructor.name,
|
|
105
|
+
backupId,
|
|
106
|
+
suggestion: 'Extend BaseBackupDriver and implement the verify() method'
|
|
107
|
+
});
|
|
82
108
|
}
|
|
83
109
|
|
|
84
110
|
/**
|
|
@@ -86,7 +112,11 @@ export default class BaseBackupDriver {
|
|
|
86
112
|
* @returns {string} Driver type
|
|
87
113
|
*/
|
|
88
114
|
getType() {
|
|
89
|
-
throw new
|
|
115
|
+
throw new BackupError('getType() method must be implemented by subclass', {
|
|
116
|
+
operation: 'getType',
|
|
117
|
+
driver: this.constructor.name,
|
|
118
|
+
suggestion: 'Extend BaseBackupDriver and implement the getType() method'
|
|
119
|
+
});
|
|
90
120
|
}
|
|
91
121
|
|
|
92
122
|
/**
|
|
@@ -5,6 +5,7 @@ import { pipeline } from 'stream/promises';
|
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import crypto from 'crypto';
|
|
7
7
|
import tryFn from '../../concerns/try-fn.js';
|
|
8
|
+
import { BackupError } from '../backup.errors.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* FilesystemBackupDriver - Stores backups on local/network filesystem
|
|
@@ -31,7 +32,11 @@ export default class FilesystemBackupDriver extends BaseBackupDriver {
|
|
|
31
32
|
async onSetup() {
|
|
32
33
|
// Validate path configuration
|
|
33
34
|
if (!this.config.path) {
|
|
34
|
-
throw new
|
|
35
|
+
throw new BackupError('FilesystemBackupDriver: path configuration is required', {
|
|
36
|
+
operation: 'onSetup',
|
|
37
|
+
driver: 'filesystem',
|
|
38
|
+
suggestion: 'Provide a path in config: new FilesystemBackupDriver({ path: "/path/to/backups" })'
|
|
39
|
+
});
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
this.log(`Initialized with path: ${this.config.path}`);
|
|
@@ -69,13 +74,28 @@ export default class FilesystemBackupDriver extends BaseBackupDriver {
|
|
|
69
74
|
);
|
|
70
75
|
|
|
71
76
|
if (!createDirOk) {
|
|
72
|
-
throw new
|
|
77
|
+
throw new BackupError('Failed to create backup directory', {
|
|
78
|
+
operation: 'upload',
|
|
79
|
+
driver: 'filesystem',
|
|
80
|
+
backupId,
|
|
81
|
+
targetDir,
|
|
82
|
+
original: createDirErr,
|
|
83
|
+
suggestion: 'Check directory permissions and disk space'
|
|
84
|
+
});
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
// Copy backup file
|
|
76
88
|
const [copyOk, copyErr] = await tryFn(() => copyFile(filePath, targetPath));
|
|
77
89
|
if (!copyOk) {
|
|
78
|
-
throw new
|
|
90
|
+
throw new BackupError('Failed to copy backup file', {
|
|
91
|
+
operation: 'upload',
|
|
92
|
+
driver: 'filesystem',
|
|
93
|
+
backupId,
|
|
94
|
+
filePath,
|
|
95
|
+
targetPath,
|
|
96
|
+
original: copyErr,
|
|
97
|
+
suggestion: 'Check file permissions and disk space'
|
|
98
|
+
});
|
|
79
99
|
}
|
|
80
100
|
|
|
81
101
|
// Write manifest
|
|
@@ -90,7 +110,14 @@ export default class FilesystemBackupDriver extends BaseBackupDriver {
|
|
|
90
110
|
if (!manifestOk) {
|
|
91
111
|
// Clean up backup file if manifest fails
|
|
92
112
|
await tryFn(() => unlink(targetPath));
|
|
93
|
-
throw new
|
|
113
|
+
throw new BackupError('Failed to write manifest file', {
|
|
114
|
+
operation: 'upload',
|
|
115
|
+
driver: 'filesystem',
|
|
116
|
+
backupId,
|
|
117
|
+
manifestPath,
|
|
118
|
+
original: manifestErr,
|
|
119
|
+
suggestion: 'Check directory permissions and disk space'
|
|
120
|
+
});
|
|
94
121
|
}
|
|
95
122
|
|
|
96
123
|
// Get file stats
|
|
@@ -116,7 +143,13 @@ export default class FilesystemBackupDriver extends BaseBackupDriver {
|
|
|
116
143
|
// Check if source exists
|
|
117
144
|
const [existsOk] = await tryFn(() => access(sourcePath));
|
|
118
145
|
if (!existsOk) {
|
|
119
|
-
throw new
|
|
146
|
+
throw new BackupError('Backup file not found', {
|
|
147
|
+
operation: 'download',
|
|
148
|
+
driver: 'filesystem',
|
|
149
|
+
backupId,
|
|
150
|
+
sourcePath,
|
|
151
|
+
suggestion: 'Check if backup exists using list() method'
|
|
152
|
+
});
|
|
120
153
|
}
|
|
121
154
|
|
|
122
155
|
// Create target directory if needed
|
|
@@ -126,7 +159,15 @@ export default class FilesystemBackupDriver extends BaseBackupDriver {
|
|
|
126
159
|
// Copy file
|
|
127
160
|
const [copyOk, copyErr] = await tryFn(() => copyFile(sourcePath, targetPath));
|
|
128
161
|
if (!copyOk) {
|
|
129
|
-
throw new
|
|
162
|
+
throw new BackupError('Failed to download backup', {
|
|
163
|
+
operation: 'download',
|
|
164
|
+
driver: 'filesystem',
|
|
165
|
+
backupId,
|
|
166
|
+
sourcePath,
|
|
167
|
+
targetPath,
|
|
168
|
+
original: copyErr,
|
|
169
|
+
suggestion: 'Check file permissions and disk space'
|
|
170
|
+
});
|
|
130
171
|
}
|
|
131
172
|
|
|
132
173
|
this.log(`Downloaded backup ${backupId} from ${sourcePath} to ${targetPath}`);
|
|
@@ -150,7 +191,14 @@ export default class FilesystemBackupDriver extends BaseBackupDriver {
|
|
|
150
191
|
const [deleteManifestOk] = await tryFn(() => unlink(manifestPath));
|
|
151
192
|
|
|
152
193
|
if (!deleteBackupOk && !deleteManifestOk) {
|
|
153
|
-
throw new
|
|
194
|
+
throw new BackupError('Failed to delete backup files', {
|
|
195
|
+
operation: 'delete',
|
|
196
|
+
driver: 'filesystem',
|
|
197
|
+
backupId,
|
|
198
|
+
backupPath,
|
|
199
|
+
manifestPath,
|
|
200
|
+
suggestion: 'Check file permissions'
|
|
201
|
+
});
|
|
154
202
|
}
|
|
155
203
|
|
|
156
204
|
this.log(`Deleted backup ${backupId}`);
|
|
@@ -2,6 +2,7 @@ import BaseBackupDriver from './base-backup-driver.class.js';
|
|
|
2
2
|
import FilesystemBackupDriver from './filesystem-backup-driver.class.js';
|
|
3
3
|
import S3BackupDriver from './s3-backup-driver.class.js';
|
|
4
4
|
import MultiBackupDriver from './multi-backup-driver.class.js';
|
|
5
|
+
import { BackupError } from '../backup.errors.js';
|
|
5
6
|
|
|
6
7
|
export {
|
|
7
8
|
BaseBackupDriver,
|
|
@@ -27,11 +28,16 @@ export const BACKUP_DRIVERS = {
|
|
|
27
28
|
*/
|
|
28
29
|
export function createBackupDriver(driver, config = {}) {
|
|
29
30
|
const DriverClass = BACKUP_DRIVERS[driver];
|
|
30
|
-
|
|
31
|
+
|
|
31
32
|
if (!DriverClass) {
|
|
32
|
-
throw new
|
|
33
|
+
throw new BackupError(`Unknown backup driver: ${driver}`, {
|
|
34
|
+
operation: 'createBackupDriver',
|
|
35
|
+
driver,
|
|
36
|
+
availableDrivers: Object.keys(BACKUP_DRIVERS),
|
|
37
|
+
suggestion: `Use one of the available drivers: ${Object.keys(BACKUP_DRIVERS).join(', ')}`
|
|
38
|
+
});
|
|
33
39
|
}
|
|
34
|
-
|
|
40
|
+
|
|
35
41
|
return new DriverClass(config);
|
|
36
42
|
}
|
|
37
43
|
|
|
@@ -43,18 +49,32 @@ export function createBackupDriver(driver, config = {}) {
|
|
|
43
49
|
*/
|
|
44
50
|
export function validateBackupConfig(driver, config = {}) {
|
|
45
51
|
if (!driver || typeof driver !== 'string') {
|
|
46
|
-
throw new
|
|
52
|
+
throw new BackupError('Driver type must be a non-empty string', {
|
|
53
|
+
operation: 'validateBackupConfig',
|
|
54
|
+
driver,
|
|
55
|
+
suggestion: 'Provide a valid driver type string (filesystem, s3, or multi)'
|
|
56
|
+
});
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
if (!BACKUP_DRIVERS[driver]) {
|
|
50
|
-
throw new
|
|
60
|
+
throw new BackupError(`Unknown backup driver: ${driver}`, {
|
|
61
|
+
operation: 'validateBackupConfig',
|
|
62
|
+
driver,
|
|
63
|
+
availableDrivers: Object.keys(BACKUP_DRIVERS),
|
|
64
|
+
suggestion: `Use one of the available drivers: ${Object.keys(BACKUP_DRIVERS).join(', ')}`
|
|
65
|
+
});
|
|
51
66
|
}
|
|
52
67
|
|
|
53
68
|
// Driver-specific validation
|
|
54
69
|
switch (driver) {
|
|
55
70
|
case 'filesystem':
|
|
56
71
|
if (!config.path) {
|
|
57
|
-
throw new
|
|
72
|
+
throw new BackupError('FilesystemBackupDriver requires "path" configuration', {
|
|
73
|
+
operation: 'validateBackupConfig',
|
|
74
|
+
driver: 'filesystem',
|
|
75
|
+
config,
|
|
76
|
+
suggestion: 'Provide a "path" property in config: { path: "/path/to/backups" }'
|
|
77
|
+
});
|
|
58
78
|
}
|
|
59
79
|
break;
|
|
60
80
|
|
|
@@ -64,13 +84,24 @@ export function validateBackupConfig(driver, config = {}) {
|
|
|
64
84
|
|
|
65
85
|
case 'multi':
|
|
66
86
|
if (!Array.isArray(config.destinations) || config.destinations.length === 0) {
|
|
67
|
-
throw new
|
|
87
|
+
throw new BackupError('MultiBackupDriver requires non-empty "destinations" array', {
|
|
88
|
+
operation: 'validateBackupConfig',
|
|
89
|
+
driver: 'multi',
|
|
90
|
+
config,
|
|
91
|
+
suggestion: 'Provide destinations array: { destinations: [{ driver: "s3", config: {...} }] }'
|
|
92
|
+
});
|
|
68
93
|
}
|
|
69
|
-
|
|
94
|
+
|
|
70
95
|
// Validate each destination
|
|
71
96
|
config.destinations.forEach((dest, index) => {
|
|
72
97
|
if (!dest.driver) {
|
|
73
|
-
throw new
|
|
98
|
+
throw new BackupError(`Destination ${index} must have a "driver" property`, {
|
|
99
|
+
operation: 'validateBackupConfig',
|
|
100
|
+
driver: 'multi',
|
|
101
|
+
destinationIndex: index,
|
|
102
|
+
destination: dest,
|
|
103
|
+
suggestion: 'Each destination must have a driver property: { driver: "s3", config: {...} }'
|
|
104
|
+
});
|
|
74
105
|
}
|
|
75
106
|
|
|
76
107
|
// Recursive validation for nested drivers
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import BaseBackupDriver from './base-backup-driver.class.js';
|
|
2
2
|
import { createBackupDriver } from './index.js';
|
|
3
3
|
import tryFn from '../../concerns/try-fn.js';
|
|
4
|
+
import { BackupError } from '../backup.errors.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* MultiBackupDriver - Manages multiple backup destinations
|
|
@@ -34,13 +35,24 @@ export default class MultiBackupDriver extends BaseBackupDriver {
|
|
|
34
35
|
|
|
35
36
|
async onSetup() {
|
|
36
37
|
if (!Array.isArray(this.config.destinations) || this.config.destinations.length === 0) {
|
|
37
|
-
throw new
|
|
38
|
+
throw new BackupError('MultiBackupDriver requires non-empty destinations array', {
|
|
39
|
+
operation: 'onSetup',
|
|
40
|
+
driver: 'multi',
|
|
41
|
+
destinationsProvided: this.config.destinations,
|
|
42
|
+
suggestion: 'Provide destinations array: { destinations: [{ driver: "s3", config: {...} }, { driver: "filesystem", config: {...} }] }'
|
|
43
|
+
});
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
// Create and setup all driver instances
|
|
41
47
|
for (const [index, destConfig] of this.config.destinations.entries()) {
|
|
42
48
|
if (!destConfig.driver) {
|
|
43
|
-
throw new
|
|
49
|
+
throw new BackupError(`Destination ${index} missing driver type`, {
|
|
50
|
+
operation: 'onSetup',
|
|
51
|
+
driver: 'multi',
|
|
52
|
+
destinationIndex: index,
|
|
53
|
+
destination: destConfig,
|
|
54
|
+
suggestion: 'Each destination must have a driver property: { driver: "s3", config: {...} } or { driver: "filesystem", config: {...} }'
|
|
55
|
+
});
|
|
44
56
|
}
|
|
45
57
|
|
|
46
58
|
try {
|
|
@@ -51,10 +63,18 @@ export default class MultiBackupDriver extends BaseBackupDriver {
|
|
|
51
63
|
config: destConfig,
|
|
52
64
|
index
|
|
53
65
|
});
|
|
54
|
-
|
|
66
|
+
|
|
55
67
|
this.log(`Setup destination ${index}: ${destConfig.driver}`);
|
|
56
68
|
} catch (error) {
|
|
57
|
-
throw new
|
|
69
|
+
throw new BackupError(`Failed to setup destination ${index}`, {
|
|
70
|
+
operation: 'onSetup',
|
|
71
|
+
driver: 'multi',
|
|
72
|
+
destinationIndex: index,
|
|
73
|
+
destinationDriver: destConfig.driver,
|
|
74
|
+
destinationConfig: destConfig.config,
|
|
75
|
+
original: error,
|
|
76
|
+
suggestion: 'Check destination driver configuration and ensure dependencies are available'
|
|
77
|
+
});
|
|
58
78
|
}
|
|
59
79
|
}
|
|
60
80
|
|
|
@@ -92,7 +112,15 @@ export default class MultiBackupDriver extends BaseBackupDriver {
|
|
|
92
112
|
}
|
|
93
113
|
}
|
|
94
114
|
|
|
95
|
-
throw new
|
|
115
|
+
throw new BackupError('All priority destinations failed', {
|
|
116
|
+
operation: 'upload',
|
|
117
|
+
driver: 'multi',
|
|
118
|
+
strategy: 'priority',
|
|
119
|
+
backupId,
|
|
120
|
+
totalDestinations: this.drivers.length,
|
|
121
|
+
failures: errors,
|
|
122
|
+
suggestion: 'Check destination configurations and ensure at least one destination is accessible'
|
|
123
|
+
});
|
|
96
124
|
}
|
|
97
125
|
|
|
98
126
|
// For 'all' and 'any' strategies, upload to all destinations
|
|
@@ -128,11 +156,29 @@ export default class MultiBackupDriver extends BaseBackupDriver {
|
|
|
128
156
|
const failedResults = allResults.filter(r => r.status === 'failed');
|
|
129
157
|
|
|
130
158
|
if (strategy === 'all' && failedResults.length > 0) {
|
|
131
|
-
throw new
|
|
159
|
+
throw new BackupError('Some destinations failed with strategy "all"', {
|
|
160
|
+
operation: 'upload',
|
|
161
|
+
driver: 'multi',
|
|
162
|
+
strategy: 'all',
|
|
163
|
+
backupId,
|
|
164
|
+
totalDestinations: this.drivers.length,
|
|
165
|
+
successCount: successResults.length,
|
|
166
|
+
failedCount: failedResults.length,
|
|
167
|
+
failures: failedResults,
|
|
168
|
+
suggestion: 'All destinations must succeed with "all" strategy. Use "any" strategy to tolerate failures, or fix failing destinations.'
|
|
169
|
+
});
|
|
132
170
|
}
|
|
133
171
|
|
|
134
172
|
if (strategy === 'any' && successResults.length === 0) {
|
|
135
|
-
throw new
|
|
173
|
+
throw new BackupError('All destinations failed with strategy "any"', {
|
|
174
|
+
operation: 'upload',
|
|
175
|
+
driver: 'multi',
|
|
176
|
+
strategy: 'any',
|
|
177
|
+
backupId,
|
|
178
|
+
totalDestinations: this.drivers.length,
|
|
179
|
+
failures: failedResults,
|
|
180
|
+
suggestion: 'At least one destination must succeed with "any" strategy. Check all destination configurations.'
|
|
181
|
+
});
|
|
136
182
|
}
|
|
137
183
|
|
|
138
184
|
return allResults;
|
|
@@ -160,7 +206,14 @@ export default class MultiBackupDriver extends BaseBackupDriver {
|
|
|
160
206
|
}
|
|
161
207
|
}
|
|
162
208
|
|
|
163
|
-
throw new
|
|
209
|
+
throw new BackupError('Failed to download backup from any destination', {
|
|
210
|
+
operation: 'download',
|
|
211
|
+
driver: 'multi',
|
|
212
|
+
backupId,
|
|
213
|
+
targetPath,
|
|
214
|
+
attemptedDestinations: destinations.length,
|
|
215
|
+
suggestion: 'Check if backup exists in at least one destination and destinations are accessible'
|
|
216
|
+
});
|
|
164
217
|
}
|
|
165
218
|
|
|
166
219
|
async delete(backupId, metadata) {
|
|
@@ -188,7 +241,14 @@ export default class MultiBackupDriver extends BaseBackupDriver {
|
|
|
188
241
|
}
|
|
189
242
|
|
|
190
243
|
if (successCount === 0 && errors.length > 0) {
|
|
191
|
-
throw new
|
|
244
|
+
throw new BackupError('Failed to delete from any destination', {
|
|
245
|
+
operation: 'delete',
|
|
246
|
+
driver: 'multi',
|
|
247
|
+
backupId,
|
|
248
|
+
attemptedDestinations: destinations.length,
|
|
249
|
+
failures: errors,
|
|
250
|
+
suggestion: 'Check if backup exists in destinations and destinations are accessible with delete permissions'
|
|
251
|
+
});
|
|
192
252
|
}
|
|
193
253
|
|
|
194
254
|
if (errors.length > 0) {
|
|
@@ -4,6 +4,7 @@ import { stat } from 'fs/promises';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import crypto from 'crypto';
|
|
6
6
|
import tryFn from '../../concerns/try-fn.js';
|
|
7
|
+
import { BackupError } from '../backup.errors.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* S3BackupDriver - Stores backups in S3-compatible storage
|
|
@@ -43,11 +44,19 @@ export default class S3BackupDriver extends BaseBackupDriver {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
if (!this.config.client) {
|
|
46
|
-
throw new
|
|
47
|
+
throw new BackupError('S3BackupDriver: client is required', {
|
|
48
|
+
operation: 'onSetup',
|
|
49
|
+
driver: 's3',
|
|
50
|
+
suggestion: 'Provide a client in config or ensure database has a client configured'
|
|
51
|
+
});
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
if (!this.config.bucket) {
|
|
50
|
-
throw new
|
|
55
|
+
throw new BackupError('S3BackupDriver: bucket is required', {
|
|
56
|
+
operation: 'onSetup',
|
|
57
|
+
driver: 's3',
|
|
58
|
+
suggestion: 'Provide a bucket in config or ensure database has a bucket configured'
|
|
59
|
+
});
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
this.log(`Initialized with bucket: ${this.config.bucket}, path: ${this.config.path}`);
|
|
@@ -108,7 +117,15 @@ export default class S3BackupDriver extends BaseBackupDriver {
|
|
|
108
117
|
});
|
|
109
118
|
|
|
110
119
|
if (!uploadOk) {
|
|
111
|
-
throw new
|
|
120
|
+
throw new BackupError('Failed to upload backup file to S3', {
|
|
121
|
+
operation: 'upload',
|
|
122
|
+
driver: 's3',
|
|
123
|
+
backupId,
|
|
124
|
+
bucket: this.config.bucket,
|
|
125
|
+
key: backupKey,
|
|
126
|
+
original: uploadErr,
|
|
127
|
+
suggestion: 'Check S3 permissions and bucket configuration'
|
|
128
|
+
});
|
|
112
129
|
}
|
|
113
130
|
|
|
114
131
|
// Upload manifest
|
|
@@ -133,7 +150,15 @@ export default class S3BackupDriver extends BaseBackupDriver {
|
|
|
133
150
|
bucket: this.config.bucket,
|
|
134
151
|
key: backupKey
|
|
135
152
|
}));
|
|
136
|
-
throw new
|
|
153
|
+
throw new BackupError('Failed to upload manifest to S3', {
|
|
154
|
+
operation: 'upload',
|
|
155
|
+
driver: 's3',
|
|
156
|
+
backupId,
|
|
157
|
+
bucket: this.config.bucket,
|
|
158
|
+
manifestKey,
|
|
159
|
+
original: manifestErr,
|
|
160
|
+
suggestion: 'Check S3 permissions and bucket configuration'
|
|
161
|
+
});
|
|
137
162
|
}
|
|
138
163
|
|
|
139
164
|
this.log(`Uploaded backup ${backupId} to s3://${this.config.bucket}/${backupKey} (${fileSize} bytes)`);
|
|
@@ -161,7 +186,16 @@ export default class S3BackupDriver extends BaseBackupDriver {
|
|
|
161
186
|
);
|
|
162
187
|
|
|
163
188
|
if (!downloadOk) {
|
|
164
|
-
throw new
|
|
189
|
+
throw new BackupError('Failed to download backup from S3', {
|
|
190
|
+
operation: 'download',
|
|
191
|
+
driver: 's3',
|
|
192
|
+
backupId,
|
|
193
|
+
bucket: this.config.bucket,
|
|
194
|
+
key: backupKey,
|
|
195
|
+
targetPath,
|
|
196
|
+
original: downloadErr,
|
|
197
|
+
suggestion: 'Check if backup exists and S3 permissions are correct'
|
|
198
|
+
});
|
|
165
199
|
}
|
|
166
200
|
|
|
167
201
|
this.log(`Downloaded backup ${backupId} from s3://${this.config.bucket}/${backupKey} to ${targetPath}`);
|
|
@@ -189,7 +223,15 @@ export default class S3BackupDriver extends BaseBackupDriver {
|
|
|
189
223
|
);
|
|
190
224
|
|
|
191
225
|
if (!deleteBackupOk && !deleteManifestOk) {
|
|
192
|
-
throw new
|
|
226
|
+
throw new BackupError('Failed to delete backup from S3', {
|
|
227
|
+
operation: 'delete',
|
|
228
|
+
driver: 's3',
|
|
229
|
+
backupId,
|
|
230
|
+
bucket: this.config.bucket,
|
|
231
|
+
backupKey,
|
|
232
|
+
manifestKey,
|
|
233
|
+
suggestion: 'Check S3 delete permissions'
|
|
234
|
+
});
|
|
193
235
|
}
|
|
194
236
|
|
|
195
237
|
this.log(`Deleted backup ${backupId} from S3`);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { S3dbError } from '../errors.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BackupError - Errors related to backup operations
|
|
5
|
+
*
|
|
6
|
+
* Used for backup driver operations including:
|
|
7
|
+
* - Driver initialization and setup
|
|
8
|
+
* - Backup upload/download/delete operations
|
|
9
|
+
* - Driver configuration validation
|
|
10
|
+
* - Multi-destination backup strategies
|
|
11
|
+
*
|
|
12
|
+
* @extends S3dbError
|
|
13
|
+
*/
|
|
14
|
+
export class BackupError extends S3dbError {
|
|
15
|
+
constructor(message, details = {}) {
|
|
16
|
+
const { driver = 'unknown', operation = 'unknown', backupId, ...rest } = details;
|
|
17
|
+
|
|
18
|
+
let description = details.description;
|
|
19
|
+
if (!description) {
|
|
20
|
+
description = `
|
|
21
|
+
Backup Operation Error
|
|
22
|
+
|
|
23
|
+
Driver: ${driver}
|
|
24
|
+
Operation: ${operation}
|
|
25
|
+
${backupId ? `Backup ID: ${backupId}` : ''}
|
|
26
|
+
|
|
27
|
+
Common causes:
|
|
28
|
+
1. Invalid backup driver configuration
|
|
29
|
+
2. Destination storage not accessible
|
|
30
|
+
3. Insufficient permissions
|
|
31
|
+
4. Network connectivity issues
|
|
32
|
+
5. Invalid backup file format
|
|
33
|
+
|
|
34
|
+
Solution:
|
|
35
|
+
Check driver configuration and ensure destination storage is accessible.
|
|
36
|
+
|
|
37
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/backup.md
|
|
38
|
+
`.trim();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
super(message, { ...rest, driver, operation, backupId, description });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default BackupError;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import EventEmitter from "events";
|
|
2
|
+
import { CacheError } from "../cache.errors.js";
|
|
2
3
|
|
|
3
4
|
export class Cache extends EventEmitter {
|
|
4
5
|
constructor(config = {}) {
|
|
@@ -13,7 +14,13 @@ export class Cache extends EventEmitter {
|
|
|
13
14
|
|
|
14
15
|
validateKey(key) {
|
|
15
16
|
if (key === null || key === undefined || typeof key !== 'string' || !key) {
|
|
16
|
-
throw new
|
|
17
|
+
throw new CacheError('Invalid cache key', {
|
|
18
|
+
operation: 'validateKey',
|
|
19
|
+
driver: this.constructor.name,
|
|
20
|
+
key,
|
|
21
|
+
keyType: typeof key,
|
|
22
|
+
suggestion: 'Cache key must be a non-empty string'
|
|
23
|
+
});
|
|
17
24
|
}
|
|
18
25
|
}
|
|
19
26
|
|