@xen-orchestra/backups 0.14.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.
Files changed (43) hide show
  1. package/Backup.js +263 -0
  2. package/DurablePartition.js +40 -0
  3. package/ImportVmBackup.js +66 -0
  4. package/README.md +28 -0
  5. package/RemoteAdapter.js +552 -0
  6. package/RestoreMetadataBackup.js +24 -0
  7. package/Task.js +151 -0
  8. package/_PoolMetadataBackup.js +75 -0
  9. package/_VmBackup.js +409 -0
  10. package/_XoMetadataBackup.js +62 -0
  11. package/_backupType.js +4 -0
  12. package/_backupWorker.js +155 -0
  13. package/_cancelableMap.js +20 -0
  14. package/_cleanVm.js +378 -0
  15. package/_deltaVm.js +347 -0
  16. package/_extractIdsFromSimplePattern.js +29 -0
  17. package/_filenameDate.js +6 -0
  18. package/_forkStreamUnpipe.js +28 -0
  19. package/_getOldEntries.js +4 -0
  20. package/_getTmpDir.js +20 -0
  21. package/_getVmBackupDir.js +6 -0
  22. package/_isValidXva.js +60 -0
  23. package/_listPartitions.js +52 -0
  24. package/_lvm.js +31 -0
  25. package/_watchStreamSize.js +7 -0
  26. package/formatVmBackups.js +34 -0
  27. package/merge-worker/cli.js +69 -0
  28. package/merge-worker/index.js +25 -0
  29. package/package.json +49 -0
  30. package/parseMetadataBackupId.js +23 -0
  31. package/runBackupWorker.js +38 -0
  32. package/writers/DeltaBackupWriter.js +221 -0
  33. package/writers/DeltaReplicationWriter.js +126 -0
  34. package/writers/FullBackupWriter.js +85 -0
  35. package/writers/FullReplicationWriter.js +88 -0
  36. package/writers/_AbstractDeltaWriter.js +26 -0
  37. package/writers/_AbstractFullWriter.js +12 -0
  38. package/writers/_AbstractWriter.js +10 -0
  39. package/writers/_MixinBackupWriter.js +51 -0
  40. package/writers/_MixinReplicationWriter.js +8 -0
  41. package/writers/_checkVhd.js +5 -0
  42. package/writers/_listReplicatedVms.js +30 -0
  43. package/writers/_packUuid.js +5 -0
@@ -0,0 +1,552 @@
1
+ const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
2
+ const Disposable = require('promise-toolbox/Disposable.js')
3
+ const fromCallback = require('promise-toolbox/fromCallback.js')
4
+ const fromEvent = require('promise-toolbox/fromEvent.js')
5
+ const pDefer = require('promise-toolbox/defer.js')
6
+ const pump = require('pump')
7
+ const { basename, dirname, join, normalize, resolve } = require('path')
8
+ const { createLogger } = require('@xen-orchestra/log')
9
+ const { createSyntheticStream, mergeVhd, default: Vhd } = require('vhd-lib')
10
+ const { deduped } = require('@vates/disposable/deduped.js')
11
+ const { execFile } = require('child_process')
12
+ const { readdir, stat } = require('fs-extra')
13
+ const { ZipFile } = require('yazl')
14
+
15
+ const { BACKUP_DIR } = require('./_getVmBackupDir.js')
16
+ const { cleanVm } = require('./_cleanVm.js')
17
+ const { getTmpDir } = require('./_getTmpDir.js')
18
+ const { isMetadataFile, isVhdFile } = require('./_backupType.js')
19
+ const { isValidXva } = require('./_isValidXva.js')
20
+ const { listPartitions, LVM_PARTITION_TYPE } = require('./_listPartitions.js')
21
+ const { lvs, pvs } = require('./_lvm.js')
22
+
23
+ const DIR_XO_CONFIG_BACKUPS = 'xo-config-backups'
24
+ exports.DIR_XO_CONFIG_BACKUPS = DIR_XO_CONFIG_BACKUPS
25
+
26
+ const DIR_XO_POOL_METADATA_BACKUPS = 'xo-pool-metadata-backups'
27
+ exports.DIR_XO_POOL_METADATA_BACKUPS = DIR_XO_POOL_METADATA_BACKUPS
28
+
29
+ const { warn } = createLogger('xo:backups:RemoteAdapter')
30
+
31
+ const compareTimestamp = (a, b) => a.timestamp - b.timestamp
32
+
33
+ const noop = Function.prototype
34
+
35
+ const resolveRelativeFromFile = (file, path) => resolve('/', dirname(file), path).slice(1)
36
+
37
+ const resolveSubpath = (root, path) => resolve(root, `.${resolve('/', path)}`)
38
+
39
+ const RE_VHDI = /^vhdi(\d+)$/
40
+
41
+ async function addDirectory(files, realPath, metadataPath) {
42
+ try {
43
+ const subFiles = await readdir(realPath)
44
+ await asyncMap(subFiles, file => addDirectory(files, realPath + '/' + file, metadataPath + '/' + file))
45
+ } catch (error) {
46
+ if (error == null || error.code !== 'ENOTDIR') {
47
+ throw error
48
+ }
49
+ files.push({
50
+ realPath,
51
+ metadataPath,
52
+ })
53
+ }
54
+ }
55
+
56
+ const createSafeReaddir = (handler, methodName) => (path, options) =>
57
+ handler.list(path, options).catch(error => {
58
+ if (error?.code !== 'ENOENT') {
59
+ warn(`${methodName} ${path}`, { error })
60
+ }
61
+ return []
62
+ })
63
+
64
+ const debounceResourceFactory = factory =>
65
+ function () {
66
+ return this._debounceResource(factory.apply(this, arguments))
67
+ }
68
+
69
+ class RemoteAdapter {
70
+ constructor(handler, { debounceResource = res => res, dirMode } = {}) {
71
+ this._debounceResource = debounceResource
72
+ this._dirMode = dirMode
73
+ this._handler = handler
74
+ }
75
+
76
+ get handler() {
77
+ return this._handler
78
+ }
79
+
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 Vhd(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
+ async _findPartition(devicePath, partitionId) {
123
+ const partitions = await listPartitions(devicePath)
124
+ const partition = partitions.find(_ => _.id === partitionId)
125
+ if (partition === undefined) {
126
+ throw new Error(`partition ${partitionId} not found`)
127
+ }
128
+ return partition
129
+ }
130
+
131
+ _getLvmLogicalVolumes = Disposable.factory(this._getLvmLogicalVolumes)
132
+ _getLvmLogicalVolumes = deduped(this._getLvmLogicalVolumes, (devicePath, pvId, vgName) => [devicePath, pvId, vgName])
133
+ _getLvmLogicalVolumes = debounceResourceFactory(this._getLvmLogicalVolumes)
134
+ async *_getLvmLogicalVolumes(devicePath, pvId, vgName) {
135
+ yield this._getLvmPhysicalVolume(devicePath, pvId && (await this._findPartition(devicePath, pvId)))
136
+
137
+ await fromCallback(execFile, 'vgchange', ['-ay', vgName])
138
+ try {
139
+ yield lvs(['lv_name', 'lv_path'], vgName)
140
+ } finally {
141
+ await fromCallback(execFile, 'vgchange', ['-an', vgName])
142
+ }
143
+ }
144
+
145
+ _getLvmPhysicalVolume = Disposable.factory(this._getLvmPhysicalVolume)
146
+ _getLvmPhysicalVolume = deduped(this._getLvmPhysicalVolume, (devicePath, partition) => [devicePath, partition?.id])
147
+ _getLvmPhysicalVolume = debounceResourceFactory(this._getLvmPhysicalVolume)
148
+ async *_getLvmPhysicalVolume(devicePath, partition) {
149
+ const args = []
150
+ if (partition !== undefined) {
151
+ args.push('-o', partition.start * 512, '--sizelimit', partition.size)
152
+ }
153
+ args.push('--show', '-f', devicePath)
154
+ const path = (await fromCallback(execFile, 'losetup', args)).trim()
155
+ try {
156
+ await fromCallback(execFile, 'pvscan', ['--cache', path])
157
+ yield path
158
+ } finally {
159
+ try {
160
+ const vgNames = await pvs('vg_name', path)
161
+ await fromCallback(execFile, 'vgchange', ['-an', ...vgNames])
162
+ } finally {
163
+ await fromCallback(execFile, 'losetup', ['-d', path])
164
+ }
165
+ }
166
+ }
167
+
168
+ _getPartition = Disposable.factory(this._getPartition)
169
+ _getPartition = deduped(this._getPartition, (devicePath, partition) => [devicePath, partition?.id])
170
+ _getPartition = debounceResourceFactory(this._getPartition)
171
+ async *_getPartition(devicePath, partition) {
172
+ const options = ['loop', 'ro']
173
+
174
+ if (partition !== undefined) {
175
+ const { size, start } = partition
176
+ options.push(`sizelimit=${size}`)
177
+ if (start !== undefined) {
178
+ options.push(`offset=${start * 512}`)
179
+ }
180
+ }
181
+
182
+ const path = yield getTmpDir()
183
+ const mount = options => {
184
+ return fromCallback(execFile, 'mount', [
185
+ `--options=${options.join(',')}`,
186
+ `--source=${devicePath}`,
187
+ `--target=${path}`,
188
+ ])
189
+ }
190
+
191
+ // `norecovery` option is used for ext3/ext4/xfs, if it fails it might be
192
+ // another fs, try without
193
+ try {
194
+ await mount([...options, 'norecovery'])
195
+ } catch (error) {
196
+ await mount(options)
197
+ }
198
+ try {
199
+ yield path
200
+ } finally {
201
+ await fromCallback(execFile, 'umount', ['--lazy', path])
202
+ }
203
+ }
204
+
205
+ _listLvmLogicalVolumes(devicePath, partition, results = []) {
206
+ return Disposable.use(this._getLvmPhysicalVolume(devicePath, partition), async path => {
207
+ const lvs = await pvs(['lv_name', 'lv_path', 'lv_size', 'vg_name'], path)
208
+ const partitionId = partition !== undefined ? partition.id : ''
209
+ lvs.forEach((lv, i) => {
210
+ const name = lv.lv_name
211
+ if (name !== '') {
212
+ results.push({
213
+ id: `${partitionId}/${lv.vg_name}/${name}`,
214
+ name,
215
+ size: lv.lv_size,
216
+ })
217
+ }
218
+ })
219
+ return results
220
+ })
221
+ }
222
+
223
+ _usePartitionFiles = Disposable.factory(this._usePartitionFiles)
224
+ async *_usePartitionFiles(diskId, partitionId, paths) {
225
+ const path = yield this.getPartition(diskId, partitionId)
226
+
227
+ const files = []
228
+ await asyncMap(paths, file =>
229
+ addDirectory(files, resolveSubpath(path, file), normalize('./' + file).replace(/\/+$/, ''))
230
+ )
231
+
232
+ return files
233
+ }
234
+
235
+ fetchPartitionFiles(diskId, partitionId, paths) {
236
+ const { promise, reject, resolve } = pDefer()
237
+ Disposable.use(
238
+ async function* () {
239
+ const files = yield this._usePartitionFiles(diskId, partitionId, paths)
240
+ const zip = new ZipFile()
241
+ files.forEach(({ realPath, metadataPath }) => zip.addFile(realPath, metadataPath))
242
+ zip.end()
243
+ const { outputStream } = zip
244
+ resolve(outputStream)
245
+ await fromEvent(outputStream, 'end')
246
+ }.bind(this)
247
+ ).catch(error => {
248
+ warn(error)
249
+ reject(error)
250
+ })
251
+ return promise
252
+ }
253
+
254
+ async deleteDeltaVmBackups(backups) {
255
+ const handler = this._handler
256
+
257
+ // unused VHDs will be detected by `cleanVm`
258
+ await asyncMapSettled(backups, ({ _filename }) => handler.unlink(_filename))
259
+ }
260
+
261
+ async deleteMetadataBackup(backupId) {
262
+ const uuidReg = '\\w{8}(-\\w{4}){3}-\\w{12}'
263
+ const metadataDirReg = 'xo-(config|pool-metadata)-backups'
264
+ const timestampReg = '\\d{8}T\\d{6}Z'
265
+ const regexp = new RegExp(`^${metadataDirReg}/${uuidReg}(/${uuidReg})?/${timestampReg}`)
266
+ if (!regexp.test(backupId)) {
267
+ throw new Error(`The id (${backupId}) not correspond to a metadata folder`)
268
+ }
269
+
270
+ await this._handler.rmtree(backupId)
271
+ }
272
+
273
+ async deleteOldMetadataBackups(dir, retention) {
274
+ const handler = this.handler
275
+ let list = await handler.list(dir)
276
+ list.sort()
277
+ list = list.filter(timestamp => /^\d{8}T\d{6}Z$/.test(timestamp)).slice(0, -retention)
278
+ await asyncMapSettled(list, timestamp => handler.rmtree(`${dir}/${timestamp}`))
279
+ }
280
+
281
+ async deleteFullVmBackups(backups) {
282
+ const handler = this._handler
283
+ await asyncMapSettled(backups, ({ _filename, xva }) =>
284
+ Promise.all([handler.unlink(_filename), handler.unlink(resolveRelativeFromFile(_filename, xva))])
285
+ )
286
+ }
287
+
288
+ async deleteVmBackup(filename) {
289
+ const metadata = JSON.parse(String(await this._handler.readFile(filename)))
290
+ metadata._filename = filename
291
+
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}`)
298
+ }
299
+ }
300
+
301
+ getDisk = Disposable.factory(this.getDisk)
302
+ getDisk = deduped(this.getDisk, diskId => [diskId])
303
+ getDisk = debounceResourceFactory(this.getDisk)
304
+ async *getDisk(diskId) {
305
+ const handler = this._handler
306
+
307
+ const diskPath = handler._getFilePath('/' + diskId)
308
+ const mountDir = yield getTmpDir()
309
+ await fromCallback(execFile, 'vhdimount', [diskPath, mountDir])
310
+ try {
311
+ let max = 0
312
+ let maxEntry
313
+ const entries = await readdir(mountDir)
314
+ entries.forEach(entry => {
315
+ const matches = RE_VHDI.exec(entry)
316
+ if (matches !== null) {
317
+ const value = +matches[1]
318
+ if (value > max) {
319
+ max = value
320
+ maxEntry = entry
321
+ }
322
+ }
323
+ })
324
+ if (max === 0) {
325
+ throw new Error('no disks found')
326
+ }
327
+
328
+ yield `${mountDir}/${maxEntry}`
329
+ } finally {
330
+ await fromCallback(execFile, 'fusermount', ['-uz', mountDir])
331
+ }
332
+ }
333
+
334
+ // partitionId values:
335
+ //
336
+ // - undefined: raw disk
337
+ // - `<partitionId>`: partitioned disk
338
+ // - `<pvId>/<vgName>/<lvName>`: LVM on a partitioned disk
339
+ // - `/<vgName>/lvName>`: LVM on a raw disk
340
+ getPartition = Disposable.factory(this.getPartition)
341
+ async *getPartition(diskId, partitionId) {
342
+ const devicePath = yield this.getDisk(diskId)
343
+ if (partitionId === undefined) {
344
+ return yield this._getPartition(devicePath)
345
+ }
346
+
347
+ const isLvmPartition = partitionId.includes('/')
348
+ if (isLvmPartition) {
349
+ const [pvId, vgName, lvName] = partitionId.split('/')
350
+ const lvs = yield this._getLvmLogicalVolumes(devicePath, pvId !== '' ? pvId : undefined, vgName)
351
+ return yield this._getPartition(lvs.find(_ => _.lv_name === lvName).lv_path)
352
+ }
353
+
354
+ return yield this._getPartition(devicePath, await this._findPartition(devicePath, partitionId))
355
+ }
356
+
357
+ async listAllVmBackups() {
358
+ const handler = this._handler
359
+
360
+ const backups = { __proto__: null }
361
+ await asyncMap(await handler.list(BACKUP_DIR), async vmUuid => {
362
+ const vmBackups = await this.listVmBackups(vmUuid)
363
+ backups[vmUuid] = vmBackups
364
+ })
365
+
366
+ return backups
367
+ }
368
+
369
+ listPartitionFiles(diskId, partitionId, path) {
370
+ return Disposable.use(this.getPartition(diskId, partitionId), async rootPath => {
371
+ path = resolveSubpath(rootPath, path)
372
+
373
+ const entriesMap = {}
374
+ await asyncMap(await readdir(path), async name => {
375
+ try {
376
+ const stats = await stat(`${path}/${name}`)
377
+ entriesMap[stats.isDirectory() ? `${name}/` : name] = {}
378
+ } catch (error) {
379
+ if (error == null || error.code !== 'ENOENT') {
380
+ throw error
381
+ }
382
+ }
383
+ })
384
+
385
+ return entriesMap
386
+ })
387
+ }
388
+
389
+ listPartitions(diskId) {
390
+ return Disposable.use(this.getDisk(diskId), async devicePath => {
391
+ const partitions = await listPartitions(devicePath)
392
+
393
+ if (partitions.length === 0) {
394
+ try {
395
+ // handle potential raw LVM physical volume
396
+ return await this._listLvmLogicalVolumes(devicePath, undefined, partitions)
397
+ } catch (error) {
398
+ return []
399
+ }
400
+ }
401
+
402
+ const results = []
403
+ await asyncMapSettled(partitions, partition =>
404
+ partition.type === LVM_PARTITION_TYPE
405
+ ? this._listLvmLogicalVolumes(devicePath, partition, results)
406
+ : results.push(partition)
407
+ )
408
+ return results
409
+ })
410
+ }
411
+
412
+ async listPoolMetadataBackups() {
413
+ const handler = this._handler
414
+ const safeReaddir = createSafeReaddir(handler, 'listPoolMetadataBackups')
415
+
416
+ const backupsByPool = {}
417
+ await asyncMap(await safeReaddir(DIR_XO_POOL_METADATA_BACKUPS, { prependDir: true }), async scheduleDir =>
418
+ asyncMap(await safeReaddir(scheduleDir), async poolId => {
419
+ const backups = backupsByPool[poolId] ?? (backupsByPool[poolId] = [])
420
+ return asyncMap(await safeReaddir(`${scheduleDir}/${poolId}`, { prependDir: true }), async backupDir => {
421
+ try {
422
+ backups.push({
423
+ id: backupDir,
424
+ ...JSON.parse(String(await handler.readFile(`${backupDir}/metadata.json`))),
425
+ })
426
+ } catch (error) {
427
+ warn(`listPoolMetadataBackups ${backupDir}`, {
428
+ error,
429
+ })
430
+ }
431
+ })
432
+ })
433
+ )
434
+
435
+ // delete empty entries and sort backups
436
+ Object.keys(backupsByPool).forEach(poolId => {
437
+ const backups = backupsByPool[poolId]
438
+ if (backups.length === 0) {
439
+ delete backupsByPool[poolId]
440
+ } else {
441
+ backups.sort(compareTimestamp)
442
+ }
443
+ })
444
+
445
+ return backupsByPool
446
+ }
447
+
448
+ async listVmBackups(vmUuid, predicate) {
449
+ const handler = this._handler
450
+ const backups = []
451
+
452
+ try {
453
+ const files = await handler.list(`${BACKUP_DIR}/${vmUuid}`, {
454
+ filter: isMetadataFile,
455
+ prependDir: true,
456
+ })
457
+ await asyncMap(files, async file => {
458
+ try {
459
+ const metadata = await this.readVmBackupMetadata(file)
460
+ if (predicate === undefined || predicate(metadata)) {
461
+ // inject an id usable by importVmBackupNg()
462
+ metadata.id = metadata._filename
463
+
464
+ backups.push(metadata)
465
+ }
466
+ } catch (error) {
467
+ warn(`listVmBackups ${file}`, { error })
468
+ }
469
+ })
470
+ } catch (error) {
471
+ let code
472
+ if (error == null || ((code = error.code) !== 'ENOENT' && code !== 'ENOTDIR')) {
473
+ throw error
474
+ }
475
+ }
476
+
477
+ return backups.sort(compareTimestamp)
478
+ }
479
+
480
+ async listXoMetadataBackups() {
481
+ const handler = this._handler
482
+ const safeReaddir = createSafeReaddir(handler, 'listXoMetadataBackups')
483
+
484
+ const backups = []
485
+ await asyncMap(await safeReaddir(DIR_XO_CONFIG_BACKUPS, { prependDir: true }), async scheduleDir =>
486
+ asyncMap(await safeReaddir(scheduleDir, { prependDir: true }), async backupDir => {
487
+ try {
488
+ backups.push({
489
+ id: backupDir,
490
+ ...JSON.parse(String(await handler.readFile(`${backupDir}/metadata.json`))),
491
+ })
492
+ } catch (error) {
493
+ warn(`listXoMetadataBackups ${backupDir}`, { error })
494
+ }
495
+ })
496
+ )
497
+
498
+ return backups.sort(compareTimestamp)
499
+ }
500
+
501
+ async outputStream(path, input, { checksum = true, validator = noop } = {}) {
502
+ await this._handler.outputStream(path, input, {
503
+ checksum,
504
+ dirMode: this._dirMode,
505
+ async validator() {
506
+ await input.task
507
+ return validator.apply(this, arguments)
508
+ },
509
+ })
510
+ }
511
+
512
+ async readDeltaVmBackup(metadata) {
513
+ const handler = this._handler
514
+ const { vbds, vdis, vhds, vifs, vm } = metadata
515
+ const dir = dirname(metadata._filename)
516
+
517
+ const streams = {}
518
+ await asyncMapSettled(Object.keys(vdis), async id => {
519
+ streams[`${id}.vhd`] = await createSyntheticStream(handler, join(dir, vhds[id]))
520
+ })
521
+
522
+ return {
523
+ streams,
524
+ vbds,
525
+ vdis,
526
+ version: '1.0.0',
527
+ vifs,
528
+ vm,
529
+ }
530
+ }
531
+
532
+ readFullVmBackup(metadata) {
533
+ return this._handler.createReadStream(resolve('/', dirname(metadata._filename), metadata.xva))
534
+ }
535
+
536
+ async readVmBackupMetadata(path) {
537
+ return Object.defineProperty(JSON.parse(await this._handler.readFile(path)), '_filename', { value: path })
538
+ }
539
+ }
540
+
541
+ Object.assign(RemoteAdapter.prototype, {
542
+ cleanVm(vmDir, { lock = true } = {}) {
543
+ if (lock) {
544
+ return Disposable.use(this._handler.lock(vmDir), () => cleanVm.apply(this, arguments))
545
+ } else {
546
+ return cleanVm.apply(this, arguments)
547
+ }
548
+ },
549
+ isValidXva,
550
+ })
551
+
552
+ exports.RemoteAdapter = RemoteAdapter
@@ -0,0 +1,24 @@
1
+ const { DIR_XO_POOL_METADATA_BACKUPS } = require('./RemoteAdapter.js')
2
+ const { PATH_DB_DUMP } = require('./_PoolMetadataBackup.js')
3
+
4
+ exports.RestoreMetadataBackup = class RestoreMetadataBackup {
5
+ constructor({ backupId, handler, xapi }) {
6
+ this._backupId = backupId
7
+ this._handler = handler
8
+ this._xapi = xapi
9
+ }
10
+
11
+ async run() {
12
+ const backupId = this._backupId
13
+ const handler = this._handler
14
+ const xapi = this._xapi
15
+
16
+ if (backupId.split('/')[0] === DIR_XO_POOL_METADATA_BACKUPS) {
17
+ return xapi.putResource(await handler.createReadStream(`${backupId}/data`), PATH_DB_DUMP, {
18
+ task: xapi.task_create('Import pool metadata'),
19
+ })
20
+ } else {
21
+ return String(await handler.readFile(`${backupId}/data.json`))
22
+ }
23
+ }
24
+ }