@xen-orchestra/backups 0.15.1 → 0.17.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 CHANGED
@@ -3,19 +3,20 @@ 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 pump = require('pump')
7
- const { basename, dirname, join, normalize, resolve } = require('path')
6
+ const groupBy = require('lodash/groupBy.js')
7
+ const { dirname, join, normalize, resolve } = require('path')
8
8
  const { createLogger } = require('@xen-orchestra/log')
9
- const { createSyntheticStream, mergeVhd, VhdFile } = require('vhd-lib')
9
+ const { Constants, createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdSynthetic } = require('vhd-lib')
10
10
  const { deduped } = require('@vates/disposable/deduped.js')
11
11
  const { execFile } = require('child_process')
12
12
  const { readdir, stat } = require('fs-extra')
13
+ const { v4: uuidv4 } = require('uuid')
13
14
  const { ZipFile } = require('yazl')
14
15
 
15
16
  const { BACKUP_DIR } = require('./_getVmBackupDir.js')
16
17
  const { cleanVm } = require('./_cleanVm.js')
17
18
  const { getTmpDir } = require('./_getTmpDir.js')
18
- const { isMetadataFile, isVhdFile } = require('./_backupType.js')
19
+ const { isMetadataFile } = require('./_backupType.js')
19
20
  const { isValidXva } = require('./_isValidXva.js')
20
21
  const { listPartitions, LVM_PARTITION_TYPE } = require('./_listPartitions.js')
21
22
  const { lvs, pvs } = require('./_lvm.js')
@@ -77,48 +78,6 @@ class RemoteAdapter {
77
78
  return this._handler
78
79
  }
79
80
 
80
- async _deleteVhd(path) {
81
- const handler = this._handler
82
- const vhds = await asyncMapSettled(
83
- await handler.list(dirname(path), {
84
- filter: isVhdFile,
85
- prependDir: true,
86
- }),
87
- async path => {
88
- try {
89
- const vhd = new VhdFile(handler, path)
90
- await vhd.readHeaderAndFooter()
91
- return {
92
- footer: vhd.footer,
93
- header: vhd.header,
94
- path,
95
- }
96
- } catch (error) {
97
- // Do not fail on corrupted VHDs (usually uncleaned temporary files),
98
- // they are probably inconsequent to the backup process and should not
99
- // fail it.
100
- warn(`BackupNg#_deleteVhd ${path}`, { error })
101
- }
102
- }
103
- )
104
- const base = basename(path)
105
- const child = vhds.find(_ => _ !== undefined && _.header.parentUnicodeName === base)
106
- if (child === undefined) {
107
- await handler.unlink(path)
108
- return 0
109
- }
110
-
111
- try {
112
- const childPath = child.path
113
- const mergedDataSize = await mergeVhd(handler, path, handler, childPath)
114
- await handler.rename(path, childPath)
115
- return mergedDataSize
116
- } catch (error) {
117
- handler.unlink(path).catch(warn)
118
- throw error
119
- }
120
- }
121
-
122
81
  async _findPartition(devicePath, partitionId) {
123
82
  const partitions = await listPartitions(devicePath)
124
83
  const partition = partitions.find(_ => _.id === partitionId)
@@ -255,7 +214,7 @@ class RemoteAdapter {
255
214
  const handler = this._handler
256
215
 
257
216
  // unused VHDs will be detected by `cleanVm`
258
- await asyncMapSettled(backups, ({ _filename }) => handler.unlink(_filename))
217
+ await asyncMapSettled(backups, ({ _filename }) => VhdAbstract.unlink(handler, _filename))
259
218
  }
260
219
 
261
220
  async deleteMetadataBackup(backupId) {
@@ -285,17 +244,22 @@ class RemoteAdapter {
285
244
  )
286
245
  }
287
246
 
288
- async deleteVmBackup(filename) {
289
- const metadata = JSON.parse(String(await this._handler.readFile(filename)))
290
- metadata._filename = filename
247
+ deleteVmBackup(file) {
248
+ return this.deleteVmBackups([file])
249
+ }
291
250
 
292
- if (metadata.mode === 'delta') {
293
- await this.deleteDeltaVmBackups([metadata])
294
- } else if (metadata.mode === 'full') {
295
- await this.deleteFullVmBackups([metadata])
296
- } else {
297
- throw new Error(`no deleter for backup mode ${metadata.mode}`)
251
+ async deleteVmBackups(files) {
252
+ const { delta, full, ...others } = groupBy(await asyncMap(files, file => this.readVmBackupMetadata(file)), 'mode')
253
+
254
+ const unsupportedModes = Object.keys(others)
255
+ if (unsupportedModes.length !== 0) {
256
+ throw new Error('no deleter for backup modes: ' + unsupportedModes.join(', '))
298
257
  }
258
+
259
+ await Promise.all([
260
+ delta !== undefined && this.deleteDeltaVmBackups(delta),
261
+ full !== undefined && this.deleteFullVmBackups(full),
262
+ ])
299
263
  }
300
264
 
301
265
  getDisk = Disposable.factory(this.getDisk)
@@ -354,6 +318,17 @@ class RemoteAdapter {
354
318
  return yield this._getPartition(devicePath, await this._findPartition(devicePath, partitionId))
355
319
  }
356
320
 
321
+ // this function will be the one where we plug the logic of the storage format by fs type/user settings
322
+
323
+ // if the file is named .vhd => vhd
324
+ // if the file is named alias.vhd => alias to a vhd
325
+ getVhdFileName(baseName) {
326
+ if (this._handler.type === 's3') {
327
+ return `${baseName}.alias.vhd` // we want an alias to a vhddirectory
328
+ }
329
+ return `${baseName}.vhd`
330
+ }
331
+
357
332
  async listAllVmBackups() {
358
333
  const handler = this._handler
359
334
 
@@ -498,6 +473,24 @@ class RemoteAdapter {
498
473
  return backups.sort(compareTimestamp)
499
474
  }
500
475
 
476
+ async writeVhd(path, input, { checksum = true, validator = noop } = {}) {
477
+ const handler = this._handler
478
+
479
+ if (path.endsWith('.alias.vhd')) {
480
+ const dataPath = `${dirname(path)}/data/${uuidv4()}.vhd`
481
+ await createVhdDirectoryFromStream(handler, dataPath, input, {
482
+ concurrency: 16,
483
+ async validator() {
484
+ await input.task
485
+ return validator.apply(this, arguments)
486
+ },
487
+ })
488
+ await VhdAbstract.createAlias(handler, path, dataPath)
489
+ } else {
490
+ await this.outputStream(path, input, { checksum, validator })
491
+ }
492
+ }
493
+
501
494
  async outputStream(path, input, { checksum = true, validator = noop } = {}) {
502
495
  await this._handler.outputStream(path, input, {
503
496
  checksum,
@@ -509,6 +502,52 @@ class RemoteAdapter {
509
502
  })
510
503
  }
511
504
 
505
+ async _createSyntheticStream(handler, paths) {
506
+ let disposableVhds = []
507
+
508
+ // if it's a path : open all hierarchy of parent
509
+ if (typeof paths === 'string') {
510
+ let vhd,
511
+ vhdPath = paths
512
+ do {
513
+ const disposable = await openVhd(handler, vhdPath)
514
+ vhd = disposable.value
515
+ disposableVhds.push(disposable)
516
+ vhdPath = resolveRelativeFromFile(vhdPath, vhd.header.parentUnicodeName)
517
+ } while (vhd.footer.diskType !== Constants.DISK_TYPES.DYNAMIC)
518
+ } else {
519
+ // only open the list of path given
520
+ disposableVhds = paths.map(path => openVhd(handler, path))
521
+ }
522
+
523
+ // I don't want the vhds to be disposed on return
524
+ // but only when the stream is done ( or failed )
525
+ const disposables = await Disposable.all(disposableVhds)
526
+ const vhds = disposables.value
527
+
528
+ let disposed = false
529
+ const disposeOnce = async () => {
530
+ if (!disposed) {
531
+ disposed = true
532
+
533
+ try {
534
+ await disposables.dispose()
535
+ } catch (error) {
536
+ warn('_createSyntheticStream: failed to dispose VHDs', { error })
537
+ }
538
+ }
539
+ }
540
+
541
+ const synthetic = new VhdSynthetic(vhds)
542
+ await synthetic.readHeaderAndFooter()
543
+ await synthetic.readBlockAllocationTable()
544
+ const stream = await synthetic.stream()
545
+ stream.on('end', disposeOnce)
546
+ stream.on('close', disposeOnce)
547
+ stream.on('error', disposeOnce)
548
+ return stream
549
+ }
550
+
512
551
  async readDeltaVmBackup(metadata) {
513
552
  const handler = this._handler
514
553
  const { vbds, vdis, vhds, vifs, vm } = metadata
@@ -516,7 +555,7 @@ class RemoteAdapter {
516
555
 
517
556
  const streams = {}
518
557
  await asyncMapSettled(Object.keys(vdis), async id => {
519
- streams[`${id}.vhd`] = await createSyntheticStream(handler, join(dir, vhds[id]))
558
+ streams[`${id}.vhd`] = await this._createSyntheticStream(handler, join(dir, vhds[id]))
520
559
  })
521
560
 
522
561
  return {
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
@@ -333,13 +338,16 @@ exports.VmBackup = class VmBackup {
333
338
 
334
339
  const baseUuidToSrcVdi = new Map()
335
340
  await asyncMap(await baseVm.$getDisks(), async baseRef => {
336
- const snapshotOf = await xapi.getField('VDI', baseRef, 'snapshot_of')
341
+ const [baseUuid, snapshotOf] = await Promise.all([
342
+ xapi.getField('VDI', baseRef, 'uuid'),
343
+ xapi.getField('VDI', baseRef, 'snapshot_of'),
344
+ ])
337
345
  const srcVdi = srcVdis[snapshotOf]
338
346
  if (srcVdi !== undefined) {
339
- baseUuidToSrcVdi.set(await xapi.getField('VDI', baseRef, 'uuid'), srcVdi)
347
+ baseUuidToSrcVdi.set(baseUuid, srcVdi)
340
348
  } else {
341
- debug('no base VDI found', {
342
- vdi: srcVdi.uuid,
349
+ debug('ignore snapshot VDI because no longer present on VM', {
350
+ vdi: baseUuid,
343
351
  })
344
352
  }
345
353
  })
@@ -351,6 +359,11 @@ exports.VmBackup = class VmBackup {
351
359
  false
352
360
  )
353
361
 
362
+ if (presentBaseVdis.size === 0) {
363
+ debug('no base VM found')
364
+ return
365
+ }
366
+
354
367
  const fullVdisRequired = new Set()
355
368
  baseUuidToSrcVdi.forEach((srcVdi, baseUuid) => {
356
369
  if (presentBaseVdis.has(baseUuid)) {
package/_cleanVm.js CHANGED
@@ -1,13 +1,14 @@
1
1
  const assert = require('assert')
2
2
  const sum = require('lodash/sum')
3
3
  const { asyncMap } = require('@xen-orchestra/async-map')
4
- const { VhdFile, mergeVhd } = require('vhd-lib')
4
+ const { Constants, mergeVhd, openVhd, VhdAbstract, VhdFile } = require('vhd-lib')
5
5
  const { dirname, resolve } = require('path')
6
- const { DISK_TYPE_DIFFERENCING } = require('vhd-lib/dist/_constants.js')
6
+ const { DISK_TYPES } = Constants
7
7
  const { isMetadataFile, isVhdFile, isXvaFile, isXvaSumFile } = require('./_backupType.js')
8
8
  const { limitConcurrency } = require('limit-concurrency-decorator')
9
9
 
10
10
  const { Task } = require('./Task.js')
11
+ const { Disposable } = require('promise-toolbox')
11
12
 
12
13
  // chain is an array of VHDs from child to parent
13
14
  //
@@ -65,12 +66,12 @@ async function mergeVhdChain(chain, { handler, onLog, remove, merge }) {
65
66
  clearInterval(handle)
66
67
 
67
68
  await Promise.all([
68
- handler.rename(parent, child),
69
+ VhdAbstract.rename(handler, parent, child),
69
70
  asyncMap(children.slice(0, -1), child => {
70
71
  onLog(`the VHD ${child} is unused`)
71
72
  if (remove) {
72
73
  onLog(`deleting unused VHD ${child}`)
73
- return handler.unlink(child)
74
+ return VhdAbstract.unlink(handler, child)
74
75
  }
75
76
  }),
76
77
  ])
@@ -137,53 +138,55 @@ exports.cleanVm = async function cleanVm(
137
138
  // remove broken VHDs
138
139
  await asyncMap(vhdsList.vhds, async path => {
139
140
  try {
140
- const vhd = new VhdFile(handler, path)
141
- await vhd.readHeaderAndFooter(!vhdsList.interruptedVhds.has(path))
142
- vhds.add(path)
143
- if (vhd.footer.diskType === DISK_TYPE_DIFFERENCING) {
144
- const parent = resolve('/', dirname(path), vhd.header.parentUnicodeName)
145
- vhdParents[path] = parent
146
- if (parent in vhdChildren) {
147
- const error = new Error('this script does not support multiple VHD children')
148
- error.parent = parent
149
- error.child1 = vhdChildren[parent]
150
- error.child2 = path
151
- throw error // should we throw?
141
+ await Disposable.use(openVhd(handler, path, { checkSecondFooter: !vhdsList.interruptedVhds.has(path) }), vhd => {
142
+ vhds.add(path)
143
+ if (vhd.footer.diskType === DISK_TYPES.DIFFERENCING) {
144
+ const parent = resolve('/', dirname(path), vhd.header.parentUnicodeName)
145
+ vhdParents[path] = parent
146
+ if (parent in vhdChildren) {
147
+ const error = new Error('this script does not support multiple VHD children')
148
+ error.parent = parent
149
+ error.child1 = vhdChildren[parent]
150
+ error.child2 = path
151
+ throw error // should we throw?
152
+ }
153
+ vhdChildren[parent] = path
152
154
  }
153
- vhdChildren[parent] = path
154
- }
155
+ })
155
156
  } catch (error) {
156
157
  onLog(`error while checking the VHD with path ${path}`, { error })
157
158
  if (error?.code === 'ERR_ASSERTION' && remove) {
158
159
  onLog(`deleting broken ${path}`)
159
- await handler.unlink(path)
160
+ return VhdAbstract.unlink(handler, path)
160
161
  }
161
162
  }
162
163
  })
163
164
 
165
+ // @todo : add check for data folder of alias not referenced in a valid alias
166
+
164
167
  // remove VHDs with missing ancestors
165
168
  {
166
169
  const deletions = []
167
170
 
168
171
  // return true if the VHD has been deleted or is missing
169
- const deleteIfOrphan = vhd => {
170
- const parent = vhdParents[vhd]
172
+ const deleteIfOrphan = vhdPath => {
173
+ const parent = vhdParents[vhdPath]
171
174
  if (parent === undefined) {
172
175
  return
173
176
  }
174
177
 
175
178
  // no longer needs to be checked
176
- delete vhdParents[vhd]
179
+ delete vhdParents[vhdPath]
177
180
 
178
181
  deleteIfOrphan(parent)
179
182
 
180
183
  if (!vhds.has(parent)) {
181
- vhds.delete(vhd)
184
+ vhds.delete(vhdPath)
182
185
 
183
- onLog(`the parent ${parent} of the VHD ${vhd} is missing`)
186
+ onLog(`the parent ${parent} of the VHD ${vhdPath} is missing`)
184
187
  if (remove) {
185
- onLog(`deleting orphan VHD ${vhd}`)
186
- deletions.push(handler.unlink(vhd))
188
+ onLog(`deleting orphan VHD ${vhdPath}`)
189
+ deletions.push(VhdAbstract.unlink(handler, vhdPath))
187
190
  }
188
191
  }
189
192
  }
@@ -254,16 +257,30 @@ exports.cleanVm = async function cleanVm(
254
257
  return Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
255
258
  })()
256
259
 
260
+ const missingVhds = linkedVhds.filter(_ => !vhds.has(_))
261
+
257
262
  // FIXME: find better approach by keeping as much of the backup as
258
263
  // possible (existing disks) even if one disk is missing
259
- if (linkedVhds.every(_ => vhds.has(_))) {
264
+ if (missingVhds.length === 0) {
260
265
  linkedVhds.forEach(_ => unusedVhds.delete(_))
261
266
 
262
- size = await asyncMap(linkedVhds, vhd => handler.getSize(vhd)).then(sum, error => {
263
- onLog(`failed to get size of ${json}`, { error })
264
- })
267
+ // checking the size of a vhd directory is costly
268
+ // 1 Http Query per 1000 blocks
269
+ // we only check size of all the vhd are VhdFiles
270
+
271
+ const shouldComputeSize = linkedVhds.every(vhd => vhd instanceof VhdFile)
272
+ if (shouldComputeSize) {
273
+ try {
274
+ await Disposable.use(Disposable.all(linkedVhds.map(vhdPath => openVhd(handler, vhdPath))), async vhds => {
275
+ const sizes = await asyncMap(vhds, vhd => vhd.getSize())
276
+ size = sum(sizes)
277
+ })
278
+ } catch (error) {
279
+ onLog(`failed to get size of ${json}`, { error })
280
+ }
281
+ }
265
282
  } else {
266
- onLog(`Some VHDs linked to the metadata ${json} are missing`)
283
+ onLog(`Some VHDs linked to the metadata ${json} are missing`, { missingVhds })
267
284
  if (remove) {
268
285
  onLog(`deleting incomplete backup ${json}`)
269
286
  await handler.unlink(json)
@@ -324,7 +341,7 @@ exports.cleanVm = async function cleanVm(
324
341
  onLog(`the VHD ${vhd} is unused`)
325
342
  if (remove) {
326
343
  onLog(`deleting unused VHD ${vhd}`)
327
- unusedVhdsDeletion.push(handler.unlink(vhd))
344
+ unusedVhdsDeletion.push(VhdAbstract.unlink(handler, vhd))
328
345
  }
329
346
  }
330
347
 
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.15.1",
11
+ "version": "0.17.0",
12
12
  "engines": {
13
13
  "node": ">=14.6"
14
14
  },
@@ -16,14 +16,14 @@
16
16
  "postversion": "npm publish --access public"
17
17
  },
18
18
  "dependencies": {
19
- "@vates/compose": "^2.0.0",
19
+ "@vates/compose": "^2.1.0",
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.18.0",
23
+ "@xen-orchestra/fs": "^0.19.2",
24
24
  "@xen-orchestra/log": "^0.3.0",
25
25
  "@xen-orchestra/template": "^0.1.0",
26
- "compare-versions": "^3.6.0",
26
+ "compare-versions": "^4.0.1",
27
27
  "d3-time-format": "^3.0.0",
28
28
  "end-of-stream": "^1.4.4",
29
29
  "fs-extra": "^10.0.0",
@@ -35,11 +35,12 @@
35
35
  "promise-toolbox": "^0.20.0",
36
36
  "proper-lockfile": "^4.1.2",
37
37
  "pump": "^3.0.0",
38
- "vhd-lib": "^1.3.0",
38
+ "uuid": "^8.3.2",
39
+ "vhd-lib": "^2.0.4",
39
40
  "yazl": "^2.5.1"
40
41
  },
41
42
  "peerDependencies": {
42
- "@xen-orchestra/xapi": "^0.8.0"
43
+ "@xen-orchestra/xapi": "^0.8.4"
43
44
  },
44
45
  "license": "AGPL-3.0-or-later",
45
46
  "author": {
@@ -3,7 +3,7 @@ const map = require('lodash/map.js')
3
3
  const mapValues = require('lodash/mapValues.js')
4
4
  const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
5
5
  const { asyncMap } = require('@xen-orchestra/async-map')
6
- const { chainVhd, checkVhdChain, VhdFile } = require('vhd-lib')
6
+ const { chainVhd, checkVhdChain, openVhd, VhdAbstract } = require('vhd-lib')
7
7
  const { createLogger } = require('@xen-orchestra/log')
8
8
  const { dirname } = require('path')
9
9
 
@@ -16,6 +16,7 @@ const { MixinBackupWriter } = require('./_MixinBackupWriter.js')
16
16
  const { AbstractDeltaWriter } = require('./_AbstractDeltaWriter.js')
17
17
  const { checkVhd } = require('./_checkVhd.js')
18
18
  const { packUuid } = require('./_packUuid.js')
19
+ const { Disposable } = require('promise-toolbox')
19
20
 
20
21
  const { warn } = createLogger('xo:backups:DeltaBackupWriter')
21
22
 
@@ -37,13 +38,13 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
37
38
  await asyncMap(vhds, async path => {
38
39
  try {
39
40
  await checkVhdChain(handler, path)
40
-
41
- const vhd = new VhdFile(handler, path)
42
- await vhd.readHeaderAndFooter()
43
- found = found || vhd.footer.uuid.equals(packUuid(baseUuid))
41
+ await Disposable.use(
42
+ openVhd(handler, path),
43
+ vhd => (found = found || vhd.footer.uuid.equals(packUuid(baseUuid)))
44
+ )
44
45
  } catch (error) {
45
46
  warn('checkBaseVdis', { error })
46
- await ignoreErrors.call(handler.unlink(path))
47
+ await ignoreErrors.call(VhdAbstract.unlink(handler, path))
47
48
  }
48
49
  })
49
50
  } catch (error) {
@@ -144,7 +145,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
144
145
  // don't do delta for it
145
146
  vdi.uuid
146
147
  : vdi.$snapshot_of$uuid
147
- }/${basename}.vhd`
148
+ }/${adapter.getVhdFileName(basename)}`
148
149
  )
149
150
 
150
151
  const metadataFilename = `${backupDir}/${basename}.json`
@@ -188,7 +189,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
188
189
  await checkVhd(handler, parentPath)
189
190
  }
190
191
 
191
- await adapter.outputStream(path, deltaExport.streams[`${id}.vhd`], {
192
+ await adapter.writeVhd(path, deltaExport.streams[`${id}.vhd`], {
192
193
  // no checksum for VHDs, because they will be invalidated by
193
194
  // merges and chainings
194
195
  checksum: false,
@@ -200,11 +201,11 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
200
201
  }
201
202
 
202
203
  // set the correct UUID in the VHD
203
- const vhd = new VhdFile(handler, path)
204
- await vhd.readHeaderAndFooter()
205
- vhd.footer.uuid = packUuid(vdi.uuid)
206
- await vhd.readBlockAllocationTable() // required by writeFooter()
207
- await vhd.writeFooter()
204
+ await Disposable.use(openVhd(handler, path), async vhd => {
205
+ vhd.footer.uuid = packUuid(vdi.uuid)
206
+ await vhd.readBlockAllocationTable() // required by writeFooter()
207
+ await vhd.writeFooter()
208
+ })
208
209
  })
209
210
  )
210
211
  return {
@@ -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
- return this._adapter
26
- .cleanVm(this.#vmBackupDir, { ...options, fixMetadata: true, onLog: warn, lock: false })
27
- .catch(warn)
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
- await handler.outputFile(join(MergeWorker.CLEAN_VM_QUEUE, formatFilenameDate(new Date())), this._backup.vm.uuid)
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
  }
@@ -1,5 +1,6 @@
1
- const Vhd = require('vhd-lib').VhdFile
1
+ const openVhd = require('vhd-lib').openVhd
2
+ const Disposable = require('promise-toolbox/Disposable')
2
3
 
3
4
  exports.checkVhd = async function checkVhd(handler, path) {
4
- await new Vhd(handler, path).readHeaderAndFooter()
5
+ await Disposable.use(openVhd(handler, path), () => {})
5
6
  }