Support gcs account key upload for backups

This commit is contained in:
Johannes Zellner
2025-03-27 16:49:35 +01:00
parent 10df195630
commit d6eb675b89

View File

@@ -1,7 +1,7 @@
<script setup>
import { ref, useTemplateRef, onMounted, computed, watch } from 'vue';
import { Dialog, Dropdown, FormGroup, TextInput, Checkbox, PasswordInput, NumberInput } from 'pankow';
import { Button, InputGroup, Dialog, Dropdown, FormGroup, TextInput, Checkbox, PasswordInput, NumberInput } from 'pankow';
import { prettyBinarySize } from 'pankow/utils';
// TODO REGIONS_UPCLOUD is not used??
import { BACKUP_FORMATS, STORAGE_PROVIDERS, REGIONS_CONTABO, REGIONS_VULTR, REGIONS_UPCLOUD, REGIONS_IONOS, REGIONS_OVH, REGIONS_LINODE, REGIONS_SCALEWAY, REGIONS_EXOSCALE, REGIONS_DIGITALOCEAN, REGIONS_HETZNER, REGIONS_WASABI, REGIONS_S3 } from '../constants.js';
@@ -28,7 +28,6 @@ const formError = ref({});
const blockDevices = ref([]);
// TODO gcs provider needs some special treatment
const advancedVisible = ref(false);
const busy = ref(false);
const provider = ref('');
@@ -62,6 +61,38 @@ const prefix = ref('');
const region = ref('');
const accessKeyId = ref('');
const secretAccessKey = ref('');
const gcsKeyFileName = ref('');
const gcsProjectId = ref('');
const gcsKeyContentJson = ref(null);
function onGcsKeyChange(event) {
delete formError.value.gcs;
const fr = new FileReader();
fr.onload = () => {
// validate input file
try {
const keyJson = JSON.parse(fr.result);
if (!keyJson.project_id) throw new Error('project_id field missing in JSON key file');
if (!keyJson.client_email) throw new Error('client_email field missing in JSON key file');
if (!keyJson.private_key) throw new Error('private_key field missing in JSON key file');
gcsProjectId.value = keyJson.project_id;
gcsKeyContentJson.value = keyJson;
} catch (e) {
if (e.name === 'SyntaxError') formError.value.gcs = 'Invalid JSON';
else formError.value.gcs = e.message;
gcsKeyFileName.value = '';
gcsProjectId.value = '';
gcsKeyContentJson.value = null;
}
};
fr.readAsText(event.target.files[0]);
gcsKeyFileName.value = event.target.files[0].name;
}
async function onSubmit() {
if (!isValid.value) return;
@@ -161,6 +192,17 @@ async function onSubmit() {
data.backupFolder = backupFolder.value;
data.noHardlinks = !useHardlinks.value;
data.preserveAttributes = true;
} else if (data.provider === 'gcs') {
// TODO we should probably allow to change the config without reuploading a new .json
if (gcsKeyContentJson.value) return;
data.bucket = bucket.value;
data.prefix = prefix.value;
data.projectId = gcsKeyContentJson.value.project_id;
data.credentials = {
client_email: gcsKeyContentJson.value.client_email,
private_key: gcsKeyContentJson.value.private_key
};
}
const limits = {
@@ -219,7 +261,6 @@ const isValid = computed(() => {
});
watch(provider, (newProvider) => {
console.log('provider change', newProvider)
if (newProvider === 'scaleway-objectstorage') {
// scaleway only supports 1000 parts per object (https://www.scaleway.com/en/docs/s3-multipart-upload/)
if (parseInt(uploadPartSize.value) < 100 * 1024 * 1024) uploadPartSize.value = 100 * 1024 * 1024;
@@ -273,6 +314,9 @@ defineExpose({
region.value = result.region;
accessKeyId.value = result.accessKeyId;
secretAccessKey.value = result.secretAccessKey;
gcsProjectId.value = result.projectId || '';
gcsKeyFileName.value = '';
gcsKeyContentJson.value = null;
dialog.value.open();
}
@@ -446,25 +490,23 @@ defineExpose({
<TextInput id="accessKeyInput" v-model="secretAccessKey" required />
</FormGroup>
<!-- <div class="form-group" ng-class="{ 'has-error': configureBackup.error.gcsKeyInput }" ng-show="configureBackup.provider === 'gcs'">
<label class="control-label" for="gcsKeyInput">{{ 'backups.configureBackupStorage.gcsServiceKey' | tr }}</label>
<div class="input-group">
<input type="file" id="gcsKeyFileInput" style="display:none"/>
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="configureBackup.gcsKey.keyFileName" id="gcsKeyInput" name="cert" onclick="getElementById('gcsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="configureBackup.busy" ng-required="configureBackup.provider === 'gcs'">
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('gcsKeyFileInput').click();"></i>
</span>
</div>
</div> -->
<FormGroup v-if="provider === 'gcs'">
<input type="file" id="gcsKeyFileInput" style="display:none" @change="onGcsKeyChange"/>
<label for="gcsKeyInput">{{ $t('backups.configureBackupStorage.gcsServiceKey') }}{{ gcsProjectId ? ` - project: ${gcsProjectId}` : '' }}</label>
<InputGroup>
<TextInput readonly required style="flex-grow: 1" v-model="gcsKeyFileName" placeholder="Service Account Key" onclick="document.getElementById('gcsKeyFileInput').click();"/>
<Button tool icon="fa fa-upload" onclick="document.getElementById('gcsKeyFileInput').click();"/>
</InputGroup>
<div class="has-error" v-show="formError.gcs">{{ formError.gcs }}</div>
</FormGroup>
<FormGroup v-if="provider !== 'noop'">
<label for="formatInput">{{ $t('backups.configureBackupStorage.format') }} <sup><a href="https://docs.cloudron.io/backups/#backup-formats" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<p class="small text-info" v-show="format !== config.format">{{ $t('backups.configureBackupStorage.formatChangeNote') }}</p>
<p class="small text-info" v-show="format === 'rsync' && (s3like(provider) || provider === 'gcs')">{{ $t('backups.configureBackupStorage.s3LikeNote') }} <sup><a href="https://docs.cloudron.io/backups/#amazon-s3" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
<select class="form-control" id="formatInput" v-model="format">
<option v-for="f in BACKUP_FORMATS" :value="f.value" :key="f.value">{{ f.name }}</option>
</select>
<p class="small text-info" v-show="format !== config.format">{{ $t('backups.configureBackupStorage.formatChangeNote') }}</p>
<p class="small text-info" v-show="format === 'rsync' && (s3like(provider) || provider === 'gcs')">{{ $t('backups.configureBackupStorage.s3LikeNote') }} <sup><a href="https://docs.cloudron.io/backups/#amazon-s3" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
</FormGroup>
<FormGroup v-if="provider !== 'noop'">