Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 10 additions & 66 deletions lib/routes/veeam/get.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
const xml2js = require('xml2js');
const { errors } = require('arsenal');
const metadata = require('../../metadata/wrapper');
const { respondWithData, buildHeadXML, getFileToBuild, isSystemXML } = require('./utils');
const { respondWithData, buildHeadXML, buildVeeamFileData } = require('./utils');
const { responseXMLBody } = require('arsenal/build/lib/s3routes/routesUtils');
const UtilizationService = require('../../../lib/utilization/instance');

/**
* Returns system.xml or capacity.xml files for a given bucket.
Expand All @@ -14,75 +11,22 @@ const UtilizationService = require('../../../lib/utilization/instance');
* @param {object} log - logger object
* @returns {undefined} -
*/
function getVeeamFile(request, response, bucketMd, log) {
async function getVeeamFile(request, response, bucketMd, log) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The async route handlers (getVeeamFile, headVeeamFile, listVeeamFiles) now return promises, but the caller in routeVeeam.js:167 (return callback(request, response, bucketMd, log)) ignores the return value since it is inside the final callback of async.waterfall. If any handler throws an unhandled error (e.g. respondWithData throws before its internal try/catch, or responseXMLBody throws), the rejection is never caught, causing an unhandled promise rejection that crashes the Node.js process. Consider adding a .catch() guard in routeVeeam.js or wrapping the top-level of each async handler in a try/catch that covers all code paths.

— Claude Code

if (!bucketMd) {
return responseXMLBody(errors.NoSuchBucket, null, response, log);
}

if ('tagging' in request.query) {
return respondWithData(request, response, log, bucketMd,
return await respondWithData(request, response, log, bucketMd,
buildHeadXML('<Tagging><TagSet></TagSet></Tagging>'));
}
return metadata.getBucket(request.bucketName, log, (err, data) => {
if (err) {
return responseXMLBody(errors.InternalError, null, response, log);
}
const finalizeRequest = bucketMetrics => {
const fileToBuild = getFileToBuild(request, data._capabilities?.VeeamSOSApi);
if (fileToBuild.error) {
return responseXMLBody(fileToBuild.error, null, response, log);
}

// Extract the last modified date, but do not include it when computing
// the file's ETag (md5)
const modified = fileToBuild.value.LastModified;
delete fileToBuild.value.LastModified;

// The SOSAPI metrics are dynamically computed using the SUR backend.
if (bucketMetrics && !fileToBuild.value.CapacityInfo?.Used) {
fileToBuild.value.CapacityInfo.Used = Number(bucketMetrics.bytesTotal);
fileToBuild.value.CapacityInfo.Available =
Number(fileToBuild.value.CapacityInfo.Capacity) - Number(bucketMetrics.bytesTotal);
// TODO CLDSRV-633 when SUR backend supports realtime metrics: it will
// report the real last cseq/date processed by SUR, instead of the current date,
// ensuring no issue in a SOSAPI context. We should use this information.
}

const builder = new xml2js.Builder({
headless: true,
});
return respondWithData(request, response, log, data,
buildHeadXML(builder.buildObject(fileToBuild.value)), modified);
};
if (!isSystemXML(request.objectKey)) {
const bucketKey = `${bucketMd._name}_${new Date(bucketMd._creationDate).getTime()}`;
return UtilizationService.getUtilizationMetrics('bucket', bucketKey, null, {}, (err, bucketMetrics) => {
if (err) {
// Handle errors from UtilizationService/scubaclient
// axios errors have status in err.response.status
const statusCode = err.response?.status || err.statusCode || err.code;
// Only handle 404 gracefully (no metrics available yet, e.g. post-install)
// For 404, continue with static capacity data (Used=0 from bucket metadata)
if (statusCode === 404) {
log.warn('UtilizationService returned 404 when fetching capacity metrics', {
method: 'getVeeamFile',
bucket: request.bucketName,
error: err.message || err.code,
});
return finalizeRequest();
}
log.error('error fetching capacity metrics from UtilizationService', {
method: 'getVeeamFile',
bucket: request.bucketName,
error: err.message || err.code,
statusCode,
});
return responseXMLBody(errors.InternalError, null, response, log);
}
return finalizeRequest(bucketMetrics);
});
}
return finalizeRequest();
});
try {
const result = await buildVeeamFileData(request, bucketMd, log);
return await respondWithData(request, response, log, result.bucketData, result.xmlContent, result.modified);
} catch (err) {
return responseXMLBody(err, null, response, log);
}
}

module.exports = getVeeamFile;
40 changes: 15 additions & 25 deletions lib/routes/veeam/head.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
const xml2js = require('xml2js');
const { errors } = require('arsenal');
const metadata = require('../../metadata/wrapper');
const { getResponseHeader, buildHeadXML, getFileToBuild } = require('./utils');
const { getResponseHeader, buildVeeamFileData } = require('./utils');
const { responseXMLBody, responseContentHeaders } = require('arsenal/build/lib/s3routes/routesUtils');

/**
Expand All @@ -13,31 +11,23 @@ const { responseXMLBody, responseContentHeaders } = require('arsenal/build/lib/s
* @param {object} log - logger object
* @returns {undefined} -
*/
function headVeeamFile(request, response, bucketMd, log) {
async function headVeeamFile(request, response, bucketMd, log) {
if (!bucketMd) {
return responseXMLBody(errors.NoSuchBucket, null, response, log);
}
return metadata.getBucket(request.bucketName, log, (err, data) => {
if (err) {
return responseXMLBody(errors.InternalError, null, response, log);
}
const fileToBuild = getFileToBuild(request, data._capabilities?.VeeamSOSApi);
if (fileToBuild.error) {
return responseXMLBody(fileToBuild.error, null, response, log);
}
let modified = new Date().toISOString();
// Extract the last modified date, but do not include it when computing
// the file's ETag (md5)
modified = fileToBuild.value.LastModified;
delete fileToBuild.value.LastModified;
// Recompute file content to generate appropriate content-md5 header
const builder = new xml2js.Builder({
headless: true,
});
const dataBuffer = Buffer.from(buildHeadXML(builder.buildObject(fileToBuild)));
return responseContentHeaders(null, {}, getResponseHeader(request, data,
dataBuffer, modified, log), response, log);
});

try {
const result = await buildVeeamFileData(request, bucketMd, log);
return responseContentHeaders(
null,
{},
getResponseHeader(request, result.bucketData, result.dataBuffer, result.modified, log),
response,
log,
);
} catch (err) {
return responseXMLBody(err, null, response, log);
}
}

module.exports = headVeeamFile;
87 changes: 50 additions & 37 deletions lib/routes/veeam/list.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
const url = require('url');
const xml2js = require('xml2js');
const { errors, errorInstances } = require('arsenal');
const querystring = require('querystring');
const metadata = require('../../metadata/wrapper');
const { responseXMLBody } = require('arsenal/build/lib/s3routes/routesUtils');
const { respondWithData, getResponseHeader, buildHeadXML, validPath } = require('./utils');
const { respondWithData, getResponseHeader, buildXML, validPath, fetchCapacityMetrics, getBucket } = require('./utils');
const { processVersions, processMasterVersions } = require('../../api/bucketGet');


/**
* Utility function to build a standard response for the LIST route.
* It adds the supported path by default as a static and default file.
Expand Down Expand Up @@ -83,51 +80,67 @@ function buildXMLResponse(request, arrayOfFiles, versioned = false) {
* @param {object} log - logger object
* @returns {undefined} -
*/
function listVeeamFiles(request, response, bucketMd, log) {
async function listVeeamFiles(request, response, bucketMd, log) {
if (!bucketMd) {
return responseXMLBody(errors.NoSuchBucket, null, response, log);
}

// Only accept list-type query parameter
if (!('list-type' in request.query) && !('versions' in request.query)) {
return responseXMLBody(errorInstances.InvalidRequest
.customizeDescription('The Veeam folder does not support this action.'), null, response, log);
}
return metadata.getBucket(request.bucketName, log, (err, data) => {
if (err) {

let data;
try {
data = await getBucket(request.bucketName, log);
} catch {
return responseXMLBody(errors.InternalError, null, response, log);
}

let bucketMetrics;
if (data._capabilities?.VeeamSOSApi?.CapacityInfo) {
try {
bucketMetrics = await fetchCapacityMetrics(bucketMd, request, log);
} catch {
return responseXMLBody(errors.InternalError, null, response, log);
}
const filesToBuild = [];
const fieldsToGenerate = [];
if (data._capabilities?.VeeamSOSApi?.SystemInfo) {
fieldsToGenerate.push({
...data._capabilities?.VeeamSOSApi?.SystemInfo,
name: `${validPath}system.xml`,
});
}
if (data._capabilities?.VeeamSOSApi?.CapacityInfo) {
fieldsToGenerate.push({
...data._capabilities?.VeeamSOSApi?.CapacityInfo,
name: `${validPath}capacity.xml`,
});
}
fieldsToGenerate.forEach(file => {
const lastModified = file.LastModified;
// eslint-disable-next-line no-param-reassign
delete file.LastModified;
const builder = new xml2js.Builder({
headless: true,
});
const dataBuffer = Buffer.from(buildHeadXML(builder.buildObject(file)));
filesToBuild.push({
...getResponseHeader(request, data,
dataBuffer, lastModified, log),
name: file.name,
});
} else {
bucketMetrics = { date: new Date() };
}

const filesToBuild = [];
const fieldsToGenerate = [];
if (data._capabilities?.VeeamSOSApi?.SystemInfo) {
fieldsToGenerate.push({
...data._capabilities?.VeeamSOSApi?.SystemInfo,
name: `${validPath}system.xml`,
});
}
if (data._capabilities?.VeeamSOSApi?.CapacityInfo) {
fieldsToGenerate.push({
...data._capabilities?.VeeamSOSApi?.CapacityInfo,
name: `${validPath}capacity.xml`,
});
}
fieldsToGenerate.forEach(file => {
const isCapacity = file.name.endsWith('capacity.xml');
const lastModified = isCapacity
? bucketMetrics.date
: file.LastModified;
// eslint-disable-next-line no-param-reassign
delete file.LastModified;
const dataBuffer = Buffer.from(buildXML(file));
filesToBuild.push({
...getResponseHeader(request, data,
dataBuffer, lastModified, log),
name: file.name,
});
// When `versions` is present, listing should return a versioned list
return respondWithData(request, response, log, data,
buildXMLResponse(request, filesToBuild, 'versions' in request.query));
});

// When `versions` is present, listing should return a versioned list
return await respondWithData(request, response, log, data,
buildXMLResponse(request, filesToBuild, 'versions' in request.query));
}

module.exports = listVeeamFiles;
3 changes: 2 additions & 1 deletion lib/routes/veeam/put.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const async = require('async');
const { callbackify } = require('util');
const { parseString } = require('xml2js');
const { receiveData, isSystemXML, getFileToBuild } = require('./utils');
const { s3routes, errors } = require('arsenal');
Expand Down Expand Up @@ -28,7 +29,7 @@ function putVeeamFile(request, response, bucketMd, log) {
next => {
// Extract the data from the request, keep it in memory
writeContinue(request, response);
return receiveData(request, log, next);
return callbackify(receiveData)(request, log, next);
},
(value, next) => parseString(value, { explicitArray: false }, (err, parsed) => {
// Convert the received XML to a JS object
Expand Down
Loading
Loading