fix(rest-api): vm export correctly handle zstd and gzip compression (#9960)

This commit is contained in:
Mathieu
2026-06-10 10:55:13 +02:00
committed by GitHub
parent e376a0e102
commit c61083e59e
7 changed files with 32 additions and 10 deletions

View File

@@ -252,7 +252,7 @@ export interface Xapi {
): Promise<void>
VM_export(
vmRef: XenApiVm['$ref'],
opts?: { cancelToken?: unknown; compress?: boolean; useSnapshot?: boolean }
opts?: { cancelToken?: unknown; compress?: boolean | 'zstd' | 'gzip'; useSnapshot?: boolean }
): ReturnType<Xapi['getResource']>
VM_import(
stream: Readable,

View File

@@ -0,0 +1,14 @@
import { createLogger } from '@xen-orchestra/log'
import type { NextFunction, Request, Response } from 'express'
const log = createLogger('xo:rest-api:deprecated.middleware')
export function vmExportCompressDeprecated(req: Request, _res: Response, next: NextFunction) {
const compress = req.query.compress
if (compress === 'true' || compress === 'false') {
log.warn("the query param 'compress' as boolean is deprecated. Please use an explicit value next time")
req.query.compress = compress === 'true' ? 'gzip' : undefined
}
next()
}

View File

@@ -45,6 +45,7 @@ import {
} from '../open-api/oa-examples/vm-snapshot.oa-example.mjs'
import { VmService } from '../vms/vm.service.mjs'
import { messageIds, partialMessages } from '../open-api/oa-examples/message.oa-example.mjs'
import { vmExportCompressDeprecated } from '../middlewares/deprecated.middleware.mjs'
@Route('vm-snapshots')
@Security('*')
@@ -104,7 +105,7 @@ export class VmSnapshotController extends XapiXoController<XoVmSnapshot> {
*/
@Extension('x-mcp-exposure', 'deny')
@Get('{id}.{format}')
@Middlewares(acl({ resource: 'vm-snapshot', action: 'export', objectId: 'params.id' }))
@Middlewares([acl({ resource: 'vm-snapshot', action: 'export', objectId: 'params.id' }), vmExportCompressDeprecated])
@SuccessResponse(200, 'Download started', 'application/octet-stream')
@Response(forbiddenOperationResp.status, forbiddenOperationResp.description)
@Response(notFoundResp.status, notFoundResp.description)
@@ -113,7 +114,7 @@ export class VmSnapshotController extends XapiXoController<XoVmSnapshot> {
@Request() req: ExRequest,
@Path() id: string,
@Path() format: 'xva' | 'ova',
@Query() compress?: boolean
@Query() compress?: Parameters<VmService['export']>[2]['compress']
): Promise<Readable> {
const stream = await this.#vmService.export(id as XoVmSnapshot['id'], 'VM-snapshot', {
compress,

View File

@@ -46,6 +46,7 @@ import {
import { VmService } from '../vms/vm.service.mjs'
import { messageIds, partialMessages } from '../open-api/oa-examples/message.oa-example.mjs'
import { partialTasks, taskIds } from '../open-api/oa-examples/task.oa-example.mjs'
import { vmExportCompressDeprecated } from '../middlewares/deprecated.middleware.mjs'
@Route('vm-templates')
@Security('*')
@@ -105,7 +106,7 @@ export class VmTemplateController extends XapiXoController<XoVmTemplate> {
*/
@Extension('x-mcp-exposure', 'deny')
@Get('{id}.{format}')
@Middlewares(acl({ resource: 'vm-template', action: 'export', objectId: 'params.id' }))
@Middlewares([acl({ resource: 'vm-template', action: 'export', objectId: 'params.id' }), vmExportCompressDeprecated])
@SuccessResponse(200, 'Download started', 'application/octet-stream')
@Response(forbiddenOperationResp.status, forbiddenOperationResp.description)
@Response(notFoundResp.status, notFoundResp.description)
@@ -114,7 +115,7 @@ export class VmTemplateController extends XapiXoController<XoVmTemplate> {
@Request() req: ExRequest,
@Path() id: string,
@Path() format: 'xva' | 'ova',
@Query() compress?: boolean
@Query() compress?: Parameters<VmService['export']>[2]['compress']
): Promise<Readable> {
const stream = await this.#vmService.export(id as XoVmTemplate['id'], 'VM-template', {
compress,

View File

@@ -35,7 +35,6 @@ import type {
XoMessage,
XoNetwork,
XoVif,
XoPif,
XoSr,
} from '@vates/types'
import { PassThrough, Readable } from 'node:stream'
@@ -72,6 +71,7 @@ import { messageIds, partialMessages } from '../open-api/oa-examples/message.oa-
import type { UnbrandedVmDashboard, UpdateVmRequestBody } from './vm.type.mjs'
import type { CreateActionReturnType } from '../abstract-classes/base-controller.mjs'
import { Task } from '@vates/task'
import { vmExportCompressDeprecated } from '../middlewares/deprecated.middleware.mjs'
const IGNORED_VDIS_TAG = '[NOSNAP]'
@@ -141,7 +141,7 @@ export class VmController extends XapiXoController<XoVm> {
*/
@Extension('x-mcp-exposure', 'deny')
@Get('{id}.{format}')
@Middlewares(acl({ resource: 'vm', action: 'export', objectId: 'params.id' }))
@Middlewares([acl({ resource: 'vm', action: 'export', objectId: 'params.id' }), vmExportCompressDeprecated])
@SuccessResponse(200, 'Download started', 'application/octet-stream')
@Response(forbiddenOperationResp.status, forbiddenOperationResp.description)
@Response(notFoundResp.status, notFoundResp.description)
@@ -149,8 +149,8 @@ export class VmController extends XapiXoController<XoVm> {
async exportVm(
@Request() req: ExRequest,
@Path() id: string,
@Path() format: 'xva' | 'ova',
@Query() compress?: boolean
@Path() format: Parameters<VmService['export']>[2]['format'],
@Query() compress?: Parameters<VmService['export']>[2]['compress']
): Promise<Readable> {
const stream = await this.#vmService.export(id as XoVm['id'], 'VM', { compress, format, response: req.res })
process.on('SIGTERM', () => req.destroy())

View File

@@ -180,7 +180,11 @@ export class VmService {
async export<Vm extends XoVm | XoVmSnapshot | XoVmTemplate>(
id: Vm['id'],
vmType: Vm['type'],
{ compress, format, response }: { compress?: boolean; format: 'ova' | 'xva'; response?: ExResponse }
{
compress,
format,
response,
}: { compress?: boolean | 'zstd' | 'gzip'; format: 'ova' | 'xva'; response?: ExResponse }
): Promise<Readable> {
const xapiVm = this.#restApi.getXapiObject(id, vmType)

View File

@@ -10,6 +10,7 @@
### Enhancements
> Users must be able to say: "Nice enhancement, I'm eager to test it"
- [IPMI-Plugin] Add default outlet regex to the dell preset (PR [#9884](https://github.com/vatesfr/xen-orchestra/pull/9884))
- [REST API] Expose `POST /backup-repositories` and `PATCH /backup-repositories/:id` REST routes (PR [#9852](https://github.com/vatesfr/xen-orchestra/pull/9852))
@@ -54,6 +55,7 @@
- xo-server-sdn-controller: apply/clean network rules on VIF update (PR [#9933](https://github.com/vatesfr/xen-orchestra/pull/9933))
- [Rest Api] Fix `possibly unhandled rejection invalid crendentials` (PR [#9938](https://github.com/vatesfr/xen-orchestra/pull/9938))
- [Backups] Fixed "Cannot read properties of undefined" issues (PR [#9944](https://github.com/vatesfr/xen-orchestra/pull/9944))
- [REST API] `GET /vms/:id.:format`, `GET /vm-templates/:id.:format`, `GET /vm-snapshots/:id.:format` now correctly support explicit compress query param (`zstd` | `gzip`). Still support `true` | `false` as deprecated value (PR [#9960](https://github.com/vatesfr/xen-orchestra/pull/9960))
### Packages to release