@xen-orchestra/backups 0.16.1 → 0.18.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/RemoteAdapter.js +51 -18
- package/_VmBackup.js +10 -0
- package/_backupWorker.js +1 -0
- package/_cleanVm.js +90 -46
- package/package.json +4 -4
- package/writers/DeltaBackupWriter.js +3 -4
- package/writers/_MixinBackupWriter.js +19 -5
package/RemoteAdapter.js
CHANGED
|
@@ -3,9 +3,10 @@ const Disposable = require('promise-toolbox/Disposable.js')
|
|
|
3
3
|
const fromCallback = require('promise-toolbox/fromCallback.js')
|
|
4
4
|
const fromEvent = require('promise-toolbox/fromEvent.js')
|
|
5
5
|
const pDefer = require('promise-toolbox/defer.js')
|
|
6
|
+
const groupBy = require('lodash/groupBy.js')
|
|
6
7
|
const { dirname, join, normalize, resolve } = require('path')
|
|
7
8
|
const { createLogger } = require('@xen-orchestra/log')
|
|
8
|
-
const { Constants, createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdSynthetic } = require('vhd-lib')
|
|
9
|
+
const { Constants, createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdDirectory, VhdSynthetic } = require('vhd-lib')
|
|
9
10
|
const { deduped } = require('@vates/disposable/deduped.js')
|
|
10
11
|
const { execFile } = require('child_process')
|
|
11
12
|
const { readdir, stat } = require('fs-extra')
|
|
@@ -67,10 +68,11 @@ const debounceResourceFactory = factory =>
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
class RemoteAdapter {
|
|
70
|
-
constructor(handler, { debounceResource = res => res, dirMode } = {}) {
|
|
71
|
+
constructor(handler, { debounceResource = res => res, dirMode, vhdDirectoryCompression = 'brotli' } = {}) {
|
|
71
72
|
this._debounceResource = debounceResource
|
|
72
73
|
this._dirMode = dirMode
|
|
73
74
|
this._handler = handler
|
|
75
|
+
this._vhdDirectoryCompression = vhdDirectoryCompression
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
get handler() {
|
|
@@ -190,6 +192,22 @@ class RemoteAdapter {
|
|
|
190
192
|
return files
|
|
191
193
|
}
|
|
192
194
|
|
|
195
|
+
// check if we will be allowed to merge a a vhd created in this adapter
|
|
196
|
+
// with the vhd at path `path`
|
|
197
|
+
async isMergeableParent(packedParentUid, path) {
|
|
198
|
+
return await Disposable.use(openVhd(this.handler, path), vhd => {
|
|
199
|
+
// this baseUuid is not linked with this vhd
|
|
200
|
+
if (!vhd.footer.uuid.equals(packedParentUid)) {
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const isVhdDirectory = vhd instanceof VhdDirectory
|
|
205
|
+
return isVhdDirectory
|
|
206
|
+
? this.#useVhdDirectory && this.#getCompressionType() === vhd.compressionType
|
|
207
|
+
: !this.#useVhdDirectory
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
193
211
|
fetchPartitionFiles(diskId, partitionId, paths) {
|
|
194
212
|
const { promise, reject, resolve } = pDefer()
|
|
195
213
|
Disposable.use(
|
|
@@ -243,17 +261,34 @@ class RemoteAdapter {
|
|
|
243
261
|
)
|
|
244
262
|
}
|
|
245
263
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
264
|
+
deleteVmBackup(file) {
|
|
265
|
+
return this.deleteVmBackups([file])
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async deleteVmBackups(files) {
|
|
269
|
+
const { delta, full, ...others } = groupBy(await asyncMap(files, file => this.readVmBackupMetadata(file)), 'mode')
|
|
249
270
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
await this.deleteFullVmBackups([metadata])
|
|
254
|
-
} else {
|
|
255
|
-
throw new Error(`no deleter for backup mode ${metadata.mode}`)
|
|
271
|
+
const unsupportedModes = Object.keys(others)
|
|
272
|
+
if (unsupportedModes.length !== 0) {
|
|
273
|
+
throw new Error('no deleter for backup modes: ' + unsupportedModes.join(', '))
|
|
256
274
|
}
|
|
275
|
+
|
|
276
|
+
await Promise.all([
|
|
277
|
+
delta !== undefined && this.deleteDeltaVmBackups(delta),
|
|
278
|
+
full !== undefined && this.deleteFullVmBackups(full),
|
|
279
|
+
])
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
#getCompressionType() {
|
|
283
|
+
return this._vhdDirectoryCompression
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#useVhdDirectory() {
|
|
287
|
+
return this.handler.type === 's3'
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
#useAlias() {
|
|
291
|
+
return this.#useVhdDirectory()
|
|
257
292
|
}
|
|
258
293
|
|
|
259
294
|
getDisk = Disposable.factory(this.getDisk)
|
|
@@ -312,13 +347,10 @@ class RemoteAdapter {
|
|
|
312
347
|
return yield this._getPartition(devicePath, await this._findPartition(devicePath, partitionId))
|
|
313
348
|
}
|
|
314
349
|
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
// if the file is named .vhd => vhd
|
|
318
|
-
// if the file is named alias.vhd => alias to a vhd
|
|
350
|
+
// if we use alias on this remote, we have to name the file alias.vhd
|
|
319
351
|
getVhdFileName(baseName) {
|
|
320
|
-
if (this
|
|
321
|
-
return `${baseName}.alias.vhd`
|
|
352
|
+
if (this.#useAlias()) {
|
|
353
|
+
return `${baseName}.alias.vhd`
|
|
322
354
|
}
|
|
323
355
|
return `${baseName}.vhd`
|
|
324
356
|
}
|
|
@@ -470,10 +502,11 @@ class RemoteAdapter {
|
|
|
470
502
|
async writeVhd(path, input, { checksum = true, validator = noop } = {}) {
|
|
471
503
|
const handler = this._handler
|
|
472
504
|
|
|
473
|
-
if (
|
|
505
|
+
if (this.#useVhdDirectory()) {
|
|
474
506
|
const dataPath = `${dirname(path)}/data/${uuidv4()}.vhd`
|
|
475
507
|
await createVhdDirectoryFromStream(handler, dataPath, input, {
|
|
476
508
|
concurrency: 16,
|
|
509
|
+
compression: this.#getCompressionType(),
|
|
477
510
|
async validator() {
|
|
478
511
|
await input.task
|
|
479
512
|
return validator.apply(this, arguments)
|
package/_VmBackup.js
CHANGED
|
@@ -36,6 +36,11 @@ const forkDeltaExport = deltaExport =>
|
|
|
36
36
|
|
|
37
37
|
exports.VmBackup = class VmBackup {
|
|
38
38
|
constructor({ config, getSnapshotNameLabel, job, remoteAdapters, remotes, schedule, settings, srs, vm }) {
|
|
39
|
+
if (vm.other_config['xo:backup:job'] === job.id) {
|
|
40
|
+
// otherwise replicated VMs would be matched and replicated again and again
|
|
41
|
+
throw new Error('cannot backup a VM created by this very job')
|
|
42
|
+
}
|
|
43
|
+
|
|
39
44
|
this.config = config
|
|
40
45
|
this.job = job
|
|
41
46
|
this.remoteAdapters = remoteAdapters
|
|
@@ -354,6 +359,11 @@ exports.VmBackup = class VmBackup {
|
|
|
354
359
|
false
|
|
355
360
|
)
|
|
356
361
|
|
|
362
|
+
if (presentBaseVdis.size === 0) {
|
|
363
|
+
debug('no base VM found')
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
|
|
357
367
|
const fullVdisRequired = new Set()
|
|
358
368
|
baseUuidToSrcVdi.forEach((srcVdi, baseUuid) => {
|
|
359
369
|
if (presentBaseVdis.has(baseUuid)) {
|
package/_backupWorker.js
CHANGED
package/_cleanVm.js
CHANGED
|
@@ -10,6 +10,24 @@ const { limitConcurrency } = require('limit-concurrency-decorator')
|
|
|
10
10
|
const { Task } = require('./Task.js')
|
|
11
11
|
const { Disposable } = require('promise-toolbox')
|
|
12
12
|
|
|
13
|
+
// checking the size of a vhd directory is costly
|
|
14
|
+
// 1 Http Query per 1000 blocks
|
|
15
|
+
// we only check size of all the vhd are VhdFiles
|
|
16
|
+
function shouldComputeVhdsSize(vhds) {
|
|
17
|
+
return vhds.every(vhd => vhd instanceof VhdFile)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const computeVhdsSize = (handler, vhdPaths) =>
|
|
21
|
+
Disposable.use(
|
|
22
|
+
vhdPaths.map(vhdPath => openVhd(handler, vhdPath)),
|
|
23
|
+
async vhds => {
|
|
24
|
+
if (shouldComputeVhdsSize(vhds)) {
|
|
25
|
+
const sizes = await asyncMap(vhds, vhd => vhd.getSize())
|
|
26
|
+
return sum(sizes)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
|
|
13
31
|
// chain is an array of VHDs from child to parent
|
|
14
32
|
//
|
|
15
33
|
// the whole chain will be merged into parent, parent will be renamed to child
|
|
@@ -130,6 +148,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
130
148
|
const handler = this._handler
|
|
131
149
|
|
|
132
150
|
const vhds = new Set()
|
|
151
|
+
const vhdsToJSons = new Set()
|
|
133
152
|
const vhdParents = { __proto__: null }
|
|
134
153
|
const vhdChildren = { __proto__: null }
|
|
135
154
|
|
|
@@ -202,7 +221,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
202
221
|
await Promise.all(deletions)
|
|
203
222
|
}
|
|
204
223
|
|
|
205
|
-
const jsons =
|
|
224
|
+
const jsons = new Set()
|
|
206
225
|
const xvas = new Set()
|
|
207
226
|
const xvaSums = []
|
|
208
227
|
const entries = await handler.list(vmDir, {
|
|
@@ -210,7 +229,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
210
229
|
})
|
|
211
230
|
entries.forEach(path => {
|
|
212
231
|
if (isMetadataFile(path)) {
|
|
213
|
-
jsons.
|
|
232
|
+
jsons.add(path)
|
|
214
233
|
} else if (isXvaFile(path)) {
|
|
215
234
|
xvas.add(path)
|
|
216
235
|
} else if (isXvaSumFile(path)) {
|
|
@@ -232,22 +251,25 @@ exports.cleanVm = async function cleanVm(
|
|
|
232
251
|
// compile the list of unused XVAs and VHDs, and remove backup metadata which
|
|
233
252
|
// reference a missing XVA/VHD
|
|
234
253
|
await asyncMap(jsons, async json => {
|
|
235
|
-
|
|
254
|
+
let metadata
|
|
255
|
+
try {
|
|
256
|
+
metadata = JSON.parse(await handler.readFile(json))
|
|
257
|
+
} catch (error) {
|
|
258
|
+
onLog(`failed to read metadata file ${json}`, { error })
|
|
259
|
+
jsons.delete(json)
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
|
|
236
263
|
const { mode } = metadata
|
|
237
|
-
let size
|
|
238
264
|
if (mode === 'full') {
|
|
239
265
|
const linkedXva = resolve('/', vmDir, metadata.xva)
|
|
240
|
-
|
|
241
266
|
if (xvas.has(linkedXva)) {
|
|
242
267
|
unusedXvas.delete(linkedXva)
|
|
243
|
-
|
|
244
|
-
size = await handler.getSize(linkedXva).catch(error => {
|
|
245
|
-
onLog(`failed to get size of ${json}`, { error })
|
|
246
|
-
})
|
|
247
268
|
} else {
|
|
248
269
|
onLog(`the XVA linked to the metadata ${json} is missing`)
|
|
249
270
|
if (remove) {
|
|
250
271
|
onLog(`deleting incomplete backup ${json}`)
|
|
272
|
+
jsons.delete(json)
|
|
251
273
|
await handler.unlink(json)
|
|
252
274
|
}
|
|
253
275
|
}
|
|
@@ -256,50 +278,25 @@ exports.cleanVm = async function cleanVm(
|
|
|
256
278
|
const { vhds } = metadata
|
|
257
279
|
return Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
|
|
258
280
|
})()
|
|
281
|
+
|
|
282
|
+
const missingVhds = linkedVhds.filter(_ => !vhds.has(_))
|
|
283
|
+
|
|
259
284
|
// FIXME: find better approach by keeping as much of the backup as
|
|
260
285
|
// possible (existing disks) even if one disk is missing
|
|
261
|
-
if (
|
|
286
|
+
if (missingVhds.length === 0) {
|
|
262
287
|
linkedVhds.forEach(_ => unusedVhds.delete(_))
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
// we only check size of all the vhd are VhdFiles
|
|
267
|
-
|
|
268
|
-
const shouldComputeSize = linkedVhds.every(vhd => vhd instanceof VhdFile)
|
|
269
|
-
if (shouldComputeSize) {
|
|
270
|
-
try {
|
|
271
|
-
await Disposable.use(Disposable.all(linkedVhds.map(vhdPath => openVhd(handler, vhdPath))), async vhds => {
|
|
272
|
-
const sizes = await asyncMap(vhds, vhd => vhd.getSize())
|
|
273
|
-
size = sum(sizes)
|
|
274
|
-
})
|
|
275
|
-
} catch (error) {
|
|
276
|
-
onLog(`failed to get size of ${json}`, { error })
|
|
277
|
-
}
|
|
278
|
-
}
|
|
288
|
+
linkedVhds.forEach(path => {
|
|
289
|
+
vhdsToJSons[path] = json
|
|
290
|
+
})
|
|
279
291
|
} else {
|
|
280
|
-
onLog(`Some VHDs linked to the metadata ${json} are missing
|
|
292
|
+
onLog(`Some VHDs linked to the metadata ${json} are missing`, { missingVhds })
|
|
281
293
|
if (remove) {
|
|
282
294
|
onLog(`deleting incomplete backup ${json}`)
|
|
295
|
+
jsons.delete(json)
|
|
283
296
|
await handler.unlink(json)
|
|
284
297
|
}
|
|
285
298
|
}
|
|
286
299
|
}
|
|
287
|
-
|
|
288
|
-
const metadataSize = metadata.size
|
|
289
|
-
if (size !== undefined && metadataSize !== size) {
|
|
290
|
-
onLog(`incorrect size in metadata: ${metadataSize ?? 'none'} instead of ${size}`)
|
|
291
|
-
|
|
292
|
-
// don't update if the the stored size is greater than found files,
|
|
293
|
-
// it can indicates a problem
|
|
294
|
-
if (fixMetadata && (metadataSize === undefined || metadataSize < size)) {
|
|
295
|
-
try {
|
|
296
|
-
metadata.size = size
|
|
297
|
-
await handler.writeFile(json, JSON.stringify(metadata), { flags: 'w' })
|
|
298
|
-
} catch (error) {
|
|
299
|
-
onLog(`failed to update size in backup metadata ${json}`, { error })
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
300
|
})
|
|
304
301
|
|
|
305
302
|
// TODO: parallelize by vm/job/vdi
|
|
@@ -358,9 +355,15 @@ exports.cleanVm = async function cleanVm(
|
|
|
358
355
|
})
|
|
359
356
|
}
|
|
360
357
|
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
358
|
+
const metadataWithMergedVhd = {}
|
|
359
|
+
const doMerge = async () => {
|
|
360
|
+
await asyncMap(toMerge, async chain => {
|
|
361
|
+
const merged = await limitedMergeVhdChain(chain, { handler, onLog, remove, merge })
|
|
362
|
+
if (merged !== undefined) {
|
|
363
|
+
const metadataPath = vhdsToJSons[chain[0]] // all the chain should have the same metada file
|
|
364
|
+
metadataWithMergedVhd[metadataPath] = true
|
|
365
|
+
}
|
|
366
|
+
})
|
|
364
367
|
}
|
|
365
368
|
|
|
366
369
|
await Promise.all([
|
|
@@ -385,6 +388,47 @@ exports.cleanVm = async function cleanVm(
|
|
|
385
388
|
}),
|
|
386
389
|
])
|
|
387
390
|
|
|
391
|
+
// update size for delta metadata with merged VHD
|
|
392
|
+
// check for the other that the size is the same as the real file size
|
|
393
|
+
|
|
394
|
+
await asyncMap(jsons, async metadataPath => {
|
|
395
|
+
const metadata = JSON.parse(await handler.readFile(metadataPath))
|
|
396
|
+
|
|
397
|
+
let fileSystemSize
|
|
398
|
+
const merged = metadataWithMergedVhd[metadataPath] !== undefined
|
|
399
|
+
|
|
400
|
+
const { mode, size, vhds, xva } = metadata
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
if (mode === 'full') {
|
|
404
|
+
// a full backup : check size
|
|
405
|
+
const linkedXva = resolve('/', vmDir, xva)
|
|
406
|
+
fileSystemSize = await handler.getSize(linkedXva)
|
|
407
|
+
} else if (mode === 'delta') {
|
|
408
|
+
const linkedVhds = Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
|
|
409
|
+
fileSystemSize = await computeVhdsSize(handler, linkedVhds)
|
|
410
|
+
|
|
411
|
+
// don't warn if the size has changed after a merge
|
|
412
|
+
if (!merged && fileSystemSize !== size) {
|
|
413
|
+
onLog(`incorrect size in metadata: ${size ?? 'none'} instead of ${fileSystemSize}`)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
} catch (error) {
|
|
417
|
+
onLog(`failed to get size of ${metadataPath}`, { error })
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// systematically update size after a merge
|
|
422
|
+
if ((merged || fixMetadata) && size !== fileSystemSize) {
|
|
423
|
+
metadata.size = fileSystemSize
|
|
424
|
+
try {
|
|
425
|
+
await handler.writeFile(metadataPath, JSON.stringify(metadata), { flags: 'w' })
|
|
426
|
+
} catch (error) {
|
|
427
|
+
onLog(`failed to update size in backup metadata ${metadataPath} after merge`, { error })
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
})
|
|
431
|
+
|
|
388
432
|
return {
|
|
389
433
|
// boolean whether some VHDs were merged (or should be merged)
|
|
390
434
|
merge: toMerge.length !== 0,
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
|
10
10
|
},
|
|
11
|
-
"version": "0.
|
|
11
|
+
"version": "0.18.0",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.6"
|
|
14
14
|
},
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"@vates/disposable": "^0.1.1",
|
|
21
21
|
"@vates/parse-duration": "^0.1.1",
|
|
22
22
|
"@xen-orchestra/async-map": "^0.1.2",
|
|
23
|
-
"@xen-orchestra/fs": "^0.19.
|
|
23
|
+
"@xen-orchestra/fs": "^0.19.3",
|
|
24
24
|
"@xen-orchestra/log": "^0.3.0",
|
|
25
25
|
"@xen-orchestra/template": "^0.1.0",
|
|
26
26
|
"compare-versions": "^4.0.1",
|
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
"proper-lockfile": "^4.1.2",
|
|
37
37
|
"pump": "^3.0.0",
|
|
38
38
|
"uuid": "^8.3.2",
|
|
39
|
-
"vhd-lib": "^2.0
|
|
39
|
+
"vhd-lib": "^2.1.0",
|
|
40
40
|
"yazl": "^2.5.1"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@xen-orchestra/xapi": "^0.8.
|
|
43
|
+
"@xen-orchestra/xapi": "^0.8.5"
|
|
44
44
|
},
|
|
45
45
|
"license": "AGPL-3.0-or-later",
|
|
46
46
|
"author": {
|
|
@@ -24,6 +24,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
|
|
24
24
|
async checkBaseVdis(baseUuidToSrcVdi) {
|
|
25
25
|
const { handler } = this._adapter
|
|
26
26
|
const backup = this._backup
|
|
27
|
+
const adapter = this._adapter
|
|
27
28
|
|
|
28
29
|
const backupDir = getVmBackupDir(backup.vm.uuid)
|
|
29
30
|
const vdisDir = `${backupDir}/vdis/${backup.job.id}`
|
|
@@ -35,13 +36,11 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
|
|
|
35
36
|
filter: _ => _[0] !== '.' && _.endsWith('.vhd'),
|
|
36
37
|
prependDir: true,
|
|
37
38
|
})
|
|
39
|
+
const packedBaseUuid = packUuid(baseUuid)
|
|
38
40
|
await asyncMap(vhds, async path => {
|
|
39
41
|
try {
|
|
40
42
|
await checkVhdChain(handler, path)
|
|
41
|
-
await
|
|
42
|
-
openVhd(handler, path),
|
|
43
|
-
vhd => (found = found || vhd.footer.uuid.equals(packUuid(baseUuid)))
|
|
44
|
-
)
|
|
43
|
+
found = found || (await adapter.isMergeableParent(packedBaseUuid, path))
|
|
45
44
|
} catch (error) {
|
|
46
45
|
warn('checkBaseVdis', { error })
|
|
47
46
|
await ignoreErrors.call(VhdAbstract.unlink(handler, path))
|
|
@@ -21,10 +21,18 @@ exports.MixinBackupWriter = (BaseClass = Object) =>
|
|
|
21
21
|
this.#vmBackupDir = getVmBackupDir(this._backup.vm.uuid)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
_cleanVm(options) {
|
|
25
|
-
|
|
26
|
-
.cleanVm(this.#vmBackupDir, {
|
|
27
|
-
|
|
24
|
+
async _cleanVm(options) {
|
|
25
|
+
try {
|
|
26
|
+
return await this._adapter.cleanVm(this.#vmBackupDir, {
|
|
27
|
+
...options,
|
|
28
|
+
fixMetadata: true,
|
|
29
|
+
onLog: warn,
|
|
30
|
+
lock: false,
|
|
31
|
+
})
|
|
32
|
+
} catch (error) {
|
|
33
|
+
warn(error)
|
|
34
|
+
return {}
|
|
35
|
+
}
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
async beforeBackup() {
|
|
@@ -43,7 +51,13 @@ exports.MixinBackupWriter = (BaseClass = Object) =>
|
|
|
43
51
|
// merge worker only compatible with local remotes
|
|
44
52
|
const { handler } = this._adapter
|
|
45
53
|
if (merge && !disableMergeWorker && typeof handler._getRealPath === 'function') {
|
|
46
|
-
|
|
54
|
+
const taskFile =
|
|
55
|
+
join(MergeWorker.CLEAN_VM_QUEUE, formatFilenameDate(new Date())) +
|
|
56
|
+
'-' +
|
|
57
|
+
// add a random suffix to avoid collision in case multiple tasks are created at the same second
|
|
58
|
+
Math.random().toString(36).slice(2)
|
|
59
|
+
|
|
60
|
+
await handler.outputFile(taskFile, this._backup.vm.uuid)
|
|
47
61
|
const remotePath = handler._getRealPath()
|
|
48
62
|
await MergeWorker.run(remotePath)
|
|
49
63
|
}
|