diff --git a/dashboard/src/utils.js b/dashboard/src/utils.js
index 2658e22be..605a1e504 100644
--- a/dashboard/src/utils.js
+++ b/dashboard/src/utils.js
@@ -1,6 +1,6 @@
import { prettyBinarySize } from '@cloudron/pankow/utils';
-import { RELAY_PROVIDERS, ISTATES, STORAGE_PROVIDERS, EVENTS } from './constants.js';
+import { RELAY_PROVIDERS, ISTATES, EVENTS } from './constants.js';
import { Marked } from 'marked';
function safeMarked() {
@@ -55,39 +55,6 @@ function s3like(provider) {
|| provider === 'contabo-objectstorage' || provider === 'synology-c2-objectstorage';
}
-function regionName(provider, endpoint) {
- const storageProvider = STORAGE_PROVIDERS.find(sp => sp.value === provider);
- const regions = storageProvider.regions;
- if (!regions) return endpoint;
- const region = regions.find(r => r.value === endpoint);
- if (!region) return endpoint;
- return region.name;
-}
-
-function prettySiteLocation(site) {
- switch (site.provider) {
- case 'filesystem':
- return site.config.backupDir + (site.config.prefix ? `/${site.config.prefix}` : '');
- case 'disk':
- case 'ext4':
- case 'xfs':
- case 'mountpoint':
- return (site.config.mountOptions.diskPath || site.config.mountPoint) + (site.config.prefix ? ` / ${site.config.prefix}` : '');
- case 'cifs':
- case 'nfs':
- case 'sshfs':
- return site.config.mountOptions.host + ':' + site.config.mountOptions.remoteDir + (site.config.prefix ? ` / ${site.config.prefix}` : '');
- case 's3':
- return site.config.region + ' / ' + site.config.bucket + (site.config.prefix ? ` / ${site.config.prefix}` : '');
- case 'minio':
- return site.config.endpoint + ' / ' + site.config.bucket + (site.config.prefix ? ` / ${site.config.prefix}` : '');
- case 'gcs':
- return site.config.bucket + (site.config.prefix ? ` / ${site.config.prefix}` : '');
- default:
- return regionName(site.provider, site.config.endpoint) + ' / ' + site.config.bucket + (site.config.prefix ? ` / ${site.config.prefix}` : '');
- }
-}
-
function eventlogDetails(eventLog, app = null, appIdContext = '') {
const data = eventLog.data;
const errorMessage = data.errorMessage;
@@ -712,7 +679,6 @@ export {
getColor,
prettySchedule,
parseSchedule,
- prettySiteLocation,
parseFullBackupPath
};
@@ -735,6 +701,5 @@ export default {
getColor,
prettySchedule,
parseSchedule,
- prettySiteLocation,
parseFullBackupPath
};
diff --git a/dashboard/src/views/BackupSitesView.vue b/dashboard/src/views/BackupSitesView.vue
index 00d3f20f4..ff5771acf 100644
--- a/dashboard/src/views/BackupSitesView.vue
+++ b/dashboard/src/views/BackupSitesView.vue
@@ -20,7 +20,7 @@ import BackupSitesModel from '../models/BackupSitesModel.js';
import TasksModel from '../models/TasksModel.js';
import AppsModel from '../models/AppsModel.js';
-import { prettySchedule, prettySiteLocation } from '../utils.js';
+import { prettySchedule } from '../utils.js';
const profile = inject('profile');
@@ -318,7 +318,7 @@ onMounted(async () => {
Storage: {{ site.provider }} ({{ site.format }})
- at {{ prettySiteLocation(site) }}
+ at {{ site.locationLabel }}
diff --git a/src/backupsites.js b/src/backupsites.js
index 8e67eb944..3ac75197e 100644
--- a/src/backupsites.js
+++ b/src/backupsites.js
@@ -79,6 +79,8 @@ function postProcess(result) {
result.contents = safe.JSON.parse(result.contentsJson) || null;
delete result.contentsJson;
+ result.locationLabel = storageApi(result).getLocationLabel(result.config);
+
return result;
}
@@ -440,6 +442,12 @@ async function setConfig(backupSite, newConfig, auditSource) {
log('setConfig: validating new storage configuration');
const sanitizedConfig = await storageApi(backupSite).verifyConfig({ id: backupSite.id, provider: backupSite.provider, config: newConfig });
+ const newLocationLabel = storageApi(backupSite).getLocationLabel(sanitizedConfig);
+ const existingSites = await list();
+ if (existingSites.some(s => s.id !== backupSite.id && s.locationLabel === newLocationLabel)) {
+ throw new BoxError(BoxError.ALREADY_EXISTS, 'A backup site with the same storage destination already exists');
+ }
+
await update(backupSite, { config: sanitizedConfig });
log('setConfig: setting up new storage configuration');
@@ -490,6 +498,12 @@ async function add(data, auditSource) {
log('add: validating new storage configuration');
const sanitizedConfig = await storageApi({ provider }).verifyConfig({id, provider, config });
+ const newLocationLabel = storageApi({ provider }).getLocationLabel(sanitizedConfig);
+ const existingSites = await list();
+ if (existingSites.some(s => s.locationLabel === newLocationLabel)) {
+ throw new BoxError(BoxError.ALREADY_EXISTS, 'A backup site with the same storage destination already exists');
+ }
+
await database.query('INSERT INTO backupSites (id, name, provider, configJson, contentsJson, limitsJson, integrityKeyPairJson, retentionJson, schedule, encryptionJson, format, enableForUpdates) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[ id, name, provider, JSON.stringify(sanitizedConfig), JSON.stringify(contents), JSON.stringify(limits), JSON.stringify(integrityKeyPair), JSON.stringify(retention), schedule, JSON.stringify(encryption), format, enableForUpdates ]);
diff --git a/src/storage/filesystem.js b/src/storage/filesystem.js
index 8f215e085..2e46966c3 100644
--- a/src/storage/filesystem.js
+++ b/src/storage/filesystem.js
@@ -406,6 +406,28 @@ async function verifyConfig({ id, provider, config }) {
return newConfig;
}
+function getLocationLabel(config) {
+ assert.strictEqual(typeof config, 'object');
+
+ const suffix = config.prefix ? ` / ${config.prefix}` : '';
+
+ switch (config._provider) {
+ case mounts.MOUNT_TYPE_FILESYSTEM:
+ return config.backupDir + suffix;
+ case mounts.MOUNT_TYPE_DISK:
+ case mounts.MOUNT_TYPE_EXT4:
+ case mounts.MOUNT_TYPE_XFS:
+ case mounts.MOUNT_TYPE_MOUNTPOINT:
+ return (config.mountOptions?.diskPath || config.mountPoint) + suffix;
+ case mounts.MOUNT_TYPE_CIFS:
+ case mounts.MOUNT_TYPE_NFS:
+ case mounts.MOUNT_TYPE_SSHFS:
+ return config.mountOptions.host + ':' + config.mountOptions.remoteDir + suffix;
+ default:
+ return config._provider + suffix;
+ }
+}
+
function removePrivateFields(config) {
delete config.mountOptions?.password;
delete config.mountOptions?.privateKey;
@@ -432,6 +454,7 @@ export default {
cleanup,
verifyConfig,
+ getLocationLabel,
removePrivateFields,
injectPrivateFields,
diff --git a/src/storage/gcs.js b/src/storage/gcs.js
index b11f4e11f..f2787dff7 100644
--- a/src/storage/gcs.js
+++ b/src/storage/gcs.js
@@ -251,6 +251,12 @@ async function teardown(config) {
assert.strictEqual(typeof config, 'object');
}
+function getLocationLabel(config) {
+ assert.strictEqual(typeof config, 'object');
+
+ return config.bucket + (config.prefix ? ` / ${config.prefix}` : '');
+}
+
function removePrivateFields(config) {
delete config.credentials.private_key;
return config;
@@ -281,6 +287,7 @@ export default {
cleanup,
verifyConfig,
+ getLocationLabel,
removePrivateFields,
injectPrivateFields,
};
diff --git a/src/storage/interface.js b/src/storage/interface.js
index bb466f782..3fdde0f9a 100644
--- a/src/storage/interface.js
+++ b/src/storage/interface.js
@@ -12,6 +12,12 @@ import BoxError from '../boxerror.js';
// for the other API calls we leave it to the backend to retry. this allows
// them to tune the concurrency based on failures/rate limits accordingly
+function getLocationLabel(config) {
+ assert.strictEqual(typeof config, 'object');
+
+ return '';
+}
+
function removePrivateFields(config) {
// in-place removal of tokens and api keys
return config;
@@ -164,6 +170,7 @@ export default {
cleanup,
verifyConfig,
+ getLocationLabel,
removePrivateFields,
injectPrivateFields
};
diff --git a/src/storage/s3.js b/src/storage/s3.js
index a0648443c..5b1fa8d54 100644
--- a/src/storage/s3.js
+++ b/src/storage/s3.js
@@ -514,6 +514,13 @@ async function teardown(config) {
assert.strictEqual(typeof config, 'object');
}
+function getLocationLabel(config) {
+ assert.strictEqual(typeof config, 'object');
+
+ const location = config._provider === 's3' ? (config.region || 'us-east-1') : config.endpoint;
+ return location + ' / ' + config.bucket + (config.prefix ? ` / ${config.prefix}` : '');
+}
+
function removePrivateFields(config) {
delete config.secretAccessKey;
delete config._provider;
@@ -676,6 +683,7 @@ export default {
teardown,
cleanup,
verifyConfig,
+ getLocationLabel,
removePrivateFields,
injectPrivateFields,
getAvailableSize,