simple-backups 1.1.0 → 1.2.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/README.md +115 -1
- package/package.json +17 -5
- package/src/S3SimpleBackups.js +626 -0
- package/src/index.d.ts +157 -0
- package/src/index.js +2 -1
package/README.md
CHANGED
|
@@ -16,10 +16,12 @@ This package aims to simplify the task of automatically creating and pruning bac
|
|
|
16
16
|
## Features
|
|
17
17
|
- Simple-to-use API
|
|
18
18
|
- JSDoc Type Support
|
|
19
|
+
- TypeScript Type Definitions
|
|
19
20
|
- Automatic Backup Creation
|
|
20
21
|
- Multi-Window Backup Retention
|
|
22
|
+
- S3-Compatible Object Storage
|
|
21
23
|
- CPU & Memory Efficient
|
|
22
|
-
-
|
|
24
|
+
- Dependency-Free Core
|
|
23
25
|
|
|
24
26
|
## Installation
|
|
25
27
|
SimpleBackups can be installed using node package manager (`npm`)
|
|
@@ -82,6 +84,118 @@ backups.on('error', (error) => {
|
|
|
82
84
|
});
|
|
83
85
|
```
|
|
84
86
|
|
|
87
|
+
## TypeScript
|
|
88
|
+
SimpleBackups includes first-party TypeScript declarations. No separate `@types` package is required.
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import {
|
|
92
|
+
SimpleBackups,
|
|
93
|
+
S3SimpleBackups,
|
|
94
|
+
type Backup,
|
|
95
|
+
type BackupAdapters,
|
|
96
|
+
type BackupWindow,
|
|
97
|
+
type S3SimpleBackupsOptions,
|
|
98
|
+
type SimpleBackupsOptions
|
|
99
|
+
} from 'simple-backups';
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## S3SimpleBackups
|
|
103
|
+
`S3SimpleBackups` extends `SimpleBackups` and supplies its `list`, `create` and `delete` adapters using an S3-compatible object storage bucket. The user only needs to provide an asynchronous adapter which creates a local backup file and returns an object containing its path.
|
|
104
|
+
|
|
105
|
+
The core `SimpleBackups` class remains dependency-free. S3 support uses the official modular [`@aws-sdk/client-s3`](https://www.npmjs.com/package/@aws-sdk/client-s3) package as its only direct runtime dependency.
|
|
106
|
+
|
|
107
|
+
The following example creates a MySQL dump in the operating system's temporary directory and returns an object containing its path and native S3 upload options. `S3SimpleBackups` determines the filename and file size, streams the file to Vultr Object Storage and removes the temporary file after the upload attempt.
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
import { exec } from 'node:child_process';
|
|
111
|
+
import { tmpdir } from 'node:os';
|
|
112
|
+
import { join } from 'node:path';
|
|
113
|
+
import { promisify } from 'node:util';
|
|
114
|
+
import { S3SimpleBackups } from 'simple-backups';
|
|
115
|
+
|
|
116
|
+
// Convert exec into a promise so the adapter can await mysqldump.
|
|
117
|
+
const exec_async = promisify(exec);
|
|
118
|
+
|
|
119
|
+
// Retention window intervals are expressed in milliseconds.
|
|
120
|
+
const hour = 1000 * 60 * 60;
|
|
121
|
+
const day = hour * 24;
|
|
122
|
+
|
|
123
|
+
const backups = new S3SimpleBackups({
|
|
124
|
+
// Keep one backup per hour for 24 hours and one per day for 30 days.
|
|
125
|
+
windows: [
|
|
126
|
+
{ interval_ms: hour, limit: 24 },
|
|
127
|
+
{ interval_ms: day, limit: 30 }
|
|
128
|
+
],
|
|
129
|
+
|
|
130
|
+
// The bucket name and connection properties used by the official AWS S3
|
|
131
|
+
// client. Vultr accepts us-east-1 as the request-signing region.
|
|
132
|
+
bucket: {
|
|
133
|
+
name: 'wuup-db-backups',
|
|
134
|
+
region: 'us-east-1',
|
|
135
|
+
endpoint: 'https://ewr1.vultrobjects.com',
|
|
136
|
+
credentials: {
|
|
137
|
+
accessKeyId: process.env.S3_ACCESS_KEY_ID,
|
|
138
|
+
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// This is the only storage adapter the application needs to implement.
|
|
143
|
+
adapters: {
|
|
144
|
+
async create() {
|
|
145
|
+
// Include a timestamp so every temporary filename is unique.
|
|
146
|
+
const file_path = join(tmpdir(), `wuupdb_${Date.now()}.sql`);
|
|
147
|
+
|
|
148
|
+
// mysqldump runs as a child process. Awaiting it does not block the
|
|
149
|
+
// Node.js event loop; it only delays this backup until the dump exits.
|
|
150
|
+
await exec_async(`mysqldump wuupdb > "${file_path}"`);
|
|
151
|
+
|
|
152
|
+
// S3SimpleBackups opens, sizes, streams and removes this file. The
|
|
153
|
+
// options object is passed to the underlying S3 SDK as-is.
|
|
154
|
+
return {
|
|
155
|
+
path: file_path,
|
|
156
|
+
options: {
|
|
157
|
+
ContentType: 'application/sql',
|
|
158
|
+
StorageClass: 'STANDARD_IA',
|
|
159
|
+
Metadata: {
|
|
160
|
+
database: 'wuupdb'
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
backups.on('create', (backup) => {
|
|
169
|
+
console.log('Created S3 backup:', backup.id);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
backups.on('delete', (backup) => {
|
|
173
|
+
console.log('Deleted S3 backup:', backup.id);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
backups.on('error', (error) => {
|
|
177
|
+
console.error(error);
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`S3SimpleBackups` uses the local backup file's basename as the S3 object key without adding or changing anything.
|
|
182
|
+
|
|
183
|
+
```text
|
|
184
|
+
wuupdb_1784230616789.sql
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Every object in the configured bucket is treated as a backup managed by this instance. Backup timestamps are read from the standard S3 `LastModified` value returned by `ListObjectsV2`; timestamps are not encoded into object keys or custom metadata.
|
|
188
|
+
|
|
189
|
+
S3 identifies an object by its key and does not assign a separate object ID. Uploading another file with the same name would overwrite the existing object unless bucket versioning is enabled. To protect retained backups consistently across S3-compatible providers, `S3SimpleBackups` rejects a filename which already exists. Backup creation adapters should therefore create unique filenames, commonly by including `Date.now()` as shown above.
|
|
190
|
+
|
|
191
|
+
Files larger than the configured part size are streamed using bounded [S3 multipart uploads](https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html). The default part size is 8 MiB and the default concurrency is four parts. These can be customized with `part_size` and `queue_size`. Memory use is bounded by the part size and upload concurrency rather than the total backup size.
|
|
192
|
+
|
|
193
|
+
The creation adapter must return `{ path, options? }`. The optional `options` object is passed directly to the underlying S3 SDK, so it uses native SDK property names such as `ContentType`, `StorageClass`, `Metadata`, `ServerSideEncryption` and `SSEKMSKeyId`. `Bucket`, `Key`, `Body` and `ContentLength` are managed internally and cannot be overridden. Created local backup files are removed after every upload attempt.
|
|
194
|
+
|
|
195
|
+
An existing `S3Client` can be provided using `client`. Injected clients are not destroyed by default; clients constructed from the `bucket` configuration are destroyed when the `S3SimpleBackups` instance is destroyed. This behavior can be overridden with `destroy_client`.
|
|
196
|
+
|
|
197
|
+
The configured S3 identity needs permission to list the bucket and to create, upload, complete, abort and delete its objects.
|
|
198
|
+
|
|
85
199
|
## SimpleBackups
|
|
86
200
|
Below is a breakdown of the `SimpleBackups` class.
|
|
87
201
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simple-backups",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Simple yet unopinionated
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Simple yet unopinionated package to automatically create and prune backups using custom adapters or S3-compatible storage.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
7
8
|
"exports": {
|
|
8
|
-
".":
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.d.ts",
|
|
11
|
+
"import": "./src/index.js",
|
|
12
|
+
"default": "./src/index.js"
|
|
13
|
+
}
|
|
9
14
|
},
|
|
10
15
|
"files": [
|
|
11
16
|
"src",
|
|
@@ -13,7 +18,7 @@
|
|
|
13
18
|
"LICENSE"
|
|
14
19
|
],
|
|
15
20
|
"scripts": {
|
|
16
|
-
"test": "node --test"
|
|
21
|
+
"test": "node --test && tsc --project tsconfig.json"
|
|
17
22
|
},
|
|
18
23
|
"keywords": [
|
|
19
24
|
"backup",
|
|
@@ -24,10 +29,17 @@
|
|
|
24
29
|
"javascript",
|
|
25
30
|
"s3-bucket",
|
|
26
31
|
"unopinionated",
|
|
27
|
-
"
|
|
32
|
+
"s3-compatible"
|
|
28
33
|
],
|
|
29
34
|
"license": "MIT",
|
|
30
35
|
"engines": {
|
|
31
36
|
"node": ">=20"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@aws-sdk/client-s3": "3.1089.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "20.19.43",
|
|
43
|
+
"typescript": "7.0.2"
|
|
32
44
|
}
|
|
33
45
|
}
|
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
import { createReadStream } from 'node:fs';
|
|
2
|
+
import { rm, stat } from 'node:fs/promises';
|
|
3
|
+
import { basename } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
AbortMultipartUploadCommand,
|
|
7
|
+
CompleteMultipartUploadCommand,
|
|
8
|
+
CreateMultipartUploadCommand,
|
|
9
|
+
DeleteObjectCommand,
|
|
10
|
+
ListObjectsV2Command,
|
|
11
|
+
PutObjectCommand,
|
|
12
|
+
S3Client,
|
|
13
|
+
UploadPartCommand,
|
|
14
|
+
} from '@aws-sdk/client-s3';
|
|
15
|
+
|
|
16
|
+
import { SimpleBackups } from './SimpleBackups.js';
|
|
17
|
+
|
|
18
|
+
const minimum_part_size = 1024 * 1024 * 5;
|
|
19
|
+
const maximum_part_size = 1024 * 1024 * 1024 * 5;
|
|
20
|
+
const maximum_parts = 10000;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} S3SimpleBackupsCreateResult
|
|
24
|
+
* @property {string} path The path of the backup file to upload.
|
|
25
|
+
* @property {S3SimpleBackupsUploadOptions} [options] S3 SDK options applied to the uploaded object.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {Object} S3SimpleBackupsAdapters
|
|
30
|
+
* @property {() => Promise<S3SimpleBackupsCreateResult>} create Creates a backup file and returns its path and upload options.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {Omit<import('@aws-sdk/client-s3').CreateMultipartUploadCommandInput, 'Bucket'|'Key'>} S3SimpleBackupsUploadOptions
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {import('@aws-sdk/client-s3').S3ClientConfig & {name: string}} S3SimpleBackupsBucket
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {Object} S3SimpleBackupsOptions
|
|
43
|
+
* @property {import('./SimpleBackups.js').BackupWindow[]} windows The backup windows to use.
|
|
44
|
+
* @property {S3SimpleBackupsBucket} bucket The S3 bucket and client configuration to use.
|
|
45
|
+
* @property {S3SimpleBackupsAdapters} adapters The backup creation adapter to use.
|
|
46
|
+
* @property {S3Client} [client] An existing S3 client to use.
|
|
47
|
+
* @property {boolean} [destroy_client] Whether the S3 client should be destroyed with this instance.
|
|
48
|
+
* @property {number} [part_size=8388608] The multipart upload part size in bytes.
|
|
49
|
+
* @property {number} [queue_size=4] The maximum number of parts to upload concurrently.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Implements the S3 storage operations used by S3SimpleBackups.
|
|
54
|
+
*/
|
|
55
|
+
class S3BackupStorage {
|
|
56
|
+
/** @type {S3SimpleBackupsBucket} */
|
|
57
|
+
#bucket;
|
|
58
|
+
|
|
59
|
+
/** @type {S3Client|undefined} */
|
|
60
|
+
#client;
|
|
61
|
+
|
|
62
|
+
/** @type {S3SimpleBackupsAdapters} */
|
|
63
|
+
#adapters;
|
|
64
|
+
|
|
65
|
+
/** @type {Set<string>} */
|
|
66
|
+
#backup_keys = new Set();
|
|
67
|
+
|
|
68
|
+
/** @type {boolean} */
|
|
69
|
+
#destroy_client;
|
|
70
|
+
|
|
71
|
+
/** @type {number} */
|
|
72
|
+
#part_size;
|
|
73
|
+
|
|
74
|
+
/** @type {number} */
|
|
75
|
+
#queue_size;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {S3SimpleBackupsOptions} options
|
|
79
|
+
*/
|
|
80
|
+
constructor(options) {
|
|
81
|
+
if (
|
|
82
|
+
!options?.bucket ||
|
|
83
|
+
typeof options.bucket !== 'object' ||
|
|
84
|
+
Array.isArray(options.bucket)
|
|
85
|
+
)
|
|
86
|
+
throw new Error('You must specify an S3 bucket configuration object.');
|
|
87
|
+
if (!options.bucket.name || typeof options.bucket.name !== 'string' || !options.bucket.name.trim())
|
|
88
|
+
throw new Error('You must specify a non-empty S3 bucket name.');
|
|
89
|
+
if (typeof options?.adapters?.create !== 'function')
|
|
90
|
+
throw new Error('You must specify an S3 backup adapters create method.');
|
|
91
|
+
if (options?.client !== undefined && typeof options.client?.send !== 'function')
|
|
92
|
+
throw new Error('S3 backup client must be an S3Client-compatible object.');
|
|
93
|
+
if (
|
|
94
|
+
options?.destroy_client !== undefined &&
|
|
95
|
+
typeof options.destroy_client !== 'boolean'
|
|
96
|
+
)
|
|
97
|
+
throw new Error('S3 backup destroy_client must be a boolean.');
|
|
98
|
+
if (
|
|
99
|
+
options?.part_size !== undefined &&
|
|
100
|
+
(!Number.isInteger(options.part_size) ||
|
|
101
|
+
options.part_size < minimum_part_size ||
|
|
102
|
+
options.part_size > maximum_part_size)
|
|
103
|
+
)
|
|
104
|
+
throw new Error(
|
|
105
|
+
`S3 backup part_size must be an integer between ${minimum_part_size} and ${maximum_part_size}.`,
|
|
106
|
+
);
|
|
107
|
+
if (
|
|
108
|
+
options?.queue_size !== undefined &&
|
|
109
|
+
(!Number.isInteger(options.queue_size) || options.queue_size <= 0)
|
|
110
|
+
)
|
|
111
|
+
throw new Error('S3 backup queue_size must be a positive integer.');
|
|
112
|
+
this.#adapters = options.adapters;
|
|
113
|
+
this.#bucket = options.bucket;
|
|
114
|
+
this.#client = options.client;
|
|
115
|
+
this.#destroy_client = options.destroy_client ?? !options.client;
|
|
116
|
+
this.#part_size = options.part_size ?? 1024 * 1024 * 8;
|
|
117
|
+
this.#queue_size = options.queue_size ?? 4;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Lists every backup stored in this instance's bucket.
|
|
122
|
+
* @returns {Promise<import('./SimpleBackups.js').Backup[]>}
|
|
123
|
+
*/
|
|
124
|
+
async list() {
|
|
125
|
+
const backups = [];
|
|
126
|
+
let continuation_token;
|
|
127
|
+
|
|
128
|
+
do {
|
|
129
|
+
const response = await this.client.send(
|
|
130
|
+
new ListObjectsV2Command({
|
|
131
|
+
Bucket: this.#bucket.name,
|
|
132
|
+
ContinuationToken: continuation_token,
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
for (const object of response.Contents || []) {
|
|
137
|
+
if (!object.Key) continue;
|
|
138
|
+
const timestamp = this._get_backup_timestamp(object.LastModified);
|
|
139
|
+
if (timestamp === undefined) continue;
|
|
140
|
+
|
|
141
|
+
backups.push({
|
|
142
|
+
id: object.Key,
|
|
143
|
+
timestamp,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
continuation_token = response.IsTruncated ? response.NextContinuationToken : undefined;
|
|
148
|
+
} while (continuation_token);
|
|
149
|
+
|
|
150
|
+
this.#backup_keys = new Set(backups.map((backup) => backup.id));
|
|
151
|
+
return backups;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Creates a backup file using the user adapter and uploads it to S3.
|
|
156
|
+
* @returns {Promise<import('./SimpleBackups.js').Backup>}
|
|
157
|
+
*/
|
|
158
|
+
async create() {
|
|
159
|
+
const source = await this.#adapters.create();
|
|
160
|
+
this._validate_create_result(source);
|
|
161
|
+
|
|
162
|
+
const file = await stat(source.path);
|
|
163
|
+
if (!file.isFile()) throw new Error('S3 backup path must reference a file.');
|
|
164
|
+
|
|
165
|
+
const timestamp = Date.now();
|
|
166
|
+
const key = this._get_backup_key(basename(source.path));
|
|
167
|
+
if (this.#backup_keys.has(key)) {
|
|
168
|
+
await rm(source.path, { force: true });
|
|
169
|
+
throw new Error(`An S3 backup named "${key}" already exists.`);
|
|
170
|
+
}
|
|
171
|
+
const upload_options = this._get_upload_options(source);
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
await this._upload(key, source.path, file.size, upload_options);
|
|
175
|
+
} catch (upload_error) {
|
|
176
|
+
try {
|
|
177
|
+
await rm(source.path, { force: true });
|
|
178
|
+
} catch (cleanup_error) {
|
|
179
|
+
throw new AggregateError(
|
|
180
|
+
[upload_error, cleanup_error],
|
|
181
|
+
'S3 backup upload and cleanup both failed.',
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
throw upload_error;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
await rm(source.path, { force: true });
|
|
189
|
+
} catch (cleanup_error) {
|
|
190
|
+
try {
|
|
191
|
+
await this.client.send(
|
|
192
|
+
new DeleteObjectCommand({
|
|
193
|
+
Bucket: this.#bucket.name,
|
|
194
|
+
Key: key,
|
|
195
|
+
ExpectedBucketOwner: upload_options.ExpectedBucketOwner,
|
|
196
|
+
RequestPayer: upload_options.RequestPayer,
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
} catch (rollback_error) {
|
|
200
|
+
throw new AggregateError(
|
|
201
|
+
[cleanup_error, rollback_error],
|
|
202
|
+
'S3 backup cleanup and upload rollback both failed.',
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
throw cleanup_error;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this.#backup_keys.add(key);
|
|
209
|
+
return {
|
|
210
|
+
id: key,
|
|
211
|
+
timestamp,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Deletes a backup from S3.
|
|
217
|
+
* @param {import('./SimpleBackups.js').Backup} backup
|
|
218
|
+
* @returns {Promise<boolean>}
|
|
219
|
+
*/
|
|
220
|
+
async delete(backup) {
|
|
221
|
+
await this.client.send(
|
|
222
|
+
new DeleteObjectCommand({
|
|
223
|
+
Bucket: this.#bucket.name,
|
|
224
|
+
Key: backup.id,
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
this.#backup_keys.delete(backup.id);
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Destroys the internally managed S3 client when configured to do so.
|
|
233
|
+
*/
|
|
234
|
+
destroy() {
|
|
235
|
+
if (this.#destroy_client) this.#client?.destroy();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Uploads a backup file using a single request or a bounded multipart upload.
|
|
240
|
+
* @param {string} key
|
|
241
|
+
* @param {string} path
|
|
242
|
+
* @param {number} content_length
|
|
243
|
+
* @param {S3SimpleBackupsUploadOptions} upload_options
|
|
244
|
+
*/
|
|
245
|
+
async _upload(key, path, content_length, upload_options) {
|
|
246
|
+
if (content_length <= this.#part_size) {
|
|
247
|
+
await this._put_object(
|
|
248
|
+
key,
|
|
249
|
+
content_length ? createReadStream(path) : Buffer.alloc(0),
|
|
250
|
+
content_length,
|
|
251
|
+
upload_options,
|
|
252
|
+
);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const required_part_size = Math.ceil(content_length / maximum_parts);
|
|
257
|
+
const part_size = Math.max(this.#part_size, required_part_size);
|
|
258
|
+
if (part_size > maximum_part_size)
|
|
259
|
+
throw new Error('S3 backup file exceeds the maximum multipart upload size.');
|
|
260
|
+
|
|
261
|
+
const iterator = this._get_parts(createReadStream(path), part_size)[Symbol.asyncIterator]();
|
|
262
|
+
const first = await iterator.next();
|
|
263
|
+
if (first.done) {
|
|
264
|
+
await this._put_object(key, Buffer.alloc(0), 0, upload_options);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (first.value.length < part_size) {
|
|
268
|
+
await this._put_object(key, first.value, first.value.length, upload_options);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await this._multipart_upload(
|
|
273
|
+
key,
|
|
274
|
+
upload_options,
|
|
275
|
+
[first.value],
|
|
276
|
+
iterator,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Uploads a body using a single PutObject request.
|
|
282
|
+
* @param {string} key
|
|
283
|
+
* @param {NonNullable<import('@aws-sdk/client-s3').PutObjectCommandInput['Body']>} body
|
|
284
|
+
* @param {number} content_length
|
|
285
|
+
* @param {S3SimpleBackupsUploadOptions} upload_options
|
|
286
|
+
*/
|
|
287
|
+
async _put_object(key, body, content_length, upload_options) {
|
|
288
|
+
await this.client.send(
|
|
289
|
+
new PutObjectCommand({
|
|
290
|
+
...upload_options,
|
|
291
|
+
Bucket: this.#bucket.name,
|
|
292
|
+
Key: key,
|
|
293
|
+
Body: body,
|
|
294
|
+
ContentLength: content_length,
|
|
295
|
+
}),
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Uploads buffered parts concurrently and completes or aborts the multipart upload.
|
|
301
|
+
* @param {string} key
|
|
302
|
+
* @param {S3SimpleBackupsUploadOptions} upload_options
|
|
303
|
+
* @param {Buffer[]} initial_parts
|
|
304
|
+
* @param {AsyncIterator<Buffer>} iterator
|
|
305
|
+
*/
|
|
306
|
+
async _multipart_upload(key, upload_options, initial_parts, iterator) {
|
|
307
|
+
const create_response = await this.client.send(
|
|
308
|
+
new CreateMultipartUploadCommand({
|
|
309
|
+
...upload_options,
|
|
310
|
+
Bucket: this.#bucket.name,
|
|
311
|
+
Key: key,
|
|
312
|
+
}),
|
|
313
|
+
);
|
|
314
|
+
if (!create_response.UploadId)
|
|
315
|
+
throw new Error('S3 did not return an upload ID for the multipart backup upload.');
|
|
316
|
+
|
|
317
|
+
const upload_id = create_response.UploadId;
|
|
318
|
+
const completed_parts = [];
|
|
319
|
+
const uploads = new Set();
|
|
320
|
+
let part_upload_error;
|
|
321
|
+
let part_number = 0;
|
|
322
|
+
|
|
323
|
+
const upload_part = (body) => {
|
|
324
|
+
part_number++;
|
|
325
|
+
if (part_number > maximum_parts)
|
|
326
|
+
throw new Error(`S3 backup uploads cannot exceed ${maximum_parts} parts.`);
|
|
327
|
+
|
|
328
|
+
const current_part_number = part_number;
|
|
329
|
+
const upload = this._upload_part(
|
|
330
|
+
key,
|
|
331
|
+
upload_id,
|
|
332
|
+
current_part_number,
|
|
333
|
+
body,
|
|
334
|
+
upload_options,
|
|
335
|
+
)
|
|
336
|
+
.then((completed_part) => {
|
|
337
|
+
completed_parts[current_part_number - 1] = completed_part;
|
|
338
|
+
})
|
|
339
|
+
.catch((error) => {
|
|
340
|
+
part_upload_error ??= error;
|
|
341
|
+
throw error;
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
uploads.add(upload);
|
|
345
|
+
upload.then(
|
|
346
|
+
() => uploads.delete(upload),
|
|
347
|
+
() => uploads.delete(upload),
|
|
348
|
+
);
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
for (const part of initial_parts) {
|
|
353
|
+
upload_part(part);
|
|
354
|
+
if (uploads.size >= this.#queue_size) await Promise.race(uploads);
|
|
355
|
+
if (part_upload_error) throw part_upload_error;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
for (;;) {
|
|
359
|
+
const next = await iterator.next();
|
|
360
|
+
if (part_upload_error) throw part_upload_error;
|
|
361
|
+
if (next.done) break;
|
|
362
|
+
|
|
363
|
+
upload_part(next.value);
|
|
364
|
+
if (uploads.size >= this.#queue_size) await Promise.race(uploads);
|
|
365
|
+
if (part_upload_error) throw part_upload_error;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
await Promise.all(uploads);
|
|
369
|
+
if (part_upload_error) throw part_upload_error;
|
|
370
|
+
await this.client.send(
|
|
371
|
+
new CompleteMultipartUploadCommand({
|
|
372
|
+
Bucket: this.#bucket.name,
|
|
373
|
+
Key: key,
|
|
374
|
+
UploadId: upload_id,
|
|
375
|
+
MultipartUpload: {
|
|
376
|
+
Parts: completed_parts,
|
|
377
|
+
},
|
|
378
|
+
ExpectedBucketOwner: upload_options.ExpectedBucketOwner,
|
|
379
|
+
RequestPayer: upload_options.RequestPayer,
|
|
380
|
+
}),
|
|
381
|
+
);
|
|
382
|
+
} catch (upload_error) {
|
|
383
|
+
await Promise.allSettled(uploads);
|
|
384
|
+
try {
|
|
385
|
+
await this.client.send(
|
|
386
|
+
new AbortMultipartUploadCommand({
|
|
387
|
+
Bucket: this.#bucket.name,
|
|
388
|
+
Key: key,
|
|
389
|
+
UploadId: upload_id,
|
|
390
|
+
ExpectedBucketOwner: upload_options.ExpectedBucketOwner,
|
|
391
|
+
RequestPayer: upload_options.RequestPayer,
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
394
|
+
} catch (abort_error) {
|
|
395
|
+
throw new AggregateError(
|
|
396
|
+
[upload_error, abort_error],
|
|
397
|
+
'S3 backup multipart upload and abort both failed.',
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
throw upload_error;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Uploads a single multipart body part.
|
|
406
|
+
* @param {string} key
|
|
407
|
+
* @param {string} upload_id
|
|
408
|
+
* @param {number} part_number
|
|
409
|
+
* @param {Buffer} body
|
|
410
|
+
* @param {S3SimpleBackupsUploadOptions} upload_options
|
|
411
|
+
* @returns {Promise<import('@aws-sdk/client-s3').CompletedPart>}
|
|
412
|
+
*/
|
|
413
|
+
async _upload_part(key, upload_id, part_number, body, upload_options) {
|
|
414
|
+
const response = await this.client.send(
|
|
415
|
+
new UploadPartCommand({
|
|
416
|
+
Bucket: this.#bucket.name,
|
|
417
|
+
Key: key,
|
|
418
|
+
UploadId: upload_id,
|
|
419
|
+
PartNumber: part_number,
|
|
420
|
+
Body: body,
|
|
421
|
+
ContentLength: body.length,
|
|
422
|
+
ChecksumAlgorithm: upload_options.ChecksumAlgorithm,
|
|
423
|
+
ExpectedBucketOwner: upload_options.ExpectedBucketOwner,
|
|
424
|
+
RequestPayer: upload_options.RequestPayer,
|
|
425
|
+
SSECustomerAlgorithm: upload_options.SSECustomerAlgorithm,
|
|
426
|
+
SSECustomerKey: upload_options.SSECustomerKey,
|
|
427
|
+
SSECustomerKeyMD5: upload_options.SSECustomerKeyMD5,
|
|
428
|
+
}),
|
|
429
|
+
);
|
|
430
|
+
if (!response.ETag)
|
|
431
|
+
throw new Error(`S3 did not return an ETag for backup upload part ${part_number}.`);
|
|
432
|
+
|
|
433
|
+
const completed_part = {
|
|
434
|
+
ETag: response.ETag,
|
|
435
|
+
PartNumber: part_number,
|
|
436
|
+
};
|
|
437
|
+
for (const checksum of [
|
|
438
|
+
'ChecksumCRC32',
|
|
439
|
+
'ChecksumCRC32C',
|
|
440
|
+
'ChecksumCRC64NVME',
|
|
441
|
+
'ChecksumSHA1',
|
|
442
|
+
'ChecksumSHA256',
|
|
443
|
+
]) {
|
|
444
|
+
if (response[checksum] !== undefined) completed_part[checksum] = response[checksum];
|
|
445
|
+
}
|
|
446
|
+
return completed_part;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Splits a backup file stream into fixed-size multipart buffers.
|
|
451
|
+
* @param {import('node:fs').ReadStream} body
|
|
452
|
+
* @param {number} part_size
|
|
453
|
+
* @returns {AsyncGenerator<Buffer>}
|
|
454
|
+
*/
|
|
455
|
+
async *_get_parts(body, part_size) {
|
|
456
|
+
let buffers = [];
|
|
457
|
+
let buffered_bytes = 0;
|
|
458
|
+
|
|
459
|
+
for await (const chunk of body) {
|
|
460
|
+
const buffer = this._to_buffer(chunk);
|
|
461
|
+
let offset = 0;
|
|
462
|
+
|
|
463
|
+
while (offset < buffer.length) {
|
|
464
|
+
const available_bytes = part_size - buffered_bytes;
|
|
465
|
+
const bytes_to_copy = Math.min(available_bytes, buffer.length - offset);
|
|
466
|
+
buffers.push(buffer.subarray(offset, offset + bytes_to_copy));
|
|
467
|
+
buffered_bytes += bytes_to_copy;
|
|
468
|
+
offset += bytes_to_copy;
|
|
469
|
+
|
|
470
|
+
if (buffered_bytes === part_size) {
|
|
471
|
+
yield Buffer.concat(buffers, part_size);
|
|
472
|
+
buffers = [];
|
|
473
|
+
buffered_bytes = 0;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (buffered_bytes > 0) yield Buffer.concat(buffers, buffered_bytes);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Converts a streamed body chunk into a Buffer.
|
|
483
|
+
* @param {unknown} chunk
|
|
484
|
+
* @returns {Buffer}
|
|
485
|
+
*/
|
|
486
|
+
_to_buffer(chunk) {
|
|
487
|
+
if (typeof chunk === 'string') return Buffer.from(chunk);
|
|
488
|
+
if (chunk instanceof Uint8Array)
|
|
489
|
+
return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
490
|
+
if (chunk instanceof ArrayBuffer) return Buffer.from(chunk);
|
|
491
|
+
throw new Error('S3 backup streams must emit strings or byte arrays.');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Validates a backup creation adapter result.
|
|
496
|
+
* @param {S3SimpleBackupsCreateResult} source
|
|
497
|
+
*/
|
|
498
|
+
_validate_create_result(source) {
|
|
499
|
+
if (!source || typeof source !== 'object' || Array.isArray(source))
|
|
500
|
+
throw new Error('S3 backup adapters create method must return an object.');
|
|
501
|
+
if (!source.path || typeof source.path !== 'string')
|
|
502
|
+
throw new Error('S3 backup adapters create method must return a non-empty path.');
|
|
503
|
+
if (
|
|
504
|
+
source.options !== undefined &&
|
|
505
|
+
(!source.options || typeof source.options !== 'object' || Array.isArray(source.options))
|
|
506
|
+
)
|
|
507
|
+
throw new Error('S3 backup options must be an object.');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Creates an object key using the backup filename.
|
|
512
|
+
* @param {string} filename
|
|
513
|
+
* @returns {string}
|
|
514
|
+
*/
|
|
515
|
+
_get_backup_key(filename) {
|
|
516
|
+
return filename;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Gets a timestamp from an S3 modification date.
|
|
521
|
+
* @param {Date|undefined} last_modified
|
|
522
|
+
* @returns {number|undefined}
|
|
523
|
+
*/
|
|
524
|
+
_get_backup_timestamp(last_modified) {
|
|
525
|
+
const timestamp = new Date(last_modified).getTime();
|
|
526
|
+
if (!Number.isFinite(timestamp) || timestamp < 0) return;
|
|
527
|
+
return timestamp;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Returns the protected S3 SDK options for a backup upload.
|
|
532
|
+
* @param {S3SimpleBackupsCreateResult} source
|
|
533
|
+
* @returns {S3SimpleBackupsUploadOptions}
|
|
534
|
+
*/
|
|
535
|
+
_get_upload_options(source) {
|
|
536
|
+
return this._remove_protected_upload_options(source.options ?? {});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Removes request properties which are managed internally by this class.
|
|
541
|
+
* @param {S3SimpleBackupsUploadOptions} upload_options
|
|
542
|
+
* @returns {S3SimpleBackupsUploadOptions}
|
|
543
|
+
*/
|
|
544
|
+
_remove_protected_upload_options(upload_options) {
|
|
545
|
+
const {
|
|
546
|
+
Body: _,
|
|
547
|
+
Bucket: __,
|
|
548
|
+
ContentLength: ___,
|
|
549
|
+
Key: ____,
|
|
550
|
+
...remaining_upload_options
|
|
551
|
+
} = upload_options;
|
|
552
|
+
return remaining_upload_options;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Returns the configured S3 client, constructing it lazily when needed.
|
|
557
|
+
* @returns {S3Client}
|
|
558
|
+
*/
|
|
559
|
+
get client() {
|
|
560
|
+
if (!this.#client) {
|
|
561
|
+
const { name: _, ...client_options } = this.#bucket;
|
|
562
|
+
this.#client = new S3Client({
|
|
563
|
+
requestChecksumCalculation: 'WHEN_REQUIRED',
|
|
564
|
+
responseChecksumValidation: 'WHEN_REQUIRED',
|
|
565
|
+
...client_options,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
return this.#client;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Returns the configured S3 bucket.
|
|
573
|
+
*/
|
|
574
|
+
get bucket() {
|
|
575
|
+
return this.#bucket.name;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* A SimpleBackups implementation which stores backups in any S3-compatible object storage bucket.
|
|
581
|
+
*/
|
|
582
|
+
export class S3SimpleBackups extends SimpleBackups {
|
|
583
|
+
/** @type {S3BackupStorage} */
|
|
584
|
+
#storage;
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Constructs a new S3SimpleBackups instance.
|
|
588
|
+
* @param {S3SimpleBackupsOptions} options
|
|
589
|
+
*/
|
|
590
|
+
constructor(options) {
|
|
591
|
+
const storage = new S3BackupStorage(options);
|
|
592
|
+
super({
|
|
593
|
+
windows: options?.windows,
|
|
594
|
+
adapters: {
|
|
595
|
+
list: () => storage.list(),
|
|
596
|
+
create: () => storage.create(),
|
|
597
|
+
delete: (backup) => storage.delete(backup),
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
this.#storage = storage;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Destroys this instance and its internally managed S3 client.
|
|
605
|
+
*/
|
|
606
|
+
destroy() {
|
|
607
|
+
if (this.destroyed) return;
|
|
608
|
+
super.destroy();
|
|
609
|
+
this.#storage.destroy();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Returns the configured S3 client.
|
|
614
|
+
*/
|
|
615
|
+
get client() {
|
|
616
|
+
return this.#storage.client;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Returns the configured S3 bucket.
|
|
621
|
+
*/
|
|
622
|
+
get bucket() {
|
|
623
|
+
return this.#storage.bucket;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreateMultipartUploadCommandInput,
|
|
3
|
+
S3Client,
|
|
4
|
+
S3ClientConfig,
|
|
5
|
+
} from '@aws-sdk/client-s3';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A backup managed by SimpleBackups.
|
|
9
|
+
*/
|
|
10
|
+
export interface Backup {
|
|
11
|
+
/** The unique identifier of the backup. */
|
|
12
|
+
id: string;
|
|
13
|
+
|
|
14
|
+
/** The timestamp at which the backup was created (in milliseconds). */
|
|
15
|
+
timestamp: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A retention window used by SimpleBackups.
|
|
20
|
+
*/
|
|
21
|
+
export interface BackupWindow {
|
|
22
|
+
/** The width of each retention slot in this window (in milliseconds). */
|
|
23
|
+
interval_ms: number;
|
|
24
|
+
|
|
25
|
+
/** The maximum number of backups to keep in this window. */
|
|
26
|
+
limit: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Adapter methods used to persist backups in an upstream storage mechanism.
|
|
31
|
+
*/
|
|
32
|
+
export interface BackupAdapters {
|
|
33
|
+
/** Lists all backups from the upstream storage mechanism. */
|
|
34
|
+
list(): Backup[] | Promise<Backup[]>;
|
|
35
|
+
|
|
36
|
+
/** Creates and persists a new backup in the upstream storage mechanism. */
|
|
37
|
+
create(): Backup | Promise<Backup>;
|
|
38
|
+
|
|
39
|
+
/** Attempts to delete a backup and reports whether the deletion succeeded. */
|
|
40
|
+
delete(backup: Backup): boolean | Promise<boolean>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Options used to construct a SimpleBackups instance.
|
|
45
|
+
*/
|
|
46
|
+
export interface SimpleBackupsOptions {
|
|
47
|
+
/** The backup adapters to use. */
|
|
48
|
+
adapters: BackupAdapters;
|
|
49
|
+
|
|
50
|
+
/** The backup retention windows to maintain. */
|
|
51
|
+
windows: BackupWindow[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type SimpleBackupsBackupEvent = 'create' | 'delete';
|
|
55
|
+
export type SimpleBackupsErrorEvent = 'error';
|
|
56
|
+
export type SimpleBackupsBackupListener = (backup: Backup) => void;
|
|
57
|
+
export type SimpleBackupsErrorListener = (error: Error) => void;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Automatically creates and prunes backups using the configured adapters and retention windows.
|
|
61
|
+
*/
|
|
62
|
+
export class SimpleBackups {
|
|
63
|
+
constructor(options: SimpleBackupsOptions);
|
|
64
|
+
|
|
65
|
+
on(event: SimpleBackupsBackupEvent, listener: SimpleBackupsBackupListener): this;
|
|
66
|
+
on(event: SimpleBackupsErrorEvent, listener: SimpleBackupsErrorListener): this;
|
|
67
|
+
|
|
68
|
+
once(event: SimpleBackupsBackupEvent, listener: SimpleBackupsBackupListener): this;
|
|
69
|
+
once(event: SimpleBackupsErrorEvent, listener: SimpleBackupsErrorListener): this;
|
|
70
|
+
|
|
71
|
+
off(event: SimpleBackupsBackupEvent, listener: SimpleBackupsBackupListener): this;
|
|
72
|
+
off(event: SimpleBackupsErrorEvent, listener: SimpleBackupsErrorListener): this;
|
|
73
|
+
|
|
74
|
+
emit(event: SimpleBackupsBackupEvent, backup: Backup): boolean;
|
|
75
|
+
emit(event: SimpleBackupsErrorEvent, error: Error): boolean;
|
|
76
|
+
|
|
77
|
+
/** Destroys this instance, stops its internal interval and removes its event listeners. */
|
|
78
|
+
destroy(): void;
|
|
79
|
+
|
|
80
|
+
/** The current cached backups, returned as a new array. */
|
|
81
|
+
readonly backups: Backup[];
|
|
82
|
+
|
|
83
|
+
/** Whether this instance has been destroyed. */
|
|
84
|
+
readonly destroyed: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** S3 SDK upload options which can be applied without overriding internally managed fields. */
|
|
88
|
+
export type S3SimpleBackupsUploadOptions = Omit<
|
|
89
|
+
CreateMultipartUploadCommandInput,
|
|
90
|
+
'Body' | 'Bucket' | 'ContentLength' | 'Key'
|
|
91
|
+
>;
|
|
92
|
+
|
|
93
|
+
/** The bucket name and native configuration used to construct the S3 client. */
|
|
94
|
+
export type S3SimpleBackupsBucket = S3ClientConfig & {
|
|
95
|
+
/** The name of the S3 bucket in which backups should be stored. */
|
|
96
|
+
name: string;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A backup file and its optional S3 object properties.
|
|
101
|
+
*/
|
|
102
|
+
export interface S3SimpleBackupsCreateResult {
|
|
103
|
+
/** The path of the backup file to upload. */
|
|
104
|
+
path: string;
|
|
105
|
+
|
|
106
|
+
/** Native S3 SDK options applied to the uploaded object. */
|
|
107
|
+
options?: S3SimpleBackupsUploadOptions;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The backup creation adapter used by S3SimpleBackups.
|
|
112
|
+
*/
|
|
113
|
+
export interface S3SimpleBackupsAdapters {
|
|
114
|
+
/** Creates a backup file and returns its path and upload options. */
|
|
115
|
+
create(): Promise<S3SimpleBackupsCreateResult>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Options used to construct an S3SimpleBackups instance.
|
|
120
|
+
*/
|
|
121
|
+
export interface S3SimpleBackupsOptions {
|
|
122
|
+
/** The backup retention windows to maintain. */
|
|
123
|
+
windows: BackupWindow[];
|
|
124
|
+
|
|
125
|
+
/** The S3 bucket and client configuration to use. */
|
|
126
|
+
bucket: S3SimpleBackupsBucket;
|
|
127
|
+
|
|
128
|
+
/** The backup creation adapter to use. */
|
|
129
|
+
adapters: S3SimpleBackupsAdapters;
|
|
130
|
+
|
|
131
|
+
/** An existing S3 client to use. */
|
|
132
|
+
client?: S3Client;
|
|
133
|
+
|
|
134
|
+
/** Whether the S3 client should be destroyed with this instance. */
|
|
135
|
+
destroy_client?: boolean;
|
|
136
|
+
|
|
137
|
+
/** The multipart upload part size in bytes. */
|
|
138
|
+
part_size?: number;
|
|
139
|
+
|
|
140
|
+
/** The maximum number of parts to upload concurrently. */
|
|
141
|
+
queue_size?: number;
|
|
142
|
+
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* A SimpleBackups implementation which stores backups in any S3-compatible object storage bucket.
|
|
147
|
+
*/
|
|
148
|
+
export class S3SimpleBackups extends SimpleBackups {
|
|
149
|
+
constructor(options: S3SimpleBackupsOptions);
|
|
150
|
+
|
|
151
|
+
/** The configured S3 client. */
|
|
152
|
+
readonly client: S3Client;
|
|
153
|
+
|
|
154
|
+
/** The configured S3 bucket. */
|
|
155
|
+
readonly bucket: string;
|
|
156
|
+
|
|
157
|
+
}
|
package/src/index.js
CHANGED