@elementspanel/epanel-plugin-backup 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,772 @@
1
+ /******/ (() => { // webpackBootstrap
2
+ /******/ "use strict";
3
+ /******/ var __webpack_modules__ = ({
4
+
5
+ /***/ "./plugins/backup/src/backend/index.ts"
6
+ (__unused_webpack_module, exports, __webpack_require__) {
7
+
8
+
9
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
+ };
12
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
13
+ exports.inject = void 0;
14
+ exports.apply = apply;
15
+ const archiver_1 = __importDefault(__webpack_require__("archiver"));
16
+ const child_process_1 = __webpack_require__("child_process");
17
+ const fs_extra_1 = __importDefault(__webpack_require__("fs-extra"));
18
+ const os_1 = __importDefault(__webpack_require__("os"));
19
+ const path_1 = __importDefault(__webpack_require__("path"));
20
+ const promises_1 = __webpack_require__("stream/promises");
21
+ const uuid_1 = __webpack_require__("uuid");
22
+ const i18n_1 = __webpack_require__("./plugins/backup/src/i18n/index.ts");
23
+ exports.inject = [
24
+ "i18n",
25
+ "protocol",
26
+ "instances",
27
+ "tasks",
28
+ "schedules",
29
+ "features",
30
+ "archive",
31
+ "settings",
32
+ "settingsForm"
33
+ ];
34
+ function apply(ctx) {
35
+ ctx.i18n.define(i18n_1.localeMessages);
36
+ const BACKUP_FORMATS = ["zip", "tar.gz", "7z"];
37
+ // Described, not drawn. The fields a backup actually reads live in the
38
+ // daemon configuration, and the panel's plugin manager renders this
39
+ // declaration; there is no browser half of a daemon plugin to put a form in.
40
+ ctx.settingsForm.declare({
41
+ fields: () => [
42
+ {
43
+ key: "instanceBackupPath",
44
+ type: "string",
45
+ title: ctx.i18n.$t("TXT_CODE_DBACKUP_PATH"),
46
+ description: ctx.i18n.$t("TXT_CODE_DBACKUP_PATH_TIP")
47
+ },
48
+ {
49
+ key: "instanceBackupFormat",
50
+ type: "select",
51
+ title: ctx.i18n.$t("TXT_CODE_DBACKUP_FORMAT"),
52
+ options: BACKUP_FORMATS.map((format) => ({ value: format, label: format }))
53
+ },
54
+ {
55
+ key: "instanceBackupCompressionLevel",
56
+ type: "number",
57
+ title: ctx.i18n.$t("TXT_CODE_DBACKUP_LEVEL"),
58
+ description: ctx.i18n.$t("TXT_CODE_DBACKUP_LEVEL_TIP"),
59
+ min: 0,
60
+ max: 9
61
+ },
62
+ {
63
+ key: "instanceBackupMaxSize",
64
+ type: "number",
65
+ title: ctx.i18n.$t("TXT_CODE_DBACKUP_MAX_SIZE"),
66
+ description: ctx.i18n.$t("TXT_CODE_DBACKUP_MAX_SIZE_TIP"),
67
+ min: 0
68
+ }
69
+ ],
70
+ read: () => ({
71
+ instanceBackupPath: ctx.settings.config.instanceBackupPath,
72
+ instanceBackupFormat: ctx.settings.config.instanceBackupFormat,
73
+ instanceBackupCompressionLevel: ctx.settings.config.instanceBackupCompressionLevel,
74
+ instanceBackupMaxSize: ctx.settings.config.instanceBackupMaxSize
75
+ }),
76
+ write: (values) => {
77
+ const config = ctx.settings.config;
78
+ if (values.instanceBackupPath != null) {
79
+ config.instanceBackupPath = String(values.instanceBackupPath);
80
+ }
81
+ const format = values.instanceBackupFormat;
82
+ if (typeof format === "string" && BACKUP_FORMATS.includes(format)) {
83
+ config.instanceBackupFormat = format;
84
+ }
85
+ const level = Number(values.instanceBackupCompressionLevel);
86
+ if (Number.isInteger(level) && level >= 0 && level <= 9) {
87
+ config.instanceBackupCompressionLevel = level;
88
+ }
89
+ const maxSize = Number(values.instanceBackupMaxSize);
90
+ if (Number.isFinite(maxSize) && maxSize >= 0) {
91
+ config.instanceBackupMaxSize = maxSize;
92
+ }
93
+ ctx.settings.save();
94
+ }
95
+ });
96
+ const { AsyncTask, Center: TaskCenter } = ctx.tasks;
97
+ const { GitignoreMatcher, decompressWithProgress, check7zipStatus, sevenZipPath, zipTimeoutSeconds } = ctx.archive;
98
+ const Instance = ctx.instances.Instance;
99
+ const t = ctx.i18n.$t;
100
+ const protocol = ctx.protocol;
101
+ const instances = ctx.instances.subsystem;
102
+ const logger = ctx.logger;
103
+ const BACKUP_EXTENSIONS = [".zip", ".tar.gz", ".7z"];
104
+ const GB_IN_BYTES = 1024 * 1024 * 1024;
105
+ const getBackupDirPath = (instanceUuid) => path_1.default.join(path_1.default.normalize(ctx.settings.config.instanceBackupPath || path_1.default.join(process.cwd(), "data/backups")), instanceUuid);
106
+ // Returns the instance's backup archives, newest first. `time` is in milliseconds.
107
+ const listBackupFiles = async (instanceUuid) => {
108
+ const instanceBackupDir = getBackupDirPath(instanceUuid);
109
+ if (!(await fs_extra_1.default.pathExists(instanceBackupDir)))
110
+ return [];
111
+ const backups = [];
112
+ for (const file of await fs_extra_1.default.readdir(instanceBackupDir)) {
113
+ const lowerFileName = file.toLowerCase();
114
+ if (!BACKUP_EXTENSIONS.some((extension) => lowerFileName.endsWith(extension)))
115
+ continue;
116
+ const stat = await fs_extra_1.default.stat(path_1.default.join(instanceBackupDir, file)).catch(() => null);
117
+ if (!stat?.isFile())
118
+ continue;
119
+ backups.push({ name: file, size: stat.size, time: stat.birthtimeMs || stat.ctimeMs });
120
+ }
121
+ return backups.sort((a, b) => b.time - a.time);
122
+ };
123
+ // One instance may store at most `instanceBackupMaxSize` GB of backup archives
124
+ // (0 = unlimited). A new backup is only allowed while the space left is at
125
+ // least as large as the average size of the archives already stored, so the
126
+ // archive about to be written stays within the budget.
127
+ const getBackupBudget = (backups) => {
128
+ const limitGb = Number(ctx.settings.config.instanceBackupMaxSize);
129
+ const unlimited = !Number.isFinite(limitGb) || limitGb <= 0;
130
+ const usedBytes = backups.reduce((total, backup) => total + backup.size, 0);
131
+ const averageBytes = backups.length > 0 ? usedBytes / backups.length : 0;
132
+ return {
133
+ unlimited,
134
+ limitGb,
135
+ usedBytes,
136
+ averageBytes,
137
+ exceeded: !unlimited && limitGb * GB_IN_BYTES - usedBytes < averageBytes
138
+ };
139
+ };
140
+ const toGigabytes = (bytes) => (bytes / GB_IN_BYTES).toFixed(2);
141
+ class InstanceBackupTask extends AsyncTask {
142
+ instance;
143
+ static TYPE = "InstanceBackupTask";
144
+ backupPath = "";
145
+ backupFileName = "";
146
+ ownsBusyState = false;
147
+ constructor(instance) {
148
+ super();
149
+ this.instance = instance;
150
+ this.taskId = `${InstanceBackupTask.TYPE}-${instance.instanceUuid}-${(0, uuid_1.v4)()}`;
151
+ this.type = InstanceBackupTask.TYPE;
152
+ }
153
+ async onStart() {
154
+ this.ownsBusyState = false;
155
+ try {
156
+ if (this.instance.status() === Instance.STATUS_BUSY) {
157
+ throw new Error(t("TXT_CODE_instanceConf.instanceBusy"));
158
+ }
159
+ const budget = getBackupBudget(await listBackupFiles(this.instance.instanceUuid));
160
+ if (budget.exceeded) {
161
+ throw new Error(t("TXT_CODE_INSTANCE_BACKUP_QUOTA_EXCEEDED", {
162
+ limit: String(budget.limitGb),
163
+ used: toGigabytes(budget.usedBytes),
164
+ average: toGigabytes(budget.averageBytes)
165
+ }));
166
+ }
167
+ this.instance.println("INFO", t("TXT_CODE_INSTANCE_BACKUP_START"));
168
+ const configuredPath = ctx.settings.config.instanceBackupPath;
169
+ this.backupPath = path_1.default.normalize(configuredPath || path_1.default.join(process.cwd(), "data/backups"));
170
+ await fs_extra_1.default.ensureDir(this.backupPath);
171
+ if (this.instance.status() !== Instance.STATUS_STOP) {
172
+ this.instance.println("INFO", t("TXT_CODE_INSTANCE_BACKUP_STOPPING"));
173
+ await this.instance.execPreset("stop");
174
+ let retry = 0;
175
+ while (this.instance.status() !== Instance.STATUS_STOP && retry < 60) {
176
+ await new Promise((resolve) => setTimeout(resolve, 1000));
177
+ retry++;
178
+ }
179
+ if (this.instance.status() !== Instance.STATUS_STOP) {
180
+ throw new Error(t("TXT_CODE_INSTANCE_BACKUP_STOP_TIMEOUT"));
181
+ }
182
+ }
183
+ this.instance.status(Instance.STATUS_BUSY);
184
+ this.ownsBusyState = true;
185
+ this.instance.println("INFO", t("TXT_CODE_INSTANCE_BACKUP_COMPRESSING"));
186
+ const now = new Date();
187
+ const dateStr = now.getFullYear() +
188
+ "-" +
189
+ String(now.getMonth() + 1).padStart(2, "0") +
190
+ "-" +
191
+ String(now.getDate()).padStart(2, "0") +
192
+ "_" +
193
+ String(now.getHours()).padStart(2, "0") +
194
+ "-" +
195
+ String(now.getMinutes()).padStart(2, "0") +
196
+ "-" +
197
+ String(now.getSeconds()).padStart(2, "0");
198
+ const backupId = (0, uuid_1.v4)().split("-")[0];
199
+ const configuredFormat = ctx.settings.config.instanceBackupFormat;
200
+ const backupFormat = configuredFormat === "tar.gz" || configuredFormat === "7z" ? configuredFormat : "zip";
201
+ const configuredLevel = ctx.settings.config.instanceBackupCompressionLevel;
202
+ const compressionLevel = Number.isInteger(configuredLevel)
203
+ ? Math.min(9, Math.max(0, configuredLevel))
204
+ : 9;
205
+ this.backupFileName = `${backupId}-${dateStr}.${backupFormat}`;
206
+ const instanceBackupDir = path_1.default.join(this.backupPath, this.instance.instanceUuid);
207
+ await fs_extra_1.default.ensureDir(instanceBackupDir);
208
+ const targetArchivePath = path_1.default.join(instanceBackupDir, this.backupFileName);
209
+ const instanceCwd = this.instance.absoluteCwdPath();
210
+ const epbaklstPath = path_1.default.join(instanceCwd, ".epbaklst");
211
+ let gitignoreMatcher = null;
212
+ let blacklistedCount = 0;
213
+ if (await fs_extra_1.default.pathExists(epbaklstPath)) {
214
+ const content = await fs_extra_1.default.readFile(epbaklstPath, "utf-8");
215
+ const firstLine = content.split(/\r?\n/)[0]?.trim() || "";
216
+ const whitelistMode = firstLine.toLowerCase() === "$white";
217
+ gitignoreMatcher = new GitignoreMatcher(content, instanceCwd, whitelistMode);
218
+ }
219
+ const whitelistMode = gitignoreMatcher ? gitignoreMatcher.isWhitelistMode() : false;
220
+ const allFiles = [];
221
+ let totalSize = 0;
222
+ const walkDir = async (dir, relativePath = "", whitelistedParent = false) => {
223
+ const entries = await fs_extra_1.default.readdir(dir, { withFileTypes: true });
224
+ for (const entry of entries) {
225
+ const fullPath = path_1.default.join(dir, entry.name);
226
+ // A backup directory inside the instance must not back up itself.
227
+ if (path_1.default.resolve(fullPath) === path_1.default.resolve(instanceBackupDir))
228
+ continue;
229
+ const relPath = path_1.default.join(relativePath, entry.name);
230
+ if (entry.isDirectory()) {
231
+ let dirWhitelisted = whitelistedParent;
232
+ if (!dirWhitelisted && gitignoreMatcher) {
233
+ const dirIgnored = gitignoreMatcher.isIgnored(relPath, true);
234
+ if (whitelistMode) {
235
+ dirWhitelisted = !dirIgnored;
236
+ }
237
+ else if (dirIgnored) {
238
+ blacklistedCount++;
239
+ continue;
240
+ }
241
+ }
242
+ await walkDir(fullPath, relPath, dirWhitelisted);
243
+ }
244
+ else {
245
+ if (!whitelistedParent &&
246
+ gitignoreMatcher &&
247
+ gitignoreMatcher.isIgnored(relPath, false)) {
248
+ blacklistedCount++;
249
+ continue;
250
+ }
251
+ const stat = await fs_extra_1.default.stat(fullPath);
252
+ allFiles.push({ filePath: relPath, stat });
253
+ totalSize += stat.size;
254
+ }
255
+ }
256
+ };
257
+ await walkDir(instanceCwd);
258
+ if (gitignoreMatcher && blacklistedCount > 0) {
259
+ this.instance.println("INFO", t("TXT_CODE_INSTANCE_BACKUP_EXCLUDED", { num: String(blacklistedCount) }));
260
+ }
261
+ const progressPrefix = `\x1b[K\r`;
262
+ let lastPercent = -1;
263
+ const printProgress = (percent) => {
264
+ const normalizedPercent = Math.min(100, Math.max(0, Math.floor(percent)));
265
+ if (normalizedPercent <= lastPercent)
266
+ return;
267
+ lastPercent = normalizedPercent;
268
+ const barLength = 30;
269
+ const filled = Math.floor((normalizedPercent / 100) * barLength);
270
+ const empty = barLength - filled;
271
+ const bar = "[" + "#".repeat(filled) + " ".repeat(empty) + "]";
272
+ this.instance.print(`${progressPrefix}${bar} ${normalizedPercent}%`);
273
+ };
274
+ if (backupFormat === "7z") {
275
+ if (!(await check7zipStatus()))
276
+ throw new Error(t("TXT_CODE_a0ede210"));
277
+ const absoluteTargetArchivePath = path_1.default.resolve(targetArchivePath);
278
+ const listFilePath = path_1.default.join(os_1.default.tmpdir(), `elements-panel-instance-backup-${backupId}.lst`);
279
+ try {
280
+ await fs_extra_1.default.writeFile(listFilePath, allFiles.map((file) => file.filePath.replace(/\\/g, "/")).join("\n"), "utf8");
281
+ const result = await new Promise((resolve, reject) => {
282
+ const child = (0, child_process_1.spawn)(sevenZipPath, [
283
+ "a",
284
+ absoluteTargetArchivePath,
285
+ "-t7z",
286
+ `-mx=${compressionLevel}`,
287
+ "-scsUTF-8",
288
+ "-spd",
289
+ "-bsp1",
290
+ `@${listFilePath}`
291
+ ], { cwd: instanceCwd, windowsHide: true });
292
+ let stdout = "";
293
+ let stderr = "";
294
+ let progressOutput = "";
295
+ let settled = false;
296
+ const timeout = setTimeout(() => {
297
+ child.kill();
298
+ if (!settled) {
299
+ settled = true;
300
+ reject(new Error(t("TXT_CODE_1d1ec400")));
301
+ }
302
+ }, zipTimeoutSeconds * 1000);
303
+ const consume = (chunk, isStderr) => {
304
+ const text = chunk.toString();
305
+ if (isStderr) {
306
+ stderr += text;
307
+ }
308
+ else {
309
+ stdout += text;
310
+ progressOutput = `${progressOutput}${text}`.slice(-128);
311
+ const matches = progressOutput.match(/(?:^|\D)(\d{1,3})%/g) || [];
312
+ for (const match of matches) {
313
+ const percent = Number(match.match(/\d+/)?.[0]);
314
+ if (Number.isFinite(percent))
315
+ printProgress(percent);
316
+ }
317
+ const lastPercentIndex = progressOutput.lastIndexOf("%");
318
+ if (lastPercentIndex >= 0)
319
+ progressOutput = progressOutput.slice(lastPercentIndex + 1);
320
+ }
321
+ };
322
+ printProgress(0);
323
+ child.stdout.on("data", (chunk) => consume(chunk, false));
324
+ child.stderr.on("data", (chunk) => consume(chunk, true));
325
+ child.on("error", (error) => {
326
+ clearTimeout(timeout);
327
+ if (!settled) {
328
+ settled = true;
329
+ reject(error);
330
+ }
331
+ });
332
+ child.on("close", (code) => {
333
+ clearTimeout(timeout);
334
+ if (settled)
335
+ return;
336
+ settled = true;
337
+ if (code === 0)
338
+ resolve({ stdout, stderr });
339
+ else
340
+ reject(new Error(`${stdout}\n${stderr}`.trim() || `7-Zip exited with code ${code}`));
341
+ });
342
+ });
343
+ const output = `${result.stdout}\n${result.stderr}`;
344
+ const archiveStat = await fs_extra_1.default.stat(absoluteTargetArchivePath).catch(() => null);
345
+ if (!archiveStat?.isFile() || archiveStat.size === 0) {
346
+ throw new Error(output.trim() || t("TXT_CODE_11ecd5a9"));
347
+ }
348
+ printProgress(100);
349
+ }
350
+ catch (error) {
351
+ await fs_extra_1.default.remove(absoluteTargetArchivePath).catch(() => undefined);
352
+ throw error;
353
+ }
354
+ finally {
355
+ await fs_extra_1.default.remove(listFilePath).catch(() => undefined);
356
+ }
357
+ }
358
+ else {
359
+ const output = fs_extra_1.default.createWriteStream(targetArchivePath);
360
+ const archive = backupFormat === "tar.gz"
361
+ ? (0, archiver_1.default)("tar", { gzip: true, gzipOptions: { level: compressionLevel } })
362
+ : (0, archiver_1.default)("zip", { zlib: { level: compressionLevel } });
363
+ for (const file of allFiles) {
364
+ archive.file(path_1.default.join(instanceCwd, file.filePath), { name: file.filePath });
365
+ }
366
+ const progressInterval = setInterval(() => {
367
+ const processedBytes = archive.pointer();
368
+ printProgress(totalSize > 0 ? (processedBytes / totalSize) * 100 : 0);
369
+ }, 200);
370
+ try {
371
+ await Promise.all([(0, promises_1.pipeline)(archive, output), archive.finalize()]);
372
+ printProgress(100);
373
+ }
374
+ catch (error) {
375
+ archive.abort();
376
+ output.destroy();
377
+ await fs_extra_1.default.remove(targetArchivePath).catch(() => { });
378
+ throw error;
379
+ }
380
+ finally {
381
+ clearInterval(progressInterval);
382
+ }
383
+ }
384
+ this.instance.print("\n");
385
+ this.instance.println("INFO", t("TXT_CODE_INSTANCE_BACKUP_SUCCESS", { name: this.backupFileName }));
386
+ logger.info(`Instance backup success: ${this.instance.config.nickname} -> ${targetArchivePath}`);
387
+ await this.stop();
388
+ }
389
+ catch (error) {
390
+ this.instance.println("ERROR", t("TXT_CODE_INSTANCE_BACKUP_FAILED", { err: error.message }));
391
+ await this.error(error);
392
+ }
393
+ }
394
+ async onStop() {
395
+ // Release the busy state here rather than in `onStart`'s finally: `wait()`
396
+ // resolves from this stop, so a following scheduled action would otherwise
397
+ // still read STATUS_BUSY and skip itself.
398
+ if (this.ownsBusyState) {
399
+ this.ownsBusyState = false;
400
+ if (this.instance.status() === Instance.STATUS_BUSY) {
401
+ this.instance.status(Instance.STATUS_STOP);
402
+ }
403
+ }
404
+ this.instance.print("\n");
405
+ }
406
+ async onError(error) {
407
+ logger.error(`InstanceBackupTask Error: ${error.message}`);
408
+ }
409
+ toObject() {
410
+ return {
411
+ taskId: this.taskId,
412
+ status: this.status(),
413
+ instanceUuid: this.instance.instanceUuid,
414
+ backupFileName: this.backupFileName,
415
+ backupPath: this.backupPath
416
+ };
417
+ }
418
+ }
419
+ const createBackupTask = (instance) => new InstanceBackupTask(instance);
420
+ const getBackupPath = (instanceUuid, backupName) => {
421
+ if (typeof backupName !== "string" || !backupName || path_1.default.basename(backupName) !== backupName) {
422
+ throw new Error(t("TXT_CODE_Instance_router.accessFileErr"));
423
+ }
424
+ const backupRoot = path_1.default.resolve(ctx.settings.config.instanceBackupPath || path_1.default.join(process.cwd(), "data/backups"));
425
+ const instanceBackupDir = path_1.default.resolve(backupRoot, instanceUuid);
426
+ const archivePath = path_1.default.resolve(instanceBackupDir, backupName);
427
+ const relativePath = path_1.default.relative(instanceBackupDir, archivePath);
428
+ if (!relativePath ||
429
+ relativePath === ".." ||
430
+ relativePath.startsWith(".." + path_1.default.sep) ||
431
+ path_1.default.isAbsolute(relativePath)) {
432
+ throw new Error(t("TXT_CODE_Instance_router.accessFileErr"));
433
+ }
434
+ return archivePath;
435
+ };
436
+ ctx.tasks.register("instance_backup", {
437
+ type: InstanceBackupTask.TYPE,
438
+ create: createBackupTask
439
+ });
440
+ // A scheduled backup must not be blocked by a full budget, so the oldest
441
+ // archives are dropped until a new one fits. The task's own check then passes.
442
+ const freeBackupBudget = async (instance) => {
443
+ const instanceBackupDir = getBackupDirPath(instance.instanceUuid);
444
+ let backups = await listBackupFiles(instance.instanceUuid);
445
+ let budget = getBackupBudget(backups);
446
+ while (budget.exceeded && backups.length > 0) {
447
+ const oldest = backups[backups.length - 1];
448
+ await fs_extra_1.default.remove(path_1.default.join(instanceBackupDir, oldest.name));
449
+ instance.println("INFO", t("TXT_CODE_INSTANCE_BACKUP_AUTO_DELETE_OLDEST", { name: oldest.name }));
450
+ backups = await listBackupFiles(instance.instanceUuid);
451
+ budget = getBackupBudget(backups);
452
+ }
453
+ };
454
+ ctx.schedules.register("backup", async (instance) => {
455
+ const runningBackup = TaskCenter.getTasks(InstanceBackupTask.TYPE).find((task) => task.toObject().instanceUuid === instance.instanceUuid && task.status() === 1);
456
+ const backupTask = runningBackup || createBackupTask(instance);
457
+ if (!runningBackup) {
458
+ await freeBackupBudget(instance);
459
+ TaskCenter.addTask(backupTask);
460
+ }
461
+ await backupTask.wait();
462
+ });
463
+ ctx.protocol.on("instance/backup/list", async (routerCtx, data) => {
464
+ try {
465
+ const instanceUuid = data.instanceUuid;
466
+ if (!instances.getInstance(instanceUuid))
467
+ throw new Error(t("TXT_CODE_3bfb9e04"));
468
+ const backups = await listBackupFiles(instanceUuid);
469
+ protocol.response(routerCtx, backups.map((backup) => ({ ...backup, time: new Date(backup.time).toLocaleString() })));
470
+ }
471
+ catch (error) {
472
+ protocol.responseError(routerCtx, error);
473
+ }
474
+ });
475
+ ctx.protocol.on("instance/backup/delete", async (routerCtx, data) => {
476
+ try {
477
+ if (!instances.getInstance(data.instanceUuid))
478
+ throw new Error(t("TXT_CODE_3bfb9e04"));
479
+ const filePath = getBackupPath(data.instanceUuid, data.backupName);
480
+ if (fs_extra_1.default.existsSync(filePath))
481
+ await fs_extra_1.default.remove(filePath);
482
+ protocol.response(routerCtx, true);
483
+ }
484
+ catch (error) {
485
+ protocol.responseError(routerCtx, error);
486
+ }
487
+ });
488
+ ctx.protocol.on("instance/backup/restore", async (routerCtx, data) => {
489
+ try {
490
+ const instance = instances.getInstance(data.instanceUuid);
491
+ if (!instance)
492
+ throw new Error(t("TXT_CODE_3bfb9e04"));
493
+ if (instance.status() !== Instance.STATUS_STOP) {
494
+ if (instance.status() === Instance.STATUS_BUSY) {
495
+ throw new Error(t("TXT_CODE_instanceConf.instanceBusy"));
496
+ }
497
+ if (instance.status() === Instance.STATUS_RUNNING ||
498
+ instance.status() === Instance.STATUS_STARTING) {
499
+ instance.println("INFO", t("TXT_CODE_INSTANCE_BACKUP_STOPPING"));
500
+ await instance.execPreset("stop");
501
+ }
502
+ let stopSuccess = false;
503
+ for (let i = 0; i < 60; i++) {
504
+ if (instance.status() === Instance.STATUS_STOP) {
505
+ stopSuccess = true;
506
+ break;
507
+ }
508
+ await new Promise((resolve) => setTimeout(resolve, 1000));
509
+ }
510
+ if (!stopSuccess)
511
+ throw new Error(t("TXT_CODE_INSTANCE_BACKUP_STOP_TIMEOUT"));
512
+ }
513
+ const archivePath = getBackupPath(data.instanceUuid, data.backupName);
514
+ if (!fs_extra_1.default.existsSync(archivePath))
515
+ throw new Error(t("TXT_CODE_Instance_router.accessFileErr"));
516
+ instance.status(Instance.STATUS_BUSY);
517
+ instance.println("INFO", t("TXT_CODE_INSTANCE_BACKUP_RESTORING"));
518
+ const tasks = ctx.get("backgroundTasks");
519
+ let current = 0;
520
+ tasks?.progress(() => ({ current, total: 100 }));
521
+ const pending = (async () => {
522
+ try {
523
+ const destDir = instance.absoluteCwdPath();
524
+ const progressPrefix = `\x1b[K\r`;
525
+ let lastPercent = -1;
526
+ await decompressWithProgress(archivePath, destDir, (percent) => {
527
+ current = percent;
528
+ if (percent === lastPercent)
529
+ return;
530
+ lastPercent = percent;
531
+ const barLength = 30;
532
+ const filled = Math.floor((percent / 100) * barLength);
533
+ const bar = "[" + "#".repeat(filled) + " ".repeat(barLength - filled) + "]";
534
+ instance.print(`${progressPrefix}${bar} ${percent}%`);
535
+ }, instance.config.fileCode);
536
+ instance.print("\n");
537
+ instance.println("INFO", t("TXT_CODE_INSTANCE_BACKUP_RESTORE_SUCCESS"));
538
+ }
539
+ catch (error) {
540
+ tasks?.fail(error);
541
+ instance.print("\n");
542
+ logger.error(t("TXT_CODE_INSTANCE_BACKUP_RESTORE_FAILED", { err: error.message }));
543
+ instance.println("ERROR", t("TXT_CODE_INSTANCE_BACKUP_RESTORE_FAILED", { err: error.message }));
544
+ }
545
+ finally {
546
+ instance.status(Instance.STATUS_STOP);
547
+ }
548
+ })();
549
+ tasks?.follow(pending);
550
+ protocol.response(routerCtx, true);
551
+ }
552
+ catch (error) {
553
+ protocol.responseError(routerCtx, error);
554
+ }
555
+ });
556
+ ctx.features.add("instanceBackup");
557
+ }
558
+
559
+
560
+ /***/ },
561
+
562
+ /***/ "./plugins/backup/src/i18n/index.ts"
563
+ (__unused_webpack_module, exports, __webpack_require__) {
564
+
565
+
566
+ var __importDefault = (this && this.__importDefault) || function (mod) {
567
+ return (mod && mod.__esModule) ? mod : { "default": mod };
568
+ };
569
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
570
+ exports.localeMessages = void 0;
571
+ const de_DE_json_1 = __importDefault(__webpack_require__("./plugins/backup/src/i18n/de_DE.json"));
572
+ const en_US_json_1 = __importDefault(__webpack_require__("./plugins/backup/src/i18n/en_US.json"));
573
+ const es_ES_json_1 = __importDefault(__webpack_require__("./plugins/backup/src/i18n/es_ES.json"));
574
+ const fr_FR_json_1 = __importDefault(__webpack_require__("./plugins/backup/src/i18n/fr_FR.json"));
575
+ const ja_JP_json_1 = __importDefault(__webpack_require__("./plugins/backup/src/i18n/ja_JP.json"));
576
+ const ko_KR_json_1 = __importDefault(__webpack_require__("./plugins/backup/src/i18n/ko_KR.json"));
577
+ const pt_BR_json_1 = __importDefault(__webpack_require__("./plugins/backup/src/i18n/pt_BR.json"));
578
+ const ru_RU_json_1 = __importDefault(__webpack_require__("./plugins/backup/src/i18n/ru_RU.json"));
579
+ const th_TH_json_1 = __importDefault(__webpack_require__("./plugins/backup/src/i18n/th_TH.json"));
580
+ const tr_TR_json_1 = __importDefault(__webpack_require__("./plugins/backup/src/i18n/tr_TR.json"));
581
+ const zh_CN_json_1 = __importDefault(__webpack_require__("./plugins/backup/src/i18n/zh_CN.json"));
582
+ const zh_TW_json_1 = __importDefault(__webpack_require__("./plugins/backup/src/i18n/zh_TW.json"));
583
+ // The lines a backup prints into the instance console, plus the errors its
584
+ // protocol handlers throw. Registered with the daemon's i18next instance at
585
+ // setup, so the daemon catalogue carries nothing backup-specific.
586
+ exports.localeMessages = {
587
+ de_de: de_DE_json_1.default,
588
+ en_us: en_US_json_1.default,
589
+ es_es: es_ES_json_1.default,
590
+ fr_fr: fr_FR_json_1.default,
591
+ ja_jp: ja_JP_json_1.default,
592
+ ko_kr: ko_KR_json_1.default,
593
+ pt_br: pt_BR_json_1.default,
594
+ ru_ru: ru_RU_json_1.default,
595
+ th_th: th_TH_json_1.default,
596
+ tr_tr: tr_TR_json_1.default,
597
+ zh_cn: zh_CN_json_1.default,
598
+ zh_tw: zh_TW_json_1.default
599
+ };
600
+
601
+
602
+ /***/ },
603
+
604
+ /***/ "archiver"
605
+ (module) {
606
+
607
+ module.exports = require("archiver");
608
+
609
+ /***/ },
610
+
611
+ /***/ "fs-extra"
612
+ (module) {
613
+
614
+ module.exports = require("fs-extra");
615
+
616
+ /***/ },
617
+
618
+ /***/ "uuid"
619
+ (module) {
620
+
621
+ module.exports = require("uuid");
622
+
623
+ /***/ },
624
+
625
+ /***/ "child_process"
626
+ (module) {
627
+
628
+ module.exports = require("child_process");
629
+
630
+ /***/ },
631
+
632
+ /***/ "os"
633
+ (module) {
634
+
635
+ module.exports = require("os");
636
+
637
+ /***/ },
638
+
639
+ /***/ "path"
640
+ (module) {
641
+
642
+ module.exports = require("path");
643
+
644
+ /***/ },
645
+
646
+ /***/ "stream/promises"
647
+ (module) {
648
+
649
+ module.exports = require("stream/promises");
650
+
651
+ /***/ },
652
+
653
+ /***/ "./plugins/backup/src/i18n/de_DE.json"
654
+ (module) {
655
+
656
+ module.exports = /*#__PURE__*/JSON.parse('{"TXT_CODE_DBACKUP_FORMAT":"Archivformat","TXT_CODE_DBACKUP_LEVEL":"Kompressionsstufe","TXT_CODE_DBACKUP_LEVEL_TIP":"0 speichert ohne Kompression, 9 komprimiert am stärksten. Wird von tar.gz- und 7z-Voreinstellungen mit eigener Stufe ignoriert.","TXT_CODE_DBACKUP_MAX_SIZE":"Maximale Backup-Größe pro Instanz (GB)","TXT_CODE_DBACKUP_MAX_SIZE_TIP":"Gesamtspeicherplatz, den die Backups einer Instanz belegen dürfen. 0 bedeutet unbegrenzt. Reicht der verbleibende Platz nicht für ein durchschnittliches Backup, werden manuelle Backups abgelehnt und geplante Backups löschen das älteste Archiv.","TXT_CODE_DBACKUP_PATH":"Backup-Verzeichnis","TXT_CODE_DBACKUP_PATH_TIP":"Wohin Instanz-Backups geschrieben werden. Relative Pfade gelten ab dem Daemon-Verzeichnis; leer bedeutet data/backups.","TXT_CODE_INSTANCE_BACKUP_AUTO_DELETE_OLDEST":"Backup-Speicherplatz erschöpft, das älteste Backup wurde automatisch gelöscht: {{name}}","TXT_CODE_INSTANCE_BACKUP_COMPRESSING":"Instanzdateien werden komprimiert, bitte warten...","TXT_CODE_INSTANCE_BACKUP_EXCLUDED":"{{num}} Dateien/Verzeichnisse durch .epbaklst-Regeln ausgeschlossen","TXT_CODE_INSTANCE_BACKUP_FAILED":"Instanzsicherung fehlgeschlagen: {{err}}","TXT_CODE_INSTANCE_BACKUP_QUOTA_EXCEEDED":"Nicht genügend Backup-Speicherplatz: Das Limit dieser Instanz beträgt {{limit}}GB, {{used}}GB sind belegt und ein Backup ist durchschnittlich {{average}}GB groß. Löschen Sie alte Backups und versuchen Sie es erneut.","TXT_CODE_INSTANCE_BACKUP_RESTORE_FAILED":"Wiederherstellung der Sicherung fehlgeschlagen: {{err}}","TXT_CODE_INSTANCE_BACKUP_RESTORE_SUCCESS":"Sicherung erfolgreich wiederhergestellt!","TXT_CODE_INSTANCE_BACKUP_RESTORING":"Sicherung wird wiederhergestellt...","TXT_CODE_INSTANCE_BACKUP_START":"Instanzsicherung wird gestartet...","TXT_CODE_INSTANCE_BACKUP_STOPPING":"Instanz wird angehalten...","TXT_CODE_INSTANCE_BACKUP_STOP_TIMEOUT":"Zeitüberschreitung beim Anhalten der Instanz, Sicherung abgebrochen.","TXT_CODE_INSTANCE_BACKUP_SUCCESS":"Instanzsicherung erfolgreich! Datei: {{name}}","TXT_CODE_instanceConf.instanceBusy":"Die Instanz befindet sich derzeit in einem ausgelasteten Zustand und kann keine Aktionen ausführen."}');
657
+
658
+ /***/ },
659
+
660
+ /***/ "./plugins/backup/src/i18n/en_US.json"
661
+ (module) {
662
+
663
+ module.exports = /*#__PURE__*/JSON.parse('{"TXT_CODE_DBACKUP_FORMAT":"Archive Format","TXT_CODE_DBACKUP_LEVEL":"Compression Level","TXT_CODE_DBACKUP_LEVEL_TIP":"0 stores without compressing, 9 compresses hardest. Ignored by tar.gz and 7z presets that set their own.","TXT_CODE_DBACKUP_MAX_SIZE":"Max Backup Size per Instance (GB)","TXT_CODE_DBACKUP_MAX_SIZE_TIP":"Total space one instance\'s backups may occupy. 0 means unlimited. When the space left is smaller than an average backup, manual backups are refused and scheduled backups delete the oldest archive.","TXT_CODE_DBACKUP_PATH":"Backup Directory","TXT_CODE_DBACKUP_PATH_TIP":"Where instance backups are written. Relative paths resolve against the daemon directory; empty means data/backups.","TXT_CODE_INSTANCE_BACKUP_AUTO_DELETE_OLDEST":"Backup space is full, the oldest backup was deleted automatically: {{name}}","TXT_CODE_INSTANCE_BACKUP_COMPRESSING":"Compressing instance files, please wait...","TXT_CODE_INSTANCE_BACKUP_EXCLUDED":"{{num}} files/directories excluded by .epbaklst rules","TXT_CODE_INSTANCE_BACKUP_FAILED":"Instance backup failed: {{err}}","TXT_CODE_INSTANCE_BACKUP_QUOTA_EXCEEDED":"Not enough backup space: the limit for this instance is {{limit}}GB, {{used}}GB is used and a backup averages {{average}}GB. Delete old backups before trying again.","TXT_CODE_INSTANCE_BACKUP_RESTORE_FAILED":"Backup restoration failed: {{err}}","TXT_CODE_INSTANCE_BACKUP_RESTORE_SUCCESS":"Backup restored successfully!","TXT_CODE_INSTANCE_BACKUP_RESTORING":"Restoring backup...","TXT_CODE_INSTANCE_BACKUP_START":"Starting instance backup...","TXT_CODE_INSTANCE_BACKUP_STOPPING":"Stopping instance...","TXT_CODE_INSTANCE_BACKUP_STOP_TIMEOUT":"Stop instance timeout, backup cancelled.","TXT_CODE_INSTANCE_BACKUP_SUCCESS":"Instance backup successful! File: {{name}}","TXT_CODE_instanceConf.instanceBusy":"The current instance is busy and cannot perform any operations."}');
664
+
665
+ /***/ },
666
+
667
+ /***/ "./plugins/backup/src/i18n/es_ES.json"
668
+ (module) {
669
+
670
+ module.exports = /*#__PURE__*/JSON.parse('{"TXT_CODE_DBACKUP_FORMAT":"Formato de archivo","TXT_CODE_DBACKUP_LEVEL":"Nivel de compresión","TXT_CODE_DBACKUP_LEVEL_TIP":"0 almacena sin comprimir, 9 comprime al máximo. Se ignora en tar.gz y 7z cuando fijan el suyo.","TXT_CODE_DBACKUP_MAX_SIZE":"Tamaño máximo de copias por instancia (GB)","TXT_CODE_DBACKUP_MAX_SIZE_TIP":"Espacio total que pueden ocupar las copias de una instancia. 0 significa sin límite. Si el espacio restante es menor que el tamaño medio de una copia, las copias manuales se rechazan y las copias programadas eliminan el archivo más antiguo.","TXT_CODE_DBACKUP_PATH":"Directorio de copias","TXT_CODE_DBACKUP_PATH_TIP":"Dónde se escriben las copias de seguridad. Las rutas relativas se resuelven desde el directorio del demonio; vacío significa data/backups.","TXT_CODE_INSTANCE_BACKUP_AUTO_DELETE_OLDEST":"Espacio de copias agotado, se eliminó automáticamente la copia más antigua: {{name}}","TXT_CODE_INSTANCE_BACKUP_COMPRESSING":"Comprimiendo los archivos de la instancia, espera...","TXT_CODE_INSTANCE_BACKUP_EXCLUDED":"{{num}} archivos/directorios excluidos por las reglas de .epbaklst","TXT_CODE_INSTANCE_BACKUP_FAILED":"Error al crear la copia de la instancia: {{err}}","TXT_CODE_INSTANCE_BACKUP_QUOTA_EXCEEDED":"Espacio de copias insuficiente: el límite de esta instancia es {{limit}}GB, se usan {{used}}GB y una copia ocupa {{average}}GB de media. Elimine copias antiguas antes de reintentar.","TXT_CODE_INSTANCE_BACKUP_RESTORE_FAILED":"Error al restaurar la copia: {{err}}","TXT_CODE_INSTANCE_BACKUP_RESTORE_SUCCESS":"¡Copia restaurada correctamente!","TXT_CODE_INSTANCE_BACKUP_RESTORING":"Restaurando copia...","TXT_CODE_INSTANCE_BACKUP_START":"Iniciando la copia de la instancia...","TXT_CODE_INSTANCE_BACKUP_STOPPING":"Deteniendo la instancia...","TXT_CODE_INSTANCE_BACKUP_STOP_TIMEOUT":"Se agotó el tiempo al detener la instancia; se canceló la copia.","TXT_CODE_INSTANCE_BACKUP_SUCCESS":"¡Copia de la instancia creada! Archivo: {{name}}","TXT_CODE_instanceConf.instanceBusy":"La instancia se encuentra actualmente en un estado ocupado y no puede realizar ninguna acción."}');
671
+
672
+ /***/ },
673
+
674
+ /***/ "./plugins/backup/src/i18n/fr_FR.json"
675
+ (module) {
676
+
677
+ module.exports = /*#__PURE__*/JSON.parse('{"TXT_CODE_DBACKUP_FORMAT":"Format d archive","TXT_CODE_DBACKUP_LEVEL":"Niveau de compression","TXT_CODE_DBACKUP_LEVEL_TIP":"0 stocke sans compresser, 9 compresse au maximum. Ignoré par tar.gz et 7z qui fixent le leur.","TXT_CODE_DBACKUP_MAX_SIZE":"Taille maximale des sauvegardes par instance (GB)","TXT_CODE_DBACKUP_MAX_SIZE_TIP":"Espace total que les sauvegardes d’une instance peuvent occuper. 0 signifie illimité. Lorsque l’espace restant est inférieur à la taille moyenne d’une sauvegarde, les sauvegardes manuelles sont refusées et les sauvegardes planifiées suppriment l’archive la plus ancienne.","TXT_CODE_DBACKUP_PATH":"Répertoire de sauvegarde","TXT_CODE_DBACKUP_PATH_TIP":"Où les sauvegardes d instances sont écrites. Les chemins relatifs partent du répertoire du démon ; vide signifie data/backups.","TXT_CODE_INSTANCE_BACKUP_AUTO_DELETE_OLDEST":"Espace de sauvegarde insuffisant, la sauvegarde la plus ancienne a été supprimée automatiquement : {{name}}","TXT_CODE_INSTANCE_BACKUP_COMPRESSING":"Compression des fichiers de l’instance, veuillez patienter...","TXT_CODE_INSTANCE_BACKUP_EXCLUDED":"{{num}} fichiers/répertoires exclus par les règles .epbaklst","TXT_CODE_INSTANCE_BACKUP_FAILED":"Échec de la sauvegarde de l’instance : {{err}}","TXT_CODE_INSTANCE_BACKUP_QUOTA_EXCEEDED":"Espace de sauvegarde insuffisant : la limite de cette instance est de {{limit}}GB, {{used}}GB sont utilisés et une sauvegarde fait en moyenne {{average}}GB. Supprimez d’anciennes sauvegardes avant de réessayer.","TXT_CODE_INSTANCE_BACKUP_RESTORE_FAILED":"Échec de la restauration : {{err}}","TXT_CODE_INSTANCE_BACKUP_RESTORE_SUCCESS":"Sauvegarde restaurée avec succès !","TXT_CODE_INSTANCE_BACKUP_RESTORING":"Restauration de la sauvegarde...","TXT_CODE_INSTANCE_BACKUP_START":"Démarrage de la sauvegarde de l’instance...","TXT_CODE_INSTANCE_BACKUP_STOPPING":"Arrêt de l’instance...","TXT_CODE_INSTANCE_BACKUP_STOP_TIMEOUT":"Délai d’arrêt de l’instance dépassé, sauvegarde annulée.","TXT_CODE_INSTANCE_BACKUP_SUCCESS":"Sauvegarde de l’instance réussie ! Fichier : {{name}}","TXT_CODE_instanceConf.instanceBusy":"L\'instance est actuellement dans un état occupé et ne peut effectuer aucune action."}');
678
+
679
+ /***/ },
680
+
681
+ /***/ "./plugins/backup/src/i18n/ja_JP.json"
682
+ (module) {
683
+
684
+ module.exports = /*#__PURE__*/JSON.parse('{"TXT_CODE_DBACKUP_FORMAT":"アーカイブ形式","TXT_CODE_DBACKUP_LEVEL":"圧縮レベル","TXT_CODE_DBACKUP_LEVEL_TIP":"0 は無圧縮で格納、9 が最高圧縮です。独自のレベルを持つ tar.gz と 7z では無視されます。","TXT_CODE_DBACKUP_MAX_SIZE":"インスタンスごとの最大バックアップ容量 (GB)","TXT_CODE_DBACKUP_MAX_SIZE_TIP":"1 つのインスタンスのバックアップが占有できる合計容量です。0 は無制限を意味します。残り容量が平均バックアップサイズを下回る場合、手動バックアップは拒否され、スケジュールされたバックアップは最も古いアーカイブを削除します。","TXT_CODE_DBACKUP_PATH":"バックアップ先ディレクトリ","TXT_CODE_DBACKUP_PATH_TIP":"インスタンスのバックアップの保存先。相対パスはデーモンディレクトリ基準です。空欄の場合は data/backups になります。","TXT_CODE_INSTANCE_BACKUP_AUTO_DELETE_OLDEST":"バックアップ容量が不足しているため、最も古いバックアップを自動削除しました:{{name}}","TXT_CODE_INSTANCE_BACKUP_COMPRESSING":"インスタンスファイルを圧縮中です。お待ちください...","TXT_CODE_INSTANCE_BACKUP_EXCLUDED":".epbaklstルールにより{{num}}個のファイル/ディレクトリを除外しました","TXT_CODE_INSTANCE_BACKUP_FAILED":"インスタンスのバックアップに失敗しました:{{err}}","TXT_CODE_INSTANCE_BACKUP_QUOTA_EXCEEDED":"バックアップ容量が不足しています:このインスタンスの上限は {{limit}}GB、使用量は {{used}}GB、平均バックアップサイズは {{average}}GB です。古いバックアップを削除してから再試行してください。","TXT_CODE_INSTANCE_BACKUP_RESTORE_FAILED":"バックアップの復元に失敗しました:{{err}}","TXT_CODE_INSTANCE_BACKUP_RESTORE_SUCCESS":"バックアップを復元しました!","TXT_CODE_INSTANCE_BACKUP_RESTORING":"バックアップを復元中...","TXT_CODE_INSTANCE_BACKUP_START":"インスタンスのバックアップを開始中...","TXT_CODE_INSTANCE_BACKUP_STOPPING":"インスタンスを停止中...","TXT_CODE_INSTANCE_BACKUP_STOP_TIMEOUT":"インスタンスの停止がタイムアウトしたため、バックアップをキャンセルしました。","TXT_CODE_INSTANCE_BACKUP_SUCCESS":"インスタンスのバックアップに成功しました!ファイル:{{name}}","TXT_CODE_instanceConf.instanceBusy":"インスタンスは現在ビジー状態にあるため、アクションを実行できません。"}');
685
+
686
+ /***/ },
687
+
688
+ /***/ "./plugins/backup/src/i18n/ko_KR.json"
689
+ (module) {
690
+
691
+ module.exports = /*#__PURE__*/JSON.parse('{"TXT_CODE_DBACKUP_FORMAT":"압축 형식","TXT_CODE_DBACKUP_LEVEL":"압축 수준","TXT_CODE_DBACKUP_LEVEL_TIP":"0은 압축 없이 저장하고 9는 가장 강하게 압축합니다. 자체 수준을 쓰는 tar.gz와 7z에서는 무시됩니다.","TXT_CODE_DBACKUP_MAX_SIZE":"인스턴스별 최대 백업 용량 (GB)","TXT_CODE_DBACKUP_MAX_SIZE_TIP":"한 인스턴스의 백업이 차지할 수 있는 총 용량입니다. 0은 무제한입니다. 남은 용량이 평균 백업 크기보다 작으면 수동 백업은 거부되고, 예약 백업은 가장 오래된 백업을 삭제합니다.","TXT_CODE_DBACKUP_PATH":"백업 디렉터리","TXT_CODE_DBACKUP_PATH_TIP":"인스턴스 백업을 저장할 위치입니다. 상대 경로는 데몬 디렉터리를 기준으로 하며, 비워 두면 data/backups 입니다.","TXT_CODE_INSTANCE_BACKUP_AUTO_DELETE_OLDEST":"백업 공간이 부족하여 가장 오래된 백업을 자동으로 삭제했습니다: {{name}}","TXT_CODE_INSTANCE_BACKUP_COMPRESSING":"인스턴스 파일을 압축하는 중입니다. 잠시 기다려 주세요...","TXT_CODE_INSTANCE_BACKUP_EXCLUDED":".epbaklst 규칙으로 {{num}}개 파일/디렉터리 제외","TXT_CODE_INSTANCE_BACKUP_FAILED":"인스턴스 백업 실패: {{err}}","TXT_CODE_INSTANCE_BACKUP_QUOTA_EXCEEDED":"백업 공간이 부족합니다: 이 인스턴스의 한도는 {{limit}}GB, 사용량은 {{used}}GB, 평균 백업 크기는 {{average}}GB입니다. 이전 백업을 삭제한 후 다시 시도하세요.","TXT_CODE_INSTANCE_BACKUP_RESTORE_FAILED":"백업 복원 실패: {{err}}","TXT_CODE_INSTANCE_BACKUP_RESTORE_SUCCESS":"백업을 복원했습니다!","TXT_CODE_INSTANCE_BACKUP_RESTORING":"백업 복원 중...","TXT_CODE_INSTANCE_BACKUP_START":"인스턴스 백업 시작 중...","TXT_CODE_INSTANCE_BACKUP_STOPPING":"인스턴스 중지 중...","TXT_CODE_INSTANCE_BACKUP_STOP_TIMEOUT":"인스턴스 중지 시간이 초과되어 백업이 취소되었습니다.","TXT_CODE_INSTANCE_BACKUP_SUCCESS":"인스턴스 백업 성공! 파일: {{name}}","TXT_CODE_instanceConf.instanceBusy":"인스턴스가 현재 사용 중 상태이므로 어떤 작업도 수행할 수 없습니다."}');
692
+
693
+ /***/ },
694
+
695
+ /***/ "./plugins/backup/src/i18n/pt_BR.json"
696
+ (module) {
697
+
698
+ module.exports = /*#__PURE__*/JSON.parse('{"TXT_CODE_DBACKUP_FORMAT":"Formato do arquivo","TXT_CODE_DBACKUP_LEVEL":"Nível de compressão","TXT_CODE_DBACKUP_LEVEL_TIP":"0 armazena sem compactar, 9 compacta ao máximo. Ignorado por tar.gz e 7z que definem o seu.","TXT_CODE_DBACKUP_MAX_SIZE":"Tamanho máximo de backup por instância (GB)","TXT_CODE_DBACKUP_MAX_SIZE_TIP":"Espaço total que os backups de uma instância podem ocupar. 0 significa ilimitado. Quando o espaço restante é menor que o tamanho médio de um backup, backups manuais são recusados e backups agendados excluem o arquivo mais antigo.","TXT_CODE_DBACKUP_PATH":"Diretório de backups","TXT_CODE_DBACKUP_PATH_TIP":"Onde os backups de instâncias são gravados. Caminhos relativos partem do diretório do daemon; vazio significa data/backups.","TXT_CODE_INSTANCE_BACKUP_AUTO_DELETE_OLDEST":"Espaço de backup insuficiente, o backup mais antigo foi excluído automaticamente: {{name}}","TXT_CODE_INSTANCE_BACKUP_COMPRESSING":"Compactando os arquivos da instância, aguarde...","TXT_CODE_INSTANCE_BACKUP_EXCLUDED":"{{num}} arquivos/diretórios excluídos pelas regras de .epbaklst","TXT_CODE_INSTANCE_BACKUP_FAILED":"Falha no backup da instância: {{err}}","TXT_CODE_INSTANCE_BACKUP_QUOTA_EXCEEDED":"Espaço de backup insuficiente: o limite desta instância é {{limit}}GB, {{used}}GB estão em uso e um backup tem em média {{average}}GB. Exclua backups antigos antes de tentar novamente.","TXT_CODE_INSTANCE_BACKUP_RESTORE_FAILED":"Falha ao restaurar o backup: {{err}}","TXT_CODE_INSTANCE_BACKUP_RESTORE_SUCCESS":"Backup restaurado com sucesso!","TXT_CODE_INSTANCE_BACKUP_RESTORING":"Restaurando backup...","TXT_CODE_INSTANCE_BACKUP_START":"Iniciando o backup da instância...","TXT_CODE_INSTANCE_BACKUP_STOPPING":"Parando a instância...","TXT_CODE_INSTANCE_BACKUP_STOP_TIMEOUT":"O tempo limite para parar a instância foi atingido; o backup foi cancelado.","TXT_CODE_INSTANCE_BACKUP_SUCCESS":"Backup da instância concluído! Arquivo: {{name}}","TXT_CODE_instanceConf.instanceBusy":"A instância atual está ocupada e não pode realizar nenhuma operação."}');
699
+
700
+ /***/ },
701
+
702
+ /***/ "./plugins/backup/src/i18n/ru_RU.json"
703
+ (module) {
704
+
705
+ module.exports = /*#__PURE__*/JSON.parse('{"TXT_CODE_DBACKUP_FORMAT":"Формат архива","TXT_CODE_DBACKUP_LEVEL":"Уровень сжатия","TXT_CODE_DBACKUP_LEVEL_TIP":"0 — без сжатия, 9 — максимальное. Игнорируется для tar.gz и 7z, задающих свой уровень.","TXT_CODE_DBACKUP_MAX_SIZE":"Максимальный объем резервных копий на экземпляр (GB)","TXT_CODE_DBACKUP_MAX_SIZE_TIP":"Общий объем, который могут занимать резервные копии одного экземпляра. 0 означает без ограничений. Если оставшегося места меньше среднего размера копии, ручное резервное копирование отклоняется, а запланированное удаляет самый старый архив.","TXT_CODE_DBACKUP_PATH":"Каталог резервных копий","TXT_CODE_DBACKUP_PATH_TIP":"Куда записываются резервные копии экземпляров. Относительные пути отсчитываются от каталога демона; пусто означает data/backups.","TXT_CODE_INSTANCE_BACKUP_AUTO_DELETE_OLDEST":"Недостаточно места для резервных копий, самая старая копия удалена автоматически: {{name}}","TXT_CODE_INSTANCE_BACKUP_COMPRESSING":"Сжатие файлов экземпляра, подождите...","TXT_CODE_INSTANCE_BACKUP_EXCLUDED":"{{num}} файлов/каталогов исключено правилами .epbaklst","TXT_CODE_INSTANCE_BACKUP_FAILED":"Ошибка резервного копирования экземпляра: {{err}}","TXT_CODE_INSTANCE_BACKUP_QUOTA_EXCEEDED":"Недостаточно места для резервной копии: лимит этого экземпляра — {{limit}}GB, использовано {{used}}GB, средний размер копии — {{average}}GB. Удалите старые резервные копии и повторите попытку.","TXT_CODE_INSTANCE_BACKUP_RESTORE_FAILED":"Ошибка восстановления резервной копии: {{err}}","TXT_CODE_INSTANCE_BACKUP_RESTORE_SUCCESS":"Резервная копия успешно восстановлена!","TXT_CODE_INSTANCE_BACKUP_RESTORING":"Восстановление резервной копии...","TXT_CODE_INSTANCE_BACKUP_START":"Запуск резервного копирования экземпляра...","TXT_CODE_INSTANCE_BACKUP_STOPPING":"Остановка экземпляра...","TXT_CODE_INSTANCE_BACKUP_STOP_TIMEOUT":"Превышено время остановки экземпляра, резервное копирование отменено.","TXT_CODE_INSTANCE_BACKUP_SUCCESS":"Резервная копия экземпляра создана! Файл: {{name}}","TXT_CODE_instanceConf.instanceBusy":"Текущий экземпляр занят и не может выполнять операции."}');
706
+
707
+ /***/ },
708
+
709
+ /***/ "./plugins/backup/src/i18n/th_TH.json"
710
+ (module) {
711
+
712
+ module.exports = /*#__PURE__*/JSON.parse('{"TXT_CODE_DBACKUP_FORMAT":"รูปแบบไฟล์บีบอัด","TXT_CODE_DBACKUP_LEVEL":"ระดับการบีบอัด","TXT_CODE_DBACKUP_LEVEL_TIP":"0 คือเก็บโดยไม่บีบอัด 9 คือบีบอัดสูงสุด จะถูกละเว้นสำหรับ tar.gz และ 7z ที่กำหนดระดับของตัวเอง","TXT_CODE_DBACKUP_MAX_SIZE":"ขนาดสำรองข้อมูลสูงสุดต่ออินสแตนซ์ (GB)","TXT_CODE_DBACKUP_MAX_SIZE_TIP":"พื้นที่รวมที่ข้อมูลสำรองของอินสแตนซ์หนึ่งสามารถใช้ได้ 0 หมายถึงไม่จำกัด เมื่อพื้นที่ที่เหลือน้อยกว่าขนาดเฉลี่ยของข้อมูลสำรอง การสำรองข้อมูลด้วยตนเองจะถูกปฏิเสธ และการสำรองข้อมูลตามกำหนดจะลบข้อมูลที่เก่าที่สุด","TXT_CODE_DBACKUP_PATH":"ไดเรกทอรีสำรองข้อมูล","TXT_CODE_DBACKUP_PATH_TIP":"ตำแหน่งที่เก็บข้อมูลสำรองของอินสแตนซ์ เส้นทางแบบสัมพัทธ์อ้างอิงจากไดเรกทอรีของดีมอน เว้นว่างไว้จะเป็น data/backups","TXT_CODE_INSTANCE_BACKUP_AUTO_DELETE_OLDEST":"พื้นที่สำรองข้อมูลไม่เพียงพอ ลบข้อมูลสำรองที่เก่าที่สุดโดยอัตโนมัติแล้ว: {{name}}","TXT_CODE_INSTANCE_BACKUP_COMPRESSING":"กำลังบีบอัดไฟล์อินสแตนซ์ โปรดรอ...","TXT_CODE_INSTANCE_BACKUP_EXCLUDED":"ยกเว้นไฟล์/ไดเรกทอรี {{num}} รายการตามกฎ .epbaklst","TXT_CODE_INSTANCE_BACKUP_FAILED":"สำรองข้อมูลอินสแตนซ์ล้มเหลว: {{err}}","TXT_CODE_INSTANCE_BACKUP_QUOTA_EXCEEDED":"พื้นที่สำรองข้อมูลไม่เพียงพอ: ขีดจำกัดของอินสแตนซ์นี้คือ {{limit}}GB ใช้งานไป {{used}}GB และข้อมูลสำรองเฉลี่ย {{average}}GB กรุณาลบข้อมูลสำรองเก่าก่อนลองใหม่","TXT_CODE_INSTANCE_BACKUP_RESTORE_FAILED":"กู้คืนข้อมูลสำรองล้มเหลว: {{err}}","TXT_CODE_INSTANCE_BACKUP_RESTORE_SUCCESS":"กู้คืนข้อมูลสำรองสำเร็จ!","TXT_CODE_INSTANCE_BACKUP_RESTORING":"กำลังกู้คืนข้อมูลสำรอง...","TXT_CODE_INSTANCE_BACKUP_START":"กำลังเริ่มสำรองข้อมูลอินสแตนซ์...","TXT_CODE_INSTANCE_BACKUP_STOPPING":"กำลังหยุดอินสแตนซ์...","TXT_CODE_INSTANCE_BACKUP_STOP_TIMEOUT":"หยุดอินสแตนซ์หมดเวลา ยกเลิกการสำรองข้อมูลแล้ว","TXT_CODE_INSTANCE_BACKUP_SUCCESS":"สำรองข้อมูลอินสแตนซ์สำเร็จ! ไฟล์: {{name}}","TXT_CODE_instanceConf.instanceBusy":"อินสแตนซ์ปัจจุบันกำลังยุ่งและไม่สามารถดำเนินการใดๆ ได้"}');
713
+
714
+ /***/ },
715
+
716
+ /***/ "./plugins/backup/src/i18n/tr_TR.json"
717
+ (module) {
718
+
719
+ module.exports = /*#__PURE__*/JSON.parse('{"TXT_CODE_DBACKUP_FORMAT":"Arşiv biçimi","TXT_CODE_DBACKUP_LEVEL":"Sıkıştırma düzeyi","TXT_CODE_DBACKUP_LEVEL_TIP":"0 sıkıştırmadan saklar, 9 en çok sıkıştırır. Kendi düzeyini belirleyen tar.gz ve 7z için yok sayılır.","TXT_CODE_DBACKUP_MAX_SIZE":"Örnek başına maksimum yedek boyutu (GB)","TXT_CODE_DBACKUP_MAX_SIZE_TIP":"Bir örneğin yedeklerinin kullanabileceği toplam alan. 0 sınırsız anlamına gelir. Kalan alan ortalama bir yedekten küçükse elle yedekleme reddedilir ve zamanlanmış yedekleme en eski arşivi siler.","TXT_CODE_DBACKUP_PATH":"Yedek dizini","TXT_CODE_DBACKUP_PATH_TIP":"Örnek yedeklerinin yazılacağı yer. Göreli yollar daemon dizinine göre çözümlenir; boş bırakılırsa data/backups kullanılır.","TXT_CODE_INSTANCE_BACKUP_AUTO_DELETE_OLDEST":"Yedek alanı yetersiz, en eski yedek otomatik olarak silindi: {{name}}","TXT_CODE_INSTANCE_BACKUP_COMPRESSING":"Örnek dosyaları sıkıştırılıyor, lütfen bekleyin...","TXT_CODE_INSTANCE_BACKUP_EXCLUDED":".epbaklst kurallarıyla {{num}} dosya/dizin hariç tutuldu","TXT_CODE_INSTANCE_BACKUP_FAILED":"Örnek yedekleme başarısız: {{err}}","TXT_CODE_INSTANCE_BACKUP_QUOTA_EXCEEDED":"Yedek alanı yetersiz: bu örneğin sınırı {{limit}}GB, {{used}}GB kullanılıyor ve bir yedek ortalama {{average}}GB. Yeniden denemeden önce eski yedekleri silin.","TXT_CODE_INSTANCE_BACKUP_RESTORE_FAILED":"Yedek geri yüklenemedi: {{err}}","TXT_CODE_INSTANCE_BACKUP_RESTORE_SUCCESS":"Yedek başarıyla geri yüklendi!","TXT_CODE_INSTANCE_BACKUP_RESTORING":"Yedek geri yükleniyor...","TXT_CODE_INSTANCE_BACKUP_START":"Örnek yedeklemesi başlatılıyor...","TXT_CODE_INSTANCE_BACKUP_STOPPING":"Örnek durduruluyor...","TXT_CODE_INSTANCE_BACKUP_STOP_TIMEOUT":"Örneği durdurma zaman aşımına uğradı, yedekleme iptal edildi.","TXT_CODE_INSTANCE_BACKUP_SUCCESS":"Örnek yedekleme başarılı! Dosya: {{name}}","TXT_CODE_instanceConf.instanceBusy":"Geçerli örnek meşgul ve herhangi bir işlem gerçekleştiremiyor."}');
720
+
721
+ /***/ },
722
+
723
+ /***/ "./plugins/backup/src/i18n/zh_CN.json"
724
+ (module) {
725
+
726
+ module.exports = /*#__PURE__*/JSON.parse('{"TXT_CODE_DBACKUP_FORMAT":"压缩格式","TXT_CODE_DBACKUP_LEVEL":"压缩等级","TXT_CODE_DBACKUP_LEVEL_TIP":"0 表示只打包不压缩,9 为最高压缩率。tar.gz 与 7z 自带等级时会忽略此项。","TXT_CODE_DBACKUP_MAX_SIZE":"每个实例最大备份数据量 (GB)","TXT_CODE_DBACKUP_MAX_SIZE_TIP":"单个实例全部备份文件可占用的总空间上限,0 表示不限制。当剩余空间小于平均每个备份的大小时,手动备份会被拒绝并提示删除旧备份,计划任务备份会自动删除最旧的备份。","TXT_CODE_DBACKUP_PATH":"备份目录","TXT_CODE_DBACKUP_PATH_TIP":"实例备份的存放位置。相对路径相对于守护进程目录;留空则为 data/backups。","TXT_CODE_INSTANCE_BACKUP_AUTO_DELETE_OLDEST":"备份空间不足,已自动删除最旧的备份:{{name}}","TXT_CODE_INSTANCE_BACKUP_COMPRESSING":"正在压缩实例文件...","TXT_CODE_INSTANCE_BACKUP_EXCLUDED":"已有 {{num}} 个文件/目录被 .epbaklst 规则排除","TXT_CODE_INSTANCE_BACKUP_FAILED":"实例备份失败:{{err}}","TXT_CODE_INSTANCE_BACKUP_QUOTA_EXCEEDED":"备份空间不足,无法备份:该实例的备份上限为 {{limit}}GB,已使用 {{used}}GB,平均每个备份 {{average}}GB。请先删除旧备份后重试。","TXT_CODE_INSTANCE_BACKUP_RESTORE_FAILED":"备份恢复失败:{{err}}","TXT_CODE_INSTANCE_BACKUP_RESTORE_SUCCESS":"备份恢复成功!","TXT_CODE_INSTANCE_BACKUP_RESTORING":"正在恢复备份...","TXT_CODE_INSTANCE_BACKUP_START":"开始备份实例...","TXT_CODE_INSTANCE_BACKUP_STOPPING":"正在停止实例...","TXT_CODE_INSTANCE_BACKUP_STOP_TIMEOUT":"停止实例超时,备份已取消。","TXT_CODE_INSTANCE_BACKUP_SUCCESS":"实例备份成功:{{name}}","TXT_CODE_instanceConf.instanceBusy":"当前实例正处于忙碌状态,无法执行任何操作."}');
727
+
728
+ /***/ },
729
+
730
+ /***/ "./plugins/backup/src/i18n/zh_TW.json"
731
+ (module) {
732
+
733
+ module.exports = /*#__PURE__*/JSON.parse('{"TXT_CODE_DBACKUP_FORMAT":"壓縮格式","TXT_CODE_DBACKUP_LEVEL":"壓縮等級","TXT_CODE_DBACKUP_LEVEL_TIP":"0 表示只打包不壓縮,9 為最高壓縮率。tar.gz 與 7z 自帶等級時會忽略此項。","TXT_CODE_DBACKUP_MAX_SIZE":"每個實例最大備份資料量 (GB)","TXT_CODE_DBACKUP_MAX_SIZE_TIP":"單個實例全部備份檔案可佔用的總空間上限,0 表示不限制。當剩餘空間小於平均每個備份的大小時,手動備份會被拒絕並提示刪除舊備份,排程任務備份會自動刪除最舊的備份。","TXT_CODE_DBACKUP_PATH":"備份目錄","TXT_CODE_DBACKUP_PATH_TIP":"實例備份的存放位置。相對路徑相對於守護行程目錄;留空則為 data/backups。","TXT_CODE_INSTANCE_BACKUP_AUTO_DELETE_OLDEST":"備份空間不足,已自動刪除最舊的備份:{{name}}","TXT_CODE_INSTANCE_BACKUP_COMPRESSING":"正在壓縮實例文件...","TXT_CODE_INSTANCE_BACKUP_EXCLUDED":"已有 {{num}} 个文件/目錄被 .epbaklst 規則排除","TXT_CODE_INSTANCE_BACKUP_FAILED":"實例備份失败:{{err}}","TXT_CODE_INSTANCE_BACKUP_QUOTA_EXCEEDED":"備份空間不足,無法備份:此實例的備份上限為 {{limit}}GB,已使用 {{used}}GB,平均每個備份 {{average}}GB。請先刪除舊備份後再試。","TXT_CODE_INSTANCE_BACKUP_RESTORE_FAILED":"備份恢復失败:{{err}}","TXT_CODE_INSTANCE_BACKUP_RESTORE_SUCCESS":"備份恢復成功!","TXT_CODE_INSTANCE_BACKUP_RESTORING":"正在恢復備份...","TXT_CODE_INSTANCE_BACKUP_START":"開始備份實例...","TXT_CODE_INSTANCE_BACKUP_STOPPING":"正在停止實例...","TXT_CODE_INSTANCE_BACKUP_STOP_TIMEOUT":"停止實例超時,備份已取消。","TXT_CODE_INSTANCE_BACKUP_SUCCESS":"實例備份成功:{{name}}","TXT_CODE_instanceConf.instanceBusy":"目前實例正處於忙碌狀態,無法執行任何動作."}');
734
+
735
+ /***/ }
736
+
737
+ /******/ });
738
+ /************************************************************************/
739
+ /******/ // The module cache
740
+ /******/ var __webpack_module_cache__ = {};
741
+ /******/
742
+ /******/ // The require function
743
+ /******/ function __webpack_require__(moduleId) {
744
+ /******/ // Check if module is in cache
745
+ /******/ var cachedModule = __webpack_module_cache__[moduleId];
746
+ /******/ if (cachedModule !== undefined) {
747
+ /******/ return cachedModule.exports;
748
+ /******/ }
749
+ /******/ // Create a new module (and put it into the cache)
750
+ /******/ var module = __webpack_module_cache__[moduleId] = {
751
+ /******/ // no module.id needed
752
+ /******/ // no module.loaded needed
753
+ /******/ exports: {}
754
+ /******/ };
755
+ /******/
756
+ /******/ // Execute the module function
757
+ /******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
758
+ /******/
759
+ /******/ // Return the exports of the module
760
+ /******/ return module.exports;
761
+ /******/ }
762
+ /******/
763
+ /************************************************************************/
764
+ /******/
765
+ /******/ // startup
766
+ /******/ // Load entry module and return exports
767
+ /******/ // This entry module is referenced by other modules so it can't be inlined
768
+ /******/ var __webpack_exports__ = __webpack_require__("./plugins/backup/src/backend/index.ts");
769
+ /******/ module.exports = __webpack_exports__;
770
+ /******/
771
+ /******/ })()
772
+ ;