2025-04-28 18:05:29 +02:00
< script setup >
import { ref , onMounted } from 'vue' ;
import { Button , InputGroup , SingleSelect , FormGroup , TextInput , Checkbox , PasswordInput , NumberInput } from 'pankow' ;
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' ;
import SystemModel from '../models/SystemModel.js' ;
import { mountlike , s3like } from '../utils.js' ;
const provider = defineModel ( 'provider' ) ;
const providerConfig = defineModel ( 'providerConfig' ) ;
2025-04-29 16:58:36 +02:00
const formError = defineProps ( {
formError : { } ,
importOnly : {
type : Boolean ,
default : false ,
}
} ) ;
2025-04-28 18:05:29 +02:00
const systemModel = SystemModel . create ( ) ;
const storageProviders = STORAGE _PROVIDERS . concat ( [
{ name : 'No-op (Only for testing)' , value : 'noop' }
] ) ;
const blockDevices = ref ( [ ] ) ;
const disk = ref ( '' ) ;
const gcsKeyFileName = ref ( '' ) ;
const gcsProjectId = ref ( '' ) ;
const gcsKeyContentJson = ref ( null ) ;
const gcsFileParseError = ref ( '' ) ;
function onGcsKeyChange ( event ) {
gcsFileParseError . value = '' ;
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' ) gcsFileParseError . value = 'Invalid JSON' ;
else gcsFileParseError . value = e . message ;
gcsKeyFileName . value = '' ;
gcsProjectId . value = '' ;
gcsKeyContentJson . value = null ;
}
} ;
fr . readAsText ( event . target . files [ 0 ] ) ;
gcsKeyFileName . value = event . target . files [ 0 ] . name ;
}
async function getBlockDevices ( ) {
const [ error , result ] = await systemModel . blockDevices ( ) ;
if ( error ) return console . error ( error ) ;
// amend label for UI
result . forEach ( d => {
d . label = d . path ;
// pre-select current if set
if ( d . path === providerConfig . value . mountOptionDiskPath || ( '/dev/disk/by-uuid/' + d . uuid ) === providerConfig . value . mountOptionDiskPath ) {
disk . value = d . path ;
}
} ) ;
// only offer non /, /boot or /home disks
// only offer xfs and ext4 disks
blockDevices . value = result
. filter ( d => { return d . mountpoint !== '/' && d . mountpoint !== '/home' && d . mountpoint !== '/boot' ; } )
. filter ( d => { return d . type === 'xfs' || d . type === 'ext4' ; } ) ;
}
onMounted ( async ( ) => {
await getBlockDevices ( ) ;
} ) ;
< / script >
< template >
< div >
< div class = "error-label" v-show = "formError.generic" > {{ formError.generic }} < / div >
< FormGroup >
< label for = "providerInput" > { { $t ( 'backups.configureBackupStorage.provider' ) } } < sup > < a href = "https://docs.cloudron.io/backups/#storage-providers" class = "help" target = "_blank" > < i class = "fa fa-question-circle" > < / i > < / a > < / sup > < / label >
<!-- TODO < div class = "warning-label" v-show = "config.provider !== provider" > Backups in the old storage location have to be removed manually. < / div > - - >
< SingleSelect id = "providerInput" v-model = "provider" :options="storageProviders" option-key="value" option-label="name" / >
< / FormGroup >
<!-- Noop -- >
< div class = "warning-label" v-show = "provider === 'noop'" > {{ $ t ( ' backups.configureBackupStorage.noopNote ' ) }} < / div >
< ! - - mountpoint - - >
< FormGroup v-if = "provider === 'mountpoint'" >
< label for = "mountPointInput" > { { $t ( 'backups.configureBackupStorage.mountPoint' ) } } < / label >
< TextInput id = "mountPointInput" v-model = "providerConfig.mountPoint" placeholder="/mnt/backups" required / >
< div v-html = "$t('backups.configureBackupStorage.mountPointDescription', { providerDocsLink: `https://docs.cloudron.io/backups/#${provider}` })" > < / div >
< / FormGroup >
< ! - - CIFS / NFS / SSHFS - - >
< FormGroup v-if = "provider === 'cifs' || provider === 'nfs' || provider === 'sshfs'" >
< label for = "mountOptionHostInput" > { { $t ( 'backups.configureBackupStorage.server' ) } } ( { { provider } } ) < / label >
< TextInput id = "mountOptionHostInput" v-model = "providerConfig.mountOptionHost" placeholder="Server IP or hostname" required / >
< / FormGroup >
<!-- CIFS -- >
< Checkbox v-if = "provider === 'cifs'" v-model="providerConfig.mountOptionSeal" :label="$t('backups.configureBackupStorage.cifsSealSupport')" / >
<!-- CIFS / NFS / SSHFS -- >
< FormGroup v-if = "provider === 'cifs' || provider === 'nfs' || provider === 'sshfs'" >
< label for = "mountOptionRemoteDirInput" > { { $t ( 'backups.configureBackupStorage.remoteDirectory' ) } } ( { { provider } } ) < / label >
< TextInput id = "mountOptionRemoteDirInput" v-model = "providerConfig.mountOptionRemoteDir" placeholder="/share" required / >
< / FormGroup >
<!-- CIFS -- >
< FormGroup v-if = "provider === 'cifs'" >
< label for = "mountOptionUsernameInput" > { { $t ( 'backups.configureBackupStorage.username' ) } } ( { { provider } } ) < / label >
< TextInput id = "mountOptionUsernameInput" v-model = "providerConfig.mountOptionUsername" required / >
< / FormGroup >
<!-- CIFS -- >
< FormGroup v-if = "provider === 'cifs'" >
< label for = "mountOptionPasswordInput" > { { $t ( 'backups.configureBackupStorage.password' ) } } ( { { provider } } ) < / label >
< PasswordInput id = "mountOptionPasswordInput" v-model = "providerConfig.mountOptionPassword" required / >
< / FormGroup >
<!-- EXT4 / XFS -- >
< FormGroup v-if = "provider === 'xfs' || provider === 'ext4'" >
< label for = "mountOptionDiskPathInput" > { { $t ( 'backups.configureBackupStorage.diskPath' ) } } < / label >
< TextInput id = "mountOptionDiskPathInput" v-model = "providerConfig.mountOptionDiskPath" placeholder="/dev/disk/by-uuid/uuid" required / >
< / FormGroup >
<!-- Disk -- >
< FormGroup v-if = "provider === 'disk'" >
< label class = "control-label" > { { $t ( 'backups.configureBackupStorage.diskPath' ) } } < / label >
< SingleSelect v-model = "disk" :options="providerConfig.blockDevices" option-label="label" option-key="path" required / >
< / FormGroup >
<!-- SSHFS -- >
< FormGroup v-if = "provider === 'sshfs'" >
< label for = "mountOptionPortInput" > { { $t ( 'backups.configureBackupStorage.port' ) } } < / label >
< NumberInput v-model = "providerConfig.mountOptionPort" id="mountOptionPortInput" required / >
< / FormGroup >
<!-- SSHFS -- >
< FormGroup v-if = "provider === 'sshfs'" >
< label for = "mountOptionUserInput" > { { $t ( 'backups.configureBackupStorage.user' ) } } < / label >
< TextInput id = "mountOptionUserInput" v-model = "providerConfig.mountOptionUser" required / >
< / FormGroup >
<!-- SSHFS -- >
< FormGroup v-if = "provider === 'sshfs'" >
< label for = "mountOptionPrivateKeyInput" > { { $t ( 'backups.configureBackupStorage.privateKey' ) } } < / label >
< textarea id = "mountOptionPrivateKeyInput" v-model = "providerConfig.mountOptionPrivateKey" required > < / textarea >
< / FormGroup >
<!-- Filesystem -- >
2025-04-29 16:58:36 +02:00
< FormGroup v-if = "provider === 'filesystem' && !importOnly" >
2025-04-28 18:05:29 +02:00
< label for = "backupFolderInput" > { { $t ( 'backups.configureBackupStorage.localDirectory' ) } } < / label >
< TextInput id = "backupFolderInput" v-model = "providerConfig.backupFolder" placeholder="Directory for backups" required / >
< / FormGroup >
<!-- Filesystem / SSHFS / CIFS / NFS / EXT4 / mountpoint -- >
2025-04-29 16:58:36 +02:00
< Checkbox v-if = "(provider === 'filesystem' || mountlike(provider)) && !importOnly" v-model="providerConfig.useHardlinks" :label="$t('backups.configureBackupStorage.hardlinksLabel')" />
2025-04-28 18:05:29 +02:00
< ! - - CIFS / mountpoint - - >
2025-04-29 16:58:36 +02:00
< Checkbox v-if = "(provider === 'mountpoint' || provider === 'cifs') && !importOnly" v-model="providerConfig.preserveAttributes" :label="$t('backups.configureBackupStorage.preserveAttributesLabel')" />
2025-04-28 18:05:29 +02:00
< ! - - mountpoint - - >
2025-04-29 16:58:36 +02:00
< Checkbox v-if = "provider === 'mountpoint' && !importOnly" v-model="providerConfig.chown" :label="$t('backups.configureBackupStorage.chown')" />
2025-04-28 18:05:29 +02:00
< ! - - S3 / Minio / SOS / GCS / UpCloud / B2 / R2 - - >
< FormGroup v-if = "provider === 'minio' || provider === 'upcloud-objectstorage' || provider === 'backblaze-b2' || provider === 'cloudflare-r2' || provider === 's3-v4-compat' || provider === 'idrive-e2'" >
< label for = "endpointInput" > { { $t ( 'backups.configureBackupStorage.s3Endpoint' ) } } < / label >
< TextInput id = "endpointInput" v-model = "providerConfig.endpoint" placeholder="URL" required / >
< / FormGroup >
< Checkbox v-if = "provider === 'minio' || provider === 's3-v4-compat'" v-model="providerConfig.acceptSelfSignedCerts" :label="$t('backups.configureBackupStorage.acceptSelfSignedCerts')" />
< FormGroup v-if = "s3like(provider) || provider === 'gcs'" >
< label for = "bucketInput" > { { $t ( 'backups.configureBackupStorage.bucketName' ) } } < / label >
< TextInput id = "bucketInput" v-model = "providerConfig.bucket" required / >
< / FormGroup >
2025-04-29 16:58:36 +02:00
< FormGroup v-if = "(provider !== 'filesystem' && provider !== 'noop') && !importOnly" >
2025-04-28 18:05:29 +02:00
< label for = "prefixInput" > { { $t ( 'backups.configureBackupStorage.prefix' ) } } < / label >
< TextInput id = "prefixInput" v-model = "providerConfig.prefix" placeholder="Prefix for backup file names" / >
< / FormGroup >
<!-- S3 / Minio / SOS / GCS -- >
< FormGroup v-if ="
provider === 's3' ||
provider === 'digitalocean-spaces' ||
provider === 'hetzner-objectstorage' ||
provider === 'wasabi' ||
provider === 'scaleway-objectstorage' ||
provider === 'linode-objectstorage' ||
provider === 'ovh-objectstorage' ||
provider === 'ionos-objectstorage' ||
provider === 'vultr-objectstorage' ||
provider === 'contabo-objectstorage' ||
provider === 'exoscale-sos'
"
>
<label for="regionInput">{{ $t('backups.configureBackupStorage.region') }}</label>
<SingleSelect id="regionInput" v-if="provider === 's3'" v-model="providerConfig.region" :options="REGIONS_S3" option-label="name" option-key="value" required />
<SingleSelect id="regionInput" v-if="provider === 'digitalocean-spaces'" v-model="providerConfig.endpoint" :options="REGIONS_DIGITALOCEAN" option-label="name" option-key="value" required />
<SingleSelect id="regionInput" v-if="provider === 'hetzner-objectstorage'" v-model="providerConfig.endpoint" :options="REGIONS_HETZNER" option-label="name" option-key="value" required />
<SingleSelect id="regionInput" v-if="provider === 'exoscale-sos'" v-model="providerConfig.endpoint" :options="REGIONS_EXOSCALE" option-label="name" option-key="value" required />
<SingleSelect id="regionInput" v-if="provider === 'wasabi'" v-model="providerConfig.endpoint" :options="REGIONS_WASABI" option-label="name" option-key="value" required />
<SingleSelect id="regionInput" v-if="provider === 'scaleway-objectstorage'" v-model="providerConfig.endpoint" :options="REGIONS_SCALEWAY" option-label="name" option-key="value" required />
<SingleSelect id="regionInput" v-if="provider === 'linode-objectstorage'" v-model="providerConfig.endpoint" :options="REGIONS_LINODE" option-label="name" option-key="value" required />
<SingleSelect id="regionInput" v-if="provider === 'ovh-objectstorage'" v-model="providerConfig.endpoint" :options="REGIONS_OVH" option-label="name" option-key="value" required />
<SingleSelect id="regionInput" v-if="provider === 'ionos-objectstorage'" v-model="providerConfig.endpoint" :options="REGIONS_IONOS" option-label="name" option-key="value" required />
<SingleSelect id="regionInput" v-if="provider === 'vultr-objectstorage'" v-model="providerConfig.endpoint" :options="REGIONS_VULTR" option-label="name" option-key="value" required />
<SingleSelect id="regionInput" v-if="provider === 'contabo-objectstorage'" v-model="providerConfig.endpoint" :options="REGIONS_CONTABO" option-label="name" option-key="value" required />
</FormGroup>
<FormGroup v-if="provider === 's3-v4-compat'">
<label for="s3v4CompatRegionInput">{{ $t('backups.configureBackupStorage.region') }}</label>
<TextInput id="s3v4CompatRegionInput" v-model="providerConfig.region" placeholder="Leave empty to use us-east-1 as default" />
</FormGroup>
<FormGroup v-if="s3like(provider)">
<label for="accessKeyIdInput">{{ $t('backups.configureBackupStorage.s3AccessKeyId') }}</label>
<TextInput id="accessKeyIdInput" v-model="providerConfig.accessKeyId" required />
</FormGroup>
<FormGroup v-if="s3like(provider)">
<label for="accessKeyInput">{{ $t('backups.configureBackupStorage.s3SecretAccessKey') }}</label>
<TextInput id="accessKeyInput" v-model="providerConfig.secretAccessKey" required />
</FormGroup>
<FormGroup v-if="provider === 'gcs'">
<input type="file" id="gcsKeyFileInput" style="display:none" @change="onGcsKeyChange"/>
<label for="gcsKeyInput">{{ $t('backups.configureBackupStorage.gcsServiceKey') }}{{ providerConfig.gcsProjectId ? ` - project: ${providerConfig.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="error-label" v-show="gcsFileParseError">{{ gcsFileParseError }}</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>
<SingleSelect id="formatInput" v-model="providerConfig.format" :options="BACKUP_FORMATS" option-label="name" option-key="value" required />
<!-- TODO old config format not known <div class="warning-label" v-show="format !== config.format">{{ $t('backups.configureBackupStorage.formatChangeNote') }}</div> -->
<div class="warning-label" v-show="providerConfig.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></div>
</FormGroup>
<FormGroup v-if="provider !== 'noop'">
<label for="encryptionPassswordInput">{{ $t('backups.configureBackupStorage.encryptionPassword') }} <sup><a href="https://docs.cloudron.io/backups/#encryption" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<TextInput id="encryptionPassswordInput" v-model="providerConfig.encryptionPassword" :placeholder="$t('backups.configureBackupStorage.encryptionPasswordPlaceholder')" />
<div class="error-label" v-show="providerConfig.encryptionPassword">{{ $t('backups.configureBackupStorage.encryptionDescription') }}</div>
</FormGroup>
<Checkbox v-if="providerConfig.format === 'rsync' && providerConfig.encryptionPassword" v-model="providerConfig.encryptedFilenames" :label="$t('backups.configureBackupStorage.encryptFilenames')" />
< / div >
< / template >