@xen-orchestra/backups 0.29.1 → 0.29.3

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
@@ -232,21 +232,23 @@ class RemoteAdapter {
232
232
  return promise
233
233
  }
234
234
 
235
- #removeVmBackupsFromCache(backups) {
236
- for (const [dir, filenames] of Object.entries(
237
- groupBy(
238
- backups.map(_ => _._filename),
239
- dirname
240
- )
241
- )) {
242
- // detached async action, will not reject
243
- this._updateCache(dir + '/cache.json.gz', backups => {
244
- for (const filename of filenames) {
245
- debug('removing cache entry', { entry: filename })
246
- delete backups[filename]
247
- }
248
- })
249
- }
235
+ async #removeVmBackupsFromCache(backups) {
236
+ await asyncEach(
237
+ Object.entries(
238
+ groupBy(
239
+ backups.map(_ => _._filename),
240
+ dirname
241
+ )
242
+ ),
243
+ ([dir, filenames]) =>
244
+ // will not reject
245
+ this._updateCache(dir + '/cache.json.gz', backups => {
246
+ for (const filename of filenames) {
247
+ debug('removing cache entry', { entry: filename })
248
+ delete backups[filename]
249
+ }
250
+ })
251
+ )
250
252
  }
251
253
 
252
254
  async deleteDeltaVmBackups(backups) {
@@ -255,7 +257,7 @@ class RemoteAdapter {
255
257
  // this will delete the json, unused VHDs will be detected by `cleanVm`
256
258
  await asyncMapSettled(backups, ({ _filename }) => handler.unlink(_filename))
257
259
 
258
- this.#removeVmBackupsFromCache(backups)
260
+ await this.#removeVmBackupsFromCache(backups)
259
261
  }
260
262
 
261
263
  async deleteMetadataBackup(backupId) {
@@ -284,7 +286,7 @@ class RemoteAdapter {
284
286
  Promise.all([handler.unlink(_filename), handler.unlink(resolveRelativeFromFile(_filename, xva))])
285
287
  )
286
288
 
287
- this.#removeVmBackupsFromCache(backups)
289
+ await this.#removeVmBackupsFromCache(backups)
288
290
  }
289
291
 
290
292
  deleteVmBackup(file) {
@@ -508,7 +510,7 @@ class RemoteAdapter {
508
510
  return `${BACKUP_DIR}/${vmUuid}/cache.json.gz`
509
511
  }
510
512
 
511
- async #readCache(path) {
513
+ async _readCache(path) {
512
514
  try {
513
515
  return JSON.parse(await fromCallback(zlib.gunzip, await this.handler.readFile(path)))
514
516
  } catch (error) {
@@ -521,15 +523,15 @@ class RemoteAdapter {
521
523
  _updateCache = synchronized.withKey()(this._updateCache)
522
524
  // eslint-disable-next-line no-dupe-class-members
523
525
  async _updateCache(path, fn) {
524
- const cache = await this.#readCache(path)
526
+ const cache = await this._readCache(path)
525
527
  if (cache !== undefined) {
526
528
  fn(cache)
527
529
 
528
- await this.#writeCache(path, cache)
530
+ await this._writeCache(path, cache)
529
531
  }
530
532
  }
531
533
 
532
- async #writeCache(path, data) {
534
+ async _writeCache(path, data) {
533
535
  try {
534
536
  await this.handler.writeFile(path, await fromCallback(zlib.gzip, JSON.stringify(data)), { flags: 'w' })
535
537
  } catch (error) {
@@ -577,7 +579,7 @@ class RemoteAdapter {
577
579
  async _readCacheListVmBackups(vmUuid) {
578
580
  const path = this.#getVmBackupsCache(vmUuid)
579
581
 
580
- const cache = await this.#readCache(path)
582
+ const cache = await this._readCache(path)
581
583
  if (cache !== undefined) {
582
584
  debug('found VM backups cache, using it', { path })
583
585
  return cache
@@ -590,7 +592,7 @@ class RemoteAdapter {
590
592
  }
591
593
 
592
594
  // detached async action, will not reject
593
- this.#writeCache(path, backups)
595
+ this._writeCache(path, backups)
594
596
 
595
597
  return backups
596
598
  }
@@ -641,7 +643,7 @@ class RemoteAdapter {
641
643
  })
642
644
 
643
645
  // will not throw
644
- this._updateCache(this.#getVmBackupsCache(vmUuid), backups => {
646
+ await this._updateCache(this.#getVmBackupsCache(vmUuid), backups => {
645
647
  debug('adding cache entry', { entry: path })
646
648
  backups[path] = {
647
649
  ...metadata,
package/_cleanVm.js CHANGED
@@ -311,7 +311,6 @@ exports.cleanVm = async function cleanVm(
311
311
  }
312
312
 
313
313
  const jsons = new Set()
314
- let mustInvalidateCache = false
315
314
  const xvas = new Set()
316
315
  const xvaSums = []
317
316
  const entries = await handler.list(vmDir, {
@@ -327,6 +326,20 @@ exports.cleanVm = async function cleanVm(
327
326
  }
328
327
  })
329
328
 
329
+ const cachePath = vmDir + '/cache.json.gz'
330
+
331
+ let mustRegenerateCache
332
+ {
333
+ const cache = await this._readCache(cachePath)
334
+ const actual = cache === undefined ? 0 : Object.keys(cache).length
335
+ const expected = jsons.size
336
+
337
+ mustRegenerateCache = actual !== expected
338
+ if (mustRegenerateCache) {
339
+ logWarn('unexpected number of entries in backup cache', { path: cachePath, actual, expected })
340
+ }
341
+ }
342
+
330
343
  await asyncMap(xvas, async path => {
331
344
  // check is not good enough to delete the file, the best we can do is report
332
345
  // it
@@ -338,6 +351,8 @@ exports.cleanVm = async function cleanVm(
338
351
  const unusedVhds = new Set(vhds)
339
352
  const unusedXvas = new Set(xvas)
340
353
 
354
+ const backups = new Map()
355
+
341
356
  // compile the list of unused XVAs and VHDs, and remove backup metadata which
342
357
  // reference a missing XVA/VHD
343
358
  await asyncMap(jsons, async json => {
@@ -350,19 +365,16 @@ exports.cleanVm = async function cleanVm(
350
365
  return
351
366
  }
352
367
 
368
+ let isBackupComplete
369
+
353
370
  const { mode } = metadata
354
371
  if (mode === 'full') {
355
372
  const linkedXva = resolve('/', vmDir, metadata.xva)
356
- if (xvas.has(linkedXva)) {
373
+ isBackupComplete = xvas.has(linkedXva)
374
+ if (isBackupComplete) {
357
375
  unusedXvas.delete(linkedXva)
358
376
  } else {
359
377
  logWarn('the XVA linked to the backup is missing', { backup: json, xva: linkedXva })
360
- if (remove) {
361
- logInfo('deleting incomplete backup', { path: json })
362
- jsons.delete(json)
363
- mustInvalidateCache = true
364
- await handler.unlink(json)
365
- }
366
378
  }
367
379
  } else if (mode === 'delta') {
368
380
  const linkedVhds = (() => {
@@ -371,22 +383,28 @@ exports.cleanVm = async function cleanVm(
371
383
  })()
372
384
 
373
385
  const missingVhds = linkedVhds.filter(_ => !vhds.has(_))
386
+ isBackupComplete = missingVhds.length === 0
374
387
 
375
388
  // FIXME: find better approach by keeping as much of the backup as
376
389
  // possible (existing disks) even if one disk is missing
377
- if (missingVhds.length === 0) {
390
+ if (isBackupComplete) {
378
391
  linkedVhds.forEach(_ => unusedVhds.delete(_))
379
392
  linkedVhds.forEach(path => {
380
393
  vhdsToJSons[path] = json
381
394
  })
382
395
  } else {
383
396
  logWarn('some VHDs linked to the backup are missing', { backup: json, missingVhds })
384
- if (remove) {
385
- logInfo('deleting incomplete backup', { path: json })
386
- mustInvalidateCache = true
387
- jsons.delete(json)
388
- await handler.unlink(json)
389
- }
397
+ }
398
+ }
399
+
400
+ if (isBackupComplete) {
401
+ backups.set(json, metadata)
402
+ } else {
403
+ jsons.delete(json)
404
+ if (remove) {
405
+ logInfo('deleting incomplete backup', { backup: json })
406
+ mustRegenerateCache = true
407
+ await handler.unlink(json)
390
408
  }
391
409
  }
392
410
  })
@@ -496,7 +514,7 @@ exports.cleanVm = async function cleanVm(
496
514
  // check for the other that the size is the same as the real file size
497
515
 
498
516
  await asyncMap(jsons, async metadataPath => {
499
- const metadata = JSON.parse(await handler.readFile(metadataPath))
517
+ const metadata = backups.get(metadataPath)
500
518
 
501
519
  let fileSystemSize
502
520
  const merged = metadataWithMergedVhd[metadataPath] !== undefined
@@ -538,6 +556,7 @@ exports.cleanVm = async function cleanVm(
538
556
  // systematically update size after a merge
539
557
  if ((merged || fixMetadata) && size !== fileSystemSize) {
540
558
  metadata.size = fileSystemSize
559
+ mustRegenerateCache = true
541
560
  try {
542
561
  await handler.writeFile(metadataPath, JSON.stringify(metadata), { flags: 'w' })
543
562
  } catch (error) {
@@ -546,9 +565,16 @@ exports.cleanVm = async function cleanVm(
546
565
  }
547
566
  })
548
567
 
549
- // purge cache if a metadata file has been deleted
550
- if (mustInvalidateCache) {
551
- await handler.unlink(vmDir + '/cache.json.gz')
568
+ if (mustRegenerateCache) {
569
+ const cache = {}
570
+ for (const [path, content] of backups.entries()) {
571
+ cache[path] = {
572
+ _filename: path,
573
+ id: path,
574
+ ...content,
575
+ }
576
+ }
577
+ await this._writeCache(cachePath, cache)
552
578
  }
553
579
 
554
580
  return {
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.29.1",
11
+ "version": "0.29.3",
12
12
  "engines": {
13
13
  "node": ">=14.6"
14
14
  },
@@ -52,7 +52,7 @@
52
52
  "tmp": "^0.2.1"
53
53
  },
54
54
  "peerDependencies": {
55
- "@xen-orchestra/xapi": "^1.5.2"
55
+ "@xen-orchestra/xapi": "^1.5.3"
56
56
  },
57
57
  "license": "AGPL-3.0-or-later",
58
58
  "author": {