@skylord123/node-red-contrib-backrest
Version:
Node-RED nodes for interacting with Backrest, a restic-powered backup management tool.
192 lines (178 loc) • 9.99 kB
JavaScript
const axios = require('axios');
module.exports = function (RED) {
function BackrestQueryNode(config) {
RED.nodes.createNode(this, config);
const node = this;
const serverConfig = RED.nodes.getNode(config.config);
if (!serverConfig) {
node.error("No Backrest server configuration found");
return;
}
const baseURL = serverConfig.backrest_url || '';
const username = serverConfig.credentials.username || '';
const password = serverConfig.credentials.password || '';
const outputType = config.outputType || "msg";
const outputValue = config.outputValue || "payload";
// Predefined API endpoints with descriptions
const apiMap = {
"/v1.Backrest/GetOperations": {
description: "Fetch the list of operations from the Backrest server.",
input: { type: "object", example: { lastN: 1000, selector: { repoId: "repo-id" } } },
output: { type: "array", description: "Array of operations with details (type, status, etc.)." }
},
"/v1.Backrest/GetConfig": {
description: "Retrieve the Backrest server configuration.",
input: { type: "none", example: null },
output: { type: "object", description: "The Backrest configuration object." }
},
"/v1.Backrest/SetConfig": {
description: "Update the Backrest server configuration.",
input: { type: "object", example: { key: "value" } },
output: { type: "object", description: "The updated configuration object." }
},
"/v1.Backrest/CheckRepoExists": {
description: "Check if a repository exists on the Backrest server.",
input: { type: "object", example: { id: "repo-id" } },
output: { type: "boolean", description: "True if the repository exists, false otherwise." }
},
"/v1.Backrest/AddRepo": {
description: "Add a new repository to the Backrest server.",
input: { type: "object", example: { id: "repo-id", path: "/path/to/repo" } },
output: { type: "object", description: "Updated config object including the new repository." }
},
"/v1.Backrest/GetOperationEvents": {
description: "Stream real-time operation events (created, updated, or deleted).",
input: { type: "none", example: null },
output: { type: "stream", description: "A stream of operation events." }
},
"/v1.Backrest/ListSnapshots": {
description: "List snapshots for a repository or plan.",
input: { type: "object", example: { repoId: "repo-id", planId: "plan-id" } },
output: { type: "array", description: "Array of snapshots with metadata." }
},
"/v1.Backrest/ListSnapshotFiles": {
description: "List files within a snapshot at a specific path.",
input: { type: "object", example: { repoId: "repository-id", snapshotId: "snapshot-id", path: "target/path" } },
output: { type: "object", description: "Contains the path and an array of file entries." }
},
"/v1.Backrest/Backup": {
description: "Schedule a backup operation.",
input: { type: "object", example: { value: "plan-id" } },
output: { type: "none", description: "No response body, indicates successful queueing." }
},
"/v1.Backrest/DoRepoTask": {
description: "Schedule a repository task (prune, stats, etc.).",
input: { type: "object", example: { repoId: "repo-id", task: "TASK_PRUNE" } },
output: { type: "none", description: "No response body, indicates successful queueing." }
},
"/v1.Backrest/Forget": {
description: "Schedule a forget operation to clean up snapshots.",
input: { type: "object", example: { repoId: "repo-id", planId: "plan-id", snapshotId: "snap-id" } },
output: { type: "none", description: "No response body, indicates successful queueing." }
},
"/v1.Backrest/Restore": {
description: "Schedule a restore operation for a snapshot.",
input: { type: "object", example: { planId: "plan-id", repoId: "repo-id", snapshotId: "snapshot-id", path: "/source/path", target: "/restore/path" } },
output: { type: "none", description: "No response body, indicates successful queueing." }
},
"/v1.Backrest/Cancel": {
description: "Attempt to cancel an operation by its ID.",
input: { type: "object", example: { value: 12345 } },
output: { type: "none", description: "No response body, indicates cancellation request submitted." }
},
"/v1.Backrest/GetLogs": {
description: "Stream logs for a specific operation.",
input: { type: "object", example: { ref: "operation-ref" } },
output: { type: "stream", description: "A stream of log data." }
},
"/v1.Backrest/RunCommand": {
description: "Execute a custom Restic command on a repository.",
input: { type: "object", example: { repoId: "repo-id", command: "restic-command" } },
output: { type: "object", description: "Operation ID of the submitted command." }
},
"/v1.Backrest/GetDownloadURL": {
description: "Retrieve a signed URL for downloading forget operation results.",
input: { type: "object", example: { value: 12345 } },
output: { type: "object", description: "Signed URL for downloading results." }
},
"/v1.Backrest/ClearHistory": {
description: "Clear operation history on the server.",
input: { type: "object", example: { selector: { planId: "plan-id" }, onlyFailed: true } },
output: { type: "none", description: "No response body, indicates history cleared." }
},
"/v1.Backrest/PathAutocomplete": {
description: "Provide path autocomplete suggestions for a given path.",
input: { type: "object", example: { value: "/base/path" } },
output: { type: "array", description: "List of autocomplete suggestions." }
},
"/v1.Backrest/GetSummaryDashboard": {
description: "Retrieve summary data for the dashboard view.",
input: { type: "none", example: null },
output: { type: "object", description: "Summary metrics and stats for the dashboard." }
}
};
node.on('input', function (msg, send, done) {
const endpoint = config.endpoint || '/v1.Backrest/GetOperations';
if (!apiMap[endpoint]) {
node.error(`Unknown API endpoint: ${endpoint}`);
if (done) done(new Error(`Unknown API endpoint: ${endpoint}`));
return;
}
const apiConfig = apiMap[endpoint];
// Evaluate input property
RED.util.evaluateNodeProperty(config.inputValue, config.inputType, node, msg, (err, evaluatedInput) => {
if (err) {
node.error(`Input Evaluation Error: ${err.message}`);
if (done) done(err);
return;
}
let payload = evaluatedInput || {};
if (apiConfig.input.type === "none") {
// Force empty object if endpoint does not require input
payload = {};
}
const headers = { 'Content-Type': 'application/json' };
const url = `${baseURL}${endpoint}`;
axios.post(url, payload, {
headers,
auth: (username && password) ? { username, password } : undefined
})
.then(response => {
// Decide where to store output based on outputType + outputValue
switch (outputType) {
case "flow":
node.context().flow.set(outputValue, response.data);
send(msg);
break;
case "global":
node.context().global.set(outputValue, response.data);
send(msg);
break;
case "msg":
default:
RED.util.setMessageProperty(msg, outputValue, response.data, true);
send(msg);
break;
}
if (done) done();
})
.catch(error => {
const errorDetails = {
status: error.response?.status || 'N/A',
url: url,
data: error.response?.data ? JSON.stringify(error.response.data) : 'No response data',
message: error.message || 'Unknown error'
};
node.error(`Request failed - ${JSON.stringify(errorDetails)}`);
if (done) done(error);
});
});
});
}
RED.nodes.registerType('backrest-api', BackrestQueryNode, {
credentials: {
username: { type: 'text' },
password: { type: 'password' }
}
});
};