* Added S3 Integration, need to add backup limit * Fix validation in s3 doctype * Added auto-deletion of old backups and backup_limit usage * Fixes for codacy PR review * Improved exception handling. * Update s3_backup_settings.pyversion-14
@@ -32,6 +32,11 @@ def get_data(): | |||||
"name": "Dropbox Settings", | "name": "Dropbox Settings", | ||||
"description": _("Dropbox backup settings"), | "description": _("Dropbox backup settings"), | ||||
}, | }, | ||||
{ | |||||
"type": "doctype", | |||||
"name": "S3 Backup Settings", | |||||
"description": _("S3 Backup Settings"), | |||||
}, | |||||
] | ] | ||||
}, | }, | ||||
{ | { | ||||
@@ -156,15 +156,19 @@ scheduler_events = { | |||||
"frappe.core.doctype.authentication_log.authentication_log.clear_authentication_logs" | "frappe.core.doctype.authentication_log.authentication_log.clear_authentication_logs" | ||||
], | ], | ||||
"daily_long": [ | "daily_long": [ | ||||
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily" | |||||
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily", | |||||
"frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_daily" | |||||
], | ], | ||||
"weekly_long": [ | "weekly_long": [ | ||||
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_weekly" | |||||
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_weekly", | |||||
"frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_weekly" | |||||
], | ], | ||||
"monthly": [ | "monthly": [ | ||||
"frappe.email.doctype.auto_email_report.auto_email_report.send_monthly" | "frappe.email.doctype.auto_email_report.auto_email_report.send_monthly" | ||||
], | |||||
"monthly_long": [ | |||||
"frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_monthly" | |||||
] | ] | ||||
} | } | ||||
get_translated_dict = { | get_translated_dict = { | ||||
@@ -0,0 +1,26 @@ | |||||
// Copyright (c) 2017, Frappe Technologies and contributors | |||||
// For license information, please see license.txt | |||||
frappe.ui.form.on('S3 Backup Settings', { | |||||
refresh: function(frm) { | |||||
frm.clear_custom_buttons(); | |||||
frm.events.take_backup(frm); | |||||
}, | |||||
take_backup: function(frm) { | |||||
if (frm.doc.access_key_id && frm.doc.secret_access_key) { | |||||
frm.add_custom_button(__("Take Backup Now"), function(){ | |||||
frm.dashboard.set_headline_alert("S3 Backup Started!"); | |||||
frappe.call({ | |||||
method: "frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_s3", | |||||
callback: function(r) { | |||||
if(!r.exc) { | |||||
frappe.msgprint(__("S3 Backup complete!")); | |||||
frm.dashboard.clear_headline(); | |||||
} | |||||
} | |||||
}); | |||||
}).addClass("btn-primary"); | |||||
} | |||||
} | |||||
}); |
@@ -0,0 +1,273 @@ | |||||
{ | |||||
"allow_copy": 0, | |||||
"allow_guest_to_view": 0, | |||||
"allow_import": 0, | |||||
"allow_rename": 0, | |||||
"beta": 0, | |||||
"creation": "2017-09-04 20:57:20.129205", | |||||
"custom": 0, | |||||
"docstatus": 0, | |||||
"doctype": "DocType", | |||||
"document_type": "", | |||||
"editable_grid": 1, | |||||
"engine": "InnoDB", | |||||
"fields": [ | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "enabled", | |||||
"fieldtype": "Check", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Enable Automatic Backup", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "notify_email", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 0, | |||||
"label": "Send Notifications To", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "frequency", | |||||
"fieldtype": "Select", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 0, | |||||
"label": "Backup Frequency", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"options": "Daily\nWeekly\nMonthly\nNone", | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "access_key_id", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 0, | |||||
"label": "Access Key ID", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "secret_access_key", | |||||
"fieldtype": "Password", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 0, | |||||
"label": "Secret Access Key", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "bucket", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Bucket", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "backup_limit", | |||||
"fieldtype": "Int", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Backup Limit", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
} | |||||
], | |||||
"has_web_view": 0, | |||||
"hide_heading": 0, | |||||
"hide_toolbar": 1, | |||||
"idx": 0, | |||||
"image_view": 0, | |||||
"in_create": 0, | |||||
"is_submittable": 0, | |||||
"issingle": 1, | |||||
"istable": 0, | |||||
"max_attachments": 0, | |||||
"modified": "2017-10-06 18:27:09.022674", | |||||
"modified_by": "Administrator", | |||||
"module": "Integrations", | |||||
"name": "S3 Backup Settings", | |||||
"name_case": "", | |||||
"owner": "Administrator", | |||||
"permissions": [ | |||||
{ | |||||
"amend": 0, | |||||
"apply_user_permissions": 0, | |||||
"cancel": 0, | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"export": 0, | |||||
"if_owner": 0, | |||||
"import": 0, | |||||
"permlevel": 0, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 0, | |||||
"role": "System Manager", | |||||
"set_user_permissions": 0, | |||||
"share": 1, | |||||
"submit": 0, | |||||
"write": 1 | |||||
} | |||||
], | |||||
"quick_entry": 1, | |||||
"read_only": 0, | |||||
"read_only_onload": 0, | |||||
"show_name_in_global_search": 0, | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"track_changes": 1, | |||||
"track_seen": 0 | |||||
} |
@@ -0,0 +1,153 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# Copyright (c) 2017, Frappe Technologies and contributors | |||||
# For license information, please see license.txt | |||||
from __future__ import unicode_literals | |||||
import os | |||||
import os.path | |||||
import frappe | |||||
import boto3 | |||||
from frappe import _ | |||||
from frappe.model.document import Document | |||||
from frappe.utils import cint, split_emails | |||||
from frappe.utils.background_jobs import enqueue | |||||
from botocore.exceptions import ClientError | |||||
class S3BackupSettings(Document): | |||||
def validate(self): | |||||
conn = boto3.client( | |||||
's3', | |||||
aws_access_key_id=self.access_key_id, | |||||
aws_secret_access_key=self.get_password('secret_access_key'), | |||||
) | |||||
bucket_lower = str(self.bucket).lower() | |||||
try: | |||||
conn.list_buckets() | |||||
except ClientError: | |||||
frappe.throw(_("Invalid Access Key ID or Secret Access Key.")) | |||||
try: | |||||
conn.create_bucket(Bucket=bucket_lower) | |||||
except ClientError: | |||||
frappe.throw(_("Unable to create bucket: {0}. Change it to a more unique name.").format(bucket_lower)) | |||||
@frappe.whitelist() | |||||
def take_backup(): | |||||
"Enqueue longjob for taking backup to s3" | |||||
enqueue("frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_s3", queue='long', timeout=1500) | |||||
frappe.msgprint(_("Queued for backup. It may take a few minutes to an hour.")) | |||||
def take_backups_daily(): | |||||
take_backups_if("Daily") | |||||
def take_backups_weekly(): | |||||
take_backups_if("Weekly") | |||||
def take_backups_monthly(): | |||||
take_backups_if("Monthly") | |||||
def take_backups_if(freq): | |||||
if cint(frappe.db.get_value("S3 Backup Settings", None, "enabled")): | |||||
if frappe.db.get_value("S3 Backup Settings", None, "frequency") == freq: | |||||
take_backups_s3() | |||||
@frappe.whitelist() | |||||
def take_backups_s3(): | |||||
try: | |||||
backup_to_s3() | |||||
send_email(True, "S3 Backup Settings") | |||||
except Exception: | |||||
error_message = frappe.get_traceback() | |||||
frappe.errprint(error_message) | |||||
send_email(False, "S3 Backup Settings", error_message) | |||||
def send_email(success, service_name, error_status=None): | |||||
if success: | |||||
subject = "Backup Upload Successful" | |||||
message = """<h3>Backup Uploaded Successfully! </h3><p>Hi there, this is just to inform you | |||||
that your backup was successfully uploaded to your Amazon S3 bucket. So relax!</p> """ | |||||
else: | |||||
subject = "[Warning] Backup Upload Failed" | |||||
message = """<h3>Backup Upload Failed! </h3><p>Oops, your automated backup to Amazon S3 failed. | |||||
</p> <p>Error message: %s</p> <p>Please contact your system manager | |||||
for more information.</p>""" % error_status | |||||
if not frappe.db: | |||||
frappe.connect() | |||||
if frappe.db.get_value("S3 Backup Settings", None, "notification_email"): | |||||
recipients = split_emails(frappe.db.get_value("S3 Backup Settings", None, "notification_email")) | |||||
frappe.sendmail(recipients=recipients, subject=subject, message=message) | |||||
def backup_to_s3(): | |||||
from frappe.utils.backups import new_backup | |||||
from frappe.utils import get_backups_path | |||||
doc = frappe.get_single("S3 Backup Settings") | |||||
bucket = doc.bucket | |||||
conn = boto3.client( | |||||
's3', | |||||
aws_access_key_id=doc.access_key_id, | |||||
aws_secret_access_key=doc.get_password('secret_access_key'), | |||||
) | |||||
backup = new_backup(ignore_files=False, backup_path_db=None, | |||||
backup_path_files=None, backup_path_private_files=None, force=True) | |||||
db_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db)) | |||||
files_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_files)) | |||||
private_files = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_private_files)) | |||||
folder = os.path.basename(db_filename)[:15] + '/' | |||||
# for adding datetime to folder name | |||||
upload_file_to_s3(db_filename, folder, conn, bucket) | |||||
upload_file_to_s3(private_files, folder, conn, bucket) | |||||
upload_file_to_s3(files_filename, folder, conn, bucket) | |||||
delete_old_backups(doc.backup_limit, bucket) | |||||
def upload_file_to_s3(filename, folder, conn, bucket): | |||||
destpath = os.path.join(folder, os.path.basename(filename)) | |||||
try: | |||||
print "Uploading file:", filename | |||||
conn.upload_file(filename, bucket, destpath) | |||||
except Exception as e: | |||||
print "Error uploading: %s" % (e) | |||||
def delete_old_backups(limit, bucket): | |||||
all_backups = list() | |||||
doc = frappe.get_single("S3 Backup Settings") | |||||
backup_limit = int(limit) | |||||
s3 = boto3.resource( | |||||
's3', | |||||
aws_access_key_id=doc.access_key_id, | |||||
aws_secret_access_key=doc.get_password('secret_access_key'), | |||||
) | |||||
bucket = s3.Bucket(bucket) | |||||
objects = bucket.meta.client.list_objects_v2(Bucket=bucket.name, Delimiter='/') | |||||
for obj in objects.get('CommonPrefixes'): | |||||
all_backups.append(obj.get('Prefix')) | |||||
oldest_backup = sorted(all_backups)[0] | |||||
if len(all_backups) > backup_limit: | |||||
print "Deleting Backup: {0}".format(oldest_backup) | |||||
for obj in bucket.objects.filter(Prefix=oldest_backup): | |||||
# delete all keys that are inside the oldest_backup | |||||
s3.Object(bucket.name, obj.key).delete() |
@@ -0,0 +1,23 @@ | |||||
/* eslint-disable */ | |||||
// rename this file from _test_[name] to test_[name] to activate | |||||
// and remove above this line | |||||
QUnit.test("test: S3 Backup Settings", function (assert) { | |||||
let done = assert.async(); | |||||
// number of asserts | |||||
assert.expect(1); | |||||
frappe.run_serially([ | |||||
// insert a new S3 Backup Settings | |||||
() => frappe.tests.make('S3 Backup Settings', [ | |||||
// values to be set | |||||
{key: 'value'} | |||||
]), | |||||
() => { | |||||
assert.equal(cur_frm.doc.key, 'value'); | |||||
}, | |||||
() => done() | |||||
]); | |||||
}); |
@@ -0,0 +1,9 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# Copyright (c) 2017, Frappe Technologies and Contributors | |||||
# See license.txt | |||||
from __future__ import unicode_literals | |||||
import unittest | |||||
class TestS3BackupSettings(unittest.TestCase): | |||||
pass |
@@ -1,3 +1,4 @@ | |||||
boto3 | |||||
chardet | chardet | ||||
cssmin | cssmin | ||||
dropbox==7.3.1 | dropbox==7.3.1 | ||||