backups: make id, provider a backend specific setting

the backend can stash whatever values it wants in the config.
just like the DNS backends, we make verifyConfig return a sanitized config.
added benefit is that extra user fields (via API) are also not dumped into the db.
This commit is contained in:
Girish Ramakrishnan
2025-08-01 18:55:04 +02:00
parent 9dfe6242b9
commit 53e9925880
7 changed files with 165 additions and 134 deletions

View File

@@ -5,7 +5,7 @@ exports = module.exports = {
teardown,
cleanup,
testConfig,
verifyConfig,
removePrivateFields,
injectPrivateFields,
@@ -41,7 +41,8 @@ const assert = require('assert'),
{ Readable } = require('stream'),
{ S3, NoSuchKey, NoSuchBucket } = require('@aws-sdk/client-s3'),
safe = require('safetydance'),
{ Upload } = require('@aws-sdk/lib-storage');
{ Upload } = require('@aws-sdk/lib-storage'),
_ = require('../underscore.js');
function S3_NOT_FOUND(error) {
return error instanceof NoSuchKey || error instanceof NoSuchBucket;
@@ -567,39 +568,41 @@ async function cleanup(apiConfig, progressCallback) {
}
}
async function testConfig(apiConfig) {
assert.strictEqual(typeof apiConfig, 'object');
async function verifyConfig({ id, provider, config }) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
if (typeof apiConfig.accessKeyId !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'accessKeyId must be a string');
if (typeof apiConfig.secretAccessKey !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'secretAccessKey must be a string');
if (typeof config.accessKeyId !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'accessKeyId must be a string');
if (typeof config.secretAccessKey !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'secretAccessKey must be a string');
if (typeof apiConfig.bucket !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'bucket must be a string');
if (typeof config.bucket !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'bucket must be a string');
// the node module seems to incorrectly accept bucket name with '/'
if (apiConfig.bucket.includes('/')) throw new BoxError(BoxError.BAD_FIELD, 'bucket name cannot contain "/"');
if (config.bucket.includes('/')) throw new BoxError(BoxError.BAD_FIELD, 'bucket name cannot contain "/"');
// names must be lowercase and start with a letter or number. can contain dashes
if (apiConfig.bucket.includes('_') || apiConfig.bucket.match(/[A-Z]/)) throw new BoxError(BoxError.BAD_FIELD, 'bucket name cannot contain "_" or capitals');
if (config.bucket.includes('_') || config.bucket.match(/[A-Z]/)) throw new BoxError(BoxError.BAD_FIELD, 'bucket name cannot contain "_" or capitals');
if (typeof apiConfig.prefix !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'prefix must be a string');
if ('signatureVersion' in apiConfig && typeof apiConfig.signatureVersion !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'signatureVersion must be a string');
if ('endpoint' in apiConfig && typeof apiConfig.endpoint !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'endpoint must be a string');
if (typeof config.prefix !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'prefix must be a string');
if ('signatureVersion' in config && typeof config.signatureVersion !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'signatureVersion must be a string');
if ('endpoint' in config && typeof config.endpoint !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'endpoint must be a string');
if ('acceptSelfSignedCerts' in apiConfig && typeof apiConfig.acceptSelfSignedCerts !== 'boolean') throw new BoxError(BoxError.BAD_FIELD, 'acceptSelfSignedCerts must be a boolean');
if ('s3ForcePathStyle' in apiConfig && typeof apiConfig.s3ForcePathStyle !== 'boolean') throw new BoxError(BoxError.BAD_FIELD, 's3ForcePathStyle must be a boolean');
if ('acceptSelfSignedCerts' in config && typeof config.acceptSelfSignedCerts !== 'boolean') throw new BoxError(BoxError.BAD_FIELD, 'acceptSelfSignedCerts must be a boolean');
if ('s3ForcePathStyle' in config && typeof config.s3ForcePathStyle !== 'boolean') throw new BoxError(BoxError.BAD_FIELD, 's3ForcePathStyle must be a boolean');
const putParams = {
Bucket: apiConfig.bucket,
Key: path.join(apiConfig.prefix, 'snapshot/cloudron-testfile'),
Bucket: config.bucket,
Key: path.join(config.prefix, 'snapshot/cloudron-testfile'),
Body: 'testcontent'
};
const s3 = createS3Client(apiConfig, {});
const s3 = createS3Client(config, {});
const [putError] = await safe(s3.putObject(putParams));
if (putError) throw new BoxError(BoxError.EXTERNAL_ERROR, `Error put object cloudron-testfile. ${formatError(putError)}`);
const listParams = {
Bucket: apiConfig.bucket,
Prefix: path.join(apiConfig.prefix, 'snapshot'),
Bucket: config.bucket,
Prefix: path.join(config.prefix, 'snapshot'),
MaxKeys: 1
};
@@ -607,12 +610,15 @@ async function testConfig(apiConfig) {
if (listError) throw new BoxError(BoxError.EXTERNAL_ERROR, `Error listing objects. ${formatError(listError)}`);
const delParams = {
Bucket: apiConfig.bucket,
Key: path.join(apiConfig.prefix, 'snapshot/cloudron-testfile')
Bucket: config.bucket,
Key: path.join(config.prefix, 'snapshot/cloudron-testfile')
};
const [delError] = await safe(s3.deleteObject(delParams));
if (delError) throw new BoxError(BoxError.EXTERNAL_ERROR, `Error del object cloudron-testfile. ${formatError(delError)}`);
const newConfig = _.pick(config, ['accessKeyId', 'secretAccessKey', 'bucket', 'prefix', 'signatureVersion', 'acceptSelfSignedCerts', 'endpoint', 's3ForcePathStyle' ]);
return { provider, ...newConfig };
}
async function setup(apiConfig) {
@@ -625,9 +631,11 @@ async function teardown(apiConfig) {
function removePrivateFields(apiConfig) {
apiConfig.secretAccessKey = constants.SECRET_PLACEHOLDER;
delete apiConfig.provider;
return apiConfig;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.secretAccessKey === constants.SECRET_PLACEHOLDER) newConfig.secretAccessKey = currentConfig.secretAccessKey;
newConfig.provider = currentConfig.provider;
}