@delali/sirannon-db 0.2.3-next.38 → 0.3.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/dist/backup/index.d.ts +5 -5
- package/dist/backup/index.mjs +4 -4
- package/dist/backup-scheduler/index.d.ts +58 -8
- package/dist/backup-scheduler/index.mjs +3 -2
- package/dist/{change-tracker-BLKRX8ic.d.ts → change-tracker-uG9gWvsY.d.ts} +1 -1
- package/dist/chunk-2QCPVE67.mjs +143 -0
- package/dist/{chunk-P53I2G6J.mjs → chunk-63VBFSCS.mjs} +2 -2
- package/dist/{chunk-CR6HTXSC.mjs → chunk-7SC2TZAS.mjs} +1 -1
- package/dist/chunk-DVS4QSIZ.mjs +124 -0
- package/dist/{chunk-IPDHHIG2.mjs → chunk-DXXCLTFF.mjs} +205 -45
- package/dist/{chunk-G7AXGAEA.mjs → chunk-OJQHM4MT.mjs} +202 -100
- package/dist/{chunk-KSYFFYAP.mjs → chunk-U445A3C4.mjs} +1 -70
- package/dist/{chunk-GJ6SX53Z.mjs → chunk-W77EEVOT.mjs} +4 -120
- package/dist/client/index.d.ts +8 -8
- package/dist/client/topology.d.ts +4 -4
- package/dist/{client-base-DldBgJWS.d.ts → client-base-C-B8u5Lm.d.ts} +2 -2
- package/dist/core/index.d.ts +9 -9
- package/dist/core/index.mjs +14 -5
- package/dist/{database-DSbJl-k5.d.ts → database-BlhUdJAO.d.ts} +7 -6
- package/dist/{database-backup-CliAhaEm.d.ts → database-backup-C090lMHc.d.ts} +2 -2
- package/dist/driver/better-sqlite3.d.ts +1 -1
- package/dist/driver/better-sqlite3.mjs +7 -6
- package/dist/driver/bun.d.ts +1 -1
- package/dist/driver/expo.d.ts +1 -1
- package/dist/driver/node.d.ts +1 -1
- package/dist/driver/node.mjs +7 -6
- package/dist/driver/wa-sqlite.d.ts +1 -1
- package/dist/file-migrations/index.d.ts +1 -1
- package/dist/{protocol-BK0Yxu5P.d.ts → protocol-QekTGoKn.d.ts} +1 -1
- package/dist/replication/index.d.ts +7 -7
- package/dist/{restore-options-O5gYlr3n.d.ts → restore-options-C9MW9gpm.d.ts} +1 -1
- package/dist/server/index.d.ts +9 -9
- package/dist/server/index.mjs +5 -4
- package/dist/{server-options-CFgBQDLz.d.ts → server-options-DeK6ZG8u.d.ts} +1 -1
- package/dist/{sirannon-Bo0PX2Cj.d.ts → sirannon-C9cTgFrw.d.ts} +2 -2
- package/dist/transport/grpc.d.ts +3 -3
- package/dist/transport/memory.d.ts +3 -3
- package/dist/{types-CL8uxXIV.d.ts → types-C1yBGTdx.d.ts} +2 -2
- package/dist/{types-CopTgHG3.d.ts → types-DApb7dvw.d.ts} +151 -5
- package/package.json +8 -7
- package/dist/chunk-GBM4AU24.mjs +0 -42
|
@@ -1,13 +1,38 @@
|
|
|
1
1
|
import { startCopyWithoutHoldingWriter } from './chunk-W67A4H2E.mjs';
|
|
2
|
-
import {
|
|
2
|
+
import { readLogPosition } from './chunk-2QCPVE67.mjs';
|
|
3
|
+
import { destinationWithDeadline, DEFAULT_DESTINATION_TIMEOUT_MS, assertDeadline, withinDeadline } from './chunk-DVS4QSIZ.mjs';
|
|
3
4
|
import { randomHex } from './chunk-V5FUJEYY.mjs';
|
|
4
5
|
import { BackupError, SirannonError } from './chunk-PBRXXISQ.mjs';
|
|
5
|
-
import { existsSync, mkdirSync,
|
|
6
|
-
import { resolve, dirname, join } from 'path';
|
|
6
|
+
import { existsSync, mkdirSync, statSync, readdirSync, lstatSync, rmSync } from 'fs';
|
|
7
|
+
import { resolve, dirname, join, basename, extname } from 'path';
|
|
7
8
|
import { open, mkdtemp, rm } from 'fs/promises';
|
|
8
9
|
import { tmpdir } from 'os';
|
|
9
10
|
import { createHash } from 'crypto';
|
|
10
11
|
|
|
12
|
+
// src/core/backup/report.ts
|
|
13
|
+
var DEFAULT_PIECE_BYTES = 16 * 1024 * 1024;
|
|
14
|
+
function defaultDestinationName() {
|
|
15
|
+
return `backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.db`;
|
|
16
|
+
}
|
|
17
|
+
function assertPieceBytes(pieceBytes) {
|
|
18
|
+
if (!Number.isInteger(pieceBytes) || pieceBytes <= 0) {
|
|
19
|
+
throw new SirannonError(
|
|
20
|
+
`Piece size must be a positive whole number of bytes, and it was ${pieceBytes}`,
|
|
21
|
+
"BACKUP_ERROR"
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function readPageSize(conn) {
|
|
26
|
+
const stmt = await conn.prepare("PRAGMA page_size");
|
|
27
|
+
const row = await stmt.get();
|
|
28
|
+
return row ? Number(row.page_size) : 0;
|
|
29
|
+
}
|
|
30
|
+
async function readMainDatabasePath(conn) {
|
|
31
|
+
const stmt = await conn.prepare("PRAGMA database_list");
|
|
32
|
+
const rows = await stmt.all();
|
|
33
|
+
const main = rows.find((row) => row.name === "main");
|
|
34
|
+
return main?.file ?? "";
|
|
35
|
+
}
|
|
11
36
|
function destinationPieceError(name, index, err) {
|
|
12
37
|
if (err instanceof SirannonError) return err;
|
|
13
38
|
return new SirannonError(
|
|
@@ -60,25 +85,6 @@ async function sendFileInPieces(sourcePath, destination, name, pieceBytes, finge
|
|
|
60
85
|
return { pieceCount: index, bytesWritten, ...digest ? { fingerprint: digest.digest("hex") } : {} };
|
|
61
86
|
}
|
|
62
87
|
|
|
63
|
-
// src/core/backup/report.ts
|
|
64
|
-
var DEFAULT_PIECE_BYTES = 16 * 1024 * 1024;
|
|
65
|
-
function defaultDestinationName() {
|
|
66
|
-
return `backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.db`;
|
|
67
|
-
}
|
|
68
|
-
function assertPieceBytes(pieceBytes) {
|
|
69
|
-
if (!Number.isInteger(pieceBytes) || pieceBytes <= 0) {
|
|
70
|
-
throw new SirannonError(
|
|
71
|
-
`Piece size must be a positive whole number of bytes, and it was ${pieceBytes}`,
|
|
72
|
-
"BACKUP_ERROR"
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
async function readPageSize(conn) {
|
|
77
|
-
const stmt = await conn.prepare("PRAGMA page_size");
|
|
78
|
-
const row = await stmt.get();
|
|
79
|
-
return row ? Number(row.page_size) : 0;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
88
|
// src/core/backup/stepped-copy.ts
|
|
83
89
|
var DEFAULT_PAGES_PER_STEP = 256;
|
|
84
90
|
var DEFAULT_RESTART_LIMIT = 3;
|
|
@@ -182,11 +188,16 @@ async function copyDatabaseStepwise(conn, options) {
|
|
|
182
188
|
});
|
|
183
189
|
copy.catch(() => {
|
|
184
190
|
});
|
|
191
|
+
let copyStopped = false;
|
|
192
|
+
const stops = () => {
|
|
193
|
+
copyStopped = true;
|
|
194
|
+
};
|
|
195
|
+
copy.then(stops, stops);
|
|
185
196
|
try {
|
|
186
197
|
const final = await Promise.race([copy, stalled]);
|
|
187
198
|
return { pageCount: final.totalPages, restarts };
|
|
188
199
|
} catch (err) {
|
|
189
|
-
options.onCopyLeftRunning?.(copy);
|
|
200
|
+
if (!copyStopped) options.onCopyLeftRunning?.(copy);
|
|
190
201
|
throw err;
|
|
191
202
|
} finally {
|
|
192
203
|
if (stallTimer) clearTimeout(stallTimer);
|
|
@@ -227,6 +238,7 @@ async function copyToDestinationStaged(conn, request) {
|
|
|
227
238
|
}
|
|
228
239
|
});
|
|
229
240
|
const copyMs = Date.now() - copyStartedAt;
|
|
241
|
+
const logPosition = await readLogPosition(request.sourcePath);
|
|
230
242
|
const transferStartedAt = Date.now();
|
|
231
243
|
const sent = await sendFileInPieces(
|
|
232
244
|
stagedPath,
|
|
@@ -264,6 +276,7 @@ async function copyToDestinationStaged(conn, request) {
|
|
|
264
276
|
pieceCount: sent.pieceCount,
|
|
265
277
|
pieceBytes,
|
|
266
278
|
restarts: copy.restarts,
|
|
279
|
+
...logPosition ? { logPosition } : {},
|
|
267
280
|
...sent.fingerprint ? { fingerprint: sent.fingerprint } : {}
|
|
268
281
|
};
|
|
269
282
|
} finally {
|
|
@@ -609,6 +622,7 @@ async function copyToDestinationStreamed(conn, request, support) {
|
|
|
609
622
|
}
|
|
610
623
|
});
|
|
611
624
|
const copyMs = Date.now() - copyStartedAt;
|
|
625
|
+
const logPosition = await readLogPosition(request.sourcePath);
|
|
612
626
|
const bytesStreamed = await host.finish(streamId);
|
|
613
627
|
copying = false;
|
|
614
628
|
await pumping;
|
|
@@ -635,6 +649,7 @@ async function copyToDestinationStreamed(conn, request, support) {
|
|
|
635
649
|
pieceCount: piecesWritten,
|
|
636
650
|
pieceBytes,
|
|
637
651
|
restarts: copy.restarts,
|
|
652
|
+
...logPosition ? { logPosition } : {},
|
|
638
653
|
...fingerprint ? { fingerprint } : {}
|
|
639
654
|
};
|
|
640
655
|
} catch (err) {
|
|
@@ -663,6 +678,20 @@ function once(action) {
|
|
|
663
678
|
action();
|
|
664
679
|
};
|
|
665
680
|
}
|
|
681
|
+
function removeQuietly(path) {
|
|
682
|
+
try {
|
|
683
|
+
rmSync(path, { force: true });
|
|
684
|
+
} catch {
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
function removeOnceCopyStops(copy, path) {
|
|
688
|
+
const remove = () => removeQuietly(path);
|
|
689
|
+
copy.then(remove, remove);
|
|
690
|
+
}
|
|
691
|
+
function backupFailure(destPath, err) {
|
|
692
|
+
if (err instanceof SirannonError) return err;
|
|
693
|
+
return new BackupError(`Backup to '${destPath}' failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
694
|
+
}
|
|
666
695
|
function hasControlCharacters(s) {
|
|
667
696
|
for (let i = 0; i < s.length; i++) {
|
|
668
697
|
const code = s.charCodeAt(i);
|
|
@@ -674,6 +703,15 @@ var BackupManager = class {
|
|
|
674
703
|
constructor(streaming) {
|
|
675
704
|
this.streaming = streaming;
|
|
676
705
|
}
|
|
706
|
+
/**
|
|
707
|
+
* Copies the database behind a connection to a file, while that database
|
|
708
|
+
* stays open for reads and writes.
|
|
709
|
+
*
|
|
710
|
+
* @param conn - Connection the copy runs on, which must be the connection that writes.
|
|
711
|
+
* @param destPath - Path to write the copy to. A file already there stops the copy.
|
|
712
|
+
* @param onFirstStep - Called once the copy's first step is done, so the caller can hand the writer back.
|
|
713
|
+
* @returns What the copy moved, how long it took, and how often it restarted.
|
|
714
|
+
*/
|
|
677
715
|
async backup(conn, destPath, onFirstStep) {
|
|
678
716
|
if (hasControlCharacters(destPath)) {
|
|
679
717
|
throw new BackupError("Backup path contains invalid characters");
|
|
@@ -696,15 +734,68 @@ var BackupManager = class {
|
|
|
696
734
|
if (existsSync(resolved)) {
|
|
697
735
|
throw new BackupError(`Backup destination '${destPath}' already exists`);
|
|
698
736
|
}
|
|
737
|
+
let pageSize;
|
|
699
738
|
try {
|
|
700
|
-
await
|
|
739
|
+
pageSize = await readPageSize(conn);
|
|
701
740
|
} catch (err) {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
741
|
+
throw backupFailure(destPath, err);
|
|
742
|
+
}
|
|
743
|
+
const startedAt = Date.now();
|
|
744
|
+
const copy = await this.copyOrClearUp(conn, resolved, destPath, onFirstStep);
|
|
745
|
+
const finishedAt = Date.now();
|
|
746
|
+
return {
|
|
747
|
+
runId: randomHex(8),
|
|
748
|
+
destPath: resolved,
|
|
749
|
+
startedAt,
|
|
750
|
+
finishedAt,
|
|
751
|
+
durationMs: finishedAt - startedAt,
|
|
752
|
+
pageCount: copy.pageCount,
|
|
753
|
+
pageSize,
|
|
754
|
+
byteLength: this.fileBytes(resolved, destPath),
|
|
755
|
+
restarts: copy.restarts
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Reads the size of the file a copy wrote.
|
|
760
|
+
*
|
|
761
|
+
* @param resolved - Absolute path of that file.
|
|
762
|
+
* @param destPath - Path the caller named, which the error states.
|
|
763
|
+
* @returns Bytes it holds.
|
|
764
|
+
*/
|
|
765
|
+
fileBytes(resolved, destPath) {
|
|
766
|
+
try {
|
|
767
|
+
return statSync(resolved).size;
|
|
768
|
+
} catch (err) {
|
|
769
|
+
throw backupFailure(destPath, err);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Runs the copy, and removes the file it was writing where that copy fails, so
|
|
774
|
+
* that a database missing its later pages never stays on disk as though the
|
|
775
|
+
* copy had finished. Sirannon would otherwise count that half-written file
|
|
776
|
+
* among the copies it keeps, and it would evict a whole copy to make room for
|
|
777
|
+
* one nothing can restore.
|
|
778
|
+
*
|
|
779
|
+
* Sirannon stops waiting on a copy once the stall deadline passes, and SQLite
|
|
780
|
+
* keeps writing that copy to the file. A removal that raced a live copy would
|
|
781
|
+
* leave a truncated file in place, and on Windows it would fail outright.
|
|
782
|
+
* Sirannon therefore removes the file only once the copy stops, while the
|
|
783
|
+
* caller receives the failure at once and waits for none of that.
|
|
784
|
+
*/
|
|
785
|
+
async copyOrClearUp(conn, resolved, destPath, onFirstStep) {
|
|
786
|
+
const abandoned = { copy: null };
|
|
787
|
+
try {
|
|
788
|
+
return await copyDatabaseStepwise(conn, {
|
|
789
|
+
destPath: resolved,
|
|
790
|
+
onStep: onFirstStep ? once(onFirstStep) : void 0,
|
|
791
|
+
onCopyLeftRunning: (copy) => {
|
|
792
|
+
abandoned.copy = copy;
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
} catch (err) {
|
|
796
|
+
if (abandoned.copy) removeOnceCopyStops(abandoned.copy, resolved);
|
|
797
|
+
else removeQuietly(resolved);
|
|
798
|
+
throw backupFailure(destPath, err);
|
|
708
799
|
}
|
|
709
800
|
}
|
|
710
801
|
async copyToDestination(conn, request) {
|
|
@@ -1004,6 +1095,7 @@ var DEFAULT_MAX_FILES = 5;
|
|
|
1004
1095
|
var MINUTE_RESOLUTION_MS = 6e4;
|
|
1005
1096
|
var SECOND_RESOLUTION_MS = 1e3;
|
|
1006
1097
|
var DST_LOOKBACK_MS = 3 * 60 * 60 * 1e3;
|
|
1098
|
+
var DEFAULT_ON_BACKUP_TIMEOUT_MS = 6e5;
|
|
1007
1099
|
function toError(value) {
|
|
1008
1100
|
if (value instanceof Error) {
|
|
1009
1101
|
return value;
|
|
@@ -1063,12 +1155,11 @@ var BackupScheduler = class {
|
|
|
1063
1155
|
* Starts repeating backups and returns a function that stops them.
|
|
1064
1156
|
*
|
|
1065
1157
|
* @param conn - Connection to the database being copied.
|
|
1066
|
-
* @param
|
|
1067
|
-
* @param runExclusive - Runs each copy with the database's writer held so that no write commits mid-copy.
|
|
1158
|
+
* @param request - Cron expression, destination directory, retention, and time zone, along with the callbacks and the database the copies come from.
|
|
1068
1159
|
* @returns A function that stops the schedule.
|
|
1069
1160
|
*/
|
|
1070
|
-
schedule(conn,
|
|
1071
|
-
const { cron: cronExpr, destDir, maxFiles = DEFAULT_MAX_FILES,
|
|
1161
|
+
schedule(conn, request) {
|
|
1162
|
+
const { cron: cronExpr, destDir, maxFiles = DEFAULT_MAX_FILES, timezone } = request;
|
|
1072
1163
|
let cron;
|
|
1073
1164
|
try {
|
|
1074
1165
|
cron = parseCron(cronExpr);
|
|
@@ -1084,6 +1175,8 @@ var BackupScheduler = class {
|
|
|
1084
1175
|
throw new BackupError(`Invalid timezone '${timezone}': ${err instanceof Error ? err.message : String(err)}`);
|
|
1085
1176
|
}
|
|
1086
1177
|
}
|
|
1178
|
+
const onBackupTimeoutMs = request.onBackupTimeoutMs ?? DEFAULT_ON_BACKUP_TIMEOUT_MS;
|
|
1179
|
+
assertDeadline(onBackupTimeoutMs, "onBackupTimeoutMs");
|
|
1087
1180
|
const resolvedDir = resolve(destDir);
|
|
1088
1181
|
if (!existsSync(resolvedDir)) {
|
|
1089
1182
|
try {
|
|
@@ -1094,32 +1187,99 @@ var BackupScheduler = class {
|
|
|
1094
1187
|
);
|
|
1095
1188
|
}
|
|
1096
1189
|
}
|
|
1097
|
-
return this.run(conn, cron, resolvedDir, maxFiles, timezone,
|
|
1190
|
+
return this.run(conn, { ...request, cron, resolvedDir, maxFiles, timezone, onBackupTimeoutMs });
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Works out the database and the file every report names. A caller may supply
|
|
1194
|
+
* either of them, and Sirannon reads the file SQLite has open where the caller
|
|
1195
|
+
* named none. Sirannon puts that question to the connection that writes, so it
|
|
1196
|
+
* asks with nothing else holding the writer.
|
|
1197
|
+
*
|
|
1198
|
+
* @param conn - Connection the copies come from.
|
|
1199
|
+
* @param run - The schedule, which holds whichever of the two the caller named.
|
|
1200
|
+
* @param runExclusive - Runs the question with nothing else holding the writer.
|
|
1201
|
+
* @returns The database identifier and the path of the file the copies come from.
|
|
1202
|
+
* @throws A `BACKUP_ERROR` where the caller named no source file and SQLite has none open.
|
|
1203
|
+
*/
|
|
1204
|
+
async namesFor(conn, run, runExclusive) {
|
|
1205
|
+
if (run.databaseId !== void 0 && run.sourcePath !== void 0) {
|
|
1206
|
+
return { databaseId: run.databaseId, sourcePath: run.sourcePath };
|
|
1207
|
+
}
|
|
1208
|
+
let sourcePath = run.sourcePath ?? "";
|
|
1209
|
+
if (sourcePath === "") {
|
|
1210
|
+
await runExclusive(async () => {
|
|
1211
|
+
sourcePath = await readMainDatabasePath(conn);
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
if (sourcePath === "") {
|
|
1215
|
+
throw new BackupError(
|
|
1216
|
+
"This schedule names no source file, and SQLite reports no file open on the connection it copies, so no report could name one. Give the schedule a sourcePath."
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
return { databaseId: run.databaseId ?? basename(sourcePath, extname(sourcePath)), sourcePath };
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Passes one finished copy to the caller's completion callback, and stops
|
|
1223
|
+
* waiting on that callback once its deadline passes.
|
|
1224
|
+
*
|
|
1225
|
+
* @param report - What the copy produced.
|
|
1226
|
+
* @param run - The schedule, which holds the callback and its deadline.
|
|
1227
|
+
*/
|
|
1228
|
+
async handToCaller(report, run) {
|
|
1229
|
+
const { onBackup, onBackupTimeoutMs } = run;
|
|
1230
|
+
if (!onBackup) return;
|
|
1231
|
+
const handed = Promise.resolve(onBackup(report));
|
|
1232
|
+
if (onBackupTimeoutMs === 0) {
|
|
1233
|
+
await handed;
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
await withinDeadline(
|
|
1237
|
+
handed,
|
|
1238
|
+
onBackupTimeoutMs,
|
|
1239
|
+
() => new SirannonError(
|
|
1240
|
+
`The onBackup callback for database '${report.databaseId}' did not return within ${onBackupTimeoutMs}ms, so the schedule went on without it. The copy at '${report.destPath}' is on disk and whatever that callback was doing with it has not finished.`,
|
|
1241
|
+
"BACKUP_ERROR"
|
|
1242
|
+
)
|
|
1243
|
+
);
|
|
1098
1244
|
}
|
|
1099
|
-
run(conn,
|
|
1245
|
+
run(conn, run) {
|
|
1246
|
+
const { cron, resolvedDir, maxFiles, timezone, onError } = run;
|
|
1247
|
+
const runExclusive = run.runExclusive ?? runDirect;
|
|
1100
1248
|
const tickMs = cron.hasSeconds ? SECOND_RESOLUTION_MS : MINUTE_RESOLUTION_MS;
|
|
1101
1249
|
let timer = null;
|
|
1102
1250
|
let stopped = false;
|
|
1103
1251
|
let running = false;
|
|
1104
1252
|
let tickState = INITIAL_TICK_STATE;
|
|
1253
|
+
const announce = (err) => {
|
|
1254
|
+
if (!onError) return;
|
|
1255
|
+
try {
|
|
1256
|
+
onError(toError(err));
|
|
1257
|
+
} catch {
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
let source = null;
|
|
1105
1261
|
const runBackup = async () => {
|
|
1262
|
+
let copy = null;
|
|
1106
1263
|
try {
|
|
1264
|
+
const names = source ?? await this.namesFor(conn, run, runExclusive);
|
|
1265
|
+
source = names;
|
|
1107
1266
|
const destPath = join(resolvedDir, this.manager.generateFilename());
|
|
1108
|
-
await startCopyWithoutHoldingWriter(
|
|
1267
|
+
copy = await startCopyWithoutHoldingWriter(
|
|
1109
1268
|
runExclusive,
|
|
1110
1269
|
(onFirstStep) => this.manager.backup(conn, destPath, onFirstStep)
|
|
1111
1270
|
);
|
|
1112
|
-
this.
|
|
1271
|
+
await this.handToCaller({ ...copy, ...names }, run);
|
|
1113
1272
|
} catch (err) {
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1273
|
+
announce(err);
|
|
1274
|
+
}
|
|
1275
|
+
if (copy) {
|
|
1276
|
+
try {
|
|
1277
|
+
this.manager.rotate(resolvedDir, maxFiles);
|
|
1278
|
+
} catch (err) {
|
|
1279
|
+
announce(err);
|
|
1119
1280
|
}
|
|
1120
|
-
} finally {
|
|
1121
|
-
running = false;
|
|
1122
1281
|
}
|
|
1282
|
+
running = false;
|
|
1123
1283
|
};
|
|
1124
1284
|
const scheduleNext = () => {
|
|
1125
1285
|
if (stopped) {
|