2018-06-07 14:22:48 +02:00
'use strict' ;
angular . module ( 'Application' ) . controller ( 'BackupsController' , [ '$scope' , '$location' , '$rootScope' , '$timeout' , 'Client' , 'AppStore' , function ( $scope , $location , $rootScope , $timeout , Client , AppStore ) {
Client . onReady ( function ( ) {
if ( ! Client . hasScope ( 'settings' ) ) $location . path ( '/' ) ;
if ( Client . getConfig ( ) . provider === 'caas' ) $location . path ( '/' ) ;
} ) ;
$scope . config = Client . getConfig ( ) ;
$scope . backupConfig = { } ;
$scope . lastBackup = null ;
$scope . backups = [ ] ;
// List is from http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
$scope . s3Regions = [
{ name : 'Asia Pacific (Mumbai)' , value : 'ap-south-1' } ,
{ name : 'Asia Pacific (Seoul)' , value : 'ap-northeast-2' } ,
{ name : 'Asia Pacific (Singapore)' , value : 'ap-southeast-1' } ,
{ name : 'Asia Pacific (Sydney)' , value : 'ap-southeast-2' } ,
{ name : 'Asia Pacific (Tokyo)' , value : 'ap-northeast-1' } ,
{ name : 'Canada (Central)' , value : 'ca-central-1' } ,
{ name : 'EU (Frankfurt)' , value : 'eu-central-1' } ,
{ name : 'EU (Ireland)' , value : 'eu-west-1' } ,
{ name : 'EU (London)' , value : 'eu-west-2' } ,
{ name : 'South America (São Paulo)' , value : 'sa-east-1' } ,
{ name : 'US East (N. Virginia)' , value : 'us-east-1' } ,
{ name : 'US East (Ohio)' , value : 'us-east-2' } ,
{ name : 'US West (N. California)' , value : 'us-west-1' } ,
{ name : 'US West (Oregon)' , value : 'us-west-2' } ,
] ;
$scope . doSpacesRegions = [
{ name : 'AMS3' , value : 'https://ams3.digitaloceanspaces.com' } ,
{ name : 'NYC3' , value : 'https://nyc3.digitaloceanspaces.com' } ,
{ name : 'SGP1' , value : 'https://sgp1.digitaloceanspaces.com' }
] ;
$scope . storageProvider = [
{ name : 'Amazon S3' , value : 's3' } ,
{ name : 'DigitalOcean Spaces' , value : 'digitalocean-spaces' } ,
{ name : 'Exoscale SOS' , value : 'exoscale-sos' } ,
{ name : 'Filesystem' , value : 'filesystem' } ,
{ name : 'Google Cloud Storage' , value : 'gcs' } ,
{ name : 'Minio' , value : 'minio' } ,
{ name : 'No-op (Only for testing)' , value : 'noop' } ,
{ name : 'S3 API Compatible (v4)' , value : 's3-v4-compat' } ,
] ;
$scope . retentionTimes = [
{ name : '2 days' , value : 2 * 24 * 60 * 60 } ,
{ name : '1 week' , value : 7 * 24 * 60 * 60 } , // the default
{ name : '1 month' , value : 30 * 24 * 60 * 60 } ,
{ name : 'Forever' , value : - 1 }
] ;
$scope . formats = [
{ name : 'Tarball (zipped)' , value : 'tgz' } ,
{ name : 'rsync' , value : 'rsync' }
] ;
$scope . prettyProviderName = function ( provider ) {
switch ( provider ) {
case 'caas' : return 'Managed Cloudron' ;
default : return provider ;
}
} ;
$scope . createBackup = {
busy : false ,
percent : 0 ,
message : '' ,
errorMessage : '' ,
result : '' ,
updateStatus : function ( ) {
Client . progress ( function ( error , data ) {
if ( error ) return window . setTimeout ( $scope . createBackup . updateStatus , 250 ) ;
// check if we are done
if ( ! data . backup || data . backup . percent >= 100 ) {
if ( data . backup && data . backup . message ) console . error ( 'Backup message: ' + data . backup . message ) ; // backup error message
$scope . createBackup . busy = false ;
$scope . createBackup . message = '' ;
$scope . createBackup . detail = '' ;
$scope . createBackup . percent = 100 ; // indicates that 'result' is valid
$scope . createBackup . result = data . backup ? data . backup . message : null ;
return fetchBackups ( ) ;
}
$scope . createBackup . busy = true ;
$scope . createBackup . percent = data . backup . percent ;
$scope . createBackup . message = data . backup . message ;
$scope . createBackup . detail = data . backup . detail ;
window . setTimeout ( $scope . createBackup . updateStatus , 500 ) ;
} ) ;
} ,
doCreateBackup : function ( ) {
$scope . createBackup . busy = true ;
$scope . createBackup . percent = 0 ;
$scope . createBackup . message = '' ;
$scope . createBackup . detail = '' ;
$scope . createBackup . result = '' ;
$scope . createBackup . errorMessage = '' ;
Client . backup ( function ( error ) {
if ( error ) {
if ( error . statusCode === 409 && error . message . indexOf ( 'full_backup' ) !== - 1 ) {
$scope . createBackup . errorMessage = 'Backup already in progress. Please retry later.' ;
} else if ( error . statusCode === 409 ) {
$scope . createBackup . errorMessage = 'App task is currently in progress. Please retry later.' ;
} else {
console . error ( error ) ;
$scope . createBackup . errorMessage = error . message ;
}
$scope . createBackup . busy = false ;
$ ( '#createBackupFailedModal' ) . modal ( 'show' ) ;
return ;
}
$scope . createBackup . updateStatus ( ) ;
} ) ;
}
} ;
$scope . s3like = function ( provider ) {
return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat' || provider === 'exoscale-sos' || provider === 'digitalocean-spaces' ;
} ;
$scope . configureBackup = {
busy : false ,
error : { } ,
provider : '' ,
bucket : '' ,
prefix : '' ,
accessKeyId : '' ,
secretAccessKey : '' ,
gcsKey : { keyFileName : '' , content : '' } ,
region : '' ,
endpoint : '' ,
backupFolder : '' ,
retentionSecs : 7 * 24 * 60 * 60 ,
acceptSelfSignedCerts : false ,
useHardlinks : true ,
2018-06-07 11:19:43 -07:00
externalDisk : false ,
2018-06-07 14:22:48 +02:00
format : 'tgz' ,
clearForm : function ( ) {
$scope . configureBackup . bucket = '' ;
$scope . configureBackup . prefix = '' ;
$scope . configureBackup . accessKeyId = '' ;
$scope . configureBackup . secretAccessKey = '' ;
$scope . configureBackup . gcsKey . keyFileName = '' ;
$scope . configureBackup . gcsKey . content = '' ;
$scope . configureBackup . endpoint = '' ;
$scope . configureBackup . region = '' ;
$scope . configureBackup . backupFolder = '' ;
$scope . configureBackup . retentionSecs = 7 * 24 * 60 * 60 ;
$scope . configureBackup . format = 'tgz' ;
$scope . configureBackup . acceptSelfSignedCerts = false ;
$scope . configureBackup . useHardlinks = true ;
2018-06-07 11:19:43 -07:00
$scope . configureBackup . externalDisk = false ;
2018-06-07 14:22:48 +02:00
} ,
show : function ( ) {
$scope . configureBackup . error = { } ;
$scope . configureBackup . busy = false ;
$scope . configureBackup . provider = $scope . backupConfig . provider ;
$scope . configureBackup . bucket = $scope . backupConfig . bucket ;
$scope . configureBackup . prefix = $scope . backupConfig . prefix ;
$scope . configureBackup . region = $scope . backupConfig . region ;
$scope . configureBackup . accessKeyId = $scope . backupConfig . accessKeyId ;
$scope . configureBackup . secretAccessKey = $scope . backupConfig . secretAccessKey ;
if ( $scope . backupConfig . provider === 'gcs' ) {
$scope . configureBackup . gcsKey . keyFileName = $scope . backupConfig . credentials . client _email ;
$scope . configureBackup . gcsKey . content = JSON . stringify ( {
project _id : $scope . backupConfig . projectId ,
client _email : $scope . backupConfig . credentials . client _email ,
private _key : $scope . backupConfig . credentials . private _key ,
} ) ;
}
$scope . configureBackup . endpoint = $scope . backupConfig . endpoint ;
$scope . configureBackup . key = $scope . backupConfig . key ;
$scope . configureBackup . backupFolder = $scope . backupConfig . backupFolder ;
$scope . configureBackup . retentionSecs = $scope . backupConfig . retentionSecs ;
$scope . configureBackup . format = $scope . backupConfig . format ;
$scope . configureBackup . acceptSelfSignedCerts = ! ! $scope . backupConfig . acceptSelfSignedCerts ;
$scope . configureBackup . useHardlinks = ! $scope . backupConfig . noHardlinks ;
2018-06-07 11:19:43 -07:00
$scope . configureBackup . externalDisk = ! ! $scope . backupConfig . externalDisk ;
2018-06-07 14:22:48 +02:00
$ ( '#configureBackupModal' ) . modal ( 'show' ) ;
} ,
submit : function ( ) {
$scope . configureBackup . error = { } ;
$scope . configureBackup . busy = true ;
var backupConfig = {
provider : $scope . configureBackup . provider ,
key : $scope . configureBackup . key ,
retentionSecs : $scope . configureBackup . retentionSecs ,
format : $scope . configureBackup . format
} ;
// only set provider specific fields, this will clear them in the db
if ( $scope . s3like ( backupConfig . provider ) ) {
backupConfig . bucket = $scope . configureBackup . bucket ;
backupConfig . prefix = $scope . configureBackup . prefix ;
backupConfig . accessKeyId = $scope . configureBackup . accessKeyId ;
backupConfig . secretAccessKey = $scope . configureBackup . secretAccessKey ;
if ( $scope . configureBackup . endpoint ) backupConfig . endpoint = $scope . configureBackup . endpoint ;
if ( backupConfig . provider === 's3' ) {
if ( $scope . configureBackup . region ) backupConfig . region = $scope . configureBackup . region ;
} else if ( backupConfig . provider === 'minio' || backupConfig . provider === 's3-v4-compat' ) {
backupConfig . region = 'us-east-1' ;
backupConfig . acceptSelfSignedCerts = $scope . configureBackup . acceptSelfSignedCerts ;
} else if ( backupConfig . provider === 'exoscale-sos' ) {
backupConfig . endpoint = 'https://sos-ch-dk-2.exo.io' ;
backupConfig . region = 'us-east-1' ;
backupConfig . signatureVersion = 'v4' ;
} else if ( backupConfig . provider === 'digitalocean-spaces' ) {
backupConfig . region = 'us-east-1' ;
}
} else if ( backupConfig . provider === 'gcs' ) {
backupConfig . bucket = $scope . configureBackup . bucket ;
backupConfig . prefix = $scope . configureBackup . prefix ;
try {
var serviceAccountKey = JSON . parse ( $scope . configureBackup . gcsKey . content ) ;
backupConfig . projectId = serviceAccountKey . project _id ;
backupConfig . credentials = {
client _email : serviceAccountKey . client _email ,
private _key : serviceAccountKey . private _key
} ;
if ( ! backupConfig . projectId || ! backupConfig . credentials || ! backupConfig . credentials . client _email || ! backupConfig . credentials . private _key ) {
throw 'fields_missing' ;
}
} catch ( e ) {
$scope . configureBackup . error . generic = 'Cannot parse Google Service Account Key: ' + e . message ;
$scope . configureBackup . error . gcsKeyInput = true ;
$scope . configureBackup . busy = false ;
return ;
}
} else if ( backupConfig . provider === 'filesystem' ) {
backupConfig . backupFolder = $scope . configureBackup . backupFolder ;
backupConfig . noHardlinks = ! $scope . configureBackup . useHardlinks ;
2018-06-07 11:19:43 -07:00
backupConfig . externalDisk = $scope . configureBackup . externalDisk ;
2018-06-07 14:22:48 +02:00
}
Client . setBackupConfig ( backupConfig , function ( error ) {
$scope . configureBackup . busy = false ;
if ( error ) {
if ( error . statusCode === 402 ) {
$scope . configureBackup . error . generic = error . message ;
if ( error . message . indexOf ( 'AWS Access Key Id' ) !== - 1 ) {
$scope . configureBackup . error . accessKeyId = true ;
$scope . configureBackup . accessKeyId = '' ;
$scope . configureBackupForm . accessKeyId . $setPristine ( ) ;
$ ( '#inputConfigureBackupAccessKeyId' ) . focus ( ) ;
} else if ( error . message . indexOf ( 'not match the signature' ) !== - 1 ) {
$scope . configureBackup . error . secretAccessKey = true ;
$scope . configureBackup . secretAccessKey = '' ;
$scope . configureBackupForm . secretAccessKey . $setPristine ( ) ;
$ ( '#inputConfigureBackupSecretAccessKey' ) . focus ( ) ;
} else if ( error . message . toLowerCase ( ) === 'access denied' ) {
$scope . configureBackup . error . bucket = true ;
$scope . configureBackup . bucket = '' ;
$scope . configureBackupForm . bucket . $setPristine ( ) ;
$ ( '#inputConfigureBackupBucket' ) . focus ( ) ;
} else if ( error . message . indexOf ( 'ECONNREFUSED' ) !== - 1 ) {
$scope . configureBackup . error . generic = 'Unknown region' ;
$scope . configureBackup . error . region = true ;
$scope . configureBackupForm . region . $setPristine ( ) ;
$ ( '#inputConfigureBackupDORegion' ) . focus ( ) ;
} else if ( error . message . toLowerCase ( ) === 'wrong region' ) {
$scope . configureBackup . error . generic = 'Wrong S3 Region' ;
$scope . configureBackup . error . region = true ;
$scope . configureBackupForm . region . $setPristine ( ) ;
$ ( '#inputConfigureBackupS3Region' ) . focus ( ) ;
} else {
$ ( '#inputConfigureBackupBucket' ) . focus ( ) ;
}
} else if ( error . statusCode === 400 ) {
$scope . configureBackup . error . generic = error . message ;
if ( $scope . configureBackup . provider === 'filesystem' ) {
$scope . configureBackup . error . backupFolder = true ;
}
} else {
console . error ( 'Unable to change provider.' , error ) ;
}
return ;
}
// $scope.configureBackup.reset();
$ ( '#configureBackupModal' ) . modal ( 'hide' ) ;
// now refresh the ui
Client . refreshConfig ( ) ;
getBackupConfig ( ) ;
} ) ;
}
} ;
function fetchBackups ( ) {
Client . getBackups ( function ( error , backups ) {
if ( error ) return console . error ( error ) ;
$scope . backups = backups ;
if ( $scope . backups . length > 0 ) {
$scope . lastBackup = backups [ 0 ] ;
} else {
$scope . lastBackup = null ;
}
} ) ;
}
function getBackupConfig ( ) {
Client . getBackupConfig ( function ( error , backupConfig ) {
if ( error ) return console . error ( error ) ;
$scope . backupConfig = backupConfig ;
2018-06-07 11:19:43 -07:00
Client . clearNotifications ( ) ;
2018-06-07 14:22:48 +02:00
// Check if a proper storage backend is configured. TODO: this check fails if /var/backups is actually external
2018-06-07 11:19:43 -07:00
if ( backupConfig . provider === 'filesystem' && ! backupConfig . externalDisk ) {
2018-06-07 14:22:48 +02:00
var actionScope = $scope . $new ( true ) ;
2018-06-07 11:19:43 -07:00
actionScope . action = '/#/backups' ;
2018-06-07 14:22:48 +02:00
2018-06-07 11:19:43 -07:00
Client . notify ( 'Backup Configuration' ,
'Cloudron backups are currently on the same disk as the Cloudron server instance. This is dangerous and can lead to complete data loss if the disk fails.' +
'Please setup an external backup storage (or use server snapshots) and confirm this in backup configuration' ,
true /* persistent */ , 'error' , actionScope ) ;
2018-06-07 14:22:48 +02:00
}
} ) ;
}
Client . onReady ( function ( ) {
fetchBackups ( ) ;
getBackupConfig ( ) ;
// show backup status
$scope . createBackup . updateStatus ( ) ;
} ) ;
function readFileLocally ( obj , file , fileName ) {
return function ( event ) {
$scope . $apply ( function ( ) {
obj [ file ] = null ;
obj [ fileName ] = event . target . files [ 0 ] . name ;
var reader = new FileReader ( ) ;
reader . onload = function ( result ) {
if ( ! result . target || ! result . target . result ) return console . error ( 'Unable to read local file' ) ;
obj [ file ] = result . target . result ;
} ;
reader . readAsText ( event . target . files [ 0 ] ) ;
} ) ;
} ;
}
document . getElementById ( 'gcsKeyFileInput' ) . onchange = readFileLocally ( $scope . configureBackup . gcsKey , 'content' , 'keyFileName' ) ;
// setup all the dialog focus handling
[ 'configureBackupModal' ] . forEach ( function ( id ) {
$ ( '#' + id ) . on ( 'shown.bs.modal' , function ( ) {
$ ( this ) . find ( "[autofocus]:first" ) . focus ( ) ;
} ) ;
} ) ;
$ ( '.modal-backdrop' ) . remove ( ) ;
} ] ) ;