2025-05-06 11:45:49 +02:00
< script setup >
import { ref , onMounted , useTemplateRef } from 'vue' ;
2025-08-16 19:26:19 +02:00
import { Notification , ProgressBar , Button , SingleSelect , FormGroup , PasswordInput , TextInput , Checkbox } from '@cloudron/pankow' ;
2025-07-22 16:48:51 +02:00
import { copyToClipboard } from '@cloudron/pankow/utils' ;
2025-05-06 14:43:53 +02:00
import { REGIONS _CONTABO , REGIONS _VULTR , REGIONS _IONOS , REGIONS _OVH , REGIONS _LINODE , REGIONS _SCALEWAY , REGIONS _WASABI } from '../constants.js' ;
import { redirectIfNeeded , mountlike , s3like } from '../utils.js' ;
2025-05-06 11:45:49 +02:00
import ProvisionModel from '../models/ProvisionModel.js' ;
import BackupProviderForm from '../components/BackupProviderForm.vue' ;
const provisionModel = ProvisionModel . create ( ) ;
const ipProviders = [
{ name : 'Disabled' , value : 'noop' } ,
{ name : 'Public IP' , value : 'generic' } ,
{ name : 'Static IP Address' , value : 'fixed' } ,
{ name : 'Network Interface' , value : 'network-interface' }
] ;
const formError = ref ( { } ) ;
const busy = ref ( false ) ;
const ready = ref ( false ) ;
const waitingForRestore = ref ( false ) ;
const progressMessage = ref ( '' ) ;
const taskMinutesActive = ref ( 0 ) ;
const provider = ref ( '' ) ;
const providerConfig = ref ( { } ) ;
const remotePath = ref ( '' ) ;
2025-08-05 14:13:39 +02:00
const format = ref ( '' ) ;
2025-08-16 19:26:19 +02:00
const encrypted = ref ( false ) ;
const encryptionPasswordHint = ref ( '' ) ;
2025-08-05 14:13:39 +02:00
const encryptionPassword = ref ( '' ) ;
const encryptedFilenames = ref ( false ) ;
2025-05-06 11:45:49 +02:00
const showAdvanced = ref ( false ) ;
const ipv4Provider = ref ( 'generic' ) ;
const ipv4Address = ref ( '' ) ;
const ipv4Interface = ref ( '' ) ;
const ipv6Provider = ref ( 'generic' ) ;
const ipv6Address = ref ( '' ) ;
const ipv6Interface = ref ( '' ) ;
const skipDnsSetup = ref ( false ) ;
const form = useTemplateRef ( 'form' ) ;
const isFormValid = ref ( false ) ;
function checkValidity ( ) {
if ( ! provider . value ) return false ;
isFormValid . value = form . value . checkValidity ( ) ;
}
async function waitForRestore ( ) {
waitingForRestore . value = true ;
formError . value = { } ;
const [ error , result ] = await provisionModel . status ( ) ;
if ( error ) {
setTimeout ( waitForRestore , 5000 ) ;
return console . error ( error ) ;
}
if ( ! result . restore . active ) {
if ( ! result . adminFqdn || result . restore . errorMessage ) { // restore reset or errored. start over
formError . value . dnsWait = result . restore . errorMessage ;
waitingForRestore . value = false ;
2025-07-22 16:24:46 +02:00
busy . value = false ;
2025-05-06 11:45:49 +02:00
} else { // proceed to dashboard
window . location . href = 'https://' + result . adminFqdn ;
}
return ;
}
progressMessage . value = result . restore . message ;
// TODO do we have the time here?
taskMinutesActive . value = ( new Date ( ) - new Date ( result . restore . startTime ) ) / 60000 ;
setTimeout ( waitForRestore , 5000 ) ;
}
async function onSubmit ( ) {
if ( ! isFormValid . value ) return ;
busy . value = true ;
formError . value = { } ;
if ( remotePath . value . indexOf ( '/' ) === - 1 ) {
2025-05-06 14:43:53 +02:00
error . value . generic = 'Backup id must include the directory path' ;
error . value . remotePath = true ;
busy . value = false ;
return ;
2025-05-06 11:45:49 +02:00
}
if ( remotePath . value . indexOf ( 'box' ) === - 1 ) {
2025-05-06 14:43:53 +02:00
error . value . generic = 'Backup id must contain "box"' ;
error . value . remotePath = true ;
busy . value = false ;
return ;
2025-05-06 11:45:49 +02:00
}
const version = remotePath . value . match ( /_v(\d+.\d+.\d+)/ ) ;
if ( ! version ) {
2025-05-06 14:43:53 +02:00
formError . value . generic = 'Backup id is missing version information' ;
formError . value . remotePath = true ;
busy . value = false ;
return ;
2025-05-06 11:45:49 +02:00
}
2025-08-05 14:13:39 +02:00
const config = { } ; // filled below
2025-05-06 11:45:49 +02:00
const data = {
2025-05-06 14:43:53 +02:00
backupConfig : {
provider : provider . value ,
2025-08-05 14:13:39 +02:00
config , // filled below
format : format . value ,
2025-05-06 14:43:53 +02:00
} ,
2025-08-05 14:13:39 +02:00
remotePath : remotePath . value ,
2025-05-06 14:43:53 +02:00
version : version ? version [ 1 ] : '' ,
ipv4Config : {
provider : ipv4Provider . value ,
ip : ipv4Address . value ,
ifname : ipv4Interface . value ,
} ,
ipv6Config : {
provider : ipv6Provider . value ,
ip : ipv6Address . value ,
ifname : ipv6Interface . value ,
} ,
skipDnsSetup : skipDnsSetup . value ,
2025-05-06 11:45:49 +02:00
} ;
2025-08-16 19:26:19 +02:00
if ( encrypted . value ) {
data . backupConfig . encryptionPassword = encryptionPassword . value ;
data . backupConfig . encryptedFilenames = encryptedFilenames . value ;
2025-05-06 14:43:53 +02:00
}
if ( s3like ( provider . value ) ) {
2025-08-05 14:13:39 +02:00
config . endpoint = providerConfig . value . endpoint ;
config . prefix = providerConfig . value . prefix ;
config . bucket = providerConfig . value . bucket ;
config . accessKeyId = providerConfig . value . accessKeyId ;
config . secretAccessKey = providerConfig . value . secretAccessKey ;
2025-05-06 14:43:53 +02:00
if ( provider . value === 's3' ) {
2025-08-05 14:13:39 +02:00
config . region = providerConfig . value . region || undefined ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'minio' || provider . value === 's3-v4-compat' ) {
2025-08-05 14:13:39 +02:00
config . region = providerConfig . value . region || 'us-east-1' ;
config . acceptSelfSignedCerts = providerConfig . value . acceptSelfSignedCerts ;
config . s3ForcePathStyle = true ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'exoscale-sos' ) {
2025-08-05 14:13:39 +02:00
config . region = 'us-east-1' ;
config . signatureVersion = 'v4' ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'wasabi' ) {
2025-08-05 14:13:39 +02:00
config . region = REGIONS _WASABI . find ( function ( x ) { return x . value === config . endpoint ; } ) . region ;
config . signatureVersion = 'v4' ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'scaleway-objectstorage' ) {
2025-08-05 14:13:39 +02:00
config . region = REGIONS _SCALEWAY . find ( function ( x ) { return x . value === config . endpoint ; } ) . region ;
config . signatureVersion = 'v4' ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'linode-objectstorage' ) {
2025-08-05 14:13:39 +02:00
config . region = REGIONS _LINODE . find ( function ( x ) { return x . value === config . endpoint ; } ) . region ;
config . signatureVersion = 'v4' ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'ovh-objectstorage' ) {
2025-08-05 14:13:39 +02:00
config . region = REGIONS _OVH . find ( function ( x ) { return x . value === config . endpoint ; } ) . region ;
config . signatureVersion = 'v4' ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'ionos-objectstorage' ) {
2025-08-05 14:13:39 +02:00
config . region = REGIONS _IONOS . find ( function ( x ) { return x . value === config . endpoint ; } ) . region ;
config . signatureVersion = 'v4' ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'vultr-objectstorage' ) {
2025-08-05 14:13:39 +02:00
config . region = REGIONS _VULTR . find ( function ( x ) { return x . value === config . endpoint ; } ) . region ;
config . signatureVersion = 'v4' ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'contabo-objectstorage' ) {
2025-08-05 14:13:39 +02:00
config . region = REGIONS _CONTABO . find ( function ( x ) { return x . value === config . endpoint ; } ) . region ;
config . signatureVersion = 'v4' ;
config . s3ForcePathStyle = true ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'upcloud-objectstorage' ) { // the UI sets region and endpoint
2025-08-05 14:13:39 +02:00
const m = /^.*\.(.*)\.upcloudobjects.com$/ . exec ( config . endpoint ) ;
config . region = m ? m [ 1 ] : 'us-east-1' ; // let it fail in validation phase if m is not valid
config . signatureVersion = 'v4' ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'digitalocean-spaces' ) {
2025-08-05 14:13:39 +02:00
config . region = 'us-east-1' ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'hetzner-objectstorage' ) {
2025-08-05 14:13:39 +02:00
config . region = 'us-east-1' ;
config . signatureVersion = 'v4' ;
2025-05-06 14:43:53 +02:00
}
} else if ( mountlike ( provider . value ) ) {
2025-08-05 14:13:39 +02:00
config . prefix = providerConfig . value . prefix ;
config . noHardlinks = ! providerConfig . value . useHardlinks ;
config . mountOptions = { } ;
2025-05-06 14:43:53 +02:00
if ( provider . value === 'cifs' || provider . value === 'sshfs' || provider . value === 'nfs' ) {
2025-08-05 14:13:39 +02:00
config . mountOptions . host = providerConfig . value . mountOptionHost ;
config . mountOptions . remoteDir = providerConfig . value . mountOptionRemoteDir ;
2025-05-06 14:43:53 +02:00
if ( provider . value === 'cifs' ) {
2025-08-05 14:13:39 +02:00
config . mountOptions . username = providerConfig . value . mountOptionUsername ;
config . mountOptions . password = providerConfig . value . mountOptionPassword ;
config . mountOptions . seal = ! ! providerConfig . value . mountOptionSeal ;
config . preserveAttributes = ! ! providerConfig . value . preserveAttributes ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'sshfs' ) {
2025-08-05 14:13:39 +02:00
config . mountOptions . user = providerConfig . value . mountOptionUser ;
config . mountOptions . port = parseInt ( providerConfig . value . mountOptionPort ) ;
config . mountOptions . privateKey = providerConfig . value . mountOptionPrivateKey ;
config . preserveAttributes = true ;
2025-05-06 14:43:53 +02:00
}
} else if ( provider . value === 'ext4' || provider . value === 'xfs' || provider . value === 'disk' ) {
2025-08-05 14:13:39 +02:00
config . mountOptions . diskPath = providerConfig . value . mountOptionDiskPath ;
config . preserveAttributes = true ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'mountpoint' ) {
2025-08-05 14:13:39 +02:00
config . mountPoint = providerConfig . value . mountPoint ;
config . chown = ! ! providerConfig . value . chown ;
config . preserveAttributes = ! ! providerConfig . value . preserveAttributes ;
2025-05-06 14:43:53 +02:00
}
} else if ( provider . value === 'filesystem' ) {
2025-08-05 14:13:39 +02:00
config . backupDir = providerConfig . value . backupDir ;
config . noHardlinks = ! providerConfig . value . useHardlinks ;
config . preserveAttributes = true ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'gcs' ) {
2025-08-05 14:13:39 +02:00
config . bucket = providerConfig . value . bucket ;
config . prefix = providerConfig . value . prefix ;
config . projectId = providerConfig . value . projectId ;
config . credentials = providerConfig . value . credentials ;
2025-05-06 14:43:53 +02:00
}
2025-05-06 11:45:49 +02:00
const [ error ] = await provisionModel . restore ( data ) ;
if ( error ) {
if ( error . status === 424 ) {
formError . value . generic = error . body . message ;
if ( error . body . message . indexOf ( 'AWS Access Key Id' ) !== - 1 ) {
formError . value . accessKeyId = true ;
} else if ( error . body . message . indexOf ( 'not match the signature' ) !== - 1 ) {
formError . value . secretAccessKey = true ;
} else if ( error . body . message . toLowerCase ( ) === 'access denied' ) {
formError . value . bucket = true ;
} else if ( error . body . message . indexOf ( 'ECONNREFUSED' ) !== - 1 ) {
formError . value . generic = 'Unknown region' ;
formError . value . region = true ;
} else if ( error . body . message . toLowerCase ( ) === 'wrong region' ) {
formError . value . generic = 'Wrong S3 Region' ;
formError . value . region = true ;
} else {
formError . value . generic = error . body . message ;
}
} else {
formError . value . generic = error . body ? error . body . message : 'Internal error' ;
console . error ( error ) ;
}
busy . value = false ;
return ;
}
waitForRestore ( ) ;
}
function onBackupConfigChanged ( event ) {
const reader = new FileReader ( ) ;
reader . onload = function ( result ) {
if ( ! result . target || ! result . target . result ) return console . error ( 'Unable to read backup config' ) ;
2025-08-05 14:13:39 +02:00
let data ;
2025-05-06 11:45:49 +02:00
try {
2025-08-05 14:13:39 +02:00
data = JSON . parse ( result . target . result ) ; // 'provider', 'config', 'limits', 'format', 'remotePath', 'encrypted', 'encryptedFilenames'
if ( data . provider === 'filesystem' ) { // this allows a user to upload a backup to server and import easily with an absolute path
data . remotePath = ` ${ data . config . backupDir } / ${ data . remotePath } ` ;
2025-05-06 11:45:49 +02:00
}
} catch ( e ) {
console . error ( 'Unable to parse backup config' , e ) ;
return ;
}
2025-08-05 14:13:39 +02:00
provider . value = data . provider ;
remotePath . value = data . remotePath ;
providerConfig . value = data . config ;
format . value = data . format ;
2025-08-16 19:26:19 +02:00
encrypted . value = ! ! data . encrypted ;
encryptionPasswordHint . value = data . encryptionPasswordHint || '' ;
encryptionPassword . value = '' ;
2025-08-05 14:13:39 +02:00
encryptedFilenames . value = data . encryptedFilenames ;
2025-05-06 11:45:49 +02:00
} ;
reader . readAsText ( event . target . files [ 0 ] ) ;
}
const backupConfigFileInput = useTemplateRef ( 'backupConfigFileInput' ) ;
function onUploadBackupConfig ( ) {
backupConfigFileInput . value . click ( ) ;
}
2025-07-22 18:03:40 +02:00
function onCopyToClipboard ( value ) {
copyToClipboard ( value ) ;
window . pankow . notify ( 'copied to clipboard' ) ;
2025-07-22 16:48:51 +02:00
}
2025-05-06 11:45:49 +02:00
onMounted ( async ( ) => {
2025-05-06 14:43:53 +02:00
let [ error , result ] = await provisionModel . status ( ) ;
2025-05-06 11:45:49 +02:00
if ( error ) return console . error ( error ) ;
2025-08-16 19:28:33 +02:00
if ( redirectIfNeeded ( result , 'restore' ) ) return ; // redirected to some other view...
2025-05-06 11:45:49 +02:00
2025-08-16 19:28:33 +02:00
[ error , result ] = await provisionModel . detectIp ( ) ;
if ( error ) return console . error ( error ) ;
2025-05-06 14:43:53 +02:00
ipv4Provider . value = result . ipv4 ? 'generic' : 'noop' ;
ipv6Provider . value = result . ipv6 ? 'generic' : 'noop' ;
2025-05-06 11:45:49 +02:00
ready . value = true ;
} ) ;
< / script >
< template >
< div class = "container" v-if = "ready" >
2025-07-22 16:48:51 +02:00
< Notification / >
2025-05-06 11:45:49 +02:00
< Transition name = "slide-fade" mode = "out-in" >
2025-07-22 16:48:51 +02:00
< div class = "view" v-if = "waitingForRestore" style="text-align: center;" >
2025-05-06 11:45:49 +02:00
< div >
2025-07-22 16:48:51 +02:00
< h3 > Please wait while Cloudron is restoring < / h3 >
< ProgressBar mode = "indeterminate" :show-label = "false" :slim = "true" / >
< h3 > { { progressMessage } } < / h3 >
< / div >
< div style = "position: absolute; bottom: 10px; text-align: center; width: 100%; font-size: 12px;" >
< span v-show = "taskMinutesActive >= 4">If restore appears stuck, it can be restarted by running <code @click="onCopyToClipboard('systemctl restart box')" > systemctl restart box < / code > and reloading this page. < / span >
2025-05-06 11:45:49 +02:00
< br / >
< br / >
2025-07-22 16:48:51 +02:00
You can follow the logs on the server at < code @click ="onCopyToClipboard('/home/yellowtent/platformdata/logs/box.log')" > / home / yellowtent / platformdata / logs / box.log < / code >
2025-05-06 11:45:49 +02:00
< / div >
< / div >
2025-07-22 16:48:51 +02:00
< div class = "view" v-else style = "max-width: 500px;" >
2025-09-09 15:55:01 +02:00
< h1 style = "text-align: center" > Cloudron Restore < / h1 >
2025-09-09 16:14:20 +02:00
< p > Provide the backup information to restore from , or
2025-07-22 18:03:40 +02:00
< input type = "file" ref = "backupConfigFileInput" @change ="onBackupConfigChanged" accept = "application/json, text/json" style = "display:none" / >
2025-09-09 16:14:20 +02:00
< button type = "button" style = "background: none; border: none; color: #007bff; cursor: pointer; text-decoration: underline; padding: 0;" @click ="onUploadBackupConfig()" >
upload a Backup Config
< / button >
< / p >
2025-05-06 11:45:49 +02:00
2025-05-06 11:54:02 +02:00
< div class = "error-label" v-if = "formError.generic" > {{ formError.generic }} < / div >
2025-05-06 11:45:49 +02:00
< form ref = "form" @submit.prevent ="onSubmit()" @input ="checkValidity()" >
< fieldset :disabled = "busy" >
< input type = "submit" style = "display: none;" / >
<!-- remotePath contains the prefix as well -- >
< FormGroup >
< label for = "inputRemotePath" > { { $t ( 'app.importBackupDialog.remotePath' ) } } < sup > < a href = "https://docs.cloudron.io/backups/#import-app-backup" class = "help" target = "_blank" > < i class = "fa fa-question-circle" > < / i > < / a > < / sup > < / label >
< TextInput id = "inputRemotePath" v-model = "remotePath" required / >
< / FormGroup >
2025-08-05 14:13:39 +02:00
< BackupProviderForm ref = "form"
v - model : provider = "provider"
v - model : provider - config = "providerConfig"
v - model : format = "format"
: form - error = "formError"
: provisioning = "true"
: import - only = "true" / >
2025-05-06 11:45:49 +02:00
< div v-show = "showAdvanced" >
2025-08-19 10:49:27 +02:00
< Checkbox style = "padding-top: 12px" v-model = "encrypted" :label="$t('backups.configureBackupStorage.usesEncryption')" />
2025-08-16 19:26:19 +02:00
< FormGroup v-if = "encrypted" >
< label for = "encryptionPassswordInput" > { { $t ( 'backups.configureBackupStorage.encryptionPassword' ) } } < / label >
< PasswordInput id = "encryptionPassswordInput" v-model = "encryptionPassword" :placeholder="$t('backups.configureBackupStorage.encryptionPasswordPlaceholder')" required / >
< div class = "warning-label" v-if = "encryptionPasswordHint" > {{ $ t ( ' backups.configureBackupStorage.encryptionHint ' ) }} : {{ encryptionPasswordHint }} < / div >
< / FormGroup >
< Checkbox v-if = "encrypted && format === 'rsync'" v-model="encryptedFilenames" :label="$t('backups.configureBackupStorage.encryptFilenames')" />
2025-05-06 11:45:49 +02:00
<!-- IPv4 provider -- >
< FormGroup >
< label class = "control-label" > IPv4 Configuration < sup > < a href = "https://docs.cloudron.io/networking/#ip-configuration" class = "help" target = "_blank" > < i class = "fa fa-question-circle" > < / i > < / a > < / sup > < / label >
< SingleSelect v-model = "ipv4Provider" :options="ipProviders" option-key="value" option-label="name" / >
< / FormGroup >
<!-- IPv4 Fixed -- >
< FormGroup v-if = "ipv4Provider === 'fixed'" >
< label for = "ipv4AddressInput" > IPv4 Address < / label >
< TextInput id = "ipv4AddressInput" v-model = "ipv4Address" required / >
< / FormGroup >
<!-- IPv4 Network Interface -- >
< FormGroup v-if = "ipv4Provider === 'network-interface'" >
< label for = "ipv4InterfaceInput" > IPv4 Interface Name < / label >
< TextInput id = "ipv4InterfaceInput" v-model = "ipv4Interface" required / >
< / FormGroup >
<!-- IPv6 provider -- >
< FormGroup >
< label class = "control-label" > IPv6 Configuration < sup > < a href = "https://docs.cloudron.io/networking/#ip-configuration" class = "help" target = "_blank" > < i class = "fa fa-question-circle" > < / i > < / a > < / sup > < / label >
< SingleSelect v-model = "ipv6Provider" :options="ipProviders" option-key="value" option-label="name" / >
< / FormGroup >
<!-- IPv6 Fixed -- >
< FormGroup v-if = "ipv6Provider === 'fixed'" >
< label for = "ipv6AddressInput" > IPv6 Address < / label >
< TextInput id = "ipv6AddressInput" v-model = "ipv6Address" required / >
< / FormGroup >
<!-- IPv6 Network Interface -- >
< FormGroup v-if = "ipv6Provider === 'network-interface'" >
< label for = "ipv6InterfaceInpt" > IPv6 Interface Name < / label >
< TextInput id = "ipv6InterfaceInpt" v-model = "ipv6Interface" required / >
< / FormGroup >
< / div >
< div class = "actionable" @click ="showAdvanced = false" v-if = "showAdvanced" > Hide Advanced settings < / div >
< div class = "actionable" @click ="showAdvanced = true" v-else > Advanced settings... < / div >
< div style = "margin-top: 18px" >
< Checkbox v-model = "skipDnsSetup" label="Dry run" />
2025-09-09 16:14:20 +02:00
< small class = "helper-text" >
2025-05-06 11:45:49 +02:00
When enabled , apps are restored but the DNS records are not updated to point to this server .
To access the dashboard , this browser 's host must have an entry in <code>/etc/hosts</code> for the dashboard domain to this server' s IP .
See the < a href = "https://docs.cloudron.io/backups/#dry-run" target = "_blank" > docs < / a > for more information .
< / small >
< / div >
< / fieldset >
< / form >
2025-09-09 15:55:01 +02:00
< div class = "actions" >
2025-07-22 18:03:40 +02:00
< Button @click ="onSubmit()" : disabled = "busy || !isFormValid" :loading = "busy" > Restore < / Button >
2025-09-09 15:55:01 +02:00
< a class = "setup" href = "/setup.html" > Looking to setup ? < / a >
2025-07-22 18:03:40 +02:00
< / div >
2025-09-09 15:55:01 +02:00
2025-05-06 11:45:49 +02:00
< / div >
< / Transition >
< / div >
< / template >
2025-09-09 15:55:01 +02:00
< style scoped >
. actions {
margin - top : 1.5 em ;
display : flex ;
flex - direction : column ;
align - items : center ;
}
. actions . setup {
margin - top : 1 em ;
font - size : 0.9 em ;
}
< / style >