diff --git a/dashboard/src/Index.vue b/dashboard/src/Index.vue
index 9a4b928b6..feddc881f 100644
--- a/dashboard/src/Index.vue
+++ b/dashboard/src/Index.vue
@@ -23,6 +23,7 @@ import AppearanceView from './views/AppearanceView.vue';
import AppstoreView from './views/AppstoreView.vue';
import BackupTargetsView from './views/BackupTargetsView.vue';
import BackupAppArchivesView from './views/BackupAppArchivesView.vue';
+import BackupListView from './views/BackupListView.vue';
import CloudronAccountView from './views/CloudronAccountView.vue';
import DomainsView from './views/DomainsView.vue';
import EmailDomainView from './views/EmailDomainView.vue';
@@ -51,6 +52,7 @@ const VIEWS = {
APPS: 'apps',
APPSTORE: 'appstore',
BACKUP_TARGETS: 'backup-targets',
+ BACKUP_LIST: 'backup-list',
BACKUP_APP_ARCHIVES: 'backup-app-archives',
CLOUDRON_ACCOUNT: 'cloudron-account',
DOMAINS: 'domains',
@@ -173,6 +175,8 @@ function onHashChange() {
view.value = VIEWS.APPEARANCE;
} else if (v === VIEWS.BACKUP_TARGETS && profile.value.isAtLeastAdmin) {
view.value = VIEWS.BACKUP_TARGETS;
+ } else if (v === VIEWS.BACKUP_LIST && profile.value.isAtLeastAdmin) {
+ view.value = VIEWS.BACKUP_LIST;
} else if (v === VIEWS.BACKUP_APP_ARCHIVES && profile.value.isAtLeastAdmin) {
view.value = VIEWS.BACKUP_APP_ARCHIVES;
} else if (v === VIEWS.CLOUDRON_ACCOUNT && profile.value.isAtLeastOwner) {
@@ -306,7 +310,8 @@ onMounted(async () => {
@@ -360,6 +365,7 @@ onMounted(async () => {
+
diff --git a/dashboard/src/components/BackupList.vue b/dashboard/src/components/BackupList.vue
index d4bd4a7b7..f3f602d7f 100644
--- a/dashboard/src/components/BackupList.vue
+++ b/dashboard/src/components/BackupList.vue
@@ -7,19 +7,17 @@ const t = i18n.t;
import { ref, onMounted, useTemplateRef } from 'vue';
import { Button, ButtonGroup, ProgressBar, FormGroup, TextInput, Checkbox, TableView, Dialog } from '@cloudron/pankow';
import { prettyLongDate, copyToClipboard } from '@cloudron/pankow/utils';
-import { TASK_TYPES, SECRET_PLACEHOLDER } from '../constants.js';
+import { TASK_TYPES } from '../constants.js';
import Section from '../components/Section.vue';
import BackupsModel from '../models/BackupsModel.js';
+import BackupTargetsModel from '../models/BackupTargetsModel.js';
import AppsModel from '../models/AppsModel.js';
import TasksModel from '../models/TasksModel.js';
import DashboardModel from '../models/DashboardModel.js';
import { download } from '../utils.js';
-const props = defineProps({
- config: Object
-});
-
const backupsModel = BackupsModel.create();
+const backupTargetsModel = BackupTargetsModel.create();
const appsModel = AppsModel.create();
const tasksModel = TasksModel.create();
const dashboardModel = DashboardModel.create();
@@ -167,24 +165,23 @@ async function onStopBackup() {
}
async function onDownloadConfig(backup) {
- const [error, result] = await dashboardModel.config();
+ const [error, dashboardConfig] = await dashboardModel.config();
if (error) return console.error(error);
- // secrets and tokens already come with placeholder characters we remove them
+ const [backupTargetError, backupTarget] = await backupTargetsModel.get(backup.targetId);
+ if (backupTargetError) return console.error(backupTargetError);
+
const tmp = {
- remotePath: backup.remotePath,
- encrypted: !!props.config.password // we add this just to help the import UI
+ remotePath: backup.remotePath
};
+ for (const k of ['provider', 'config', 'limits', 'format', 'encrypted', 'encryptedFilenames']) {
+ tmp[k] = backupTarget[k];
+ }
- Object.keys(props.config).forEach((k) => {
- if (props.config[k] !== SECRET_PLACEHOLDER) tmp[k] = props.config[k];
- });
-
- const filename = `${result.adminFqdn}-backup-config-${(new Date(backup.creationTime)).toISOString().split('T')[0]}.json`;
+ const filename = `${dashboardConfig.adminFqdn}-backup-config-${(new Date(backup.creationTime)).toISOString().split('T')[0]}.json`;
download(filename, JSON.stringify(tmp, null, 4));
}
-
// backups info dialog
const infoDialog = useTemplateRef('infoDialog');
const infoBackup = ref({ contents: [] });
diff --git a/dashboard/src/components/BackupTargetDialog.vue b/dashboard/src/components/BackupTargetDialog.vue
index 75abbb0eb..cf79188f5 100644
--- a/dashboard/src/components/BackupTargetDialog.vue
+++ b/dashboard/src/components/BackupTargetDialog.vue
@@ -17,7 +17,7 @@ const systemModel = SystemModel.create();
const dialog = useTemplateRef('dialog');
const form = useTemplateRef('form');
const target = ref({});
-const label = ref('');
+const name = ref('');
const encrypted = ref(false);
const encryptedFilenames = ref(false);
const formError = ref({});
@@ -146,7 +146,7 @@ async function onSubmitAdd() {
formError.value = {};
busy.value = true;
- const [error, targetId] = await backupTargetsModel.add(label.value, format.value, provider.value, data, schedulePattern, retention, limitsConfig, encryptionPassword, encryptedFilenames);
+ const [error, targetId] = await backupTargetsModel.add(name.value, format.value, provider.value, data, schedulePattern, retention, limitsConfig, encryptionPassword, encryptedFilenames);
if (error) {
formError.value.generic = error.body ? error.body.message : 'Internal error';
busy.value = false;
@@ -168,7 +168,14 @@ async function onSubmitAdd() {
async function onSubmitEdit() {
// TODO set config (eg. provider config passwords)
- // TODO set label somehow?
+
+ // name
+ let [error] = await backupTargetsModel.setName(target.value.id, name.value);
+ if (error) {
+ formError.value.generic = 'Failed to set name';
+ busy.value = false;
+ return console.error(error);
+ }
// limits
const limitsConfig = {
@@ -180,7 +187,7 @@ async function onSubmitEdit() {
// deleteConcurrency: parseInt(providerConfig.value.limits.deleteConcurrency),
};
- const [error] = await backupTargetsModel.setLimits(target.value.id, limitsConfig);
+ [error] = await backupTargetsModel.setLimits(target.value.id, limitsConfig);
if (error) {
formError.value.generic = 'Failed to set limits';
busy.value = false;
@@ -227,7 +234,7 @@ defineExpose({
formError.value = {};
busy.value = false;
- label.value = t?.label || '';
+ name.value = t?.name || '';
provider.value = t?.provider || '';
format.value = t?.format || '';
isPrimary.value = t?.primary || false;
@@ -273,14 +280,14 @@ defineExpose({
-
-
-
-
+
+
+
+
diff --git a/dashboard/src/models/BackupTargetsModel.js b/dashboard/src/models/BackupTargetsModel.js
index e47298463..fd40cf9b8 100644
--- a/dashboard/src/models/BackupTargetsModel.js
+++ b/dashboard/src/models/BackupTargetsModel.js
@@ -93,6 +93,17 @@ function create() {
if (error || result.status !== 200) return [error || result];
return [null];
},
+ async setName(id, name) {
+ let error, result;
+ try {
+ result = await fetcher.post(`${API_ORIGIN}/api/v1/backup_targets/${id}/configure/name`, { name }, { access_token: accessToken });
+ } catch (e) {
+ error = e;
+ }
+
+ if (error || result.status !== 200) return [error || result];
+ return [null];
+ },
async setLimits(id, limits) {
let error, result;
try {
diff --git a/dashboard/src/views/BackupListView.vue b/dashboard/src/views/BackupListView.vue
new file mode 100644
index 000000000..f986dcf9a
--- /dev/null
+++ b/dashboard/src/views/BackupListView.vue
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
diff --git a/dashboard/src/views/BackupTargetsView.vue b/dashboard/src/views/BackupTargetsView.vue
index ea54bced8..f37af52bb 100644
--- a/dashboard/src/views/BackupTargetsView.vue
+++ b/dashboard/src/views/BackupTargetsView.vue
@@ -30,12 +30,12 @@ const columns = {
status: {
width: '30px',
},
- provider: {
- label: 'Provider',
+ name: {
+ label: 'Name',
sort: true,
},
- label: {
- label: 'Label',
+ provider: {
+ label: 'Provider',
sort: true,
},
format: {
@@ -164,13 +164,14 @@ onMounted(async () => {
+
+ {{ target.name }}
+
+
{{ target.provider }}
-
- {{ target.label }}
-
{{ target.format }}
diff --git a/src/storage/filesystem.js b/src/storage/filesystem.js
index bbfdb4d92..84750265f 100644
--- a/src/storage/filesystem.js
+++ b/src/storage/filesystem.js
@@ -346,24 +346,27 @@ async function verifyConfig({ id, provider, config }) {
}
}
- const tmp = _.pick(config, ['noHardlinks', 'chown', 'preserveAttributes', 'backupDir', 'prefix', 'mountOptions', 'mountPoint']);
- const newConfig = { _provider: provider, _managedMountPath: managedMountPath, ...tmp };
- const fullPath = getRootPath(newConfig);
+ const newConfig = _.pick(config, ['noHardlinks', 'chown', 'preserveAttributes', 'backupDir', 'prefix', 'mountOptions', 'mountPoint']);
+ newConfig._provider = provider;
+ const fullPath = getRootPath({ ...newConfig, _managedMountPath: `${managedMountPath}-validation` });
if (!safe.fs.mkdirSync(path.join(fullPath, 'snapshot'), { recursive: true }) && safe.error.code !== 'EEXIST') {
if (safe.error && safe.error.code === 'EACCES') throw new BoxError(BoxError.BAD_FIELD, `Access denied. Create ${fullPath}/snapshot and run "chown yellowtent:yellowtent ${fullPath}" on the server`);
throw new BoxError(BoxError.BAD_FIELD, safe.error.message);
}
- if (!safe.fs.writeFileSync(path.join(fullPath, 'cloudron-testfile'), 'testcontent')) {
+ if (!safe.fs.writeFileSync(path.join(fullPath, 'snapshot/cloudron-testfile'), 'testcontent')) {
throw new BoxError(BoxError.BAD_FIELD, `Unable to create test file as 'yellowtent' user in ${fullPath}: ${safe.error.message}. Check dir/mount permissions`);
- }
+ }
- if (!safe.fs.unlinkSync(path.join(fullPath, 'cloudron-testfile'))) {
+ if (!safe.fs.unlinkSync(path.join(fullPath, 'snapshot/cloudron-testfile'))) {
throw new BoxError(BoxError.BAD_FIELD, `Unable to remove test file as 'yellowtent' user in ${fullPath}: ${safe.error.message}. Check dir/mount permissions`);
}
- if (mounts.isManagedProvider(provider)) await mounts.removeMount({ hostPath: `${managedMountPath}-validation`, mountType: provider, mountOptions: config.mountOptions });
+ if (mounts.isManagedProvider(provider)) {
+ await mounts.removeMount({ hostPath: `${managedMountPath}-validation`, mountType: provider, mountOptions: config.mountOptions });
+ newConfig._managedMountPath = managedMountPath;
+ }
return newConfig;
}
diff --git a/src/storage/gcs.js b/src/storage/gcs.js
index 8ee49ed1d..88e0f985c 100644
--- a/src/storage/gcs.js
+++ b/src/storage/gcs.js
@@ -233,7 +233,7 @@ async function verifyConfig({ id, provider, config }) {
// attempt to upload and delete a file with new credentials
const bucket = getBucket(config);
- const testFile = bucket.file(path.join(config.prefix, 'cloudron-testfile'));
+ const testFile = bucket.file(path.join(config.prefix, 'snapshot/cloudron-testfile'));
const uploadStream = testFile.createWriteStream({ resumable: false });
@@ -257,7 +257,7 @@ async function verifyConfig({ id, provider, config }) {
const [listError] = await safe(bucket.getFiles(query));
if (listError) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to get files: ${listError.message}`);
- const [delError] = await safe(bucket.file(path.join(config.prefix, 'cloudron-testfile')).delete());
+ const [delError] = await safe(bucket.file(path.join(config.prefix, 'snapshot/cloudron-testfile')).delete());
if (delError) throw new BoxError(BoxError.EXTERNAL_ERROR, delError.message);
debug('testConfig: deleted cloudron-testfile');