Browse Source

Mar 15 [UPDT] : Updated 'auto_database_backup'

pull/254/merge
AjmalCybro 1 year ago
parent
commit
d543fb3934
  1. 2
      auto_database_backup/__manifest__.py
  2. 5
      auto_database_backup/doc/RELEASE_NOTES.md
  3. 455
      auto_database_backup/models/db_backup_configure.py
  4. 21
      auto_database_backup/views/db_backup_configure_views.xml

2
auto_database_backup/__manifest__.py

@ -22,7 +22,7 @@
{ {
'name': "Automatic Database Backup To Local Server, Remote Server, Google " 'name': "Automatic Database Backup To Local Server, Remote Server, Google "
"Drive, Dropbox, Onedrive, Nextcloud and Amazon S3", "Drive, Dropbox, Onedrive, Nextcloud and Amazon S3",
'version': '15.0.4.1.2', 'version': '15.0.5.1.0',
'category': 'Discuss,Extra Tools', 'category': 'Discuss,Extra Tools',
'summary': """Generate automatic backup of databases and store to local, 'summary': """Generate automatic backup of databases and store to local,
google drive, dropbox, nextcloud, amazon S3, onedrive or google drive, dropbox, nextcloud, amazon S3, onedrive or

5
auto_database_backup/doc/RELEASE_NOTES.md

@ -24,3 +24,8 @@
#### Version 15.0.4.1.2 #### Version 15.0.4.1.2
#### ADD #### ADD
- Nextcloud and Amazon S3 integration added. Backup can be stored into Nextcloud and Amazon S3. - Nextcloud and Amazon S3 integration added. Backup can be stored into Nextcloud and Amazon S3.
#### 16.02.2024
#### Version 15.0.5.1.0
#### UPDT
- Fixed internal server error in Onedrive and Google Drive integration.

455
auto_database_backup/models/db_backup_configure.py

@ -260,6 +260,30 @@ class DbBackupConfigure(models.Model):
nextcloud_folder_key = fields.Char(string='Next Cloud Folder Id', nextcloud_folder_key = fields.Char(string='Next Cloud Folder Id',
help="Field used to store the unique " help="Field used to store the unique "
"identifier for a Nextcloud folder.") "identifier for a Nextcloud folder.")
gdrive_backup_error_test = fields.Boolean(string="Google Drive Error Test")
onedrive_backup_error_test = fields.Boolean(string="OneDrive Error Test")
@api.onchange('backup_destination')
def _onchange_backup_destination(self):
self.write({
"gdrive_backup_error_test": False,
"onedrive_backup_error_test": False
})
@api.onchange('gdrive_client_key', 'gdrive_client_secret',
'google_drive_folder', 'onedrive_client_key',
'onedrive_client_secret', 'onedrive_folder_key')
def _onchange_gdrive_backup_error_test(self):
if self.backup_destination == 'google_drive':
if self.gdrive_backup_error_test:
self.write({
"gdrive_backup_error_test": False
})
if self.backup_destination == 'onedrive':
if self.onedrive_backup_error_test:
self.write({
"onedrive_backup_error_test": False
})
def action_nextcloud(self): def action_nextcloud(self):
"""If it has next_cloud_password, domain, and next_cloud_user_name """If it has next_cloud_password, domain, and next_cloud_user_name
@ -453,11 +477,13 @@ class DbBackupConfigure(models.Model):
'gdrive_token_validity': fields.Datetime.now() + timedelta( 'gdrive_token_validity': fields.Datetime.now() + timedelta(
seconds=expires_in) if expires_in else False, seconds=expires_in) if expires_in else False,
}) })
except requests.HTTPError: if self.gdrive_backup_error_test:
error_msg = _( self.write({
"Something went wrong during your token generation. Maybe " 'gdrive_backup_error_test': False
"your Authorization Code is invalid") })
raise UserError(error_msg) except Exception:
if not self.gdrive_backup_error_test:
self.write({"gdrive_backup_error_test": True})
@api.depends('onedrive_access_token', 'onedrive_refresh_token') @api.depends('onedrive_access_token', 'onedrive_refresh_token')
def _compute_is_onedrive_token_generated(self): def _compute_is_onedrive_token_generated(self):
@ -566,10 +592,13 @@ class DbBackupConfigure(models.Model):
'onedrive_token_validity': fields.Datetime.now() + timedelta 'onedrive_token_validity': fields.Datetime.now() + timedelta
(seconds=expires_in) if expires_in else False, (seconds=expires_in) if expires_in else False,
}) })
except requests.HTTPError as error: if self.onedrive_backup_error_test:
_logger.exception("Bad microsoft onedrive request : %s !", self.write({
error.response.content) 'onedrive_backup_error_test': False
raise error })
except Exception:
if not self.onedrive_backup_error_test:
self.write({"onedrive_backup_error_test": True})
def get_dropbox_auth_url(self): def get_dropbox_auth_url(self):
"""Return dropbox authorization url""" """Return dropbox authorization url"""
@ -580,11 +609,15 @@ class DbBackupConfigure(models.Model):
def set_dropbox_refresh_token(self, auth_code): def set_dropbox_refresh_token(self, auth_code):
"""Generate and set the dropbox refresh token from authorization code""" """Generate and set the dropbox refresh token from authorization code"""
dbx_auth = dropbox.oauth.DropboxOAuth2FlowNoRedirect( try:
self.dropbox_client_key, self.dropbox_client_secret, dbx_auth = dropbox.oauth.DropboxOAuth2FlowNoRedirect(
token_access_type='offline') self.dropbox_client_key, self.dropbox_client_secret,
outh_result = dbx_auth.finish(auth_code) token_access_type='offline')
self.dropbox_refresh_token = outh_result.refresh_token outh_result = dbx_auth.finish(auth_code)
self.dropbox_refresh_token = outh_result.refresh_token
except Exception:
raise ValidationError(
'Please Enter Valid Authentication Code')
@api.constrains('db_name') @api.constrains('db_name')
def _check_db_credentials(self): def _check_db_credentials(self):
@ -764,129 +797,153 @@ class DbBackupConfigure(models.Model):
client.close() client.close()
# Google Drive backup # Google Drive backup
elif rec.backup_destination == 'google_drive': elif rec.backup_destination == 'google_drive':
if (rec.gdrive_token_validity is not False and
rec.gdrive_token_validity <= fields.Datetime.now()):
rec.generate_gdrive_refresh_token()
temp = tempfile.NamedTemporaryFile(
suffix='.%s' % rec.backup_format)
with open(temp.name, "wb+") as tmp:
odoo.service.db.dump_db(rec.db_name, tmp, rec.backup_format)
try: try:
headers = { if (rec.gdrive_token_validity is not False and
"Authorization": "Bearer %s" % rec.gdrive_access_token} rec.gdrive_token_validity <= fields.Datetime.now()):
para = { rec.generate_gdrive_refresh_token()
"name": backup_filename, temp = tempfile.NamedTemporaryFile(
"parents": [rec.google_drive_folder_key], suffix='.%s' % rec.backup_format)
} with open(temp.name, "wb+") as tmp:
files = { odoo.service.db.dump_db(rec.db_name, tmp, rec.backup_format)
'data': ('metadata', json.dumps(para), try:
'application/json; charset=UTF-8'), headers = {
'file': open(temp.name, "rb") "Authorization": "Bearer %s" % rec.gdrive_access_token}
} para = {
requests.post( "name": backup_filename,
"https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart", "parents": [rec.google_drive_folder_key],
headers=headers, }
files=files files = {
) 'data': ('metadata', json.dumps(para),
if rec.auto_remove: 'application/json; charset=UTF-8'),
query = "parents = '%s'" % rec.google_drive_folder_key 'file': open(temp.name, "rb")
files_req = requests.get( }
"https://www.googleapis.com/drive/v3/files?q=%s" % query, requests.post(
headers=headers) "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart",
files = files_req.json()['files'] headers=headers,
for file in files: files=files
file_date_req = requests.get( )
"https://www.googleapis.com/drive/v3/files/%s?fields=createdTime" % if rec.auto_remove:
file['id'], headers=headers) query = "parents = '%s'" % rec.google_drive_folder_key
create_time = file_date_req.json()['createdTime'][ files_req = requests.get(
:19].replace('T', ' ') "https://www.googleapis.com/drive/v3/files?q=%s" % query,
diff_days = ( headers=headers)
fields.datetime.now() files = files_req.json()['files']
- fields.datetime.strptime( for file in files:
create_time, '%Y-%m-%d %H:%M:%S')).days file_date_req = requests.get(
if diff_days >= rec.days_to_remove: "https://www.googleapis.com/drive/v3/files/%s?fields=createdTime" %
requests.delete(
"https://www.googleapis.com/drive/v3/files/%s" %
file['id'], headers=headers) file['id'], headers=headers)
if rec.notify_user: create_time = file_date_req.json()['createdTime'][
mail_template_success.send_mail(rec.id, force_send=True) :19].replace('T', ' ')
except Exception as e: diff_days = (
rec.generated_exception = e fields.datetime.now()
_logger.info('Google Drive Exception: %s', e) - fields.datetime.strptime(
create_time, '%Y-%m-%d %H:%M:%S')).days
if diff_days >= rec.days_to_remove:
requests.delete(
"https://www.googleapis.com/drive/v3/files/%s" %
file['id'], headers=headers)
if rec.notify_user:
mail_template_success.send_mail(rec.id, force_send=True)
except Exception as e:
rec.generated_exception = e
_logger.info('Google Drive Exception: %s', e)
if rec.notify_user:
mail_template_failed.send_mail(rec.id, force_send=True)
except Exception:
if rec.notify_user: if rec.notify_user:
mail_template_failed.send_mail(rec.id, force_send=True) mail_template_failed.send_mail(rec.id, force_send=True)
raise ValidationError(
'Please check the credentials before activation')
else:
raise ValidationError('Please check connection')
# Dropbox backup # Dropbox backup
elif rec.backup_destination == 'dropbox': elif rec.backup_destination == 'dropbox':
temp = tempfile.NamedTemporaryFile(
suffix='.%s' % rec.backup_format)
with open(temp.name, "wb+") as tmp:
odoo.service.db.dump_db(rec.db_name, tmp, rec.backup_format)
try: try:
drop_connection = dropbox.Dropbox( temp = tempfile.NamedTemporaryFile(
app_key=rec.dropbox_client_key, suffix='.%s' % rec.backup_format)
app_secret=rec.dropbox_client_secret, with open(temp.name, "wb+") as tmp:
oauth2_refresh_token=rec.dropbox_refresh_token) odoo.service.db.dump_db(rec.db_name, tmp, rec.backup_format)
dropbox_destination = rec.dropbox_folder + '/' + backup_filename try:
drop_connection.files_upload(temp.read(), drop_connection = dropbox.Dropbox(
dropbox_destination) app_key=rec.dropbox_client_key,
if rec.auto_remove: app_secret=rec.dropbox_client_secret,
files = drop_connection.files_list_folder( oauth2_refresh_token=rec.dropbox_refresh_token)
rec.dropbox_folder) dropbox_destination = rec.dropbox_folder + '/' + backup_filename
file_entries = files.entries drop_connection.files_upload(temp.read(),
expired_files = list(filter( dropbox_destination)
lambda fl: (fields.datetime.now() - if rec.auto_remove:
fl.client_modified).days >= files = drop_connection.files_list_folder(
rec.days_to_remove, rec.dropbox_folder)
file_entries)) file_entries = files.entries
for file in expired_files: expired_files = list(filter(
drop_connection.files_delete_v2(file.path_display) lambda fl: (fields.datetime.now() -
if rec.notify_user: fl.client_modified).days >=
mail_template_success.send_mail(rec.id, force_send=True) rec.days_to_remove,
except Exception as error: file_entries))
rec.generated_exception = error for file in expired_files:
_logger.info('Dropbox Exception: %s', error) drop_connection.files_delete_v2(file.path_display)
if rec.notify_user:
mail_template_success.send_mail(rec.id, force_send=True)
except Exception as error:
rec.generated_exception = error
_logger.info('Dropbox Exception: %s', error)
if rec.notify_user:
mail_template_failed.send_mail(rec.id, force_send=True)
except Exception:
if rec.notify_user: if rec.notify_user:
mail_template_failed.send_mail(rec.id, force_send=True) mail_template_failed.send_mail(rec.id, force_send=True)
raise ValidationError(
'Please check the credentials before activation')
else:
raise ValidationError('Please check connection')
# Onedrive Backup # Onedrive Backup
elif rec.backup_destination == 'onedrive': elif rec.backup_destination == 'onedrive':
if (rec.onedrive_token_validity is not False and
rec.onedrive_token_validity <= fields.Datetime.now()):
rec.generate_onedrive_refresh_token()
temp = tempfile.NamedTemporaryFile(
suffix='.%s' % rec.backup_format)
with open(temp.name, "wb+") as tmp:
odoo.service.db.dump_db(rec.db_name, tmp, rec.backup_format)
headers = {
'Authorization': 'Bearer %s' % rec.onedrive_access_token,
'Content-Type': 'application/json'}
upload_session_url = MICROSOFT_GRAPH_END_POINT + "/v1.0/me/drive/items/%s:/%s:/createUploadSession" % (
rec.onedrive_folder_key, backup_filename)
try: try:
upload_session = requests.post(upload_session_url, if (rec.onedrive_token_validity is not False and
headers=headers) rec.onedrive_token_validity <= fields.Datetime.now()):
upload_url = upload_session.json().get('uploadUrl') rec.generate_onedrive_refresh_token()
requests.put(upload_url, data=temp.read()) temp = tempfile.NamedTemporaryFile(
if rec.auto_remove: suffix='.%s' % rec.backup_format)
list_url = MICROSOFT_GRAPH_END_POINT + "/v1.0/me/drive/items/%s/children" % rec.onedrive_folder_key with open(temp.name, "wb+") as tmp:
response = requests.get(list_url, headers=headers) odoo.service.db.dump_db(rec.db_name, tmp, rec.backup_format)
files = response.json().get('value') headers = {
for file in files: 'Authorization': 'Bearer %s' % rec.onedrive_access_token,
create_time = file['createdDateTime'][:19].replace( 'Content-Type': 'application/json'}
'T', ' ') upload_session_url = MICROSOFT_GRAPH_END_POINT + "/v1.0/me/drive/items/%s:/%s:/createUploadSession" % (
diff_days = ( rec.onedrive_folder_key, backup_filename)
fields.datetime.now() - fields.datetime.strptime( try:
create_time, '%Y-%m-%d %H:%M:%S')).days upload_session = requests.post(upload_session_url,
if diff_days >= rec.days_to_remove: headers=headers)
delete_url = MICROSOFT_GRAPH_END_POINT + "/v1.0/me/drive/items/%s" % \ upload_url = upload_session.json().get('uploadUrl')
file['id'] requests.put(upload_url, data=temp.read())
requests.delete(delete_url, headers=headers) if rec.auto_remove:
if rec.notify_user: list_url = MICROSOFT_GRAPH_END_POINT + "/v1.0/me/drive/items/%s/children" % rec.onedrive_folder_key
mail_template_success.send_mail(rec.id, force_send=True) response = requests.get(list_url, headers=headers)
except Exception as error: files = response.json().get('value')
rec.generated_exception = error for file in files:
_logger.info('Onedrive Exception: %s', error) create_time = file['createdDateTime'][:19].replace(
'T', ' ')
diff_days = (
fields.datetime.now() - fields.datetime.strptime(
create_time, '%Y-%m-%d %H:%M:%S')).days
if diff_days >= rec.days_to_remove:
delete_url = MICROSOFT_GRAPH_END_POINT + "/v1.0/me/drive/items/%s" % \
file['id']
requests.delete(delete_url, headers=headers)
if rec.notify_user:
mail_template_success.send_mail(rec.id, force_send=True)
except Exception as error:
rec.generated_exception = error
_logger.info('Onedrive Exception: %s', error)
if rec.notify_user:
mail_template_failed.send_mail(rec.id, force_send=True)
except Exception:
if rec.notify_user: if rec.notify_user:
mail_template_failed.send_mail(rec.id, force_send=True) mail_template_failed.send_mail(rec.id, force_send=True)
raise ValidationError(
'Please check the credentials before activation')
else:
raise ValidationError('Please check connection')
# amazon S3 backup # amazon S3 backup
elif rec.backup_destination == 'amazon_s3': elif rec.backup_destination == 'amazon_s3':
if rec.aws_access_key and rec.aws_secret_access_key: if rec.aws_access_key and rec.aws_secret_access_key:
@ -965,77 +1022,85 @@ class DbBackupConfigure(models.Model):
force_send=True) force_send=True)
# nextcloud backup # nextcloud backup
elif rec.backup_destination == 'next_cloud': elif rec.backup_destination == 'next_cloud':
if rec.domain and rec.next_cloud_password and \ try:
rec.next_cloud_user_name: if rec.domain and rec.next_cloud_password and \
try: rec.next_cloud_user_name:
# Connect to NextCloud using the provided username try:
# and password # Connect to NextCloud using the provided username
ncx = NextCloud(rec.domain, # and password
auth=HTTPBasicAuth( ncx = NextCloud(rec.domain,
rec.next_cloud_user_name, auth=HTTPBasicAuth(
rec.next_cloud_password)) rec.next_cloud_user_name,
# Connect to NextCloud again to perform additional rec.next_cloud_password))
# operations # Connect to NextCloud again to perform additional
nc = nextcloud_client.Client(rec.domain) # operations
nc.login(rec.next_cloud_user_name, nc = nextcloud_client.Client(rec.domain)
rec.next_cloud_password) nc.login(rec.next_cloud_user_name,
# Get the folder name from the NextCloud folder ID rec.next_cloud_password)
folder_name = rec.nextcloud_folder_key # Get the folder name from the NextCloud folder ID
# If auto_remove is enabled, remove backup files folder_name = rec.nextcloud_folder_key
# older than specified days # If auto_remove is enabled, remove backup files
if rec.auto_remove: # older than specified days
folder_path = "/" + folder_name if rec.auto_remove:
for item in nc.list(folder_path): folder_path = "/" + folder_name
backup_file_name = item.path.split("/")[-1] for item in nc.list(folder_path):
backup_date_str = backup_file_name.split("_")[ backup_file_name = item.path.split("/")[-1]
2] backup_date_str = backup_file_name.split("_")[
backup_date = fields.datetime.strptime( 2]
backup_date_str, '%Y-%m-%d').date() backup_date = fields.datetime.strptime(
if (fields.date.today() - backup_date).days \ backup_date_str, '%Y-%m-%d').date()
>= rec.days_to_remove: if (fields.date.today() - backup_date).days \
nc.delete(item.path) >= rec.days_to_remove:
# If notify_user is enabled, send a success email nc.delete(item.path)
# notification # If notify_user is enabled, send a success email
if rec.notify_user:
mail_template_success.send_mail(rec.id,
force_send=True)
except Exception as error:
rec.generated_exception = error
_logger.info('NextCloud Exception: %s', error)
if rec.notify_user:
# If an exception occurs, send a failed email
# notification # notification
mail_template_failed.send_mail(rec.id, if rec.notify_user:
force_send=True) mail_template_success.send_mail(rec.id,
# Get the list of folders in the root directory of NextCloud force_send=True)
data = ncx.list_folders('/').__dict__ except Exception as error:
folders = [ rec.generated_exception = error
[file_name['href'].split('/')[-2], file_name['file_id']] _logger.info('NextCloud Exception: %s', error)
for file_name in data['data'] if if rec.notify_user:
file_name['href'].endswith('/')] # If an exception occurs, send a failed email
# If the folder name is not found in the list of folders, # notification
# create the folder mail_template_failed.send_mail(rec.id,
if folder_name.replace('/', '') not in [file[0] for file in force_send=True)
folders]: # Get the list of folders in the root directory of NextCloud
nc.mkdir(folder_name) data = ncx.list_folders('/').__dict__
# Dump the database to a temporary file folders = [
temp = tempfile.NamedTemporaryFile( [file_name['href'].split('/')[-2], file_name['file_id']]
suffix='.%s' % rec.backup_format) for file_name in data['data'] if
with open(temp.name, "wb+") as tmp: file_name['href'].endswith('/')]
odoo.service.db.dump_db(rec.db_name, tmp, # If the folder name is not found in the list of folders,
rec.backup_format) # create the folder
backup_file_path = temp.name if folder_name.replace('/', '') not in [file[0] for file in
remote_file_path = f"/{folder_name}/{rec.db_name}_" \ folders]:
f"{backup_time}.{rec.backup_format}" nc.mkdir(folder_name)
nc.put_file(remote_file_path, backup_file_path) # Dump the database to a temporary file
temp = tempfile.NamedTemporaryFile(
suffix='.%s' % rec.backup_format)
with open(temp.name, "wb+") as tmp:
odoo.service.db.dump_db(rec.db_name, tmp,
rec.backup_format)
backup_file_path = temp.name
remote_file_path = f"/{folder_name}/{rec.db_name}_" \
f"{backup_time}.{rec.backup_format}"
nc.put_file(remote_file_path, backup_file_path)
else:
# Dump the database to a temporary file
temp = tempfile.NamedTemporaryFile(
suffix='.%s' % rec.backup_format)
with open(temp.name, "wb+") as tmp:
odoo.service.db.dump_db(rec.db_name, tmp,
rec.backup_format)
backup_file_path = temp.name
remote_file_path = f"/{folder_name}/{rec.db_name}_" \
f"{backup_time}.{rec.backup_format}"
nc.put_file(remote_file_path, backup_file_path)
except Exception:
if rec.notify_user:
mail_template_failed.send_mail(rec.id, force_send=True)
raise ValidationError(
'Please check the credentials before activation')
else: else:
# Dump the database to a temporary file raise ValidationError('Please check connection')
temp = tempfile.NamedTemporaryFile(
suffix='.%s' % rec.backup_format)
with open(temp.name, "wb+") as tmp:
odoo.service.db.dump_db(rec.db_name, tmp,
rec.backup_format)
backup_file_path = temp.name
remote_file_path = f"/{folder_name}/{rec.db_name}_" \
f"{backup_time}.{rec.backup_format}"
nc.put_file(remote_file_path, backup_file_path)

21
auto_database_backup/views/db_backup_configure_views.xml

@ -20,6 +20,8 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<form> <form>
<sheet> <sheet>
<field name="gdrive_backup_error_test" invisible="1"/>
<field name="onedrive_backup_error_test" invisible="1"/>
<div class="oe_title"> <div class="oe_title">
<h1> <h1>
<field name="name" placeholder="Name..."/> <field name="name" placeholder="Name..."/>
@ -128,7 +130,21 @@
</button> </button>
</div> </div>
</div> </div>
<div> <div class="alert alert-danger" role="alert" style="margin-bottom:0px;width: 229%;" attrs="{'invisible': [('gdrive_backup_error_test', '=', False)]}">
Something went wrong during your token generation. Maybe your Authorization Code is invalid
</div>
<div attrs="{'invisible': [('backup_destination', '!=', 'onedrive')]}">
<div attrs="{'invisible': ['|', ('backup_destination', '!=', 'onedrive'), ('is_onedrive_token_generated', '=', False)]}">
<i class="text-success fa fa-check"/>
Refresh token set
</div>
<div attrs="{'invisible': ['|', ('backup_destination', '!=', 'onedrive'), ('is_onedrive_token_generated', '=', True)]}">
<i class="fa fa-exclamation-triangle text-warning"/>
No refresh token set
</div>
</div>
<div attrs="{'invisible': [('backup_destination', '!=', 'onedrive')]}">
<div attrs="{'invisible': ['|', ('backup_destination', '!=', 'onedrive'), ('is_onedrive_token_generated', '=', True)]}"> <div attrs="{'invisible': ['|', ('backup_destination', '!=', 'onedrive'), ('is_onedrive_token_generated', '=', True)]}">
<button class="btn btn-link" <button class="btn btn-link"
name="action_get_onedrive_auth_code" name="action_get_onedrive_auth_code"
@ -146,6 +162,9 @@
</button> </button>
</div> </div>
</div> </div>
<div class="alert alert-danger" role="alert" style="margin-bottom:0px;width: 91%%;" attrs="{'invisible': [('onedrive_backup_error_test', '=', False)]}">
Bad microsoft onedrive request. Maybe your Authorization Code is invalid
</div>
<field name="dropbox_refresh_token" invisible="1"/> <field name="dropbox_refresh_token" invisible="1"/>
<field name="is_dropbox_token_generated" invisible="1"/> <field name="is_dropbox_token_generated" invisible="1"/>
<field name="dropbox_folder" attrs="{'invisible': [('backup_destination', '!=', 'dropbox')], 'required': [('backup_destination', '=', 'dropbox')]}"/> <field name="dropbox_folder" attrs="{'invisible': [('backup_destination', '!=', 'dropbox')], 'required': [('backup_destination', '=', 'dropbox')]}"/>

Loading…
Cancel
Save