@xen-orchestra/backups 0.18.3 → 0.20.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/Backup.js +4 -2
- package/DurablePartition.js +2 -0
- package/ImportVmBackup.js +4 -2
- package/RemoteAdapter.js +52 -25
- package/RestoreMetadataBackup.js +2 -0
- package/Task.js +6 -1
- package/_PoolMetadataBackup.js +2 -0
- package/_VmBackup.js +23 -6
- package/_XoMetadataBackup.js +2 -0
- package/_backupType.js +2 -0
- package/_backupWorker.js +29 -12
- package/_cancelableMap.js +4 -2
- package/_cleanVm.js +93 -18
- package/_deltaVm.js +19 -3
- package/_extractIdsFromSimplePattern.js +2 -0
- package/_filenameDate.js +2 -0
- package/_forkStreamUnpipe.js +2 -0
- package/_getOldEntries.js +2 -0
- package/_getTmpDir.js +3 -1
- package/_getVmBackupDir.js +2 -0
- package/_isValidXva.js +21 -6
- package/_listPartitions.js +3 -1
- package/_lvm.js +3 -1
- package/_watchStreamSize.js +2 -0
- package/formatVmBackups.js +2 -0
- package/merge-worker/cli.js +23 -2
- package/merge-worker/index.js +2 -0
- package/package.json +6 -6
- package/parseMetadataBackupId.js +2 -0
- package/runBackupWorker.js +2 -0
- package/writers/DeltaBackupWriter.js +3 -1
- package/writers/DeltaReplicationWriter.js +3 -1
- package/writers/FullBackupWriter.js +2 -0
- package/writers/FullReplicationWriter.js +3 -1
- package/writers/_AbstractDeltaWriter.js +2 -0
- package/writers/_AbstractFullWriter.js +2 -0
- package/writers/_AbstractWriter.js +2 -0
- package/writers/_MixinBackupWriter.js +8 -5
- package/writers/_MixinReplicationWriter.js +2 -0
- package/writers/_checkVhd.js +2 -0
- package/writers/_listReplicatedVms.js +2 -0
- package/writers/_packUuid.js +2 -0
package/Backup.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
|
|
2
|
-
const Disposable = require('promise-toolbox/Disposable
|
|
3
|
-
const ignoreErrors = require('promise-toolbox/ignoreErrors
|
|
4
|
+
const Disposable = require('promise-toolbox/Disposable')
|
|
5
|
+
const ignoreErrors = require('promise-toolbox/ignoreErrors')
|
|
4
6
|
const { compileTemplate } = require('@xen-orchestra/template')
|
|
5
7
|
const { limitConcurrency } = require('limit-concurrency-decorator')
|
|
6
8
|
|
package/DurablePartition.js
CHANGED
package/ImportVmBackup.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const assert = require('assert')
|
|
2
4
|
|
|
3
5
|
const { formatFilenameDate } = require('./_filenameDate.js')
|
|
@@ -6,9 +8,9 @@ const { Task } = require('./Task.js')
|
|
|
6
8
|
const { watchStreamSize } = require('./_watchStreamSize.js')
|
|
7
9
|
|
|
8
10
|
exports.ImportVmBackup = class ImportVmBackup {
|
|
9
|
-
constructor({ adapter, metadata, srUuid, xapi, settings: { newMacAddresses } = {} }) {
|
|
11
|
+
constructor({ adapter, metadata, srUuid, xapi, settings: { newMacAddresses, mapVdisSrs } = {} }) {
|
|
10
12
|
this._adapter = adapter
|
|
11
|
-
this._importDeltaVmSettings = { newMacAddresses }
|
|
13
|
+
this._importDeltaVmSettings = { newMacAddresses, mapVdisSrs }
|
|
12
14
|
this._metadata = metadata
|
|
13
15
|
this._srUuid = srUuid
|
|
14
16
|
this._xapi = xapi
|
package/RemoteAdapter.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
|
|
2
|
-
const Disposable = require('promise-toolbox/Disposable
|
|
3
|
-
const fromCallback = require('promise-toolbox/fromCallback
|
|
4
|
-
const fromEvent = require('promise-toolbox/fromEvent
|
|
5
|
-
const pDefer = require('promise-toolbox/defer
|
|
4
|
+
const Disposable = require('promise-toolbox/Disposable')
|
|
5
|
+
const fromCallback = require('promise-toolbox/fromCallback')
|
|
6
|
+
const fromEvent = require('promise-toolbox/fromEvent')
|
|
7
|
+
const pDefer = require('promise-toolbox/defer')
|
|
6
8
|
const groupBy = require('lodash/groupBy.js')
|
|
7
9
|
const { dirname, join, normalize, resolve } = require('path')
|
|
8
10
|
const { createLogger } = require('@xen-orchestra/log')
|
|
9
11
|
const { Constants, createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdDirectory, VhdSynthetic } = require('vhd-lib')
|
|
10
12
|
const { deduped } = require('@vates/disposable/deduped.js')
|
|
13
|
+
const { decorateMethodsWith } = require('@vates/decorate-with')
|
|
14
|
+
const { compose } = require('@vates/compose')
|
|
11
15
|
const { execFile } = require('child_process')
|
|
12
16
|
const { readdir, stat } = require('fs-extra')
|
|
13
17
|
const { v4: uuidv4 } = require('uuid')
|
|
@@ -88,9 +92,6 @@ class RemoteAdapter {
|
|
|
88
92
|
return partition
|
|
89
93
|
}
|
|
90
94
|
|
|
91
|
-
_getLvmLogicalVolumes = Disposable.factory(this._getLvmLogicalVolumes)
|
|
92
|
-
_getLvmLogicalVolumes = deduped(this._getLvmLogicalVolumes, (devicePath, pvId, vgName) => [devicePath, pvId, vgName])
|
|
93
|
-
_getLvmLogicalVolumes = debounceResourceFactory(this._getLvmLogicalVolumes)
|
|
94
95
|
async *_getLvmLogicalVolumes(devicePath, pvId, vgName) {
|
|
95
96
|
yield this._getLvmPhysicalVolume(devicePath, pvId && (await this._findPartition(devicePath, pvId)))
|
|
96
97
|
|
|
@@ -102,9 +103,6 @@ class RemoteAdapter {
|
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
_getLvmPhysicalVolume = Disposable.factory(this._getLvmPhysicalVolume)
|
|
106
|
-
_getLvmPhysicalVolume = deduped(this._getLvmPhysicalVolume, (devicePath, partition) => [devicePath, partition?.id])
|
|
107
|
-
_getLvmPhysicalVolume = debounceResourceFactory(this._getLvmPhysicalVolume)
|
|
108
106
|
async *_getLvmPhysicalVolume(devicePath, partition) {
|
|
109
107
|
const args = []
|
|
110
108
|
if (partition !== undefined) {
|
|
@@ -125,9 +123,6 @@ class RemoteAdapter {
|
|
|
125
123
|
}
|
|
126
124
|
}
|
|
127
125
|
|
|
128
|
-
_getPartition = Disposable.factory(this._getPartition)
|
|
129
|
-
_getPartition = deduped(this._getPartition, (devicePath, partition) => [devicePath, partition?.id])
|
|
130
|
-
_getPartition = debounceResourceFactory(this._getPartition)
|
|
131
126
|
async *_getPartition(devicePath, partition) {
|
|
132
127
|
const options = ['loop', 'ro']
|
|
133
128
|
|
|
@@ -180,7 +175,6 @@ class RemoteAdapter {
|
|
|
180
175
|
})
|
|
181
176
|
}
|
|
182
177
|
|
|
183
|
-
_usePartitionFiles = Disposable.factory(this._usePartitionFiles)
|
|
184
178
|
async *_usePartitionFiles(diskId, partitionId, paths) {
|
|
185
179
|
const path = yield this.getPartition(diskId, partitionId)
|
|
186
180
|
|
|
@@ -230,8 +224,8 @@ class RemoteAdapter {
|
|
|
230
224
|
async deleteDeltaVmBackups(backups) {
|
|
231
225
|
const handler = this._handler
|
|
232
226
|
|
|
233
|
-
// unused VHDs will be detected by `cleanVm`
|
|
234
|
-
await asyncMapSettled(backups, ({ _filename }) =>
|
|
227
|
+
// this will delete the json, unused VHDs will be detected by `cleanVm`
|
|
228
|
+
await asyncMapSettled(backups, ({ _filename }) => handler.unlink(_filename))
|
|
235
229
|
}
|
|
236
230
|
|
|
237
231
|
async deleteMetadataBackup(backupId) {
|
|
@@ -277,6 +271,12 @@ class RemoteAdapter {
|
|
|
277
271
|
delta !== undefined && this.deleteDeltaVmBackups(delta),
|
|
278
272
|
full !== undefined && this.deleteFullVmBackups(full),
|
|
279
273
|
])
|
|
274
|
+
|
|
275
|
+
const dirs = new Set(files.map(file => dirname(file)))
|
|
276
|
+
for (const dir of dirs) {
|
|
277
|
+
// don't merge in main process, unused VHDs will be merged in the next backup run
|
|
278
|
+
await this.cleanVm(dir, { remove: true, onLog: warn })
|
|
279
|
+
}
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
#getCompressionType() {
|
|
@@ -291,9 +291,6 @@ class RemoteAdapter {
|
|
|
291
291
|
return this.#useVhdDirectory()
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
-
getDisk = Disposable.factory(this.getDisk)
|
|
295
|
-
getDisk = deduped(this.getDisk, diskId => [diskId])
|
|
296
|
-
getDisk = debounceResourceFactory(this.getDisk)
|
|
297
294
|
async *getDisk(diskId) {
|
|
298
295
|
const handler = this._handler
|
|
299
296
|
|
|
@@ -330,7 +327,6 @@ class RemoteAdapter {
|
|
|
330
327
|
// - `<partitionId>`: partitioned disk
|
|
331
328
|
// - `<pvId>/<vgName>/<lvName>`: LVM on a partitioned disk
|
|
332
329
|
// - `/<vgName>/lvName>`: LVM on a raw disk
|
|
333
|
-
getPartition = Disposable.factory(this.getPartition)
|
|
334
330
|
async *getPartition(diskId, partitionId) {
|
|
335
331
|
const devicePath = yield this.getDisk(diskId)
|
|
336
332
|
if (partitionId === undefined) {
|
|
@@ -359,9 +355,14 @@ class RemoteAdapter {
|
|
|
359
355
|
const handler = this._handler
|
|
360
356
|
|
|
361
357
|
const backups = { __proto__: null }
|
|
362
|
-
await asyncMap(await handler.list(BACKUP_DIR), async
|
|
363
|
-
|
|
364
|
-
|
|
358
|
+
await asyncMap(await handler.list(BACKUP_DIR), async entry => {
|
|
359
|
+
// ignore hidden and lock files
|
|
360
|
+
if (entry[0] !== '.' && !entry.endsWith('.lock')) {
|
|
361
|
+
const vmBackups = await this.listVmBackups(entry)
|
|
362
|
+
if (vmBackups.length !== 0) {
|
|
363
|
+
backups[entry] = vmBackups
|
|
364
|
+
}
|
|
365
|
+
}
|
|
365
366
|
})
|
|
366
367
|
|
|
367
368
|
return backups
|
|
@@ -534,8 +535,8 @@ class RemoteAdapter {
|
|
|
534
535
|
|
|
535
536
|
// if it's a path : open all hierarchy of parent
|
|
536
537
|
if (typeof paths === 'string') {
|
|
537
|
-
let vhd
|
|
538
|
-
|
|
538
|
+
let vhd
|
|
539
|
+
let vhdPath = paths
|
|
539
540
|
do {
|
|
540
541
|
const disposable = await openVhd(handler, vhdPath)
|
|
541
542
|
vhd = disposable.value
|
|
@@ -615,4 +616,30 @@ Object.assign(RemoteAdapter.prototype, {
|
|
|
615
616
|
isValidXva,
|
|
616
617
|
})
|
|
617
618
|
|
|
619
|
+
decorateMethodsWith(RemoteAdapter, {
|
|
620
|
+
_getLvmLogicalVolumes: compose([
|
|
621
|
+
Disposable.factory,
|
|
622
|
+
[deduped, (devicePath, pvId, vgName) => [devicePath, pvId, vgName]],
|
|
623
|
+
debounceResourceFactory,
|
|
624
|
+
]),
|
|
625
|
+
|
|
626
|
+
_getLvmPhysicalVolume: compose([
|
|
627
|
+
Disposable.factory,
|
|
628
|
+
[deduped, (devicePath, partition) => [devicePath, partition?.id]],
|
|
629
|
+
debounceResourceFactory,
|
|
630
|
+
]),
|
|
631
|
+
|
|
632
|
+
_getPartition: compose([
|
|
633
|
+
Disposable.factory,
|
|
634
|
+
[deduped, (devicePath, partition) => [devicePath, partition?.id]],
|
|
635
|
+
debounceResourceFactory,
|
|
636
|
+
]),
|
|
637
|
+
|
|
638
|
+
_usePartitionFiles: Disposable.factory,
|
|
639
|
+
|
|
640
|
+
getDisk: compose([Disposable.factory, [deduped, diskId => [diskId]], debounceResourceFactory]),
|
|
641
|
+
|
|
642
|
+
getPartition: Disposable.factory,
|
|
643
|
+
})
|
|
644
|
+
|
|
618
645
|
exports.RemoteAdapter = RemoteAdapter
|
package/RestoreMetadataBackup.js
CHANGED
package/Task.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const CancelToken = require('promise-toolbox/CancelToken')
|
|
2
4
|
const Zone = require('node-zone')
|
|
3
5
|
|
|
4
6
|
const logAfterEnd = () => {
|
|
@@ -7,6 +9,8 @@ const logAfterEnd = () => {
|
|
|
7
9
|
|
|
8
10
|
const noop = Function.prototype
|
|
9
11
|
|
|
12
|
+
const serializeErrors = errors => (Array.isArray(errors) ? errors.map(serializeError) : errors)
|
|
13
|
+
|
|
10
14
|
// Create a serializable object from an error.
|
|
11
15
|
//
|
|
12
16
|
// Otherwise some fields might be non-enumerable and missing from logs.
|
|
@@ -15,6 +19,7 @@ const serializeError = error =>
|
|
|
15
19
|
? {
|
|
16
20
|
...error, // Copy enumerable properties.
|
|
17
21
|
code: error.code,
|
|
22
|
+
errors: serializeErrors(error.errors), // supports AggregateError
|
|
18
23
|
message: error.message,
|
|
19
24
|
name: error.name,
|
|
20
25
|
stack: error.stack,
|
package/_PoolMetadataBackup.js
CHANGED
package/_VmBackup.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const assert = require('assert')
|
|
2
4
|
const findLast = require('lodash/findLast.js')
|
|
3
5
|
const groupBy = require('lodash/groupBy.js')
|
|
4
|
-
const ignoreErrors = require('promise-toolbox/ignoreErrors
|
|
6
|
+
const ignoreErrors = require('promise-toolbox/ignoreErrors')
|
|
5
7
|
const keyBy = require('lodash/keyBy.js')
|
|
6
8
|
const mapValues = require('lodash/mapValues.js')
|
|
7
9
|
const { asyncMap } = require('@xen-orchestra/async-map')
|
|
8
10
|
const { createLogger } = require('@xen-orchestra/log')
|
|
11
|
+
const { decorateMethodsWith } = require('@vates/decorate-with')
|
|
9
12
|
const { defer } = require('golike-defer')
|
|
10
13
|
const { formatDateTime } = require('@xen-orchestra/xapi')
|
|
11
14
|
|
|
@@ -21,6 +24,13 @@ const { watchStreamSize } = require('./_watchStreamSize.js')
|
|
|
21
24
|
|
|
22
25
|
const { debug, warn } = createLogger('xo:backups:VmBackup')
|
|
23
26
|
|
|
27
|
+
class AggregateError extends Error {
|
|
28
|
+
constructor(errors, message) {
|
|
29
|
+
super(message)
|
|
30
|
+
this.errors = errors
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
const asyncEach = async (iterable, fn, thisArg = iterable) => {
|
|
25
35
|
for (const item of iterable) {
|
|
26
36
|
await fn.call(thisArg, item)
|
|
@@ -34,10 +44,11 @@ const forkDeltaExport = deltaExport =>
|
|
|
34
44
|
},
|
|
35
45
|
})
|
|
36
46
|
|
|
37
|
-
|
|
47
|
+
class VmBackup {
|
|
38
48
|
constructor({ config, getSnapshotNameLabel, job, remoteAdapters, remotes, schedule, settings, srs, vm }) {
|
|
39
|
-
if (vm.other_config['xo:backup:job'] === job.id) {
|
|
40
|
-
//
|
|
49
|
+
if (vm.other_config['xo:backup:job'] === job.id && 'start' in vm.blocked_operations) {
|
|
50
|
+
// don't match replicated VMs created by this very job otherwise they
|
|
51
|
+
// will be replicated again and again
|
|
41
52
|
throw new Error('cannot backup a VM created by this very job')
|
|
42
53
|
}
|
|
43
54
|
|
|
@@ -124,16 +135,18 @@ exports.VmBackup = class VmBackup {
|
|
|
124
135
|
return
|
|
125
136
|
}
|
|
126
137
|
|
|
138
|
+
const errors = []
|
|
127
139
|
await (parallel ? asyncMap : asyncEach)(writers, async function (writer) {
|
|
128
140
|
try {
|
|
129
141
|
await fn(writer)
|
|
130
142
|
} catch (error) {
|
|
143
|
+
errors.push(error)
|
|
131
144
|
this.delete(writer)
|
|
132
145
|
warn(warnMessage, { error, writer: writer.constructor.name })
|
|
133
146
|
}
|
|
134
147
|
})
|
|
135
148
|
if (writers.size === 0) {
|
|
136
|
-
throw new
|
|
149
|
+
throw new AggregateError(errors, 'all targets have failed, step: ' + warnMessage)
|
|
137
150
|
}
|
|
138
151
|
}
|
|
139
152
|
|
|
@@ -384,7 +397,6 @@ exports.VmBackup = class VmBackup {
|
|
|
384
397
|
this._fullVdisRequired = fullVdisRequired
|
|
385
398
|
}
|
|
386
399
|
|
|
387
|
-
run = defer(this.run)
|
|
388
400
|
async run($defer) {
|
|
389
401
|
const settings = this._settings
|
|
390
402
|
assert(
|
|
@@ -432,3 +444,8 @@ exports.VmBackup = class VmBackup {
|
|
|
432
444
|
}
|
|
433
445
|
}
|
|
434
446
|
}
|
|
447
|
+
exports.VmBackup = VmBackup
|
|
448
|
+
|
|
449
|
+
decorateMethodsWith(VmBackup, {
|
|
450
|
+
run: defer,
|
|
451
|
+
})
|
package/_XoMetadataBackup.js
CHANGED
package/_backupType.js
CHANGED
package/_backupWorker.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
require('@xen-orchestra/log/configure.js').catchGlobalErrors(
|
|
2
4
|
require('@xen-orchestra/log').createLogger('xo:backups:worker')
|
|
3
5
|
)
|
|
4
6
|
|
|
5
|
-
const Disposable = require('promise-toolbox/Disposable
|
|
6
|
-
const ignoreErrors = require('promise-toolbox/ignoreErrors
|
|
7
|
+
const Disposable = require('promise-toolbox/Disposable')
|
|
8
|
+
const ignoreErrors = require('promise-toolbox/ignoreErrors')
|
|
7
9
|
const { compose } = require('@vates/compose')
|
|
8
10
|
const { createDebounceResource } = require('@vates/disposable/debounceResource.js')
|
|
11
|
+
const { decorateMethodsWith } = require('@vates/decorate-with')
|
|
9
12
|
const { deduped } = require('@vates/disposable/deduped.js')
|
|
10
13
|
const { getHandler } = require('@xen-orchestra/fs')
|
|
11
14
|
const { parseDuration } = require('@vates/parse-duration')
|
|
@@ -58,11 +61,6 @@ class BackupWorker {
|
|
|
58
61
|
}).run()
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
getAdapter = Disposable.factory(this.getAdapter)
|
|
62
|
-
getAdapter = deduped(this.getAdapter, remote => [remote.url])
|
|
63
|
-
getAdapter = compose(this.getAdapter, function (resource) {
|
|
64
|
-
return this.debounceResource(resource)
|
|
65
|
-
})
|
|
66
64
|
async *getAdapter(remote) {
|
|
67
65
|
const handler = getHandler(remote, this.#remoteOptions)
|
|
68
66
|
await handler.sync()
|
|
@@ -77,11 +75,6 @@ class BackupWorker {
|
|
|
77
75
|
}
|
|
78
76
|
}
|
|
79
77
|
|
|
80
|
-
getXapi = Disposable.factory(this.getXapi)
|
|
81
|
-
getXapi = deduped(this.getXapi, ({ url }) => [url])
|
|
82
|
-
getXapi = compose(this.getXapi, function (resource) {
|
|
83
|
-
return this.debounceResource(resource)
|
|
84
|
-
})
|
|
85
78
|
async *getXapi({ credentials: { username: user, password }, ...opts }) {
|
|
86
79
|
const xapi = new Xapi({
|
|
87
80
|
...this.#xapiOptions,
|
|
@@ -103,6 +96,30 @@ class BackupWorker {
|
|
|
103
96
|
}
|
|
104
97
|
}
|
|
105
98
|
|
|
99
|
+
decorateMethodsWith(BackupWorker, {
|
|
100
|
+
getAdapter: compose([
|
|
101
|
+
Disposable.factory,
|
|
102
|
+
[deduped, remote => [remote.url]],
|
|
103
|
+
[
|
|
104
|
+
compose,
|
|
105
|
+
function (resource) {
|
|
106
|
+
return this.debounceResource(resource)
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
]),
|
|
110
|
+
|
|
111
|
+
getXapi: compose([
|
|
112
|
+
Disposable.factory,
|
|
113
|
+
[deduped, xapi => [xapi.url]],
|
|
114
|
+
[
|
|
115
|
+
compose,
|
|
116
|
+
function (resource) {
|
|
117
|
+
return this.debounceResource(resource)
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
]),
|
|
121
|
+
})
|
|
122
|
+
|
|
106
123
|
// Received message:
|
|
107
124
|
//
|
|
108
125
|
// Message {
|
package/_cancelableMap.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const cancelable = require('promise-toolbox/cancelable')
|
|
4
|
+
const CancelToken = require('promise-toolbox/CancelToken')
|
|
3
5
|
|
|
4
6
|
// Similar to `Promise.all` + `map` but pass a cancel token to the callback
|
|
5
7
|
//
|
package/_cleanVm.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const assert = require('assert')
|
|
2
4
|
const sum = require('lodash/sum')
|
|
3
5
|
const { asyncMap } = require('@xen-orchestra/async-map')
|
|
4
6
|
const { Constants, mergeVhd, openVhd, VhdAbstract, VhdFile } = require('vhd-lib')
|
|
7
|
+
const { isVhdAlias, resolveVhdAlias } = require('vhd-lib/aliases')
|
|
5
8
|
const { dirname, resolve } = require('path')
|
|
6
9
|
const { DISK_TYPES } = Constants
|
|
7
10
|
const { isMetadataFile, isVhdFile, isXvaFile, isXvaSumFile } = require('./_backupType.js')
|
|
@@ -82,7 +85,6 @@ async function mergeVhdChain(chain, { handler, onLog, remove, merge }) {
|
|
|
82
85
|
)
|
|
83
86
|
|
|
84
87
|
clearInterval(handle)
|
|
85
|
-
|
|
86
88
|
await Promise.all([
|
|
87
89
|
VhdAbstract.rename(handler, parent, child),
|
|
88
90
|
asyncMap(children.slice(0, -1), child => {
|
|
@@ -100,10 +102,11 @@ async function mergeVhdChain(chain, { handler, onLog, remove, merge }) {
|
|
|
100
102
|
|
|
101
103
|
const noop = Function.prototype
|
|
102
104
|
|
|
103
|
-
const INTERRUPTED_VHDS_REG =
|
|
105
|
+
const INTERRUPTED_VHDS_REG = /^\.(.+)\.merge.json$/
|
|
104
106
|
const listVhds = async (handler, vmDir) => {
|
|
105
|
-
const vhds =
|
|
106
|
-
const
|
|
107
|
+
const vhds = new Set()
|
|
108
|
+
const aliases = {}
|
|
109
|
+
const interruptedVhds = new Map()
|
|
107
110
|
|
|
108
111
|
await asyncMap(
|
|
109
112
|
await handler.list(`${vmDir}/vdis`, {
|
|
@@ -118,25 +121,77 @@ const listVhds = async (handler, vmDir) => {
|
|
|
118
121
|
async vdiDir => {
|
|
119
122
|
const list = await handler.list(vdiDir, {
|
|
120
123
|
filter: file => isVhdFile(file) || INTERRUPTED_VHDS_REG.test(file),
|
|
121
|
-
prependDir: true,
|
|
122
124
|
})
|
|
123
|
-
|
|
125
|
+
aliases[vdiDir] = list.filter(vhd => isVhdAlias(vhd)).map(file => `${vdiDir}/${file}`)
|
|
124
126
|
list.forEach(file => {
|
|
125
127
|
const res = INTERRUPTED_VHDS_REG.exec(file)
|
|
126
128
|
if (res === null) {
|
|
127
|
-
vhds.
|
|
129
|
+
vhds.add(`${vdiDir}/${file}`)
|
|
128
130
|
} else {
|
|
129
|
-
|
|
130
|
-
interruptedVhds.add(`${dir}/${file}`)
|
|
131
|
+
interruptedVhds.set(`${vdiDir}/${res[1]}`, `${vdiDir}/${file}`)
|
|
131
132
|
}
|
|
132
133
|
})
|
|
133
134
|
}
|
|
134
135
|
)
|
|
135
136
|
)
|
|
136
137
|
|
|
137
|
-
return { vhds, interruptedVhds }
|
|
138
|
+
return { vhds, interruptedVhds, aliases }
|
|
138
139
|
}
|
|
139
140
|
|
|
141
|
+
async function checkAliases(aliasPaths, targetDataRepository, { handler, onLog = noop, remove = false }) {
|
|
142
|
+
const aliasFound = []
|
|
143
|
+
for (const path of aliasPaths) {
|
|
144
|
+
const target = await resolveVhdAlias(handler, path)
|
|
145
|
+
|
|
146
|
+
if (!isVhdFile(target)) {
|
|
147
|
+
onLog(`Alias ${path} references a non vhd target: ${target}`)
|
|
148
|
+
if (remove) {
|
|
149
|
+
await handler.unlink(target)
|
|
150
|
+
await handler.unlink(path)
|
|
151
|
+
}
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const { dispose } = await openVhd(handler, target)
|
|
157
|
+
try {
|
|
158
|
+
await dispose()
|
|
159
|
+
} catch (e) {
|
|
160
|
+
// error during dispose should not trigger a deletion
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
onLog(`target ${target} of alias ${path} is missing or broken`, { error })
|
|
164
|
+
if (remove) {
|
|
165
|
+
try {
|
|
166
|
+
await VhdAbstract.unlink(handler, path)
|
|
167
|
+
} catch (e) {
|
|
168
|
+
if (e.code !== 'ENOENT') {
|
|
169
|
+
onLog(`Error while deleting target ${target} of alias ${path}`, { error: e })
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
aliasFound.push(resolve('/', target))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const entries = await handler.list(targetDataRepository, {
|
|
180
|
+
ignoreMissing: true,
|
|
181
|
+
prependDir: true,
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
entries.forEach(async entry => {
|
|
185
|
+
if (!aliasFound.includes(entry)) {
|
|
186
|
+
onLog(`the Vhd ${entry} is not referenced by a an alias`)
|
|
187
|
+
if (remove) {
|
|
188
|
+
await VhdAbstract.unlink(handler, entry)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
exports.checkAliases = checkAliases
|
|
194
|
+
|
|
140
195
|
const defaultMergeLimiter = limitConcurrency(1)
|
|
141
196
|
|
|
142
197
|
exports.cleanVm = async function cleanVm(
|
|
@@ -147,18 +202,16 @@ exports.cleanVm = async function cleanVm(
|
|
|
147
202
|
|
|
148
203
|
const handler = this._handler
|
|
149
204
|
|
|
150
|
-
const vhds = new Set()
|
|
151
205
|
const vhdsToJSons = new Set()
|
|
152
206
|
const vhdParents = { __proto__: null }
|
|
153
207
|
const vhdChildren = { __proto__: null }
|
|
154
208
|
|
|
155
|
-
const
|
|
209
|
+
const { vhds, interruptedVhds, aliases } = await listVhds(handler, vmDir)
|
|
156
210
|
|
|
157
211
|
// remove broken VHDs
|
|
158
|
-
await asyncMap(
|
|
212
|
+
await asyncMap(vhds, async path => {
|
|
159
213
|
try {
|
|
160
|
-
await Disposable.use(openVhd(handler, path, { checkSecondFooter: !
|
|
161
|
-
vhds.add(path)
|
|
214
|
+
await Disposable.use(openVhd(handler, path, { checkSecondFooter: !interruptedVhds.has(path) }), vhd => {
|
|
162
215
|
if (vhd.footer.diskType === DISK_TYPES.DIFFERENCING) {
|
|
163
216
|
const parent = resolve('/', dirname(path), vhd.header.parentUnicodeName)
|
|
164
217
|
vhdParents[path] = parent
|
|
@@ -173,6 +226,7 @@ exports.cleanVm = async function cleanVm(
|
|
|
173
226
|
}
|
|
174
227
|
})
|
|
175
228
|
} catch (error) {
|
|
229
|
+
vhds.delete(path)
|
|
176
230
|
onLog(`error while checking the VHD with path ${path}`, { error })
|
|
177
231
|
if (error?.code === 'ERR_ASSERTION' && remove) {
|
|
178
232
|
onLog(`deleting broken ${path}`)
|
|
@@ -181,7 +235,28 @@ exports.cleanVm = async function cleanVm(
|
|
|
181
235
|
}
|
|
182
236
|
})
|
|
183
237
|
|
|
184
|
-
//
|
|
238
|
+
// remove interrupted merge states for missing VHDs
|
|
239
|
+
for (const interruptedVhd of interruptedVhds.keys()) {
|
|
240
|
+
if (!vhds.has(interruptedVhd)) {
|
|
241
|
+
const statePath = interruptedVhds.get(interruptedVhd)
|
|
242
|
+
interruptedVhds.delete(interruptedVhd)
|
|
243
|
+
|
|
244
|
+
onLog('orphan merge state', {
|
|
245
|
+
mergeStatePath: statePath,
|
|
246
|
+
missingVhdPath: interruptedVhd,
|
|
247
|
+
})
|
|
248
|
+
if (remove) {
|
|
249
|
+
onLog(`deleting orphan merge state ${statePath}`)
|
|
250
|
+
await handler.unlink(statePath)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// check if alias are correct
|
|
256
|
+
// check if all vhd in data subfolder have a corresponding alias
|
|
257
|
+
await asyncMap(Object.keys(aliases), async dir => {
|
|
258
|
+
await checkAliases(aliases[dir], `${dir}/data`, { handler, onLog, remove })
|
|
259
|
+
})
|
|
185
260
|
|
|
186
261
|
// remove VHDs with missing ancestors
|
|
187
262
|
{
|
|
@@ -344,9 +419,9 @@ exports.cleanVm = async function cleanVm(
|
|
|
344
419
|
})
|
|
345
420
|
|
|
346
421
|
// merge interrupted VHDs
|
|
347
|
-
|
|
422
|
+
for (const parent of interruptedVhds.keys()) {
|
|
348
423
|
vhdChainsToMerge[parent] = [vhdChildren[parent], parent]
|
|
349
|
-
}
|
|
424
|
+
}
|
|
350
425
|
|
|
351
426
|
Object.values(vhdChainsToMerge).forEach(chain => {
|
|
352
427
|
if (chain !== undefined) {
|
package/_deltaVm.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const compareVersions = require('compare-versions')
|
|
2
4
|
const find = require('lodash/find.js')
|
|
3
5
|
const groupBy = require('lodash/groupBy.js')
|
|
4
|
-
const ignoreErrors = require('promise-toolbox/ignoreErrors
|
|
6
|
+
const ignoreErrors = require('promise-toolbox/ignoreErrors')
|
|
5
7
|
const omit = require('lodash/omit.js')
|
|
6
8
|
const { asyncMap } = require('@xen-orchestra/async-map')
|
|
7
9
|
const { CancelToken } = require('promise-toolbox')
|
|
@@ -17,6 +19,14 @@ const TAG_COPY_SRC = 'xo:copy_of'
|
|
|
17
19
|
exports.TAG_COPY_SRC = TAG_COPY_SRC
|
|
18
20
|
|
|
19
21
|
const ensureArray = value => (value === undefined ? [] : Array.isArray(value) ? value : [value])
|
|
22
|
+
const resolveUuid = async (xapi, cache, uuid, type) => {
|
|
23
|
+
let ref = cache.get(uuid)
|
|
24
|
+
if (ref === undefined) {
|
|
25
|
+
ref = await xapi.call(`${type}.get_by_uuid`, uuid)
|
|
26
|
+
cache.set(uuid, ref)
|
|
27
|
+
}
|
|
28
|
+
return ref
|
|
29
|
+
}
|
|
20
30
|
|
|
21
31
|
exports.exportDeltaVm = async function exportDeltaVm(
|
|
22
32
|
vm,
|
|
@@ -165,6 +175,12 @@ exports.importDeltaVm = defer(async function importDeltaVm(
|
|
|
165
175
|
}
|
|
166
176
|
}
|
|
167
177
|
|
|
178
|
+
const cache = new Map()
|
|
179
|
+
const mapVdisSrRefs = {}
|
|
180
|
+
for (const [vdiUuid, srUuid] of Object.entries(mapVdisSrs)) {
|
|
181
|
+
mapVdisSrRefs[vdiUuid] = await resolveUuid(xapi, cache, srUuid, 'SR')
|
|
182
|
+
}
|
|
183
|
+
|
|
168
184
|
const baseVdis = {}
|
|
169
185
|
baseVm &&
|
|
170
186
|
baseVm.$VBDs.forEach(vbd => {
|
|
@@ -188,7 +204,7 @@ exports.importDeltaVm = defer(async function importDeltaVm(
|
|
|
188
204
|
[TAG_BASE_DELTA]: undefined,
|
|
189
205
|
[TAG_COPY_SRC]: vdi.uuid,
|
|
190
206
|
},
|
|
191
|
-
sr:
|
|
207
|
+
sr: mapVdisSrRefs[vdi.uuid] ?? sr.$ref,
|
|
192
208
|
})
|
|
193
209
|
)
|
|
194
210
|
$defer.onFailure(() => suspendVdi.$destroy())
|
|
@@ -255,7 +271,7 @@ exports.importDeltaVm = defer(async function importDeltaVm(
|
|
|
255
271
|
[TAG_BASE_DELTA]: undefined,
|
|
256
272
|
[TAG_COPY_SRC]: vdi.uuid,
|
|
257
273
|
},
|
|
258
|
-
SR:
|
|
274
|
+
SR: mapVdisSrRefs[vdi.uuid] ?? sr.$ref,
|
|
259
275
|
})
|
|
260
276
|
)
|
|
261
277
|
$defer.onFailure(() => newVdi.$destroy())
|
package/_filenameDate.js
CHANGED
package/_forkStreamUnpipe.js
CHANGED
package/_getOldEntries.js
CHANGED
package/_getTmpDir.js
CHANGED
package/_getVmBackupDir.js
CHANGED
package/_isValidXva.js
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const assert = require('assert')
|
|
2
4
|
|
|
3
|
-
const
|
|
5
|
+
const COMPRESSED_MAGIC_NUMBERS = [
|
|
4
6
|
// https://tools.ietf.org/html/rfc1952.html#page-5
|
|
5
|
-
|
|
7
|
+
Buffer.from('1F8B', 'hex'),
|
|
8
|
+
|
|
9
|
+
// https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md#zstandard-frames
|
|
10
|
+
Buffer.from('28B52FFD', 'hex'),
|
|
11
|
+
]
|
|
12
|
+
const MAGIC_NUMBER_MAX_LENGTH = Math.max(...COMPRESSED_MAGIC_NUMBERS.map(_ => _.length))
|
|
13
|
+
|
|
14
|
+
const isCompressedFile = async (handler, fd) => {
|
|
15
|
+
const header = Buffer.allocUnsafe(MAGIC_NUMBER_MAX_LENGTH)
|
|
16
|
+
assert.strictEqual((await handler.read(fd, header, 0)).bytesRead, header.length)
|
|
6
17
|
|
|
7
|
-
|
|
8
|
-
|
|
18
|
+
for (const magicNumber of COMPRESSED_MAGIC_NUMBERS) {
|
|
19
|
+
if (magicNumber.compare(header, 0, magicNumber.length) === 0) {
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return false
|
|
9
24
|
}
|
|
10
25
|
|
|
11
26
|
// TODO: better check?
|
|
@@ -43,8 +58,8 @@ async function isValidXva(path) {
|
|
|
43
58
|
return false
|
|
44
59
|
}
|
|
45
60
|
|
|
46
|
-
return (await
|
|
47
|
-
? true //
|
|
61
|
+
return (await isCompressedFile(handler, fd))
|
|
62
|
+
? true // compressed files cannot be validated at this time
|
|
48
63
|
: await isValidTar(handler, size, fd)
|
|
49
64
|
} finally {
|
|
50
65
|
handler.closeFile(fd).catch(noop)
|
package/_listPartitions.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fromCallback = require('promise-toolbox/fromCallback')
|
|
2
4
|
const { createLogger } = require('@xen-orchestra/log')
|
|
3
5
|
const { createParser } = require('parse-pairs')
|
|
4
6
|
const { execFile } = require('child_process')
|
package/_lvm.js
CHANGED
package/_watchStreamSize.js
CHANGED
package/formatVmBackups.js
CHANGED
package/merge-worker/cli.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
'use strict'
|
|
4
|
+
|
|
3
5
|
const { catchGlobalErrors } = require('@xen-orchestra/log/configure.js')
|
|
4
6
|
const { createLogger } = require('@xen-orchestra/log')
|
|
5
7
|
const { getSyncedHandler } = require('@xen-orchestra/fs')
|
|
@@ -41,13 +43,32 @@ const main = Disposable.wrap(async function* main(args) {
|
|
|
41
43
|
let taskFiles
|
|
42
44
|
while ((taskFiles = await listRetry()) !== undefined) {
|
|
43
45
|
const taskFileBasename = min(taskFiles)
|
|
46
|
+
const previousTaskFile = join(CLEAN_VM_QUEUE, taskFileBasename)
|
|
44
47
|
const taskFile = join(CLEAN_VM_QUEUE, '_' + taskFileBasename)
|
|
45
48
|
|
|
46
49
|
// move this task to the end
|
|
47
|
-
|
|
50
|
+
try {
|
|
51
|
+
await handler.rename(previousTaskFile, taskFile)
|
|
52
|
+
} catch (error) {
|
|
53
|
+
// this error occurs if the task failed too many times (i.e. too many `_` prefixes)
|
|
54
|
+
// there is nothing more that can be done
|
|
55
|
+
if (error.code === 'ENAMETOOLONG') {
|
|
56
|
+
await handler.unlink(previousTaskFile)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
throw error
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
try {
|
|
49
63
|
const vmDir = getVmBackupDir(String(await handler.readFile(taskFile)))
|
|
50
|
-
|
|
64
|
+
try {
|
|
65
|
+
await adapter.cleanVm(vmDir, { merge: true, onLog: info, remove: true })
|
|
66
|
+
} catch (error) {
|
|
67
|
+
// consider the clean successful if the VM dir is missing
|
|
68
|
+
if (error.code !== 'ENOENT') {
|
|
69
|
+
throw error
|
|
70
|
+
}
|
|
71
|
+
}
|
|
51
72
|
|
|
52
73
|
handler.unlink(taskFile).catch(error => warn('deleting task failure', { error }))
|
|
53
74
|
} catch (error) {
|
package/merge-worker/index.js
CHANGED
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.20.0",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.6"
|
|
14
14
|
},
|
|
@@ -17,10 +17,11 @@
|
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@vates/compose": "^2.1.0",
|
|
20
|
+
"@vates/decorate-with": "^1.0.0",
|
|
20
21
|
"@vates/disposable": "^0.1.1",
|
|
21
22
|
"@vates/parse-duration": "^0.1.1",
|
|
22
23
|
"@xen-orchestra/async-map": "^0.1.2",
|
|
23
|
-
"@xen-orchestra/fs": "^0.
|
|
24
|
+
"@xen-orchestra/fs": "^0.20.0",
|
|
24
25
|
"@xen-orchestra/log": "^0.3.0",
|
|
25
26
|
"@xen-orchestra/template": "^0.1.0",
|
|
26
27
|
"compare-versions": "^4.0.1",
|
|
@@ -32,15 +33,14 @@
|
|
|
32
33
|
"lodash": "^4.17.20",
|
|
33
34
|
"node-zone": "^0.4.0",
|
|
34
35
|
"parse-pairs": "^1.1.0",
|
|
35
|
-
"promise-toolbox": "^0.
|
|
36
|
+
"promise-toolbox": "^0.21.0",
|
|
36
37
|
"proper-lockfile": "^4.1.2",
|
|
37
|
-
"pump": "^3.0.0",
|
|
38
38
|
"uuid": "^8.3.2",
|
|
39
|
-
"vhd-lib": "^3.
|
|
39
|
+
"vhd-lib": "^3.1.0",
|
|
40
40
|
"yazl": "^2.5.1"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@xen-orchestra/xapi": "^0.
|
|
43
|
+
"@xen-orchestra/xapi": "^0.9.0"
|
|
44
44
|
},
|
|
45
45
|
"license": "AGPL-3.0-or-later",
|
|
46
46
|
"author": {
|
package/parseMetadataBackupId.js
CHANGED
package/runBackupWorker.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const assert = require('assert')
|
|
2
4
|
const map = require('lodash/map.js')
|
|
3
5
|
const mapValues = require('lodash/mapValues.js')
|
|
4
|
-
const ignoreErrors = require('promise-toolbox/ignoreErrors
|
|
6
|
+
const ignoreErrors = require('promise-toolbox/ignoreErrors')
|
|
5
7
|
const { asyncMap } = require('@xen-orchestra/async-map')
|
|
6
8
|
const { chainVhd, checkVhdChain, openVhd, VhdAbstract } = require('vhd-lib')
|
|
7
9
|
const { createLogger } = require('@xen-orchestra/log')
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
|
|
2
|
-
const ignoreErrors = require('promise-toolbox/ignoreErrors
|
|
4
|
+
const ignoreErrors = require('promise-toolbox/ignoreErrors')
|
|
3
5
|
const { formatDateTime } = require('@xen-orchestra/xapi')
|
|
4
6
|
|
|
5
7
|
const { formatFilenameDate } = require('../_filenameDate.js')
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const ignoreErrors = require('promise-toolbox/ignoreErrors')
|
|
2
4
|
const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
|
|
3
5
|
const { formatDateTime } = require('@xen-orchestra/xapi')
|
|
4
6
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const { createLogger } = require('@xen-orchestra/log')
|
|
2
4
|
const { join } = require('path')
|
|
3
5
|
|
|
4
|
-
const {
|
|
6
|
+
const { getVmBackupDir } = require('../_getVmBackupDir.js')
|
|
5
7
|
const MergeWorker = require('../merge-worker/index.js')
|
|
6
8
|
const { formatFilenameDate } = require('../_filenameDate.js')
|
|
7
9
|
|
|
@@ -44,13 +46,14 @@ exports.MixinBackupWriter = (BaseClass = Object) =>
|
|
|
44
46
|
|
|
45
47
|
async afterBackup() {
|
|
46
48
|
const { disableMergeWorker } = this._backup.config
|
|
49
|
+
// merge worker only compatible with local remotes
|
|
50
|
+
const { handler } = this._adapter
|
|
51
|
+
const willMergeInWorker = !disableMergeWorker && typeof handler._getRealPath === 'function'
|
|
47
52
|
|
|
48
|
-
const { merge } = await this._cleanVm({ remove: true, merge:
|
|
53
|
+
const { merge } = await this._cleanVm({ remove: true, merge: !willMergeInWorker })
|
|
49
54
|
await this.#lock.dispose()
|
|
50
55
|
|
|
51
|
-
|
|
52
|
-
const { handler } = this._adapter
|
|
53
|
-
if (merge && !disableMergeWorker && typeof handler._getRealPath === 'function') {
|
|
56
|
+
if (merge && willMergeInWorker) {
|
|
54
57
|
const taskFile =
|
|
55
58
|
join(MergeWorker.CLEAN_VM_QUEUE, formatFilenameDate(new Date())) +
|
|
56
59
|
'-' +
|
package/writers/_checkVhd.js
CHANGED