@xen-orchestra/backups 0.68.1 → 0.69.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.
@@ -0,0 +1,204 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @typedef {import('@xen-orchestra/disk-transform').DiskBlock} DiskBlock
5
+ */
6
+
7
+ import { RandomAccessDisk } from '@xen-orchestra/disk-transform'
8
+ export class RemoteDisk extends RandomAccessDisk {
9
+ /**
10
+ * Abstract
11
+ * @param {Object} options
12
+ * @param {boolean} options.force
13
+ * @returns {Promise<void>}
14
+ */
15
+ async init(options = { force: false }) {
16
+ throw new Error(`init must be implemented`)
17
+ }
18
+
19
+ /**
20
+ * Abstract
21
+ * @returns {Promise<void>}
22
+ */
23
+ async close() {
24
+ throw new Error(`close must be implemented`)
25
+ }
26
+
27
+ /**
28
+ * Abstract
29
+ * @returns {number}
30
+ */
31
+ getVirtualSize() {
32
+ throw new Error(`getVirtualSize must be implemented`)
33
+ }
34
+
35
+ /**
36
+ * Abstract
37
+ * @returns {number} size
38
+ */
39
+ getSizeOnDisk() {
40
+ throw new Error(`getSizeOnDisk must be implemented`)
41
+ }
42
+
43
+ /**
44
+ * @returns {number}
45
+ */
46
+ getBlockSize() {
47
+ throw new Error(`getBlockSize must be implemented`)
48
+ }
49
+
50
+ /**
51
+ * Abstract
52
+ * @returns {string}
53
+ */
54
+ getPath() {
55
+ throw new Error(`getPath must be implemented`)
56
+ }
57
+
58
+ /**
59
+ * Abstract
60
+ * @returns {string}
61
+ */
62
+ getUuid() {
63
+ throw new Error(`getUuid must be implemented`)
64
+ }
65
+
66
+ /**
67
+ * Abstract
68
+ * @returns {Promise<boolean>} canMergeConcurently
69
+ */
70
+ async canMergeConcurently() {
71
+ throw new Error(`canMergeConcurently must be implemented`)
72
+ }
73
+
74
+ /**
75
+ * Abstract
76
+ * @returns {number} getMaxBlockCount
77
+ */
78
+ getMaxBlockCount() {
79
+ throw new Error(`getMaxBlockCount must be implemented`)
80
+ }
81
+
82
+ /**
83
+ * Checks if the VHD contains a specific block.
84
+ * @param {number} index
85
+ * @returns {boolean}
86
+ */
87
+ hasBlock(index) {
88
+ throw new Error(`hasBlock must be implemented`)
89
+ }
90
+
91
+ /**
92
+ * Abstract
93
+ * Gets the indexes of all blocks in the VHD.
94
+ * @returns {Array<number>}
95
+ */
96
+ getBlockIndexes() {
97
+ throw new Error(`getBlockIndexes must be implemented`)
98
+ }
99
+
100
+ /**
101
+ * Abstract
102
+ * Returns the parent non inizialized instance
103
+ * @returns {RemoteDisk}
104
+ */
105
+ instantiateParent() {
106
+ throw new Error(`instantiateParent must be implemented`)
107
+ }
108
+
109
+ /**
110
+ * Abstract
111
+ * Writes a full block.
112
+ * @param {DiskBlock} diskBlock
113
+ * @return {Promise<number>} blockSize
114
+ */
115
+ async writeBlock(diskBlock) {
116
+ throw new Error(`writeBlock must be implemented`)
117
+ }
118
+
119
+ /**
120
+ * Abstract
121
+ * Reads a specific block from the VHD.
122
+ * @param {number} index
123
+ * @returns {Promise<DiskBlock>} diskBlock
124
+ */
125
+ async readBlock(index) {
126
+ throw new Error(`readBlock must be implemented`)
127
+ }
128
+
129
+ /**
130
+ * Abstract
131
+ * Reads a specific block from the child disk to copy/move it to this disk.
132
+ * @param {RemoteDisk} childDisk
133
+ * @param {number} index
134
+ * @param {boolean} isResumingMerge
135
+ * @returns {Promise<number>} blockSize
136
+ */
137
+ async mergeBlock(childDisk, index, isResumingMerge) {
138
+ throw new Error(`mergeBlock must be implemented`)
139
+ }
140
+
141
+ /**
142
+ * Abstract
143
+ * Manually set the disk allocated blocks.
144
+ * @param {Array<number>} blockIds
145
+ * @returns {Promise<void>}
146
+ */
147
+ async setAllocatedBlocks(blockIds) {
148
+ throw new Error(`setAllocatedBlocks must be implemented`)
149
+ }
150
+
151
+ /**
152
+ * Abstract
153
+ * @param {RemoteDisk} childDisk
154
+ * @returns {Promise<void>}
155
+ */
156
+ async flushMetadata(childDisk) {
157
+ throw new Error(`flushMetadata must be implemented`)
158
+ }
159
+
160
+ /**
161
+ * Abstract
162
+ * @param {RemoteDisk} childDisk
163
+ */
164
+ mergeMetadata(childDisk) {
165
+ throw new Error(`mergeMetadata must be implemented`)
166
+ }
167
+
168
+ /**
169
+ * Abstract
170
+ * Checks if the VHD is a differencing disk.
171
+ * @returns {boolean}
172
+ */
173
+ isDifferencing() {
174
+ throw new Error(`isDifferencing must be implemented`)
175
+ }
176
+
177
+ /**
178
+ * Abstract
179
+ * Rename alias/disk
180
+ * @param {string} newPath
181
+ */
182
+ async rename(newPath) {
183
+ throw new Error(`rename must be implemented`)
184
+ }
185
+
186
+ /**
187
+ * Abstract
188
+ * Deletes alias/disk/disks
189
+ */
190
+ async unlink() {
191
+ throw new Error(`unlink must be implemented`)
192
+ }
193
+
194
+ /**
195
+ * @returns {Promise<RemoteDisk>}
196
+ */
197
+ async openParent() {
198
+ const parent = await super.openParent()
199
+ if (!(parent instanceof RemoteDisk)) {
200
+ throw new Error('parent of a RemoteDisk must be also a RemoteDisk')
201
+ }
202
+ return parent
203
+ }
204
+ }
@@ -0,0 +1,458 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @typedef {import('vhd-lib/Vhd/VhdDirectory.js').VhdDirectory} VhdDirectory
5
+ * @typedef {import('vhd-lib/Vhd/VhdFile.js').VhdFile} VhdFile
6
+ * @typedef {import('vhd-lib/_createFooterHeader').VhdFooter} VhdFooter
7
+ * @typedef {import('@xen-orchestra/disk-transform').DiskBlock} DiskBlock
8
+ * @typedef {import('@xen-orchestra/disk-transform').FileAccessor} FileAccessor
9
+ *
10
+
11
+ */
12
+
13
+ import { openVhd, VhdAbstract, VhdDirectory } from 'vhd-lib'
14
+ import { RemoteDisk } from './RemoteDisk.mjs'
15
+ import { DISK_TYPES } from 'vhd-lib/_constants.js'
16
+ import { isVhdAlias, resolveVhdAlias } from 'vhd-lib/aliases.js'
17
+ import { stringify } from 'uuid'
18
+ import { dirname, join } from 'node:path'
19
+
20
+ export class RemoteVhdDisk extends RemoteDisk {
21
+ /**
22
+ * @type {string}
23
+ */
24
+ #path
25
+
26
+ /**
27
+ * @type {FileAccessor}
28
+ */
29
+ #handler
30
+
31
+ /**
32
+ * @type {VhdFile | VhdDirectory | undefined}
33
+ */
34
+ #vhd
35
+
36
+ /**
37
+ * @type {boolean | undefined}
38
+ */
39
+ #isDifferencing
40
+
41
+ /**
42
+ * @type {number}
43
+ */
44
+ #blockSize = 2 * 1024 * 1024
45
+
46
+ /**
47
+ * @type {number}
48
+ */
49
+ #bitmapSize = 512
50
+
51
+ /**
52
+ * @type {() => any}
53
+ */
54
+ #dispose = () => {}
55
+
56
+ /**
57
+ * @param {Object} params
58
+ * @param {FileAccessor} params.handler
59
+ * @param {string} params.path
60
+ */
61
+ constructor({ handler, path }) {
62
+ super()
63
+ // @todo : ensure this is the full path from the root of the remote
64
+ this.#path = path
65
+ this.#handler = handler
66
+ }
67
+
68
+ /**
69
+ * @param {Object} [options]
70
+ * @param {boolean} [options.force=false]
71
+ * @returns {Promise<void>}
72
+ */
73
+ async init(options = {}) {
74
+ if (this.#vhd === undefined) {
75
+ try {
76
+ const { value, dispose } = await openVhd(this.#handler, await resolveVhdAlias(this.#handler, this.#path), {
77
+ checkSecondFooter: !options.force,
78
+ })
79
+ this.#vhd = value
80
+
81
+ if ((await this.isDirectory()) && !isVhdAlias(this.#path)) {
82
+ this.#vhd = undefined
83
+ throw Object.assign(new Error("Can't init vhd directory without using alias"), { code: 'NOT_SUPPORTED' })
84
+ }
85
+
86
+ this.#dispose = dispose
87
+ await this.#vhd.readBlockAllocationTable()
88
+ this.#isDifferencing = value.footer.diskType === DISK_TYPES.DIFFERENCING
89
+ } catch (error) {
90
+ await this.close()
91
+ throw error
92
+ }
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Closes the VHD.
98
+ * @returns {Promise<void>}
99
+ */
100
+ async close() {
101
+ await this.#dispose()
102
+ }
103
+
104
+ /**
105
+ * @returns {number}
106
+ */
107
+ getVirtualSize() {
108
+ if (this.#vhd === undefined) {
109
+ throw new Error(`can't call getVirtualSize of a RemoteVhdDisk before init`)
110
+ }
111
+ return this.#vhd.footer.currentSize
112
+ }
113
+
114
+ /**
115
+ * @returns {number} size
116
+ */
117
+ getSizeOnDisk() {
118
+ if (this.#vhd === undefined) {
119
+ throw new Error(`can't call getVirtualSize of a RemoteVhdDisk before init`)
120
+ }
121
+
122
+ return this.#vhd.streamSize()
123
+ }
124
+
125
+ /**
126
+ * @returns {number}
127
+ */
128
+ getBlockSize() {
129
+ return this.#blockSize
130
+ }
131
+
132
+ /**
133
+ * @returns {string}
134
+ */
135
+ getPath() {
136
+ return this.#path
137
+ }
138
+
139
+ /**
140
+ * @returns {string}
141
+ */
142
+ getUuid() {
143
+ if (this.#vhd === undefined) {
144
+ throw new Error(`can't call getUid of a RemoteVhdDisk before init`)
145
+ }
146
+
147
+ return stringify(this.#vhd.footer.uuid)
148
+ }
149
+
150
+ /**
151
+ * @returns {string}
152
+ */
153
+ getParentUuid() {
154
+ if (this.#vhd === undefined) {
155
+ throw new Error(`can't call getParentUid of a RemoteVhdDisk before init`)
156
+ }
157
+
158
+ return stringify(this.#vhd.header.parentUuid)
159
+ }
160
+
161
+ /**
162
+ * @returns {Promise<boolean>} canMergeConcurently
163
+ */
164
+ async canMergeConcurently() {
165
+ return await this.isDirectory()
166
+ }
167
+
168
+ /**
169
+ * @returns {number} getMaxBlockCount
170
+ */
171
+ getMaxBlockCount() {
172
+ if (this.#vhd === undefined) {
173
+ throw new Error(`can't call getMaxBlockCount of a RemoteVhdDisk before init`)
174
+ }
175
+
176
+ return this.#vhd.header.maxTableEntries
177
+ }
178
+
179
+ /**
180
+ * Checks if the VHD contains a specific block.
181
+ * @param {number} index
182
+ * @returns {boolean}
183
+ */
184
+ hasBlock(index) {
185
+ if (this.#vhd === undefined) {
186
+ throw new Error(`can't call hasblock of a RemoteVhdDisk before init`)
187
+ }
188
+ return this.#vhd.containsBlock(index)
189
+ }
190
+
191
+ /**
192
+ * Gets the indexes of all blocks in the VHD.
193
+ * @returns {Array<number>}
194
+ */
195
+ getBlockIndexes() {
196
+ if (this.#vhd === undefined) {
197
+ throw new Error(`can't call getBlockIndexes of a RemoteVhdDisk before init`)
198
+ }
199
+ const indexes = []
200
+ for (let blockIndex = 0; blockIndex < this.#vhd.header.maxTableEntries; blockIndex++) {
201
+ if (this.hasBlock(blockIndex)) {
202
+ indexes.push(blockIndex)
203
+ }
204
+ }
205
+ return indexes
206
+ }
207
+
208
+ /**
209
+ * Returns the parent non inizialized instance
210
+ * @returns {RemoteDisk}
211
+ */
212
+ instantiateParent() {
213
+ if (this.#vhd === undefined) {
214
+ throw new Error(`can't call instantiateParent of a RemoteVhdDisk before init`)
215
+ }
216
+
217
+ const parentPath = this.#vhd.header.parentUnicodeName
218
+ const fullParentPath = join(dirname(this.#path), parentPath)
219
+
220
+ if (!parentPath) {
221
+ throw new Error(`disk ${this.#path} doesn't have parents`)
222
+ }
223
+
224
+ const parent = new RemoteVhdDisk({ handler: this.#handler, path: fullParentPath })
225
+ return parent
226
+ }
227
+
228
+ /**
229
+ * Writes a full block into this VHD.
230
+ * @param {DiskBlock} diskBlock
231
+ * @return {Promise<number>} blockSize
232
+ */
233
+ async writeBlock(diskBlock) {
234
+ if (this.#vhd === undefined) {
235
+ throw new Error(`can't call readBlock of a RemoteVhdDisk before init`)
236
+ }
237
+ await this.#vhd.writeEntireBlock({
238
+ id: diskBlock.index,
239
+ buffer: Buffer.concat([Buffer.alloc(this.#bitmapSize, 255), diskBlock.data]),
240
+ })
241
+
242
+ return this.getBlockSize()
243
+ }
244
+
245
+ /**
246
+ * Reads a specific block from the VHD.
247
+ * @param {number} index
248
+ * @returns {Promise<DiskBlock>} diskBlock
249
+ */
250
+ async readBlock(index) {
251
+ if (this.#vhd === undefined) {
252
+ throw new Error(`can't call readBlock of a RemoteVhdDisk before init`)
253
+ }
254
+ const { data } = await this.#vhd.readBlock(index)
255
+ return {
256
+ index,
257
+ data,
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Reads a specific block from the child disk to copy/move it to this disk.
263
+ * @param {RemoteDisk} childDisk
264
+ * @param {number} index
265
+ * @param {boolean} isResumingMerge
266
+ * @returns {Promise<number>} blockSize
267
+ */
268
+ async mergeBlock(childDisk, index, isResumingMerge) {
269
+ if ((await this.isDirectory()) && childDisk instanceof RemoteVhdDisk && (await childDisk.isDirectory())) {
270
+ try {
271
+ await this.#handler.rename(childDisk.getBlockPath(index), this.getBlockPath(index))
272
+
273
+ this.setAllocatedBlocks([index])
274
+ } catch (error) {
275
+ // @ts-ignore
276
+ if (error.code === 'ENOENT' && isResumingMerge === true) {
277
+ // when resuming, the blocks moved since the last merge state write are
278
+ // not in the child anymore but it should be ok
279
+
280
+ // it will throw an error if block is missing in parent
281
+ // won't detect if the block was already in parent and is broken/missing in child
282
+
283
+ // since we can't know the initial size, this will create a discrepancy
284
+ // on the size
285
+ const { data } = await this.readBlock(index)
286
+ if (data.length !== this.getBlockSize()) {
287
+ throw error
288
+ } else {
289
+ this.setAllocatedBlocks([index])
290
+ }
291
+ } else {
292
+ throw error
293
+ }
294
+ }
295
+
296
+ return this.getBlockSize()
297
+ } else {
298
+ return this.writeBlock(await childDisk.readBlock(index))
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Gets a specific block path from the VHD directory.
304
+ * @param {number} index
305
+ * @returns {string} blockPath
306
+ */
307
+ getBlockPath(index) {
308
+ if (this.#vhd === undefined) {
309
+ throw new Error(`can't call readBlock of a RemoteVhdDisk before init`)
310
+ }
311
+
312
+ if (this.#vhd instanceof VhdDirectory) {
313
+ return this.#vhd.getFullBlockPath(index)
314
+ } else {
315
+ throw new Error(`can't call getBlockPath of non directory VHD`)
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Manually set the disk allocated blocks.
321
+ * @param {Array<number>} blockIds
322
+ * @returns {Promise<void>}
323
+ */
324
+ async setAllocatedBlocks(blockIds) {
325
+ if (this.#vhd instanceof VhdDirectory) {
326
+ this.#vhd.setAllocatedBlocks(blockIds)
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Writes Block Allocation Table
332
+ * @param {RemoteDisk} childDisk
333
+ * @returns {Promise<void>}
334
+ */
335
+ async flushMetadata(childDisk) {
336
+ if (this.#vhd === undefined) {
337
+ throw new Error(`can't call flushMetadata of a RemoteVhdDisk before init`)
338
+ }
339
+
340
+ await this.#vhd.writeBlockAllocationTable()
341
+ }
342
+
343
+ /**
344
+ * @returns {VhdFooter}
345
+ */
346
+ getMetadata() {
347
+ if (this.#vhd === undefined) {
348
+ throw new Error(`can't call getMetadata of a RemoteVhdDisk before init`)
349
+ }
350
+
351
+ return this.#vhd.footer
352
+ }
353
+
354
+ /**
355
+ * @param {RemoteVhdDisk} childDisk
356
+ */
357
+ async mergeMetadata(childDisk) {
358
+ const childDiskMetadata = childDisk.getMetadata()
359
+
360
+ if (this.#vhd === undefined) {
361
+ throw new Error(`can't call mergeMetadata of a RemoteVhdDisk before init`)
362
+ }
363
+
364
+ // @ts-ignore
365
+ this.#vhd.footer.currentSize = childDiskMetadata.currentSize
366
+ // @ts-ignore
367
+ this.#vhd.footer.diskGeometry = { ...childDiskMetadata.diskGeometry }
368
+ // @ts-ignore
369
+ this.#vhd.footer.originalSize = childDiskMetadata.originalSize
370
+ // @ts-ignore
371
+ this.#vhd.footer.timestamp = childDiskMetadata.timestamp
372
+ // @ts-ignore
373
+ this.#vhd.footer.uuid = childDiskMetadata.uuid
374
+
375
+ await this.#vhd.writeFooter()
376
+ }
377
+
378
+ /**
379
+ * Checks if the VHD is a differencing disk.
380
+ * @returns {boolean}
381
+ */
382
+ isDifferencing() {
383
+ if (this.#isDifferencing === undefined) {
384
+ throw new Error(`can't call isDifferencing of a RemoteVhdDisk before init`)
385
+ }
386
+ return this.#isDifferencing
387
+ }
388
+
389
+ /**
390
+ * Rename alias/disk
391
+ * @param {string} newPath
392
+ */
393
+ async rename(newPath) {
394
+ if (isVhdAlias(newPath)) {
395
+ const dataPath = await resolveVhdAlias(this.#handler, this.#path)
396
+
397
+ await this.#handler.unlink(this.#path)
398
+ await this.#handler.unlink(newPath)
399
+
400
+ await VhdAbstract.createAlias(this.#handler, newPath, dataPath)
401
+
402
+ this.#path = newPath
403
+ } else {
404
+ try {
405
+ await this.#handler.unlink(newPath)
406
+ } catch (err) {
407
+ if (err && typeof err === 'object' && 'code' in err && err.code === 'EISDIR') {
408
+ await this.#handler.rmtree(newPath).catch(() => {})
409
+ }
410
+ }
411
+
412
+ await this.#handler.rename(this.#path, newPath)
413
+
414
+ this.#path = newPath
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Deletes disk
420
+ */
421
+ async unlink() {
422
+ if (this.#vhd === undefined) {
423
+ throw new Error(`can't call unlink of a RemoteVhdDisk before init`)
424
+ }
425
+
426
+ await this.close()
427
+
428
+ if (isVhdAlias(this.#path)) {
429
+ try {
430
+ await this.#handler.unlink(await resolveVhdAlias(this.#handler, this.#path))
431
+ } catch (err) {
432
+ if (err && typeof err === 'object' && 'code' in err && err.code === 'EISDIR') {
433
+ await this.#handler.rmtree(await resolveVhdAlias(this.#handler, this.#path)).catch(() => {})
434
+ }
435
+ }
436
+ }
437
+
438
+ try {
439
+ await this.#handler.unlink(this.#path)
440
+ } catch (err) {
441
+ if (err && typeof err === 'object' && 'code' in err && err.code === 'EISDIR') {
442
+ await this.#handler.rmtree(this.#path).catch(() => {})
443
+ }
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Check if the disk is a VHD directory.
449
+ * @returns {Promise<boolean>}
450
+ */
451
+ async isDirectory() {
452
+ if (this.#vhd === undefined) {
453
+ throw new Error(`can't call isDirectory of a RemoteVhdDisk before init`)
454
+ }
455
+
456
+ return this.#vhd instanceof VhdDirectory
457
+ }
458
+ }